Mail Morph: switching messages closes the reader and never reopens it; rapid switching must be instant #1368

Closed
opened 2026-10-07 14:08:45 +00:00 by kayg · 40 comments
Owner

Owner report 2026-10-07 on production 0419170fe, Mail Morph layout: "with an email open in the tab morph view and then changing the email still causes the tab to close but never opens again. rapid spam scrolling through emails should be instant and it shouldn't take any time to load."
Owner decision (2026-10-07), supersedes #1104's chip-on-every-switch: moving to another message (click, arrow keys, j/k, rapid scrolling through the list) REPLACES the open reader in place in the same frame: no close/reopen, no morph to a chip. A tab chip is created only when the User explicitly keeps a message (Cmd/Ctrl+click or a Keep/pin action, keyboard accessible, with the warm tooltip and shortcut).
Performance: list rows carry the reader header (DESIGN §59 rule 3); the reader paints header and cached body from the client cache synchronously; bodies for the next and previous messages are prefetched; a message held under the selection for less than ~150 ms does not fetch its body (debounce only the network, never the header paint); stale responses for messages the User already left are aborted and never painted. Holding ArrowDown through 200 messages stays at frame rate and the final message shows its body as soon as it is fetched.

Owner report 2026-10-07 on production 0419170fe, Mail Morph layout: "with an email open in the tab morph view and then changing the email still causes the tab to close but never opens again. rapid spam scrolling through emails should be instant and it shouldn't take any time to load." Owner decision (2026-10-07), supersedes #1104's chip-on-every-switch: moving to another message (click, arrow keys, j/k, rapid scrolling through the list) REPLACES the open reader in place in the same frame: no close/reopen, no morph to a chip. A tab chip is created only when the User explicitly keeps a message (Cmd/Ctrl+click or a Keep/pin action, keyboard accessible, with the warm tooltip and shortcut). Performance: list rows carry the reader header (DESIGN §59 rule 3); the reader paints header and cached body from the client cache synchronously; bodies for the next and previous messages are prefetched; a message held under the selection for less than ~150 ms does not fetch its body (debounce only the network, never the header paint); stale responses for messages the User already left are aborted and never painted. Holding ArrowDown through 200 messages stays at frame rate and the final message shows its body as soon as it is fetched.
Author
Owner

Starting implementation on branch job/mailmorph-1368 from base 0419170fe4. First step is reproducing the reported reader-close bug against a production build and locating the current Mail Morph selection and chip flows.

Starting implementation on branch job/mailmorph-1368 from base 0419170fe456b58614befcb12d9c9d255118f1c0. First step is reproducing the reported reader-close bug against a production build and locating the current Mail Morph selection and chip flows.
Author
Owner

Finding: the Morph card closes from its window-level pointerdown handler when a click begins on the Mail list, before row selection runs; this is the likely close-without-reopen path to verify in production. Initial build attempt stopped before Vite because this worktree has no installed web dependencies (Cannot find module '@excalidraw/excalidraw'); installing from bun.lock before continuing.

Finding: the Morph card closes from its window-level pointerdown handler when a click begins on the Mail list, before row selection runs; this is the likely close-without-reopen path to verify in production. Initial build attempt stopped before Vite because this worktree has no installed web dependencies (Cannot find module '@excalidraw/excalidraw'); installing from bun.lock before continuing.
Author
Owner

Correction to my earlier probe note: the initial second-row selector resolved before the second row loaded, so that click did not reproduce a selection change. I reran the probe against the fixture message ID directly.

On the production build, opening the first message and holding pointerdown on the second row showed this sequence: the row pointerdown changed the Tab Bar capsule from shortcuts to tabs; pointerup selected the second message; the capsule then changed back to shortcuts. The Morph card remained in the DOM during the sample, but its close callback and the capsule transition were both triggered by the outside-pointer handler. This confirms the race: a pointer on the Mail list is treated as an outside click, so the reader starts closing at the same time the selection changes. I am changing the outside-pointer rule so Mail list interaction keeps the Morph reader open, and I will cover the selection and stale-body paths with regression tests.

Correction to my earlier probe note: the initial second-row selector resolved before the second row loaded, so that click did not reproduce a selection change. I reran the probe against the fixture message ID directly. On the production build, opening the first message and holding pointerdown on the second row showed this sequence: the row pointerdown changed the Tab Bar capsule from `shortcuts` to `tabs`; pointerup selected the second message; the capsule then changed back to `shortcuts`. The Morph card remained in the DOM during the sample, but its close callback and the capsule transition were both triggered by the outside-pointer handler. This confirms the race: a pointer on the Mail list is treated as an outside click, so the reader starts closing at the same time the selection changes. I am changing the outside-pointer rule so Mail list interaction keeps the Morph reader open, and I will cover the selection and stale-body paths with regression tests.
Author
Owner

The focused cache checks exposed an access-freshness edge: account-access invalidation cleared the shared body cache but left the recent-preference timestamp fresh for a retained reference. I changed access invalidation to expire the synchronous-paint fence as well. Verification: bunx vitest run src/lib/api/revision-cache.test.ts src/lib/mail/remoteContent.test.ts --maxWorkers=2 → 2 files, 18 tests passed. This preserves the current remote-content/access check before a warm body can paint synchronously.

The focused cache checks exposed an access-freshness edge: account-access invalidation cleared the shared body cache but left the recent-preference timestamp fresh for a retained reference. I changed access invalidation to expire the synchronous-paint fence as well. Verification: `bunx vitest run src/lib/api/revision-cache.test.ts src/lib/mail/remoteContent.test.ts --maxWorkers=2` → 2 files, 18 tests passed. This preserves the current remote-content/access check before a warm body can paint synchronously.
Author
Owner

The shortcut conflict check found that Cmd/Ctrl+Shift+K is reserved by Edge on Windows, so I changed the Keep shortcut to Cmd/Ctrl+Alt+K. The regression suite now verifies its Mac and Windows labels and the registry's sourced browser/OS conflict audit passes. Verification: bunx vitest run src/lib/api/revision-cache.test.ts src/lib/mail/remoteContent.test.ts src/lib/shortcuts/registry.test.ts --maxWorkers=2 → 3 files, 36 tests passed.

The shortcut conflict check found that Cmd/Ctrl+Shift+K is reserved by Edge on Windows, so I changed the Keep shortcut to Cmd/Ctrl+Alt+K. The regression suite now verifies its Mac and Windows labels and the registry's sourced browser/OS conflict audit passes. Verification: `bunx vitest run src/lib/api/revision-cache.test.ts src/lib/mail/remoteContent.test.ts src/lib/shortcuts/registry.test.ts --maxWorkers=2` → 3 files, 36 tests passed.
Author
Owner

Implementation finding: kept message Tabs use the shared Pill and PillGroup primitives. The constrained scroll capsule now owns the row shrink boundary; long subject labels use a documented/tested Pill truncate option so the feature does not restyle shared chrome locally. This is a small public UI-package addition for #1368, with README and component coverage.

