mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 20:38:22 +00:00
feat(tui): pin IN/OUT to opposite ends and label the cost column COST
`IN/OUT` read as one blurry ratio (`57.2K/898`). Draw the counts as `IN - OUT` with IN pinned to the left edge of the column and OUT to the right, so the two values never collide: the separator lands in a fixed column and the output count right-aligns, even as the numbers change width between rows. Both halves are sized from the widest value on show, so the separator is placed correctly without a hardcoded budget that a long output count (`1.9K`, `24.3K`) could overrun. The sats column is now `COST` and spells out its unit per row (`3.00 sats`), right-aligned so the unit lines up; the column gained a cell to fit the widest value the formatter can emit.
This commit is contained in:
@@ -223,19 +223,22 @@ describe("renderRecent token bars", () => {
|
||||
expect(out).toContain(COLORS.green + "█".repeat(18) + COLORS.red + "█".repeat(2) + COLORS.reset);
|
||||
// No blue (output) segment: output is reported as the second number.
|
||||
expect(out).not.toContain(COLORS.blue + "█");
|
||||
expect(stripAnsi(out)).toContain("10.0K/2.5K");
|
||||
// IN and OUT are pinned to opposite ends of the column.
|
||||
expect(stripAnsi(out)).toContain("10.0K - 2.5K");
|
||||
expect(stripAnsi(out)).toContain("IN - OUT");
|
||||
// The bar is labelled `CACHE HIT` and no longer carries a legend line.
|
||||
expect(stripAnsi(out)).toContain("CACHE HIT");
|
||||
expect(stripAnsi(out)).not.toContain("bars:");
|
||||
});
|
||||
|
||||
test("shows only the total sats cost, not the input/output breakdown", () => {
|
||||
test("shows only the total cost in sats, not the input/output breakdown", () => {
|
||||
const out = stripAnsi(renderRecent(stats, 120, buildClientNaming([], [])));
|
||||
|
||||
expect(out).toContain("TOTAL SATS");
|
||||
expect(out).toContain("COST");
|
||||
// 3000 total msats -> 3.00 sats; the 1.00/2.00 input/output split is gone.
|
||||
expect(out).toContain("3.00");
|
||||
expect(out).not.toContain("1.00/2.00");
|
||||
expect(out).toContain("3.00 sats");
|
||||
expect(out).not.toContain("1.00 sats");
|
||||
expect(out).not.toContain("2.00 sats");
|
||||
});
|
||||
|
||||
test("keeps every row the same visible width as the box", () => {
|
||||
|
||||
+16
-8
@@ -682,9 +682,7 @@ export function renderRecent(stats: UsageStats, width: number, naming: ClientNam
|
||||
if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests");
|
||||
|
||||
const timeCol = 8;
|
||||
const costCol = 11;
|
||||
// Width reserved right of each token bar for `input/output` token counts.
|
||||
const inOutCol = 13;
|
||||
const costCol = 12;
|
||||
// Width reserved left of the token counts for the cache-hit bar. `CACHE HIT`
|
||||
// is the header drawn over the bar, so the bar can never be narrower.
|
||||
const minBarWidth = "CACHE HIT".length;
|
||||
@@ -702,6 +700,15 @@ export function renderRecent(stats: UsageStats, width: number, naming: ClientNam
|
||||
);
|
||||
const maxLabelLen = clientLabels.reduce((max, label) => Math.max(max, label.length), 6);
|
||||
|
||||
// Token counts are drawn as `IN - OUT` with IN pinned to the left edge of the
|
||||
// column and OUT to the right, so both halves need the widest value on show.
|
||||
const tokenRows = recentEntries.map((entry) => tokenSegments(entry));
|
||||
const inputTexts = tokenRows.map((row) => formatNumber(row.input));
|
||||
const outputTexts = tokenRows.map((row) => formatNumber(row.output));
|
||||
const inputCol = Math.max(...inputTexts.map((text) => text.length));
|
||||
const outputCol = Math.max(...outputTexts.map((text) => text.length));
|
||||
const inOutCol = inputCol + outputCol + " - ".length;
|
||||
|
||||
// Lay out the columns against the box's inner width: start from the widest
|
||||
// layout, hand the slack to the provider column, then give space back in
|
||||
// priority order (bar, provider, model, client) until everything fits.
|
||||
@@ -731,8 +738,8 @@ export function renderRecent(stats: UsageStats, width: number, naming: ClientNam
|
||||
const header = [
|
||||
"TIME".padEnd(timeCol),
|
||||
"MODEL".padEnd(modelCol),
|
||||
`${"CACHE HIT".padEnd(tokensCol - inOutCol)}${"IN/OUT".padStart(inOutCol)}`,
|
||||
"TOTAL SATS".padEnd(costCol),
|
||||
"CACHE HIT".padEnd(tokensCol - inOutCol) + "IN".padEnd(inputCol) + " - " + "OUT".padStart(outputCol),
|
||||
"COST".padEnd(costCol),
|
||||
...(showProvider ? ["BASE:PROVIDER".padEnd(providerCol)] : []),
|
||||
"CLIENT".padEnd(clientCol),
|
||||
];
|
||||
@@ -743,14 +750,15 @@ export function renderRecent(stats: UsageStats, width: number, naming: ClientNam
|
||||
const entry = recentEntries[i]!;
|
||||
const time = formatTime(entry.timestamp).slice(0, 8);
|
||||
const model = entry.modelId.slice(0, modelCol).padEnd(modelCol);
|
||||
const segments = tokenSegments(entry);
|
||||
const segments = tokenRows[i]!;
|
||||
const bar = renderStackedBar([
|
||||
{ value: segments.cacheRead, color: TOKEN_BAR_COLORS.cacheRead },
|
||||
{ value: segments.notCached, color: TOKEN_BAR_COLORS.notCached },
|
||||
], barWidth);
|
||||
const tokens = `${bar} ${`${formatNumber(segments.input)}/${formatNumber(segments.output)}`.padStart(inOutCol)}`;
|
||||
const tokens = `${bar} ${inputTexts[i]!.padEnd(inputCol)} - ${outputTexts[i]!.padStart(outputCol)}`;
|
||||
const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
|
||||
const cost = formatCost(totalSats);
|
||||
// Right-aligned so the `sats` unit ends at the same column on every row.
|
||||
const cost = `${formatCost(totalSats)} sats`.padStart(costCol);
|
||||
const baseUrl = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "");
|
||||
const provider = `${baseUrl}:${entry.provider || "unknown"}`.slice(0, providerCol).padEnd(providerCol);
|
||||
const clientLabel = clientLabels[i]!.slice(0, clientCol).padEnd(clientCol);
|
||||
|
||||
Reference in New Issue
Block a user