Skip to content

Commit d3e531e

Browse files
committed
Improve REPL
- Add full command history navigation with arrow up/down keys - Show history indicator while browsing (yellow box with position) - Auto-resize textarea to fit content - Improve scroll behavior to show output after execution - Remove redundant auto-fill of previous code - Streamline page layout (remove heading, fix scrollbar issues)
1 parent 9903bdf commit d3e531e

File tree

1 file changed

+229
-33
lines changed

1 file changed

+229
-33
lines changed

mcp-server/src/static/repl.html

Lines changed: 229 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,34 @@
66
<title>Penpot API REPL</title>
77
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
88
<style>
9+
html,
910
body {
10-
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
11-
max-width: 1200px;
12-
margin: 0 auto;
13-
padding: 20px;
14-
background-color: #f5f5f5;
11+
height: 100%;
12+
margin: 0;
13+
padding: 0;
14+
overflow: hidden;
1515
}
1616

17-
h1 {
18-
color: #333;
19-
text-align: center;
20-
margin-bottom: 30px;
17+
body {
18+
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
19+
background-color: #f5f5f5;
20+
display: flex;
21+
flex-direction: column;
22+
padding: 15px;
23+
box-sizing: border-box;
2124
}
2225

2326
.repl-container {
2427
background-color: white;
2528
border: 1px solid #ddd;
2629
border-radius: 4px;
2730
padding: 15px;
28-
min-height: 400px;
29-
max-height: 80vh;
31+
flex: 1;
3032
overflow-y: auto;
33+
max-width: 1200px;
34+
width: 100%;
35+
margin: 0 auto;
36+
box-sizing: border-box;
3137
}
3238

3339
.repl-entry {
@@ -55,7 +61,7 @@
5561

5662
.code-input {
5763
width: 100%;
58-
min-height: 60px;
64+
min-height: 80px;
5965
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
6066
font-size: 14px;
6167
border: 1px solid #ddd;
@@ -159,46 +165,62 @@
159165
text-align: center;
160166
color: #666;
161167
font-size: 12px;
162-
margin-top: 15px;
168+
padding: 10px 0;
163169
font-style: italic;
170+
flex-shrink: 0;
164171
}
165172

166173
.entry-number {
167174
color: #666;
168175
font-size: 12px;
169176
margin-bottom: 5px;
170177
}
178+
179+
.history-indicator {
180+
background-color: #fff3cd;
181+
border: 1px solid #ffc107;
182+
border-radius: 4px;
183+
padding: 4px 10px;
184+
font-size: 12px;
185+
color: #856404;
186+
display: inline-block;
187+
margin-bottom: 8px;
188+
}
171189
</style>
172190
</head>
173191
<body>
174-
<h1>Penpot API REPL</h1>
175-
176192
<div class="repl-container" id="repl-container">
177193
<!-- REPL entries will be dynamically added here -->
178194
</div>
179195

180-
<div class="controls-hint">Press Ctrl+Enter to execute code • Shift+Enter for new line</div>
196+
<div class="controls-hint">Ctrl+Enter to execute • Arrow up/down for command history</div>
181197

182198
<script>
183199
$(document).ready(function () {
184200
let isExecuting = false;
185201
let entryCounter = 1;
186-
let lastCode = ""; // store the last executed code
202+
let commandHistory = []; // full history of executed commands
203+
let historyIndex = 0; // current position in history
204+
let isBrowsingHistory = false; // whether we are currently browsing history
205+
let tempInput = ""; // temporary storage for current input when browsing history
187206

188207
// create the initial input entry
189208
createNewEntry();
190209

191210
function createNewEntry() {
192211
const entryId = `entry-${entryCounter}`;
193-
const defaultCode = lastCode || "";
212+
const isFirstEntry = entryCounter === 1;
213+
const placeholder = isFirstEntry
214+
? `// Enter your JavaScript code here...
215+
console.log('Hello from Penpot!');
216+
return 'This will be the result';`
217+
: "";
194218
const entryHtml = `
195219
<div class="repl-entry" id="${entryId}">
196220
<div class="entry-number">In [${entryCounter}]:</div>
197221
<div class="input-section">
198222
<textarea class="code-input" id="code-input-${entryCounter}"
199-
placeholder="// Enter your JavaScript code here...
200-
console.log('Hello from Penpot!');
201-
return 'This will be the result';">${escapeHtml(defaultCode)}</textarea>
223+
placeholder="${placeholder}"></textarea>
202224
<button class="execute-btn" id="execute-btn-${entryCounter}">Execute Code</button>
203225
</div>
204226
</div>
@@ -209,30 +231,200 @@ <h1>Penpot API REPL</h1>
209231
// bind events for this entry
210232
bindEntryEvents(entryCounter);
211233

212-
// focus on the new input
213-
$(`#code-input-${entryCounter}`).focus();
234+
// focus on the new input without scrolling
235+
const $input = $(`#code-input-${entryCounter}`);
236+
$input[0].focus({ preventScroll: true });
214237

215-
// auto-resize textarea
216-
$(`#code-input-${entryCounter}`).on("input", function () {
217-
this.style.height = "auto";
218-
this.style.height = Math.max(60, this.scrollHeight) + "px";
238+
// auto-resize textarea on input
239+
$input.on("input", function () {
240+
autoResizeTextarea(this);
219241
});
220242

221243
entryCounter++;
222244
}
223245

246+
/**
247+
* Resizes a textarea to fit its content, with a minimum height.
248+
* Adds border height since scrollHeight excludes borders but box-sizing: border-box includes them.
249+
*/
250+
function autoResizeTextarea(textarea) {
251+
textarea.style.height = "auto";
252+
// add 2px for top and bottom border (1px each)
253+
textarea.style.height = Math.max(80, textarea.scrollHeight + 2) + "px";
254+
}
255+
256+
/**
257+
* Checks if the cursor is at the beginning of a textarea (position 0 with no selection).
258+
*/
259+
function isCursorAtBeginning(textarea) {
260+
return textarea.selectionStart === 0 && textarea.selectionEnd === 0;
261+
}
262+
263+
/**
264+
* Checks if the cursor is at the end of a textarea (position at text length with no selection).
265+
*/
266+
function isCursorAtEnd(textarea) {
267+
const len = textarea.value.length;
268+
return textarea.selectionStart === len && textarea.selectionEnd === len;
269+
}
270+
271+
/**
272+
* Navigates through command history for the given entry's textarea.
273+
* @param direction -1 for previous (up), +1 for next (down)
274+
* @param entryNum the entry number
275+
*/
276+
function navigateHistory(direction, entryNum) {
277+
const $codeInput = $(`#code-input-${entryNum}`);
278+
const textarea = $codeInput[0];
279+
280+
if (commandHistory.length === 0) return;
281+
282+
if (direction === -1) {
283+
// going back in history (arrow up)
284+
if (!isBrowsingHistory) {
285+
// starting to browse history: save current input
286+
tempInput = $codeInput.val();
287+
isBrowsingHistory = true;
288+
historyIndex = commandHistory.length - 1;
289+
} else if (historyIndex > 0) {
290+
// go further back in history
291+
historyIndex--;
292+
} else {
293+
// already at oldest entry, do nothing
294+
return;
295+
}
296+
$codeInput.val(commandHistory[historyIndex]);
297+
autoResizeTextarea(textarea);
298+
// keep cursor at beginning for continued history navigation
299+
textarea.setSelectionRange(0, 0);
300+
// show history position (1 = most recent)
301+
const position = commandHistory.length - historyIndex;
302+
showHistoryIndicator(entryNum, position, commandHistory.length);
303+
} else {
304+
// going forward in history (arrow down)
305+
if (!isBrowsingHistory) {
306+
// not browsing history, do nothing
307+
return;
308+
} else if (historyIndex >= commandHistory.length - 1) {
309+
// at most recent entry, return to original input
310+
isBrowsingHistory = false;
311+
$codeInput.val(tempInput);
312+
autoResizeTextarea(textarea);
313+
// cursor at beginning (same as when we entered history)
314+
textarea.setSelectionRange(0, 0);
315+
hideHistoryIndicator();
316+
} else {
317+
// go forward in history
318+
historyIndex++;
319+
$codeInput.val(commandHistory[historyIndex]);
320+
autoResizeTextarea(textarea);
321+
// keep cursor at beginning
322+
textarea.setSelectionRange(0, 0);
323+
// update history position indicator
324+
const position = commandHistory.length - historyIndex;
325+
showHistoryIndicator(entryNum, position, commandHistory.length);
326+
}
327+
}
328+
}
329+
330+
/**
331+
* Exits history browsing mode, keeping current content in the input.
332+
* Moves cursor to end of input.
333+
* @param entryNum the entry number (optional, cursor not moved if not provided)
334+
*/
335+
function exitHistoryBrowsing(entryNum) {
336+
if (isBrowsingHistory) {
337+
isBrowsingHistory = false;
338+
hideHistoryIndicator();
339+
if (entryNum !== undefined) {
340+
const textarea = $(`#code-input-${entryNum}`)[0];
341+
const len = textarea.value.length;
342+
textarea.setSelectionRange(len, len);
343+
}
344+
}
345+
}
346+
347+
/**
348+
* Scrolls the repl container to show the output section of the given entry.
349+
*/
350+
function scrollToOutput($entry) {
351+
const $container = $("#repl-container");
352+
const $outputSection = $entry.find(".output-section");
353+
if ($outputSection.length) {
354+
const containerTop = $container.offset().top;
355+
const outputTop = $outputSection.offset().top;
356+
const scrollTop = $container.scrollTop();
357+
$container.animate(
358+
{
359+
scrollTop: scrollTop + (outputTop - containerTop),
360+
},
361+
300
362+
);
363+
}
364+
}
365+
366+
/**
367+
* Shows or updates the history indicator for the current entry.
368+
* @param entryNum the entry number
369+
* @param position 1-based position from most recent (1 = most recent)
370+
* @param total total number of history items
371+
*/
372+
function showHistoryIndicator(entryNum, position, total) {
373+
const $entry = $(`#entry-${entryNum}`);
374+
let $indicator = $entry.find(".history-indicator");
375+
376+
if ($indicator.length === 0) {
377+
$entry.find(".input-section").before('<div class="history-indicator"></div>');
378+
$indicator = $entry.find(".history-indicator");
379+
}
380+
381+
$indicator.text(`History item ${position}/${total}`);
382+
}
383+
384+
/**
385+
* Hides the history indicator.
386+
*/
387+
function hideHistoryIndicator() {
388+
$(".history-indicator").remove();
389+
}
390+
224391
function bindEntryEvents(entryNum) {
225392
const $executeBtn = $(`#execute-btn-${entryNum}`);
226393
const $codeInput = $(`#code-input-${entryNum}`);
227394

228395
// bind execute button click
229396
$executeBtn.on("click", () => executeCode(entryNum));
230397

231-
// bind Ctrl+Enter keyboard shortcut
398+
// bind keyboard shortcuts
232399
$codeInput.on("keydown", function (e) {
400+
// Ctrl+Enter to execute
233401
if (e.ctrlKey && e.key === "Enter") {
234402
e.preventDefault();
403+
exitHistoryBrowsing(entryNum);
235404
executeCode(entryNum);
405+
return;
406+
}
407+
408+
// arrow up at beginning of input (or while browsing history): navigate to previous history entry
409+
if (e.key === "ArrowUp" && (isBrowsingHistory || isCursorAtBeginning(this))) {
410+
e.preventDefault();
411+
navigateHistory(-1, entryNum);
412+
return;
413+
}
414+
415+
// arrow down at end of input (or while browsing history): navigate to next history entry
416+
if (e.key === "ArrowDown" && (isBrowsingHistory || isCursorAtEnd(this))) {
417+
e.preventDefault();
418+
navigateHistory(+1, entryNum);
419+
return;
420+
}
421+
422+
// any key except pure modifier keys exits history browsing
423+
if (isBrowsingHistory) {
424+
const isModifierOnly = ["Shift", "Control", "Alt", "Meta"].includes(e.key);
425+
if (!isModifierOnly) {
426+
exitHistoryBrowsing();
427+
}
236428
}
237429
});
238430
}
@@ -328,15 +520,16 @@ <h1>Penpot API REPL</h1>
328520
$codeInput.prop("readonly", true);
329521
$(`#execute-btn-${entryNum}`).remove();
330522

331-
// store the code for the next entry
332-
lastCode = code;
523+
// store the code in history
524+
commandHistory.push(code);
525+
isBrowsingHistory = false; // reset history navigation
526+
tempInput = ""; // clear temporary input
333527

334528
// create a new entry for the next input
335529
createNewEntry();
336530

337-
// scroll to the new entry
338-
const $container = $("#repl-container");
339-
$container.scrollTop($container[0].scrollHeight);
531+
// scroll to the output section of the executed entry
532+
scrollToOutput($entry);
340533
},
341534
error: function (xhr) {
342535
let errorData;
@@ -346,6 +539,9 @@ <h1>Penpot API REPL</h1>
346539
errorData = { error: "Network error or invalid response" };
347540
}
348541
displayResult(entryNum, errorData, true);
542+
543+
// scroll to the error output
544+
scrollToOutput($entry);
349545
},
350546
complete: function () {
351547
setExecuting(entryNum, false);

0 commit comments

Comments
 (0)