Skip to content

chore(deps-dev): bump @playwright/test from 1.62.0 to 1.62.1 in the dev-dependencies group - #583

Merged
BorisTyshkevich merged 95 commits into
mainfrom
dependabot/npm_and_yarn/dev-dependencies-edf4b8f5b6
Aug 5, 2026
Merged

chore(deps-dev): bump @playwright/test from 1.62.0 to 1.62.1 in the dev-dependencies group#583
BorisTyshkevich merged 95 commits into
mainfrom
dependabot/npm_and_yarn/dev-dependencies-edf4b8f5b6

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 3, 2026

Copy link
Copy Markdown
Contributor

Bumps the dev-dependencies group with 1 update: @playwright/test.

Updates @playwright/test from 1.62.0 to 1.62.1

Release notes

Sourced from @​playwright/test's releases.

v1.62.1

Bug Fixes

  • #41989 [Regression]: tsconfig "extends" bare specifier isn't resolved via node_modules walk-up like tsc (fatal since 1.62)
  • #41998 [Regression]: directory-form tsconfig project references ("path": "../pkg") fail to resolve (fatal since 1.62)
  • #41985 Accessibility snapshot drops button name when text is nested inside spans with aria-hidden SVG
  • #42000 [Regression]: page.evaluate() arg of a branded primitive type (string & { brand }) no longer type-checks since 1.62
  • #42013 [BUG]Image-type actionable elements are not presented in the snapshot.