Implementation finding: kept message Tabs use the shared Pill and PillGroup primitives. The constrained scroll capsule now owns the row shrink boundary; long subject labels use a documented/tested Pill `truncate` option so the feature does not restyle shared chrome locally. This is a small public UI-package addition for #1368, with README and component coverage.
Author
Owner

Production regression finding: the first seeded production run passed pointer switching, but 200 ArrowDown selections timed out waiting for the final body. selectRowAt(..., 'morph') updated the row while the list route was active but skipped showSelectedDetail, so no final body request started. I am routing Morph keyboard selection through the same debounced, abortable reader loader and will rerun the production probe.

Production regression finding: the first seeded production run passed pointer switching, but 200 ArrowDown selections timed out waiting for the final body. `selectRowAt(..., 'morph')` updated the row while the list route was active but skipped `showSelectedDetail`, so no final body request started. I am routing Morph keyboard selection through the same debounced, abortable reader loader and will rerun the production probe.
Author
Owner

Measurement finding: the local production key-repeat probe reached the final body, but failed the 16.7 ms frame-p95 gate at 1534.2 ms. The host load average was 61.35 / 59.79 / 59.02 immediately after the run, with concurrent build and test processes. I am moving the time-boxed measurement to the free single-tenant perf VM under perf-run so the result can distinguish code cost from local host contention.

Measurement finding: the local production key-repeat probe reached the final body, but failed the 16.7 ms frame-p95 gate at 1534.2 ms. The host load average was 61.35 / 59.79 / 59.02 immediately after the run, with concurrent build and test processes. I am moving the time-boxed measurement to the free single-tenant perf VM under `perf-run` so the result can distinguish code cost from local host contention.
Author
Owner

The local 200-message production probe reached the final body in 68.0 ms with exactly one final-body fetch, but recorded 1191.9 ms frame p95 and 222 body requests (1.110 per message). The local host load average was 65.43 / 66.70 / 63.12 immediately after the run. The free perf VM attempts are blocked before the browser run by its startup runtime (calternal-server exits with Error: NotFound after its native media launch check); I am collecting remaining UI regression and screenshot evidence locally and will report both limits explicitly.

The local 200-message production probe reached the final body in 68.0 ms with exactly one final-body fetch, but recorded 1191.9 ms frame p95 and 222 body requests (1.110 per message). The local host load average was 65.43 / 66.70 / 63.12 immediately after the run. The free perf VM attempts are blocked before the browser run by its startup runtime (`calternal-server` exits with `Error: NotFound` after its native media launch check); I am collecting remaining UI regression and screenshot evidence locally and will report both limits explicitly.
Author
Owner

Finding: the stale-body regression probe held a Playwright-routed response and waited for requestfailed before releasing the route. The handler could not finish until released, so the assertion timed out and browser cleanup remained blocked. The probe now tracks the actual fetch AbortSignal for the stale message, releases the route in finally, and still asserts the selected final message body wins. This keeps the product behavior assertion intact while avoiding a route-interception deadlock.

Finding: the stale-body regression probe held a Playwright-routed response and waited for `requestfailed` before releasing the route. The handler could not finish until released, so the assertion timed out and browser cleanup remained blocked. The probe now tracks the actual fetch AbortSignal for the stale message, releases the route in `finally`, and still asserts the selected final message body wins. This keeps the product behavior assertion intact while avoiding a route-interception deadlock.
Author
Owner

Local production profile (shared host load average 66.58 / 63.26 / 63.63): 200 ArrowDown selections measured frame p95 1224.1 ms; final body visible in 48.6 ms with one final-message fetch; 228 body requests including neighbors (1.140 per selected message); cached click-to-reader-visible 180.7 ms with zero body requests. The frame and cached-click budgets fail under this host load; I will retain the numbers as local evidence and report the dedicated perf VM limitation rather than treat these timings as representative.

Local production profile (shared host load average 66.58 / 63.26 / 63.63): 200 ArrowDown selections measured frame p95 1224.1 ms; final body visible in 48.6 ms with one final-message fetch; 228 body requests including neighbors (1.140 per selected message); cached click-to-reader-visible 180.7 ms with zero body requests. The frame and cached-click budgets fail under this host load; I will retain the numbers as local evidence and report the dedicated perf VM limitation rather than treat these timings as representative.
Author
Owner

Finding during the required local Mail Morph profile: bun run perf:mailmorph-1368 failed before measuring because bench/blaze.mjs resolved tests/adversarial/mail_screenshot_fixture.py relative to apps/web, while the fixture is at the repository root. I changed the profile to resolve the fixture from import.meta.url and restarted the profile once. No user data or real accounts were used.

Finding during the required local Mail Morph profile: `bun run perf:mailmorph-1368` failed before measuring because `bench/blaze.mjs` resolved `tests/adversarial/mail_screenshot_fixture.py` relative to `apps/web`, while the fixture is at the repository root. I changed the profile to resolve the fixture from `import.meta.url` and restarted the profile once. No user data or real accounts were used.
Author
Owner

#1368 complete

Branch: job/mailmorph-1368
Head: 58e113943de5400851cb67a527903105696ceac8

Built

Reproduced the Morph close bug: closeOutside treated the message-list region as outside the reader, so selecting another row closed the card. The reader shell now stays mounted and replaces its row-backed header and body in place. Switching messages does not create a chip. Explicit Keep creates one stable-ID Mail Tab through Cmd/Ctrl-click, the reader header action, or the row menu.

Header text comes from the selected list row. Sanitized bodies use the shared cache for synchronous warm paints, fetch after roughly 150 ms of selection quiet, and prefetch adjacent messages. Abandoned body requests are aborted and stale completions are fenced. Sanitization is unchanged. The Mail Morph design section documents this and marks #1104 superseded.

Shared Pill, PillGroup, and Menu provide the visible controls. Shared Pill/PillGroup gained documented truncation and constrained-group options. Kept tabs expose open, remove, Copy link, keyboard and touch actions. Body errors use the reader's calm error state.

Files

  • Mail view and cache: apps/web/src/lib/mail/MailKeptTabs.svelte, MailMorphCard.svelte, MailReaderContent.svelte, MailView.svelte, mailCache.ts, remoteContent.ts
  • Cache and shortcut behavior/tests: apps/web/src/lib/api/revision-cache.ts, revision-cache.test.ts, apps/web/src/lib/mail/remoteContent.test.ts, apps/web/src/lib/shortcuts/registry.ts, registry.test.ts
  • Shared UI: packages/ui/src/components/Pill.svelte, PillGroup.svelte, packages/ui/README.md, apps/web/src/lib/components/PillGroup.svelte.test.ts
  • Production E2E and benchmark: apps/web/e2e/mailmorph-1368.mjs, apps/web/package.json, bench/blaze.mjs
  • Design and exact performance pins: docs/DESIGN.md, contracts/perf/adoption-1058.json, contracts/perf/exceptions.json, contracts/perf/ratchet.json

UX gaps closed

Message switching now keeps the reader visible. Keep works from the row and reader, while Cmd/Ctrl-click keeps exactly one tab. Kept tabs support Copy link and removal; keyboard and touch actions use shared controls. Production checks covered stale-request cancellation, final body visibility, error state, the row menu, and the one-tab invariant.

