Repository navigation
feat(mobile): add terminal toolbar paste button #8454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
|
|
||
| import { encodeTerminalPaste } from "./encodeTerminalPaste"; | ||
|
|
||
| describe("encodeTerminalPaste", () => { | ||
| it("returns empty for empty input", () => { | ||
| expect(encodeTerminalPaste("")).toBe(""); | ||
| }); | ||
|
|
||
| it("passes through plain text when not bracketed", () => { | ||
| expect(encodeTerminalPaste("hello")).toBe("hello"); | ||
| }); | ||
|
|
||
| it("replaces newlines with carriage returns when not bracketed", () => { | ||
| expect(encodeTerminalPaste("one\ntwo\r\nthree")).toBe("one\rtwo\rthree"); | ||
| }); | ||
|
|
||
| it("strips bracketed-paste end sequences to block injection", () => { | ||
| expect(encodeTerminalPaste("safe\u001b[201~; rm -rf /\n")).toBe("safe; rm -rf /\r"); | ||
| expect(encodeTerminalPaste("\u001b[201~\u001b[201~")).toBe(""); | ||
| }); | ||
|
|
||
| it("wraps only when bracketed is explicitly enabled", () => { | ||
| expect(encodeTerminalPaste("a\nb", { bracketed: true })).toBe("\u001b[200~a\nb\u001b[201~"); | ||
| }); | ||
|
|
||
| it("still strips embedded end sequences before wrapping", () => { | ||
| expect(encodeTerminalPaste("x\u001b[201~y", { bracketed: true })).toBe( | ||
| "\u001b[200~xy\u001b[201~", | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /** | ||
| * Encode clipboard text for PTY write, matching web Ghostty `encodePaste` | ||
| * semantics as closely as we can without a native mode query. | ||
| * | ||
| * Web reads DECSET 2004 (bracketed paste) from libghostty and only wraps when | ||
| * active. Mobile's native surface does not expose that mode yet, so callers | ||
| * default to `bracketed: false` — never emit literal ESC[200~/ESC[201~ into | ||
| * shells that have not enabled the mode. | ||
| * | ||
| * Always strips ESC[201~ from the payload to block paste-end injection. | ||
| */ | ||
|
|
||
| const BRACKETED_PASTE_START = "\u001b[200~"; | ||
| const BRACKETED_PASTE_END = "\u001b[201~"; | ||
|
|
||
| export function encodeTerminalPaste( | ||
| text: string, | ||
| options: { readonly bracketed?: boolean } = {}, | ||
| ): string { | ||
| // Split/join so every occurrence is removed, including adjacent repeats. | ||
| const sanitized = text.split(BRACKETED_PASTE_END).join(""); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High
🤖 Copy this AI Prompt to have your agent fix this: |
||
| if (sanitized.length === 0) { | ||
| return ""; | ||
| } | ||
|
|
||
| if (options.bracketed === true) { | ||
| return `${BRACKETED_PASTE_START}${sanitized}${BRACKETED_PASTE_END}`; | ||
| } | ||
|
|
||
| // Ghostty replaces newlines with CR when bracketed paste is off so each | ||
| // line is a carriage return rather than a bare LF (Ctrl+J in raw mode). | ||
| return sanitized.replace(/\r\n/g, "\r").replace(/\n/g, "\r"); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Paste skips Ghostty control sanitizationHigh Severity
Reviewed by Cursor Bugbot for commit 8a2f76476778ff8ccde01cf375d4a3176ef3f403. Configure here. |
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
terminal/ThreadTerminalRouteScreen.tsx:1031handlePasteFromClipboardcan write clipboard contents to the previously selected terminal after the user switches terminals or leaves the screen. The asynchronousClipboard.getStringAsync()continuation always calls the capturedwriteInputwithout checking that the route/session is still current, so a delayed paste can execute commands in an unintended shell. Invalidate the pending read on terminal-key/navigation changes and only callwriteInputwhile the request still belongs to the active terminal.🤖 Copy this AI Prompt to have your agent fix this: