Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/performance/scroll-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,13 @@ Installed T124640Z before source edits: each physical notch delivers one SGR whe

After: actual T134212Z dependencies plus only the Toad source overlay, same authentic P108 root/display/font. Settings search shows default1; editing to3 immediately gives3rows/notch in both windows, then1 persists on reset. At1 each viewport receives15 notches (five isolated, ten fast) and makes15 one-row native moves. Ten rapid notches cover10 rows briskly rather than coarse two-row jumps, with no animation backlog; this is not controlled latency A/B. Recording and inspected frames: `/home/ts/.cache/agent-scratch/core-p170-wheel/after/wheel.mp4`; result.json separates reader-restoration compensation. An initial Ctrl+A edit inserted31 (Home, not Select All); that real data is retained, then explicit selection corrected3→1. No provider input/replay. Original index restored byte-identically, bus0bytes; original config untouched via a separate private copy; normal close. Source **3files+19/-9, net+10**, approved Src-Budget10, ratchet0/no positive metrics; AST282 Toad/254 native modules, no omissions, dynamic binding remains qualified. Installed acceptance is still Codex-owned.

### 2026-11-01 installed wrapping capture — incomplete, no timing verdict

| Installed inputs | Recording and sampling | Qualification |
|---|---|---|
| Core `4d3a1c083`, Toad `2f982fe7`, Textual `7c62867` | Capture05: 36-second, 60-fps recording completed (video rc0). Read-only py-spy requested 20 seconds at 100 Hz, fell over 40 seconds behind and exceeded capture_live's 50-second wall limit (profiler wrapper rc1). After the owned target exited it wrote 573 samples, 0 sample errors. | **Not a completed fixed-scenario or frame-time result.** The first image still shows Connecting; held PageUp/PageDown began before saved history was ready. The later image shows connected history. No total-frame or input-to-paint median/p95 is established, and sampling backlog prevents a representative CPU-time claim. |

This was the installed default `toad-comms` launcher in plain st on owned Xvfb `:3`, viewing this agent's existing active session with private XDG directories. No prompt was submitted and no uncertain input was replayed. The owned frontend/display retired and the host lease released. Artifacts: `/home/ts/.cache/agent-scratch/pi-ui-performance/installed-scroll-4d3-05/` (`launch.json`, `before.png`, `after.png`, `scroll.mp4`, `profile.log`, `held-pageup-manifest.json`, `held-pageup.speedscope.json`, `result.json`, `retirement.json`); controller log beside that directory. This does not establish movement during the hold from the endpoint images alone.

The subsequent receipt-height fix, Toad `375ec8f9` / Core `c8b2fb4d4`, was installed after this recording. It has no physical timing result in this row. `capture_live.py --frame-meter` uses `sys.remote_exec`; that running-process injection was not used. A qualifying follow-through must establish ready history and actual composer focus before gestures, retain the normal-launch instrumentation boundary, and report the receipt-height and wrapping behavior on the exact reopened build rather than transferring this failed run to it.

3 changes: 3 additions & 0 deletions src/toad/transcript_preparation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from toad.render_backend import ReusableRenderTask
from toad.transcript_state import LatestViewportRequest
from toad.widgets.committed_presentation import PreparedAppendCommit, PreparedPageCommit
from toad.widgets.history_anchor import LatestPosition
from toad.widgets.message_notifications import NothingToRead, PaintedRead
from toad.widgets.presentation_window import OLDER_END, PageEnd, PageRequest
Expand Down Expand Up @@ -288,6 +289,7 @@ class PreparedPageSource:
runtime: PreparationRuntime
scope: PreparationScope
through: TranscriptCursor
commit_preparation: ClassVar[type[PreparedPageCommit]] = PreparedPageCommit

def __init__(self) -> None:
# This source holds the resources after their widgets detach. These are the
Expand Down Expand Up @@ -499,6 +501,7 @@ class SavedHistory(ReplaceablePageSource):

# The work that reads and prepares one page of this kind of source.
work: ClassVar[type[TranscriptPageWork]] = TranscriptPageWork
commit_preparation = PreparedAppendCommit