Commits
  • 26a9e47 cherry-pick(#42043): docs: release notes for v1.62 Python, Java, and .NET (#4...
  • 0a81d5d cherry-pick(#42040): docs(release-notes): mention the isolated headless clipb...
  • 8376826 cherry-pick(#42034): fix(aria): keep icon-only clickable elements in ai snaps...
  • 66c5cc9 chore: mark v1.62.1 (#42020)
  • 9672bc3 cherry-pick(#42009): fix(types): support branded primitives in evaluate argum...
  • 4325804 cherry-pick(#41988): fix(aria): preserve names from collapsed text contributors
  • 9632f8e cherry-pick(#42005): fix(tsconfig): do not throw when "extends"/"references" ...
  • See full diff in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore <dependency name> major version will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
  • @dependabot ignore <dependency name> minor version will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
  • @dependabot ignore <dependency name> will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
  • @dependabot unignore <dependency name> will remove all of the ignore conditions of the specified dependency
  • @dependabot unignore <dependency name> <ignore condition> will remove the ignore condition of the specified dependency and ignore conditions

BorisTyshkevich and others added 27 commits July 29, 2026 22:25
Phase 1 of 4 for the drag-foldable desktop left navigation. Adds the pure
module that owns every layout decision the feature needs, plus the browser
preferences behind it. No user-visible change: the rail, the docked focused
drawer and the resize separator arrive in phase 3.

`src/core/left-nav-layout.ts` holds the named constants and thresholds, the
explicit 'wide' | 'rail' mode, the hysteresis reducer, the keyboard separator
resolver, rail activation, the separator's ARIA range, and the mobile
projection. Hysteresis is the threshold PAIR — folding needs a proposal below
140px, restoring one above 260px — so the mode is sticky between them and no
single pointer pixel can oscillate it. The keyboard path routes through the
same reducer as the pointer path, so "keyboard operations match pointer
transitions" holds by construction rather than by two implementations agreeing.

Every function takes the navigation's proposed TOTAL width and derives each
mode's own panel width itself. An earlier revision handed the reducer a
mode-relative width, and that made a monotone rightward drag snap the
navigation 108px BACKWARDS on the frame a drawer converted to the wide sidebar,
because the two measurements disagree by exactly the rail's width at the
crossing. The regression tests now sweep a whole pointer path and assert the
width response is monotone; per-frame assertions could not see it, since each
frame's output was individually defensible.

Deviations from the issue's *suggested* shape, all deliberate:

- the wide width reuses the existing `asb:sidebarPx` rather than adding a
  parallel `wideWidthPx`. That key already persists exactly this width over
  exactly this range; two owners of one width is a bug waiting to happen. The
  new keys are only `asb:leftNavMode` and `asb:leftNavDrawerPx`.
- the focused drawer's band is [fold, wide] = [140, 260], not the wide
  sidebar's [180, 420]. A drawer drag must fold below the fold threshold and
  convert above the wide one, so the wide range is mostly unreachable for a
  drawer; MIN/MAX govern the sidebar.
- a drag follows the pointer and does NOT restore remembered widths. The issue
  asks a rail -> wide drag for both "restore the last useful wide width" and
  "deterministic resize feedback", and those cannot both hold: the next
  pointermove overwrites whatever the crossing frame installed, so a restored
  width survives one frame and reads as a flicker. `End` restores the
  remembered width — it is the discrete counterpart, with no pointer to honour.
- the centre-width clamp moves to phase 4, where its caller is. A single
  total-in/total-out signature is the wrong shape: there is no inverse from a
  total back to (mode, panel width), so it could return a width no mode can
  render. Deferring also respects CLAUDE.md hard rule 5.

Also drops the second owner of [180, 420]: `splitters.ts`'s 'col' axis, which
WRITES `sidebarPx`, kept its own copy of the bounds as literals while the load
path moved onto the constants. Behaviour is unchanged (a real drag always
carries a finite clientX); the sidebar e2e drag specs pass untouched.

`clampWideWidthPx` closes a NaN hole on the way past: `clamp` is not NaN-safe
(`Math.max(180, NaN)` is NaN), so a corrupt `asb:sidebarPx` would have decoded
to `width: NaNpx`, which the browser drops. Hardening, not a reproducible bug —
no code path writes a non-numeric value. The same hole in the four sibling
geometry keys is filed as #570 (`inbox`).

`'library'` is this module's section name but NOT the value `AppState.sidePanel`
stores for it: that is still `'saved'`, because #427 renamed the label and left
the persisted value alone. Phase 2's registry owns the one mapping; the type's
doc records it so phase 2 does not rediscover it the hard way.

npm test 6734 passing, `left-nav-layout.ts` and `splitters.ts` both at
100/100/100/100. Sabotage-checked: reverting the crossing to the remembered
width, basing the bare-rail arrow step on `wideWidthPx`, loosening the drawer's
fold comparison to `<=`, and adding a field to the workspace projection each
fail a specific test.

Part of #487

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPnt3pq98P5Y1ww3sWawp1
A third-party review pass raised six findings against the phase-1 module. All six
reproduced against the real code; all six are fixed here. The most valuable one is
the exact mirror of the bug the earlier review caught, which this suite had been
blessing with a test.

1. **Plain ArrowLeft could never fold a wide sidebar.** At the 180px floor a -16
   step proposes 164, which clamps straight back to 180 — so eleven presses from a
   300px sidebar sat at 180 forever while the equivalent pointer path folded:

     keyboard  284,268,252,236,220,204,188,180,180,180,180 -> wide
     pointer   284,268,252,236,220,204,188,180,180,180,48  -> rail

   This is the same class of dead end as the bare-rail one already fixed, at the
   opposite end, and the previous commit's test explicitly blessed it on the
   grounds that Home and Shift+Arrow escape. That is not a defence: the W3C
   splitter pattern makes plain Left/Right the separator's move keys, and
   `aria-valuemin: 48` was advertised while the control refused to move.

   Arrows now resize within a band and perform the band edge's semantic
   transition, symmetric at both ends. `keyboardBaseTotalPx`'s virtual 260 base is
   gone with it — it was a false relative step (ArrowRight from a 48px rail moved
   +228, not +16) and it discarded a remembered 420, handing back 276. A bare
   rail's ArrowRight now restores the remembered width, like End.

2. **`resolveRailActivation` is a toggle, so it cannot be the `openFocusedSection`
   seam** #487 mandates for #428. Bounded drag-hover re-asserts intent while a
   query is held over the Dashboards icon, so a toggle flaps the drawer open and
   shut on alternate notifications. Added `resolveRailOpen`, idempotent and
   identity-returning when already open; the toggle stays for clicks.

3. **The coherence invariant was a precondition, not a postcondition.** `state.ts`
   stores `mode` and `focusedSection` as two independently writable signals, and
   the reducers preserved an illegal pair rather than healing it — `drag({mode:
   'wide', focusedSection: 'databases'}, 300)` returned it intact, and `End` handed
   it straight back. Every reducer now normalizes its input through
   `normalizeLeftNavigationLayout`, which also lifts non-finite and out-of-band
   widths, so `leftNavigationLayoutIsCoherent` is "normalizing changes nothing" and
   covers widths rather than only the mode/section pairing. A NaN width can no
   longer reach `aria-valuenow`.

4. **`parseInt` accepted a numeric prefix**, so `'12junk'` decoded to 12 and
   `'200px'` to 200 while the documented contract promised the default. Added
   `decodeStoredPx`, which requires the whole string to be a finite number — and
   therefore also rejects a stored `'Infinity'`, which is corruption rather than a
   width pressed against a bound.

5. **The restore memory is sampling-dependent** and this commit does NOT fix it:
   from a 300px sidebar, a single coarse sample past the fold remembers 300, but an
   intermediate sample inside the 140-179 dead zone rests the width at the floor
   first and remembers 180. One field is serving as both the live drag width and
   the restore memory; separating them needs a drag-session snapshot, which a pure
   reducer cannot take. Pinned with a test so phase 3 has to change it
   deliberately, and recorded as a phase-3 obligation in the ship log.

6. **The centre clamp's phase boundary was unsafe.** Phase 3 turns the feature on
   while the clamp sat in phase 4, leaving a shippable interval where both docked
   panels could starve the centre surface. Moved to phase 3, before activation.

Two findings were verified and NOT changed, with reasons recorded in the ship log:
the drawer's [140, 260] band (a 150px drawer may be unusably narrow, but #487 says
these constants are settled only by real-browser verification), and the separator's
discontinuous ARIA interior (inherent to one control spanning two modes; phase 3
adds mode-aware `aria-valuetext`).

npm test 6755 passing, `left-nav-layout.ts` at 100/100/100/100 (17 functions, 64
lines, 86 branches). Sabotage-checked: dropping the wide-floor transition, making
the open seam a toggle, removing reducer normalization, and reverting to parseInt
each fail specific tests. Sidebar drag e2e green.

Part of #487

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPnt3pq98P5Y1ww3sWawp1
feat(#487): left-navigation layout core and preferences (phase 1)
Make the four navigation sections hostable by any container, over one shared
store per section. The wide sidebar renders exactly as it does today.

`src/ui/nav-sections.ts` maps each `LeftNavigationSection` to its label, icon
factory, accessible label, wide pane and — the point — its single PERSISTENT host
element. `app-shell.ts` composes both sidebar panes out of `registry.entries`
filtered by pane, so the shell no longer names which sections belong where. The
hosts are built once and never rebuilt, extending #426's `buildSidebarUpper`
contract to the lower pane: phase 3's rail and docked drawer MOVE these elements,
and a moved element keeps its input values, expansion and scroll, which is how
"wide and focused presentations share and preserve all navigation state" becomes
structural rather than save/restore logic.

The lower pane's Library and History sections gained separate persistent
search/list pairs (`historySearch`/`historyList` join `savedSearch`/`savedList`,
which are now the Library's). Before this both rendered through one pair a section
switch repainted — workable for two tabs in one pane, but neither section's live
DOM could be handed to another container without carrying the other's content
along. Behaviour is unchanged: only the active section renders, and a switch still
clears the shared search filter.

Both wide switchers now take their label and icon from `NAV_SECTION_META`. Leaving
the upper one hard-coded would have left the registry a second source of truth for
exactly the sections phase 3's rail presents again, so a relabel would drift
silently. The `'library' ↔ 'saved'` bridge lives in `core/left-nav-layout.ts`
beside the other decoders, because `state.ts` applies it at the load boundary and
must not import `src/ui/`; the registry re-exports it as its UI-side owner.

Fixes a real bug this exposed: `asb:sidePanel` was read undecoded and every reader
compared `=== 'saved'`, so an unrecognized or obsolete stored value fell through to
History — neither the documented default nor the value's own meaning. Harmless
with one shared element pair; with two hosts, two readers resolving one value
differently expose one section's host while painting into the other's, i.e. a
blank pane. It is decoded once at load now, like `leftNavMode` two lines below it,
and `AppState.sidePanel` narrowed from `Signal<string>` to `Signal<SidePanelKey>`.

All four hosts share `.nav-section-host` / `data-section` (was `.upper-role-host` /
`data-role`, upper pane only) so phase 3's drawer needs no per-section layout rule.

Tests: `## Tests` → Wide state 1-4, one host per section, and the exposure rules.
`tests/e2e/dashboard-tree.spec.js` gains a lower-pane geometry assertion — the
split added a flex wrapper between `.saved-pane` and its scroller, and happy-dom
computes no layout, so that pane's box model had no gate in any suite.

Part of #487. Follow-up filed: #572 (`inbox`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
Four findings, all reproduced against the real code first.

**The shell/renderer agreement test never ran the shell.** The out-of-union
`sidePanel` case lived in `saved-history.test.ts` and called `renderSavedHistory`
directly, so it pinned only the renderer's half of the blank-pane invariant — the
shell could have gone back to resolving the value inline and it would still have
passed. The case now also exists in `app-shell.test.ts` against the real mounted
shell, asserting exposure AND painted content together, which is the only place the
two halves can be caught disagreeing.

**`WorkbenchStateSlice.sidePanel` widened the narrowed signal back to
`Signal<string>`**, so that session stayed type-authorized to write an arbitrary
string into a signal this branch had just narrowed to `SidePanelKey` — the claim
"only ever holds a SidePanelKey" was not mechanically true across the boundary. It
derives from `AppState['sidePanel']` now, like the other slices do.

**A retained inactive search input could repaint the OTHER section.** The hidden
host keeps its listeners, `state.libraryFilter` is still one shared string, and
`renderList` paints the active section — so an event from a stale input rewrote the
filter and repainted the visible list with the wrong section's text. Unreachable
through the UI (a `display: none` subtree gets no events), but phase 3 moves hosts
into containers where a host can be visible while another section is active, so the
handlers now enforce ownership rather than relying on CSS. A guard, not a redesign:
per-section filter state is what fixes the shared string, and phase 3 owns it.

**No test proved the LOWER switcher reads the registry** — the mirror of the gap
already fixed for the upper one, and hard-coding `Library`/`History` back in passed
everything. Added the same override-the-registry test. The icon-factory test now
covers all four sections instead of Library alone.

Not done here, recorded as a phase-3 prerequisite in the ship log: the lower
renderer still requires `savedTabsRow` to exist and resolves its target from the
global `sidePanel` at call time, so a switcher-less drawer needs
`renderLowerTabs`/`renderLowerSection` split apart. That is phase 3's own refactor,
not something to smuggle into a phase whose gate is "behaviour unchanged".

Part of #487.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
Second pass on the updated head approved phase 2 and left two minors, both real.

**The registry's icon MAPPING was unpinned.** The tests proved every entry is a
factory that mints a fresh node, and two mutation tests proved both switchers read
the table — but a wrong icon in the table propagates consistently to every
presentation, so swapping Library's and History's icons passed the whole suite.
Pinned by identity (`NAV_SECTION_META.library.icon === Icon.layers`). This is the
same lesson as the two earlier unfalsifiable tests, one level up: proving the
consumers read the source says nothing about the source being right.

**The host CSS comment overstated what a hidden host preserves.** It claimed search
text and scroll survive "a section switch", which is true for the upper pane's two
roles and deliberately FALSE for the lower pane — the switcher clears the shared
`libraryFilter` and activating the destination repaints its search box and list. The
comment now separates the three cases (upper role switch, lower section switch, and a
move between containers), because a phase-3 maintainer reading the old wording would
assume lower search preservation was already solved when it is phase 3's job.

Also asserts both lower lists stay mounted across a section switch, not merely that
their object identity holds — identity alone would survive a host being detached and
replaced by a look-alike.

Part of #487.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
feat(#487): navigation section registry (phase 2)
…step 1)

Adds the pure core pieces phase 3 needs before any UI wiring: a resize
session (begin/advance/commit) that fixes the sampling-dependent restore
memory from phase 1 for both bands without reintroducing it (two rounds
of adversarial review caught that "continuously updated" memory has the
same bug relocated), and clampLeftNavigationToMaximumTotal + the viewport-
aware separator ARIA ceiling for the centre-width safety constraint. No
production consumer yet — wiring lands in later steps of this phase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
New src/application/left-nav.ts: openFocusedSection/toggleFocusedSection
compose the pane-selection write and the layout write into ONE batch(),
fixing a real atomicity gap the phase-2 handoff flagged (two independently
batched functions can let an effect observe a mismatched intermediate
state). Also fixes a confirmed persistence gap: opening a lower section
via this seam now persists `sidePanel` exactly like the wide sidebar's own
tab switch already does (saved-history.ts's switchTo), so a rail/drawer
selection survives a reload.

Wires app.openFocusedSection as the deterministic seam #428 needs. No UI
calls it yet — the rail/drawer land in a later step of this phase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
…phase 3, step 3)

Replaces the shared state.libraryFilter with lowerNavigationFilters, a
per-section record — switching between Library and History no longer
clears the search box, required by this phase's own acceptance bullet
that wide and focused presentations share and preserve all navigation
state (a deliberate, user-visible change from phase 2's "unchanged"
gate, documented in the CHANGELOG).

Splits renderSavedHistory so both lower sections render their own
content unconditionally, mirroring the upper pane's renderSchema/
renderDashboardTree pattern, closing two confirmed bugs: a section not
active at mount never painted until the first switch, and a section's
content going stale when its own data changed while the other section
was exposed (History-while-Library-active, and the reverse). The two
call sites that guarded a History repaint behind `sidePanel === 'history'`
(app.recordHistory, the script-run history path) are now unconditional.

Also closes #572: the lower switcher's tabs gain type="button" and
aria-pressed, matching the upper switcher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
An independent ChatGPT review of the phase-3 steps 1-3 diff found a real
bug, confirmed by hand: commitLeftNavigationResize compared two
POST-CLAMP layouts (effective vs effectiveAtStart) to decide whether a
band's width changed. That's wrong for a restore command (Home/End/a
bare-rail ArrowRight) under an active viewport clamp — a bare rail's
dormant preferred width passes through effectiveAtStart unclamped (there
is nothing to clamp yet), so a restore's honest, unclamped proposal gets
compared against it, differs, and the TRANSIENT clamped render value gets
committed instead of the user's real preference. E.g. a 420px preference
restored on a ~800px window would silently downgrade to ~313px.

Fix: the session now tracks the RAW reducer proposal separately from the
clamped effective layout, and the commit decision compares the raw
proposal against the preference, never the clamped value. This also
folds the viewport clamp into advanceLeftNavigationResize itself, so a
future caller can no longer forget to apply it.

Also: openFocusedSection/toggleFocusedSection no longer re-persist
sidePanel when the value is already current (relevant since #428's
bounded drag-hover reasserts the same section repeatedly), a stale test
that checked a field name already renamed in step 3, a dead field on
WorkbenchStateSlice, and stale doc references in ADR-0001 and
saved-query-service.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
Two new standalone modules, not yet wired into the app shell:

- src/ui/left-rail.ts: the compact icon rail, four launchers built from
  the section registry (nav-sections.ts) so a rail tooltip/aria-label can
  never disagree with the wide switchers' own label for the same section.
  A click routes through toggleFocusedSection.

- src/ui/left-nav-separator.ts: the resize/mode-changing separator that
  will replace splitters.ts's 'col' axis in the next step. Mirrors
  splitters.ts's existing mouse-event drag model (not Pointer Events).
  Every pixel decision routes through the LeftNavigationResizeSession
  from left-nav-layout.ts; this module's own job is pointer/keyboard
  mechanics, session bookkeeping, and ARIA, never the resize arithmetic
  or painting the sidebar directly (that's the injected
  applyEffectiveLayout seam a later step implements). Terminates safely
  on blur and visibilitychange; mouseup processes its own final
  coordinate rather than the last mousemove's.

Caught during review before wiring anything up: commitSession never
called applyEffectiveLayout, so a keyboard-driven resize updated state
and ARIA but never actually repainted the sidebar (the pointer path only
worked because advanceTo already paints on every mousemove/final
mouseup). Fixed by having commitSession paint with the session's own
final effective layout — the one place a keyboard resize ever reaches
the DOM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
…-shell (phase 3, step 4b)

The feature becomes reachable. .sidebar is RE-PRESENTED, never moved or
duplicated: a data-nav-mode attribute (wide/rail/drawer) selects between
the wide two-pane sidebar and the rail's focused drawer, written by ONE
function (applyEffectiveLeftNavigationLayout) that is the sole authority
for every presentation attribute — hidden toggles, width, the drawer's
title, aria-labelledby. It's called by initial mount, a "preferred-state"
effect (mode/section/isMobile changes), the resize separator's own
session steps, and an injected shell-width observer seam (so a plain
browser-window resize with no active drag still honors the centre-content
minimum).

splitters.ts's 'col' axis is deleted entirely — left-nav-separator.ts
owns that gesture now, routing every proposal through the mode reducer
instead of a bare clamp. Reconciles four stale comments (nav-sections.ts,
styles.css) that described DOM host movement, a design this phase
rejected in favor of re-presentation.

Also wires a real ResizeObserver-backed default for the shell-width
observer seam into app.ts (mirroring the existing matchMedia injection
pattern) — the seam existed and was tested from the prior commit but
had no production implementation, so a real user's window resize would
not have triggered it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
…licts

A second ChatGPT review of the rail/separator/app-shell composition
found four real bugs, all confirmed by hand before fixing:

1. CRITICAL: .side-pane/.side-tabs have `display: flex` class rules with
   no `[hidden]` override, so setting `.hidden` on the wide-only panes
   and tab rows did NOTHING in a real browser — the exact footgun this
   codebase already guards against for .query-host/.dashboard-host/
   .nav-section-host/.left-rail/.left-nav-title. Every unit test passed
   because happy-dom never computes CSS, but a focused drawer would have
   shown both panes and both tab switchers simultaneously in production.

2. The separator's own ARIA (aria-valuenow/valuemax/valuetext) went
   stale whenever the width changed from outside its own gesture (mount,
   a plain window resize) — its internal effect only re-runs on a mode/
   section signal change, never a width-only one. Fixed with a new
   refreshAria() the app-shell's presentation function calls on every
   one of its own invocations.

3. The ResizeObserver-driven reclamp could silently overwrite an active
   drag's in-progress (not-yet-committed) paint with stale committed
   state, visibly snapping the sidebar away from the pointer mid-drag.
   Fixed with a new isSessionActive() the observer callback checks
   before repainting.

4. A keyboard press could commit a second, conflicting session while a
   pointer drag was still active (reachable: Tab-focus the separator,
   mousedown it too, then press an arrow while the button is held).
   Fixed by ignoring keyboard input while a pointer session is active.

Plus small corrections: a stale CHANGELOG note claiming ResizeObserver
wiring was still a follow-up (it already landed), a stale "standalone,
not wired yet" module comment, an inaccurate per-button-selectivity
comment, and dispose() now clears the .dragging class and any
in-progress session instead of leaving them dangling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
…e 3, step 5)

Escape closes a focused drawer and returns focus to the rail launcher
that opened it, without acting when a nested handler already claimed
the key (a non-empty search filter, a saved-row edit form's own
Escape-to-cancel) or when there's no drawer to close. Fixes a real bug
along the way: the search box's Escape handler claimed every Escape
unconditionally, even when already empty, so Escape could never bubble
up to close the drawer while focus sat in an empty search box.

Converting a focused drawer to the wide sidebar (a resize-separator
drag past the wide threshold, or the End/bare-rail-ArrowRight keyboard
restore) restores focus to that section's own wide-mode tab, resolved
by a live querySelector at the moment of the transition since the tab
rows rebuild on every repaint. Both wide switchers' tab buttons gain a
data-section attribute to make this (and the rail's own focus-restore
target) addressable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
…to 180px

Real-browser (Playwright, chromium+webkit) coverage for the feature
happy-dom cannot see: folding to the rail, opening/switching/closing a
focused drawer, the drawer pushing the work surface rather than
floating over it, Escape-to-close with focus restoration, dragging back
to wide with focus landing on the matching tab, separator keyboard
operation, and the mobile rail-suppression fallback. Re-fixtures the
one e2e test that was actually stale under the new fold/hysteresis
rule (a drag to clientX=100 used to clamp to a 180px floor; it now
folds to the rail) into two tests — the dead zone at a different
coordinate, and the new fold itself.

This pass settled phase 1's open question about the focused drawer's
resize floor: at 140px (as shipped) the Dashboards section's three
titles rendered as "Sa...", "O...", "A..." — unreadable and
indistinguishable — while at 180px they read as "Sales re...", "Ops
late...", "A very lo..." — still ellipsized but genuinely readable.
Raised the drawer's floor to 180px, reusing LEFT_PANEL_MIN_PX rather
than a second constant, which gives the drawer the wide sidebar's own
dead-zone mechanism for free from the existing clamp — except the
keyboard path, which needed an explicit mirror of the wide sidebar's
own "already at floor, step left folds" check, since arrow keys take
fixed relative steps that can get stranded in the newly-separated
fold-threshold/floor gap the way the wide sidebar's keyboard handling
already had to solve once before.

Also fixes a real, confirmed WebKit-only bug the e2e pass surfaced:
clicking a rail icon to close its own drawer didn't return focus to
the icon on Safari (WebKit doesn't focus a clicked button natively,
unlike Chromium/Firefox, unlike the Escape path's explicit focus call).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
…ch panes

Independent review found a real regression: revealDashboard (File Menu
-> New Dashboard, and after an import commits) and revealAssignedPanel
(Library "Add to dashboard...", drag/drop settlement, Panels-row
create-and-assign) wrote state.upperRole directly, bypassing the
openFocusedSection seam this phase built specifically so opening a
section works whether the nav is wide, folded, or already showing a
drawer on something else.

Before this phase the sidebar was always the wide two-pane view, so a
bare upperRole write was always enough to make the Dashboards pane
visible. Now that folding exists, folding the nav (or opening a drawer
on Library/History/Databases) before triggering either reveal action
left the tree's own expand/scroll/select work happening inside a
hidden pane, or, at a bare rail, behind an entirely hidden sidebar.

Both now route through openFocusedSection, which reduces to the exact
same upperRole write in wide mode (resolveRailOpen is a no-op there)
and additionally opens/switches the focused drawer when folded.

Also: closes #572 (never logged in the CHANGELOG when it shipped in an
earlier commit of this phase), and a stale nav-sections.ts comment
still describing host movement between containers instead of the
re-presentation this phase actually uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
A comprehensive final review of PR #574's complete diff found four real,
confirmed bugs interacting across pieces that were each reviewed separately
earlier: leftNavSection going stale across a mobile round-trip, an active
resize session persisting through that same breakpoint crossing, a
blur/visibilitychange commit using a stale pre-shrink clamp, and focus
restoration firing on transient frames of a non-monotone drag. All four
verified against the real code before fixing, and fixed together since two
share one reconciliation point (the isMobile-transition handling).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZfeGpkpJw2zsfPtysGHUN
…t-src

The demo release's connect-src was bounded to a fixed 4-origin allowlist
(the chart default), so "Advanced — connect to another server" against any
other ClickHouse host was silently blocked by CSP and surfaced as a bare
"Failed to fetch" with no indication the request never left the browser.
This shared instance is used ad hoc against arbitrary clusters, so its
values-demo.yaml override now sets connectSrc to the "https:" scheme-source
instead of inheriting the fixed list. The chart's own secure-by-default is
unchanged for every other install.

Closes-context: #575 (follow-up UX gap, filed separately)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWApnNReVKwis3HdFfeUga
fix(deploy): allow any HTTPS host on sql.demo.altinity.cloud's connec…
A fresh integrated pass on PR #574 found one new preference-corruption
blocker and two remaining interaction/accessibility gaps, all confirmed by
hand before fixing:

1. BLOCKER: a click-and-release on the resize separator with no genuine
   movement could silently overwrite the stored wide/drawer width
   preference — mousedown recorded no starting coordinate or grip offset, so
   mouseup always treated the release clientX as a brand-new raw proposal,
   even a pure click landing wherever a viewport clamp happened to render
   the handle. The session now records the grip point and skips advancing
   entirely on a genuine no-op click; every advance also subtracts the
   recorded grip offset so a drag grabbed anywhere in the 7px handle tracks
   the pointer from its own grab point rather than the handle's left edge.

2. A semantic left-navigation command (Escape, a rail click, a programmatic
   reveal) running while a pointer resize was still in progress could be
   silently undone by that drag's own eventual commit, since the drag keeps
   its own uncommitted layout snapshot independent of state.
   application/left-nav.ts's openFocusedSection/toggleFocusedSection now
   call an injected preemptActiveResize seam first, which app-shell.ts wires
   to cancel the active session and repaint from the committed layout — one
   choke point every caller goes through, rather than each call site having
   to remember it.

3. Crossing into the mobile breakpoint from an open focused drawer could
   move focus onto a hidden desktop tab button, because the wide-mode focus
   restoration keyed only on navMode === 'wide', which the mobile
   projection also forces. Now gated behind !state.isMobile.value.

4. Folding an open drawer to the bare rail via a pointer drag left focus
   stranded in the drawer's now-hidden content, with no restoration to the
   rail launcher — the fourth transition, unlike Escape-close/rail-icon-
   close/drawer-to-wide, that had none. Scoped to
   sidebar.contains(document.activeElement) so a keyboard fold (which needs
   the separator itself focused) is left alone.

Also corrected two CHANGELOG overstatements (live-region announcement
coverage; an "omitted in every test" claim contradicted by several tests
that inject observeElementWidth) and added a forward-looking caveat on the
480px centre-minimum "never" claim pending phase 4's right inspector.

Every fix has unit coverage (including a direct pure-seam test for
preemptActiveResize) and, for the click-without-drag blocker, real-browser
Playwright coverage across chromium/webkit. Confirmed each new/updated test
actually fails without its corresponding fix before restoring it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgVCic37D9gREMNZDUk9BN
…ar transitions

The phase-3 review's restoration logic only ever used a TRACKED focused
section, which core/left-nav-layout.ts's own mode/section coherence
invariant keeps null throughout 'wide' — so a wide sidebar folding straight
to bare rail (no drawer step in between) had no restoration target even
with focus genuinely inside it. Fixed by asking the DOM which
[data-section] host the actually-focused element sits under, falling back
to it only when no tracked section is available.

Two more gaps existed because CSS alone can hide .sidebar independently of
data-nav-mode: entering mobile Editor/Results, and switching to the
Dashboard surface on mobile. Both now land focus on a stable bottom-nav
button instead of losing it. A real Chromium probe confirmed the browser
drops a focused descendant to <body> by the next microtask once an
ancestor goes hidden unless something explicitly moves it first — a
failure shape happy-dom cannot demonstrate at all.

Two related gaps surfaced while verifying this in real browsers are
tracked as a follow-up, not fixed here: the resize separator's live
multi-frame drag repaint can blur focus on an intermediate frame before
any commit-time JS runs (a pre-existing gap, not unique to this fix), and
the mobile-crossing rescue's own focus() call was found to race the
browser's async layout recompute under real parallel load on Chromium
specifically — so that path is unit-tested only, not e2e-asserted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Totddr5jBYDfFSau2ZnWPq
…87p3

feat(#487): rail, focused drawer and resize separator (phase 3)
Move ship and ship-phase out of .claude/skills into a new root-level
skills/ directory, and symlink .claude/skills, .agents/skills, and
.codex/skills to it so Claude Code, the Codex CLI, and generic
agent tooling share one source of truth instead of three copies.

Also vendor the sql-browser-dashboard skill: it turns an already-known
SQL/result-column investigation into a validated PortableBundleV2
Dashboard bundle and publishes it through the save_dashboard MCP tool
(or leaves it as a downloadable JSON file when that tool isn't wired
up). Verified its authoring profile field-for-field against this
repo's real schemas/*.json — no drift found.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3pqYYJrP8zAmLmrJvzcxx
chore(skills): promote skills/ to a first-class repo-root directory
Inventories current shipped UX plus committed-but-unshipped product
contracts (#487, #488, #214, #420-423) by surface, split into
shipped/target-contract/accessibility-gaps/invariants, for handoff into
the V2 professional UI redesign tracked in #582.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXFetrHXThuDbpgnfJyTyA
Bumps the dev-dependencies group with 1 update: [@playwright/test](https://github.com/microsoft/playwright).


Updates `@playwright/test` from 1.62.0 to 1.62.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](microsoft/playwright@v1.62.0...v1.62.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Aug 3, 2026
@dependabot
dependabot Bot requested a review from BorisTyshkevich as a code owner August 3, 2026 06:24
BorisTyshkevich and others added 28 commits August 4, 2026 22:11
…hape change

`syncSqlRoute` and `rewriteWorkspaceRoute` moved off the flat `App` contract
onto `app.nav` in wave 4 of the composition-root decomposition; production
call sites were repointed but two raw-ESM e2e fixtures
(import-example-dashboard.html, oauth-document-recovery/index.html) still
called the old flat members directly, throwing during module evaluation and
leaving window.__ready/__oauthRecoveryReady unset — a silent
page.waitForFunction timeout rather than a clear error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e comments

Readiness-review findings applied before opening the PR: a phase-4
CHANGELOG entry under [Unreleased], Architecture.md/Source-Map.md updated
for the new src/application/workspace-session.ts and
src/application/surface-navigation.ts modules plus the two extracted
src/ui/workbench/* controllers, six now-unused imports removed from
app.ts (fixedAnchor, SavedQueryV2, QueryOrName, Result, ScriptResult,
ScriptEntry — their only users moved to the extracted modules), and two
dashboard.test.ts comments corrected from the removed flat
`app.serializeWrite` to `app.workspaceSession.serializeWrite`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cumented var-strip signature gap

app.types.ts's header comment still described the pre-wave-5 `const app = {}
as App` cast, contradicting the typed-literal construction that replaced it.

The Workbench var-strip rebuild signature (moved verbatim from app.ts, present
on origin/main before this PR) folds in enum option COUNT but not option
IDENTITY, so a same-cardinality option-set change doesn't trigger a rebuild
and the dropdown goes stale. Pre-existing, not introduced by this refactor;
documented in place and filed as #605 rather than fixed here, matching how
#603 (the popover stale-close defect) was handled — a pure structural
extraction is not the place for a behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…root-588

refactor(#588): decompose the createApp composition root (phase 4)
… (wave 1)

Move the repaint-decision logic out of renderDashboard's effect() callback in
ui/dashboard.ts into a pure dashboard/application/dashboard-repaint-plan.ts —
no DOM, no signals, 100% covered. dashboard.ts still owns every side effect
and commits the returned memo fields one at a time, at the exact point each
corresponding side effect runs, preserving partial-failure semantics if a
side effect (e.g. the variable-persist save seam) throws mid-publish. The
grid-drag-cancel path's direct lastGridSig reset becomes an explicit
gridStructureInvalidationRev counter the planner alone consumes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…esture concurrency (wave 2)

Move wireTileDrag/wireGridResize/the modifier-cue install out of
renderDashboard's closure into src/ui/dashboard-tile-gestures.ts behind an
injected TileGestureDeps seam. Zero functional change: deliberately keeps
the pre-existing concurrency model a naive reading would not expect — a
resize has no guard against an active drag or another resize, the shared
gesture-cancel slot is last-writer-wins/self-clearing, neither gesture
filters window pointermove/pointerup by pointerId, and a drag snapshots the
active engine once at pointerdown while its rendered-surface lookups stay
live for the same gesture. Added a characterization suite to
dashboard.test.ts (run and green against both the pre- and post-extraction
code) plus a dedicated dashboard-tile-gestures.test.ts for the new module.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…tests, reconcile docs (wave 3)

Retires the dashboard.test.ts DOM-simulation gesture/resize-cancel mechanics
that Wave 2's dashboard-tile-gestures.test.ts and Wave 1's
dashboard-repaint-plan/-integration.test.ts now cover directly and more
thoroughly. Fully redundant blocks (no-arm/non-primary-press gating, the
modkey toggle, the flow drop-target mechanics, the resize-cancel variants,
the grid clamp math) are deleted; a handful thin to the one real
production-wiring case per behavior the plan calls for. Net: dashboard.test.ts
closes 3 lines below its pre-Wave-1 baseline despite waves 1-2 having added
205 lines of new characterization tests, satisfying #589's AC5. Also
consolidates the two wave-level CHANGELOG bullets into one phase-level entry
(matching the #588 precedent) and reconciles .wiki/Architecture.md +
.wiki/Source-Map.md for the two new modules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…set, correct dispose() doc order

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…-bag work on early return, broaden single-authority proofs

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…ity, broaden sabotage coverage, fix stale doc comment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…, strengthen interleaving proof

Finding A (P2): dashboardRepaintPlan's own composition and ui/dashboard.ts's
granular plan* call sequence are two independently-maintained protocols for
the same decision logic. Added a protocol-equivalence test that manually
threads the six granular functions the exact way dashboard.ts does and
asserts dashboardRepaintPlan produces the byte-identical result, across four
representative publishes (unchanged republish, bar rebuild, engine switch,
array-valued persist). Sabotage-verified: hardcoding a wrong rebuildBar
argument into dashboardRepaintPlan's internal composition breaks the new
test.

Finding B (P3): the existing compute/apply interleaving regression test only
proved the bar-rebuild stage survives a same-publish persist throw. Added two
new tests proving the same for pushOptions and refreshTimeRangeLabels — the
two stages gated `!rebuildBar`, so they can only be exercised on a refresh()
publish rather than a committed-value publish. Each test tags every publish
with a generation counter (bumped by planBarRebuild, called first every
publish) so the persist-throw lands on the exact publish where the target
flag's real side effect fires, regardless of a refresh wave's several
internal publishes. Sabotage-verified: reordering persist ahead of
options/label in ui/dashboard.ts's effect breaks both new tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…ues #606/#607

The wave-3 CHANGELOG entry cited dashboard.ts's line-count delta as measured
at that point (-509); a later ChatGPT-review correctness fix (restoring
compute/apply interleaving) added ~53 lines back, landing the final count at
-456 — below AC4's ~500-700 target. Corrected here rather than left stale, per
this PR's own disclosure. Also cross-references the two inbox issues (#606,
#607) filed for the deliberately-preserved gesture-concurrency and
engine-snapshot quirks, and notes the six-function decomposition the
interleaving fix introduced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
…s-589

refactor(#589): extract dashboardRepaintPlan + createTileGestureController out of renderDashboard (phase 5)
…oops

Collapse the attended/unattended split: /ship is now one autonomous flow —
coordinator + fresh worker per unit, one integration branch and one PR per
run. Every unit's plan iterates through a coordinator-run ChatGPT plan
review loop (exit on VERDICT: APPROVED, max 5 passes); the PR iterates the
existing code review loop (exit on VERDICT: SHIP, max 3, script-enforced).
Auto-merge without prompting when all proof conditions hold at one exact
head; any failure — including loop exhaustion — is a full stop for a human
decision.

Also fixes the inconsistencies found in review: the local-e2e contradiction
in unattended.md, the unsatisfiable "CI all engines" claim, the stale
"third pass" merge rule in ship-phase (deleted) and both wiki pages, the
steps-2–5 wording drift, the Chrome single-session concurrency hazard (all
chatgpt-review invocations are coordinator-serialized), and vague
"approved temporary directory" phrasing. per-issue-cycle.md is renumbered
to its own steps 1–4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
chore(skills): make /ship single-mode with plan/code ChatGPT review loops
Encode the plan and code review loops as deterministic Workflow scripts
instead of prose the coordinator follows by hand:

- references/plan-review-loop.workflow.mjs — the whole 5-pass plan loop in
  one run: serialized chatgpt-review plan passes, schema-validated verdict
  (fail-closed to REVISE), parallel read-only verification of every
  finding, and a revise agent that folds accepted findings into the plan
  file in place (rejected ones become "## Review responses" rebuttals).
  Cap 5 is a loop bound.
- references/code-review-pass.workflow.mjs — one PR pass per run: review
  (publishes the comment), parallel verification, then a fix agent that
  applies accepted findings with tests, loops the full local gate to
  green, and commits locally only. The coordinator still owns push, CI
  wait, re-invocation, and the gate; the 3-pass cap stays enforced by the
  chatgpt-review script.
- references/review-loops.md — the invocation contract: args, return
  statuses and required coordinator actions, and the hard rules
  (coordinator-only, one review workflow at a time, question files carry
  the verdict protocol, findings never silently dropped).

SKILL.md steps 2.2/2.3/3.5, the operating rules, per-issue-cycle.md's plan
review section, and the footguns are rewired accordingly. One behavior
change: the loop's revise agent (not the waiting worker) edits the plan
file, so the worker re-reads the approved plan before implementing — the
file, not the worker's draft, is the contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bdx3RwYDjVdPdFCggKVQ
chore(skills): run /ship review loops as Workflow scripts
The plan-review-loop and code-review-pass workflow scripts had three
bugs discovered while running /ship end-to-end against the ChatGPT
review loops:

- Workflow's `args` arrives JSON-encoded as a string, not parsed, even
  when the caller passes a real object. Both scripts now defensively
  JSON.parse it, so `args.field` reads no longer silently see
  `undefined` and fail the required-args check.
- The review-runner agent must genuinely wait out a 10-25 minute
  external ChatGPT review. Running the CLI in the background got the
  agent force-terminated (structured-output-enforce) under two minutes
  in, before any response could exist; running it in the foreground
  with no --timeout let the Bash tool's own 10-minute cap kill the
  process before it could flush any JSON. Both scripts now run the CLI
  in the foreground with an explicit --timeout under that cap, retry
  with --session on a non-terminal status (with backoff on
  rate_limited), and — this is the one that actually discarded a
  complete review — treat completion as "response_text has a
  parseable trailing VERDICT line", not "status === completed": a
  UI-level rate-limit banner can appear over an already-finished
  answer, and the literal status field is not authoritative about
  whether real content exists.
- 'low' effort on the review-runner agent was observed capping its
  turn/wall-clock budget so tightly it got force-terminated before any
  real work happened; removed.

Also implements the requested coding-vs-planning model split: sonnet
for coding/implementation agents (worker implement step, finding
verification, fix-accepted-findings), fable at high effort for
planning agents (worker plan-writing step, the plan-review loop's
revise-the-plan agent). SKILL.md's step 2.1 now spawns a
plan-only planner and a separate fresh coding agent in step 2.3,
rather than one persistent worker resumed across both roles, since a
spawned agent's model can't change mid-conversation.

plan-review-loop.workflow.mjs also gains an optional session/
conversationUrl/startPass in args, so the coordinator can resume a
conversation already in progress after a needs_human/error return
instead of abandoning it and starting a fresh one.
ChatGPT virtualizes/prunes older turns out of the DOM in long
conversations (confirmed live: only the last two rendered turns stay
mounted, e.g. conversation-turn-6 and conversation-turn-8 — earlier
ones are gone). browser.mjs's completion and recovery-vs-fresh-submit
decisions were both built on assistantCount(page) > before, a raw
element count that assumed the DOM only ever grows. Once pruning
kicks in for a sufficiently long conversation, that count plateaus
(or even drops), which silently broke two things at once:

- waitForCompletion's `count > before` check never became true, so
  it waited out the full --timeout and threw 'timed_out' with an
  EMPTY response_text — discarding a real, complete answer that was
  sitting there the whole time.
- review()'s recovery-vs-fresh-submission decision
  (`existingCount > recordedPasses`) could go the wrong way and
  submit a brand new duplicate message into the conversation instead
  of recovering the one already there. Confirmed live: at least one
  redundant resubmission went out and generated unwatched, and the
  plan file got uploaded 9 times under the same name, which is why
  ChatGPT's own upload UI started collision-renaming it
  (plan-590(9).md).

Fix: track a SHA-256 fingerprint of the current LAST assistant
message's text instead of a count.
`review()` now decides "is there an uncollected response" by
comparing the live tail's fingerprint against `session.
lastResponseFingerprint` (persisted on every successful pass) —
content identity, not position. `waitForCompletion`'s `before` is now
either the pre-submission baseline text (fresh submission: accept
only a tail that differs from it) or `null` (recovery: accept
whatever is already there). Both are immune to how many turns are
currently mounted.

Also names each pass's upload with the real pass number
(plan-590-pass4.md) instead of re-uploading the plan/diff file's own
literal path every time — that path is the review-session identity
and must never move, so a same-content, differently-named temp copy
is uploaded instead. Fixes the ChatGPT-side collision-rename
confusion above and, as a side effect, makes it easy to see in the
ChatGPT UI which upload belongs to which pass.

state.mjs's session record gains `lastResponseFingerprint` alongside
`passCount`. Removed the now-fully-unused `assistantCount()`.

Added two tests simulating a pruned DOM (assistant locator always
returns exactly one element, with different content across
submit/recovery) proving both paths detect the new response
correctly regardless of how many turns are actually mounted.
30/30 tests pass.
…ew-loop-fixes

chore(skills): fix /ship review-loop workflow args and model split
fix(chatgpt-review): detect completion by content, not DOM element count
… retire dashboardTreeRevision

app.currentWorkspace/app.mainSurface become signal-backed accessor pairs
(peeking getter, notifying setter) so a mutation is its own notification —
the #426/#427 bug class (a write site forgetting to bump the invalidation
counter) becomes structurally impossible. app.committedWorkspace
(ReadonlySignal<StoredWorkspaceV5|null>) and app.treeNavigation
(a computed structural key over kind/dashboardId/currentMember) are the two
tracked reads app-shell.ts's tab-count/tree/Library effects subscribe
through, replacing state.dashboardTreeRevision.

app.currentWorkspace's setter is asymmetric (no null): a transitional null
publication is a named departure operation owned by a new closure-private
surface-retirement coordinator in app.ts (retireToWorkspaceLoading/
-Missing/-Failure/-Login), which batches the publication atomically with
disposing any live shell so a surface never repaints against transitional
state — five independent review passes each found a different call site
where this raced disposal, hence one coordinator with exclusive mutation
authority rather than five patches. SurfaceStatePort/DashboardApp/TabsApp/
DashboardTreeApp are narrowed to readonly on currentWorkspace (the fourth,
DashboardTreeApp, was documented read-only but not type-readonly before
this change).

app.reloadDashboardRoute() (a post-commit fold-and-reassign that would
double-publish once the aggregate is signal-backed) is deleted;
afterLibraryChange's Dashboard branch calls the render-only
app.renderCurrentSurface() instead.

Tests: tests/unit/surface-lifecycle-arch.test.ts (a static-source scan
backing the coordinator's compile/scan-layered "no lifecycle bypass"
claim) and tests/unit/surface-accessor-contracts.test.ts (@ts-expect-error
fixtures for the asymmetric setter and the four narrowed ports) are new,
plus a new app.test.ts describe block covering the issue's Tests #1/#3/#5
and the plan's invariant map (delivery-only no-ops, adversarial-id
collision-freedom, failure-path status/null ordering, the four-arm
live-shell no-repaint sweep, one-commit-exactly-once settlement, and the
mixed-snapshot batch-ordering regression). fake-app.ts's makeApp() installs
real per-call signals on the returned object (object spread evaluates an
accessor pair into a plain value, so the fake needs the same defineProperty
treatment createApp() gets natively) for reactivity parity in fixtures.

No persisted/schema change, no user-visible behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…le wiki roadmap

Closes two gaps a review pass found against plan-590.md:

- tests/unit/app.test.ts: the plan's §5 "Issue Tests #4" / invariant (b)
  counting-discipline test was committed to but never added. commitUi
  (dashboard-tree.ts) and the reactive tree effect converge on the exact
  same renderDashboardTree function, so a DOM-content assertion alone
  can't tell "one correct imperative repaint" apart from "one imperative
  plus one erroneous reactive re-run" — both leave the same final DOM.
  Added a real-createApp() test asserting (i) a UI-driven chevron-toggle
  op (through commitUi) produces exactly one tree repaint and zero
  upper-tab/lower-pane repaints, and (ii) a direct dashboardTreeUi Map
  mutation with no UI op repaints nothing. Counts through
  deriveDashboardTree (application/dashboard-tree-model.ts), not
  renderDashboardTree itself: commitUi calls renderDashboardTree via a
  same-module binding, invisible to a vi.spyOn namespace patch, while
  deriveDashboardTree is a genuine cross-module call renderDashboardTree
  makes exactly once per invocation regardless of caller. Sabotage-verified
  both arms (a double render call in commitUi; a stray render call after
  the direct Map mutation) — both correctly fail the new test.

- .wiki/Decisions-and-Roadmap.md: reconciled the #593 refactor-umbrella
  phase list's #590 entry to reflect the work landing on
  wip/590-reactive-workspace, cross-referencing ADR-0001's new #590
  addendum, per CLAUDE.md's "reconcile forward work" discipline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…kspace

refactor(#590): make the committed workspace aggregate reactive, retire dashboardTreeRevision
feat(skills): add ChatGPT-authored /ship planning
Bumps the dev-dependencies group with 1 update: [@playwright/test](https://github.com/microsoft/playwright).


Updates `@playwright/test` from 1.62.0 to 1.62.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](microsoft/playwright@v1.62.0...v1.62.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
@BorisTyshkevich
BorisTyshkevich merged commit 5acceb3 into main Aug 5, 2026
1 check passed
@dependabot
dependabot Bot deleted the dependabot/npm_and_yarn/dev-dependencies-edf4b8f5b6 branch August 5, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant