BUG: mode tray scroll jumps active tab to the left; highlight moves before the view (slow on real network) #243

Closed
opened 2026-09-27 16:36:07 +00:00 by kayg · 15 comments
Owner

Owner report from production (calternal.cloud, phone, 2026-09-27): "why is it that the tab switching animation now takes so long? and the active tab auto becomes the left one? the tab horizontal scroll positioning should not change when tabs are changed within the active view. the position should only change when active tab is switched to a tab not presently in view. and i notice that tab active state changes first and then the view renders … does not feel very smooth."

Causes found by the orchestrator (verify)

  1. Active tab jumps to the left edge. packages/ui/src/components/SegmentedControl.svelte layoutTrack() runs from a ResizeObserver on EVERY frame of the 240 ms width morph (the selected item grows to show its label, the old one shrinks). Each frame it calls revealTarget() (segmentScroll.ts) with mid-morph widths; while the growing item does not yet fit, it scrolls to that item's start, i.e. makes it the leftmost item. Mandatory scroll-snap then keeps it there. The window width (windowWidth) also changes per frame.
  2. Highlight first, view later. The selected state flips instantly by design (comment ~L609), but the new mode's route loads its data over the network. calternal.cloud is in Germany; the owner is in India (~150–250 ms RTT per request), so the highlight moves and the view appears a few hundred ms later. Locally this was invisible.
  3. "Animation takes so long" is probably (2) plus the 240 ms morph; measure it.

