Repository navigation
Large batch of tiny follow-up fixes #2303
Description
Small, non-blocking follow-ups collected from reviews of the Studio alignment work (reviews of #2225 to #2256). None changes a design and none needs discussion, so any of them can ride along in a nearby PR. This issue also absorbs the open follow-up bundles #2300 and #2142 (see the end).
Draft PR #2309 applies the ticked items in sections A, D and E. They stay ticked here once it merges; the unticked ones are not in it.
Draft stack #2325 (PRs #2311 to #2324) applies the items below that name a draft PR. Items are ticked once a PR is drafted; they stay open until it merges. Items marked closed or already done need no PR.
A. From the review of the merged alignment PRs (#2225, #2226, #2232)
- One token formatter (feat(studio): add the @/ alias and port the prototype's foundation primitives and utilities #2225).
formatTokensexists in three places with different behavior:lib/formatters.ts(number,toFixed(1), gives "1.0M"),features/chat/turn-stats.ts(string, goes throughNumber(BigInt(value))) and a local copy infeatures/chat/chat-session-controls.tsx(truncates, gives "1m" with a lowercase m). The same count renders differently in the session controls and in turn stats. Use one formatter. Also fix the comment inturn-stats.tsthat says the BigInt step avoids precision loss;Number()of the BigInt loses exactly the same precision. - Remove prototype-history comments (feat(studio): add the @/ alias and port the prototype's foundation primitives and utilities #2225).
lib/error-message.tsreferences "D7-43" and "Phase 7", which mean nothing in this repo. - Redundant
px-4(feat(studio): converge card, table, tooltip, and input with the prototype #2226).features/memory/facts-about-you.tsxpassespx-4on the sortable heads, butTableHeadalready defaults topx-4. Visual no-op; delete the classes. - Show a failed background refetch on the Learning page (feat(studio): align Settings > Learning with the prototype #2232). In
features/learning/learning-settings-page.tsxthe list error renders only whenitems.length === 0, so a failed refetch with earlier pages loaded shows nothing and the row can keep showing as Pending. Render the error wheneverisErroris true, without double-rendering next to the Load more error. Narrow case: it needs two consecutive failures right after a successful decision, and the 409 path self-heals. - Optional: "<1m ago" (feat(studio): align Settings > Learning with the prototype #2232).
formatRelativeTimereturns<1mandproposal-row.tsxrenders{updated} ago. Consider "just now". Note: this changes the sharedformatRelativeTime, which the schedule detail page will also use once the relative-time item in section C lands. Closed, no change: the prototype shows "<1m ago" everywhere and never "just now", so Studio matches the design.
B. Studio chat approvals (from the review of #1975; was #2142, parent #1848)
Worth fixing:
- Plan-verdict logic is duplicated and has diverged.
chat-workspace.tsx(planUnavailableReason,respondToPlan, separate in-flight/uncertain sets,viewedSessionIdguard) vsside-thread-panel.tsx(plan folded into the ordinary path, no uncertain state for plans). After an ambiguous failure, retry/Escape can be allowed on one surface and blocked on the other. Extract oneusePlanVerdictControlhook. Draft refactor(studio): move the workspace's verdict state onto VerdictLedger #2323 (migrate the workspace to the shared ledger) and refactor(studio): share one plan verdict flow via usePlanVerdictControl #2324 (oneusePlanVerdictControlhook). -
chat-workspace.tsxis ~3400 lines. Move plan-continuation follow/reattach state intoplan-continuation.ts. Draft refactor(studio): move Message out of chat-workspace #2322 movesMessageout, and refactor(studio): share one plan verdict flow via usePlanVerdictControl #2324 moves the plan follow and reattach state out.consumeRunand the authorization flow are not moved: too entangled for a small PR. - Pending-ask card ternary is copied at 3 sites (
chat-transcript.tsx,chat-workspace.tsx,side-thread-panel.tsx). Extract aPendingAskCard. Draft refactor(studio): extract PendingAskCard from four copied ternaries #2321. It was copied at four sites, not three. -
resolvePlanAskmaps genericfailed_preconditionto 409 "stale" (apps/server/src/mecatl/chat.ts). Keep onlystale_run_control,ask_not_pending,not_awaiting_plan, or log the underlying code. Closed with no change to the mapping: the only genericfailed_preconditionon this path is "a live run owns its plan continuation", so the four-code set is right. Narrowing it would turn that case into an "uncertain" verdict. Logging the code needs a logger the service does not have. - Docs: in
user-docs/building/deployment/studio.md, the newReview tool calls and plansheading sits above the unrelated "Review authorization" paragraph. Already fixed on main: the headings are in the right order. - Test gaps: 500ms draft-clear boundary (499/500ms); held Escape (
repeat: true) cannot fire ask deny / plan iterate / panel / stop; verdict keys with a dialog or menu open (or state in docs that only Escape applies); a planted secret in ask args/diff. Draft fix(studio): make the Escape double-press window strictly under 500 ms #2318 (499/500 ms boundary, held Escape at each layer, same-tick double-click on ordinary verdicts, side thread). The rest already existed. The planted-secret test is dropped: scrubbing happens in the engine and the UI renders args as given.
Nitpicks:
- Add
unmatchedApprovals()tochat-state.ts(themessage.tools?.some(approvalMatchesToolCall)check is repeated 7 times). Already done on main (chat-state.ts). - Use one key helper:
settledAskKeysusesrunId\0askId,approvalKeyuses a JSON tuple withsessionId. Draft refactor(studio): move the workspace's verdict state onto VerdictLedger #2323 (settledAskKey).settledAskKeysitself stays: stream retract events reset the ledger, so a replayedpermission.askwould otherwise bring back a ghost card. -
plan-continuation.ts: replace the hand-rolled AbortController/deadline/Promise.racewithAbortSignal.any([signal, AbortSignal.timeout(ms)]). Already done on main (plan-continuation.tsusesAbortSignal.any). -
chat-escape.ts: drop the redundantfocusinbranch inchanged(); hoistdisarm()and reuse it in the seconduseEffect. Draft fix(studio): make the Escape double-press window strictly under 500 ms #2318 drops thechangedalias and fixes the 500 ms boundary. Thefocusinlistener stays on purpose: a focus move should cancel an armed press. -
stalePlanAskreuses codestale_run_control, so clients cannot tell a stale plan ask from a stale permission ask. Draft fix(studio): give a stale plan ask its own error code #2311 (stale_plan_ask). -
runtime.tscopyCapabilitieshardcodesstorageMigration: falseand derivesbashfromshell; add a comment or fix the schema. Checkdetail()'s newclient.server.compatibility()call does not add a round trip per request. Draft fix(studio): stop a compatibility round trip per request #2312: comments on both fields, anddetail()no longer makes a compatibility RPC per request. The fields stay in the schema. -
ci.yml:STUDIO_REQUIRE_MECATED=1on the Playwright step is read only bymecated-mock.test.ts, not bychat-approval.spec.ts. Drop it or make the spec honour it. Already gone fromci.yml. - Docs: the Escape paragraph duplicates the Keyboard shortcuts page; state once and link. Draft docs(studio): shrink the Escape section to what the app does not show #2314.
- Tests: malformed/missing args at card level, unmatched
callIdpermission ask, double-click sends one POST, confirmuseRealTimersinafterEachinchat-escape.test.tsx. Draft fix(studio): make the Escape double-press window strictly under 500 ms #2318 covers the boundary, held Escape and the double-click tests. Most of the rest already existed on main. The planted-secret test is dropped.
C. Studio schedules: detail refresh, focus and shortcuts (follow-up to #2194, from its second review; was #2300, parent #2210)
Correctness and accessibility details on the schedule detail page and the inventory that #2194 left for later. None blocks use today.
Behavior:
- Run now leaves history stale.
runActioninschedules-workspace.tsxno longer invalidates the fires query, so reopening the detail within the 30sstaleTimeshows history without the new fire. Draft fix(studio): refresh schedule history after actions and tidy the detail page #2315. - Delete flashes "not found". The detail page awaits the list invalidation before
navigate, soDetailState(role="alert") renders for a moment and screen readers announce an error after a successful delete. Navigate first, or drop the item from the cache instead of refetching. Draft fix(studio): refresh schedule history after actions and tidy the detail page #2315. In a browser the "not found" alert did not reproduce (checked with a DOM mutation observer and CPU throttling); the visible effect was the detail page waiting for the list refetch before leaving. The fix navigates immediately. - Run limit is invisible before the first fire.
describeRunsreturns "None yet" and dropsmaxFires, and the form no longer shows it, so a cron created withmaxFires=10never says it will stop. Draft fix(studio): refresh schedule history after actions and tidy the detail page #2315 ("None yet · up to N").
Focus and keyboard:
- Focus after Edit from a row menu.
openEditorsavesdocument.activeElement, which is the menu item and is gone once the menu closes, so focus lands on<body>. Pass the trigger, asopenDeletedoes. The detail page's edit dialog doesn't restore focus at all. Draft fix(studio): return focus after schedule edits and scope the filter shortcut #2316. -
/while a dialog is open. WithoutuseShortcutSuppression,/inside the Schedule or Edit dialog jumps to the search behind the modal. It is also swallowed in the loading and empty states where there is no search box. Draft fix(studio): return focus after schedule edits and scope the filter shortcut #2316.
Code:
- Duplicate relative-time formatting.
formatSpan,describeNextRunanddescribeLastRuninschedule-detail.tsxre-implementformatUntilTimeandformatRelativeTimefromlib/formatters.ts. Draft fix(studio): refresh schedule history after actions and tidy the detail page #2315. - Cron parsing in hot paths.
describeCronruns inside the sort comparator (schedule-inventory.ts), and filtering runs twice per change (desktop and mobile). Compute the labels once per list, filter once, then sort. refactor(studio): describe cron schedules with cronstrue #2285 removed the cron parse from the comparator and memoizesdescribeCron; the double filtering remains. Draft fix(studio): refresh schedule history after actions and tidy the detail page #2315 filters once. refactor(studio): describe cron schedules with cronstrue #2285 had already fixed the cron parse in the comparator. - Outcome mapping location. The fire outcome mapping from
session.StopReasonlives in the web app (fireOutcomeinschedule-detail.tsx). Reviewers preferred the BFF so every client reads the same vocabulary. Draft refactor(studio): have the BFF report each schedule fire's outcome #2317 (additiveoutcomeenum on the BFF fire response; the web labels it).
Layout, from triaging #1852's leftovers:
- Resolve the breakpoint guard's exemption for schedules.
breakpoints.test.ts#L39haspending = ["features/schedules/"], sosm:grid-cols-2passes unchecked; Studio alignment: accessibility and 500px breakpoint sweep #2208 closed without sweeping schedules. The prototype usessm:grid-cols-2on the same element, so allow-list it with that reason and droppending. No PR yet. - Time input clips "AM". The
w-[120px]time field clips the meridiem (reproduced in Chrome at 1280px). The prototype has the same width, so it may be copied; widen it. No PR yet.
Not included: the detail page does not poll while a schedule is merely scheduled, so "in 30m" and a newly fired run can go stale until the window regains focus. Judged low value, since the page is mostly static configuration and the inventory polls every 5s.
D. From the post-merge review of #2233, #2251, #2252 and #2253
- IME guard on the queue-edit box (feat(studio): port the prototype's chat composer, pills, and queued strip #2253). In
queued-message-strip.tsx#L84-L86Enter saves the edit with noisComposing/ keyCode 229 check. A user committing an IME composition (for example Chinese, Japanese or Korean input) with Enter saves a half-edited queued message. The main composer already guards this; add the same guard. - Docs overstate the header authorization cue (fix(studio): keep the running chat's Stop inside the phone header #2233).
user-docs/operating/studio.md#L315-L317says the chat header shows Waiting for authorization, with an amber key on phones. That is only true while the event stream is open. When a run parks for authorization, the stream ends, the run counts as not running, and the header pill, the key and the Stop group disappear. The in-chat status and the tool row's Review authorization button remain. Reword the docs to point at the in-chat status. If you would rather keep the cue in the header, drive it from the pending-authorization flag instead of the running flag. Also: the new e2e test passes only because its mock server keeps the connection open after the authorization event; add a case where the stream closes. The docs sentence is reworded in fix(studio): apply the tiny follow-up fixes from #2303 #2309; the e2e case where the stream closes is still open. The e2e case where the stream closes is in draft fix(studio): keep the reader put when a disclosure expands, and copy clean selections #2319. The header cue stays transient, like the prototype (which has no header pill; authorization lives in the transcript). - Optional: focus after the approval detail panel closes (feat(studio): port the prototype's chat approval card, detail panel, and shortcuts #2251). When the panel closes because its ask was settled,
chat-workspace.tsx#L2710clears the preview directly and skips the restore-focus path. The opener is usually gone by then, so keyboard focus drops to the page body. Move focus to the composer or the next pending ask. Draft fix(studio): focus the composer after the approval panel closes, hide Steer when unsupported #2320: focus goes to the composer. - Optional: gate "Steer agent" on the daemon capability (feat(studio): align Settings sections and About with the prototype #2252).
interface-settings.tsx#L65-L67always offers it, whilesettings-workspace.tsx#L243-L244already readscapabilities.steer. On a daemon started with--no-steerthe message falls back to the queue, but the notice says "The run ended before steering" (chat-workspace.tsx#L1572), which is wrong. Hide or disable the option when steering is off, and fix the notice. (Traced by reading code; not run against a--no-steerdaemon.) Draft fix(studio): focus the composer after the approval panel closes, hide Steer when unsupported #2320: the option is hidden, and Enter and Shift+Enter queue, when the daemon cannot steer.
Related: a queued message whose send fails is not restored (#2307, filed under #2302).
E. From the review of the last three alignment PRs (#2254, #2255, #2256)
Line links point at each PR's head commit, because these PRs weren't merged when reviewed.
- Tool row loses its status for screen readers (feat(studio): port the prototype's chat transcript and delegation views #2254). The row button has
aria-label="Open {name} details", which replaces its content, so the hidden "failed / running / completed" text and the raw tool name are never announced (tool-call-list.tsx#L126). The collapsed header's "N failed" and the side panel still show failures, but inside an expanded list a failed row can't be told apart by ear. Put the status in the label, e.g.Open Bash details, failed. The prototype has the same label. - Bound the tool-output summarizers (feat(studio): port the prototype's chat transcript and delegation views #2254).
summarizeArgsandsummarizeResult(tool-summary.ts#L52,#L102) runJSON.parse, a whitespace pass and[...text](#L22-L24) over the whole output on every row mount. Built-in tools are capped at 25 KB, so only a multi-MB MCP tool output would hurt, but it is a one-line hardening: slice to a fixed prefix first. - Optional: copying a selection from the transcript includes hidden labels (feat(studio): port the prototype's chat transcript and delegation views #2254). The selection toolbar copies
selection.toString(), which now contains the screen-reader-only row text such as "completed Tool: Bash". Copy, Add to chat and Add to canvas would include it. Draft fix(studio): keep the reader put when a disclosure expands, and copy clean selections #2319. - Optional, needs a small design call: expanding something in the last message scrolls to the bottom (feat(studio): port the prototype's chat transcript and delegation views #2254). The new ResizeObserver follows content growth whenever the reader is within 80 px of the bottom (
chat-workspace.tsx#L508-L524). That fixes cards growing in place, but it also pulls the reader down when they themselves expand Activity, reasoning or "Show all N lines", so the toggle can end up above the viewport. One option: pause following for a moment after a click inside the transcript. The previous behavior only followed when messages changed. Draft fix(studio): keep the reader put when a disclosure expands, and copy clean selections #2319: a click that expands something stops following. The prototype never scrolls on expansion. - Optional: forking a listed chat navigates after its awaits (feat(studio): port the prototype's session dialogs, menus, and side panels #2255). If the user goes elsewhere during the roughly one-second fork,
forkListedChatstill moves them to the new chat (chat-workspace.tsx#L1299). No data is lost and the action is user-initiated. Compare the current session id before navigating. - Harden the breakpoint guard (fix(studio): sweep Studio's accessibility, 500px breakpoints, and phone overflow #2256). The test bans
sm:,md:,max-sm:andmax-md:(breakpoints.test.ts#L60), butmin-sm:,min-md:andmin-[40rem]:compile to the same media queries and are not caught. Addmin-smandmin-mdto the set. (Checked by compiling a sample against Tailwind 4.2.1; the lockfile pins 4.3.3.)
F. From the review of the skill Files section (#2155)
- Declare the transport-level statuses on the knowledge routes' OpenAPI responses. None of the 16 knowledge routes declares
400or429(routes/knowledge.ts), althoughapp.onErrorcan return both (a429from a rate-limited daemon was reproduced on the skill files route). Do it for all of them at once, so the routes stay consistent. Draft fix(studio): declare 400 and 429 on the knowledge routes #2313. It is 15 routes, not 16.
Merged issues
#2300 (section C) and #2142 (section B) were folded into this issue and closed as duplicates. Their text was copied here unchanged apart from checkboxes and grouping.
Part of #2210.
Fully or partially written by an AI agent.
Activity
fix(studio): apply the tiny follow-up fixes from #2303 (#2309)
[-]Tiny follow-up fixes[/-][+]Large batch of tiny follow-up fixes[/+]