From 3abe2bf75e18f342365e628e2bac3283233d077b Mon Sep 17 00:00:00 2001 From: richard-epsilla Date: Sun, 25 Oct 2026 23:59:26 -0700 Subject: [PATCH] sheets: every cell wraps, and every row fits its content on load, edit and column resize The grid showed a long value as one line with an ellipsis, and the only rows that grew were the ones a run had just touched: the run measured them once and stored their heights. reifyui 0.8.4 wraps every cell and lets the table fit each row to its tallest cell, up to eight lines, past which the cell scrolls inside itself. So the kit stops doing that work. The run no longer writes row heights: a stored height now means a row someone sized, and it would stop the row refitting when its column is resized or its text edited. The layout effect that worked out a line clamp for every cell on every render is gone with the clamp. An agent cell's answer, and the live text of a running cell, wrap the way the grid's own text does. package.json and the lock take reifyui 0.8.4 (npm ci, npm test 64 of 64 and npm run build pass on node 22, the image's version). Co-Authored-By: Claude Opus 5.5 --- kits/sheets/app/package-lock.json | 8 +-- kits/sheets/app/package.json | 2 +- .../sheets/app/src/components/HarnessCell.jsx | 11 +-- kits/sheets/app/src/pages/SheetPage.jsx | 68 ++----------------- kits/sheets/app/src/styles/app.css | 11 ++- 5 files changed, 22 insertions(+), 78 deletions(-) diff --git a/kits/sheets/app/package-lock.json b/kits/sheets/app/package-lock.json index 0f44869..c47836e 100644 --- a/kits/sheets/app/package-lock.json +++ b/kits/sheets/app/package-lock.json @@ -13,7 +13,7 @@ "react-dom": "^18.3.1", "react-file-icon": "^1.6.0", "react-markdown": "^9.0.1", - "reifyui": "^0.8.3", + "reifyui": "^0.8.4", "remark-gfm": "^4.0.0", "xlsx": "^0.18.5" }, @@ -2957,9 +2957,9 @@ } }, "node_modules/reifyui": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/reifyui/-/reifyui-0.8.3.tgz", - "integrity": "sha512-oWsptZh6v4SMv9UnnsprsWPDcBYG2GDIaR694xRZchoK+qpXy9ZY0ZI7vw+RwdS7vlP0GIpHmMqmyn6fmPdH7A==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/reifyui/-/reifyui-0.8.4.tgz", + "integrity": "sha512-Vxf3C3unmXo41sy2oPMBT3JZ0CNBUHaA61/BjdTDJ9gewwSwti8SHVivtXcwCBCzGuWsLqxfsPAUo3H4oZbphQ==", "license": "MIT", "engines": { "node": ">=18" diff --git a/kits/sheets/app/package.json b/kits/sheets/app/package.json index e655a01..74c01ac 100644 --- a/kits/sheets/app/package.json +++ b/kits/sheets/app/package.json @@ -15,7 +15,7 @@ "react-dom": "^18.3.1", "react-file-icon": "^1.6.0", "react-markdown": "^9.0.1", - "reifyui": "^0.8.3", + "reifyui": "^0.8.4", "remark-gfm": "^4.0.0", "xlsx": "^0.18.5" }, diff --git a/kits/sheets/app/src/components/HarnessCell.jsx b/kits/sheets/app/src/components/HarnessCell.jsx index 36c9a9e..be3451c 100644 --- a/kits/sheets/app/src/components/HarnessCell.jsx +++ b/kits/sheets/app/src/components/HarnessCell.jsx @@ -1,7 +1,8 @@ // What an agent cell looks like in the grid. // -// Mounted through SheetGrid's renderCell slot, inside the cell's own value box, so the row-height -// line clamp and the cell chrome still apply. +// Mounted through SheetGrid's renderCell slot, inside the cell's own value box, so the cell chrome +// still applies and the row fits this cell the way it fits any other: the answer wraps, the row +// grows to it up to the grid's cap, and past the cap the cell scrolls. // // Every state here is a state the run genuinely produces. There is no percentage and no estimate: // the server exposes no queue — past its concurrency limit turns are accepted and block silently, @@ -81,7 +82,7 @@ export function HarnessCell({ cell, live, readOnly, onRun, onPreviewFile }) { return ( - {cell.error || 'This cell failed.'} + {cell.error || 'This cell failed.'} {tools} ); @@ -89,14 +90,14 @@ export function HarnessCell({ cell, live, readOnly, onRun, onPreviewFile }) { if (status === 'skipped') { return ( - {cell.error || 'Skipped.'} + {cell.error || 'Skipped.'} {tools} ); } return ( - + {tools} diff --git a/kits/sheets/app/src/pages/SheetPage.jsx b/kits/sheets/app/src/pages/SheetPage.jsx index 714e91a..bbf2f1c 100644 --- a/kits/sheets/app/src/pages/SheetPage.jsx +++ b/kits/sheets/app/src/pages/SheetPage.jsx @@ -8,12 +8,12 @@ // The run happens in this tab. There is no workflow engine and no batch endpoint in this // deployment, so the browser is the orchestrator; the UI says that before you press Run and says // exactly what happened if you leave. -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Download, HelpCircle, Home } from 'lucide-react'; import { PaneResizer, useResizablePane, useDialog } from 'reifyui'; -import { SheetGrid, FilePreview, fitRowHeights } from 'reifyui'; +import { SheetGrid, FilePreview } from 'reifyui'; import { containerFileUrl, getResponse, lastAssistantText, sessionTurns } from 'reifyui/harness'; import { defaultAgentId, getSheet, isPending, markViewed, renameSheet, runnableHarnesses, saveSheet, @@ -36,10 +36,6 @@ const SAVE_DEBOUNCE_MS = 400; const STATUS_POLL_MS = 2000; // while a turn is live: the grid fills in as it is written const STATUS_IDLE_MS = 10000; // while nothing is: still notices a turn started elsewhere const RELOAD_POLL_MS = 4000; -// The line height and vertical padding the grid lays a cell out with (reifyui's sheet.css sets -// line-height: 18px). They turn a measured height into a number of lines for --shg-clamp. -const CLAMP_LINE = 18; -const CLAMP_PAD = 10; const now = () => Math.floor(Date.now() / 1000); const escapeRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -106,7 +102,6 @@ export function SheetPage({ id: routeId, seed }) { idRef.current = id; const runnerRef = useRef(null); const exportRef = useRef(null); - const gridRef = useRef(null); const mirrored = useRef(''); // the session title we last asserted const save = useRef({ timer: null, inFlight: false, backoff: 0, wanted: null }); @@ -385,14 +380,10 @@ export function SheetPage({ id: routeId, seed }) { runnerRef.current = null; setRunning(false); - // The answers are on screen now, so their real height is measurable. Rows the run touched - // grow to fit what is in them — the text and the file cards — rather than staying at the - // height they had when they were empty. - const touched = [...new Set(p.cells.map((c) => c.rowId))]; - const heights = fitRowHeights(gridRef.current, { rowIds: touched }); - + // No row heights are written here. The grid fits every row to its tallest cell on each + // render, so an answer has its room the moment it lands; a height stored after the run + // would pin the row and stop it refitting when its column is resized or its text edited. const done = JSON.parse(JSON.stringify(sheetRef.current)); - for (const r of done.rows || []) if (heights[r.id]) r.height = heights[r.id]; if (done.run?.id === rid) { done.run = { ...done.run, @@ -404,53 +395,6 @@ export function SheetPage({ id: routeId, seed }) { } }, [env, concurrency, commit, dialog]); - // ── how many lines of text a cell has room for ──────────────────────────── - // The grid derives --shg-clamp from a row's STORED height, falling back to 34px — one line — - // for any row never explicitly sized. But a row is usually tall because of ONE column: an agent - // cell keeping its files under its answer. Every other cell then showed a single ellipsised - // line above a block of empty row, and the taller the neighbour, the more space went to waste. - // - // Three things have to be true at once, and each is a trap on its own: - // - // The basis cannot be the RENDERED height. More lines makes the row taller, which allows more - // lines: measured live the rows climb 92 → 137 → 182 and never settle. So every clamp goes - // back to one line before measuring, making the basis the height the row's OTHER content - // needs — a quantity the clamp cannot influence. - // - // The basis cannot be the . A table cell is stretched to its row and reports the row - // height for every column, which makes them all look equally full. The measurement is taken - // on the text's own box, which sizes to its content. - // - // The count cannot be shared by the row. A cell carrying files under its text has far less - // room than one carrying nothing, and one number for both is the bug restated. - // - // Set on each : a custom property resolves from the nearest ancestor, so this wins over the - // row-level value the grid sets, for both this kit's answer text and the grid's own value cell. - useLayoutEffect(() => { - const trs = gridRef.current?.querySelectorAll('tbody tr[data-row-id]'); - if (!trs?.length) return; - const rows = [...trs]; - for (const tr of rows) for (const td of tr.children) td.style.setProperty('--shg-clamp', '1'); - const plan = rows.map((tr) => ({ - base: tr.offsetHeight, // one reflow, every clamp at its floor - cells: [...tr.children].map((td) => { - const txt = td.querySelector('[data-shg-clamp-text]'); - const box = txt?.parentElement; - return { td, extra: box ? Math.max(0, box.scrollHeight - txt.offsetHeight) : 0 }; - }), - })); - for (const { base, cells } of plan) { - const floorLines = Math.max(1, Math.floor((base - CLAMP_PAD) / CLAMP_LINE)); - // The height the row needs for the cell carrying the most beside its text to still get the - // row's baseline count. Every other cell then fills THAT, which is the whole point. - const need = Math.max(...cells.map((c) => c.extra)) + floorLines * CLAMP_LINE + CLAMP_PAD; - for (const { td, extra } of cells) { - td.style.setProperty('--shg-clamp', String(Math.max(floorLines, - Math.floor((need - extra - CLAMP_PAD) / CLAMP_LINE)))); - } - } - }); - const stopRun = useCallback(() => { runnerRef.current?.stop(); }, []); // Leaving mid-run stops the walk. Say so with the platform's own guard rather than inventing a @@ -650,7 +594,7 @@ export function SheetPage({ id: routeId, seed }) { )} ) : ( -
+