Skip to content

Commit 293d661

Browse files
Fix shellv2 multi-line code paste parsing (#2092)
* fix: properly handle multiline block pasting in shellv2 When users pasted multiline string blocks (e.g. from Python code), the BrowserRepl was evaluating each line individually with `\n` mapped to Enter. Consequently, double newlines representing empty lines within blocks prematurely triggered the REPL to end execution contexts, preventing entire copy-paste sequences from loading correctly. This commit updates the `BrowserWasmAdapter` to process pasted blocks via a single method (`paste_input`), sequentially submitting and managing completion buffers. In the frontend hook, paste multi-character sequences containing linebreaks are now captured and passed as a solid chunk, while simple append logic remains unmodified. Co-authored-by: KCarretto <16250309+KCarretto@users.noreply.github.com> * fix: correctly handle multiline python code paste blocks in shellv2 Pasting multi-line Python snippets containing empty lines caused the frontend `BrowserRepl` loop to misinterpret the empty lines as end-of-block signals when processed line-by-line. This caused premature submission and subsequently threw errors or split logic into disconnected blocks. This fix modifies `BrowserWasmAdapter`'s `paste_input()` handler to send the entire clipboard payload buffer straight to `BrowserRepl.input(textToInput)` in one piece, correctly preserving the block logic without prematurely submitting execution on intermediate blank lines. Co-authored-by: KCarretto <16250309+KCarretto@users.noreply.github.com> * file cleanup and rebuild --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: KCarretto <16250309+KCarretto@users.noreply.github.com>
1 parent 79f1d98 commit 293d661

File tree

9 files changed

+102
-12
lines changed

9 files changed

+102
-12
lines changed

tavern/internal/www/build/asset-manifest.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tavern/internal/www/build/index.html

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tavern/internal/www/build/static/js/main.da331003.js renamed to tavern/internal/www/build/static/js/main.27ceb4e0.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tavern/internal/www/build/static/js/main.da331003.js.LICENSE.txt renamed to tavern/internal/www/build/static/js/main.27ceb4e0.js.LICENSE.txt

File renamed without changes.

tavern/internal/www/build/static/js/main.da331003.js.map renamed to tavern/internal/www/build/static/js/main.27ceb4e0.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
-1.02 KB
Binary file not shown.
-1.02 KB
Binary file not shown.

tavern/internal/www/src/lib/browser-adapter.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,47 @@ export class BrowserWasmAdapter {
141141
}
142142
}
143143

144+
paste_input(text: string): ExecutionResult {
145+
if (!this.repl) {
146+
return { status: "error", message: "REPL not initialized" };
147+
}
148+
149+
// Just pass the entire pasted text into the repl at once.
150+
// If the block is complete, it will return "complete" with the whole block.
151+
// We append an extra newline to ensure it evaluates blocks that end without one.
152+
let textToInput = text;
153+
if (!textToInput.endsWith('\n')) {
154+
textToInput += '\n';
155+
}
156+
157+
// BrowserRepl handles parsing the entire string into its internal buffer,
158+
// so we can just call it once with the full string block.
159+
const resultJson = this.repl.input(textToInput);
160+
161+
try {
162+
const result = JSON.parse(resultJson);
163+
164+
if (result.status === "complete") {
165+
if (this.isWsOpen && this.ws) {
166+
this.ws.send(JSON.stringify({
167+
kind: WebsocketMessageKind.Input,
168+
input: result.payload
169+
}));
170+
} else {
171+
return { status: "error", message: "WebSocket not connected" };
172+
}
173+
return { status: "complete" };
174+
} else if (result.status === "incomplete") {
175+
return { status: "incomplete", prompt: result.prompt };
176+
} else {
177+
return { status: "error", message: result.message };
178+
}
179+
} catch (e) {
180+
console.error("Failed to parse REPL result", e);
181+
return { status: "error", message: "Internal REPL error" };
182+
}
183+
}
184+
144185
complete(line: string, cursor: number): { suggestions: string[], start: number } {
145186
if (!this.repl) return { suggestions: [], start: cursor };
146187
const resultJson = this.repl.complete(line, cursor);

tavern/internal/www/src/pages/shellv2/hooks/useShellTerminal.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -892,10 +892,59 @@ export const useShellTerminal = (
892892
termInstance.current.onData((data) => {
893893
// Check if this is a paste or multi-character sequence (not starting with ESC)
894894
if (data.length > 1 && data.charCodeAt(0) !== 27) {
895-
// Normalize newlines to \r so they trigger the "Enter" key code (13)
896-
const normalized = data.replace(/\r\n/g, "\r").replace(/\n/g, "\r");
897-
for (const char of normalized) {
898-
handleData(char, true);
895+
// Check for connection status and block input
896+
if (isLateCheckinRef.current || connectionStatusRef.current !== "connected") return;
897+
898+
const hasNewlines = data.includes('\r') || data.includes('\n');
899+
900+
if (hasNewlines) {
901+
// Normalize to \n for our adapter
902+
const normalized = data.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
903+
904+
const state = shellState.current;
905+
const term = termInstance.current;
906+
if (!term) return;
907+
908+
// Write the pasted text to the terminal, formatting newlines appropriately
909+
const displayFormatted = normalized.replace(/\n/g, "\r\n");
910+
term.write(displayFormatted);
911+
term.write("\r\n"); // Ensure we're on a new line after paste
912+
913+
// We need to prepend any existing input buffer to the paste
914+
const fullText = state.inputBuffer + normalized;
915+
state.currentBlock += fullText + "\n";
916+
917+
// Call our new paste method
918+
const res = adapter.current?.paste_input(fullText);
919+
920+
if (res?.status === "complete") {
921+
if (state.currentBlock.trim()) {
922+
state.history.push(state.currentBlock.trimEnd());
923+
saveHistory(state.history);
924+
}
925+
state.currentBlock = "";
926+
state.historyIndex = -1;
927+
state.inputBuffer = "";
928+
state.cursorPos = 0;
929+
state.prompt = ">>> ";
930+
term.write(state.prompt);
931+
} else if (res?.status === "incomplete") {
932+
state.prompt = res.prompt || ".. ";
933+
term.write(state.prompt);
934+
state.inputBuffer = "";
935+
state.cursorPos = 0;
936+
} else {
937+
term.write(`Error: ${res?.message}\r\n>>> `);
938+
state.currentBlock = "";
939+
state.inputBuffer = "";
940+
state.cursorPos = 0;
941+
state.prompt = ">>> ";
942+
}
943+
lastBufferHeight.current = 0;
944+
} else {
945+
for (const char of data) {
946+
handleData(char, true);
947+
}
899948
}
900949
} else {
901950
handleData(data, false);

0 commit comments

Comments
 (0)