Skip to content

fix(session): size backend-initiated spawns, paste file paths - #369

Merged
yicheng47 merged 2 commits into
mainfrom
fix/367-368-spawn-width-and-paste-paths
Jul 28, 2026
Merged

fix(session): size backend-initiated spawns, paste file paths#369
yicheng47 merged 2 commits into
mainfrom
fix/367-368-spawn-width-and-paste-paths

Conversation

@yicheng47

Copy link
Copy Markdown
Owner

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_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 (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_state and 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 the runtimeClearsOnResize duplication).

  • Mission and chat panes sit in different boxes, so they cache under separate keys and never read each other.
  • Empty cache still means 80x24. No fallback estimate — guessing would not avoid the purge, and the cache self-heals once any pane has rendered.
  • An explicit caller-supplied size still wins; the UI path is untouched.
  • The purge gate, 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: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 — 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 objc2 NSPasteboard binding and the POSIX paths are injected instead, matching Terminal.app / iTerm2 / Ghostty.

  • Gates on absence of usable text, not item.kind === "file" — whether WKWebView exposes an arbitrary non-image file as a file item is unverified, and a kind-based trigger that never fires would make the feature silently do nothing.
  • Commits synchronously before consulting the pasteboard. preventDefault after an await is a no-op, so the handler cannot call the command and then decide.
  • Ordinary text pastes return early and pay no IPC.
  • A file reference beats image bytes: a Finder-copied shot.png pastes 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.
  • Quoted only when the path contains whitespace or shell metacharacters, with embedded single quotes escaped. Injected through raw stdin, never inject_paste — nothing is submitted.

The orchestration lives in src/lib/terminalPaste.ts with 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_state round-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 — preventDefault moved 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

  • The two fixes share commands/session.rs, lib.rs, api.ts, and RunnerTerminal.tsx, so they land in one implementation commit — splitting them would leave an intermediate commit that does not build.
  • objc2-app-kit gains NSPasteboard + NSPasteboardItem and objc2-foundation gains NSURL; objc2 gates every class behind its own feature, so these would not compile otherwise. No new packages, so Cargo.lock is unchanged.

Closes #367
Closes #368

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
@yicheng47
yicheng47 merged commit 42cffe2 into main Jul 28, 2026
2 checks passed
@yicheng47
yicheng47 deleted the fix/367-368-spawn-width-and-paste-paths branch July 28, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant