Skip to content

fix(mac): AppKit timeline P0s — revision adoption, disclosure height, scroll contract, SwiftUI seam - #444

Merged
SergeSerb2 merged 2 commits into
mainfrom
surgecode/appkit-timeline-fixes
Aug 2, 2026
Merged

fix(mac): AppKit timeline P0s — revision adoption, disclosure height, scroll contract, SwiftUI seam#444
SergeSerb2 merged 2 commits into
mainfrom
surgecode/appkit-timeline-fixes

Conversation

@SergeSerb2

Copy link
Copy Markdown
Owner

Summary

The AppKit timeline batch from the 2026-08-02 bug hunt — both P0s plus the scroll-contract and seam defects.

P0 — chat stops updating: a metadata-only presentation refresh (status flip, context-window tick, plan progress) stamped the store with the whole canonical revision including timeline lanes it never applied; the timeline mutation buffered behind it in the same 33 ms flush was then discarded as a duplicate — appended rows/streamed text silently never appeared until an unrelated rebuild. The refresh now claims only the lanes it applied (regression test included).

P0 — clipped disclosures: expanding any tool/reasoning/group/subagent row never invalidated the cached row height, so 40 lines of output rendered inside a 44 pt collapsed frame, permanently (the cache key ignored disclosure, so scrolling away and back replayed the stale height). Toggles now route through a controller-installed store callback that drops the cache entry, re-measures under anchor capture; the cache key carries the disclosure bit. Chevrons render only for rows that actually have expandable content.

Scroll contract (per docs/architecture/appkit-renderer.md):

  • User scrolling back to within 48 pt of the tail repins (previously nothing but the dock action could repin — on settled threads there was no repin affordance at all and the next turn streamed off-screen).
  • Scroller-knob drags and assistive scrolls now classify as user scrolls and unpin (they were treated as layout — reading history via the scrollbar got yanked to the tail on every delta).
  • Pinned scrolls account for the bottom content inset (streams sat 10 pt short of the true tail forever).
  • The Fenwick height index rebuilds from the same measured heights the table serves, so anchor restores/turn-rail jumps stop landing hundreds of points off after long scroll sessions.
  • The live streaming row is re-measured per in-place update instead of keeping a byte-count estimate all stream (30-line lists no longer overlap neighbors while streaming).

SwiftUI seam:

  • ChatTimelineHost gains a Coordinator deferring pin/activity callbacks past the current update (mirrors SidebarListHost) — removes modify-during-update undefined behavior.
  • ChatScreen stops stomping controller-owned pin state + activity on thread switch: returning to a thread keeps its restored viewport (was always yanked to tail one runloop later) and the activity dock stays visible mid-run (was hidden until the agent's phase changed).
  • The pinned activity dock's measured height becomes bottom content inset — it no longer occludes ~65 pt of live transcript; the composer material slab fades on a keyed animation instead of hard-cutting against scenery.
  • Turn rail forwards wheel events and hit-tests only its notch buttons (it swallowed scrolling over a 22 pt strip).

Also: subagent command elapsed labels respect the settled-thread guard (no immortal 1 Hz clock on idle threads); row action failures (approve/deny/retry/stop) surface in the app's error UI instead of a DEBUG-only field.

Area

  • apps/mac — native macOS app
  • apps/windows
  • apps/mobile
  • apps/server
  • Shared packages or relay
  • Build, CI, or release tooling
  • Docs

Release size

  • size:XS
  • size:S
  • size:M
  • size:L
  • size:XL

Verification

  • pnpm run verify --all after merging current main: 5/5 steps pass, exit 0
  • Full Swift suite: 275 + 1041 + 28 tests pass; new tests: revision-lane regression (store), near-tail repin + modality detach (anchor + controller), scroll suites updated to the architecture doc's repin rule

🤖 Generated with Claude Code

SergeSerb2 and others added 2 commits August 2, 2026 14:20
A selectionOrStatus/environment refresh rebuilt semantic rows from the
previous timeline but stamped the presentation snapshot with the whole
canonical revision read from the bridge — including timeline lanes it
never applied. When one 33ms batch flush carried a metadata publish plus
a timeline write, the controller's metadata fetch already saw the newer
timeline revision, adopted it, and the buffered timeline mutation was
then discarded as a duplicate: the appended row or streamed text never
appeared until an unrelated structural rebuild. The refresh now stamps
(held timeline lanes, applied metadata/environment lanes), so the
buffered mutation applies normally.

Also: a tool row's expandability now counts its parsed detail, not just
its output — the expanded body shows both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scroll contract (docs/architecture/appkit-renderer.md):
- ScrollAnchor repins on a user scroll that lands within 48pt of the
  tail (the doc's repin rule; previously only the dock action could
  repin, so a user who scrolled back down never re-followed the stream).
- Scroller-knob drags and assistive scrolls are classified as user
  scrolling (unattributed clip scrolls in TimelineTrackingScrollView),
  so they finally unpin instead of being yanked back on the next delta.
- scrollToBottom/jumpToTurn account for the bottom content inset; pinned
  streams no longer sit 10pt short of the actual tail.
- The Fenwick height index rebuilds from the same measured-height source
  the table delegate uses, so anchor restores and turn-rail jumps stop
  landing off by the accumulated measured-vs-estimated delta.
- The live streaming Markdown row is re-measured after every in-place
  update instead of keeping a byte-count estimate for the whole stream.

Disclosure (P0): toggling any tool/reasoning/group/subagent disclosure
now invalidates the row's cached height through a controller-installed
store callback, re-queries the row height under anchor capture, and
re-measures the expanded cell; the height-cache key also carries the
disclosure bit so a cached collapsed height can never answer for the
expanded state. Previously expanded output was clipped to the collapsed
frame forever. Chevrons render only when a row has expandable content.

SwiftUI seam:
- ChatTimelineHost gains a Coordinator that defers pin/activity
  callbacks past the current SwiftUI update (mirrors SidebarListHost),
  removing modify-during-update hazards that dropped writes.
- ChatScreen no longer stomps controller-owned pin state and activity on
  thread switch: returning to a thread keeps its restored viewport and
  the activity dock stays visible mid-run.
- The pinned activity dock's measured height is reserved as bottom
  content inset so it stops occluding the transcript tail; the material
  slab behind the composer fades with a keyed animation instead of
  popping.
- updateExternalPinState re-attaches only on a genuine transition,
  ending a programmatic scroll per body evaluation while pinned.

Rows/chrome: the turn rail forwards wheel events to the transcript and
hit-tests only its notch buttons (it swallowed scrolling over a 22pt
strip); subagent command elapsed labels respect the settled-thread
guard so idle threads own no ticking clock; row action failures are
routed to the app's error surface instead of a DEBUG-only field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SergeSerb2 SergeSerb2 added the size:L Broad feature or substantial cross-package change label Aug 2, 2026
@SergeSerb2
SergeSerb2 merged commit ea0b6fa into main Aug 2, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L Broad feature or substantial cross-package change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant