@dreb/coding-agent 2.67.0 → 2.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/core/agent-session.d.ts +1 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/daily-cost-tracker.d.ts +58 -7
  6. package/dist/core/daily-cost-tracker.d.ts.map +1 -1
  7. package/dist/core/daily-cost-tracker.js +146 -88
  8. package/dist/core/daily-cost-tracker.js.map +1 -1
  9. package/dist/core/footer-data-provider.d.ts +11 -3
  10. package/dist/core/footer-data-provider.d.ts.map +1 -1
  11. package/dist/core/footer-data-provider.js +19 -3
  12. package/dist/core/footer-data-provider.js.map +1 -1
  13. package/dist/core/session-manager.d.ts +14 -0
  14. package/dist/core/session-manager.d.ts.map +1 -1
  15. package/dist/core/session-manager.js +182 -88
  16. package/dist/core/session-manager.js.map +1 -1
  17. package/dist/core/subagent-session-cost-tracker.d.ts +22 -0
  18. package/dist/core/subagent-session-cost-tracker.d.ts.map +1 -0
  19. package/dist/core/subagent-session-cost-tracker.js +94 -0
  20. package/dist/core/subagent-session-cost-tracker.js.map +1 -0
  21. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/footer.js +8 -2
  23. package/dist/modes/interactive/components/footer.js.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.js +7 -2
  26. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  27. package/dist/modes/rpc/rpc-client.d.ts +7 -2
  28. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  29. package/dist/modes/rpc/rpc-client.js +3 -2
  30. package/dist/modes/rpc/rpc-client.js.map +1 -1
  31. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  32. package/dist/modes/rpc/rpc-mode.js +29 -6
  33. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  34. package/dist/modes/rpc/rpc-types.d.ts +2 -0
  35. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  36. package/dist/modes/rpc/rpc-types.js.map +1 -1
  37. package/docs/dashboard.md +21 -15
  38. package/docs/rpc.md +8 -2
  39. package/docs/settings.md +1 -1
  40. package/package.json +1 -1
@@ -8,27 +8,78 @@ 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
+ }
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
+ }
11
36
  /**
12
37
  * Tracks aggregate cost across all sessions for the current calendar day.
13
- * Scans session files filtered by filename timestamp, caches result for O(1) footer access.
38
+ * Scans main session roots and subagent session roots, caches result for O(1) footer access.
14
39
  * Refreshes periodically (60s) and on-demand via refresh().
15
40
  */
16
41
  export declare class DailyCostTracker {
17
42
  private static readonly REFRESH_INTERVAL_MS;
18
- private cachedCost;
43
+ private cachedMain;
44
+ private cachedSubagent;
19
45
  private refreshTimer;
20
46
  private disposed;
21
- private sessionsDir;
22
- constructor(sessionsDir?: string);
23
- /** Get cached daily cost total. O(1). */
47
+ private readonly nestedMainRoots;
48
+ private readonly flatMainRoots;
49
+ private readonly subagentRoots;
50
+ constructor(sources?: DailyCostSources);
51
+ /** Get cached daily cost total (main + subagent). O(1). */
24
52
  getDailyCost(): number;
53
+ /** Get cached daily cost breakdown: total, main, and subagent. O(1). */
54
+ getDailyCostBreakdown(): DailyCostBreakdown;
25
55
  /** Force an async refresh of the daily cost. */
26
56
  refresh(): Promise<void>;
27
57
  /** Clean up timer and prevent in-flight scans from updating cache. */
28
58
  dispose(): void;
29
59
  private initialScan;
30
60
  private scheduleNextRefresh;
31
- private scanDailyCost;
32
- private sumCostFromFile;
61
+ private scanCosts;
62
+ private collectMainFiles;
63
+ private collectSubagentFiles;
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[];
33
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;
34
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;;;;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;IAE5B,YAAY,WAAW,CAAC,EAAE,MAAM,EAI/B;IAED,yCAAyC;IACzC,YAAY,IAAI,MAAM,CAErB;IAED,gDAAgD;IAC1C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAO7B;IAED,sEAAsE;IACtE,OAAO,IAAI,IAAI,CAMd;YAEa,WAAW;IAQzB,OAAO,CAAC,mBAAmB;YAiBb,aAAa;YA6Cb,eAAe;CAuB7B","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.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 } 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).
@@ -25,33 +24,78 @@ export function isSameLocalDay(date, today) {
25
24
  date.getMonth() === today.getMonth() &&
26
25
  date.getDate() === today.getDate());