def __init__(
self, loader: Callable[..., Awaitable[TranscriptPage]], through: TranscriptCursor,
Expand Down
21 changes: 20 additions & 1 deletion src/toad/transcript_source_preparation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""The pager's source-owned read, lookahead and retirement lifetime."""
from __future__ import annotations

Expand Down Expand Up @@ -310,8 +310,27 @@
edge = self.pages[-1].prepared
return NEWER_END.can_extend(edge) or edge.page.has_newer

async def publish_append(self, through, publication_current: Callable[[], bool]) -> bool:
"""A commit may retire live blocks only after publishing its destination.

extend also serves held readers: its True can mean access advanced
without new paint. A followed commit must instead finish a known gap
through the existing bounded destination owner before reporting success.
"""
if not await self.extend(through, publication_current):
return False
if not publication_current():
return False
if self.drawn_end_more:
if not await self.jump_latest(
LatestViewportRequest(LatestPosition.hold(self.window)), publication_current,
):
return False
return (publication_current() and not self.drawn_end_more
and self.pages[-1].prepared.page.after.same_position(through))

async def extend(self, through, publication_current: Callable[[], bool] = lambda: True) -> bool:
"""Append to a complete followed tail; a gap uses the bounded latest read."""
"""Extend source access, preserving held readers that have not drawn its end."""
if not publication_current():
return False
self.page_source.through = through
Expand Down
87 changes: 53 additions & 34 deletions src/toad/widgets/committed_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ class PreparedCommit:
history: CommittedHistory
sequences: frozenset[int]

@classmethod
async def capture(cls, view: Conversation, history: TranscriptHistory, page: TranscriptPage,
sequences: frozenset[int]) -> Self:
"""A retained reader accepts coverage without preparing replacement paint."""
return cls(history, sequences)

@property
def outgoing(self) -> frozenset[OutgoingMessageReference]:
return self.history.outgoing_coverage
Expand All @@ -263,6 +269,13 @@ class PreparedPageCommit(PreparedCommit):
history: TranscriptHistory
page: PreparedTranscriptPage

@classmethod
async def read_snapshot[Snapshot](
cls, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory,
) -> Snapshot:
"""Replacement needs the complete destination, even for a closed prefix."""
return await read()

@classmethod
async def capture(cls, view: Conversation, history: TranscriptHistory, page: TranscriptPage, sequences: frozenset[int]) -> Self:
from toad.transcript_preparation import TranscriptPageRenderTask
Expand All @@ -281,21 +294,51 @@ async def draw(self, cursor, is_current: Callable[[], bool], live_blocks) -> boo
if not history.source_ready or not is_current():
return False
operation = history.reserve_source_work(partial(CommitTranscript, live_blocks=live_blocks))
return await operation.execute(history, partial(
history.show_newest,
return await operation.execute(history, partial(self.publish, cursor, is_current))

async def publish(self, cursor: TranscriptCursor, is_current: Callable[[], bool]) -> bool:
history = self.history
return await history.show_newest(
PreparedReplacementViewportRequest(LatestPosition.hold(history.window), self.page),
publication_current=is_current,
))
)


class PreparedAppendCommit(PreparedPageCommit):
"""The saved source extends its original pages, retaining their rendered prefix."""

@classmethod
async def read_snapshot[Snapshot](
cls, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory,
) -> Snapshot:
return await read(position=history.page_source.through.later_page())

async def publish(self, cursor: TranscriptCursor, is_current: Callable[[], bool]) -> bool:
return await self.history.publish_append(cursor, publication_current=is_current)


class CommitPlan(ABC):
"""The captured viewport intent owns source acquisition and publication."""
"""Capture source acquisition and viewport publication together for one read."""

preparation: type[PreparedCommit]

def preparation_for_source(self, source: type[PreparedPageCommit]) -> type[PreparedCommit]:
"""A held viewport retains its original paint after a successful read."""
return PreparedCommit

@abstractmethod
async def read_snapshot[Snapshot](
self, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory,
) -> Snapshot:
"""Acquire the saved destination this plan will publish."""
"""The source declares acquisition; its result fixes this read's publication."""
source = history.page_source.commit_preparation
self.preparation = self.preparation_for_source(source)
try:
return await source.read_snapshot(read, history)
except TranscriptChanged:
# A full retry is a replacement, even when the file path is reused
# and its new cursor contains the old byte offset.
self.preparation = PreparedPageCommit
return await PreparedPageCommit.read_snapshot(read, history)

@abstractmethod
def current(self, window: HistoryWindow) -> bool:
Expand All @@ -316,14 +359,9 @@ async def prepare(self, view: Conversation, history: TranscriptHistory,
sequences |= history.covered_sequences(required) | frozenset(
sequence for sequence in required if page.after.covers_incoming(sequence)
)
return await self.extend(view, history, page, sequences)
return await self.preparation.capture(view, history, page, sequences)
return await PreparedPageCommit.capture(view, history, page, sequences)

@abstractmethod
async def extend(self, view: Conversation, history: TranscriptHistory, page: TranscriptPage,
sequences: frozenset[int]) -> PreparedCommit:
"""Prepare publication within the history's existing source."""

def commit(self, prepared: PreparedCommit, cursor: TranscriptCursor) -> None:
"""Apply source metadata only after the final retirement checks."""

Expand All @@ -339,18 +377,12 @@ class FollowTailCommit(CommitPlan):
def __init__(self, window: HistoryWindow) -> None:
self.revision = window.scroll_revision

async def read_snapshot[Snapshot](
self, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory,
) -> Snapshot:
# No after cursor: Core reads newest-first at its captured saved cut.
return await read()

def current(self, window: HistoryWindow) -> bool:
return window.follows_tail and window.scroll_revision == self.revision

async def extend(self, view: Conversation, history: TranscriptHistory, page: TranscriptPage,
sequences: frozenset[int]) -> PreparedPageCommit:
return await PreparedPageCommit.capture(view, history, page, sequences)
def preparation_for_source(self, source: type[PreparedPageCommit]) -> type[PreparedPageCommit]:
"""A following viewport publishes through the same member that acquired it."""
return source

def finish(self, view, cursor) -> None:
view.transcript.binding.painted(view.transcript, cursor)
Expand All @@ -363,15 +395,6 @@ def restore_reader(self, window) -> None:
window.anchor()

class RetainViewportCommit(CommitPlan):
async def read_snapshot[Snapshot](
self, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory,
) -> Snapshot:
try:
return await read(position=history.page_source.through.later_page())
except TranscriptChanged:
# A replaced source cannot extend the old reader's source range.
return await read()

def current(self, window: HistoryWindow) -> bool:
# Extending the source leaves the loaded rows and reader position intact.
# Retirement checks the current viewport again when applying the change.
Expand All @@ -392,10 +415,6 @@ def permits(self, view, candidates) -> bool:
return not any(widget in visible and visible[widget][0].overlaps(viewport)
for widget in candidates)

async def extend(self, view: Conversation, history: TranscriptHistory, page: TranscriptPage,
sequences: frozenset[int]) -> PreparedCommit:
return PreparedCommit(history, sequences)

def commit(self, prepared, cursor) -> None:
prepared.history.retain_committed(cursor)

Expand Down
6 changes: 5 additions & 1 deletion src/toad/widgets/conversation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import annotations

import asyncio
Expand Down Expand Up @@ -1782,7 +1782,11 @@
# Height pressure is not source evidence. Dropping the source
# pager or an uncovered wire claim lets a later observation
# recreate the same record as a new tail arrival.
if self.contents.virtual_size.height > high_mark:
# Saved pages already have a bounded preparation owner. Only
# the native tail consumes the live-block pressure allowance.
live_height = (self.contents.virtual_size.height
- self.transcript.binding.history.outer_size.height)
if live_height > high_mark:
self.transcript.require_commit()
else:
await self.prune_window(low_mark, high_mark)
Expand Down
Loading
Loading