The published group->tab invariant read "no group may name a tab absent from
`tabs`". That is not the contract. A terminal occupies its group under the parent
tab id but publishes one row per pane, so `tabOrder` names `term-a` while `tabs`
carries `term-a::<leafId>`. The rule passed only because both fixtures were
browser/editor-only; adding an ordinary mounted terminal to one fails it.
The real rule is resolvability, not equality: a group may only name a tab the
client can resolve, and the client's `buildHostToLocalTabIdMap` keys terminals by
surface id *and* `parentTabId`. Move the invariants into a named module that says
so, and put a terminal in the fixture so the terminal case is covered rather than
avoided.
Also pins a guard that was silently unpinned: the mirrored-tab check runs twice —
once choosing which terminals the group projection may name, once emitting rows —
and deleting the projection copy left the whole runtime suite green, because no
fixture had a web-only mirrored terminal inside a group.
Review follow-up. `resolvePublishedActiveGroupId` was a second copy of the rule
already in `projectLocalStructuredSessionTabs`; extract it once and call it from
both, so the structured projection cannot drift from the publisher.
Ablating the shared rule left the structured projection's own tests green, so
pin it there too: a snapshot whose active group holds no visible structured tab
must not keep pointing at it, and must fall to null when nothing survives.
Also repairs a cross-file comment reference that dangled after the relocation.
When every tab in the active group is unpublishable — a combined-diff tab,
a workspace-document browser tab — the group is held back from `tabGroups`,
but the snapshot still published `activeGroupId` naming it. The client was
told the active group is one it never received.
Resolve the published pointer against the published group set: keep it when
the group survived, else the first published group, else null. Both fallbacks
are values three of the three read sites already handle today.
The published invariants covered group->tab dangling references and the
snapshot's own `activeTabId`, but nothing checked `activeGroupId` against the
published groups. The extended invariant now checks every published group
reference — `activeGroupId` and the `tabGroupLayout` leaves — and moves to its
own file so the claim has a name.
STA-5724
* Improve markdown rich mode: distinguish HTML tags from placeholders
- Consolidate size limit and unsupported content checks into single function
- Refine HTML/JSX detection to validate against rehype-sanitize's known tag names
- Allow bare placeholders like `<id>` and `<project-id>` in rich mode rendering
* Add decision explainer for markdown rich-mode rendering fix
* rm html explainer
* fix(browser): close guest-owned split tab
* fix: check sourceId before toggling floating panel on close
The empty-panel toggle is the ambient fallback only. Guest-initiated
closes (with sourceId) target the main workspace and should not toggle
the panel.
* test(browser-split-shortcuts): remove terminal-mirrors close test and un
Removes test case that verified Cmd+W closes guest-owned browser splits when
active-tab mirrors point to a terminal, along with the helper function and
unused fixture properties that only that test required.
* Clarify automation history unavailability and improve recovery UX
- Improve error message to explain run history is unavailable due to host
version requirements, not automation failure
- Hide misleading "0 runs" count badge when history is unavailable
- Deep-link "update server" recovery actions to specific runtime environment
in settings instead of pane root
- Add test coverage for run count hiding and recovery targeting
* Watch for deep-linked settings targets that render asynchronously
Some settings panes (such as Remote Orca Servers) fetch and render their
rows asynchronously. Deep links can name targets that don't exist yet,
but the scroll effect has no way to know when they finally mount. Add a
MutationObserver-based watcher to detect when async rows appear and
trigger scrolling.
Refs #16646
Unify native, WSL, and direct SSH conflict checks behind the execution host, remove the duplicated SSH classifier, and cover orphan/configured remote behavior across both paths.
Both files describe paths with POSIX literals while their subjects compose
paths through `node:path`, so the assertions only hold where the separator
happens to be `/`.
`node-markdown-document-discovery` keys its fake tree at `/repo/docs` and
`/repo/one`, but `discoverMarkdownRelativePaths` descends with
`join(absoluteDirectoryPath, entry.name)` — `\repo\docs` on win32. The child
lookup misses, `readDirectory` yields nothing, and the walk stops at the root:
`docs/guide.mdx` disappears and the depth-limit case never reaches its limit,
so it resolves `[]` instead of rejecting. Keying the children with `join` walks
the tree the subject actually walks.
`git-fetch-head-lock` expects `cwd: '/tmp/repo'` from a subject that returns
`path.resolve(cwd, 'repo')`, which is `C:\tmp\repo` on win32. Asserting through
`path.resolve` pins the behaviour — that `-C` and `--git-dir` are resolved
against the cwd — rather than the separator of whichever machine runs the suite.
Verified on Windows 11: the two files go from 3 failed / 12 passed to
14 passed / 1 skipped, and the wider `src/shared` run shows no regression.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Make worktree palette hint rows keyboard-clickable
Hint entries like "See more" are now CommandItems that can be navigated with arrow keys and activated with Enter, instead of being non-interactive divs. This allows keyboard-only users to access the expand actions without mouse interaction.
* Make worktree palette "See more" keyboard-navigable
Preserve cursor position when expanding via keyboard: auto-select the first
newly revealed item at the previous index and restore input focus.
- Translate Agent Dashboard column headers (Needs You / Working / Idle), board title, and total count.
- Translate empty-column placeholder, "You" message badge, terminal preview actions, and error-boundary copy.
- Resolves English fallback in the Agent Dashboard (dashboardPopout) under the Korean locale; only "Done" was previously translated.
* fix(ci): stop the Linux Electron probe step from starving its own probes
The package job's "Test Linux Electron lifecycle boundary" step ran five
Electron probe files under Vitest's default file parallelism, so four full
Electron stacks competed for a 4-vCPU runner. Each probe carries its own
in-process deadline (20s for WebRTC, 25s for H3), and every observed failure
was one of those deadlines expiring: exit code 2, "no result", with every
sibling file in the same run slower than its own green maximum.
Run the step with --no-file-parallelism so each probe owns the runner, and
stop each probe nesting a private `xvfb-run --auto-servernum` X server inside
the step's own xvfb-run: reuse an inherited DISPLAY, and only own one when
there is none (shards, local dev), which leaves those lanes unchanged.
Also set each Docker-SSH E2E step's Playwright output aside before the next
step starts, because Playwright empties test-results/ on every run and only
the last lane's traces survived to the artifact.
* fix(ci): route the persisted-worker probe through the same display resolver
* perf(editor): stop re-rendering every code block on each keystroke
Profiling a 305 KB document in a packaged build showed typing was dominated
by two things that had nothing to do with the text being typed.
Tiptap re-renders a React node view whenever its document *position* changes,
even when the node and its decorations are untouched (@tiptap/react 3.22.5,
ReactNodeView.update). Typing shifts the position of every node after the
caret, so one keystroke in a document with 533 code blocks cost 533 React
renders. That re-render only exists so a component can observe a fresh
getPos(); RichMarkdownCodeBlock never reads it, so it now opts out via an
explicit `update`. getPos() stays correct for later callers — Tiptap updates
its position bookkeeping before calling `update`, and passes getPos as a live
function rather than a captured value.
The language <select> also mounted ~25 <option> elements per code block, for
a dropdown almost nobody opens: 13,858 option elements in that document, more
than a quarter of its DOM. The list now mounts on first interaction
(mousedown/focus, flushed synchronously so the native popup never paints a
stale list); until then a single option renders the same visible label. The
labels themselves were getters that re-translated on every property read, so
one render cost thousands of i18next lookups; they are now resolved once per
locale.
Median keystroke latency, packaged build, M-series:
305 KB 84 ms -> 59 ms
600 KB 265 ms -> 201 ms
Verified in the running app that the dropdown still expands to the full list
by mouse and by keyboard, that an unknown fence keeps its verbatim label and
fallback option, that changing the language still applies, and that typing
inside a code block still updates with syntax highlighting intact.
This does not move the size limit: the blocking mount (1.7 s at 300 KB) is
what pins that, and it is unchanged. The constant now records the measured
numbers, including that node-view count drives cost far more than byte size.
* fix(editor): refresh cached code language labels