UX gaps left: none found in the exercised flows. Performance targets below remain unverified under this host load.

Performance

The production key-repeat check traversed 200 messages. It issued 166 body requests including neighbor prefetches (0.83 requests per message); the final body used one fetch and became visible in 151.3 ms. In that run, frame p95 was 386.9 ms and cached click-to-reader-visible was 186.3 ms with zero body requests. Both exceed the requested 16.7 ms and 50 ms limits.

The corrected local benchmark profile measured frame p95 at 349.1 ms and failed its 16.7 ms assertion. Host load immediately after that run was 38.90, 47.42, 63.18. The baseline's 15 ms cold pass was 350 ms and its warm pass was 233.4 ms at load around 30; that baseline also says local timing is load-contaminated and the target is unverified. This job does not establish that the new path meets the requested frame or cached-click budgets. The benchmark fixture path now resolves from the repository root, independent of its working directory.

Six production screenshots were attached to this issue: desktop 1440, tablet 820 and phone 390, each in light and dark mode with macOS shortcut rendering.

Decisions

  • Keep uses ⌥⌘K on macOS and Ctrl+Alt+K elsewhere. Shift+K conflicted with a Windows/Edge browser shortcut.
  • Kept tabs live in memory for the mounted Mail view and clear when Mail unmounts or the document reloads, as recorded in DESIGN §45.

Gates

cd apps/web && bun run build — passed. Output:

Compressed 919 static variants; saved 21496450 bytes.

cd apps/web && bun run check — passed, with one existing CSS warning:

perf-lint: PASS; 0 violations; 22535 scoped exceptions
User browser caches use userStorage; only documented device/public-link exceptions remain.
Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css.
Text sizes and UI shape values use shared role tokens.
Keyboard focus rings use the shared focus tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
svelte-check found 0 errors and 1 warning in 1 file

bunx vitest run src/lib/components/PillGroup.svelte.test.ts src/lib/api/revision-cache.test.ts src/lib/mail/remoteContent.test.ts src/lib/shortcuts/registry.test.ts --maxWorkers=2 — passed:

Test Files  4 passed (4)
      Tests  38 passed (38)
   Duration  38.36s

Rust code was unchanged, so Rust crate gates were not run. cargo clean removed 7.3 GiB; apps/web/build was removed. git diff --check passed and the worktree is clean.

For the merge round

Run cd apps/web && bun run test to verify the full web test suite after integration. The production Mail Morph regression and screenshot script was run in this job; the full E2E suite remains merge-round verification.

## #1368 complete Branch: `job/mailmorph-1368` Head: `58e113943de5400851cb67a527903105696ceac8` ### Built Reproduced the Morph close bug: `closeOutside` treated the message-list region as outside the reader, so selecting another row closed the card. The reader shell now stays mounted and replaces its row-backed header and body in place. Switching messages does not create a chip. Explicit Keep creates one stable-ID Mail Tab through Cmd/Ctrl-click, the reader header action, or the row menu. Header text comes from the selected list row. Sanitized bodies use the shared cache for synchronous warm paints, fetch after roughly 150 ms of selection quiet, and prefetch adjacent messages. Abandoned body requests are aborted and stale completions are fenced. Sanitization is unchanged. The Mail Morph design section documents this and marks #1104 superseded. Shared `Pill`, `PillGroup`, and `Menu` provide the visible controls. Shared Pill/PillGroup gained documented truncation and constrained-group options. Kept tabs expose open, remove, Copy link, keyboard and touch actions. Body errors use the reader's calm error state. ### Files - Mail view and cache: `apps/web/src/lib/mail/MailKeptTabs.svelte`, `MailMorphCard.svelte`, `MailReaderContent.svelte`, `MailView.svelte`, `mailCache.ts`, `remoteContent.ts` - Cache and shortcut behavior/tests: `apps/web/src/lib/api/revision-cache.ts`, `revision-cache.test.ts`, `apps/web/src/lib/mail/remoteContent.test.ts`, `apps/web/src/lib/shortcuts/registry.ts`, `registry.test.ts` - Shared UI: `packages/ui/src/components/Pill.svelte`, `PillGroup.svelte`, `packages/ui/README.md`, `apps/web/src/lib/components/PillGroup.svelte.test.ts` - Production E2E and benchmark: `apps/web/e2e/mailmorph-1368.mjs`, `apps/web/package.json`, `bench/blaze.mjs` - Design and exact performance pins: `docs/DESIGN.md`, `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json`, `contracts/perf/ratchet.json` ### UX gaps closed Message switching now keeps the reader visible. Keep works from the row and reader, while Cmd/Ctrl-click keeps exactly one tab. Kept tabs support Copy link and removal; keyboard and touch actions use shared controls. Production checks covered stale-request cancellation, final body visibility, error state, the row menu, and the one-tab invariant. UX gaps left: none found in the exercised flows. Performance targets below remain unverified under this host load. ### Performance The production key-repeat check traversed 200 messages. It issued 166 body requests including neighbor prefetches (0.83 requests per message); the final body used one fetch and became visible in 151.3 ms. In that run, frame p95 was 386.9 ms and cached click-to-reader-visible was 186.3 ms with zero body requests. Both exceed the requested 16.7 ms and 50 ms limits. The corrected local benchmark profile measured frame p95 at 349.1 ms and failed its 16.7 ms assertion. Host load immediately after that run was `38.90, 47.42, 63.18`. The baseline's 15 ms cold pass was 350 ms and its warm pass was 233.4 ms at load around 30; that baseline also says local timing is load-contaminated and the target is unverified. This job does not establish that the new path meets the requested frame or cached-click budgets. The benchmark fixture path now resolves from the repository root, independent of its working directory. Six production screenshots were attached to this issue: desktop 1440, tablet 820 and phone 390, each in light and dark mode with macOS shortcut rendering. ### Decisions - Keep uses `⌥⌘K` on macOS and `Ctrl+Alt+K` elsewhere. Shift+K conflicted with a Windows/Edge browser shortcut. - Kept tabs live in memory for the mounted Mail view and clear when Mail unmounts or the document reloads, as recorded in DESIGN §45. ### Gates `cd apps/web && bun run build` — passed. Output: ```text Compressed 919 static variants; saved 21496450 bytes. ``` `cd apps/web && bun run check` — passed, with one existing CSS warning: ```text perf-lint: PASS; 0 violations; 22535 scoped exceptions User browser caches use userStorage; only documented device/public-link exceptions remain. Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css. Text sizes and UI shape values use shared role tokens. Keyboard focus rings use the shared focus tokens. UI transitions and animation options use shared motion tokens or documented exceptions. svelte-check found 0 errors and 1 warning in 1 file ``` `bunx vitest run src/lib/components/PillGroup.svelte.test.ts src/lib/api/revision-cache.test.ts src/lib/mail/remoteContent.test.ts src/lib/shortcuts/registry.test.ts --maxWorkers=2` — passed: ```text Test Files 4 passed (4) Tests 38 passed (38) Duration 38.36s ``` Rust code was unchanged, so Rust crate gates were not run. `cargo clean` removed 7.3 GiB; `apps/web/build` was removed. `git diff --check` passed and the worktree is clean. ### For the merge round Run `cd apps/web && bun run test` to verify the full web test suite after integration. The production Mail Morph regression and screenshot script was run in this job; the full E2E suite remains merge-round verification.
Author
Owner

