Public Access
claude/sftp-connection-icons-5ad752
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2ff9dff560 |
Terminal: deliver large pastes whole, keep keystrokes in order, and keep one session's failure off the shared socket
- A message larger than the 64 KiB receive buffer was skipped and its tail dispatched as a frame of its own. Messages are now reassembled up to 16 MiB; one beyond that is dropped whole. - Input frames are handed to the pump without waiting, and ShellStream takes no lock around its write buffer, so a snippet's text and its carriage return could interleave or lose bytes. Writes are serialised per pump in arrival order. - A resize reaching a shell SSH.NET had already disposed threw out of the receive loop and closed the socket every tab shares. Each frame's failure now stays with its session. |
||
|
|
1908189100 |
Make the terminal's right button copy and paste, and never open a menu
Selecting output and right-clicking it opened the WebView's own context menu with Copy on it, which is correct behaviour for a web page and wrong for a terminal. What a terminal's right button has meant since the Windows console got QuickEdit, and since PuTTY shipped, is this: with something selected it copies and drops the selection, with nothing selected it pastes. No menu, no second click. The gesture can only live in the page. The selection is drawn into a canvas the host never sees, and once a terminal has focus the host's window receives no input events at all — the same reason the Ctrl+plus chords are handled there. ◆ THE CLIPBOARD IS THE HOST'S IN BOTH DIRECTIONS, AND THE READING IS WHY. navigator.clipboard.readText() is gated behind the clipboard-read permission in every Chromium, and an embedded WebView with no PermissionRequested handler answers a permission request by putting a prompt on the screen. Trading a context menu for a permission dialog is not this change. So the page asks and the host answers, exactly as requestFontSizeStep does. Writing does not go the other way either, although navigator.clipboard.writeText would have worked from the page on Windows: a clipboard that is the host's in one direction and the page's in the other is two mechanisms to keep working across WebView2, WebKitGTK and Android's WebView. The host has owned a clipboard delegate since the keychain grew its COPY buttons, so each App.axaml.cs grows a ClipboardReader beside its ClipboardWriter and MainWindowViewModel takes it. Three opcodes: client ClipboardCopy and ClipboardPaste, server Clipboard. ◆ Clipboard IS NOT Paste, AND THE DIFFERENCE IS THE BRACKETING. Paste carries a snippet off the sidebar, and the page deliberately *types* a single-line one rather than bracketing it, because bash marks a bracketed paste as an active region and a snippet should read as though it had been typed. A clipboard paste is the case those markers are for — readline uses them to decide it has been pasted into — so Clipboard is always bracketed and never followed by an Enter. One opcode for both would have needed a flag meaning "which of the two kinds of paste is this", on a payload whose existing flag byte means something else. Both client frames are dispatched before the session lookup, which is what keeps the gesture working in a pane whose shell has just ended: squinting at the message a remote left behind is exactly when somebody copies the line. Dispatch was past MA0051's length once it grew a second such branch, so the frames no pump needs to see moved into Announce() and the font-size chord went with them. ◆ CAPTURED AND STOPPED, RATHER THAN MERELY PREVENTED. preventDefault alone suppresses the menu and still lets xterm's own contextmenu handler run, which moves its helper textarea under the pointer at z-index 1000 and leaves the selection sitting in it, selected. Both of those exist to serve the native menu. Taking the event in the capture phase on the pane and stopping it there is what keeps the gesture wholly the page's. rightClickSelectsWord is now false, which on macOS is a change of default rather than a restatement of it: xterm sets it true there alone, so a right-click would have selected the word under the pointer and copied that instead of pasting — wrong on one platform only, which is the worst way for it to be wrong. The renderer harness grows the pane it has been inviting since RendererReconnectionTests: a fake Terminal, the two addons, and a real UTF-8 codec, because both directions run through the page's own TextEncoder and TextDecoder and a Latin-1 stub would pass a test with an em dash in the selection. Its warning about declaring names terminal.js declares now covers the half that does not SyntaxError — a harness frame() was silently replaced by the page's frame() and failed six tests on a type error a long way from the cause. Manual checks 1.11 and 11.13 are what is left for a person: that a real contextmenu event reaches a capture listener, that no native menu is drawn once it is prevented, and that xterm's real paste() brackets. 11.13 also records what is unverified — whether Android's WebView delivers a pointer's secondary click as a contextmenu event at all. ClipboardReader is wired on that head regardless, because the alternative is a head where the paste half silently does nothing and nobody finds out; that head is not compiled here either, this machine having no android workload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef12e8cc99 |
Stop the data plane's tests racing the socket they just connected
CI went red on the run that added the renderer tests, and not on anything they assert: TerminalDataPlaneTests.Output_ReachesTheRenderer read the output frame where it expected the session's opening one, having lost a race that has been in the helper since it was written. Connected and attached are two different moments. ClientWebSocket.ConnectAsync completes on the 101, which UpgradeAsync writes before it has a WebSocket to attach — it builds one from the stream and swaps it in a few instructions later, on the accept thread. SendAsync drops anything sent in between, which is the transport's documented contract rather than a bug: there is nowhere to put a frame for a renderer that is not there, and queueing it is the unbounded growth the credit window exists to prevent. So a helper that returned on the handshake and let its caller send immediately was betting on thread scheduling, every run, on every test in the file. It only started losing now because that assembly grew nine tests and a JavaScript engine to run them in, which is more work in parallel with a window measured in instructions. The race is older than the branch that exposed it. ConnectAsync now waits for the plane's own SocketAttached, subscribed before the connection because the event can be over before ConnectAsync returns, and bounded so a socket that never attaches fails the helper instead of hanging the suite in a later receive. TerminalWorkspaceTests.ConnectRendererAsync has the same exposure through OpenSessionAsync's opening frame and now waits on WaitForRendererAsync, with a note on why that is enough for the reattach tests and what would stop being enough. ◆ NOTHING IN src CHANGED, AND THAT IS THE CONCLUSION RATHER THAN THE SHORTCUT. Production waits for exactly this moment already — every path that opens a session goes through TerminalWorkspace.WaitForRendererAsync, which resolves from the same few lines that raise the event this helper now waits on. Only the tests skipped the gate the application does not. Verified by widening the window rather than by hunting the flake: a 100ms delay inserted between the 101 and the attach, in a throwaway tree, hangs the old helper outright — both frames dropped, the test blocked in receive — and passes 89/89 with this one. Green three times over on the CI platform besides (Alpine, musl, Release, dotnet/sdk:10.0-alpine). |
||
|
|
095774c498 |
Take a returning renderer's socket over instead of refusing it
One attach per process was WebView2's truth, not Android's: the phone kills the WebView's renderer independently of the app process, the page reloads, and its fresh socket was answered 409 by a guard that never reset — with no way back short of restarting the app. Only our own page knows the token, so a second valid upgrade is that page returning; it now displaces the old socket, which may never notice it is dead on its own, since a killed renderer sends no FIN. A send into the dead socket also no longer escapes as a fault. It used to unwind the pump's flush loop, after which nothing drained the credit window and the still-live shell froze behind it for good — including the BCL quirk where such a send surfaces as an OperationCanceledException nobody's token asked for. |
||
|
|
c00e5dbc5c |
Let the terminal's text be made bigger, and remember how big
Taking pinch-zoom off the phone left nothing in its place, and there was nothing on the desktop either. This is the replacement, and it is deliberately not the thing that was removed: zoom scales what has already been drawn, so the remote goes on wrapping to a width that is no longer on screen. Changing the font size refits the grid and reports the new column count, so the far end is told it has fewer columns. That round trip is the feature. The size is one number, owned by the shell. It has to be, for two reasons that pull the same way: it must survive a relaunch, and it must be reachable from a phone that has no Ctrl key to press. So the page asks and the host decides — a signed step over a new client opcode, answered with a size over a new server opcode. The phone's buttons and the desktop's chords arrive at the same place, and a size set by either is the size both remember. Stored in settings.json beside the cache rather than in it, and that is not laziness about a migration. The cache is encrypted and unreadable until a vault is unlocked, and the first terminal of a locked launch needs the size already. Nothing secret may go in that file; ClientSettings says so out loud, because the next person to add a preference is the one who needs to read it. Where it is reachable from differs per head, and only here. The phone gets A− and A+ on the connection line — not in the accessory row, which scrolls, and a control that fixes unreadable text must never be the thing that is off-screen. The desktop gets the three chords every terminal emulator has, answered by the page while a terminal has focus and by the window when it does not, plus a row in preferences that shows the current value and names the chords rather than replacing them. Someone whose terminal is too small to read is not in a position to go looking. Clamped 8 to 32. Below eight a monospace grid stops being legible and becomes a texture, and every column of it is still a column the remote is being told exists; above thirty-two a phone in portrait has too few columns to hold a prompt. The buttons disable at the ends rather than accepting presses that do nothing, which on a terminal reads as the application having stopped responding. The preferences screen's header comment claimed none of the design's terminal settings could be saved, and listed the three things that were missing to make one work. All three now exist, so it says which one is real and why the other five still are not. Verified with the protocol suite — including that the step byte round-trips signed, since read unsigned a step down arrives as 255 and clamps to the largest font, making "smaller" do the most dramatic available version of "larger" — a data-plane test that the chord is heard with no session registered, and five shell tests: the default matches the renderer's, both clamps hold, reset works, and a size chosen in one shell is there in a second one over the same profile directory. Layout suite and both heads build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eb354bcdd9 |
Add the SSH session layer and the terminal data plane
The throughput harness the plan requires before any UI, plus the SSH plumbing under it. 94 new tests, no WebView involved. Credit-based flow control is what makes `yes` survivable. A terminal renders at 60 Hz at best while a remote produces output as fast as the network allows, and the difference has to accumulate somewhere or be refused. Credit is reserved *before* reading, never after: because the pump cannot read more than the renderer has room for, the coalescing buffer is bounded by the window rather than by how fast the remote can talk. When credit runs out the pump stops reading, SSH's own receive window closes, and the remote sshd blocks -- backpressure to the source with no custom protocol. Verified by falsification, not just by passing: with the credit gate removed three tests fail, including the throughput harness's bounded-memory assertion. Acknowledgements are clamped because they cross into JavaScript, where a buggy or hostile page could otherwise claim to have rendered a gigabyte and talk the host into an unbounded read. Host key trust is enforced by *failing* the connection rather than prompting inside the handshake. SSH.NET raises verification synchronously, so consulting the user there would block the handshake on a UI round trip and deadlock the first time the prompt needed the UI thread. Unknown host and changed key become distinct exceptions the caller resolves asynchronously. A mismatch has no retry path at all: a dialog offering to continue is how users are trained to click through the one warning that actually indicates interception. A legitimately rebuilt server is handled by removing the pin in settings, away from the moment of connecting. The data plane serves the renderer page from the same loopback listener as the socket, which makes Origin predictable -- always http://127.0.0.1:{port} -- where a WebView virtual-host mapping would give a different origin per backend and nothing to validate. The token is substituted at serve time, so it never touches disk and never appears in a URL. Being clear about what that buys: not protection from a process running as this user, which can read our memory anyway, but from a page in the user's browser attempting WebSocket connections to loopback ports, which is a real and routine thing. Two bugs the tests caught. The accept loop handled connections serially, so an upgraded WebSocket parked it inside the receive loop and every later request went unanswered -- the page's own script among them. The suite hung rather than failed, which is how I found it. And SHA-1 is unavoidable here: RFC 6455 mandates it for Sec-WebSocket-Accept, where it authenticates nothing. Suppressed narrowly with that reasoning; the alternative, HttpListener.AcceptWebSocketAsync, throws PlatformNotSupportedException off Windows. |