Skip to content

Scale Lion Studio frontend views and interactions - #3385

Open
ohdearquant wants to merge 24 commits into
mainfrom
feat/studio-frontend-scale
Open

Scale Lion Studio frontend views and interactions#3385
ohdearquant wants to merge 24 commits into
mainfrom
feat/studio-frontend-scale

Conversation

@ohdearquant

Copy link
Copy Markdown
Owner

Why

Several live Studio views did work proportional to full history or interval frequency, kept hidden streams alive, silently stopped at fixed page limits, and allowed stale requests to overwrite newer selections. Schedules also issued one history request per row, Library eagerly loaded every catalog, and resumed Run Detail polled full detail while healthy SSE was already connected. These paths become visible demo freezes as the state DB and active workload grow.

What

  • unmounts Operator when closed, defaults fresh narrow sessions closed, and makes conversation selection last-request-wins
  • makes Run Detail selection last-request-wins; terminal sessions avoid full message SSE replay while persisted signals replay once for graph history and live-to-done transitions retain their graph
  • replaces resumed Run Detail's healthy-stream polling with connection-state-driven SSE, exponential fallback only while unhealthy, and one terminal refresh; adds a scalar invocation-status endpoint
  • projects short-interval schedules in O(visible cells) instead of O(firings)
  • replaces schedules' 1+N history fan-out with one /api/schedules/summary request and one deterministic windowed query with attributable partial errors
  • caps Dagre dummy-node burden at every graph size while preserving dependency order; 111-node and adversarial 99-node fixtures stay bounded
  • coalesces schedule refresh work and queues exactly one trailing refresh
  • separates draft/applied engine filters and adds guarded pagination beyond row 100
  • lazy-loads only the active Library catalog set, preserves fulfilled rows on partial failure, discards stale tab responses, reconciles selection, and fixes /playbooks
  • serves the schedule summary's recent-run slice as a declared projection carrying no run content: no trigger_context and no error_detail, only a translatable error_class

Verification

  • frontend: 93 files / 1,935 tests
  • backend: full suite, with the only failures the ambient set that reproduces unchanged at main
  • npm run typecheck, npm run lint, Prettier, and Ruff clean on the changed files
  • the projection is asserted by name for both content-bearing columns, not only through the
    allow-list, since an allow-list can be widened without anyone rereading it
  • the classifier's no-match path is asserted to report a class rather than fall back to the
    traceback's last line, which is the arm that would leak
  • a cross-language assertion pins the server classifier's class names against the client's, since
    the detail view still classifies in the client and two copies of one rule drift apart quietly

Tradeoffs

  • Engine requests cannot be transport-aborted yet; generation guards discard stale responses.
  • Engine pagination remains offset-based, so concurrent inserts may shift pages; row dedupe prevents duplicates.
  • The schedule summary endpoint is bounded to the supplied schedule ID set; endpoint pagination/chunking remains needed beyond a database's bind-parameter ceiling.
  • Row-specific schedule errors are retained in data state while the page continues to use its existing global degraded banner.
  • A failure the server cannot classify shows as unclassified in list views rather than showing its
    message. The full text is one click away in the run detail view, which asks for it explicitly.
  • The schedule detail endpoint returns its recent-run rows unprojected. That predates this work and
    is tracked separately; this change is scoped to the summary surface it introduces.
  • Browser click-through remains the explicit permission-gated item in PR Polish Lion Studio for demo readiness #3036's audit ledger.

Closes #3012
Closes #3059
Closes #3060
Closes #3064

Related: #3009

ohdearquant and others added 19 commits August 11, 2026 07:36
…depends on

The responsive default this branch introduces means a fresh context below the
threshold never mounts the Operator dock, and the flow suite runs at a
narrower viewport than that — so every Operator flow test timed out waiting
for a panel that was deliberately absent. The suite now seeds the explicit
persisted-open choice it depends on, which is viewport-independent and
exercises the branch the shell treats as authoritative. The responsive
default itself gets its own real-browser coverage on both sides of the
threshold, so the next change to either the threshold or the mount behavior
fails there instead of as a timeout in an unrelated flow.

