Skip to content

chore: 5.2.0 release branch#50

Open
dnviti wants to merge 9 commits into
mainfrom
chore/version-bump-5.2.0
Open

chore: 5.2.0 release branch#50
dnviti wants to merge 9 commits into
mainfrom
chore/version-bump-5.2.0

Conversation

@dnviti

@dnviti dnviti commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Release container for v5.2.0. Feature and fix PRs for this release target this branch (chore/version-bump-5.2.0), not main — same flow as #37 for 5.1.2. Each merged change adds its own entry to the [5.2.0] changelog section; the section gets its date when this PR merges.

Delivered in this release

These are implemented on the branch and close when this PR merges.

Still open, not yet in this branch

Linked for tracking only — no closing keyword, so they survive this merge and move to the next release unless they land here first.

Gates before merge

  • npm test (unit suite) — 1381 passing
  • browser checks (enforced in CI, no skipping) — 298 passing
  • typecheck + build — clean

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 26, 2026 16:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prepares the chore/version-bump-5.2.0 release branch container for v5.2.0 by bumping the package version and creating a placeholder changelog section to collect entries from release-targeted PRs.

Changes:

  • Bumped package.json version from 5.1.2 to 5.2.0.
  • Bumped package-lock.json version fields to 5.2.0.
  • Added an Unreleased [5.2.0] section to CHANGELOG.md as a placeholder for incoming entries.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

File Description
package.json Updates the package version to 5.2.0 for the release branch.
package-lock.json Keeps the lockfile’s version metadata in sync with 5.2.0.
CHANGELOG.md Adds a new [5.2.0] - Unreleased section to accumulate release notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

dnviti and others added 4 commits July 26, 2026 21:13
* feat(mobile): a phone type scale and touch targets for the chat surface

The phone layout was the desktop layout, smaller. Nothing that ran had a
viewport small enough to see it: every browser check mounted at 900px or
wider, so four minor versions shipped with 10px cost figures and 16px
controls two pixels apart.

- ui/touch.ts holds the phone scale — type steps, a 44px target floor, an
  8px gap — in one place, and publishes it through a context so a copy
  button five components deep gets the same answer as the header.
- The session header is a separate layout on a phone rather than the same
  row with smaller values: one wrapping row of information at a readable
  size, one row of controls a finger can hit. It drops the Beta badge and
  the terminal and theme toggles, all of which the more sheet already has.
- The composer, the turn strips, the message bubbles and the stream ribbon
  follow the same scale. The composer no longer detects "phone" itself —
  it takes the surface's answer, which is what made it impossible to
  examine at the size it ships at.
- test/browser covers three phone viewports (portrait, short, landscape)
  and asserts geometry rather than intent: target size, neighbour spacing,
  type size, the named live figures, and nothing pushed off the side.
- test/browser/page.html now loads the app's own stylesheets. Without them
  every `var(--text-2xs)` resolved to nothing and inherited 16px, so a
  check measuring type size would have read 16px for text shipping at 10.

Refs #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(mobile): carry the phone scale into the sheets behind the chat

The first pass fixed what a closed phone surface shows. This opens the
three sheets a phone actually reaches — the trace rail, the turn index and
the model list — and applies the same rules there, because a check only
covers what its fixture opens.

- Workspace tabs, the activity filter row and its event rows, the turn
  index and its rows, the tool-call headers and the model list all take
  the phone type scale and the 44px floor.
- The model list anchors to the composer rather than to its chip on a
  phone: a 200px list hanging off a chip near the right edge of a 390px
  screen was drawn half off-screen.
- Its custom-name field is set at 16px, not 15 — below that, iOS Safari
  zooms the page as soon as the field takes focus and does not zoom back.

The check mounts fresh per state rather than toggling: a second click on
the opener left the sheet up and blamed it for every state after.
Sideways scrollers and sheet scrims are recognised for what they are — a
tab strip that scrolls has a way back to what it hides, and a scrim abuts
the sheet on purpose — so neither is reported as crowding.

Refs #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(mobile): size the shell's own phone surfaces for a thumb

