Repository navigation
Conversation
Add rawData to subscribed replies to order snapshots before live output. Write directly to xterm, isolate session sockets, and retry after disconnects. Reset the terminal in band (RIS) so queued writes and a half-parsed escape sequence cannot corrupt a snapshot. Keep HTTP input fallback and existing raw-buffer endpoints.
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.
Summary
The web terminal takes its content from two places: an HTTP request for the buffer, and the WebSocket stream for new output. The page also keeps a second copy of everything the session printed in React state, and it never reopens the WebSocket after it drops. This PR makes the WebSocket the only source. A subscribe reply now carries the current buffer, live output follows it on the same connection, and the page reconnects by itself.
In plain terms: the web page asked for a terminal's history in one request and then listened for new output in another, so lines could be missed or shown twice around the moment it started listening. It also stored a full copy of the output in the page, and a dropped connection left the page dead until you reloaded it. Now the page opens one connection per terminal, gets the history and the live output from it in order, and reconnects after a drop.
Cause
Appappended every chunk torawOutput(prev + rawData) and passed it down to xterm, which already holds the output. That copy grows with the session for as long as the page is open.api.session.buffer.rawover HTTP, separate from thesubscribemessage on the WebSocket. Output produced between the two requests could be missed or appear in both.useWebSocketonly setconnectedto false ononcloseand never reconnected, so a dropped socket stayed dropped. Its effect also depended onactiveSessionand three callbacks, so the socket was rebuilt whenever any of them changed.Changes
Server:
handlers/websocket.tsandshared/types.ts: thesubscribedreply includesrawData, the buffer at subscribe time. The HTTP raw-buffer endpoints stay.Client:
hooks/session-socket.ts(connectSessionSocket): opens a socket for one session, sendssession_listandsubscribeon open, handssubscribedtoonSnapshotand the session'sraw_datatoonRawData, reconnects afterRETRY_DELAYon close, and ignores messages from sockets that were closed on purpose.use-web-socket.ts: one socket per selected session id, using the module above. Autoselection of the first running session stays.sendInputreturns whether the socket was open.terminal-renderer.tsx: norawOutputprop and nocomponentDidUpdatediffing. It exposeswrite(chunk)andreset(snapshot)through a ref.resetwritesESC c(RIS) followed by the snapshot through xterm's write queue.Terminal.reset()acts outside that queue, so queued writes and a half-parsed escape sequence could corrupt the first characters of a snapshot.app.tsx: writes straight to the terminal through the ref, and no longer fetches the buffer over HTTP.use-session-manager.ts: input goes through the WebSocket and falls back to HTTP POST when the socket is not open, as before.handleSessionClickis gone, since selecting a session only changesactiveSessionand the socket effect subscribes.Behavior changes
subscribedWebSocket message gains arawDatafield. Existing clients ignore it.Debug: active: ..., WS raw_data: n, session_updates: n).Validation
Three new tests:
test/websocket.test.ts: the snapshot arrives before later live output, and nothing from the snapshot is replayed. It fails on main, wheresubscribedhas norawData.test/session-socket.test.tsis new and covers the new module, so it cannot run on main. One test checks that it reconnects and resubscribes after an unexpected close, the other that it ignores output from other sessions and from sockets that were closed.Four mutations each make one of them fail: removing the reconnect, accepting
raw_datafrom any session, dropping theclosedguards, and replying with an empty snapshot.bun testovertest/*.test.ts, without the npm-pack and live suites, gives 187 passing.bun run typecheck, the build typecheck,bun run lint,bunx biome format .andvite buildare clean. The CI workflow passes on this commit in my fork, including the Playwright e2e in Chromium and Firefox.I also subscribed over the WebSocket to a session with about 350 kB of output on a local OpenCode host running this change together with my other PTY fixes. The snapshot arrived complete and live output kept coming after it. I did not reproduce the snapshot ordering problem on main in a browser, so the ordering is covered by the protocol-level test only. I did not drive a reconnect by hand in a browser.
Diff
Production: -99 lines (+129/-228) across
src/web/client/**,src/web/server/handlers/websocket.tsandsrc/web/shared/types.ts, including 73 lines for the newsession-socket.ts.Tests: +119 lines (+120/-1), one new file (
test/session-socket.test.ts) and one changed (test/websocket.test.ts).