Correction to the gate note: svelte-check reported one CSS warning at packages/ui/src/components/calendar/AttachmentDeck.svelte:1055, which is outside this branch's changed files. I did not compare that warning against a pre-change run, so “existing” should be read only as “in an untouched file,” not as a verified baseline claim.

Correction to the gate note: `svelte-check` reported one CSS warning at `packages/ui/src/components/calendar/AttachmentDeck.svelte:1055`, which is outside this branch's changed files. I did not compare that warning against a pre-change run, so “existing” should be read only as “in an untouched file,” not as a verified baseline claim.
Author
Owner

Interaction audit #1369 (origin/dev, perf VM): Mail / select first item p50/p95 210/579 ms with 38 thread + 37 message GETs in the interval and dozens still pending; Mail open item 288/339 ms. mailmorph2: confirm your branch removes this fan-out (count requests per selection in the perf VM run).

Interaction audit #1369 (origin/dev, perf VM): Mail / select first item p50/p95 210/579 ms with 38 thread + 37 message GETs in the interval and dozens still pending; Mail open item 288/339 ms. mailmorph2: confirm your branch removes this fan-out (count requests per selection in the perf VM run).
Author
Owner

Starting #1368 on branch job/mailmorph-1368 at head 58e113943d. The merge base is 0419170fe4. I am fixing the kept-message strip, Keep action, phone sender line, and shared toast width; then I will re-shoot the requested production UI states and run the requested gates and perf profile.

Starting #1368 on branch job/mailmorph-1368 at head 58e113943de5400851cb67a527903105696ceac8. The merge base is 0419170fe456b58614befcb12d9c9d255118f1c0. I am fixing the kept-message strip, Keep action, phone sender line, and shared toast width; then I will re-shoot the requested production UI states and run the requested gates and perf profile.
Author
Owner

Finding during the first web check: bun run check reports perf-lint: INVALID: (coverage.surfaces, apps/web/src/lib/mail/MailKeptTabs.svelte, ...): unused or changed exception. The kept-tab markup moved while changing its shared PillGroup size, so the exact source pin needs refresh. I will update only the pin to the live surface and keep its rule unchanged.

Finding during the first web check: `bun run check` reports `perf-lint: INVALID: (coverage.surfaces, apps/web/src/lib/mail/MailKeptTabs.svelte, ...): unused or changed exception`. The kept-tab markup moved while changing its shared PillGroup size, so the exact source pin needs refresh. I will update only the pin to the live surface and keep its rule unchanged.
Author
Owner

A preliminary exposed one test typing error: this Vitest setup has no matcher. I replaced it with ; the focused sender-row test passes (1 file, 1 test). The fix is in the committed header slice. Current head: 708ae6151.

A preliminary exposed one test typing error: this Vitest setup has no matcher. I replaced it with ; the focused sender-row test passes (1 file, 1 test). The fix is in the committed header slice. Current head: 708ae6151.
Author
Owner

A preliminary bun run check exposed one test typing error: this Vitest setup has no toHaveTextContent matcher. I replaced it with textContent.toContain; the focused sender-row test passes (1 file, 1 test). The fix is in the committed header slice. Current head: 708ae6151.

A preliminary bun run check exposed one test typing error: this Vitest setup has no toHaveTextContent matcher. I replaced it with textContent.toContain; the focused sender-row test passes (1 file, 1 test). The fix is in the committed header slice. Current head: 708ae6151.
Author
Owner

The first production E2E run passed the toast, reader-switch, ArrowDown, stale-request cancellation, body-error, and Command-click checks. Its first HTML capture then failed because the test read contentDocument from the intentionally opaque sandboxed iframe; that access returns no document. I am changing the fixture assertion to inspect the sanitized srcdoc string while keeping the expected fixture content unchanged.

The same local run measured frame p95 310.9 ms and cached-open p50/p95 18.4/70.8 ms while other browser jobs were running on the shared host. These are local load-contaminated observations, not the requested perf-VM measurements. I am keeping timing assertions for the isolated perf run and running the browser E2E for behavior and captures here.

The first production E2E run passed the toast, reader-switch, ArrowDown, stale-request cancellation, body-error, and Command-click checks. Its first HTML capture then failed because the test read `contentDocument` from the intentionally opaque sandboxed iframe; that access returns no document. I am changing the fixture assertion to inspect the sanitized `srcdoc` string while keeping the expected fixture content unchanged. The same local run measured frame p95 310.9 ms and cached-open p50/p95 18.4/70.8 ms while other browser jobs were running on the shared host. These are local load-contaminated observations, not the requested perf-VM measurements. I am keeping timing assertions for the isolated perf run and running the browser E2E for behavior and captures here.
Author
Owner

The production E2E exposed a focus-ring mismatch after Return opened the reader: the focused Close action matched :focus-visible because focusQuietly passed focusVisible: undefined whenever data-input was keyboard. The owner rule from 2026-10-07 requires programmatic focus to stay ringless after keyboard shortcuts. focusQuietly now always passes focusVisible: false, and its focused tests pass (14 tests across input modality, focus trap and menu-open focus).

One run also observed 205 message-body requests after the profile's starting count of 204 during five warm opens. The cached row itself did not refetch; the assertion was counting every request and did not identify whether the extra request was an allowed neighboring prefetch. It now checks both selected body IDs for refetch and reports neighbor demand separately.

The production E2E exposed a focus-ring mismatch after Return opened the reader: the focused Close action matched `:focus-visible` because `focusQuietly` passed `focusVisible: undefined` whenever `data-input` was `keyboard`. The owner rule from 2026-10-07 requires programmatic focus to stay ringless after keyboard shortcuts. `focusQuietly` now always passes `focusVisible: false`, and its focused tests pass (14 tests across input modality, focus trap and menu-open focus). One run also observed 205 message-body requests after the profile's starting count of 204 during five warm opens. The cached row itself did not refetch; the assertion was counting every request and did not identify whether the extra request was an allowed neighboring prefetch. It now checks both selected body IDs for refetch and reports neighbor demand separately.
Author
Owner

The updated production E2E passed the 200-step profile and produced the desktop after-click and after-Return captures. The Keep click then changed its accessible name from “Keep message” to “Remove kept Tab”, so a post-click assertion that reused the old role locator timed out. The test now uses a stable shared-button selector for the post-click focus check while retaining the role locator for the initial action.

This run measured frame p95 450.3 ms, cached-open p50/p95 43.3/74.4 ms, and 179 body requests across 200 ArrowDown steps on the shared build host. These are local observations under concurrent work, not the requested perf-VM comparison.

