Shift+Enter adds a line, and a session says what it is before it starts - #61
Merged
Conversation
xterm.js encodes Enter as a bare carriage return whatever Shift is doing — faithful to the hardware, and useless in front of an agent CLI. Claude Code and Codex both read a bare CR as "send this", so in a browser terminal there was no way to put a second line in a prompt. Send ESC CR for Shift+Enter. That is not a sequence flue invented: a line editor reads it as Alt+Enter, `claude /terminal-setup` writes exactly those two bytes into VS Code's keybindings.json for `shift+enter`, and it is what iTerm2, Ghostty, WezTerm, Kitty, Warp and Windows Terminal already send. It goes in the emulator rather than the view because the view cannot put bytes on the wire without reaching past the seam. `term.input` routes them through onData, so the key bar's latched Ctrl and the replay mute gate still apply. Ctrl+Shift+Enter is left alone — the view takes that one on the way down for focus mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…its own tab Three things that were one thing. A session row now opens in a tab of its own. This list is what people come back to between sessions, and opening a terminal over it made the way back the browser's back button — which drops the terminal and its scrollback with it. The `+` inside a terminal pointed at `/?cwd=`, which is the sessions dashboard, so a session started from a terminal came up behind the whole list — page, shell, rows and all — for as long as the daemon took to answer. There is a page for it now: /new spawns, applies the metadata, and replaces itself with the terminal, on the terminal's own ground. And every way of starting a session goes through one dialog first: name, directory, machine, tags, prefilled from whatever the press implied. `spawn` carries no metadata, so a name and a tag could only ever be applied after the session existed — which meant going back to the list to do it, which nobody does. Asked here, they ride the address to the page that starts the session and land on the first frame that has an id. The page is a page and not a click handler because starting a session takes a round trip: a screen that spawned and then called window.open from the reply would be opening a popup from a continuation, which Safari refuses outright and Chrome refuses once the gesture has aged out. A link opens on the click itself. A blocked popup falls back to this tab. `flue open`'s `?cwd=` handover is untouched — it still spawns from the sessions screen, which is the one caller `adopt` has left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… bet on the runner Both cap tests in test/directory.test.ts timed out at vitest's five seconds on CI while passing on a laptop, and the difference was never anything but machine speed: the only way to test a cap is to reach it, and reaching these means 512 sequential PUTs through a Durable Object, or 256 WebSockets opened one at a time and all held open. So the caps join the deadlines that are already test seams, read the same way `handshakeTimeout` and `pairTimeout` read theirs: the binding when there is one, the constant when there is not. Production binds neither and gets 512 and 256, and the arithmetic that picks those numbers stays where it was written down. vitest binds 64 and 8. The entry bound has headroom on purpose — the shared directory that nearly every other test in the file writes into holds ten entries by the end of the run, and a bound it could reach would start refusing PUTs in tests that are not about the cap at all, which reads as "the push never came" nowhere near the number that caused it. The file drops from around 29 seconds to under 7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Three fixes, all in
web/.Shift+Enter sends a newline instead of the message
xterm.js encodes Enter as a bare carriage return whatever Shift is doing (
case 13of its key encoder) — faithful to the hardware, and useless in front of an agent CLI. Claude Code and Codex both read a bare CR as "send this", so in a browser terminal there was no way to put a second line in a prompt.Shift+Enter now sends
ESC CR. That is not a sequence flue invented: a line editor reads it as Alt+Enter,claude /terminal-setupwrites exactly those two bytes into VS Code'skeybindings.jsonforshift+enter, and iTerm2, Ghostty, WezTerm, Kitty, Warp and Windows Terminal already send it — which is what "Shift+Enter is natively supported" means in those tools.It lives in the emulator rather than the view because the view cannot put bytes on the wire without reaching past the seam.
term.inputroutes them throughonData, so the key bar's latched Ctrl and the replay mute gate still apply.Ctrl+Shift+Enteris left alone — the view takes that one on the way down for focus mode.Verified against a real pty, not only in jsdom:
cat -vin a live session records^[and then the CR.A session row opens in a tab of its own
The sessions list is what people come back to between sessions, and opening a terminal over it made the way back the browser's back button — which drops the terminal and its scrollback with it. The rows were already real anchors, so this is a
target; the router hands the click straight to the browser once one is set.The
+in a terminal no longer goes via the dashboardIt pointed at
/?cwd=, which is the sessions dashboard — so a session started from a terminal came up behind the whole list, page and shell and rows, for as long as the daemon took to answer.There is a page for it now.
/newtakes the request in its address, spawns, applies the name and tags the moment there is an id to apply them to, and replaces itself with the terminal — one pill on the terminal's own ground, no chrome in between.A page rather than a click handler because starting a session takes a round trip:
spawncarries no id, the daemon invents one. A screen that spawned and then calledwindow.openfrom the reply would be opening a popup from a continuation, which Safari refuses outright and Chrome refuses once the gesture has aged out. A link opens on the click itself. A blocked popup falls back to the current tab rather than to nothing at all.And every way of asking goes through one dialog
spawncarries no metadata, so a name and a tag could only ever be applied after the session existed — which meant starting it, watching a terminal come up, going back to the list and renaming the row. Nobody does that, so sessions stayed called after whatever shell they ran.The toolbar button, its machine menu, every group heading's
+, and the terminal's own+now open one form: name, directory, machine, tags — prefilled from whatever the press implied, and all of it optional. Opening it and pressing Start is the old one-click behaviour exactly.flue open's?cwd=handover is untouched: it still spawns from the sessions screen, which is the one calleradopthas left.Checks
pnpm vitest run— 67 files, 1319 tests, greenpnpm run lint— clean🤖 Generated with Claude Code