chore: 5.2.0 release branch#50
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
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.jsonversion from5.1.2to5.2.0. - Bumped
package-lock.jsonversion fields to5.2.0. - Added an Unreleased
[5.2.0]section toCHANGELOG.mdas 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.
* 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>
4 tasks
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)
The only thing the agent could put in front of the user was an approval: allow or deny a tool it was about to run. Anything else it needed to know it asked in prose, and the user had to guess the wording it wanted back. It can now ask a choice-based question — single-select or multi-select — which renders in the conversation where it was asked, is answered by clicking, and blocks the turn until it is. The mechanism was established by probe, not by documentation. Claude's own AskUserQuestion tool does not exist in the headless stream-json channel this app drives: it is absent from the init tool list, a tool search does not find it, and the model falls back to prose (.work/probes/ask/). So the capability is supplied rather than recognised, as an MCP server this app hands the runtime inline (.work/probes/askmcp/). Its one tool blocks on `tools/call` until a person clicks, which is what makes the agent genuinely wait. The answer travels back over the unix socket the approval hook already uses, generalised from one kind of caller to two. Nothing is written to the user's own settings or MCP configuration: both are passed inline, session-scoped. The question tool is auto-allowed — asking someone to approve being asked a question is two prompts for one decision — and questions are asked even when approvals are bypassed, since that setting has never meant answering for the user. The card is drawn from the tool call that asked, which is already persisted and replayed, so it survives a reload, a rejoin and a server restart with the question and the answer intact. Verified end to end against the real CLI in both approval modes: the model asks, the browser answers, and the reply names exactly what was picked.
Wires the question channel into the ACP adapter, where servers are offered in
the handshake (`session/new`) rather than on the command line. Same script,
same socket, same tool; which of the two routes a runtime takes is now a
property of its registry row, so a runtime without a verified route reports
`questions: false` instead of being handed a flag nobody has watched it parse.
Probed against omp before writing any of it, and the probe paid for itself
three times over — none of this is visible from the Claude path alone:
- omp names the very same tool `mcp__ccweb_ask_user_question`, with a *single*
underscore. The matcher required two, so every ACP question would have failed
to render a card.
- ACP has no tool-name field at all. The adapter uses the agent's own title for
the block ("Asking tabs vs spaces preference"), and the real name turns up
inside the arguments instead. Recognition now consults both, and the question
payload is read from either the arguments themselves or omp's envelope, which
carries them as a JSON string beside a path.
- omp's model got the option schema wrong on its first attempt, had the call
rejected before it ever reached this server, and retried. That leaves two
announced calls for one question, and claiming by announcement order pinned
the card to the attempt that failed. Correlation is now by question text,
falling back to order only when a runtime reports no arguments to match on.
The schema now accepts a bare string as an option as well as an object, since
that rejected first attempt was a round trip spent on a shape this server
always understood.
kimi is wired by the same row but is unverified: it returns `end_turn` with no
output for any prompt here, with or without this change.
Verified end to end through the real ChatSession: omp asks both a single- and a
multi-select question, both correlate to real tool ids, and the reply names
exactly what was picked. Claude re-verified unchanged. 1423 unit tests, 315
browser checks, 0 failures.
kimi turned out to ship a native `AskUserQuestion`, and in a headless ACP session it answers itself with "the user dismissed this" without anybody being asked. Its MCP support is fine — verified: it spawns this app's server, exposes the tool as `mcp__ccweb__ask_user_question`, and round-trips an answer when the model picks it. It just often picks its own instead. That makes two rules load-bearing that were previously only incidentally correct, so they are now covered: a runtime's own ask-the-user tool must not be auto-approved by the rule that exists for this app's tool, and must not draw a question card — there is no pending question behind it, so the card would render as already-answered with nothing in it. Refusing the native call through the ACP permission channel was tried and does not redirect the model: the turn simply ends, because ACP carries no reason back. Recorded in the registry rather than worked around, since wiring kimi anyway is still strictly better than not.
#54 landed on the release branch while this was in review. Only the changelog actually conflicted — two entries added to the same list — and both are kept. Re-tested on the merged result rather than trusting each PR's own green: 1443 unit tests and 319 browser checks pass together.
Closes #42. Choice-based questions (single and multi select) for claude and omp, supplied as an inline MCP tool because the runtime's own is absent headless. kimi reaches it but often prefers its own; codex/pi/grok report the capability as unavailable.
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.
Release container for v5.2.0. Feature and fix PRs for this release target this branch (
chore/version-bump-5.2.0), notmain— 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) — 1443 passing🤖 Generated with Claude Code