What happened?
When an agent session calls exit-worktree to leave a git worktree, the session view tears down and rebuilds. The message timeline unmounts, remounts with only 4 rendered messages, and content reassembles in-place — causing visible flicker, a large layout shift (CLS 0.38), and a 178ms jank burst. After the rebuild, when the user sends a follow-up message, the scroll position jumps from the bottom to near the top (scroll_top: 20451 → 4), forcing the user to manually scroll back.
Root cause chain
- Agent calls
exit-worktree → backend updates activeDirectory to project root
- Sync propagates
sync.data.path.directory change to frontend
directory-layout.tsx detects the change and navigate()s to a new URL with the updated base64-encoded directory
<Show when={resolved()} keyed> destroys and recreates the entire SDK/Sync subtree
- SyncProvider reset clears all message caches (
sync.data.message[id] becomes undefined)
session-main-view.tsx <Match when={...}> sees timelineMessagesReady → false → MessageTimeline unmounts
- Messages reload from backend →
timelineMessagesReady → true → MessageTimeline remounts with rendered_count: 4
Scroll-to-top (secondary)
After the rebuild, the user sends "下一步是什么?". A todo_force data refresh fires. The <Match when={...}> in session-main-view.tsx:132-136 uses its when value as an implicit SolidJS key. If timelineMessagesReady briefly becomes undefined during the refresh cycle, the <Match> destroys and recreates MessageTimeline. The new scroll container starts at scrollTop≈0, and the scroll-to-top is captured before forceScrollToBottom can recover.
Evidence from the session export:
| Time |
scroll_top |
user_scrolled |
scroll_height |
| 08:59:38.706 |
20451.5 |
false |
21356 |
| 08:59:41.070 |
— |
— |
— (jank=1, long_task=55ms) |
| 08:59:41.348 |
4 |
true |
21356 |
| 08:59:41.606 |
2256 |
true |
— |
scroll_height is identical (21356), ruling out content-height change. scroll_top=4 is near-zero but not zero, consistent with DOM replacement (browser overflow-anchor residual) rather than a scrollTo(0,0) call.
Steps to reproduce
- Open PawWork in a git project
- Ask the agent to
enter-worktree into a git worktree
- Let the agent complete some work (commits, file changes)
- Ask the agent to
exit-worktree
- Observe the timeline flash, content rebuild, and layout shift
- Send a follow-up message after the rebuild
- Observe the scroll position jump to the top
What did you expect to happen?
- Exiting a worktree should keep the existing message timeline intact
- No visible teardown or rebuild of the session view
- Scroll position should be preserved across the directory change
Diagnostics
Renderer diagnostics from session export
| Time |
Event |
Detail |
| 08:58:59 |
session.identity.transition |
Session identity remapped |
| 08:59:03 |
Agent calls exit-worktree |
Directory: worktree → project root |
| 08:59:16 |
session.timeline.mount |
rendered_count: 4 (was 70+) |
| 08:59:16 |
session.data.refresh |
message_force — 211ms |
| 08:59:21 |
incident.session_jank_burst |
long_task=178ms, frame_gap=258ms |
| 08:59:26 |
incident.session_layout_shift |
CLS=0.3797 |
| 08:59:41 |
scroll_top drops ~20451 → 4 |
View jumps to top |
Code pointers
packages/app/src/pages/directory-layout.tsx — <Show when={resolved()} keyed> destroys subtree on directory change
packages/app/src/pages/session/session-main-view.tsx:132-136 — <Match when={...}> implicit key destroys MessageTimeline on transient state changes
packages/app/src/pages/session/message-timeline.tsx:415-425 — onMount/onCleanup emit timeline mount/unmount events
packages/app/src/pages/session/use-session-scroll-dock.ts — createAutoScroll state reset on scrollRef change
packages/ui/src/hooks/create-auto-scroll.tsx — scroll position tracking and auto-follow logic
Environment
- PawWork: dev (local build)
- OS: macOS 25.3.0
- Session export:
pawwork-session-nimble-wizard-2026-05-04-08-59-46.json
What happened?
When an agent session calls
exit-worktreeto leave a git worktree, the session view tears down and rebuilds. The message timeline unmounts, remounts with only 4 rendered messages, and content reassembles in-place — causing visible flicker, a large layout shift (CLS 0.38), and a 178ms jank burst. After the rebuild, when the user sends a follow-up message, the scroll position jumps from the bottom to near the top (scroll_top: 20451 → 4), forcing the user to manually scroll back.Root cause chain
exit-worktree→ backend updatesactiveDirectoryto project rootsync.data.path.directorychange to frontenddirectory-layout.tsxdetects the change andnavigate()s to a new URL with the updated base64-encoded directory<Show when={resolved()} keyed>destroys and recreates the entire SDK/Sync subtreesync.data.message[id]becomesundefined)session-main-view.tsx<Match when={...}>seestimelineMessagesReady→false→MessageTimelineunmountstimelineMessagesReady→true→MessageTimelineremounts withrendered_count: 4Scroll-to-top (secondary)
After the rebuild, the user sends "下一步是什么?". A
todo_forcedata refresh fires. The<Match when={...}>insession-main-view.tsx:132-136uses itswhenvalue as an implicit SolidJS key. IftimelineMessagesReadybriefly becomesundefinedduring the refresh cycle, the<Match>destroys and recreatesMessageTimeline. The new scroll container starts at scrollTop≈0, and the scroll-to-top is captured beforeforceScrollToBottomcan recover.Evidence from the session export:
scroll_heightis identical (21356), ruling out content-height change.scroll_top=4is near-zero but not zero, consistent with DOM replacement (browser overflow-anchor residual) rather than ascrollTo(0,0)call.Steps to reproduce
enter-worktreeinto a git worktreeexit-worktreeWhat did you expect to happen?
Diagnostics
Renderer diagnostics from session export
session.identity.transitionexit-worktreesession.timeline.mountrendered_count: 4(was 70+)session.data.refreshmessage_force— 211msincident.session_jank_burstincident.session_layout_shiftCode pointers
packages/app/src/pages/directory-layout.tsx—<Show when={resolved()} keyed>destroys subtree on directory changepackages/app/src/pages/session/session-main-view.tsx:132-136—<Match when={...}>implicit key destroys MessageTimeline on transient state changespackages/app/src/pages/session/message-timeline.tsx:415-425—onMount/onCleanupemit timeline mount/unmount eventspackages/app/src/pages/session/use-session-scroll-dock.ts—createAutoScrollstate reset on scrollRef changepackages/ui/src/hooks/create-auto-scroll.tsx— scroll position tracking and auto-follow logicEnvironment
pawwork-session-nimble-wizard-2026-05-04-08-59-46.json