The updated production E2E passed the 200-step profile and produced the desktop after-click and after-Return captures. The `Keep` click then changed its accessible name from “Keep message” to “Remove kept Tab”, so a post-click assertion that reused the old role locator timed out. The test now uses a stable shared-button selector for the post-click focus check while retaining the role locator for the initial action. This run measured frame p95 450.3 ms, cached-open p50/p95 43.3/74.4 ms, and 179 body requests across 200 ArrowDown steps on the shared build host. These are local observations under concurrent work, not the requested perf-VM comparison.
Author
Owner

Finding for #1368: the merged origin/dev parent d395b6e25da20d8c67b8b9fc1a593b813939c8a7 already contains MailMorphCard.svelte and the /mail?layout=morph route. I corrected the performance comparison plan to build the same Morph surface from origin/dev and the job branch, using the same prebuilt server and 10k fixture.

The E2E retry showed Sonner keeps dismissed toast nodes mounted during its close motion. Waiting for DOM detachment timed out even after the toast entered data-removed="true". The capture helper now targets active toasts only, uses the shared dismiss action or Escape, and waits for Sonner's removed state. The full capture rerun is in progress.

Finding for #1368: the merged `origin/dev` parent `d395b6e25da20d8c67b8b9fc1a593b813939c8a7` already contains `MailMorphCard.svelte` and the `/mail?layout=morph` route. I corrected the performance comparison plan to build the same Morph surface from `origin/dev` and the job branch, using the same prebuilt server and 10k fixture. The E2E retry showed Sonner keeps dismissed toast nodes mounted during its close motion. Waiting for DOM detachment timed out even after the toast entered `data-removed="true"`. The capture helper now targets active toasts only, uses the shared dismiss action or Escape, and waits for Sonner's removed state. The full capture rerun is in progress.
Author
Owner

E2E finding and rerun: after route changes, the first virtual row could remain non-visible to Playwright's actionability check even though the list had loaded. Capture setup now resets list scroll and activates the first row through its click handler. The Mail Morph E2E then passed with the interaction checks and all 14 requested screenshots.

Exact output: Mail Morph production regressions passed; screenshots are in /home/kayg/Developer/calternal-wt/mailmorph-1368/artifacts/mailmorph.

The same run recorded local-only timing under shared-host load: 200 ArrowDown messages, frame p95 530.2 ms, cached click p50/p95 25.4/42.8 ms, 0 body refetches. These local values are not the requested perf-VM A/B.

E2E finding and rerun: after route changes, the first virtual row could remain non-visible to Playwright's actionability check even though the list had loaded. Capture setup now resets list scroll and activates the first row through its click handler. The Mail Morph E2E then passed with the interaction checks and all 14 requested screenshots. Exact output: `Mail Morph production regressions passed; screenshots are in /home/kayg/Developer/calternal-wt/mailmorph-1368/artifacts/mailmorph`. The same run recorded local-only timing under shared-host load: 200 ArrowDown messages, frame p95 530.2 ms, cached click p50/p95 25.4/42.8 ms, 0 body refetches. These local values are not the requested perf-VM A/B.
Author
Owner

The phone screenshot recheck exposed a sender/date alignment defect: at 390 px the date followed the truncated sender address with no separation. The shared sender line already uses flex spacing, but its header parent did not grow beside the fixed-width actions. I am making the metadata column take the remaining width and extending the E2E assertion to verify the date reaches the line edge at each requested width. The web check also reported one stale exact coverage.surfaces pin for the labels list; the current live hash is refreshed without changing its limit or the ratchet count.

The phone screenshot recheck exposed a sender/date alignment defect: at 390 px the date followed the truncated sender address with no separation. The shared sender line already uses flex spacing, but its header parent did not grow beside the fixed-width actions. I am making the metadata column take the remaining width and extending the E2E assertion to verify the date reaches the line edge at each requested width. The web check also reported one stale exact `coverage.surfaces` pin for the labels list; the current live hash is refreshed without changing its limit or the ratchet count.
Author
Owner

The check found the same moved MailReaderContent labels-list source hash in both contracts/perf/exceptions.json and the #1058 exact adoption checkpoint. Both hashes now match the live parsed source (fe5bae336500ba765f7dcac4f61ab6ad25afdc3050a3e1f7ebdc76752a9c53d3); scope, limit, and ratchet count are unchanged. I will rerun the check and include its final output in the completion report.

The check found the same moved MailReaderContent labels-list source hash in both `contracts/perf/exceptions.json` and the #1058 exact adoption checkpoint. Both hashes now match the live parsed source (`fe5bae336500ba765f7dcac4f61ab6ad25afdc3050a3e1f7ebdc76752a9c53d3`); scope, limit, and ratchet count are unchanged. I will rerun the check and include its final output in the completion report.
Author
Owner

Perf-lint clarified that coverage.surfaces exceptions store a combined hash of the list syntax and the full Svelte module. The previous pin refresh used the list-only hash and was correctly rejected. Both exact records now use the combined live hash (1264d8e87253883ace93845e3575b0035616571142e254949d88bd6cd342043a); no limits or exception counts changed.

Perf-lint clarified that `coverage.surfaces` exceptions store a combined hash of the list syntax and the full Svelte module. The previous pin refresh used the list-only hash and was correctly rejected. Both exact records now use the combined live hash (`1264d8e87253883ace93845e3575b0035616571142e254949d88bd6cd342043a`); no limits or exception counts changed.
Author
Owner

The first refreshed screenshot run exposed a timing gap: the service-worker Update available toast could arrive after route-level dismissal and appeared in the desktop click/shortcut captures. I now dismiss active toasts at each interaction-capture boundary. The final production E2E passes with clean screenshots and a sender-date right-edge assertion at 1440, 820 and 390 px.

The first refreshed screenshot run exposed a timing gap: the service-worker Update available toast could arrive after route-level dismissal and appeared in the desktop click/shortcut captures. I now dismiss active toasts at each interaction-capture boundary. The final production E2E passes with clean screenshots and a sender-date right-edge assertion at 1440, 820 and 390 px.
Author
Owner

The requested perf VM A/B is the only remaining work. perf-run status reports the single-tenant VM is held by bulkwrite-1393-money-dates; all code gates, the production E2E and screenshot uploads are complete. The origin/branch bundles and six-run interleaved profile command are ready, and I will start them when the slot is free.

The requested perf VM A/B is the only remaining work. `perf-run status` reports the single-tenant VM is held by `bulkwrite-1393-money-dates`; all code gates, the production E2E and screenshot uploads are complete. The origin/branch bundles and six-run interleaved profile command are ready, and I will start them when the slot is free.
Author
Owner

Perf VM setup finding for #1368: the first locked origin attempt exited 127 before running the benchmark because /root/calternal-bench-mailmorph-1368/bench/hdd-emu.sh was missing. The run recorded load inside the lock (0.43 2.54 3.66) and produced no timing sample. I am staging the benchmark harness and fixture under perf-run, then I will rerun the origin baseline.

Perf VM setup finding for #1368: the first locked origin attempt exited 127 before running the benchmark because `/root/calternal-bench-mailmorph-1368/bench/hdd-emu.sh` was missing. The run recorded load inside the lock (`0.43 2.54 3.66`) and produced no timing sample. I am staging the benchmark harness and fixture under `perf-run`, then I will rerun the origin baseline.
Author
Owner

Perf VM setup finding: after restoring the runner files, the origin attempt reached server startup but exited before profiling because calternal-server-release could not load libsherpa-onnx-c-api.so. Local ldd shows the server also needs libonnxruntime.so and libsherpa-onnx-cxx-api.so from the shared prebuilt runtime. The attempt recorded load inside the lock (4.71 5.55 4.82) and produced no timing sample. I have staged those three libraries and added their path to the runner environment before retrying; no build ran on the VM.

Perf VM setup finding: after restoring the runner files, the origin attempt reached server startup but exited before profiling because `calternal-server-release` could not load `libsherpa-onnx-c-api.so`. Local `ldd` shows the server also needs `libonnxruntime.so` and `libsherpa-onnx-cxx-api.so` from the shared prebuilt runtime. The attempt recorded load inside the lock (`4.71 5.55 4.82`) and produced no timing sample. I have staged those three libraries and added their path to the runner environment before retrying; no build ran on the VM.
Author
Owner

Perf VM baseline finding: the corrected origin run started the server but timed out at bench/blaze.mjs:713 before measuring. The origin/dev bundle uses the shared .mail-reader-content[data-blaze-id] identity but does not have the branch-only data-blaze-header-id instrumentation; the current probe required that newer attribute. The run recorded load inside the lock (3.17 3.82 4.25) and produced no timing JSON. I updated the probe to use the shared body ID for both builds and will rerun the origin baseline.

Perf VM baseline finding: the corrected origin run started the server but timed out at `bench/blaze.mjs:713` before measuring. The origin/dev bundle uses the shared `.mail-reader-content[data-blaze-id]` identity but does not have the branch-only `data-blaze-header-id` instrumentation; the current probe required that newer attribute. The run recorded load inside the lock (`3.17 3.82 4.25`) and produced no timing JSON. I updated the probe to use the shared body ID for both builds and will rerun the origin baseline.
Author
Owner

Update: the Mail Morph probe now matches reader bodies by the shared data-blaze-id, which is present in both origin/dev and the branch. This fixes the baseline selector mismatch and is committed as e0b6f0395. After the change, bun run check passed (svelte-check found 0 errors and 4 warnings in 2 files) and the Mail Morph production E2E passed. The updated probe is queued on the Perf VM behind apiaudit2-seed (60-minute reservation); no VM timing samples are available yet.

Update: the Mail Morph probe now matches reader bodies by the shared `data-blaze-id`, which is present in both origin/dev and the branch. This fixes the baseline selector mismatch and is committed as `e0b6f0395`. After the change, `bun run check` passed (`svelte-check found 0 errors and 4 warnings in 2 files`) and the Mail Morph production E2E passed. The updated probe is queued on the Perf VM behind `apiaudit2-seed` (60-minute reservation); no VM timing samples are available yet.
Author
Owner

Perf VM baseline finding: the origin run reached five warm opens and observed 41 message-body GETs, then stopped because the probe asserted the branch's zero-refetch behavior for both sides (41 !== 0 at bench/blaze.mjs:605). The branch cache assertion remains enabled; origin/dev now records its refetch count so the requested requests-per-message comparison can complete. No click or frame timing JSON was produced by that failed attempt.

Perf VM baseline finding: the origin run reached five warm opens and observed 41 message-body GETs, then stopped because the probe asserted the branch's zero-refetch behavior for both sides (`41 !== 0` at `bench/blaze.mjs:605`). The branch cache assertion remains enabled; origin/dev now records its refetch count so the requested requests-per-message comparison can complete. No click or frame timing JSON was produced by that failed attempt.
Author
Owner

Perf VM finding: the first branch profile recorded cached reader opens at p50 5.3 ms and p95 7 ms with zero body requests, then exited before writing its JSON because measured frame p95 was 116.4 ms against the existing 16.7 ms assertion. This comparison must retain the frame samples for the requested A/B report. I added CALTERNAL_PROFILE_ALLOW_FRAME_REGRESSION=1 as an explicit profile-only opt-out; ordinary benchmark runs still enforce the frame guard. The initial frame value is a finding, not a complete run, and is not yet a baseline comparison.

Perf VM finding: the first branch profile recorded cached reader opens at p50 5.3 ms and p95 7 ms with zero body requests, then exited before writing its JSON because measured frame p95 was 116.4 ms against the existing 16.7 ms assertion. This comparison must retain the frame samples for the requested A/B report. I added CALTERNAL_PROFILE_ALLOW_FRAME_REGRESSION=1 as an explicit profile-only opt-out; ordinary benchmark runs still enforce the frame guard. The initial frame value is a finding, not a complete run, and is not yet a baseline comparison.
Author
Owner

Perf VM A/B progress, pair 1 of 3 (load average recorded inside each perf-run lock): origin cached click-to-visible p95 was 89.1 ms with 7.4 body requests per open; branch was 13.3 ms with zero. During the 200-message ArrowDown pass, origin issued 58 body requests including neighbors (0.29/message), while branch issued 4 (0.02/message). The first pair's branch frame p95 was higher: cold/warm 192.7/185.2 ms versus origin 112.8/107.1 ms. This is one pair only; I will compare the other two before concluding whether frame latency is a stable regression. The branch cached-click p95 is below the requested 50 ms threshold, so it does not trigger the one-switch profiling condition at this point.

Perf VM A/B progress, pair 1 of 3 (load average recorded inside each perf-run lock): origin cached click-to-visible p95 was 89.1 ms with 7.4 body requests per open; branch was 13.3 ms with zero. During the 200-message ArrowDown pass, origin issued 58 body requests including neighbors (0.29/message), while branch issued 4 (0.02/message). The first pair's branch frame p95 was higher: cold/warm 192.7/185.2 ms versus origin 112.8/107.1 ms. This is one pair only; I will compare the other two before concluding whether frame latency is a stable regression. The branch cached-click p95 is below the requested 50 ms threshold, so it does not trigger the one-switch profiling condition at this point.
Author
Owner

Perf VM finding: origin-2 recorded cached click p95 81.5 ms, then exited before JSON output because the cold pass had finalBodyFetches=0 even though the selected body was visible. Origin/dev can satisfy the visibility check from a body already prefetched before the measured window; the one-fetch invariant is specific to the changed branch behavior. I will keep the default assertion and the branch assertion, while allowing the origin comparison mode to report zero or one final-body requests.

Perf VM finding: origin-2 recorded cached click p95 81.5 ms, then exited before JSON output because the cold pass had finalBodyFetches=0 even though the selected body was visible. Origin/dev can satisfy the visibility check from a body already prefetched before the measured window; the one-fetch invariant is specific to the changed branch behavior. I will keep the default assertion and the branch assertion, while allowing the origin comparison mode to report zero or one final-body requests.
Author
Owner

