mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(terminal): shrink widest flex table columns first (#123934)
This commit is contained in:
committed by
GitHub
parent
d8cd661517
commit
86f38f7c8f
@@ -22,6 +22,43 @@ function expectIntroducersToStartCompleteSequences(
|
||||
}
|
||||
}
|
||||
|
||||
const pluginListColumns = [
|
||||
{ key: "Name", header: "Name", minWidth: 14, flex: true },
|
||||
{ key: "ID", header: "ID", minWidth: 10, flex: true },
|
||||
{ key: "Format", header: "Format", minWidth: 9 },
|
||||
{ key: "Status", header: "Status", minWidth: 10 },
|
||||
{ key: "Source", header: "Source", minWidth: 26, flex: true },
|
||||
{ key: "Version", header: "Version", minWidth: 8 },
|
||||
];
|
||||
|
||||
const pluginListRows = [
|
||||
{
|
||||
Name: "Amazon Bedrock",
|
||||
ID: "amazon-bedrock",
|
||||
Format: "openclaw",
|
||||
Status: "enabled",
|
||||
Source: "~/Projects/openclaw/extensions/amazon-bedrock/index.ts",
|
||||
Version: "2026.8.1",
|
||||
},
|
||||
{
|
||||
Name: "N".repeat(40),
|
||||
ID: "i".repeat(22),
|
||||
Format: "openclaw",
|
||||
Status: "disabled",
|
||||
Source: `/${"s".repeat(80)}`,
|
||||
Version: "2026.8.1",
|
||||
},
|
||||
];
|
||||
|
||||
function renderPluginListTable(width: number): string {
|
||||
return renderTable({
|
||||
width,
|
||||
border: "unicode",
|
||||
columns: pluginListColumns,
|
||||
rows: pluginListRows,
|
||||
});
|
||||
}
|
||||
|
||||
describe("renderTable", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
@@ -60,6 +97,30 @@ describe("renderTable", () => {
|
||||
expect(out).toMatch(/[│|] Dashboard\s+[│|]/);
|
||||
});
|
||||
|
||||
it("keeps identifier columns intact when wider flex columns can absorb the shrink", () => {
|
||||
const out = renderPluginListTable(120);
|
||||
|
||||
expect(out).toMatch(/│ Amazon Bedrock\s+│ amazon-bedrock\s+│/);
|
||||
});
|
||||
|
||||
it.each([60, 80, 120, 160, 200])(
|
||||
"keeps the plugin-list shape deterministic and within %i columns",
|
||||
(width) => {
|
||||
const out = renderPluginListTable(width);
|
||||
const lines = out.trimEnd().split("\n");
|
||||
const headerCells = (lines[1] ?? "").split("│").slice(1, -1);
|
||||
|
||||
expect(out).toBe(renderPluginListTable(width));
|
||||
expect(Math.max(...lines.map(visibleWidth))).toBeLessThanOrEqual(width);
|
||||
expect(headerCells).toHaveLength(pluginListColumns.length);
|
||||
for (const [index, column] of pluginListColumns.entries()) {
|
||||
expect(visibleWidth(headerCells[index] ?? "")).toBeGreaterThanOrEqual(
|
||||
visibleWidth(column.header) + 2,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("expands flex columns to fill available width", () => {
|
||||
const width = 60;
|
||||
const out = renderTable({
|
||||
@@ -532,18 +593,19 @@ describe("renderTable", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps borders aligned when a narrow flex column receives wide content", () => {
|
||||
it("terminates with aligned borders when a flex column starts at its floor", () => {
|
||||
const out = renderTable({
|
||||
width: 10,
|
||||
border: "ascii",
|
||||
columns: [
|
||||
{ key: "A", header: "long header here" },
|
||||
{ key: "B", header: "", flex: true },
|
||||
{ key: "B", header: "", flex: true, maxWidth: 3 },
|
||||
],
|
||||
rows: [{ A: "data", B: "📸" }],
|
||||
});
|
||||
const lines = out.trimEnd().split("\n");
|
||||
const headerWidth = visibleWidth(lines[0] ?? "");
|
||||
expect(headerWidth).toBeGreaterThan(10);
|
||||
for (const line of lines) {
|
||||
expect(visibleWidth(line)).toBe(headerWidth);
|
||||
}
|
||||
|
||||
@@ -601,45 +601,37 @@ export function renderTable(opts: RenderTableOptions): string {
|
||||
if (maxWidth && total > maxWidth) {
|
||||
let over = total - maxWidth;
|
||||
|
||||
const flexOrder = columns
|
||||
.map((_c, i) => ({ i, w: widths[i] ?? 0 }))
|
||||
.filter(({ i }) => Boolean(columns[i]?.flex))
|
||||
.toSorted((a, b) => b.w - a.w)
|
||||
.map((x) => x.i);
|
||||
const flexColumns = columns.flatMap((column, i) => (column.flex ? [i] : []));
|
||||
const nonFlexColumns = columns.flatMap((column, i) => (column.flex ? [] : [i]));
|
||||
|
||||
const nonFlexOrder = columns
|
||||
.map((_c, i) => ({ i, w: widths[i] ?? 0 }))
|
||||
.filter(({ i }) => !columns[i]?.flex)
|
||||
.toSorted((a, b) => b.w - a.w)
|
||||
.map((x) => x.i);
|
||||
|
||||
const shrink = (order: number[], minWidths: number[]) => {
|
||||
const shrink = (indices: number[], minWidths: number[]) => {
|
||||
while (over > 0) {
|
||||
let progressed = false;
|
||||
for (const i of order) {
|
||||
let widest: number | undefined;
|
||||
for (const i of indices) {
|
||||
if ((widths[i] ?? 0) <= (minWidths[i] ?? 0)) {
|
||||
continue;
|
||||
}
|
||||
widths[i] = (widths[i] ?? 0) - 1;
|
||||
over -= 1;
|
||||
progressed = true;
|
||||
if (over <= 0) {
|
||||
break;
|
||||
// Water-fill from the widest eligible column. Strict comparison makes
|
||||
// equal-width ties deterministic: the leftmost column shrinks first.
|
||||
if (widest === undefined || (widths[i] ?? 0) > (widths[widest] ?? 0)) {
|
||||
widest = i;
|
||||
}
|
||||
}
|
||||
if (!progressed) {
|
||||
if (widest === undefined) {
|
||||
break;
|
||||
}
|
||||
widths[widest] = (widths[widest] ?? 0) - 1;
|
||||
over -= 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Prefer shrinking flex columns; only shrink non-flex if necessary.
|
||||
// If required to fit, allow flex columns to shrink below user minWidth
|
||||
// down to their absolute minimum (header + padding).
|
||||
shrink(flexOrder, preferredMinWidths);
|
||||
shrink(flexOrder, absoluteMinWidths);
|
||||
shrink(nonFlexOrder, preferredMinWidths);
|
||||
shrink(nonFlexOrder, absoluteMinWidths);
|
||||
shrink(flexColumns, preferredMinWidths);
|
||||
shrink(flexColumns, absoluteMinWidths);
|
||||
shrink(nonFlexColumns, preferredMinWidths);
|
||||
shrink(nonFlexColumns, absoluteMinWidths);
|
||||
}
|
||||
|
||||
// If we have room and any flex columns, expand them to fill the available width.
|
||||
|
||||
Reference in New Issue
Block a user