The threshold also moves from 1360 to 1364: the constant's own derivation is
the 900px pane-collapse floor plus the 408px dock plus the 56px rail, and at
1360 the dock could open into a window where the pane beside it still
collapses.
Adding stream eligibility to the message-stream effect's deps meant the done
handler's own setDone tears the subscription down before its terminal-status
refetch can resolve, so the closure-scoped cancel flag always discarded the
response: a run that finished while on screen kept showing its pre-completion
status until reload. The refetch is now guarded only by the same-session
merge gate, which is what actually protects navigation, and a mounted
regression resolves the refetch on a macrotask — the ordering a real network
response has — and asserts the rendered status flips.
The projection's output-shape tests pass identically under the bounded
bucket-jump algorithm and the one-step-per-occurrence loop it replaced, so a
regression to O(occurrences) kept a green suite. A decade of one-second
firings is ~315 million steps for the occurrence walk and ~3,700 jumps for
the bounded form; the new case fails by timeout under the former and is
instant under the latter.
…-scale

# Conflicts:
#	apps/studio/frontend/src/components/history/RunDetail.tsx
The helper this branch extracted asks the display status, which has no
bucket for completed_empty, while the inline check main widened treated
it as terminal. Taking the helper alone would have left a finished run
streaming, so the helper now covers the raw terminal statuses too.

Also admits the hooks tab into the catalog loader's input type (it
matches no item kind and correctly loads nothing), re-pins the golden
route count to the merged list's own length, and folds three duplicated
sections in the Studio internals doc into one each. Two of those
duplicates predate this branch; the third is this merge keeping a
section both sides added in different places.
…/status

Two surfaces this branch adds were never recorded, so the contract gate and
the Operator end-to-end run were both red.

HTTP baseline. The branch adds GET /api/invocations/{invocation_id}/status
and GET /api/schedules/summary, taking the route count from 140 to 142. A
structural comparison of a fresh capture against the committed baseline says
those two routes are the whole surface delta: no route was removed, and all
140 routes present in both differ in nothing but `ordinal`, which shifts
because the new routes register ahead of them and which the baseline
comparison drops by design. The openapi block gains the same two paths and
its path_count moves 107 to 109. It also picks up version 0.35.0 in place of
0.34.1 -- that one is not this branch's doing: main is already at 0.35.0 and
its baseline had simply never been regenerated since, which the equality
comparison does not cover. The hand-written count literal moves with it and
was mutation-probed (restoring 140 reddens that test alone).

Operator end-to-end. The resume watch now polls getInvocationStatus, so the
request the spec waited for -- a GET on the bare invocation path -- is no
longer issued and waitForResponse timed out. The spec now waits for the
status endpoint the code actually calls.
…-scale

# Conflicts:
#	apps/studio/frontend/src/components/history/RunDetail.test.tsx
#	apps/studio/frontend/src/components/history/RunDetail.tsx
#	apps/studio/frontend/src/routes/engine-runs/index.tsx
#	docs/internals/studio.md
…-scale

# Conflicts:
#	apps/studio/frontend/src/components/history/RunDetail.test.tsx
Four conflicts.

lib/api.ts: main widened sseSubscribe's path to a thunk and added an event id
for cursor resume; this branch added a connection-state callback. The additions
are orthogonal, so the merged signature carries both and main's cursor and
resume logic is kept whole in streamSession and streamSignals, with the
connection-state argument threaded through after it.

test_daemon_api_gate.py and test_public_surfaces.py pin route counts as
hand-typed literals. Both sides added routes, so neither literal was right.
Measured rather than chosen: _GOLDEN_ROUTES is base 136, this branch 138,
main 137, merged 139; the live HTTP capture is base 140, this branch 142,
main 141, merged 143. Both are the exact union with nothing dropped or
duplicated.

contracts/data/http.json regenerated from the merged tree. Diffed against the
base before installing: it adds exactly this branch's two routes
(/api/invocations/{invocation_id}/status, /api/schedules/summary) and main's
one (/api/identity), and loses none. The 143 literal was mutation-probed.

Also in this commit: RunDetail.tsx drops two imports this branch added and
never used; library.tsx's selection effect now depends on items and the
optimistic workflow name it reads, since filtered drops the very rows the
hidden-workflow check looks at; and the Library workflow-creation test is
skipped with its restore condition, because the catalog loader no longer
requests a tab that UNFINISHED_KINDS already hid, leaving those cases asserting
against a reload that never happens and leaking an unhandled rejection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Library catalog does not load workflow definitions, since the workflow tab
is unfinished and shows no rows. A workflow selection still renders a detail
pane, so a deep link to one resolved against a list it was never in and the
selection fell through to the first row. The name is now resolved on its own,
only when a workflow is actually selected, and the link is held while that is
in flight rather than replaced.

Separately, the engine-run detail modal is reused across selections, so a spec
reveal for the run the user just left resolved into the run they just opened.
The reveal now applies only while its own run is still the one on screen, which
the detail fetch beside it was already doing.

The workflow creation cases are no longer skipped: they drive the detail pane
rather than a tab row, and the deferred they set up now owns its rejection so an
unconsumed reload does not surface as an unhandled one.
Two follow-ons from guarding those paths the first time.

The workflow deep link was kept only while its resolver said pending. The
resolver effect and the selection effect run in one commit, and the second
reads the first's state from before it was set, so selecting a workflow on
an already-loaded catalog discarded the link on the render it arrived. The
rule is now stated the other way round: drop it only once the resolve has
settled and found nothing. An unresolved link is held, which is the safe
direction, since the alternative silently deletes what the user asked for.

The spec reveal guarded its completion on still being the current run, which
also swallowed the reset that re-enables the button. Switching runs mid
reveal left the button disabled for that run and every run after it, because
the abandoned reveal can no longer clear the flag it set. Changing runs now
resets it.
…mment

The library catalog result declared an engines slice that no request ever
produced, so the branch assigning it was unreachable and the field was
permanently empty while reading as though engine definitions were loaded. The
slice and its assignment are removed; the remaining always-empty field says so
at its declaration, since a future engine tab has to add the request rather
than assume one exists.

The engine-runs reset comment claimed the synchronous reset clears stale runs.
It does not: it resets loading, error and the pagination lock, and the previous
rows stay on screen until the new page lands.
…ntract declares

The batch recent-run slice selected every schedule_runs column and returned the rows
as they came back. The table carries operational columns no client reads: action
arguments, resume packets, lease holders, capability and library references. The API
answers without a token when LIONAGI_STUDIO_AUTH_TOKEN is unset, so a list view was
disclosing all of them for every schedule at once.

Rows are now projected onto the eleven fields the client type already declares. The
projection is an allow-list rather than a redaction list, so a column added to the
table later stays private until someone names it.

Scope note: the same over-serving exists on the schedule detail and runs-list paths,
both of which predate this branch and are untouched here. They need their own change.
The recent-run slice this endpoint introduces served trigger_context, which
holds whole external event payloads, and error_detail, which holds subprocess
stderr and exception text. The API answers without a token when
LIONAGI_STUDIO_AUTH_TOKEN is unset, and neither field is a summary fact.

trigger_context had no reader and is gone. A failed run is described by
error_class instead, a translatable classification the client renders, so the
list views keep showing why a run failed without the text that says it. A
failure the server cannot classify reports that rather than falling back to the
last line of the traceback, which would put the exception message back on the
wire. The full text stays available in the run detail view, which reads a
different endpoint and asks for it explicitly.

Both columns are asserted absent by name rather than only through the
allow-list, since an allow-list can be widened without anyone rereading it. The
no-match path has its own assertion because that is the arm that would leak. A
cross-language assertion pins the server's class names against the client
classifier the detail view still uses, since two copies of one rule drift apart
without a symptom until the same failure wears two different labels.
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lion-studio Ready Ready Preview Aug 19, 2026 1:52pm

Request Review

integration added 2 commits August 19, 2026 07:23
…llow-list

The schedule summary endpoint projected its run rows and passed schedule records
through untouched, and the older per-schedule run list was not projected at all.
Between them the API answered, without a token, with authored specs, flow YAML,
shell commands and their arguments, notification targets, ownership keys, poll
cursors, lease bookkeeping, whole trigger payloads and raw tracebacks.