The bottom bar, its more sheet and the tab switcher are the app shell's,
not the chat surface's, so a ChatView fixture never reaches them. They get
their own mount, in an iframe: two of the three are Dialogs, and a Dialog
portals to document.body and positions against the viewport — measured in
the main page it would be 800px wide whatever the host div said, so the
one question being asked would have been answered about a desktop.

- The bar's labels were a 10px caption under a 20px glyph; they are now
  the phone's meta size. It exists only on a phone, so it takes the scale
  outright rather than asking whether it is on one.
- A bottom-placed Dialog is the phone form of a dialog, so its close
  control is sized for a thumb rather than a cursor — 17x19px of glyph in
  the corner of the screen was what a finger had to find.
- The tab switcher's rows, its per-tab close and its two footer buttons
  all clear the floor.

The terminal's key strip is deliberately untouched and out of the fixture:
issue #51 lists the terminal's own on-screen controls as a non-goal.

Refs #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(mobile): label the phone's controls, and stop the transcript overflowing

Two things the geometry checks could not see, found by rendering the phone
layout and looking at it.

The controls were the right size and still unidentifiable: a `panel-left`
glyph does not say "turn index" to anybody, and on a touch screen there is
no hover to reveal the title that said so — pressing it and watching what
happened was the only way to find out. The header's four controls and the
composer's now carry the word as well as the glyph, and a check asserts
it, because `title` and `aria-label` both answer the question for nobody
holding a phone.

And the transcript kept `min-height: 160px`. A flex item cannot shrink
below its min-height, so on a screen with less than that to spare the
floor did not reserve room — it overflowed the column and painted the
conversation over the live ribbon and the composer. A phone in landscape
has about 160px for the conversation once the header, ribbon and composer
have taken theirs, which is where it showed first.

Also here: the message actions move to their own line on a phone (beside
the text they were a 44px column that made a two-line message four lines
tall), the composer's action row flows as one wrapping row instead of
leaving Send alone on a line, and the phone hint line goes — at the phone
type size it truncated to half a sentence, and what it said is now written
on the two buttons above it.

The check learned two things while finding these: a control clipped by a
scroller has a box that says nothing about where it is painted, and the
model chip falls back to the word "model", so a fixture that never
delivered one was measuring the placeholder and passing.

Closes #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(mobile): give the shared controls the touch floor, once

Adversarial pass. Three things it turned up.

The dialogs a phone reaches — chat display settings, the session list —
were still drawn for a cursor: 16x19px close, 27px selects, 29px icon
buttons. Fixed in `Button`, `Select` and `IconButton` rather than in each
dialog, which is what a design system is for: a surface reached from the
bottom bar gets the floor without knowing it is on a phone. And the shell
publishes the phone answer at its root, so those dialogs — which render
beside every conversation rather than inside one — can read it at all.

`ui/touch.ts` had three exported helpers and two constants nothing used.
A module whose job is to be the one place should hold the rules in force,
not a wishlist.

`UsageMeter`'s new name was on a plain div, where most screen readers
ignore it. It has a role now, so the name lands.

The check learned that `getBoundingClientRect()` measures the painted box:
a dialog's entrance animation scales it, so a 44px button measured 43.12
for as long as the animation ran — and headless Chrome does not reliably
finish one. How big a control is, is a layout question.

Refs #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(mobile): a floating menu, a chrome that collapses, and the screen back

Reworked after seeing the first pass on a real phone. Every rule from it
still holds; what changed is how much of the screen the chrome is allowed
to hold while holding them.

- The bottom bar is gone. Five slots along the bottom edge cost 56px plus
  the safe-area inset of a surface whose whole point is what is above them,
  and could only ever show five things — so everything else lived in a
  sheet reached through it anyway. `FloatingMenu` is one 56px square in the
  bottom right and a labelled list behind it. The header's four controls
  join it there, so it is one menu rather than two.
- The session header is a strip: what it is doing, what it has cost. Tap it
  for the runtime, the folder, the branch, the tokens, the context meter
  and the approvals state.
- The composer is the field, More, and send — on one line. The rest is
  behind More. Its stop is gone from a phone: stopping is possible exactly
  while the ribbon above it is on screen, and the ribbon has a labelled one.