27
26
  }
27
+ /**
28
+ * Sum `usage.cost.total` from all assistant messages in a JSONL session file.
29
+ * Works identically for both main sessions and subagent sessions.
30
+ */
31
+ export async function sumCostFromFile(filePath) {
32
+ try {
33
+ const content = await readFile(filePath, "utf8");
34
+ let total = 0;
35
+ for (const line of content.split("\n")) {
36
+ if (!line.trim())
37
+ continue;
38
+ try {
39
+ const entry = JSON.parse(line);
40
+ if (entry.type === "message" && entry.message?.role === "assistant") {
41
+ total += entry.message.usage?.cost?.total ?? 0;
42
+ }
43
+ }
44
+ catch {
45
+ // Skip malformed lines
46
+ }
47
+ }
48
+ return total;
49
+ }
50
+ catch {
51
+ /* File unreadable — treat as zero cost */
52
+ return 0;
53
+ }
54
+ }
55
+ function uniqueRoots(roots) {
56
+ return [...new Set(roots.filter((root) => !!root).map((root) => resolve(root)))];
57
+ }
28
58
  /**
29
59
  * Tracks aggregate cost across all sessions for the current calendar day.
30
- * Scans session files filtered by filename timestamp, caches result for O(1) footer access.
60
+ * Scans main session roots and subagent session roots, caches result for O(1) footer access.
31
61
  * Refreshes periodically (60s) and on-demand via refresh().
32
62
  */
33
63
  export class DailyCostTracker {
34
64
  static REFRESH_INTERVAL_MS = 60_000;
35
- cachedCost = 0;
65
+ cachedMain = 0;
66
+ cachedSubagent = 0;
36
67
  refreshTimer = null;
37
68
  disposed = false;
38
- sessionsDir;
39
- constructor(sessionsDir) {
40
- this.sessionsDir = sessionsDir ?? getSessionsDir();
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()]);
41
76
  // Kick off initial async scan — getDailyCost() returns 0 until it completes
42
77
  void this.initialScan();
43
78
  }
44
- /** Get cached daily cost total. O(1). */
79
+ /** Get cached daily cost total (main + subagent). O(1). */
45
80
  getDailyCost() {
46
- return this.cachedCost;
81
+ return this.cachedMain + this.cachedSubagent;
82
+ }
83
+ /** Get cached daily cost breakdown: total, main, and subagent. O(1). */
84
+ getDailyCostBreakdown() {
85
+ return {
86
+ total: this.cachedMain + this.cachedSubagent,
87
+ main: this.cachedMain,
88
+ subagent: this.cachedSubagent,
89
+ };
47
90
  }
48
91
  /** Force an async refresh of the daily cost. */
49
92
  async refresh() {
93
+ if (this.disposed)
94
+ return;
95
+ const [main, subagent] = await this.scanCosts();
50
96
  if (!this.disposed) {
51
- const cost = await this.scanDailyCost();
52
- if (!this.disposed) {
53
- this.cachedCost = cost;
54
- }
97
+ this.cachedMain = main;
98
+ this.cachedSubagent = subagent;
55
99
  }
56
100
  }
57
101
  /** Clean up timer and prevent in-flight scans from updating cache. */
@@ -63,96 +107,110 @@ export class DailyCostTracker {
63
107
  }
64
108
  }
65
109
  async initialScan() {
66
- const cost = await this.scanDailyCost();
67
- if (!this.disposed) {
68
- this.cachedCost = cost;
69
- this.scheduleNextRefresh();
70
- }
110
+ await this.refresh();
111
+ this.scheduleNextRefresh();
71
112
  }