Perf VM A/B progress, corrected pair 2 of 3: origin cached click-to-visible p95 was 129 ms with 9.6 body requests/open; branch was 8 ms with zero. The 200-message pass issued 38 origin body requests (0.19/message) and 4 branch requests (0.02/message). Cold/warm frame p95 was 109/103.5 ms on origin and 113.2/121.1 ms on branch. Origin's final body was already available before the measured window in this run; the comparison records zero fetches while checking that the body was visible. All branch cached-click samples so far remain below 50 ms, so the conditional one-switch profiling step is not triggered. Pair 3 is running.

Perf VM A/B progress, corrected pair 2 of 3: origin cached click-to-visible p95 was 129 ms with 9.6 body requests/open; branch was 8 ms with zero. The 200-message pass issued 38 origin body requests (0.19/message) and 4 branch requests (0.02/message). Cold/warm frame p95 was 109/103.5 ms on origin and 113.2/121.1 ms on branch. Origin's final body was already available before the measured window in this run; the comparison records zero fetches while checking that the body was visible. All branch cached-click samples so far remain below 50 ms, so the conditional one-switch profiling step is not triggered. Pair 3 is running.
Author
Owner

Completed — #1368

Head: c961f676fc88eff6fc4e4cd2b3725c6a27c23729 (job/mailmorph-1368). No push, deploy, merge or issue close.

What changed

  • Kept messages use the shared PillGroup at normal size. The strip is content-width and appears only when at least one message is kept.
  • Keep uses the shared IconButton with a bookmark, the “Keep” tooltip and shortcut, and a tinted pressed state. Its center line matches the reader navigation buttons.
  • The phone sender line uses the shared Mail sender layout. The address truncates and the date stays aligned.
  • The shared toaster keeps “Update available” on one line at 1440 px.
  • The reader body and iframe styles were left to #1372.

UX gaps closed

  • Kept Tabs have working open, remove, Copy link and context actions through the shared controls; the empty strip is hidden.
  • Keep is keyboard and pointer accessible and exposes its pressed state.
  • Sender, address and date stay on one row at phone width.
  • The toast stays on one line at desktop width.
  • Captures include immediately after click and shortcut open, as well as all requested viewport and theme combinations.

UX gaps left

  • Reader body font, padding and colors remain with #1372, as directed.
  • The Perf VM frame p95 remains above the 16.7 ms guard on both sides. The three-run median was 112.8/107.1 ms (origin cold/warm) and 113.6/136.3 ms (branch cold/warm). The comparison wrapper recorded these values without changing the default guard. The branch warm result is higher; this remains visible in the baseline data.

Visual proof

Fourteen Mac-emulated production screenshots are attached to this issue: 1440, 820 and 390 px; light and dark themes; plain-text and HTML messages; plus the click and shortcut captures. Files are under artifacts/mailmorph/ in the worktree. No screenshots are committed.

Perf VM A/B

Three interleaved pairs used Chromium at 1440×900, a 10k-message fixture, 200 ArrowDown steps at 15 ms cadence, the same prebuilt server (d395b6e25da20d8c67b8b9fc1a593b813939c8a7), and separate origin/dev and branch production bundles. Each run used perf-run; load averages recorded inside each lock and raw runs are in docs/perf/baseline.json.

Median of 3 runs origin/dev branch
Cached click→visible p50 / p95 77.5 / 129 ms 5.8 / 8 ms
Body requests per cached open 9.6 0
Cold ArrowDown body requests per message 0.19 0.02
Cold / warm frame p95 112.8 / 107.1 ms 113.6 / 136.3 ms
Cold / warm server CPU 890 / 290 ms 590 / 20 ms
Cold / warm server RSS peak 236,980 / 238,528 KiB 232,660 / 239,920 KiB

The branch cached-open p95s were 13.3, 8.0 and 7.9 ms. The requested >50 ms trigger did not occur, so no one-switch profile or cost fix was needed. Origin may have prefetched the final body before the measured window; comparison mode records zero or one fetch there, while the branch keeps the one-fetch assertion. The 16.7 ms frame assertion remains enabled for ordinary benchmark runs.

Files

  • Mail UI: apps/web/src/lib/mail/MailKeptTabs.svelte, MailMorphCard.svelte, MailReaderContent.svelte, MailSenderLine.svelte, MailView.svelte, mailCache.ts, remoteContent.ts; apps/web/src/lib/components/AppToaster.svelte; apps/web/src/routes/+layout.svelte; apps/web/src/app.html; apps/web/src/calternal-app.css.
  • Mail E2E and package script: apps/web/e2e/mailmorph-1368.mjs, apps/web/package.json.
  • UI and tests: packages/ui/README.md, packages/ui/src/components/Pill.svelte, PillGroup.svelte, modeIsland.css; apps/web/src/lib/mail/MailSenderLine.svelte.test.ts, apps/web/src/lib/components/PillGroup.svelte.test.ts; tests under apps/web/src/lib/a11y, api, mail, and shortcuts.
  • Perf and decisions: bench/blaze.mjs, bench/blaze.md, docs/perf/baseline.json, docs/DESIGN.md, and contracts/perf/{adoption-1058,exceptions,ratchet}.json.

Gates

cd apps/web && bun run check — verbatim summary output:

PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages
perf-lint: parse Rust product sources
perf-lint: parse browser product sources
perf-lint: validate coverage and architecture
perf-lint: PASS; 0 violations; 22535 scoped exceptions
User browser caches use userStorage; only documented device/public-link exceptions remain.
Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css.
Text sizes and UI shape values use shared role tokens.
Keyboard focus rings use the shared focus tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
svelte-check found 0 errors and 4 warnings in 2 files

Focused Vitest after the final sender/date adjustment:

Test Files  2 passed (2)
     Tests  5 passed (5)
Start at  07:27:24
Duration  19.53s (transform 81%, environment 8%, import 6%, setup 3%, tests 1%)

Mail Morph production E2E (bun run test:e2e:mailmorph-1368):

PASS update toast stays on one line at 1440px
REGRESSION switch-open PASS (pointer; 66666666-6666-4666-8666-000000002710 -> 66666666-6666-4666-8666-00000000270f)
REGRESSION abort-stale PASS (66666666-6666-4666-8666-000000002622 aborted; 66666666-6666-4666-8666-000000002621 visible)
REGRESSION body-error-state PASS (66666666-6666-4666-8666-000000002623)
REGRESSION command-click-keeps-one PASS (66666666-6666-4666-8666-0000000026f2)
Mail Morph production regressions passed; screenshots are in /home/kayg/Developer/calternal-wt/mailmorph-1368/artifacts/mailmorph

The one full bun run test --maxWorkers=2 run passed before the final sender/date alignment adjustment:

Test Files  298 passed (298)
     Tests  2123 passed (2123)
Start at  02:56:53
Duration  289.63s (environment 28%, transform 25%, import 23%, tests 17%, setup 5%)

For the merge round, run cd apps/web && bun run test --maxWorkers=2 on the combined branch to verify the final state. No Rust files changed, so no Rust format, clippy or test gates were needed. cargo clean completed (Removed 0 files); web build output is absent.

