From 323dab77414803b1afa638716fe6a989cdb37491 Mon Sep 17 00:00:00 2001 From: singularitti Date: Sun, 28 Jun 2026 02:14:35 -0500 Subject: [PATCH] fix: fill pane with last column and auto-fit worker-backed column widths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening a file whose columns don't fill the editor width — e.g. a 3-column JSONL such as a Codex rollout log (timestamp/type/payload) — showed a blank, headerless "phantom column" after the last real column. This was not a parsing bug and the file was not malformed: the grid parses exactly the right columns. The blank area is unused horizontal space. `.grid-header` and `.grid-row` are full-width flex containers (`.grid-sizer` is `min-width:100%`), so when the fixed-width cells don't fill the row the leftover width still carries the header background and each row's bottom border, which reads as an empty column. Two changes: - DataGrid.svelte: the last visible (non-frozen) column now `flex-grow`s to absorb the leftover width, so the grid always spans the pane with no phantom column. Cells keep `flex-shrink:0` and their px width acts as the flex-basis, so the column only ever grows past its natural width — when columns overflow the pane there is nothing to absorb and horizontal scroll behaves exactly as before. It is pure CSS, so it re-fits on window/pane resize for free and the header and body stay aligned. A frozen last column is skipped because sticky positioning and flex-grow don't combine cleanly. - grid.svelte.ts: worker-backed formats (JSON/Parquet/Arrow/Excel) never populate `_csvAllRows`, so the CSV/raw-rows auto-width pass never ran for them and their columns fell back to type-default widths. Fit column widths once from the first chunk on worker READY so they size to their content like CSV already does. Guarded by a flag (reset on each READY) so filtering or sorting, which re-requests chunk 0, doesn't re-fit. The fill behaviour applies to every file type, so narrow CSVs that previously showed the same trailing gap now fill the pane as well. Co-Authored-By: Claude Opus 4.8 --- webview-ui/src/components/DataGrid.svelte | 27 +++++++++++++++++++++++ webview-ui/src/stores/grid.svelte.ts | 14 ++++++++++++ 2 files changed, 41 insertions(+) diff --git a/webview-ui/src/components/DataGrid.svelte b/webview-ui/src/components/DataGrid.svelte index f5e9d50..b851c16 100644 --- a/webview-ui/src/components/DataGrid.svelte +++ b/webview-ui/src/components/DataGrid.svelte @@ -259,6 +259,19 @@ const selectedRange = $derived(gridStore.selectedRange); const frozenCols = $derived(gridStore.frozenCols); + // Visible position of the column that stretches to absorb any leftover + // horizontal space, so the grid spans the full pane with no blank + // "phantom column" on the right. We grow the last visible column unless it's + // frozen (sticky positioning + flex-grow don't mix cleanly). When the columns + // already overflow the pane, flex-grow has nothing to add and the column keeps + // its natural width (horizontal scroll kicks in as before). + const fillColPos = $derived.by(() => { + const cols = gridStore.visibleSchema; + const last = cols.length - 1; + if (last < 0) return -1; + return frozenCols.has(cols[last].index) ? -1 : last; + }); + // Returns the sticky `left` offset (in px) for a frozen column at visual position `colPos`. // Row-number column is 56px + 1px border = 57px. Sums widths of all frozen cols before this one. function getFrozenLeft(col: import('@shared/schema.js').ColumnSchema, colPos: number): number { @@ -465,6 +478,7 @@ class="header-cell" class:header-cell-selected={colSel} class:col-frozen={isFrozen} + class:col-fill={colPos === fillColPos} class:drag-over={dragOverPos === colPos && dragFromPos !== colPos} role="columnheader" tabindex="-1" @@ -519,6 +533,7 @@ onCellClick(e, row, col.index)} ondblclick={() => startEdit(row, col.index)} @@ -827,6 +843,17 @@ box-sizing: border-box; } + /* The last visible column stretches to absorb leftover horizontal space so the + grid fills the pane with no blank phantom column on the right. The inline + `width` acts as the flex-basis and flex-shrink stays 0, so the column only + ever grows past its natural width — when columns overflow the pane there's + nothing to absorb and it keeps its set width (horizontal scroll as before). */ + .header-cell.col-fill, + .cell.col-fill, + .cell-input.col-fill { + flex-grow: 1; + } + /* ── Coloured mode ────────────────────────────────────────────────────── When the user enables column colours, two things change: 1. Column separators get a slightly stronger border so the pastels diff --git a/webview-ui/src/stores/grid.svelte.ts b/webview-ui/src/stores/grid.svelte.ts index 1c6dc88..0b0848a 100644 --- a/webview-ui/src/stores/grid.svelte.ts +++ b/webview-ui/src/stores/grid.svelte.ts @@ -102,6 +102,11 @@ class GridStore { // Auto-fitted column widths, keyed by column name. Computed once after data load. private _autoWidths = new Map(); + // Guards the one-shot auto-width pass for worker-backed formats (JSON/Parquet/ + // Arrow/Excel), which never populate _csvAllRows and so miss the CSV/raw-rows + // auto-width path. Reset on every worker READY so a new dataset (or Excel sheet + // switch) re-fits its columns. + private _autoWidthsDone = false; // Viewport visibleStartRow = $state(0); @@ -1523,6 +1528,7 @@ class GridStore { this.schema = msg.payload.schema; this.totalRows = msg.payload.totalRows; this.filteredRows = msg.payload.totalRows; + this._autoWidthsDone = false; if (msg.payload.availableSheets) { this.availableSheets = msg.payload.availableSheets; this.selectedSheet = msg.payload.selectedSheet ?? msg.payload.availableSheets[0] ?? ''; @@ -1534,6 +1540,14 @@ class GridStore { case 'CHUNK': { const { requestId, rows, startRow, filteredTotal } = msg.payload; + // Worker-backed formats (JSON/Parquet/Arrow/Excel) never populate + // _csvAllRows, so the CSV/raw-rows auto-width path never runs for them. + // Fit widths once from the first chunk (natural order, no filter/sort on + // initial load) so columns size to their content like CSV does. + if (startRow === 0 && !this._autoWidthsDone && rows.length > 0 && this.schema.length > 0) { + this._computeAutoWidths(this.schema, rows); + this._autoWidthsDone = true; + } this._storeChunk(startRow, rows, requestId); this.filteredRows = filteredTotal; const resolve = this._pendingRequests.get(requestId);