Skip to content

Commit 9cec19d

Browse files
authored
Merge pull request #270 from slkiser/fix/quota-sidebar-readability
fix: improve quota sidebar readability
2 parents fa9e66b + f3923db commit 9cec19d

6 files changed

Lines changed: 74 additions & 31 deletions

File tree

src/lib/toast-format-grouped.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,18 @@ export function formatQuotaRowsGrouped(params: {
131131
: [groupHeader.slice(0, maxWidth)]),
132132
);
133133

134+
let renderedEntryBlocks = 0;
134135
for (const entry of list) {
136+
let entryBlockStarted = false;
137+
const pushEntryLine = (line: string) => {
138+
if (!entryBlockStarted) {
139+
if (renderedEntryBlocks > 0) lines.push("");
140+
renderedEntryBlocks++;
141+
entryBlockStarted = true;
142+
}
143+
lines.push(line);
144+
};
145+
135146
const interpretation = interpretAccountingRow(entry, {
136147
booleanWording: "semantic",
137148
...(!isTiny
@@ -170,24 +181,24 @@ export function formatQuotaRowsGrouped(params: {
170181
labelAndValue.length <= maxWidth &&
171182
labelAndValue.length + separator.length + timeStr.length > maxWidth
172183
) {
173-
lines.push(labelAndValue);
174-
lines.push(padLeft(timeStr, maxWidth));
184+
pushEntryLine(labelAndValue);
185+
pushEntryLine(padLeft(timeStr, maxWidth));
175186
continue;
176187
}
177188

178189
if (isAtomicValue) {
179190
const suffix = [value, timeStr].filter(Boolean).join(separator);
180191
if (suffix.length > maxWidth) {
181-
if (value.length <= maxWidth) lines.push(padLeft(value, maxWidth));
192+
if (value.length <= maxWidth) pushEntryLine(padLeft(value, maxWidth));
182193
continue;
183194
}
184195
const availableLabelWidth = maxWidth - (suffix ? separator.length + suffix.length : 0);
185196
if (availableLabelWidth <= 0) {
186-
lines.push(padLeft(suffix, maxWidth));
197+
pushEntryLine(padLeft(suffix, maxWidth));
187198
continue;
188199
}
189200
const leftText = label.slice(0, availableLabelWidth).trimEnd();
190-
lines.push(
201+
pushEntryLine(
191202
`${padRight(leftText, availableLabelWidth)}${suffix ? `${separator}${suffix}` : ""}`,
192203
);
193204
continue;
@@ -207,7 +218,7 @@ export function formatQuotaRowsGrouped(params: {
207218
padLeft(timeStr, timeWidth),
208219
padLeft(value, valueCol),
209220
].join(separator);
210-
lines.push(line.slice(0, maxWidth));
221+
pushEntryLine(line.slice(0, maxWidth));
211222
continue;
212223
}
213224

@@ -218,7 +229,7 @@ export function formatQuotaRowsGrouped(params: {
218229
1,
219230
barWidth - separator.length - valueWidth - separator.length - timeWidth,
220231
);
221-
lines.push(
232+
pushEntryLine(
222233
(
223234
padRight(leftText, leftMax) +
224235
separator +
@@ -260,7 +271,7 @@ export function formatQuotaRowsGrouped(params: {
260271
: "";
261272
const runway = isPercentEntry(entry) ? formatQuotaRunway(entry.runway) : "";
262273
const addRunwayLine = () => {
263-
if (runway) lines.push(`Runs out ${runway}`.slice(0, maxWidth));
274+
if (runway) pushEntryLine(`Runs out ${runway}`.slice(0, maxWidth));
264275
};
265276

266277
if (isTiny) {
@@ -277,7 +288,7 @@ export function formatQuotaRowsGrouped(params: {
277288
padLeft(timeStr, timeWidth),
278289
padLeft(visibleBarSuffix, percentValueCol),
279290
].join(separator);
280-
lines.push(line.slice(0, maxWidth));
291+
pushEntryLine(line.slice(0, maxWidth));
281292
addRunwayLine();
282293
continue;
283294
}
@@ -290,7 +301,7 @@ export function formatQuotaRowsGrouped(params: {
290301
padLeft(timeStr, timeWidth),
291302
padLeft(visibleBarSuffix, percentValueCol),
292303
].join(separator);
293-
lines.push(line.slice(0, maxWidth));
304+
pushEntryLine(line.slice(0, maxWidth));
294305
addRunwayLine();
295306
continue;
296307
}
@@ -305,23 +316,23 @@ export function formatQuotaRowsGrouped(params: {
305316
const rightText = parts.slice(1).join(" ");
306317
const sep = " ";
307318
const leftWidth = Math.max(1, maxWidth - sep.length - rightText.length);
308-
lines.push((padRight(left, leftWidth) + sep + rightText).slice(0, maxWidth));
319+
pushEntryLine((padRight(left, leftWidth) + sep + rightText).slice(0, maxWidth));
309320
} else {
310-
lines.push(padLeft(text, maxWidth));
321+
pushEntryLine(padLeft(text, maxWidth));
311322
}
312323
} else {
313324
// Line 1: label + time at end
314325
const timeWidth = Math.max(timeStr.length, timeCol);
315326
const leftMax = Math.max(1, maxWidth - separator.length - timeWidth);
316-
lines.push(
327+
pushEntryLine(
317328
(padRight(label, leftMax) + separator + padLeft(timeStr, timeWidth)).slice(0, maxWidth),
318329
);
319330
}
320331

321332
// Line 2: bar + percent
322333
const barCell = bar(displayedPercent, barWidth);
323334
const suffixCell = padLeft(percentLabel.slice(0, percentValueCol), percentValueCol);
324-
lines.push([barCell, suffixCell].join(separator));
335+
pushEntryLine([barCell, suffixCell].join(separator));
325336
addRunwayLine();
326337

327338
if (interpretation.basis) {
@@ -337,7 +348,7 @@ export function formatQuotaRowsGrouped(params: {
337348
if (next.length > maxWidth) break;
338349
detailLine = next;
339350
}
340-
if (detailLine) lines.push(detailLine);
351+
if (detailLine) pushEntryLine(detailLine);
341352
}
342353
}
343354
}

src/lib/tui-line-style.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
22

3-
import { SESSION_TOKEN_SECTION_HEADING } from "./session-tokens-format.js";
4-
53
export function getSidebarBodyLineColor(
64
line: string,
75
theme: Pick<TuiPluginApi["theme"]["current"], "text" | "textMuted">,
86
): TuiPluginApi["theme"]["current"]["text"] | TuiPluginApi["theme"]["current"]["textMuted"] {
9-
return line.length > 0 && SESSION_TOKEN_SECTION_HEADING.startsWith(line)
10-
? theme.text
11-
: theme.textMuted;
7+
return line.trim().length === 0 || /[]/u.test(line) ? theme.textMuted : theme.text;
128
}

src/tui.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ function SidebarContentView(props: {
389389

390390
return (
391391
<Show when={shouldRenderSidebarPanel(panel())}>
392-
<box gap={0}>
392+
<box gap={hasDetailLines() ? 1 : 0}>
393393
<box flexDirection="row">
394394
<text fg={props.api.theme.current.text} onMouseDown={toggleCollapsed}>
395395
<b>{headerText()}</b>

tests/format.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,32 @@ describe("formatQuotaRows", () => {
409409
expect(out).not.toContain("→ ");
410410
});
411411

412+
it("separates grouped percent, value, and provider blocks with single blank lines", () => {
413+
const out = formatQuotaRows({
414+
version: "1.0.0",
415+
style: "allWindows",
416+
layout: { maxWidth: 50, narrowAt: 42, tinyAt: 32 },
417+
entries: [
418+
{ name: "Alpha", group: "Alpha", label: "Five-hour:", percentRemaining: 100 },
419+
{ name: "Alpha", group: "Alpha", label: "Weekly:", percentRemaining: 60 },
420+
{ name: "Alpha", group: "Alpha", label: "Balance:", kind: "value", value: "USD 12.00" },
421+
{ name: "Beta", group: "Beta", label: "Daily:", percentRemaining: 75 },
422+
],
423+
});
424+
425+
const lines = out.split("\n");
426+
const blankIndexes = lines.flatMap((line, index) => (line === "" ? [index] : []));
427+
428+
expect(blankIndexes).toEqual([3, 6, 8]);
429+
expect(lines[0]).toBe("[Alpha]");
430+
expect(lines[4]).toMatch(/^Weekly/u);
431+
expect(lines[7]).toMatch(/^Balance/u);
432+
expect(lines[9]).toBe("[Beta]");
433+
expect(lines[10]).toMatch(/^Daily/u);
434+
expect(lines.at(-1)).toMatch(/[]/u);
435+
expect(out).not.toContain("\n\n\n");
436+
});
437+
412438
it("renders a structured quantity as an atomic value row without a bar", () => {
413439
const out = formatQuotaRows({
414440
version: "1.0.0",

tests/tui-line-style.test.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,24 @@ describe("getSidebarBodyLineColor", () => {
99
textMuted: "gray",
1010
};
1111

12-
it("uses normal text color for the sidebar session-token heading", () => {
13-
expect(getSidebarBodyLineColor(SESSION_TOKEN_SECTION_HEADING, theme)).toBe("white");
12+
it.each([
13+
"[OpenCode Go] (personal account)",
14+
"personal account label continuation",
15+
"Five-hour 2h0m",
16+
"Current balance USD 42.50",
17+
"Runs out 1d 4h",
18+
SESSION_TOKEN_SECTION_HEADING,
19+
SESSION_TOKEN_SECTION_HEADING.slice(0, 18),
20+
])("uses normal text color for readable sidebar text: %s", (line) => {
21+
expect(getSidebarBodyLineColor(line, theme)).toBe("white");
1422
});
1523

16-
it("keeps the heading highlighted when the rendered sidebar heading is width-clamped", () => {
17-
expect(getSidebarBodyLineColor(SESSION_TOKEN_SECTION_HEADING.slice(0, 18), theme)).toBe(
18-
"white",
19-
);
20-
});
21-
22-
it("keeps non-heading sidebar lines muted", () => {
23-
expect(getSidebarBodyLineColor("Unavailable", theme)).toBe("gray");
24+
it.each([
25+
"",
26+
" ",
27+
"█████████████░░░░░░░░░░░░ 50% left",
28+
"░░░░░░░░░░ 0% left",
29+
])("keeps blank separators and progress bars muted: %s", (line) => {
30+
expect(getSidebarBodyLineColor(line, theme)).toBe("gray");
2431
});
2532
});

tests/tui-smoke.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,7 @@ describe("tui plugin smoke", () => {
11401140
{ session_id: "session-1" },
11411141
) as any;
11421142
const collapsedHeader = collapsed.props.children[0];
1143+
expect(collapsed.props.gap).toBe(1);
11431144
expect(collapsedHeader.props.children[0].props.children.props.children).toBe("▶ Quota");
11441145
expect(collapsedHeader.props.children[1].props.children).toEqual([" (", 2, " providers)"]);
11451146
expect(
@@ -1155,6 +1156,7 @@ describe("tui plugin smoke", () => {
11551156
{ session_id: "session-1" },
11561157
) as any;
11571158
const expandedHeader = expanded.props.children[0];
1159+
expect(expanded.props.gap).toBe(1);
11581160
expect(expandedHeader.props.children[0].props.children.props.children).toBe("▼ Quota");
11591161
expect(
11601162
expanded.props.children[1].props.children.map((line: any) => line.props.children),
@@ -1242,6 +1244,7 @@ describe("tui plugin smoke", () => {
12421244
{ session_id: "session-1" },
12431245
) as any;
12441246
const header = rendered.props.children[0];
1247+
expect(rendered.props.gap).toBe(0);
12451248
expect(header.props.children[0].props.children.props.children).toBe("Quota");
12461249
expect(rendered.props.children[1].props.children[0].props.children).toBe("Unavailable");
12471250
});

0 commit comments

Comments
 (0)