- The ribbon holds one line and drops the elapsed/token figures rather than
  wrapping to two.

The conversation went from 43% of a 390x844 screen to 79%, and the chrome
from 215px to 159px at every viewport — asserted, because nothing else here
would have noticed it drifting back: every other rule is about the chrome
being big enough, which pushes the other way.

Three things this turned up that were not about the redesign:

- The menu animated in from `opacity: 0`, and headless Chrome never
  advanced the frame — so every rule silently skipped the panel as
  invisible and reported the state clean. It rises without fading now,
  which also means it is there under reduced motion and on a dropped first
  frame. The check asserts it is actually on screen.
- The menu button and its own scrim were both at `--z-overlay`, so which
  one a tap reached came down to document order. Said outright now, and
  asserted by what `elementFromPoint` returns at the button's centre.
- The browser checks ran in an 800x600 window while mounting a 390x740
  fixture, so a third of it was off-viewport and anything asking the
  viewport a question got the wrong answer.

Refs #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: fold the two phone entries into one that agrees with itself

The redesign superseded half of what the first entry described — the
header's control row and the bottom bar it referred to are both gone — so
the section described two layouts, neither of which shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(mobile): the bottom bar comes back as destinations, not a drawer

The bar the redesign removed held Sessions, New, Panel, Image and More —
a place to go, a thing to make, a panel to toggle, a file to attach and a
sheet of everything left over. Five slots of the most valuable chrome in
the app, spent on a list with no idea in it. Removing it was the wrong
answer to that; rethinking what it is for is the right one.

What this app does is run agent sessions, and inside one there are four
places worth being — the conversation, what the agent did about it, the
files it did that to, and a shell in the same directory — plus the other
sessions. `BottomNav` is those five, it paints which one you are on with
`aria-current="page"` and a rule that survives monochrome, and pressing one
goes there. The verbs stay on the floating button: a control that changes
where you are and a control that does something to where you are do not
belong in the same row.

It hides while the on-screen keyboard is up — the keyboard takes half the
screen and the half it leaves is the half being typed into, which is the
one moment nobody is navigating.

Making the bar able to say where you are meant one answer for the rail
rather than two, and that surfaced a real bug behind the local state that
had been hiding it: `panelOpen` is persisted, and on a phone the rail
*replaces* the conversation, so the stored preference opened every
conversation behind a panel. My first fix cleared it on mount — which
writes the phone's answer into a shared setting and closes the rail on the
desktop that set it open. It is session state held by the shell now, and
the preference is not touched.

The chrome costs 216px with the bar, against 159 without and 215 before any
of this — so the conversation keeps about seven tenths of a phone screen
rather than eight, and the bar is worth that.

Two more fixture lessons, both the same shape as the last: the browser
fixture has to compose the surface the way the shell does, or it measures
something the app never renders. It passed `DEFAULT_CHAT_VIEW` straight
through with a no-op setter and spent a whole run measuring the workspace
panel instead of the conversation; and the shell's own tests shared a store
without resetting `chat`, so a case that put a conversation on screen broke
the key-strip case three tests later.

Refs #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mobile): the jump-to-latest pill kept its own height, under the floor

CI caught it on Node 22 and not on Node 24, which is the tell: the pill
only exists while the transcript is *not* pinned to the bottom, so whether
a run saw it came down to timing. The check now scrolls the conversation
back on purpose, which is the state that control lives in.

Two ways to defeat a primitive's touch floor from a call site, and this hit
both. `height: 34` overrode it outright. Replacing that with
`height: isPhone ? undefined : 34` was worse: the style object is spread
over the primitive's own, so a present key wins whatever its value is —
React drops an undefined one and the pill came out 17px tall. The key is
omitted on a phone now.

Refs #51

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
An attachment uploaded through a conversation — a screenshot — was swept
into a commit by a `git add -A` and merged. It is data written by the
running app into whatever project it is pointed at, never source.

Removed from tracking and ignored going forward. The blob stays in the
history of the merge that carried it; rewriting a pushed release branch to
excise one screenshot is the more disruptive of the two options, so that is
left as a deliberate call rather than taken here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An entry in the agents list was one row: a name, a status badge, a
duration. That is all you got whether it was a sub-agent reading three
files or a workflow running a dozen agents across four stages.

