From de32e42e3534cbc4497c801622d7802f7a2ddc56 Mon Sep 17 00:00:00 2001 From: Mac Anderson Date: Thu, 6 Aug 2026 22:15:46 -0700 Subject: [PATCH 1/3] feat(stella-observatory): inspect a turn on its own page, not in a drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The execution drawer was the wrong container for what it held. A reconstructed prompt and a prompt diff are the widest text this product produces, and it gave them min(680px, 94vw) at 12px, reached by clicking an unmarked table row, with the diff two disclosures deep and no URL to reload or share. Inspecting a turn is now a route: `#transcript/`, a full-width page with a sticky section rail, the type one step up the scale, prev/next through the owning session's turns, and the newest call's prompt diff open on arrival. The diff itself is drawn the way a reviewer reads one — both line-number gutters, tinted rows rather than colored glyphs alone. The descent that led there is made visible too: the sessions and turn tables carried no cursor, no hover, and a `sel` class no stylesheet defined, while the panel a click revealed opened below the fold. Every payload is unchanged — the same four endpoints, the same lazy per-call reconstruction. `/api/execution` gains one field, `session_id`, without which a transcript reached by URL alone cannot name its session or step to the turns either side of it. --- .../stella-observatory/src/assets/index.html | 953 +++++++++++------- crates/stella-observatory/src/db.rs | 9 +- crates/stella-observatory/src/tests.rs | 81 +- 3 files changed, 692 insertions(+), 351 deletions(-) diff --git a/crates/stella-observatory/src/assets/index.html b/crates/stella-observatory/src/assets/index.html index 6c115303..4af7a69f 100644 --- a/crates/stella-observatory/src/assets/index.html +++ b/crates/stella-observatory/src/assets/index.html @@ -195,6 +195,17 @@ td.wrap-txt{white-space:normal} tr.click,tr.drill{cursor:pointer} tr.click:hover td,tr.drill:hover td,tr.click:focus-visible td,tr.drill:focus-visible td{background:var(--accent-wash)} +/* A selected row was being marked with a class no stylesheet defined, so the + sessions list looked identical before and after a click while a whole + detail card appeared below the fold. Selection is a gold edge plus a wash — + never hue alone, and never invisible. */ +tbody tr.sel > td{background:var(--accent-wash);color:var(--text)} +tbody tr.sel > td:first-child{box-shadow:inset 2px 0 0 var(--accent)} +/* The affordance that says a row opens something. Gold, right-aligned, and + present on every row rather than revealed on hover: the reason the turn + list read as inert is that nothing on it looked like a link. */ +td.go{color:var(--gold);text-align:right} +tr:hover td.go{text-decoration:underline} .scroll-x{overflow-x:auto} .scroll-y{max-height:340px;overflow-y:auto} .scroll-y-tall{max-height:520px;overflow-y:auto} @@ -281,37 +292,125 @@ box-shadow:0 6px 20px rgba(5,5,6,.6)} #tip b{color:var(--text);display:block;margin-bottom:2px;white-space:normal;word-break:break-all} -/* ── Drawer ───────────────────────────────────────────────────────────── */ -#drawer{position:fixed;inset:0 0 0 auto;width:min(680px,94vw);background:var(--surface); - border-left:1px solid var(--hairline-strong);transform:translateX(102%); - transition:transform .24s cubic-bezier(.2,.8,.2,1);z-index:40;overflow-y:auto; - padding:var(--sp3);visibility:hidden} -#drawer.open{transform:none;visibility:visible} -#scrim{position:fixed;inset:0;background:rgba(5,5,6,.66);opacity:0;pointer-events:none; - transition:opacity .24s;z-index:30} -#scrim.open{opacity:1;pointer-events:auto} -#drawer .close{position:absolute;top:18px;right:18px;background:none; - border:1px solid var(--control-edge);border-radius:var(--radius-sm); - color:var(--text-2);font:var(--fs-micro)/1.4 var(--mono);padding:5px 10px;cursor:pointer} -#drawer .close:hover{color:var(--text);border-color:var(--accent)} -#drawer h2{font:600 var(--fs-md)/1.4 var(--mono);margin:6px 0 2px;max-width:85%} -#drawer .sect{margin-top:var(--sp3)} -#drawer .jrnl{margin:10px 0;border-left:2px solid var(--hairline);padding-left:10px} -#drawer .jrnl.err{border-left-color:var(--bad)} -#drawer .jrnl-body{font:var(--fs-sm)/1.5 var(--mono);color:var(--text-2);margin:4px 0 0; - white-space:pre-wrap;word-break:break-word;max-height:320px;overflow-y:auto} -#drawer .jrnl-stage{font:var(--fs-micro)/1.4 var(--mono);color:var(--text-3); - text-transform:uppercase;letter-spacing:.08em;margin:12px 0 2px} -#drawer .jrnl-full{background:none;border:1px solid var(--hairline);color:var(--text-2); +/* ── Transcript page ────────────────────────────────────────────────────── + One turn, inspected, as a route of its own (`#transcript/`) + rather than an overlay. It replaced a 680px right-hand drawer: a prompt + diff and a system prompt are the widest text this product produces, and + the drawer gave them a third of the viewport, two nested disclosures deep, + with no address you could reload or share. Everything here follows from + that — full page width, the type one step up the scale, and the sections a + sticky rail jumps between instead of a single scroll to hunt through. */ +.tx-head{border-bottom:1px solid var(--hairline-strong);padding-bottom:var(--sp2); + margin-bottom:var(--sp3)} +.tx-crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:6px; + font:var(--fs-micro)/1.6 var(--mono);color:var(--text-3);margin-bottom:var(--sp1)} +.tx-crumbs .sep{color:var(--hairline-strong)} +.tx-back{background:none;border:1px solid var(--control-edge);border-radius:var(--radius-sm); + color:var(--text-2);font:var(--fs-micro)/1.4 var(--mono);padding:4px 10px;cursor:pointer} +.tx-back:hover{color:var(--text);border-color:var(--accent)} +.tx-prompt{font:600 var(--fs-md)/1.5 var(--mono);color:var(--text);max-width:92ch; + white-space:pre-wrap;word-break:break-word;margin:2px 0 var(--sp1)} +.tx-meta{display:flex;flex-wrap:wrap;align-items:center;gap:6px; + font:var(--fs-sm)/1.6 var(--mono);color:var(--text-3)} +.tx-steps{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp1);margin-top:var(--sp2)} +.tx-step{background:none;border:1px solid var(--control-edge);border-radius:var(--radius-sm); + color:var(--text-2);font:var(--fs-sm)/1.4 var(--mono);padding:5px 12px;cursor:pointer} +.tx-step:hover:not(:disabled){color:var(--text);border-color:var(--accent)} +.tx-step:disabled{color:var(--hairline-strong);border-color:var(--hairline);cursor:default} +.tx-cols{display:grid;grid-template-columns:200px minmax(0,1fr);gap:var(--sp3);align-items:start} +@media(max-width:980px){.tx-cols{grid-template-columns:minmax(0,1fr)} + .tx-rail{position:static!important;max-height:none!important}} +.tx-rail{position:sticky;top:56px;display:flex;flex-direction:column;gap:1px; + max-height:calc(100vh - 80px);overflow-y:auto} +.tx-rail a{display:flex;justify-content:space-between;gap:var(--sp1);text-decoration:none; + color:var(--text-2);font:var(--fs-sm)/1.5 var(--mono);padding:7px 11px; + border-left:2px solid transparent;border-radius:0 var(--radius-sm) var(--radius-sm) 0} +.tx-rail a:hover{color:var(--text);background:var(--accent-wash)} +.tx-rail a .n{color:var(--text-3);font-variant-numeric:tabular-nums} +.tx-rail a.on{color:var(--text);border-left-color:var(--accent);background:var(--accent-wash)} +/* Each section is a card so the page reads as parts, not one long scroll. + `scroll-margin-top` clears the sticky nav bar when the rail jumps here. */ +.tx-sect{background:var(--surface);border:1px solid var(--hairline-strong); + border-radius:var(--radius);padding:var(--sp2) var(--sp2) 18px; + margin-bottom:var(--sp2);scroll-margin-top:64px;min-width:0} +.tx-sect > h2{font:600 var(--fs-md)/1.3 var(--mono);letter-spacing:-.005em;margin-bottom:4px} +.tx-sect > .kick{margin-bottom:var(--sp2)} + +/* ── Transcript entries (shared with the sessions turn-diff rows) ─────── */ +.jrnl{margin:10px 0;border-left:2px solid var(--hairline);padding-left:12px} +.jrnl.err{border-left-color:var(--bad)} +/* One step up the scale from the drawer's 12px, with the line-height of + something meant to be read rather than glanced at. The clip stays — a + 200KB tool result must not own the page — but at page width it is a + scroll box, not a keyhole. */ +.jrnl-body{font:var(--fs-base)/1.65 var(--mono);color:var(--text-2);margin:5px 0 0; + white-space:pre-wrap;word-break:break-word;max-height:520px;overflow-y:auto; + background:var(--sunken);border:1px solid var(--hairline);border-radius:var(--radius-sm); + padding:10px 12px} +.jrnl-stage{font:var(--fs-micro)/1.4 var(--mono);color:var(--text-3); + text-transform:uppercase;letter-spacing:.08em;margin:18px 0 2px} +.jrnl-full{background:none;border:1px solid var(--control-edge);color:var(--text-2); font:var(--fs-micro)/1 var(--mono);padding:6px 10px;border-radius:var(--radius-sm); cursor:pointer;margin-top:8px} -#drawer .jrnl-full:hover{color:var(--text);border-color:var(--accent)} -/* The same button, sitting inside a step row rather than under a section. */ -#drawer .ctx-call{margin-top:0;padding:3px 8px} +.jrnl-full:hover{color:var(--text);border-color:var(--accent)} + +/* ── Context-receipt call list ──────────────────────────────────────────── + One row per recorded model call, each row a control: the two buttons pick + what the panel below shows, and the picked one stays lit. The drawer drew + these as anonymous rows whose output appeared in a shared box far below + them, so nothing said which row you were looking at. */ +.ctx-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp1); + padding:9px 11px;border:1px solid var(--hairline);border-radius:var(--radius-sm); + margin-bottom:6px;font:var(--fs-sm)/1.5 var(--mono);color:var(--text-2)} +.ctx-row.on{border-color:var(--accent-edge);background:var(--accent-wash)} +.ctx-row .who{color:var(--text);font-weight:600} +.ctx-row .spacer{margin-left:auto} +.ctx-row button{margin-top:0;padding:4px 10px;font:var(--fs-micro)/1.4 var(--mono)} +.ctx-row button.on{color:var(--ground);background:var(--accent);border-color:var(--accent); + font-weight:600} +#ctx-body{margin-top:var(--sp2)} +#ctx-body:empty{display:none} +.ctx-verdict{font:var(--fs-sm)/1.6 var(--mono);margin-bottom:var(--sp1)} + +/* ── Unified diff ───────────────────────────────────────────────────────── + Git's shape, drawn the way a reviewer expects to read it: old and new line + numbers in their own gutters, the sigil in a third, and the change carried + by a tinted row rather than by colored glyphs alone — hue is never the + only signal (BRAND.md). `minmax(max-content,1fr)` is what lets a long + line scroll the block horizontally while the row tint still paints the + full width. */ +.dx{overflow-x:auto;border:1px solid var(--hairline);border-radius:var(--radius-sm); + background:var(--sunken)} +.dx-hunk{min-width:100%} +.dx-hunk + .dx-hunk{border-top:1px solid var(--hairline)} +.dx-at{font:var(--fs-micro)/1.8 var(--mono);color:var(--text-3);background:var(--raised); + padding:3px 12px;white-space:pre;border-bottom:1px solid var(--hairline)} +.dx-line{display:grid;grid-template-columns:52px 52px 22px minmax(max-content,1fr); + font:var(--fs-base)/1.6 var(--mono);color:var(--text-2)} +.dx-line > span{padding:0 6px;white-space:pre} +.dx-line .no,.dx-line .nn{color:var(--text-3);text-align:right;font-size:var(--fs-micro); + font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none} +.dx-line .sg{text-align:center;-webkit-user-select:none;user-select:none} +.dx-line .tx{padding-right:var(--sp2)} +.dx-line.add{background:rgba(74,222,128,.10);color:var(--text)} +.dx-line.add .sg{color:var(--ok)} +.dx-line.rem{background:rgba(255,92,122,.10);color:var(--text)} +.dx-line.rem .sg{color:var(--bad)} +.dx-tally{font:600 var(--fs-md)/1.4 var(--mono);margin:0 0 4px} +.dx-tally .plus{color:var(--ok)} +.dx-tally .minus{color:var(--bad)} +.dx-scope{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp1);margin-bottom:var(--sp1)} + .step-row{display:grid;grid-template-columns:34px minmax(0,1fr) 160px;gap:var(--sp2); align-items:center;font:var(--fs-micro)/1.6 var(--mono);color:var(--text-3);padding:3px 0} .step-row .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .step-row > span:last-child{text-align:right} +/* The step/tool/file rows carry 11px in a card beside a chart, where they are + scanned. On the transcript page they ARE the content, so they take the same + step up the scale as everything else here. */ +.tx-sect .step-row{font-size:var(--fs-sm);color:var(--text-2);padding:4px 0; + border-bottom:1px solid var(--hairline)} +.tx-sect .step-row:last-child{border-bottom:0} .tokbar{display:flex;height:10px;border-radius:0 2px 2px 0;overflow:hidden;gap:2px} .tokbar i{display:block;height:100%} /* The per-turn diff affordance in the sessions turns table (#1870). */ @@ -365,6 +464,12 @@

observatory

+ + @@ -393,7 +498,8 @@

observatory

-
Every session of this workspace · registry + store · select one
+
Every session of this workspace · registry + store · + select one to list its turns below

Sessions

@@ -401,7 +507,9 @@

Sessions

Nothing selected

Session replay

-
Turn by turn · oldest first · select a turn to replay it
+
Turn by turn · oldest first · + open a turn for its full transcript, the context each model call was + sent, and what changed in the prompt between calls
+ +
+
+
+ +
+
+
+
@@ -730,8 +848,6 @@

Stores & databases

stella · verified done, not claimed done · dashboard refreshes every 5s while visible
-
-