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 intoAug 5, 2026
Conversation
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>
…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>
dependabot
Bot
deleted the
dependabot/npm_and_yarn/dev-dependencies-edf4b8f5b6
branch
August 5, 2026 14:57
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.
Bumps the dev-dependencies group with 1 update: @playwright/test.
Updates
@playwright/testfrom 1.62.0 to 1.62.1Release notes
Sourced from @playwright/test's releases.
Commits
26a9e47cherry-pick(#42043): docs: release notes for v1.62 Python, Java, and .NET (#4...0a81d5dcherry-pick(#42040): docs(release-notes): mention the isolated headless clipb...8376826cherry-pick(#42034): fix(aria): keep icon-only clickable elements in ai snaps...66c5cc9chore: mark v1.62.1 (#42020)9672bc3cherry-pick(#42009): fix(types): support branded primitives in evaluate argum...4325804cherry-pick(#41988): fix(aria): preserve names from collapsed text contributors9632f8echerry-pick(#42005): fix(tsconfig): do not throw when "extends"/"references" ...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 rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore <dependency name> major versionwill 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 versionwill 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