Two reports, both against 376a938, whose message claimed to fix them. Both
of its explanations were wrong. These two are measured.
Folding Pinned or Chats crashed with "No Observable object of type
WindowModel found". Not the mid-dispatch teardown the last two commits
described — deferring the write and freezing the item value were both
tried and both missed. Every list here hosts its rows in a bare
NSHostingView and injects only locale and colour scheme, so WindowModel
reaches a row purely by inheritance from an ancestor; ListViewKit parks
recycled rows in a reuse pool with no superview, and a fold detaches them
inside the very transaction the click started. A standalone harness
reproduces the trap verbatim in two shapes — a row configured before it is
attached, and a detached row re-rendering — and shows injection fixing
both. So each row's root now carries the window, the row empties itself
when it leaves the hierarchy, and the renderer takes the window as an
argument instead of reading @Environment from an AppKit callback.
The connection journey jumped upward the instant a connection succeeded,
and only with History open in the sidebar. That "only" was the clue, and
Kaede kept pointing at it: History is the one sidebar mode with a child
that demands height, and it appears exactly when status.isLive flips. Its
shell-integration banner used fixedSize(horizontal:vertical:), which
reports whatever height the sentence needs at whatever width it is asked
about — and SwiftUI asks about very narrow ones while sizing the column.
Ninety characters at a probe width wrap into a paragraph, the split view
adopts that as the height it must offer, and both columns are proposed it:
the detail column was handed 912x1125 instead of 912x688, which threw the
journey to y = -244.5 for one layout pass. A line limit bounds it.
Measured A/B/A on that one modifier: banner off, no inflated proposals;
banner on with fixedSize, 740 and 1125; banner on with lineLimit, none.
A second overflow of the same class, found while measuring and fixed here
too: the journey was a ZStack sibling of the terminal surface, so the pane
was as tall as its tallest child and a short pane reported 512 for a 440
proposal. frame(maxHeight: .infinity) passes that up rather than clamping
it. The journey is an overlay now — proposed the base's size, never
reporting one — so the terminal alone answers for the pane.
Not verified: the test suite was not run, and neither fix has been
confirmed by hand in the running app.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Floating the status bar fixed it reading as chrome bolted under the
content, and created the problem floating always has: it sits over the
pane. On a terminal it covers the last couple of lines in that corner,
and the only way out was closing the session.
It collapses to its status dot now, and expands again on click. The
preference is app-wide and persisted, beside the sidebar's section flags:
how much of the pane you are willing to have covered is a standing
choice, not a property of one session.
Collapsed is the same capsule, not a second container. A capsule as wide
as it is tall is a circle, so the shape morphs instead of cross-fading
between two shapes, and both states run through one chain from
`capsule(_:)` down so radius, height and corner placement cannot drift
apart. The height is identical either way, which is what lets the command
generator and the upload notices keep reserving one constant clearance.
`StatusBarPresentation.collapsedGlyph` answers two questions at once —
whether the capsule appears at all, and what it shrinks to. Asking them
separately is how you end up with a capsule on screen with nothing to
collapse into. The connection wins over a running transfer: a transfer is
something happening, the connection is the thing you are looking at.
`transferGlyph` moves out of the private transfer button into its own
view for the one case where the collapsed circle is not a connection dot
— a transfer running with no session in front of it.
fetch returns a page through the conversation, bounded at 256 KB. So a
page larger than that could not be read at all — the tail was truncated
and there was no second call that would reach it — and a page you meant
to refer back to was paid for twice, once to read and again to quote
into a file.
scratch_pull already solved this for hosts: it lands a remote file in
the workspace and returns a size and a digest, which is what lifts
sftp_read's inline ceiling. The network had no equivalent. scratch_fetch
is that, and its bound is the workspace's own 8 MB per-file limit rather
than what fits in a reply — so a large page arrives whole and is read a
window at a time with scratch_read.
A separate tool rather than a scratch_path on fetch, on the same grounds
scratch_pull and scratch_push were split out of sftp_read and
sftp_write. The flag would decide the tool's Safety — locallyReadOnly
without it, locallyContained with it — and one descriptor meaning two
things is harder for a model to choose correctly and harder to audit.
fetch is unchanged, byte for byte, so nothing already in a conversation's
history changes meaning.
locallyContained is the sum of its halves: reading a page, public or
private-network, is what fetch already does un-gated and never escalates
for, and the landing site is storage the app owns. It therefore needs no
approval and stays available while planning, which is right — staging
the documentation you are planning against is planning. The result
carries the title, size and digest and no page text; the title is enough
to confirm the right page arrived.
Both paths are proven in preflight, so a bad URL or a destination that
escapes the scope costs nothing rather than a page fetched and dropped.
An empty extraction fails instead of staging a zero-byte file the model
would then try to read.
851 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>