@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.
Files changed (70) hide show
  1. package/README.md +4 -2
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +1 -1
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/config.d.ts +12 -2
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +27 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +5 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +16 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/daily-cost-tracker.d.ts +22 -6
  14. package/dist/core/daily-cost-tracker.d.ts.map +1 -1
  15. package/dist/core/daily-cost-tracker.js +117 -47
  16. package/dist/core/daily-cost-tracker.js.map +1 -1
  17. package/dist/core/footer-data-provider.d.ts +10 -2
  18. package/dist/core/footer-data-provider.d.ts.map +1 -1
  19. package/dist/core/footer-data-provider.js +17 -1
  20. package/dist/core/footer-data-provider.js.map +1 -1
  21. package/dist/core/sdk.d.ts.map +1 -1
  22. package/dist/core/sdk.js +2 -1
  23. package/dist/core/sdk.js.map +1 -1
  24. package/dist/core/session-manager.d.ts +32 -4
  25. package/dist/core/session-manager.d.ts.map +1 -1
  26. package/dist/core/session-manager.js +234 -108
  27. package/dist/core/session-manager.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +4 -0
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js +4 -0
  31. package/dist/core/settings-manager.js.map +1 -1
  32. package/dist/core/subagent-session-cost-tracker.d.ts +22 -0
  33. package/dist/core/subagent-session-cost-tracker.d.ts.map +1 -0
  34. package/dist/core/subagent-session-cost-tracker.js +94 -0
  35. package/dist/core/subagent-session-cost-tracker.js.map +1 -0
  36. package/dist/core/tools/subagent.d.ts +14 -8
  37. package/dist/core/tools/subagent.d.ts.map +1 -1
  38. package/dist/core/tools/subagent.js +145 -71
  39. package/dist/core/tools/subagent.js.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/main.d.ts.map +1 -1
  45. package/dist/main.js +16 -7
  46. package/dist/main.js.map +1 -1
  47. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/footer.js +8 -2
  49. package/dist/modes/interactive/components/footer.js.map +1 -1
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +5 -1
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/dist/modes/rpc/rpc-client.d.ts +7 -2
  54. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  55. package/dist/modes/rpc/rpc-client.js +3 -2
  56. package/dist/modes/rpc/rpc-client.js.map +1 -1
  57. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  58. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  59. package/dist/modes/rpc/rpc-mode.js +34 -9
  60. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  61. package/dist/modes/rpc/rpc-types.d.ts +2 -0
  62. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  63. package/dist/modes/rpc/rpc-types.js.map +1 -1
  64. package/docs/dashboard.md +27 -15
  65. package/docs/rpc.md +9 -5
  66. package/docs/session.md +16 -7
  67. package/docs/settings.md +17 -5
  68. package/docs/skills.md +1 -1
  69. package/package.json +1 -1
  70. 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 filtered by filename timestamp, caches result for O(1) footer access.
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 cachedCost;
29
+ private cachedMain;
30
+ private cachedSubagent;
19
31
  private refreshTimer;
20
32
  private disposed;
21
33
  private sessionsDir;
22
- constructor(sessionsDir?: string);
23
- /** Get cached daily cost total. O(1). */
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 scanDailyCost;
32
- private sumCostFromFile;
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;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":"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 filtered by filename timestamp, caches result for O(1) footer access.
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
- cachedCost = 0;
63
+ cachedMain = 0;
64
+ cachedSubagent = 0;
36
65
  refreshTimer = null;
37
66
  disposed = false;
38
67
  sessionsDir;
39
- constructor(sessionsDir) {
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.cachedCost;
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 cost = await this.scanDailyCost();
90
+ const [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);
52
91
  if (!this.disposed) {
53
- this.cachedCost = cost;
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 cost = await this.scanDailyCost();
106
+ const [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);
67
107
  if (!this.disposed) {
68
- this.cachedCost = cost;
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 cost = await this.scanDailyCost();
120
+ const [main, subagent] = await Promise.all([this.scanMainSessions(), this.scanSubagentSessions()]);
80
121
  if (!this.disposed) {
81
- this.cachedCost = cost;
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
- async scanDailyCost() {
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
- 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
- }
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
- async sumCostFromFile(filePath) {
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
- const content = await readFile(filePath, "utf8");
163
+ if (!existsSync(this.subagentSessionsDir))
164
+ return 0;
165
+ const now = new Date();
136
166
  let total = 0;
137
- for (const line of content.split("\n")) {
138
- if (!line.trim())
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
- const entry = JSON.parse(line);
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
- // Skip malformed lines
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
- /* File unreadable — treat as zero cost */
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":"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,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"]}