perf(chat): make session switching feel instant

Switching sessions ran as one synchronous commit: sidebar highlight, URL,
a full timeline remount with markdown re-parse, and around nine requests,
so nothing changed on screen for 150-250ms after the click.

- ChatContainer swaps the timeline on a deferred copy of the selection, so
  the active row, URL, and tab commit first and the timeline renders behind
  them; selection policy keeps reading the live store value.
- The message fetch starts before the selection is published.
- Sidebar rows stop re-rendering on a project switch: directory-scoped sync
  hooks read the runtime context and a subscribable current-directory source
  instead of the directory-bearing context; the grouping builder reads git
  branches through a ref and section caches key the branches they use;
  descendant ids are keyed by content. Rows per switch went from 73 to 8.
- Markdown skips the async re-render when the settled cached blocks are
  already painted, and mounts synchronously once its lazy module is loaded;
  the module is preloaded at boot.
- A timeline reveal gate holds a freshly opened session at opacity 0 while
  any provisional markdown paint catches up (250ms cap), then fades the whole
  timeline in once, so text, tools, and recap appear together.
- Switch fan-out trimmed: knowledge summary deduped, MCP status refreshed only
  when stale, non-repo directories cached by the git repo check, OpenChamber
  defaults cached briefly, agent memory reused for the same project, goal
  text cached, PWA manifest rebuilt after the switch settles.
- Header tabs snap into the active state and keep the title at the same
  height in both states.
- Prefetch on row press; composer focus moved off the commit.

`bun run profile:switch` records ack/content latency, longest task, and
requests per switch, cold and warm, and compares runs against a baseline.
Measured warm switch: ack 228ms to about 40-60ms, content 228ms to about
100-120ms.
This commit is contained in:
Bohdan Triapitsyn
2026-08-29 17:01:15 +03:00
parent 123c14260a
commit edfc9779cf
32 changed files with 947 additions and 118 deletions
+25
View File
@@ -12,6 +12,7 @@ or extending these scripts. The methodology rules they enforce come from
| `bun run profile:idle` | What the app does while nobody interacts with it. |
| `bun run profile:session` | What receiving and rendering a live assistant response costs. |
| `bun run profile:animation` | What a CSS animation costs, isolated from the app. |
| `bun run profile:switch` | How long switching sessions from the sidebar takes, cold and warm. |
| `bun run profile:browser` | A manually driven capture, for interactions that cannot be scripted. |
All of them measure a real browser over CDP. Pass `--help` to any of them for
@@ -106,6 +107,30 @@ top. Note that `rotate: 360deg` is *not* equivalent to
Add a variant to `animation-fixture.html` to measure a property or technique
that is not listed.
## profile:switch
Clicks sidebar session rows with real mouse input and measures, per click, the
two moments a user feels: `ack`, when the clicked row is highlighted as active
(the first visible reaction), and `content`, when the timeline shows messages
that were not on screen before. It also reports the longest main-thread task
inside each switch and every request the switch triggered, so fan-out
regressions show up next to the latency they cause.
Every session in the plan is visited twice. The first visit is usually cold
(a network round trip for messages); the second is warm, served from the
in-memory session store. They have different budgets and are reported
separately.
```bash
bun run profile:switch -- --url http://127.0.0.1:4599 --output artifacts/switch-before
bun run profile:switch -- --url http://127.0.0.1:4599 --baseline artifacts/switch-before --budget-ack 32 --budget-content 100
```
`--sessions a,b,c` picks the rows to click; the default is the first rows in
the sidebar, so pass explicit ids to compare runs across days. The row must be
present in the sidebar; the command fails rather than measuring a click on
nothing.
## Reading The Results
Every run writes a JSON summary next to any raw capture, so results can be