72
113
  scheduleNextRefresh() {
73
114
  if (this.disposed)
74
115
  return;
75
116
  this.refreshTimer = setTimeout(async () => {
76
117
  this.refreshTimer = null;
77
- if (this.disposed)
78
- return;
79
- const cost = await this.scanDailyCost();
80
- if (!this.disposed) {
81
- this.cachedCost = cost;
82
- this.scheduleNextRefresh();
83
- }
118
+ await this.refresh();
119
+ this.scheduleNextRefresh();
84
120
  }, DailyCostTracker.REFRESH_INTERVAL_MS);
85
121
  // Allow the timer to not keep the process alive
86
122
  if (this.refreshTimer && typeof this.refreshTimer === "object" && "unref" in this.refreshTimer) {
87
123
  this.refreshTimer.unref();
88
124
  }
89
125
  }
90
- async scanDailyCost() {
91
- try {
92
- if (!existsSync(this.sessionsDir))
93
- return 0;
94
- const now = new Date();
95
- let total = 0;
96
- const projectDirs = await readdir(this.sessionsDir, { withFileTypes: true });
97
- for (const dirEntry of projectDirs) {
98
- if (!dirEntry.isDirectory())
99
- continue;
100
- const projectDir = join(this.sessionsDir, dirEntry.name);
101
- let files;
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
- }
125
- }
126
- return total;
127
- }
128
- catch {
129
- // Never crash the app
130
- 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);
131
144
  }
145
+ for (const root of this.flatMainRoots)
146
+ await addTodaysJsonlFiles(files, root, now);
147
+ return files;
132
148
  }
133
- async sumCostFromFile(filePath) {
134
- try {
135
- const content = await readFile(filePath, "utf8");
136
- let total = 0;
137
- for (const line of content.split("\n")) {
138
- if (!line.trim())
139
- continue;
140
- try {
141
- const entry = JSON.parse(line);
142
- if (entry.type === "message" && entry.message?.role === "assistant") {
143
- total += entry.message.usage?.cost?.total ?? 0;
144
- }
145
- }
146
- catch {
147
- // Skip malformed lines
148
- }
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);
149
157
  }
150
- return total;
151
- }
152
- catch {
153
- /* File unreadable — treat as zero cost */
154
- return 0;
155
158
  }
159
+ return files;
160
+ }
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));
156
167
  }
168
+ catch {
169
+ return [];
170
+ }
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
+ };
157
215
  }
158
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 { 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 { 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,3 +1,4 @@
1
+ import { type DailyCostBreakdown, type DailyCostSources } 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,12 +12,13 @@ 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;
17
19
  private refreshPending;
18
20
  private disposed;
19
- constructor();
21
+ constructor(dailyCostSources?: DailyCostSources);
20
22
  /** Current git branch, null if not in repo, "detached" if detached HEAD */
21
23
  getGitBranch(): string | null;
22
24
  /** Extension status texts set via ctx.ui.setStatus() */
@@ -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":"AAKA;;;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,sBAAsB,CAAK;IACnC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAS;IAEzB,cAIC;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,yDAAyD;IACzD,YAAY,IAAI,MAAM,CAErB;IAED,6CAA6C;IACvC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtC;IAED,4EAA4E;IAC5E,yBAAyB,IAAI,MAAM,CAElC;IAED,gDAAgD;IAChD,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7C;IAED,wBAAwB;IACxB,OAAO,IAAI,IAAI,CAgBd;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,EAClB,cAAc,GAAG,sBAAsB,GAAG,2BAA2B,GAAG,gBAAgB,GAAG,cAAc,CACzG,CAAC","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.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"]}
@@ -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,15 +16,17 @@ 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;
21
23
  refreshPending = false;
22
24
  disposed = false;
23
- constructor() {
25
+ constructor(dailyCostSources) {
24
26
  this.gitPaths = findGitPaths();
25
27
  this.setupGitWatcher();
26
- this.dailyCostTracker = new DailyCostTracker();
28
+ this.dailyCostTracker = new DailyCostTracker(dailyCostSources);
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;