Skip to content

fix(tui): restore Shift+Enter in Warp on WSL - #1109

Merged
code-yeongyu merged 5 commits into
code-yeongyu:mainfrom
deopa0402:fix/warp-wsl-newline
Aug 29, 2026
Merged

fix(tui): restore Shift+Enter in Warp on WSL#1109
code-yeongyu merged 5 commits into
code-yeongyu:mainfrom
deopa0402:fix/warp-wsl-newline

Conversation

@deopa0402

@deopa0402 deopa0402 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • restore Shift+Enter multiline input in direct local Warp-on-WSL sessions
  • normalize Warp's standalone LF input to the existing CSI-u Shift+Enter identity before editor dispatch
  • preserve plain Enter submission, bracketed paste, other terminals, and remote or multiplexed sessions

Root cause

Warp's terminal path sends Shift+Enter as bare LF (0x0a). In Senpi's legacy keyboard path, LF must also remain recognizable as Enter because some terminals emit LF for plain Enter.

The editor checks submit before newline, so Warp's Shift+Enter LF followed the Enter binding and submitted the composer instead of inserting a line break. WSL also reports process.platform === "linux", so Senpi's native Windows modifier helper cannot recover the Shift state from the host console.

Fix

ProcessTerminal now rewrites a standalone LF to the existing CSI-u Shift+Enter sequence (ESC [ 13 ; 2 u) only when both Warp and WSL are detected in a direct local session.

  • plain CR Enter is unchanged and continues to submit
  • bracketed paste bypasses the single-key normalization path
  • non-Warp and non-WSL terminals are unchanged
  • SSH and multiplexer sessions are excluded because the active client terminal may differ from inherited process environment markers
  • shared multiplexer detection covers tmux, GNU Screen, and Zellij
  • terminals already delivering explicit CSI-u Shift+Enter remain unchanged

Evidence

  • RED: the unfixed Warp-on-WSL LF path reached legacy Enter matching and submitted the composer
  • GREEN: the forwarding regression verifies Warp-on-WSL LF becomes explicit Shift+Enter while CR remains unchanged
  • boundary coverage verifies non-Warp, non-WSL, SSH, tmux, GNU Screen, and Zellij inputs are not rewritten
  • live Warp v0.2026.07.22.09.01.stable_01 on WSL verification confirmed Shift+Enter inserts a newline and Enter still submits normally

Validation

  • cd packages/tui && npm test
  • npm run check
  • npm run build
  • node scripts/check-pr-changelog.mjs --base upstream/main
  • live Senpi TUI verification in Warp on WSL

Scope

The change is limited to ProcessTerminal, shared multiplexer detection, focused terminal regression coverage, the TUI changelog, and the nearest fork-change tracker. No public API, migration, keybinding, or editor-dispatch changes.

Related

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d50a219b08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/tui/src/terminal.ts Outdated
@deopa0402
deopa0402 force-pushed the fix/warp-wsl-newline branch from d50a219 to b3d8e0d Compare August 25, 2026 00:12
@deopa0402

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3d8e0def1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/tui/CHANGELOG.md Outdated
Comment thread packages/tui/src/terminal.ts Outdated
@deopa0402
deopa0402 force-pushed the fix/warp-wsl-newline branch from b3d8e0d to 172bdf9 Compare August 25, 2026 00:22
@deopa0402

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 172bdf955d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Warp's direct WSL terminal path emits Shift+Enter as bare LF, which legacy matching also accepts as Enter. Normalize that event to explicit CSI-u Shift+Enter before editor dispatch.

Constraint: Preserve CR submit, bracketed paste payloads, other terminals, and SSH/tmux sessions
Rejected: Reorder submit and newline matching globally | breaks terminals that emit LF for plain Enter
Tested: package TUI suite; npm run check; live Warp on WSL user verification
@deopa0402
deopa0402 force-pushed the fix/warp-wsl-newline branch from 172bdf9 to 90892c5 Compare August 25, 2026 00:29
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@deopa0402

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

B1. The PR is not safe to merge as written. The rewrite is applied in the shared ProcessTerminal forwarding path, so it affects every focused TUI component, not only the multiline editor. In Warp+WSL, raw LF previously matched the legacy Enter behavior used by single-line Input and SelectList consumers (extension prompts/dialogs, search fields, selectors); after this rewrite those consumers receive ESC[13;2u (shift+enter), while they only handle enter/raw LF and then drop the CSI-u control sequence. This regresses confirmation/submission outside the target composer. Route the workaround to the multiline editor or preserve the original LF for components that do not support Shift+Enter, and add a component-level regression test.

B2. The environment heuristic is unsafe: !== undefined treats empty and spoofed markers as proof of WSL/Warp. A launcher or wrapper exporting TERM_PROGRAM=WarpTerminal plus WSL_INTEROP=/WSL_DISTRO_NAME= (or an empty WARP marker) causes every standalone LF to be rewritten. LF is a valid legacy Enter encoding, so this false positive can silently prevent submission. Require non-empty/validated markers plus a real WSL/platform check (or an explicit opt-in), and cover empty/spoofed values in the matrix.

B3. GitHub currently reports this PR as CONFLICTING with main (mergeStateStatus: DIRTY). The conflict zones documented in packages/tui/src/changes.md do not resolve the actual conflicts: a clean merge must be produced and re-reviewed before this can land.

B4. The required branch-protection contexts Check and test and Changelog gate are not present for the current head. gh pr checks shows only a neutral Cubic review and a passing GitGuardian check. The PR body's local commands and live verification do not satisfy the required GitHub gates; after resolving the merge conflict, push a current head and wait for both required contexts to pass.

The targeted behavior may be correct for the one editor scenario, but the implementation's shared scope and unvalidated environment assumptions make the advertised "other terminals"/"other TUI consumers" safety claims untrue.

Comment thread packages/tui/src/terminal.ts Outdated
Comment thread packages/tui/src/terminal.ts Outdated
@code-yeongyu

Copy link
Copy Markdown
Owner

Addressed review 5058005408 in 0e6fd44: Warp/WSL LF normalization now occurs only inside the multiline Editor, so ProcessTerminal forwards raw LF to single-line Input/selectors; detection requires Linux plus non-empty validated WSL markers and excludes SSH/multiplexer sessions; added regression coverage for raw forwarding, empty/non-Linux/spoofed markers, and editor behavior. Merged origin/main and resolved conflict markers while retaining both change histories. TUI build and full packages/tui npm test pass (all tests green). Pushed to fix/warp-wsl-newline; GitHub head/mergeability API may take a moment to refresh.

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-2 adversarial review of commit 0e6fd4463e10989d4dccde7476caa8f7207fbe2e.

Round-1 blocker verdicts

  • B1 - behavior scope: VERIFIED, coverage incomplete. ProcessTerminal.forwardInputSequence() now forwards raw LF, and normalization is at the base Editor.handleInput() boundary. That covers the built-in composer (CustomEditor), the extension multi-line editor, and other built-in Editor instances; single-line Input/selector consumers no longer receive the rewritten CSI-u sequence. The missing real-editor regression assertion is recorded as B7 below.
  • B2 - detection hardening: NOT FULLY RESOLVED. Linux and whitespace/SSH/multiplexer gates are present, but the exact predicate still accepts an absent/empty Warp session ID via TERM_PROGRAM === "WarpTerminal", and treats any string beginning /run/WSL/ as valid without validating the WSL <pid>_interop shape or existence. See B5.
  • B3 - main conflict: VERIFIED RESOLVED. The worktree is clean, git merge-base HEAD origin/main is f97db4007, the conflict marker removed from packages/coding-agent/src/modes/interactive/changes.md, and GitHub reports MERGEABLE; the current parent includes the main-side changes.
  • B4 - required checks: NOT RESOLVED. Branch protection still requires Check and test and Changelog gate, while gh pr checks 1109 reports only GitGuardian Security Checks and cubic. Local test/build claims do not satisfy these required GitHub contexts. See B6.

Remaining blockers

B5. The WSL/Warp identity predicate still has a spoofable false-positive path. With platform = "linux", { TERM_PROGRAM: "WarpTerminal", WSL_INTEROP: "/run/WSL/not-a-real-socket" } returns ESC[13;2u despite having no non-empty Warp session ID and no valid interop endpoint. { TERM_PROGRAM: "WarpTerminal", WSL_DISTRO_NAME: "spoofed", WSL_INTEROP: "spoofed" } also passes because both markers are only checked for non-emptiness. This can rewrite legacy LF submission on a Linux process with inherited or manually spoofed variables. Require the non-empty Warp ID and validate the interop marker/path shape (and cover both-marker spoofing); do not rely on startsWith("/run/WSL/") as validation.**

B6. The required branch-protection contexts are still absent for the reviewed head. gh pr checks 1109 --repo code-yeongyu/senpi shows no Check and test and no Changelog gate, although gh api .../branches/main/protection confirms both are required. Please push/wait for the current head's required contexts before merge.**

B7. The new tests do not exercise the actual Editor success path. The PR diff adds pure normalizer tests and a ProcessTerminal test with an arbitrary input callback, but no editor.test.ts regression. The existing editor tests cover an already-explicit CSI-u sequence and run independently of the new environment predicate; they would remain green if the new Editor.handleInput() normalization line were removed. Add a platform/env-injected test that drives the real Editor and asserts Warp/WSL LF inserts a newline without submit, while CR still submits.**

Validation performed

  • packages/tui/test/terminal.test.ts: 38 passed.
  • packages/tui/test/editor.test.ts: 200 passed.
  • LSP diagnostics for all changed TUI source/test files: clean.
  • Direct predicate probe reproduced the B5 cases above.

Because B2/B4 remain unresolved and the B1 regression coverage is incomplete, this PR is not approvable yet.

Comment thread packages/tui/src/terminal.ts Outdated
Comment thread packages/tui/src/components/editor.ts Outdated
@code-yeongyu

Copy link
Copy Markdown
Owner

Round-2 blocker fixes pushed in 0d3329c9e24fc0f407675672b81954dbdd7b6eae.

  • B5: Hardened Warp/WSL detection: requires a non-empty WARP_SESSION_ID or WARP_TERMINAL_SESSION_UUID (no TERM_PROGRAM-only activation), validates WSL_INTEROP as /run/WSL/<numeric>_interop, and requires the interop socket to exist. Existing SSH and multiplexer exclusions remain. Predicate tests cover empty/spoofed Warp IDs, malformed interop paths, and missing sockets.
  • B7: Added real Editor.handleInput() boundary tests with injected Linux/env/socket seams: Warp-on-WSL LF inserts a newline without submit; CR still submits; non-Warp/non-WSL LF still submits as before.
  • B6: The changelog gate passes locally against the PR base (node scripts/check-pr-changelog.mjs --base f97db40079d6a2cb3e1e383586c551e90b6ee049). New-head workflow runs are present, but GitHub fork-PR approval is required before execution: CI run, Changelog gate run. Both are currently action_required, not path-filtered.

Local verification: focused failing-first tests caught the removed TERM_PROGRAM-only positive assumption; focused B5/B7 tests then passed. Full npm test --workspace packages/tui passed, npm run build --workspace packages/tui passed, all changed files have no LSP diagnostics, and git diff --check passed.

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-3 adversarial review of commit 0d3329c9e24fc0f407675672b81954dbdd7b6eae.

Round-2 blocker resolution

  • B5 - VERIFIED. normalizeWarpWslShiftEnterInput() now requires Linux, a non-empty trimmed WARP_SESSION_ID or WARP_TERMINAL_SESSION_UUID, an exact /run/WSL/<numeric>_interop path, and a positive existence result; TERM_PROGRAM alone is ignored, and SSH/multiplexer exclusions remain. Direct probes returned raw LF for empty IDs, whitespace-only IDs, TERM_PROGRAM-only, non-numeric interop, wrong prefixes, and a nonexistent socket.
  • B6 - VERIFIED. The required Check and test and Changelog gate contexts are present for head 0d3329c9e24fc0f407675672b81954dbdd7b6eae, and gh pr checks 1109 reports both passing. All other CI checks also pass.
  • B7 - VERIFIED. The new tests drive Editor.handleInput() with injected Linux/environment/socket seams. I copied the test tree, removed only the normalization call, and the Warp-on-WSL LF test failed ('' !== 'hello\n'); with the call present, the focused Editor/terminal run passed 241/241. CR submission and non-Warp LF submission are covered and pass.

Fresh blocker

B8. existsSync does not verify that WSL_INTEROP is a socket.

At packages/tui/src/terminal.ts:89, the production default is fs.existsSync. That API follows symlinks and returns true for regular files and directories as well as Unix sockets. Therefore a syntactically valid /run/WSL/123_interop path that is a symlink to (or is replaced by) a regular file or directory still activates the LF rewrite. This is a remaining false-positive path for the exact predicate this PR is meant to harden. The check should validate the target type (for example fs.statSync(path).isSocket(), which still permits WSL's legitimate symlink aliases) and add a regression for a non-socket target.

Other requested edge cases

  • WSL1: the predicate safely leaves LF unchanged when /run/WSL/WSL_INTEROP is absent. I inspected the WSL source/docs path: the generic interop server is a Unix socket under /run/WSL, and the WSL1 path uses LxInitMessageCreateProcess; absence of that active marker provides no reliable target to normalize against. This is fail-safe rather than a new false-positive.
  • Hot path and environment changes: non-LF input returns before any filesystem call; the stat is performed only for a standalone LF after the Warp/path gates. There is no cached predicate, and production callers leave the optional seams undefined, so default calls read live process.env/process.platform; mutating an injected environment between calls changed the result in my probe.
  • The Editor seam is behavior-neutral by default: it is optional, is not supplied by production call sites, and the mutation test proves it only controls the testable normalizer inputs.

The focused local Editor/terminal tests, TUI build, diff check, and all current CI checks are green, but B8 remains before approval.

@code-yeongyu

Copy link
Copy Markdown
Owner

Round-3 B8 fixed in commit e9b9e4f47 (fix(tui): validate Warp WSL interop socket type).

  • Replaced the production fs.existsSync() default with fs.statSync(path).isSocket(), preserving the injected filesystem seam and fail-safe behavior for missing/inaccessible paths.
  • Added real filesystem coverage for regular files, directories, symlinks to non-sockets, and symlinks to real Unix sockets.
  • Failing-first evidence: the added real-socket-shape tests failed before the production change; the focused suite then passed 41/41, and terminal/input/editor passed 278/278.

Gates: TUI build passed; package-wide LSP error scan passed with 0 errors; git diff --check passed; changelog gate passed against origin/main (changes.md coverage complete, 2 production paths covered).

Pushed to deopa0402/fix/warp-wsl-newline without force.

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-4 adversarial review of commit e9b9e4f47b0528eff1e6e7cec7da3cf9be2ef520.

B8 - VERIFIED

The production default now wraps fs.statSync(socketPath).isSocket() in a try/catch, so it follows symlinks for legitimate aliases, accepts only socket targets, and fails safe when stat throws. I probed the production default by redirecting only the canonical WSL path to real temporary filesystem entries: regular file, directory, symlink to regular file, and dangling symlink all preserved raw LF; a real Unix socket and symlink to a real Unix socket both produced ESC[13;2u; an EACCES stat also preserved raw LF. The injected seam receives the canonical path and Editor forwards its optional seam; production callers leave all seams undefined.

The Linux gate makes the Windows named-pipe case unreachable, and there is no meaningful TOCTOU use after the type check for this feature's threat model. Non-LF input returns before any filesystem call. The focused terminal/editor tests passed, the full @earendil-works/pi-tui test command passed, the TUI build passed, source/test diagnostics had no errors, and git diff --check passed.

Fresh blocker

B9. The synchronous socket stat is not gated behind the Warp check.

In packages/tui/src/terminal.ts, isWsl is computed as interopPath !== undefined && validPathRegex.test(interopPath) && socketExists(interopPath), while isWarp is checked only in the final return expression. Consequently, on Linux in an ordinary non-Warp WSL session with a valid WSL_INTEROP path, every standalone LF still invokes the synchronous production fs.statSync() even though the function always returns the original LF. My seam probe with { WSL_INTEROP: "/run/WSL/321_interop" } and no Warp marker recorded one socket-check call; malformed/missing WSL paths and non-LF input did not call it.

Please short-circuit before the filesystem check when no non-empty Warp marker is present (or include isWarp && before socketExists). This keeps the synchronous stat limited to the intended Warp-on-WSL path rather than adding it to every LF handled by Editors in normal WSL sessions.

B8 is otherwise resolved; B9 is the only new blocker found.

@code-yeongyu

Copy link
Copy Markdown
Owner

Round-4 B9 fixed in commit f06a0533dd579f836b33ae38c1a84c48cff9ae46 (fix(tui): skip WSL socket check outside Warp).

  • Added a seam probe matching the review case: Linux + WSL_INTEROP=/run/WSL/321_interop + no Warp marker must return raw LF without calling the socket check.
  • Failing-first evidence: the new probe failed with AssertionError: 1 == 0 (one socket check).
  • Minimal fix: gate socketExists(interopPath) behind isWarp, keeping synchronous statSync limited to Warp-on-WSL candidates.

Verification:

  • Focused normalization/editor probe: PASS
  • Full npm --prefix packages/tui test: PASS
  • npm --prefix packages/tui run build: PASS
  • LSP diagnostics (changed source/test files, errors): no diagnostics
  • git diff --check: PASS
  • node scripts/check-pr-changelog.mjs --base origin/main: PASS

Pushed to deopa0402/fix/warp-wsl-newline; PR head verified as f06a0533dd579f836b33ae38c1a84c48cff9ae46.

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-5 adversarial review of commit f06a0533dd579f836b33ae38c1a84c48cff9ae46.

B9 - VERIFIED RESOLVED

The new isWarp && operand in the isWsl conjunction is a real short-circuit gate before socketExists(interopPath). I verified the required cases with direct probes against normalizeWarpWslShiftEnterInput(): (a) Linux with valid /run/WSL/321_interop and no Warp marker preserved raw LF and made zero socket-check calls; (b) Linux with a valid Warp marker and valid interop path called the seam exactly once and returned ESC[13;2u (both Warp marker variants reached the check); (c) a Warp marker with missing, blank, or malformed interop preserved raw LF and made zero socket-check calls. A Proxy-based probe also confirmed the Warp environment fields are evaluated before the socket seam, so there is no eager socket evaluation.

The Warp-on-WSL happy path remains active: the valid marker/path reaches the socket check and rewrites when it reports a socket. The predicate is not memoized; changing the injected environment between calls changed the result and only the newly qualifying call invoked the socket check. The isWarp probe itself is limited to trimmed environment reads and does not introduce another filesystem operation.

The focused TUI terminal/editor test run passed, the independent B9 probe passed, diagnostics for all changed TUI source/test files reported no errors, git diff --check passed, and the worktree remained clean. I found no new defensible blocker.

VERDICT: APPROVE

@code-yeongyu
code-yeongyu merged commit aaa2a2f into code-yeongyu:main Aug 29, 2026
2 checks passed
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.

2 participants