Skip to content

Commit 86adf25

Browse files
committed
feat(ui): Show cost in agent conversation popups
Pass the existing opt-in cost display setting into conversation viewers opened from FleetView and /agents. Read retained lifetime usage so completed agents keep showing their reported cost, and cover enabled and disabled display paths.
1 parent 8e02035 commit 86adf25

5 files changed

Lines changed: 60 additions & 4 deletions

File tree

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1829,7 +1829,7 @@ Terse command-style prompts produce shallow, generic work.
18291829
if (manager.abort(record.id)) {
18301830
ctx.ui.notify(`Stopped "${record.description}".`, "info");
18311831
}
1832-
}, keybindings, (message: string) => manager.steer(record.id, message));
1832+
}, keybindings, (message: string) => manager.steer(record.id, message), isCostDisplayEnabled);
18331833
},
18341834
{
18351835
overlay: true,

src/ui/conversation-viewer.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
99
import { type Component, Input, matchesKey, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
1010
import { extractText } from "../context.js";
1111
import type { AgentRecord } from "../types.js";
12-
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
12+
import { formatCost, getLifetimeTotal, getSessionContextPercent } from "../usage.js";
1313
import type { Theme } from "./agent-widget.js";
1414
import { type AgentActivity, buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
1515
import { createViewerKeys, type ViewerKeybindings, type ViewerKeys } from "./viewer-keys.js";
@@ -45,6 +45,8 @@ export class ConversationViewer implements Component {
4545
keybindings?: ViewerKeybindings,
4646
/** Send a steering message to the agent. Omitted → no compose affordance. */
4747
private onSteer?: (message: string) => void,
48+
/** Read live cost-display preference. Omitted → hide reported cost. */
49+
private showCost: () => boolean = () => false,
4850
) {
4951
this.keys = createViewerKeys(keybindings);
5052
this.unsubscribe = session.subscribe(() => {
@@ -152,11 +154,15 @@ export class ConversationViewer implements Component {
152154
const headerParts: string[] = [duration];
153155
const toolUses = this.activity?.toolUses ?? this.record.toolUses;
154156
if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
155-
const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
157+
const usage = this.activity?.lifetimeUsage ?? this.record.lifetimeUsage;
158+
const tokens = getLifetimeTotal(usage);
156159
if (tokens > 0) {
157160
const percent = getSessionContextPercent(this.activity?.session);
158161
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
159162
}
163+
if (this.showCost() && usage?.cost !== undefined) {
164+
headerParts.push(formatCost(usage.cost));
165+
}
160166

161167
lines.push(row(
162168
`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`,

src/ui/fleet-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ export class FleetList {
311311
},
312312
keybindings,
313313
(message: string) => this.manager.steer(record.id, message),
314+
this.showCost,
314315
);
315316
},
316317
{

test/conversation-viewer.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,42 @@ describe("ConversationViewer", () => {
257257
});
258258
});
259259

260+
describe("cost display", () => {
261+
it("shows reported lifetime cost when enabled", () => {
262+
const viewer = new ConversationViewer(
263+
mockTui(),
264+
mockSession(),
265+
mockRecord({ lifetimeUsage: { input: 1_000, output: 200, cacheWrite: 0, cost: 0.018 } }),
266+
undefined,
267+
ansiTheme(),
268+
vi.fn(),
269+
undefined,
270+
undefined,
271+
undefined,
272+
() => true,
273+
);
274+
275+
expect(viewer.render(80).join("\n")).toContain("~$0.018");
276+
});
277+
278+
it("hides reported lifetime cost when disabled", () => {
279+
const viewer = new ConversationViewer(
280+
mockTui(),
281+
mockSession(),
282+
mockRecord({ lifetimeUsage: { input: 1_000, output: 200, cacheWrite: 0, cost: 0.018 } }),
283+
undefined,
284+
ansiTheme(),
285+
vi.fn(),
286+
undefined,
287+
undefined,
288+
undefined,
289+
() => false,
290+
);
291+
292+
expect(viewer.render(80).join("\n")).not.toContain("~$0.018");
293+
});
294+
});
295+
260296
describe("safety net against upstream wrapTextWithAnsi bugs", () => {
261297
// These tests call buildContentLines() directly (via the private method)
262298
// because render() has its own truncation via row(). The safety net in

test/fleet-list.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ interface Harness {
4949
ui: FleetUICtx;
5050
manager: AgentManager;
5151
/** The overlay component (a real ConversationViewer) once one is opened. */
52-
overlayComponent: () => { handleInput(data: string): void } | undefined;
52+
overlayComponent: () => { handleInput(data: string): void; render(width: number): string[] } | undefined;
5353
/** Feed a key to the registered input handler; returns the consume result. */
5454
press: (data: string) => { consume?: boolean } | undefined;
5555
/** Render the currently-registered below-editor widget at the given width. */
@@ -323,6 +323,19 @@ describe("FleetList rendering", () => {
323323
expect(agentLine).toContain("~$0.018");
324324
});
325325

326+
it("passes cost display to the selected agent popup", () => {
327+
const h = harness([
328+
makeRecord({ lifetimeUsage: { input: 13_100, output: 0, cacheWrite: 0, cost: 0.018 } }),
329+
], true);
330+
h.press(DOWN); // activate (main)
331+
h.press(DOWN); // select the agent
332+
h.press(ENTER); // open the conversation popup
333+
334+
// The harness theme uses visible markup instead of ANSI escapes; use a wide
335+
// render width so that markup does not affect the viewer's truncation path.
336+
expect(h.overlayComponent()!.render(500).join("\n")).toContain("~$0.018");
337+
});
338+
326339
it("hides reported cost when cost display is disabled", () => {
327340
const h = harness([
328341
makeRecord({ lifetimeUsage: { input: 13_100, output: 0, cacheWrite: 0, cost: 0.018 } }),

0 commit comments

Comments
 (0)