Files
kandev/apps/backend/internal/gateway/websocket/terminal_messages.go
T
João Salavisa c534aac355 refactor(backend): split terminal_handler.go into pumps/sessions/messages (#1056)
* refactor(backend): split terminal_handler.go into pumps/sessions/messages

terminal_handler.go was 1,380 lines (1.7x the 800-line cap). Split it into
four sibling files in the same package, by responsibility:

- terminal_handler.go (525 lines): HTTP upgrade, route dispatch, top-level
  handler entries, shell-label resolution.
- terminal_pumps.go (725 lines): wsWriter, bridgeBinaryWebSockets,
  runTerminalBridge / runUserShellBridge and all input/resize/PTY-setup
  helpers. Extracts replayBufferedOutput from setupPtyAccessCommon to keep
  nestif under threshold.
- terminal_sessions.go (133 lines): execution-readiness helpers
  (ensurePassthroughExecutionReady, waitForRemoteExecutionReadyWithTimeout,
  remoteExecutionWithClientReady, shouldUseWorkspaceShell) and related
  timeout constants.
- terminal_messages.go (53 lines): protocol primitives (ResizePayload,
  isResizeCommand, resizeCommandByte, stripTerminalResponses), with a
  top-of-file doc comment describing the binary wire protocol.

Backed off the "typed TerminalMessage envelope" half of the task: the
current protocol is binary, not JSON-enveloped (terminal I/O is raw PTY
bytes; the only JSON is ResizePayload, sent as a binary frame prefixed by
0x01). Wrapping PTY bytes in a JSON envelope would require base64 and
break xterm.js AttachAddon for every client — the task brief explicitly
authorizes shipping just the file split when the format would change.

Adds terminal_messages_test.go pinning the wire format: exact JSON output
of ResizePayload, the 0x01-prefix detection, and a full resize-frame
round-trip. These fail loudly on any accidental change to the struct tags
or prefix byte.

Pure relocation otherwise: no signature changes, no behavior changes, no
frontend impact.

* refactor(backend): simplify detectInputSubmission with bytes.ContainsAny

Addresses Claude bot's PR review on #1056: replace the manual loop +
flag pattern with bytes.ContainsAny(data, "\n\r"). Behavior identical
(detect CR or LF anywhere in the input). Also drops the now-redundant
"Check for Enter key (CR or LF)" comment.

* ci: retrigger CI to re-test known-flaky session-resume spec

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-29 00:31:27 +01:00

54 lines
2.2 KiB
Go

package websocket
import "regexp"
// Terminal WebSocket wire protocol
// ---------------------------------
// Frames are binary WebSocket messages. The first byte distinguishes the
// frame kind:
//
// - 0x01 + '{' ... '}' → JSON resize command (ResizePayload)
// - anything else → raw PTY input/output bytes
//
// Output to the client is always raw PTY bytes — never wrapped. Wrapping
// PTY data in a JSON envelope would require base64 and break the xterm.js
// AttachAddon, so the protocol stays binary on purpose.
// resizeCommandByte is the binary protocol marker for resize messages.
// First byte 0x01 indicates resize, followed by JSON {cols, rows}.
const resizeCommandByte = 0x01
// isResizeCommand checks whether a binary frame is a resize command.
// Resize messages use 0x01 as a prefix followed by a JSON object (starts with '{').
// A bare 0x01 or 0x01 followed by non-JSON data is regular PTY input (e.g. Ctrl+A).
func isResizeCommand(data []byte) bool {
return len(data) >= 2 && data[0] == resizeCommandByte && data[1] == '{'
}
// ResizePayload is the JSON payload for resize commands.
type ResizePayload struct {
Cols uint16 `json:"cols"`
Rows uint16 `json:"rows"`
}
// terminalResponsePattern matches terminal query responses that should not be replayed.
// These are responses from the terminal emulator to queries sent by the shell, which
// appear as PTY output and get captured in the ring buffer. On replay they render as
// visible garbage text because they are response sequences, not display sequences.
//
// Matched sequences:
// - OSC 11 response: \x1b]11;rgb:XXXX/XXXX/XXXX ST (background color, ST = ESC\ or BEL)
// - DA1 response: \x1b[?<params>c (device attributes)
// - CPR response: \x1b[<row>;<col>R or \x1b[<row>R (cursor position report)
var terminalResponsePattern = regexp.MustCompile(
`\x1b]11;rgb:[0-9a-fA-F/]+(?:\x1b\\|\x07)` + // OSC 11 (ESC\ or BEL terminator)
`|\x1b\[\?[0-9;]*c` + // DA1 response
`|\x1b\[\d+(?:;\d+)?R`, // CPR response
)
// stripTerminalResponses removes terminal query response sequences from data.
// Used to clean buffered PTY output before replaying it on reconnect.
func stripTerminalResponses(data []byte) []byte {
return terminalResponsePattern.ReplaceAll(data, nil)
}