Decisions

  • In the origin/dev comparison only, a selected body may already be prefetched before the measured window, so zero final-body fetches are recorded as valid when the body is visible. The default and branch one-fetch assertion remain intact.
  • The frame assertion is bypassed only by the explicit comparison environment variable so the requested raw A/B values are written. Normal benchmark runs still enforce 16.7 ms.
## Completed — #1368 Head: `c961f676fc88eff6fc4e4cd2b3725c6a27c23729` (`job/mailmorph-1368`). No push, deploy, merge or issue close. ### What changed - Kept messages use the shared `PillGroup` at normal size. The strip is content-width and appears only when at least one message is kept. - Keep uses the shared `IconButton` with a bookmark, the “Keep” tooltip and shortcut, and a tinted pressed state. Its center line matches the reader navigation buttons. - The phone sender line uses the shared Mail sender layout. The address truncates and the date stays aligned. - The shared toaster keeps “Update available” on one line at 1440 px. - The reader body and iframe styles were left to #1372. ### UX gaps closed - Kept Tabs have working open, remove, Copy link and context actions through the shared controls; the empty strip is hidden. - Keep is keyboard and pointer accessible and exposes its pressed state. - Sender, address and date stay on one row at phone width. - The toast stays on one line at desktop width. - Captures include immediately after click and shortcut open, as well as all requested viewport and theme combinations. ### UX gaps left - Reader body font, padding and colors remain with #1372, as directed. - The Perf VM frame p95 remains above the 16.7 ms guard on both sides. The three-run median was 112.8/107.1 ms (origin cold/warm) and 113.6/136.3 ms (branch cold/warm). The comparison wrapper recorded these values without changing the default guard. The branch warm result is higher; this remains visible in the baseline data. ### Visual proof Fourteen Mac-emulated production screenshots are attached to this issue: 1440, 820 and 390 px; light and dark themes; plain-text and HTML messages; plus the click and shortcut captures. Files are under `artifacts/mailmorph/` in the worktree. No screenshots are committed. ### Perf VM A/B Three interleaved pairs used Chromium at 1440×900, a 10k-message fixture, 200 ArrowDown steps at 15 ms cadence, the same prebuilt server (`d395b6e25da20d8c67b8b9fc1a593b813939c8a7`), and separate origin/dev and branch production bundles. Each run used `perf-run`; load averages recorded inside each lock and raw runs are in `docs/perf/baseline.json`. | Median of 3 runs | origin/dev | branch | | --- | ---: | ---: | | Cached click→visible p50 / p95 | 77.5 / 129 ms | 5.8 / 8 ms | | Body requests per cached open | 9.6 | 0 | | Cold ArrowDown body requests per message | 0.19 | 0.02 | | Cold / warm frame p95 | 112.8 / 107.1 ms | 113.6 / 136.3 ms | | Cold / warm server CPU | 890 / 290 ms | 590 / 20 ms | | Cold / warm server RSS peak | 236,980 / 238,528 KiB | 232,660 / 239,920 KiB | The branch cached-open p95s were 13.3, 8.0 and 7.9 ms. The requested >50 ms trigger did not occur, so no one-switch profile or cost fix was needed. Origin may have prefetched the final body before the measured window; comparison mode records zero or one fetch there, while the branch keeps the one-fetch assertion. The 16.7 ms frame assertion remains enabled for ordinary benchmark runs. ### Files - Mail UI: `apps/web/src/lib/mail/MailKeptTabs.svelte`, `MailMorphCard.svelte`, `MailReaderContent.svelte`, `MailSenderLine.svelte`, `MailView.svelte`, `mailCache.ts`, `remoteContent.ts`; `apps/web/src/lib/components/AppToaster.svelte`; `apps/web/src/routes/+layout.svelte`; `apps/web/src/app.html`; `apps/web/src/calternal-app.css`. - Mail E2E and package script: `apps/web/e2e/mailmorph-1368.mjs`, `apps/web/package.json`. - UI and tests: `packages/ui/README.md`, `packages/ui/src/components/Pill.svelte`, `PillGroup.svelte`, `modeIsland.css`; `apps/web/src/lib/mail/MailSenderLine.svelte.test.ts`, `apps/web/src/lib/components/PillGroup.svelte.test.ts`; tests under `apps/web/src/lib/a11y`, `api`, `mail`, and `shortcuts`. - Perf and decisions: `bench/blaze.mjs`, `bench/blaze.md`, `docs/perf/baseline.json`, `docs/DESIGN.md`, and `contracts/perf/{adoption-1058,exceptions,ratchet}.json`. ### Gates `cd apps/web && bun run check` — verbatim summary output: ```text PASS production dependency licences: 759 locked package releases across apps/web, apps/docs and packages perf-lint: parse Rust product sources perf-lint: parse browser product sources perf-lint: validate coverage and architecture perf-lint: PASS; 0 violations; 22535 scoped exceptions User browser caches use userStorage; only documented device/public-link exceptions remain. Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css. Text sizes and UI shape values use shared role tokens. Keyboard focus rings use the shared focus tokens. UI transitions and animation options use shared motion tokens or documented exceptions. svelte-check found 0 errors and 4 warnings in 2 files ``` Focused Vitest after the final sender/date adjustment: ```text Test Files 2 passed (2) Tests 5 passed (5) Start at 07:27:24 Duration 19.53s (transform 81%, environment 8%, import 6%, setup 3%, tests 1%) ``` Mail Morph production E2E (`bun run test:e2e:mailmorph-1368`): ```text PASS update toast stays on one line at 1440px REGRESSION switch-open PASS (pointer; 66666666-6666-4666-8666-000000002710 -> 66666666-6666-4666-8666-00000000270f) REGRESSION abort-stale PASS (66666666-6666-4666-8666-000000002622 aborted; 66666666-6666-4666-8666-000000002621 visible) REGRESSION body-error-state PASS (66666666-6666-4666-8666-000000002623) REGRESSION command-click-keeps-one PASS (66666666-6666-4666-8666-0000000026f2) Mail Morph production regressions passed; screenshots are in /home/kayg/Developer/calternal-wt/mailmorph-1368/artifacts/mailmorph ``` The one full `bun run test --maxWorkers=2` run passed before the final sender/date alignment adjustment: ```text Test Files 298 passed (298) Tests 2123 passed (2123) Start at 02:56:53 Duration 289.63s (environment 28%, transform 25%, import 23%, tests 17%, setup 5%) ``` For the merge round, run `cd apps/web && bun run test --maxWorkers=2` on the combined branch to verify the final state. No Rust files changed, so no Rust format, clippy or test gates were needed. `cargo clean` completed (`Removed 0 files`); web build output is absent. ### Decisions - In the origin/dev comparison only, a selected body may already be prefetched before the measured window, so zero final-body fetches are recorded as valid when the body is visible. The default and branch one-fetch assertion remain intact. - The frame assertion is bypassed only by the explicit comparison environment variable so the requested raw A/B values are written. Normal benchmark runs still enforce 16.7 ms.
Author
Owner

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.
kayg closed this issue 2026-10-09 05:36:54 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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-v0#1368
No description provided.