Skip to content

Commit 9e9fb07

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 9e9fb07

File tree

1 file changed

+225
-33
lines changed

1 file changed

+225
-33
lines changed

mcp-server/src/static/repl.html

Lines changed: 225 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,33 @@
66
<title>Penpot API REPL</title>
77
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
88
<style>
9+
html, body {
10+
height: 100%;
11+
margin: 0;
12+
padding: 0;
13+
overflow: hidden;
14+
}
15+
916
body {
1017
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
11-
max-width: 1200px;
12-
margin: 0 auto;
13-
padding: 20px;
1418
background-color: #f5f5f5;
15-
}
16-
17-
h1 {
18-
color: #333;
19-
text-align: center;
20-
margin-bottom: 30px;
19+
display: flex;
20+
flex-direction: column;
21+
padding: 15px;
22+
box-sizing: border-box;
2123
}
2224

2325
.repl-container {
2426
background-color: white;
2527
border: 1px solid #ddd;
2628
border-radius: 4px;
2729
padding: 15px;
28-
min-height: 400px;
29-
max-height: 80vh;
30+
flex: 1;
3031
overflow-y: auto;
32+
max-width: 1200px;
33+
width: 100%;
34+
margin: 0 auto;
35+
box-sizing: border-box;
3136
}
3237

3338
.repl-entry {
@@ -55,7 +60,7 @@
5560

5661
.code-input {
5762
width: 100%;
58-
min-height: 60px;
63+
min-height: 80px;
5964
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
6065
font-size: 14px;
6166
border: 1px solid #ddd;
@@ -159,46 +164,62 @@
159164
text-align: center;
160165
color: #666;
161166
font-size: 12px;
162-
margin-top: 15px;
167+
padding: 10px 0;
163168
font-style: italic;
169+
flex-shrink: 0;
164170
}
165171

166172
.entry-number {
167173
color: #666;
168174
font-size: 12px;
169175
margin-bottom: 5px;
170176
}
177+
178+
.history-indicator {
179+
background-color: #fff3cd;
180+
border: 1px solid #ffc107;
181+
border-radius: 4px;
182+
padding: 4px 10px;
183+
font-size: 12px;
184+
color: #856404;
185+
display: inline-block;
186+
margin-bottom: 8px;
187+
}
171188
</style>
172189
</head>
173190
<body>
174-
<h1>Penpot API REPL</h1>
175-
176191
<div class="repl-container" id="repl-container">
177192
<!-- REPL entries will be dynamically added here -->
178193
</div>
179194

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

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

188206
// create the initial input entry
189207
createNewEntry();
190208

191209
function createNewEntry() {
192210
const entryId = `entry-${entryCounter}`;
193-
const defaultCode = lastCode || "";
211+
const isFirstEntry = entryCounter === 1;
212+
const placeholder = isFirstEntry
213+
? `// Enter your JavaScript code here...
214+
console.log('Hello from Penpot!');
215+
return 'This will be the result';`
216+
: "";
194217
const entryHtml = `
195218
<div class="repl-entry" id="${entryId}">
196219
<div class="entry-number">In [${entryCounter}]:</div>
197220
<div class="input-section">
198221
<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>
222+
placeholder="${placeholder}"></textarea>
202223
<button class="execute-btn" id="execute-btn-${entryCounter}">Execute Code</button>
203224
</div>
204225
</div>
@@ -209,30 +230,197 @@ <h1>Penpot API REPL</h1>
209230
// bind events for this entry
210231
bindEntryEvents(entryCounter);
211232

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

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";
237+
// auto-resize textarea on input
238+
$input.on("input", function () {
239+
autoResizeTextarea(this);
219240
});
220241

221242
entryCounter++;
222243
}
223244

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

228391
// bind execute button click
229392
$executeBtn.on("click", () => executeCode(entryNum));
230393

231-
// bind Ctrl+Enter keyboard shortcut
394+
// bind keyboard shortcuts
232395
$codeInput.on("keydown", function (e) {
396+
// Ctrl+Enter to execute
233397
if (e.ctrlKey && e.key === "Enter") {
234398
e.preventDefault();
399+
exitHistoryBrowsing(entryNum);
235400
executeCode(entryNum);
401+
return;
402+
}
403+
404+
// arrow up at beginning of input (or while browsing history): navigate to previous history entry
405+
if (e.key === "ArrowUp" && (isBrowsingHistory || isCursorAtBeginning(this))) {
406+
e.preventDefault();
407+
navigateHistory(-1, entryNum);
408+
return;
409+
}
410+
411+
// arrow down at end of input (or while browsing history): navigate to next history entry
412+
if (e.key === "ArrowDown" && (isBrowsingHistory || isCursorAtEnd(this))) {
413+
e.preventDefault();
414+
navigateHistory(+1, entryNum);
415+
return;
416+
}
417+
418+
// any key except pure modifier keys exits history browsing
419+
if (isBrowsingHistory) {
420+
const isModifierOnly = ["Shift", "Control", "Alt", "Meta"].includes(e.key);
421+
if (!isModifierOnly) {
422+
exitHistoryBrowsing();
423+
}
236424
}
237425
});
238426
}
@@ -328,15 +516,16 @@ <h1>Penpot API REPL</h1>
328516
$codeInput.prop("readonly", true);
329517
$(`#execute-btn-${entryNum}`).remove();
330518

331-
// store the code for the next entry
332-
lastCode = code;
519+
// store the code in history
520+
commandHistory.push(code);
521+
isBrowsingHistory = false; // reset history navigation
522+
tempInput = ""; // clear temporary input
333523

334524
// create a new entry for the next input
335525
createNewEntry();
336526

337-
// scroll to the new entry
338-
const $container = $("#repl-container");
339-
$container.scrollTop($container[0].scrollHeight);
527+
// scroll to the output section of the executed entry
528+
scrollToOutput($entry);
340529
},
341530
error: function (xhr) {
342531
let errorData;
@@ -346,6 +535,9 @@ <h1>Penpot API REPL</h1>
346535
errorData = { error: "Network error or invalid response" };
347536
}
348537
displayResult(entryNum, errorData, true);
538+
539+
// scroll to the error output
540+
scrollToOutput($entry);
349541
},
350542
complete: function () {
351543
setExecuting(entryNum, false);

0 commit comments

Comments
 (0)