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 }) { )} ) : ( -
+