@dreb/coding-agent 2.66.3 → 2.68.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 +4 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +12 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +27 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +16 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/daily-cost-tracker.d.ts +22 -6
- package/dist/core/daily-cost-tracker.d.ts.map +1 -1
- package/dist/core/daily-cost-tracker.js +117 -47
- package/dist/core/daily-cost-tracker.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +10 -2
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +17 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +2 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +32 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +234 -108
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-session-cost-tracker.d.ts +22 -0
- package/dist/core/subagent-session-cost-tracker.d.ts.map +1 -0
- package/dist/core/subagent-session-cost-tracker.js +94 -0
- package/dist/core/subagent-session-cost-tracker.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +14 -8
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +145 -71
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +16 -7
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +5 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -2
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +3 -2
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -2
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +34 -9
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +2 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/dashboard.md +27 -15
- package/docs/rpc.md +9 -5
- package/docs/session.md +16 -7
- package/docs/settings.md +17 -5
- package/docs/skills.md +1 -1
- package/package.json +1 -1
- package/skills/model-routing-guide/SKILL.md +10 -5
|
@@ -8,27 +8,43 @@ export declare function filenameTimestampToDate(fileTimestamp: string): Date | n
|
|
|
8
8
|
* Check if two dates fall on the same local calendar day.
|
|
9
9
|
*/
|
|
10
10
|
export declare function isSameLocalDay(date: Date, today: Date): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Sum `usage.cost.total` from all assistant messages in a JSONL session file.
|
|
13
|
+
* Works identically for both main sessions and subagent sessions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sumCostFromFile(filePath: string): Promise<number>;
|
|
16
|
+
/** Breakdown of daily costs between main sessions and subagent sessions. */
|
|
17
|
+
export interface DailyCostBreakdown {
|
|
18
|
+
total: number;
|
|
19
|
+
main: number;
|
|
20
|
+
subagent: number;
|
|
21
|
+
}
|
|
11
22
|
/**
|
|
12
23
|
* Tracks aggregate cost across all sessions for the current calendar day.
|
|
13
|
-
* Scans session files
|
|
24
|
+
* Scans both main session files and subagent session files, caches result for O(1) footer access.
|
|
14
25
|
* Refreshes periodically (60s) and on-demand via refresh().
|
|
15
26
|
*/
|
|
16
27
|
export declare class DailyCostTracker {
|
|
17
28
|
private static readonly REFRESH_INTERVAL_MS;
|
|
18
|
-
private
|
|
29
|
+
private cachedMain;
|
|
30
|
+
private cachedSubagent;
|
|
19
31
|
private refreshTimer;
|
|
20
32
|
private disposed;
|
|
21
33
|
private sessionsDir;
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
private subagentSessionsDir;
|
|
35
|
+
constructor(sessionsDir?: string, subagentSessionsDir?: string);
|
|
36
|
+
/** Get cached daily cost total (main + subagent). O(1). */
|
|
24
37
|
getDailyCost(): number;
|
|
38
|
+
/** Get cached daily cost breakdown: total, main, and subagent. O(1). */
|
|
39
|
+
getDailyCostBreakdown(): DailyCostBreakdown;
|
|
25
40
|
/** Force an async refresh of the daily cost. */
|
|
26
41
|
refresh(): Promise<void>;
|
|
27
42
|
/** Clean up timer and prevent in-flight scans from updating cache. */
|
|
28
43
|
dispose(): void;
|
|
29
44
|
private initialScan;
|
|
30
45
|
private scheduleNextRefresh;
|
|
31
|
-
private
|
|
32
|
-
private
|
|
46
|
+
private scanMainSessions;
|
|
47
|
+
private scanSubagentSessions;
|
|
48
|
+
private scanJsonlFilesInDir;
|
|
33
49
|
}
|
|
34
50
|
//# 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;;;;GAIG;AACH,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAU;IAErD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAS;
|
|
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,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { readdir, readFile } from "fs/promises";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { getSessionsDir } from "../config.js";
|
|
4
|
+
import { getSessionsDir, getSubagentSessionsDir } from "../config.js";
|
|
5
5
|
/**
|
|
6
6
|
* Parse a session filename timestamp back to a Date.
|
|
7
7
|
* Filename timestamps look like "2026-04-09T18-49-11-406Z" (colons and dots replaced with hyphens).
|
|
@@ -25,32 +25,72 @@ export function isSameLocalDay(date, today) {
|
|
|
25
25
|
date.getMonth() === today.getMonth() &&
|
|
26
26
|
date.getDate() === today.getDate());
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Sum `usage.cost.total` from all assistant messages in a JSONL session file.
|
|
30
|
+
* Works identically for both main sessions and subagent sessions.
|
|
31
|
+
*/
|
|
32
|
+
export async function sumCostFromFile(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await readFile(filePath, "utf8");
|
|
35
|
+
let total = 0;
|
|
36
|
+
for (const line of content.split("\n")) {
|
|
37
|
+
if (!line.trim())
|
|
38
|
+
continue;
|
|
39
|
+
try {
|
|
40
|
+
const entry = JSON.parse(line);
|
|
41
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
42
|
+
total += entry.message.usage?.cost?.total ?? 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Skip malformed lines
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return total;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* File unreadable — treat as zero cost */
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
28
56
|
/**
|
|
29
57
|
* Tracks aggregate cost across all sessions for the current calendar day.
|
|
30
|
-
* Scans session files
|
|
58
|
+
* Scans both main session files and subagent session files, caches result for O(1) footer access.
|
|
31
59
|
* Refreshes periodically (60s) and on-demand via refresh().
|
|
32
60
|
*/
|
|
33
61
|
export class DailyCostTracker {
|
|
34
62
|
static REFRESH_INTERVAL_MS = 60_000;
|
|
35
|
-
|
|
63
|
+
cachedMain = 0;
|
|
64
|
+
cachedSubagent = 0;
|
|
36
65
|
refreshTimer = null;
|
|
37
66
|
disposed = false;
|
|
38
67
|
sessionsDir;
|
|
39
|
-
|
|
68
|
+
subagentSessionsDir;
|
|
69
|
+
constructor(sessionsDir, subagentSessionsDir) {
|
|
40
70
|
this.sessionsDir = sessionsDir ?? getSessionsDir();
|
|
71
|
+
this.subagentSessionsDir = subagentSessionsDir ?? getSubagentSessionsDir();
|
|
41
72
|
// Kick off initial async scan — getDailyCost() returns 0 until it completes
|
|
42
73
|
void this.initialScan();
|
|
43
74
|
}
|
|
44
|
-
/** Get cached daily cost total. O(1). */
|
|
75
|
+
/** Get cached daily cost total (main + subagent). O(1). */
|
|
45
76
|
getDailyCost() {
|
|
46
|
-
return this.
|
|
77
|
+
return this.cachedMain + this.cachedSubagent;
|
|
78
|
+
}
|
|
79
|
+
/** Get cached daily cost breakdown: total, main, and subagent. O(1). */
|
|
80
|
+
getDailyCostBreakdown() {
|
|
81
|
+
return {
|
|
82
|
+
total: this.cachedMain + this.cachedSubagent,
|
|
83
|
+
main: this.cachedMain,
|
|
84
|
+
subagent: this.cachedSubagent,
|
|
85
|
+
};
|
|
47
86
|
}
|
|
48
87
|
/** Force an async refresh of the daily cost. */
|
|
49
88
|
async refresh() {
|
|
50
89
|
if (!this.disposed) {
|
|
51
|
-
const
|
|
90
|
+
const [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);
|
|
52
91
|
if (!this.disposed) {
|
|
53
|
-
this.
|
|
92
|
+
this.cachedMain = main;
|
|
93
|
+
this.cachedSubagent = subagent;
|
|
54
94
|
}
|
|
55
95
|
}
|
|
56
96
|
}
|
|
@@ -63,9 +103,10 @@ export class DailyCostTracker {
|
|
|
63
103
|
}
|
|
64
104
|
}
|
|
65
105
|
async initialScan() {
|
|
66
|
-
const
|
|
106
|
+
const [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);
|
|
67
107
|
if (!this.disposed) {
|
|
68
|
-
this.
|
|
108
|
+
this.cachedMain = main;
|
|
109
|
+
this.cachedSubagent = subagent;
|
|
69
110
|
this.scheduleNextRefresh();
|
|
70
111
|
}
|
|
71
112
|
}
|
|
@@ -76,9 +117,10 @@ export class DailyCostTracker {
|
|
|
76
117
|
this.refreshTimer = null;
|
|
77
118
|
if (this.disposed)
|
|
78
119
|
return;
|
|
79
|
-
const
|
|
120
|
+
const [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);
|
|
80
121
|
if (!this.disposed) {
|
|
81
|
-
this.
|
|
122
|
+
this.cachedMain = main;
|
|
123
|
+
this.cachedSubagent = subagent;
|
|
82
124
|
this.scheduleNextRefresh();
|
|
83
125
|
}
|
|
84
126
|
}, DailyCostTracker.REFRESH_INTERVAL_MS);
|
|
@@ -87,7 +129,11 @@ export class DailyCostTracker {
|
|
|
87
129
|
this.refreshTimer.unref();
|
|
88
130
|
}
|
|
89
131
|
}
|
|
90
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Scan main sessions directory.
|
|
134
|
+
* Structure: sessionsDir / projectDir / TIMESTAMP_UUID.jsonl
|
|
135
|
+
*/
|
|
136
|
+
async scanMainSessions() {
|
|
91
137
|
try {
|
|
92
138
|
if (!existsSync(this.sessionsDir))
|
|
93
139
|
return 0;
|
|
@@ -98,30 +144,7 @@ export class DailyCostTracker {
|
|
|
98
144
|
if (!dirEntry.isDirectory())
|
|
99
145
|
continue;
|
|
100
146
|
const projectDir = join(this.sessionsDir, dirEntry.name);
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
files = (await readdir(projectDir)).filter((f) => f.endsWith(".jsonl"));
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
/* Directory unreadable — skip this session dir */
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
for (const filename of files) {
|
|
110
|
-
// Extract timestamp part: everything before the UUID
|
|
111
|
-
// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl
|
|
112
|
-
const underscoreIdx = filename.indexOf("_", 20);
|
|
113
|
-
if (underscoreIdx === -1)
|
|
114
|
-
continue;
|
|
115
|
-
const timestampPart = filename.slice(0, underscoreIdx);
|
|
116
|
-
const fileDate = filenameTimestampToDate(timestampPart);
|
|
117
|
-
if (!fileDate)
|
|
118
|
-
continue;
|
|
119
|
-
// Skip sessions not from today (compares local calendar day)
|
|
120
|
-
if (!isSameLocalDay(fileDate, now))
|
|
121
|
-
continue;
|
|
122
|
-
// Read and parse the JSONL file
|
|
123
|
-
total += await this.sumCostFromFile(join(projectDir, filename));
|
|
124
|
-
}
|
|
147
|
+
total += await this.scanJsonlFilesInDir(projectDir, now);
|
|
125
148
|
}
|
|
126
149
|
return total;
|
|
127
150
|
}
|
|
@@ -130,29 +153,76 @@ export class DailyCostTracker {
|
|
|
130
153
|
return 0;
|
|
131
154
|
}
|
|
132
155
|
}
|
|
133
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Scan subagent sessions directory.
|
|
158
|
+
* Structure: subagentSessionsDir / sessionId / TIMESTAMP_UUID.jsonl
|
|
159
|
+
* subagentSessionsDir / sessionId / step-N / TIMESTAMP_UUID.jsonl
|
|
160
|
+
*/
|
|
161
|
+
async scanSubagentSessions() {
|
|
134
162
|
try {
|
|
135
|
-
|
|
163
|
+
if (!existsSync(this.subagentSessionsDir))
|
|
164
|
+
return 0;
|
|
165
|
+
const now = new Date();
|
|
136
166
|
let total = 0;
|
|
137
|
-
|
|
138
|
-
|
|
167
|
+
const sessionIdDirs = await readdir(this.subagentSessionsDir, { withFileTypes: true });
|
|
168
|
+
for (const dirEntry of sessionIdDirs) {
|
|
169
|
+
if (!dirEntry.isDirectory())
|
|
139
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;
|
|
140
176
|
try {
|
|
141
|
-
|
|
142
|
-
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
143
|
-
total += entry.message.usage?.cost?.total ?? 0;
|
|
144
|
-
}
|
|
177
|
+
subEntries = await readdir(sessionIdDir, { withFileTypes: true });
|
|
145
178
|
}
|
|
146
179
|
catch {
|
|
147
|
-
|
|
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);
|
|
148
187
|
}
|
|
149
188
|
}
|
|
150
189
|
return total;
|
|
151
190
|
}
|
|
152
191
|
catch {
|
|
153
|
-
|
|
192
|
+
// Never crash the app
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Scan a directory for today's JSONL session files and sum their costs.
|
|
198
|
+
*/
|
|
199
|
+
async scanJsonlFilesInDir(dirPath, now) {
|
|
200
|
+
let files;
|
|
201
|
+
try {
|
|
202
|
+
files = (await readdir(dirPath)).filter((f) => f.endsWith(".jsonl"));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* Directory unreadable — skip */
|
|
154
206
|
return 0;
|
|
155
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;
|
|
156
226
|
}
|
|
157
227
|
}
|
|
158
228
|
//# 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 { existsSync } from \"fs\";\nimport { readdir, readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { getSessionsDir } 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 * Tracks aggregate cost across all sessions for the current calendar day.\n * Scans session files filtered by filename timestamp, 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 cachedCost = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate disposed = false;\n\tprivate sessionsDir: string;\n\n\tconstructor(sessionsDir?: string) {\n\t\tthis.sessionsDir = sessionsDir ?? getSessionsDir();\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. O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.cachedCost;\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 cost = await this.scanDailyCost();\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.cachedCost = cost;\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 cost = await this.scanDailyCost();\n\t\tif (!this.disposed) {\n\t\t\tthis.cachedCost = cost;\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 cost = await this.scanDailyCost();\n\t\t\tif (!this.disposed) {\n\t\t\t\tthis.cachedCost = cost;\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\tprivate async scanDailyCost(): 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\tlet files: string[];\n\t\t\t\ttry {\n\t\t\t\t\tfiles = (await readdir(projectDir)).filter((f) => f.endsWith(\".jsonl\"));\n\t\t\t\t} catch {\n\t\t\t\t\t/* Directory unreadable — skip this session dir */\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tfor (const filename of files) {\n\t\t\t\t\t// Extract timestamp part: everything before the UUID\n\t\t\t\t\t// Filename: 2026-04-09T18-49-11-406Z_33137d5d-d1e4-4a0e-baca-ebd08ab0e2e0.jsonl\n\t\t\t\t\tconst underscoreIdx = filename.indexOf(\"_\", 20);\n\t\t\t\t\tif (underscoreIdx === -1) continue;\n\n\t\t\t\t\tconst timestampPart = filename.slice(0, underscoreIdx);\n\t\t\t\t\tconst fileDate = filenameTimestampToDate(timestampPart);\n\t\t\t\t\tif (!fileDate) continue;\n\n\t\t\t\t\t// Skip sessions not from today (compares local calendar day)\n\t\t\t\t\tif (!isSameLocalDay(fileDate, now)) continue;\n\n\t\t\t\t\t// Read and parse the JSONL file\n\t\t\t\t\ttotal += await this.sumCostFromFile(join(projectDir, filename));\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\tprivate async sumCostFromFile(filePath: string): Promise<number> {\n\t\ttry {\n\t\t\tconst content = await readFile(filePath, \"utf8\");\n\t\t\tlet total = 0;\n\n\t\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\t\tif (!line.trim()) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\t\tif (entry.type === \"message\" && entry.message?.role === \"assistant\") {\n\t\t\t\t\t\ttotal += entry.message.usage?.cost?.total ?? 0;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip malformed lines\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn total;\n\t\t} catch {\n\t\t\t/* File unreadable — treat as zero cost */\n\t\t\treturn 0;\n\t\t}\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 { 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,3 +1,4 @@
|
|
|
1
|
+
import { type DailyCostBreakdown } from "./daily-cost-tracker.js";
|
|
1
2
|
/**
|
|
2
3
|
* Provides git branch and extension statuses - data not otherwise accessible to extensions.
|
|
3
4
|
* Token stats, model info available via ctx.sessionManager and ctx.model.
|
|
@@ -11,6 +12,7 @@ export declare class FooterDataProvider {
|
|
|
11
12
|
private reftableWatcher;
|
|
12
13
|
private branchChangeCallbacks;
|
|
13
14
|
private dailyCostTracker;
|
|
15
|
+
private subagentSessionCostTracker;
|
|
14
16
|
private availableProviderCount;
|
|
15
17
|
private refreshTimer;
|
|
16
18
|
private refreshInFlight;
|
|
@@ -27,10 +29,16 @@ export declare class FooterDataProvider {
|
|
|
27
29
|
setExtensionStatus(key: string, text: string | undefined): void;
|
|
28
30
|
/** Internal: clear extension statuses */
|
|
29
31
|
clearExtensionStatuses(): void;
|
|
30
|
-
/** Cached daily cost total across all sessions. O(1). */
|
|
32
|
+
/** Cached daily cost total across all sessions (main + subagent). O(1). */
|
|
31
33
|
getDailyCost(): number;
|
|
34
|
+
/** Cached daily cost breakdown: total, main, and subagent. O(1). */
|
|
35
|
+
getDailyCostBreakdown(): DailyCostBreakdown;
|
|
32
36
|
/** Force refresh of the daily cost cache. */
|
|
33
37
|
refreshDailyCost(): Promise<void>;
|
|
38
|
+
/** Cached sub-agent session cost for the current parent session. O(1). */
|
|
39
|
+
getSubagentSessionCost(): number;
|
|
40
|
+
/** Force refresh of the sub-agent session cost cache. */
|
|
41
|
+
refreshSubagentSessionCost(): Promise<void>;
|
|
34
42
|
/** Number of unique providers with available models (for footer display) */
|
|
35
43
|
getAvailableProviderCount(): number;
|
|
36
44
|
/** Internal: update available provider count */
|
|
@@ -45,5 +53,5 @@ export declare class FooterDataProvider {
|
|
|
45
53
|
private setupGitWatcher;
|
|
46
54
|
}
|
|
47
55
|
/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */
|
|
48
|
-
export type ReadonlyFooterDataProvider = Pick<FooterDataProvider, "getGitBranch" | "getExtensionStatuses" | "getAvailableProviderCount" | "onBranchChange" | "getDailyCost">;
|
|
56
|
+
export type ReadonlyFooterDataProvider = Pick<FooterDataProvider, "getGitBranch" | "getExtensionStatuses" | "getAvailableProviderCount" | "onBranchChange" | "getDailyCost" | "getDailyCostBreakdown" | "getSubagentSessionCost">;
|
|
49
57
|
//# sourceMappingURL=footer-data-provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer-data-provider.d.ts","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"
|
|
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;AAIpF;;;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,cAKC;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, 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"]}
|
|
@@ -2,6 +2,7 @@ import { existsSync, watch } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { DailyCostTracker } from "./daily-cost-tracker.js";
|
|
4
4
|
import { findGitPaths, getGitBranch, getGitBranchAsync } from "./git-branch.js";
|
|
5
|
+
import { SubagentSessionCostTracker } from "./subagent-session-cost-tracker.js";
|
|
5
6
|
/**
|
|
6
7
|
* Provides git branch and extension statuses - data not otherwise accessible to extensions.
|
|
7
8
|
* Token stats, model info available via ctx.sessionManager and ctx.model.
|
|
@@ -15,6 +16,7 @@ export class FooterDataProvider {
|
|
|
15
16
|
reftableWatcher = null;
|
|
16
17
|
branchChangeCallbacks = new Set();
|
|
17
18
|
dailyCostTracker;
|
|
19
|
+
subagentSessionCostTracker;
|
|
18
20
|
availableProviderCount = 0;
|
|
19
21
|
refreshTimer = null;
|
|
20
22
|
refreshInFlight = false;
|
|
@@ -24,6 +26,7 @@ export class FooterDataProvider {
|
|
|
24
26
|
this.gitPaths = findGitPaths();
|
|
25
27
|
this.setupGitWatcher();
|
|
26
28
|
this.dailyCostTracker = new DailyCostTracker();
|
|
29
|
+
this.subagentSessionCostTracker = new SubagentSessionCostTracker();
|
|
27
30
|
}
|
|
28
31
|
/** Current git branch, null if not in repo, "detached" if detached HEAD */
|
|
29
32
|
getGitBranch() {
|
|
@@ -54,14 +57,26 @@ export class FooterDataProvider {
|
|
|
54
57
|
clearExtensionStatuses() {
|
|
55
58
|
this.extensionStatuses.clear();
|
|
56
59
|
}
|
|
57
|
-
/** Cached daily cost total across all sessions. O(1). */
|
|
60
|
+
/** Cached daily cost total across all sessions (main + subagent). O(1). */
|
|
58
61
|
getDailyCost() {
|
|
59
62
|
return this.dailyCostTracker.getDailyCost();
|
|
60
63
|
}
|
|
64
|
+
/** Cached daily cost breakdown: total, main, and subagent. O(1). */
|
|
65
|
+
getDailyCostBreakdown() {
|
|
66
|
+
return this.dailyCostTracker.getDailyCostBreakdown();
|
|
67
|
+
}
|
|
61
68
|
/** Force refresh of the daily cost cache. */
|
|
62
69
|
async refreshDailyCost() {
|
|
63
70
|
await this.dailyCostTracker.refresh();
|
|
64
71
|
}
|
|
72
|
+
/** Cached sub-agent session cost for the current parent session. O(1). */
|
|
73
|
+
getSubagentSessionCost() {
|
|
74
|
+
return this.subagentSessionCostTracker.getCost();
|
|
75
|
+
}
|
|
76
|
+
/** Force refresh of the sub-agent session cost cache. */
|
|
77
|
+
async refreshSubagentSessionCost() {
|
|
78
|
+
await this.subagentSessionCostTracker.refresh();
|
|
79
|
+
}
|
|
65
80
|
/** Number of unique providers with available models (for footer display) */
|
|
66
81
|
getAvailableProviderCount() {
|
|
67
82
|
return this.availableProviderCount;
|
|
@@ -78,6 +93,7 @@ export class FooterDataProvider {
|
|
|
78
93
|
this.refreshTimer = null;
|
|
79
94
|
}
|
|
80
95
|
this.dailyCostTracker.dispose();
|
|
96
|
+
this.subagentSessionCostTracker.dispose();
|
|
81
97
|
if (this.headWatcher) {
|
|
82
98
|
this.headWatcher.close();
|
|
83
99
|
this.headWatcher = null;
|
|
@@ -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 { DailyCostTracker } from \"./daily-cost-tracker.js\";\nimport { findGitPaths, type GitPaths, getGitBranch, getGitBranchAsync } from \"./git-branch.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 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}\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. O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.dailyCostTracker.getDailyCost();\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/** 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\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\" | \"getExtensionStatuses\" | \"getAvailableProviderCount\" | \"onBranchChange\" | \"getDailyCost\"\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, 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"]}
|