Any entry now opens into a popup with the file editor's chrome —
movable, resizable, fill-the-window — mounted by the rail rather than by
the tab that opened it, so looking at the file tree does not close it.

A sub-agent (#44) shows its own work. The runtime has been reporting
that all along: `task_started` / `task_progress` / `task_updated`
alongside messages tagged with a `parent_tool_use_id`. Nothing read the
tag, so a sub-agent's own tool calls were emitted as patches keyed by an
id no block in the conversation owned, and the reducer filed every one
of them into `orphanToolPatches` where nothing would ever render them.
They now land on the delegation that made them, as an `AgentRun` on its
tool block, fed by two new events — `agent_step`, keyed by parent *and*
by step so a result finds the call it belongs to, and `agent_progress`
for the run as a whole. A step's failure is shown with the message it
failed with, which is the part a badge cannot say.

A workflow (#45) shows the stages it narrates. There is no structured
progress channel for one, so `parseWorkflowLog` reads whatever heading
style the run uses and degrades to a flat log rather than inventing
structure that is not there.

The parented envelopes are also now barred from the streaming path.
They carry the sub-agent's own message and block indices; letting one
through would open blocks in the main conversation as though the
top-level agent had said them.

The adapter tests replay captured traffic from a real sub-agent run
(fixtures/chat/claude-subagent.jsonl), not hand-written events. The
browser checks drive the whole ChatView, because the composition is
where the interesting failures live: mounting the popup inside the panel
passed every check while a tab change silently destroyed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A step the agent spent entirely on tools still drew a row: a glyph, a
clock, three action buttons and a work pill with no sentence beside them.
A long task put half a dozen of those between one reply and the next.

Those steps no longer get a row. The list holds their ids until a message
that does speak comes along and hands them to it, so the reply's pill
counts the whole stretch and opens the trace at the *start* of it rather
than at the reply's own last command. Nothing is hidden — the rail still
holds every call, the turn strip still counts them, and a trace row or a
search hit aimed at a suppressed step now lands on the reply that speaks
for it instead of scrolling to an element that is not there.

The carried ids travel as a joined string, and the bubble subscribes to
the steps it carries: a tool call can report its duration after its own
message has closed, and the pill that inherited it has to hear about it.
Both stay inside the per-message subscription tier, so a streaming turn
still re-renders one bubble rather than the list.

Kept: a reply that has opened and produced nothing yet still shows its
caret. That is an answer about to arrive, not machinery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dnviti and others added 4 commits July 27, 2026 00:02
The sidebar groups sessions by calendar day. The fixture built its
timestamps from `Date.now()`, so a run in the first two hours of the day
put "five minutes ago" and "two hours ago" on *yesterday* — leaving the
Today group the assertions look for legitimately empty, and the suite red
for reasons that had nothing to do with the code under test.

Anchored to midday instead, which keeps every offset on the day it was
written to mean whatever time the suite runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…to it (#54)

A name the user chose used to live only in the page that typed it. The strip
read it out of its own state, nothing stored it, and the next page load — a
reload, a second window, a restart — rebuilt every tab from the name the session
was created with. The one moment the labels matter, coming back to a set of
long-running sessions, was exactly the moment they were gone.

The chosen name is now a field of its own on the session record, stored beside
the created name rather than over it, so a session nobody renamed still goes
through the generated-name rules and shows its folder. A new nullable
`custom_name` column carries it across a restart. `PATCH /api/sessions/:id/name`
takes the rename and tells every socket the user has open, so a window that is
already looking at the same sessions follows without reloading; the page that
asked renames optimistically and puts the old label back if the server refuses.

The same reload also used to lose the user's place. The selected tab is now
remembered in the browser — sessionStorage first, so two windows keep their own
tab, with localStorage behind it so a newly opened window still lands somewhere
sensible — and a remembered id for a session that is gone falls back to the
first tab rather than an empty screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(sessions): a renamed tab stays renamed, and a reload comes back to it (#54)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants