Scale Lion Studio frontend views and interactions - #3385
Open
ohdearquant wants to merge 24 commits into
Open
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
added 2 commits
August 19, 2026 08:24
…scale # Conflicts: # CHANGELOG.md
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/api/schedules/summaryrequest and one deterministic windowed query with attributable partial errors/playbookstrigger_contextand noerror_detail, only a translatableerror_classVerification
mainnpm run typecheck,npm run lint, Prettier, and Ruff clean on the changed filesallow-list, since an allow-list can be widened without anyone rereading it
traceback's last line, which is the arm that would leak
the detail view still classifies in the client and two copies of one rule drift apart quietly
Tradeoffs
message. The full text is one click away in the run detail view, which asks for it explicitly.
is tracked separately; this change is scoped to the summary surface it introduces.
Closes #3012
Closes #3059
Closes #3060
Closes #3064
Related: #3009