feat: add buildChildrenIndex and shared formatCost helper

This commit is contained in:
Igor Velho
2026-08-24 14:47:01 +01:00
parent 11582ab62b
commit 48ca2fdf43
2 changed files with 66 additions and 1 deletions
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'bun:test';
import type { Session } from '@opencode-ai/sdk/v2';
import { buildChildrenIndex } from './subagentCost';
import { buildChildrenIndex, formatCost } from './subagentCost';
function makeSession(id: string, cost: number, parentID?: string): Session {
return { id, cost, parentID } as unknown as Session;
@@ -15,3 +15,11 @@ describe('buildChildrenIndex', () => {
expect(index.get('root')).toEqual([childA, childB]);
});
});
describe('formatCost', () => {
test('prefixes with $ and trims trailing zeros', () => {
expect(formatCost(1.5)).toBe('$1.5');
expect(formatCost(0.0001)).toBe('$0.0001');
expect(formatCost(2)).toBe('$2');
});
});
@@ -0,0 +1,57 @@
import type { Session } from '@opencode-ai/sdk/v2';
// Spend is read against a budget, so it keeps its real precision instead of
// collapsing to two decimals. Trailing zeros are dropped so exact values stay
// short. Relocated from WorkStatusPrimaryGroup.tsx so both that component and
// WorkStatusSubagentsSection share one implementation.
const trimZeros = (value: string): string =>
(value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value);
export const formatCost = (cost: number): string => `$${trimZeros(cost.toFixed(4))}`;
/**
* Groups a flat live-session list by parentID. One pass, O(n). Sessions
* without a parentID (roots) are simply absent as keys — callers look up a
* specific id's children via `.get(id) ?? []`.
*/
export function buildChildrenIndex(sessions: Session[]): Map<string, Session[]> {
const index = new Map<string, Session[]>();
for (const session of sessions) {
const parentID = (session as unknown as { parentID?: string }).parentID;
if (!parentID) continue;
const existing = index.get(parentID);
if (existing) {
existing.push(session);
} else {
index.set(parentID, [session]);
}
}
return index;
}
function sessionCost(session: Session | undefined): number {
const cost = (session as unknown as { cost?: number } | undefined)?.cost;
return typeof cost === 'number' ? cost : 0;
}
/**
* Own cost plus every descendant's cost, recursively. Cycle-guarded with a
* visited set: parentID should form a tree, but this does not trust that
* invariant blindly (mirrors opencode-session-cost's src/cost.ts).
*/
export function computeSubtreeCost(
id: string,
sessionsById: Map<string, Session>,
childrenByParent: Map<string, Session[]>,
visited: Set<string> = new Set(),
): number {
if (visited.has(id)) return 0;
visited.add(id);
let total = sessionCost(sessionsById.get(id));
const children = childrenByParent.get(id) ?? [];
for (const child of children) {
total += computeSubtreeCost(child.id, sessionsById, childrenByParent, visited);
}
return total;
}