fix(session): size backend-initiated spawns, paste file paths - #369
Merged
Conversation
Impl 0039 covers #367 (backend-initiated spawns fork at 80x24 and lose scrollback). Feature 55 covers #368 (pasting a copied file inserts nothing); this revision fixes two errors in the earlier draft — the pasteboard read is new machinery rather than existing plumbing, and the handler must commit before the IPC round-trip rather than after it. Both were already in the working tree as mission inputs; committing them so the shipped code and the specs land together.
Two independent fixes. They land in one commit because both touch commands/session.rs, lib.rs, api.ts, and RunnerTerminal.tsx, and splitting them would leave an intermediate commit that does not build. #367 — backend-initiated spawn width (impl 0039) MCP mission_start and session_start_direct forked at DEFAULT_PTY_SIZE, seeding last_pty_cols = 80. The first real-cols resize then tripped the purge gate and discarded the whole transcript, so a mission left unopened lost everything the agent produced before its tab was visited. Cache a real measured grid in _app_state and read it at the fork. The gate is exact equality on cols, so an estimate one column off purges exactly as hard as no estimate — nothing is computed from window geometry, and Rust gains no chrome constants. Mission and chat panes sit in different boxes, so they cache under separate keys and never read each other. An empty cache still means 80x24; an explicit caller-supplied size still wins. Every resize path that reaches the backend records the settled grid, not just the main funnel: mission slot panes mount under display:none and push their first real grid through refreshActiveTerminal, which would otherwise leave the cache empty for exactly the panes this fixes. The resize dance records the settled rows, never the nudge. #368 — paste file paths (feature 55) Copying a file put no text on the clipboard, only public.file-url, so xterm's default paste inserted nothing. Read the flavor through a direct objc2 NSPasteboard binding and inject the POSIX paths instead, which is what Terminal.app, iTerm2, and Ghostty do. The handler gates on absence of usable text rather than item.kind, and commits synchronously before consulting the pasteboard — preventDefault after an await is a no-op, so it cannot call the command and then decide. Ordinary text pastes return early and pay no IPC. A file reference beats image bytes, so a Finder-copied shot.png pastes its path while a screenshot keeps #79's attach flow. Paths are quoted only when they contain whitespace or shell metacharacters, and go in through raw stdin so nothing is submitted. The orchestration lives in lib/terminalPaste.ts with injected effects so its ordering is testable without mounting xterm; each branch it encodes is a silent failure if it regresses. Closes #367 Closes #368
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships two independent fixes: #367 and #368.
Both are verified by Jason in the app; the review loop ran on the working-tree diff before this PR existed and came back clean.
#367 — backend-initiated spawns fork at 80x24 and lose scrollback
MCP
mission_startandsession_start_directforked atDEFAULT_PTY_SIZE, seedinglast_pty_cols = 80. The first real-cols resize then tripped the purge gate (session/manager/output.rs) and discarded the whole transcript — so a mission left unopened lost everything the agent produced before its tab was first visited. The longer it ran unobserved, the more was lost.Fixed by caching a real measured grid in
_app_stateand reading it at the fork. The gate is exact equality on cols, so an estimate one column off purges exactly as hard as no estimate at all — nothing is computed from window geometry, and Rust gains no chrome constants (which would have been a third copy of theruntimeClearsOnResizeduplication).runtime_clears_on_resize, and snapshot replay are unchanged. This removes the cause, not the safeguard.Every resize path that reaches the backend records the settled grid, not just the main funnel — mission slot panes mount under
display:noneand push their first real grid throughrefreshActiveTerminal, which would otherwise leave the cache empty for exactly the panes this fixes. The resize dance records the settled rows, never the nudge.#368 — pasting a copied file inserts nothing
Copying a file puts no text on the clipboard, only
public.file-url, so xterm's default paste inserted nothing. Now the flavor is read through a direct objc2NSPasteboardbinding and the POSIX paths are injected instead, matching Terminal.app / iTerm2 / Ghostty.item.kind === "file"— whether WKWebView exposes an arbitrary non-image file as afileitem is unverified, and a kind-based trigger that never fires would make the feature silently do nothing.preventDefaultafter anawaitis a no-op, so the handler cannot call the command and then decide.shot.pngpastes its path, while a screenshot or browser copy keeps bug: image paste into runner terminal not captured by agent CLI #79's attach flow exactly.inject_paste— nothing is submitted.The orchestration lives in
src/lib/terminalPaste.tswith injected effects so its ordering is testable without mounting xterm.Testing
Local, all green:
cargo fmt --check,cargo clippy --workspace --all-targets,cargo test --workspace(552),pnpm exec tsc --noEmit,pnpm run lint,pnpm test(268).New coverage:
_app_stateround-trip per surface incl. absent-key and pool-reopen persistence; 24 paste tests covering the text gate, quoting, and the full orchestration. The orchestration tests were mutation-checked —preventDefaultmoved after the await, text paste doing IPC, the #79 image-bytes fallback deleted, a trailing Enter on injection, and image bytes beating a file reference each fail the suite as they should.Manual smoke-test passed by Jason across both docs' Verification lists.
Notes
commands/session.rs,lib.rs,api.ts, andRunnerTerminal.tsx, so they land in one implementation commit — splitting them would leave an intermediate commit that does not build.objc2-app-kitgainsNSPasteboard+NSPasteboardItemandobjc2-foundationgainsNSURL; objc2 gates every class behind its own feature, so these would not compile otherwise. No new packages, soCargo.lockis unchanged.Closes #367
Closes #368