Required behaviour

  • Switching tabs never scrolls the tray if the newly active tab is fully visible in the FINAL layout (after the morph). Only a tab that would be out of view scrolls, minimally, once, smoothly, to its final position. No per-frame scrolling; compute from the final geometry (e.g. measure the target widths once, or scroll after the morph ends). Scroll-snap must not move the row after a tab change.
  • Highlight and view change together: start loading the target mode on pointerdown/touchstart (SvelteKit preloadData/preloadCode + the mode's data), show the new view in the same frame as the highlight when data is cached, and keep recently visited mode views/data cached (stale-while-revalidate) so switching back is instant. When data is not ready, the view switches immediately to the mode's own skeleton/last-known content, never a blank or the old view lingering after the highlight moved.
  • Morph ≤ 200 ms (align with the #236 motion tokens; ≤ 500 ms total cap), reduced motion = instant.

Evidence required

  • Unit tests for the reveal rule (in-view → no scroll; out-of-view left/right → minimal scroll; no change during morph).
  • e2e on a production build with network throttled to 200 ms RTT (Playwright CDP Network.emulateNetworkConditions): record tab switches at 390 px; assert scrollLeft unchanged for in-view switches, and that the new view's first content paints within 1 frame of the highlight when cached and within 100 ms of pointerdown otherwise (skeleton). Frame strips/video attached (not committed).
Owner report from production (calternal.cloud, phone, 2026-09-27): "why is it that the tab switching animation now takes so long? and the active tab auto becomes the left one? the tab horizontal scroll positioning should not change when tabs are changed within the active view. the position should only change when active tab is switched to a tab not presently in view. and i notice that tab active state changes first and then the view renders … does not feel very smooth." ## Causes found by the orchestrator (verify) 1. **Active tab jumps to the left edge.** `packages/ui/src/components/SegmentedControl.svelte` `layoutTrack()` runs from a ResizeObserver on EVERY frame of the 240 ms width morph (the selected item grows to show its label, the old one shrinks). Each frame it calls `revealTarget()` (`segmentScroll.ts`) with mid-morph widths; while the growing item does not yet fit, it scrolls to that item's start, i.e. makes it the leftmost item. Mandatory scroll-snap then keeps it there. The window width (`windowWidth`) also changes per frame. 2. **Highlight first, view later.** The selected state flips instantly by design (comment ~L609), but the new mode's route loads its data over the network. calternal.cloud is in Germany; the owner is in India (~150–250 ms RTT per request), so the highlight moves and the view appears a few hundred ms later. Locally this was invisible. 3. **"Animation takes so long"** is probably (2) plus the 240 ms morph; measure it. ## Required behaviour - Switching tabs never scrolls the tray if the newly active tab is fully visible in the FINAL layout (after the morph). Only a tab that would be out of view scrolls, minimally, once, smoothly, to its final position. No per-frame scrolling; compute from the final geometry (e.g. measure the target widths once, or scroll after the morph ends). Scroll-snap must not move the row after a tab change. - Highlight and view change **together**: start loading the target mode on pointerdown/touchstart (SvelteKit `preloadData`/`preloadCode` + the mode's data), show the new view in the same frame as the highlight when data is cached, and keep recently visited mode views/data cached (stale-while-revalidate) so switching back is instant. When data is not ready, the view switches immediately to the mode's own skeleton/last-known content, never a blank or the old view lingering after the highlight moved. - Morph ≤ 200 ms (align with the #236 motion tokens; ≤ 500 ms total cap), reduced motion = instant. ## Evidence required - Unit tests for the reveal rule (in-view → no scroll; out-of-view left/right → minimal scroll; no change during morph). - e2e on a production build with **network throttled to 200 ms RTT** (Playwright CDP `Network.emulateNetworkConditions`): record tab switches at 390 px; assert scrollLeft unchanged for in-view switches, and that the new view's first content paints within 1 frame of the highlight when cached and within 100 ms of pointerdown otherwise (skeleton). Frame strips/video attached (not committed).
Author
Owner

Started tray-fix on branch job/tray-fix, based on dev at 0558cc4eca. I’m tracing the existing segment geometry, route loading, and production e2e harness before changing behavior.

Started tray-fix on branch job/tray-fix, based on dev at 0558cc4ecaeb7f51af1e06acd31870b5f87b9f00. I’m tracing the existing segment geometry, route loading, and production e2e harness before changing behavior.
Author
Owner

Confirmed in code: SegmentedControl.svelte calls layoutTrack from a ResizeObserver on the track and every tab, and its active-id effect also calls layoutTrack after tick. layoutTrack both sizes the viewport and writes scrollLeft through revealTarget, so width-morph frames can trigger reveal decisions from intermediate geometry as reported. TabBar currently has no pointerdown preload hook; mode selection calls goto from click only.

Confirmed in code: SegmentedControl.svelte calls layoutTrack from a ResizeObserver on the track and every tab, and its active-id effect also calls layoutTrack after tick. layoutTrack both sizes the viewport and writes scrollLeft through revealTarget, so width-morph frames can trigger reveal decisions from intermediate geometry as reported. TabBar currently has no pointerdown preload hook; mode selection calls goto from click only.
Author
Owner

Production build reported Svelte warning for TurnHistory's captured when it chose the Ask list limit. Changed the limit to a derived value; the rebuilt production output contains no Svelte warning.

Production build reported Svelte warning for TurnHistory's captured when it chose the Ask list limit. Changed the limit to a derived value; the rebuilt production output contains no Svelte warning.
Author
Owner

The production build reported Svelte diagnostic state_referenced_locally on TurnHistory kind usage. Changed listLimit to a derived value. The rebuilt production bundle completed without Svelte warnings.

The production build reported Svelte diagnostic state_referenced_locally on TurnHistory kind usage. Changed listLimit to a derived value. The rebuilt production bundle completed without Svelte warnings.
Author
Owner

Production evidence at 390 px with 200 ms RTT: the cold Files switch first painted 1,181 ms after pointerdown because goto began loading the cold route before the browser painted the loading state. Cold navigation now yields two animation frames for the mode status before goto; the cached path remains immediate. I am rerunning the production flow against this change.

Production evidence at 390 px with 200 ms RTT: the cold Files switch first painted 1,181 ms after pointerdown because goto began loading the cold route before the browser painted the loading state. Cold navigation now yields two animation frames for the mode status before goto; the cached path remains immediate. I am rerunning the production flow against this change.
Author
Owner

During production E2E validation, the launcher initially used /home/kayg/Developer/calternal-wt/fonts/target/debug/calternal-server. The server embeds apps/web/build at compile time (crates/calternal-server/src/main.rs uses RustEmbed), so it served another worktree's frontend and those traces did not exercise this branch. I am building the server in tray-fix and will rerun against the matching production assets.

During production E2E validation, the launcher initially used `/home/kayg/Developer/calternal-wt/fonts/target/debug/calternal-server`. The server embeds `apps/web/build` at compile time (`crates/calternal-server/src/main.rs` uses `RustEmbed`), so it served another worktree's frontend and those traces did not exercise this branch. I am building the server in `tray-fix` and will rerun against the matching production assets.
Author
Owner

The required merge from dev conflicted in TabBar.svelte, +layout.svelte, and the tray unit tests. I preserved dev's contextual bottom bar and carried the tray's pointerdown preload/cancel callbacks through the new TabBar branch. Pending mode navigation now hides stale contextual actions so the selected mode tray and loading view stay aligned. The focused tray suite passed: 16 tests.

The required merge from `dev` conflicted in `TabBar.svelte`, `+layout.svelte`, and the tray unit tests. I preserved `dev`'s contextual bottom bar and carried the tray's pointerdown preload/cancel callbacks through the new `TabBar` branch. Pending mode navigation now hides stale contextual actions so the selected mode tray and loading view stay aligned. The focused tray suite passed: 16 tests.
Author
Owner

The first post-merge production E2E run reached setup, then could not find the Calendar tab in the 390 px tray. A new TabBar integration test reproduced the cause: ReferenceError: onpreload is not defined at the mode branch render. The merge had retained callback attributes but dropped their $props() bindings. Restoring onpreload and onpreloadcancel makes the focused suite pass: 17 tests.

The first post-merge production E2E run reached setup, then could not find the Calendar tab in the 390 px tray. A new TabBar integration test reproduced the cause: `ReferenceError: onpreload is not defined` at the mode branch render. The merge had retained callback attributes but dropped their `$props()` bindings. Restoring `onpreload` and `onpreloadcancel` makes the focused suite pass: 17 tests.
Author
Owner

The merged production tray now renders Calendar and Files correctly. A 390 px production run with 200 ms RTT reached the Files loading view with the tab selected on its first sampled frame, but that frame was 103.5 ms after pointerdown (3.5 ms over the 100 ms threshold). A separate run passed the in-view checks (scrollLeft 0, no scroll events, selected item visible) and failed the measured <=200 ms morph assertion. The component sets the label morph to 140 ms; multiple Cargo builds, tests, and checks were running on this shared host during these samples, so I am treating the timing overages as load findings under the issue's rule. The recording remains at artifacts/tray-fix/tray-390px-200ms-rtt.webm and will be attached with the final report.

The merged production tray now renders Calendar and Files correctly. A 390 px production run with 200 ms RTT reached the Files loading view with the tab selected on its first sampled frame, but that frame was 103.5 ms after pointerdown (3.5 ms over the 100 ms threshold). A separate run passed the in-view checks (`scrollLeft` 0, no scroll events, selected item visible) and failed the measured <=200 ms morph assertion. The component sets the label morph to 140 ms; multiple Cargo builds, tests, and checks were running on this shared host during these samples, so I am treating the timing overages as load findings under the issue's rule. The recording remains at `artifacts/tray-fix/tray-390px-200ms-rtt.webm` and will be attached with the final report.
Author
Owner

Starting final continuation on job/tray-fix, based on merge base 3ad937ec888c1fe45ce6e1d3052383e024dc5220. Current committed head: 1489013b3db88f8143a1e84d75d44b7c5347f561. I will merge current dev once, run the required final gates once, and report any remaining work.

Starting final continuation on `job/tray-fix`, based on merge base `3ad937ec888c1fe45ce6e1d3052383e024dc5220`. Current committed head: `1489013b3db88f8143a1e84d75d44b7c5347f561`. I will merge current `dev` once, run the required final gates once, and report any remaining work.
Author
Owner

The final local-server adversarial round found one non-load response inconsistency and I filed it as #276. The authorization matrix made 936 requests across 234 OpenAPI operations and four identities; a standard User's PUT /api/v1/admin/config request returned 422 where the matrix expected 403. This probe did not gain access. The finding and reproduction are recorded in #276.

The final local-server adversarial round found one non-load response inconsistency and I filed it as #276. The authorization matrix made 936 requests across 234 OpenAPI operations and four identities; a standard User's `PUT /api/v1/admin/config` request returned 422 where the matrix expected 403. This probe did not gain access. The finding and reproduction are recorded in #276.
Author
Owner

A second adversarial finding is filed as #277. The cross-plugin isolation probe shared Photos with another User and opted the recipient into Shared/<owner>/Photos; /api/v1/photos/timeline returned HTTP 200 with an empty day list for the full 60-second refresh window. The test and shared-host context are recorded in #277.

A second adversarial finding is filed as #277. The cross-plugin isolation probe shared `Photos` with another User and opted the recipient into `Shared/<owner>/Photos`; `/api/v1/photos/timeline` returned HTTP 200 with an empty day list for the full 60-second refresh window. The test and shared-host context are recorded in #277.
Author
Owner

Completed the #243 continuation on job/tray-fix.

Commits

  • fda7edb1 Stop mode tray revealing tabs during width morph.
  • e4a98411 Preload mode routes on pointerdown.
  • 8ec8af18 Warm mode routes and retain recent view data.
  • 5e1ab5ee Derive Ask history limit from mode kind.
  • 9a427538 Switch mode views with pointerdown preloading.
  • a0debedb Keep mode label morphs within the phone frame cap.
  • 1489013b Restore mode tray preload callback bindings.
  • 7f03fda3 Merge current dev; preserve both listing cache behavior and RequestCache propagation.

Head: 7f03fda38844372291e55f4461454c6706092128 (pushed to origin/job/tray-fix).

Built

The mode tray now waits for final layout geometry before revealing a tab. Pointerdown preloads mode code and data; recent mode data stays cached for instant return. Cold navigation shows the target mode loading view with the selected tab. The label morph is capped at 140 ms. The merged Files listing code keeps the first-page stale-while-revalidate cache and passes RequestCache to first-page, refresh, and later-page fetches.

Main files: packages/ui/src/components/SegmentedControl.svelte, packages/ui/src/components/TabBar.svelte, apps/web/src/lib/navigation/modePreload.ts, apps/web/src/routes/+layout.svelte, mode data stores and their tests, apps/web/e2e/shell.mjs, apps/web/src/lib/tray.svelte.test.ts, apps/web/src/routes/settings/ai/TurnHistory.svelte, and apps/web/src/lib/files/api.ts.

Review artifacts

390 px production frame

390 px production recording with 200 ms RTT

The in-view switch sample kept scrollLeft at 0 with no scroll events. A cold switch sample painted 103.5 ms after pointerdown (3.5 ms over the 100 ms target), and a separate sample missed the 200 ms morph target while other builds ran on the shared host. These timing-only observations remain for visual review.

Gates

cargo fmt --check exited 0 and printed no output.

The first Clippy invocation stopped because the local apps/web/build directory was absent for RustEmbed. After building the production web app, Clippy passed. Verbatim successful output:

   Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/tray-fix/crates/calternal-server)
    Checking calternal-collab v0.0.1 (/home/kayg/Developer/calternal-wt/tray-fix/crates/calternal-collab)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 17.08s

cargo test exited 0. Verbatim test-result lines:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 37.38s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.90s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.51s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.32s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 108.69s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.75s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.58s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.30s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.30s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 47.62s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 43.17s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.74s
test result: ok. 9 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.49s
test result: ok. 17 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 0.26s
test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.78s
test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.75s
test result: ok. 486 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.16s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.10s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.16s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.34s
test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.38s
test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.78s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.05s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.55s
test result: ok. 112 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 55.66s
test result: ok. 93 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 52.08s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s
test result: ok. 42 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 4.18s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
test result: ok. 25 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.99s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.31s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.31s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
test result: ok. 1 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 6.12s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 57 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.52s
test result: ok. 51 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.00s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.49s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

bun run check exited 0. Verbatim output:

Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/tray-fix/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test exited 0. Verbatim summary:

 Test Files  89 passed (89)
      Tests  618 passed (618)
   Start at  23:51:31
   Duration  78.83s (transform 63%, environment 14%, import 12%, tests 8%, setup 3%)

Adversarial round

The one local-server round ran for its 480-second timebox and stopped during the exhaustion probe (timeout exit 124). Before the cap, the authz matrix ran 936 requests across 234 OpenAPI operations and four identities; it found PUT /api/v1/admin/config returned 422 rather than 403 for a standard User. The cross-plugin probe found a shared Photos root still empty after the full 60-second refresh window. These findings are filed as #276 and #277, and are recorded in the #243 comments. The public-link probe also reported zip mid-stream as SLOW after 30 seconds; that is load-only.

Decisions and remaining work

The design doc does not specify how to combine the tray branch's first-page cache with the incoming dev RequestCache option. I kept both: the new option reaches first-page requests, stale revalidation, and subsequent pages.

Remaining work: continue the adversarial runner after exhaustion; triage and fix #276 and #277. The timing-only E2E overages are attached for review.

Completed the #243 continuation on `job/tray-fix`. ## Commits - `fda7edb1` Stop mode tray revealing tabs during width morph. - `e4a98411` Preload mode routes on pointerdown. - `8ec8af18` Warm mode routes and retain recent view data. - `5e1ab5ee` Derive Ask history limit from mode kind. - `9a427538` Switch mode views with pointerdown preloading. - `a0debedb` Keep mode label morphs within the phone frame cap. - `1489013b` Restore mode tray preload callback bindings. - `7f03fda3` Merge current `dev`; preserve both listing cache behavior and `RequestCache` propagation. Head: `7f03fda38844372291e55f4461454c6706092128` (pushed to `origin/job/tray-fix`). ## Built The mode tray now waits for final layout geometry before revealing a tab. Pointerdown preloads mode code and data; recent mode data stays cached for instant return. Cold navigation shows the target mode loading view with the selected tab. The label morph is capped at 140 ms. The merged Files listing code keeps the first-page stale-while-revalidate cache and passes `RequestCache` to first-page, refresh, and later-page fetches. Main files: `packages/ui/src/components/SegmentedControl.svelte`, `packages/ui/src/components/TabBar.svelte`, `apps/web/src/lib/navigation/modePreload.ts`, `apps/web/src/routes/+layout.svelte`, mode data stores and their tests, `apps/web/e2e/shell.mjs`, `apps/web/src/lib/tray.svelte.test.ts`, `apps/web/src/routes/settings/ai/TurnHistory.svelte`, and `apps/web/src/lib/files/api.ts`. ## Review artifacts ![390 px production frame](https://git.kayg.org/attachments/dec8dc54-dd14-4182-a080-6bc0ba5a4ac2) [390 px production recording with 200 ms RTT](https://git.kayg.org/attachments/d7407939-6771-41d0-bc0d-a6a30ded5a2f) The in-view switch sample kept `scrollLeft` at 0 with no scroll events. A cold switch sample painted 103.5 ms after pointerdown (3.5 ms over the 100 ms target), and a separate sample missed the 200 ms morph target while other builds ran on the shared host. These timing-only observations remain for visual review. ## Gates `cargo fmt --check` exited 0 and printed no output. The first Clippy invocation stopped because the local `apps/web/build` directory was absent for `RustEmbed`. After building the production web app, Clippy passed. Verbatim successful output: ```text Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/tray-fix/crates/calternal-server) Checking calternal-collab v0.0.1 (/home/kayg/Developer/calternal-wt/tray-fix/crates/calternal-collab) Finished `dev` profile [unoptimized + debuginfo] target(s) in 17.08s ``` `cargo test` exited 0. Verbatim test-result lines: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 37.38s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.90s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.51s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.32s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 108.69s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.52s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.75s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.58s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.30s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.30s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 47.62s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 43.17s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.74s test result: ok. 9 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.49s test result: ok. 17 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 0.26s test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.78s test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.75s test result: ok. 486 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.16s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.10s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.16s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.34s test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.38s test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.78s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.05s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.55s test result: ok. 112 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 55.66s test result: ok. 93 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 52.08s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s test result: ok. 42 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 4.18s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s test result: ok. 25 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.99s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.31s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.31s test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s test result: ok. 1 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 6.12s test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s test result: ok. 57 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 12.52s test result: ok. 51 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.00s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.49s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `bun run check` exited 0. Verbatim output: ```text Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/tray-fix/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test` exited 0. Verbatim summary: ```text Test Files 89 passed (89) Tests 618 passed (618) Start at 23:51:31 Duration 78.83s (transform 63%, environment 14%, import 12%, tests 8%, setup 3%) ``` ## Adversarial round The one local-server round ran for its 480-second timebox and stopped during the exhaustion probe (timeout exit 124). Before the cap, the authz matrix ran 936 requests across 234 OpenAPI operations and four identities; it found `PUT /api/v1/admin/config` returned 422 rather than 403 for a standard User. The cross-plugin probe found a shared Photos root still empty after the full 60-second refresh window. These findings are filed as #276 and #277, and are recorded in the #243 comments. The public-link probe also reported `zip mid-stream` as SLOW after 30 seconds; that is load-only. ## Decisions and remaining work The design doc does not specify how to combine the tray branch's first-page cache with the incoming `dev` `RequestCache` option. I kept both: the new option reaches first-page requests, stale revalidation, and subsequent pages. Remaining work: continue the adversarial runner after `exhaustion`; triage and fix #276 and #277. The timing-only E2E overages are attached for review.
Author
Owner

Decision addendum for the owner: the design says a cold switch must show its mode skeleton within 100 ms, but does not specify how to schedule it against navigation. The implementation gives the cold loading state two animation frames to paint before starting goto; cached mode switches navigate immediately. During merge conflict resolution, I also kept the tray branch's first-page cache while carrying dev's RequestCache option through the first-page, refresh and later-page requests.

Decision addendum for the owner: the design says a cold switch must show its mode skeleton within 100 ms, but does not specify how to schedule it against navigation. The implementation gives the cold loading state two animation frames to paint before starting `goto`; cached mode switches navigate immediately. During merge conflict resolution, I also kept the tray branch's first-page cache while carrying `dev`'s `RequestCache` option through the first-page, refresh and later-page requests.
Author
Owner

Merged in c70b196b (svelte-check 0/0, web 619/619). Reviewed the 200 ms-RTT recording: highlight and view switch together, loading state shows immediately. Follow-up: first open of Files at 200 ms RTT shows 'Opening Files…' for ~1 s (sequential request waterfall) → folded into #258.

Merged in c70b196b (svelte-check 0/0, web 619/619). Reviewed the 200 ms-RTT recording: highlight and view switch together, loading state shows immediately. Follow-up: first open of Files at 200 ms RTT shows 'Opening Files…' for ~1 s (sequential request waterfall) → folded into #258.
kayg closed this issue 2026-09-27 22:13:45 +00:00
kayg referenced this issue from a commit 2026-09-27 22:13:45 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#243
No description provided.