Both record kinds now have a named field list, applied at every list surface
including the rows nested inside a schedule record and inside the status view. A
run list carries a classification of a failure; the text that produced it stays on
the single-run route, which the detail view now fetches when a row is expanded.

The reconciled outcome needed the same treatment for a different reason: when the
reconciler falls through to the occurrence it uses the raw error text as its
summary, so the text arrives under a name no field list covers. Occurrence-sourced
summaries now carry the classification instead.

The field lists deliberately serve more than the web client declares, because the
CLI reads a remaining-runs counter and a spend rollup that no web view renders.
… missing

Two user-visible defects in the schedules and history views, plus the test that
pins why a third one is not a defect.

A run whose status belongs to no display bucket, completed_empty being the one
that exists today, derives to "running" so that an unknown status is never
mislabelled as done. The resume fallback asked only whether the invocation was
live, so it read a finished run as still going, rescheduled forever, and never
performed the terminal detail refresh. It now asks the terminal helper first,
which already knew about this class and existed for this reason.

The schedules board dropped a schedule's runs when its recent-run slice failed
to load, which renders identically to a schedule that has never fired. The hook
already distinguished the two and published the set of affected ids; nothing
read it. Both the table and the card views now render "Unavailable" for those
rows, so a failed request stops reading as a fact about the schedule.

The large-graph layout budget rescales rank gaps before Dagre while the rank map
returned beside them is the pre-rescale one, which looks like drawn positions
drifting from the ranking edges are routed by. It cannot: the edge that sets a
node's rank always has a gap of one, the cap never reduces a gap below one, so
it can never touch a rank-determining edge. Capping only releases reserved dummy
ranks. That argument is now a test, and it goes red if a future cap returns zero
or the gap floor moves.
integration added 2 commits August 19, 2026 08:24
The canvas switches an edge from a bezier to a stepped route past a rank
distance of two, on the reasoning that such an edge sweeps across the cards of
every rank between its endpoints. It read that distance off the layout's rank
map, which describes the dependency graph and not the picture.

Those two agree until the layout caps its rank spacing. The cap exists so a
graph with a hundred independent inputs feeding one late consumer does not
expand into thousands of synthetic layout nodes, and it works by relaxing the
minimum spacing on the long edges. Relaxing the constraint is what lets the
layout place those inputs beside their consumer instead of in a column of their
own. Their map entries still say rank zero, so every one of their edges was
routed as a ten-rank sweep while being drawn between neighbouring columns.

The layout now stamps each edge with the distance between the columns its
endpoints were actually drawn in, measured from the final geometry the way the
adjacent continuation mark already is, and the canvas uses it as it comes. The
rank map keeps its own meaning and its own name: it describes the graph, and
nothing now asks it to describe the drawing.

Two things the fixture pins that reading the code does not make obvious. The
drawing was never wrong: not one of the hundred and ten edges draws a target at
or left of its source, so dependency order held throughout and only the routing
premise was false. And the disagreement runs both ways: with the cap in effect
the hundred edges all carry a rank distance of ten while their drawn distances
run from one to eleven, so the map was overstating some and understating others.
…ns a drawing

The layout returned the ASAP rank map beside the coordinates it computed. Those
agree until the layout caps its rank spacing, and past that point they do not:
the cap relaxes the minimum spacing on long edges, which is exactly what lets
the layout place an independent node beside its late consumer while the node's
entry still reads rank zero. A caller holding both would have to know which
questions each answers, and the one caller that held both got it wrong.

That caller now reads the drawn distance stamped on each edge, so nothing was
left reading the map. It is removed rather than documented: it has been read as
a description of the drawing three times, and a field with no reader and a
misleading name will be read that way again. computeNodeDepths is exported and
returns the same values under a name that says what they are.

The tests that turned on it move to that function, except the block that only
asserted the field was passed through, which the depth function's own tests
already cover. One test replaces it, asserting the result's key set, so putting
a ranking back has to be a decision rather than an addition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment