Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions kits/sheets/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion kits/sheets/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
11 changes: 6 additions & 5 deletions kits/sheets/app/src/components/HarnessCell.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -81,22 +82,22 @@ export function HarnessCell({ cell, live, readOnly, onRun, onPreviewFile }) {
return (
<span className="hcell failed" title={cell.error || ''}>
<span className="hcell-dot failed" />
<span className="hcell-txt" data-shg-clamp-text>{cell.error || 'This cell failed.'}</span>
<span className="hcell-txt">{cell.error || 'This cell failed.'}</span>
{tools}
</span>
);
}
if (status === 'skipped') {
return (
<span className="hcell skipped" title={cell.error || ''}>
<span className="hcell-txt" data-shg-clamp-text>{cell.error || 'Skipped.'}</span>
<span className="hcell-txt">{cell.error || 'Skipped.'}</span>
{tools}
</span>
);
}
return (
<span className="hcell">
<span className="hcell-txt" data-shg-clamp-text><AnswerText value={cell.value} /></span>
<span className="hcell-txt"><AnswerText value={cell.value} /></span>
<Artifacts artifacts={cell.artifacts} onPreview={onPreviewFile} />
{tools}
</span>
Expand Down
68 changes: 6 additions & 62 deletions kits/sheets/app/src/pages/SheetPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, '\\$&');
Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -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,
Expand All @@ -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 <td>. 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 <td>: 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
Expand Down Expand Up @@ -650,7 +594,7 @@ export function SheetPage({ id: routeId, seed }) {
)}
</div>
) : (
<div className="sh-grid" ref={gridRef}>
<div className="sh-grid">
<SheetGrid
sheet={sheet}
onChange={onGridChange}
Expand Down
11 changes: 5 additions & 6 deletions kits/sheets/app/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,15 @@
took a fixed slice of every cell and the answer was ellipsised to pay for it, in every row,
whether that row produced a file or not. */
.hcell { display: flex; flex-direction: column; align-items: stretch; gap: 5px; min-width: 0; width: 100%; }
.hcell-txt {
min-width: 0; overflow: hidden; text-overflow: ellipsis;
display: -webkit-box; -webkit-box-orient: vertical;
-webkit-line-clamp: var(--shg-clamp, 1); line-clamp: var(--shg-clamp, 1);
}
/* The answer in full, the way the grid draws its own text: its line breaks kept, a long word or
address broken at the cell's edge, on the grid's 18px line. The grid's value box caps the
cell's height and scrolls past it, so nothing here clamps or ellipsises. */
.hcell-txt { min-width: 0; line-height: 18px; white-space: pre-wrap; overflow-wrap: anywhere; }
.hcell-link { font-size: 12px; color: var(--brand-ink); text-decoration: none; }
.hcell-link:hover { text-decoration: underline; }
.hcell.failed .hcell-txt { color: #B91C1C; }
.hcell.skipped .hcell-txt { color: var(--mute-2); font-style: italic; }
.hcell-live { color: var(--mute); font-size: 12px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hcell-live { color: var(--mute); font-size: 12px; line-height: 18px; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; }

.hcell-dot { width: 7px; height: 7px; border-radius: 999px; flex: none; margin-top: 5px; }
.hcell-dot.queued { background: var(--mute-2); }
Expand Down
Loading