fix: enable multiline paste in web terminal

Multiline pastes into the web terminal (especially into pi) were split
into one prompt per line because bracketed-paste markers were not
reaching the foreground app intact.

- Put the host PTY into raw mode (tty.setraw) after openpty() so it acts
  as a pass-through pipe. The default canonical line discipline was
  line-buffering input, splitting multiline pastes at newlines, and
  mangling bracketed-paste markers before docker exec / pi could see
  them. The in-container PTY (docker exec -t) provides real discipline.
- Route the mobile Paste button through xterm.js (term.paste) instead of
  sending raw clipboard text to the WebSocket, so content is wrapped in
  bracketed-paste markers when the app has enabled BPM.
- Treat a text frame as a control message only when it is a JSON object
  with a known type (resize/ack/reset); otherwise forward as raw input
  so JSON-shaped pastes are no longer silently dropped.

Quality gates: ruff, mypy (changed files), pytest unit (227 passed),
tsc, eslint
This commit is contained in:
Developer
2026-07-11 11:27:24 +00:00
parent 3c96c7b153
commit a1a77c99a6
4 changed files with 96 additions and 71 deletions
+14 -12
View File
@@ -23,12 +23,13 @@ async def test_start_passes_container_user_to_docker_exec() -> None:
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
):
with patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
with patch("src.services.terminal.terminal_session.tty.setraw"):
with patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
args, _kwargs = mock_exec.call_args
assert "docker" in args
@@ -53,12 +54,13 @@ async def test_start_omits_user_when_not_configured() -> None:
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
):
with patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
with patch("src.services.terminal.terminal_session.tty.setraw"):
with patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
args, _kwargs = mock_exec.call_args
assert "--user" not in args