Skip to content

feat(desktop): lead the session trace with an Astryx-native overview - #2289

Merged
Astro-Han merged 19 commits into
mainfrom
feat/inspector-session-overview
Aug 6, 2026
Merged

feat(desktop): lead the session trace with an Astryx-native overview#2289
Astro-Han merged 19 commits into
mainfrom
feat/inspector-session-overview

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

Redesigns the session workbar's Trace (追踪) tab to lead with an Astryx-native overview instead of only per-turn pricing.

  • Context: one ProgressBar (its own label + value) — the latest metered main call's prompt size against the window it actually ran under (contextWindow is frozen in the metering ledger at call time and passed through the trace projection). Warning ≥70%, error ≥90%.
  • Session facts: one MetadataList — model (registry display name), input / cache read (with hit rate), output (with reasoning), model calls (with retries), compactions, cost, duration, last activity. No per-turn column: per-turn numbers stay in the raw record.
  • Raw record (原始记录): the causal timeline in a Collapsible, redesigned to read as evidence — turns are named the way a reader counts (第 N 轮 / Turn N, never the uuid), stamped and metered on the header line, and each step carries a kind icon with duration/cost right-aligned. The coverage warning is demoted from a page-top banner to one quiet line inside this record, the layer it comments on.
  • Honesty rule throughout: an unreported breakdown renders as unknown, never as zero — the ledger keeps "not reported" and "reported none" apart ([RFC] Canonical model-call accounting: one ModelCallAttempt record per real provider request #1679), and so does the panel. A backend with no canonical metering (e.g. the fake backend) falls back to the plain timeline.

Out of scope on purpose: Claude's per-category context breakdown (system prompt / tools / messages) needs new runtime instrumentation and is left for a follow-up PR.

Validation

  • Pure view models (deriveInspectorOverviewModel, deriveInspectorPanelModel) with unit tests; targeted desktop suites (81 tests) green; core trace validation + runtime projection green.
  • session-workbar Playwright E2E (2 tests) passes; typecheck / lint / format:check / check-dead-css / check-copy / check-a11y all pass.
  • Iterated in Storybook (Product/Session Workbar → Trace, fixture extended with cache/contextWindow metering): light, dark, compact viewport, empty state. Live-app (Electron + fake backend) capture confirms the honest timeline fallback.
  • Note: 12 project-root / project-management desktop tests fail identically on unmodified main (environmental, unrelated).

Evidence

(Storybook screenshots — overview light/dark, raw record expanded — and one live-app capture attached below.)

@Astro-Han Astro-Han changed the title feat(desktop): 追踪页改为概览优先 — 上下文预算、Token 与缓存、会话成本 feat(desktop): lead the session trace with an Astryx-native overview Aug 5, 2026
The metering ledger freezes the window each call ran under; the trace
projection dropped it, so the Inspector could not set a prompt size
against the ceiling it was actually metered against.
Pure view model for the trace panel's new summary sections: context
budget from the latest metered main call, per-turn and per-session
token/cache stats, the latest model, and the session's active span.
Unknown stays unknown rather than reading as zero.
The 追踪 tab answered only "what did it cost" per turn. It now opens
with the glance facts — context fullness against the window the last
call was metered under, token and cache shape for the latest turn and
the session, and the session's model/cost/span — with the causal
timeline folded into a 原始记录 disclosure underneath. Unreported
breakdowns render as unknown rather than zero, matching the ledger's
own distinction.
…nents

Review feedback: the first pass carried too much custom chrome — three
hand-ruled sections, a two-column token grid and a display-size percent.
The overview is now one ProgressBar (its own label + value) plus one
MetadataList of session facts; the per-turn column is gone because the
raw timeline already carries per-turn numbers. startedAt/contextWindow
derivations with no remaining reader go with them.
…cord

Review feedback. The coverage warning was the panel's loudest element
while commenting on the least-read layer: it is now one quiet line
inside the raw record it describes. The timeline itself stops printing
uuids — a turn is named the way a reader counts (第 N 轮 / Turn N, its
position in the session), stamped and metered on the header line, and
each step carries a kind icon with its duration/cost right-aligned, so
the record scans as evidence instead of wrapped prose.
A single-value bar answers "how full", which is the smaller half of the
question a reader opens the Trace tab with; the half they act on is "full
of what". The context bar is now the split the metering ledger actually
carries — cache hit vs fresh prompt vs free headroom — with a three-column
legend under it so counts and shares line up down the list.

Only reported facts are drawn. When a provider reports no cache figure the
prompt stays one band rather than becoming a zero cache hit, empty bands
are dropped instead of rendering as slivers, and a prompt that overran its
window still fills exactly one track (#1679).

The overview also splits into the two sections it always was: one call's
prompt above, the whole session's totals below. MetadataList's page-sized
row rhythm is tightened to the legend's density so the two read as one
panel in a 400px column.
… panel

The overview had drifted into three type sizes with no rule behind them: a
12px section title over 12px legend rows, then 14px MetadataList facts, then
a 14px turn heading in the raw record. Two neighbouring sections in two
different sizes claim a hierarchy that does not exist, and the Astryx list
also painted from the neutral defaults (#262626 / #737373) beside figures in
product tokens.

Rebuilt on the Astryx IDE template's properties panel, which is the closest
reference this surface has: a level-3 heading per section, body-sized facts on
the template's `metadataCompact` rhythm, and one supporting tier for what
merely qualifies a figure. The context legend and the session facts now share
a single three-column grid — name, figure, qualifier — so a cache rate or a
retry count sits in its own column instead of being glued onto the number with
a middot, and every figure in a section ends on the same edge.

The panel palette seam is extended to the trace panel so its Astryx pieces
stop painting neutral, and the workbar's default width goes 400 → 480 to give
that grid room; a stored width still wins, so only a reader who never dragged
the handle sees it.
… as a timeline

Three problems in one layout, all of them the same mistake — putting things
next to each other that a reader is never comparing.

The session facts were laid out on the legend's shared right edge, which is
what alignment is FOR when every figure is the same unit against the same
window. A model name, a price and a timestamp compare with nothing, so that
edge only opened a lane of white space in every row. They now sit beside their
labels, with the qualifier trailing the figure; the context legend keeps its
columns, where the shared edge is the comparison.

The overview and the record were stacked with a fold between them, so the
timeline always opened below nine facts it has nothing to do with, and its own
trigger heading competed with the section headings above it. They are now two
segmented views — 概览 / 时间轴 — the same shape the Astryx IDE template uses
to segment Properties / History. A backend with no metered overview opens
straight on the timeline, since then there is only one view to be in.

And the record itself was hairline-separated blocks, which says "different
rows" where the fact is "this order, and these steps belong to that turn". It
is now an actual timeline: a node per turn (red when the turn failed), a rail
carrying its steps, totals closing the header line.
Nine table rows, each given a label column and a value column, in a 480px
panel: the labels crowded the left edge, the values ended wherever they ended,
and the right half was empty. The layout was a symptom — the list had never
been asked which of its numbers anyone reads.

Asked now, per figure, "who reads this, to decide what":

- Cost, duration and cache hit rate lead as headline stats. Those are the
  three a reader opens the tab for, and as figures rather than rows they give
  the column something worth filling.
- The context bar stays: the only thing here that answers a question about
  now rather than about the past.
- Input and output collapse into one ratio, and the cache read count goes —
  it is already stated as a rate above and drawn as a band in the bar.
- Retries and compactions report by exception. Printing `0` twice taught the
  reader to skip the block that also carries the exceptions.
- Model call count and last-activity are gone. The count had no decision
  attached; the timestamp is already on the session in the sidebar.
- What is left — model, the in/out ratio, and any exception — is one wrapped
  meta line, not a table reserving a column per fact.

The three-column grid stays exactly where alignment earns its keep: the
context legend, where every figure is the same unit against the same window
and the shared edge IS the comparison.
The tab switcher made the overview and the timeline rivals when they are
the same question at two zoom levels; the timeline is back under the
overview and the filter rides its heading line.

Inside a turn the ranking is now explicit: the turn and its failure carry
full ink, the measurement trails in supporting grey, and the steps sit a
tier below. The failure is red text rather than a filled Badge, which
out-shouted the label it qualified, and the rail's dot already flags the
row from the margin.

What a reader could not act on is gone: the per-step price (the turn
above states the same money at the level a decision happens), the date on
every turn stamp (a fact about the session, not the turn), and the
`main` call kind and compaction checkpoint id that printed on rows where
they said nothing.
The timeline was carrying five step icons, two filter controls, a
wall-clock stamp per turn and up to four numbers a row — chrome about the
record rather than the record.

Gone, each because nothing was decided on it:

- The filter, and the module behind it. A session's trace is a handful of
  turns on one screen; a search box over a list you can already see is a
  control that costs more attention than it saves, and 仅失败 now
  duplicates a red dot in the margin.
- Every step icon. Five glyphs that restated a word printed next to them
  (`read`, `compaction`, `error`).
- The wall-clock stamp on every turn. The order IS the timeline and the
  duration is beside it; the clock only mattered for correlating with a
  log the panel does not show.
- One more font weight on the step line and on the failure marker, so the
  timeline ranks by ink alone: bold for the turn, colour for the
  exception, grey for the measurement.

Three type sizes are left in the block — heading, turn, step — and two
numbers a turn.
…ter a shape

The panel was ranking with four sizes, and the smallest one carried six
different roles — stat labels, the context readout, the meta line, the
coverage notice, every step, every measurement. Six roles at one size is
not a rank, it is small print.

12px is gone from this surface. Three sizes are left, one per rank:
20px for the figures a reader opened the tab for, 16px for the two
section titles, 14px for every fact. Inside the 14px tier nothing is
bold but the turn name; colour alone separates a label from a figure, a
qualifier from what it qualifies, and an exception from either.

The filter comes back in a form that is data before it is chrome: the
failure count states how many turns failed and toggles to just those,
so the line spends a word where a Switch spent a track, a label and a
wrapped line. The query field keeps its own component and shrinks
before it pushes. What the filter hides is now announced beside the
rows it hid, not above the overview it has nothing to do with.
The overview ended in a run-on line — provider, model, input/output,
reasoning, retries, compactions — six facts sharing one grey sentence
because none of them had earned a place of its own. Asked what each one
decides, only the first survived.

The model leads the panel now, as the subject the figures are measured
on rather than the tail of a sentence about them.

Everything after it is gone:

- Token totals and the reasoning split. Cost already prices those
  tokens, the context bar already sizes the prompt, and the hit rate
  already reports the cache; `248,800 / 740` mostly restated how many
  turns the session had. Exact counts belong to the run ledger, which is
  where an audit belongs.
- Retry and compaction counts. Both are EVENTS the timeline below
  already lists — a retry as `×2` on the step that retried, a compaction
  as its own step. Counting them up here summarised a list on screen.

`InspectorTokenStats` goes with them: the panel keeps one token figure,
so the model exposes one, and the "a rate over nothing is unknown, not
zero" rule (#1679) moves onto it intact.
The model was a 14px caption over 20px figures: it ranked below the
numbers it governs, and alone at the top of the panel with no label it
read as an orphan string.

It is the section's title now, on the same head line every other block
here uses — title on the left, the fact that qualifies it on the right.
`glm-5.1` sits at the heading tier with `Z.AI` as its readout, exactly
as `上下文窗口` sits beside `18,900 / 200,000 · 9.4%` and `时间轴`
beside its failure count. Three blocks, one shape, and the panel now
opens by naming what it is about instead of by whispering it.

The overview's render condition collapses onto the same fact: the
window and the cache rate are both read off the model calls, so a
session with either necessarily has a model to name.
Every placement of it was wrong for the same reason. As a caption it
ranked below the figures it introduced; as a title it was 16px floating
over 20px. The size was never the bug — the line did not belong on this
surface.

The trace answers what a session DID and what it cost. Which model it
was configured with is setup, and the app states it twice already: the
composer names the model you are about to use, and the timeline in this
very panel labels each model call with the model that call used. A
single line picking the most recent one repeated the rows underneath it
and, in a session that switched models mid-run, contradicted them.

So the panel opens on its figures, with nothing above them. A label
over a 20px number is exactly what a StatCell is, and its label is
already at the right size for the job.

`InspectorModelRef` goes with the line, and the overview's render
condition simplifies to "the session is not empty" — cost and duration
come from the run ledger's totals, so a backend that reports no per-call
detail now keeps its two figures instead of losing the whole block.
The context bands were named after the code that produces them rather
than after what a reader sees. `新增提示` was a literal reading of
"fresh prompt" and left `缓存命中` without an opposite; `空闲` described
the window as idle when what it has is room left. The three bands are
now one vocabulary: 缓存命中 / 缓存未命中 / 剩余, with 已占用 for the
single band a provider that reports no cache figure gets.

The coverage notice was a chain of fragments built in the layout, with
`下界` — a maths term — for its main claim. It is one sentence now, and
the punctuation that joins it moved into the copy where the language
lives: Chinese gets `:` and `、`, English gets `:` and `,`. Each
fragment states its own count, which is also how English stops saying
"1 turns".

`费用不可得` became `费用未知`, which is the same fact in words someone
says out loud.
Half the words on this panel came from nowhere a reader can see. The
view model baked English literals (`compaction`, `error`, `permission`)
into rows it hands to a Chinese UI, and passed the projection's raw
identifiers straight through: `history_compact`, `allow`, `parked`,
`tool_failed`. `×2` was the attempts counter's own notation, asking the
reader to work out that one of two attempts was a retry.

The projection records facts, so the naming moves to where naming
belongs. A row now carries only identifiers that are genuinely the
session's — a model id, a tool name — and the panel supplies the word
for a row whose kind IS its identity. Every enum gets a table in the
copy file, with the raw value falling through when nobody has named it
yet, so a new call kind degrades to itself instead of to a blank.

`本轮失败 · tool_failed` collapses into one phrase: the code already
said what failed, it just said it in engineering vocabulary, and the
label beside it said it again.

Also here, from the same audit: `没有匹配的步骤——这个会话本身是有内容的`
was explaining the panel's own state machine to the reader; English
counts are singular when they are one.
Reviewed against Astryx's own incident-console template, whose inspector
is the same shape as this one: a header, facts, and a timeline.

One distance was doing three jobs. 16px separated the stat band from the
context block, the overview from the timeline, AND one turn from the
next — so proximity, the only grouping a panel without boxes has, said
nothing. Blocks are 24px apart now, turns 16px, and rows 8px. The seam
under the overview was a hairline plus two stacked 8px paddings nobody
chose; 24px of air does the job a rule was doing badly.

The rail was drawn per turn and stopped short of the next one, so the
timeline was a column of stumps rather than a line, at 12% ink. It runs
continuously now, from the first dot to the last, on --border-strong.
It is the only strong vertical on the surface and it can afford to be
seen.

The context bar stops painting free space. The track IS the empty
window, the band keeps its share of the flex so the used bands stay
proportional, and 90% of the bar stops reading as a third data series
next to the two that are actually data. The seams between bands go too:
a 2px gap in an 8px bar is a quarter of it.

The legend loses its share column. The bar is the share, the readout
already states the one share that is not obvious, and two right-aligned
number columns per row is what made three facts read as a spreadsheet.
What is left — name left, number right — is the step row's own skeleton.

Ink follows the same correction. Per-turn cost steps up to
--foreground-secondary, because it is the column a reader scans and the
whole right edge was one grey; the coverage notice steps down to muted
with only its icon in warning ink, because a disclaimer was out-shouting
the failures it comments on.

Declined from the same review: List/ListItem (its label + description +
endContent shape turns a single-baseline row into two lines), StatusDot
and Token (the rail math is solved, and a filled chip out-shouts the
label it qualifies), MetadataList (a fixed label column breaks the
legend's shared right edge).
@Astro-Han
Astro-Han force-pushed the feat/inspector-session-overview branch from d09349a to 52fe254 Compare August 6, 2026 08:21
…e truth

An adversarial review of this branch found two claims the panel makes that
it cannot keep, plus the residue of eighteen commits of redesign.

The cache hit rate divided a session's cache reads by its prompts with no
clamp, while the bar beside it clamped the same pair with a comment
explaining why. The runtime's own Google mapping guards against a provider
reporting more cache than prompt — and sets the cache figure anyway before
it bails — so the pair reaches this panel and renders as `120.0%`. Both
figures now clamp, per attempt, for the same stated reason.

The filter searched the trace's identifiers while the panel had moved to
rendering words: a reader who typed `第 7 轮`, `语义压缩` or `已允许` — every
one of them on screen — got 没有匹配的记录, while `turn-7` and
`semantic_compact`, on screen nowhere, matched. The filter now takes the
copy and searches what the row draws, which is what its own doc comment
claimed all along. `turn_failed` and `error`, two codes the projection
really emits, get names; the call-kind tables are typed against the core
union so the next kind fails the build instead of the reader.

Cut with nothing lost: `segment.ratio` (the bar grows by tokens and the
legend prints tokens), `InspectorStepRow.status`,
`InspectorTurnRow.failureMessage`/`attributedToStepId`, a one-caller
section wrapper whose only conditional was never false, and two classNames
no stylesheet ever matched.

The guards that hold the honesty invariants were tested only at their
equality boundary, so deleting either one passed; they are now tested in
the direction they exist for, and the `contextWindow` passthrough the whole
context bar hangs from finally has a projection test.
@Astro-Han
Astro-Han force-pushed the feat/inspector-session-overview branch from 52fe254 to 5dba80b Compare August 6, 2026 08:22
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 08:33
@Astro-Han
Astro-Han merged commit f7e04f9 into main Aug 6, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant