Skip to content

fix: UTF-8 boundary safety, scroll-to-bottom on reattach, tmux/dtach session persistence#9

Open
shockstricken wants to merge 1 commit into
cloudcli-ai:mainfrom
shockstruck:fix/stability-and-display
Open

fix: UTF-8 boundary safety, scroll-to-bottom on reattach, tmux/dtach session persistence#9
shockstricken wants to merge 1 commit into
cloudcli-ai:mainfrom
shockstruck:fix/stability-and-display

Conversation

@shockstricken
Copy link
Copy Markdown

Summary

Three independent fixes for stability and display issues observed when running the web-terminal plugin behind a CloudCLI host. All are opt-in or backward-compatible.

1. UTF-8 byte-boundary safety in the pty pipeline (src/server.ts)

node-pty was emitting onData chunks as strings. When the underlying read crossed the middle of a multi-byte codepoint (common with emoji, box-drawing borders, CJK), the chunk was split mid-byte and forwarded as malformed UTF-8. xterm.js then rendered the well-known smeared-border / wrong-width-character glitch.

This switches node-pty to encoding: null (raw Buffer chunks) and routes them through a per-session TextDecoder with {stream: true}. Trailing incomplete bytes are buffered until the next chunk, so what we send over the WebSocket always contains only complete codepoints.

2. Scroll-to-bottom on tab show, attach, and reconnect (src/index.ts)

Reopening a tab — or reconnecting after a disconnect — left the viewport wherever the user had last scrolled, so the prompt was often off-screen and the tab appeared frozen.

show(), attachTo(), and the 'ready' (reconnected) message handler now each call terminal.scrollToBottom() after the fit, so the user always lands at the current prompt without losing scrollback history.

3. Optional tmux / dtach session persistence (src/server.ts)

New env var WEB_TERMINAL_SESSION_BACKEND:

Value Behavior
`none` (default) One pty per WebSocket, original behavior.
`tmux` Wraps shell in `tmux -L web -u new-session -A -s `. Browser refresh reattaches instead of SIGHUPing the running program.
`dtach` Wraps shell in `dtach -A -z`. Lighter than tmux; same survival semantics.

Companion vars `WEB_TERMINAL_SESSION_NAME` and `WEB_TERMINAL_DTACH_SOCKET` tune the session/socket identifier.

This lets hosts that ship `tmux` or `dtach` give users long-running `claude` / `codex` / `gemini` sessions that survive browser refresh and intermittent network drops without any client-side changes.

Test plan

  • `npm install && npm run build` succeeds (verified locally; `tsc --noEmit` and full emit both clean)
  • In a host without the env var set, behavior matches main (one pty per WS, SIGHUP on close)
  • With `WEB_TERMINAL_SESSION_BACKEND=tmux`, opening two browser tabs to the same host attaches to the same shell; refreshing one keeps the other intact
  • Renders emoji / box-drawing borders / CJK characters cleanly when the pty stream spans chunk boundaries
  • Closing and reopening a tab lands at the bottom of the buffer

Notes

  • `package.json` and `package-lock.json` intentionally unchanged.
  • No new runtime dependencies. `tmux` and `dtach` are runtime-resolved on the host PATH only when the env var is set.
  • The local `PtyProcess.onData` interface was widened to `string | Buffer` to match `node-pty`'s actual runtime behavior in raw mode.

…istence

Three independent fixes for stability and display issues users see when
working in the web terminal:

1. UTF-8 byte-boundary safety in the pty pipeline (server.ts)
   node-pty was emitting chunks as strings, which meant a multi-byte
   codepoint landing on a chunk boundary got split mid-byte and forwarded
   to the WebSocket as malformed UTF-8. xterm.js then rendered the smeared
   border / wrong-width character glitch users reported around emoji,
   box-drawing chars, and CJK text. Switching node-pty to encoding:null
   gives us raw Buffer chunks and routing them through a per-session
   TextDecoder with {stream:true} buffers any trailing incomplete bytes
   until the next chunk arrives. The string sent over the WebSocket now
   always contains only complete codepoints.

2. Scroll-to-bottom on tab focus and reconnect (index.ts)
   Reopening a tab — or reconnecting after a disconnect — left the
   viewport wherever the user had last scrolled, which often meant the
   chat / shell pane appeared frozen with the prompt off-screen. show(),
   attachTo(), and the 'ready' (reconnected) handler now each call
   terminal.scrollToBottom() after the fit, so the user always lands at
   the current prompt without losing scrollback history.

3. Optional tmux / dtach session persistence (server.ts)
   New WEB_TERMINAL_SESSION_BACKEND env var. When set to 'tmux' the
   pty wraps the shell in tmux new-session -A -s <name>; when set to
   'dtach' it wraps in dtach -A. Either way, browser refresh closes
   the WebSocket but the shell — and any foreground program like
   claude / codex / gemini — survives, and the next connect reattaches.
   Default 'none' preserves the original behavior.

   Companion env vars WEB_TERMINAL_SESSION_NAME and
   WEB_TERMINAL_DTACH_SOCKET tune the session identifier and socket path.

README documents the new env vars under a "Session persistence" section.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant