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