feat: add buildChildrenIndex and shared formatCost helper
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from 'bun:test';
|
import { describe, expect, test } from 'bun:test';
|
||||||
import type { Session } from '@opencode-ai/sdk/v2';
|
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 {
|
function makeSession(id: string, cost: number, parentID?: string): Session {
|
||||||
return { id, cost, parentID } as unknown as Session;
|
return { id, cost, parentID } as unknown as Session;
|
||||||
@@ -15,3 +15,11 @@ describe('buildChildrenIndex', () => {
|
|||||||
expect(index.get('root')).toEqual([childA, childB]);
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user