webtun 1.4.5 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/index.html +414 -32
- package/server.js +67 -0
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -550,13 +550,18 @@ svg { display: block; }
|
|
|
550
550
|
/* ═══════════════════════════════════════════════════════
|
|
551
551
|
CONTEXT MENU
|
|
552
552
|
═══════════════════════════════════════════════════════ */
|
|
553
|
-
#ctx-menu { position: fixed; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 4px 20px var(--shadow-color); z-index: 500; display: none; min-width: 160px; overflow: hidden; }
|
|
553
|
+
#ctx-menu { position: fixed; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 4px 20px var(--shadow-color); z-index: 500; display: none; min-width: 160px; overflow: hidden; max-height: calc(100vh - 16px); overflow-y: auto; }
|
|
554
554
|
@media (max-width: 360px) { #ctx-menu { min-width: 140px; max-width: calc(100vw - 16px); font-size: 11px; } }
|
|
555
555
|
#ctx-menu.open { display: block; }
|
|
556
556
|
.ctx-item { padding: 8px 14px; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; }
|
|
557
557
|
.ctx-item:hover { background: var(--bg4); }
|
|
558
558
|
.ctx-sep { height: 1px; background: var(--border); margin: 3px 0; }
|
|
559
559
|
.ctx-item.danger { color: var(--red); }
|
|
560
|
+
.ctx-submenu-wrap { }
|
|
561
|
+
.ctx-submenu-arrow { margin-left: auto; font-size: 10px; color: var(--fg3); transition: transform 0.15s; }
|
|
562
|
+
.ctx-submenu-wrap.open > .ctx-item > .ctx-submenu-arrow { transform: rotate(90deg); }
|
|
563
|
+
.ctx-submenu-items { display: none; }
|
|
564
|
+
.ctx-submenu-wrap.open > .ctx-submenu-items { display: block; }
|
|
560
565
|
|
|
561
566
|
/* ═══════════════════════════════════════════════════════
|
|
562
567
|
TOAST
|
|
@@ -660,8 +665,65 @@ svg { display: block; }
|
|
|
660
665
|
#cmd-lib-search {
|
|
661
666
|
width: calc(100% - 28px); height: 28px; font-size: 12px; margin: 8px 14px 0;
|
|
662
667
|
}
|
|
668
|
+
.cmd-lib-tabs {
|
|
669
|
+
display: flex; gap: 0; border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
670
|
+
}
|
|
671
|
+
.cmd-lib-tab {
|
|
672
|
+
flex: 1; height: 32px; font-size: 12px; font-weight: 600; color: var(--fg2);
|
|
673
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
674
|
+
cursor: pointer; transition: all var(--transition-fast);
|
|
675
|
+
display: flex; align-items: center; justify-content: center; gap: 5px;
|
|
676
|
+
}
|
|
677
|
+
.cmd-lib-tab:hover { color: var(--fg); background: var(--bg3); }
|
|
678
|
+
.cmd-lib-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
679
|
+
#cmd-hist-list {
|
|
680
|
+
flex: 1; overflow-y: auto; overflow-x: hidden; padding: 6px; min-height: 0;
|
|
681
|
+
display: none; flex-direction: column; gap: 2px;
|
|
682
|
+
}
|
|
683
|
+
#cmd-hist-empty {
|
|
684
|
+
text-align: center; color: var(--fg2); padding: 24px 16px; font-size: 12px;
|
|
685
|
+
}
|
|
686
|
+
.cmd-hist-item {
|
|
687
|
+
display: flex; align-items: center; gap: 8px; padding: 6px 10px;
|
|
688
|
+
border-radius: var(--radius-sm); cursor: default; font-size: 12px;
|
|
689
|
+
transition: background var(--transition-fast);
|
|
690
|
+
}
|
|
691
|
+
.cmd-hist-item:hover { background: var(--bg4); }
|
|
692
|
+
.cmd-hist-item .cmd-hist-cmd {
|
|
693
|
+
flex: 1; color: var(--fg); font-family: var(--font); overflow: hidden;
|
|
694
|
+
text-overflow: ellipsis; white-space: nowrap;
|
|
695
|
+
}
|
|
696
|
+
.cmd-hist-item .cmd-hist-time {
|
|
697
|
+
font-size: 10px; color: var(--fg3); flex-shrink: 0;
|
|
698
|
+
}
|
|
699
|
+
.cmd-hist-item .cmd-hist-run {
|
|
700
|
+
width: 24px; height: 24px; display: none; align-items: center; justify-content: center;
|
|
701
|
+
border-radius: var(--radius-sm); color: var(--fg2); flex-shrink: 0; cursor: pointer;
|
|
702
|
+
}
|
|
703
|
+
.cmd-hist-item:hover .cmd-hist-run { display: flex; }
|
|
704
|
+
.cmd-hist-item .cmd-hist-run:hover { background: var(--bg4); color: var(--green); }
|
|
705
|
+
.cmd-hist-item .cmd-hist-del {
|
|
706
|
+
width: 24px; height: 24px; display: none; align-items: center; justify-content: center;
|
|
707
|
+
border-radius: var(--radius-sm); color: var(--fg2); flex-shrink: 0; cursor: pointer;
|
|
708
|
+
}
|
|
709
|
+
.cmd-hist-item:hover .cmd-hist-del { display: flex; }
|
|
710
|
+
.cmd-hist-item .cmd-hist-del:hover { color: var(--red); background: var(--bg4); }
|
|
711
|
+
.cmd-hist-header {
|
|
712
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
713
|
+
padding: 6px 10px; flex-shrink: 0;
|
|
714
|
+
}
|
|
715
|
+
.cmd-hist-header label { font-size: 11px; color: var(--fg2); display: flex; align-items: center; gap: 4px; }
|
|
716
|
+
.cmd-hist-header input[type="number"] {
|
|
717
|
+
width: 52px; height: 22px; font-size: 11px; padding: 0 4px; text-align: center;
|
|
718
|
+
}
|
|
719
|
+
.cmd-hist-clear {
|
|
720
|
+
font-size: 11px; color: var(--red); background: none; border: none; cursor: pointer;
|
|
721
|
+
padding: 2px 6px; border-radius: var(--radius-sm);
|
|
722
|
+
}
|
|
723
|
+
.cmd-hist-clear:hover { background: var(--bg4); }
|
|
663
724
|
#cmd-lib-list {
|
|
664
|
-
flex: 1; overflow-y: auto; padding: 6px; min-height: 0;
|
|
725
|
+
flex: 1; overflow-y: auto; overflow-x: hidden; padding: 6px; min-height: 0;
|
|
726
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
665
727
|
}
|
|
666
728
|
#cmd-lib-empty {
|
|
667
729
|
text-align: center; color: var(--fg2); padding: 24px 16px; font-size: 12px;
|
|
@@ -957,8 +1019,19 @@ svg { display: block; }
|
|
|
957
1019
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
958
1020
|
</button>
|
|
959
1021
|
</div>
|
|
1022
|
+
<div class="cmd-lib-tabs">
|
|
1023
|
+
<button class="cmd-lib-tab active" id="cmd-tab-lib" onclick="switchCmdTab('library')">Library</button>
|
|
1024
|
+
<button class="cmd-lib-tab" id="cmd-tab-hist" onclick="switchCmdTab('history')">History</button>
|
|
1025
|
+
</div>
|
|
960
1026
|
<input id="cmd-lib-search" type="text" placeholder="Filter commands…" oninput="filterCmdLib()">
|
|
961
1027
|
<div id="cmd-lib-list"></div>
|
|
1028
|
+
<div id="cmd-hist-list">
|
|
1029
|
+
<div id="cmd-hist-empty">No commands run yet</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="cmd-hist-header" id="cmd-hist-header" style="display:none">
|
|
1032
|
+
<label>Keep last <input type="number" id="cmd-hist-max" min="10" max="500" value="50" onchange="updateHistMax(+this.value)"> cmds</label>
|
|
1033
|
+
<button class="cmd-hist-clear" onclick="clearCmdHist()">Clear History</button>
|
|
1034
|
+
</div>
|
|
962
1035
|
<div id="cmd-lib-footer">
|
|
963
1036
|
<input id="cmd-lib-name-input" type="text" placeholder="Name" onkeydown="if(event.key==='Enter')document.getElementById('cmd-lib-cmd-input').focus()">
|
|
964
1037
|
<input id="cmd-lib-cmd-input" type="text" placeholder="Command (e.g. ls -la)" onkeydown="if(event.key==='Enter')addCustomCmd()">
|
|
@@ -1198,12 +1271,37 @@ svg { display: block; }
|
|
|
1198
1271
|
</div>
|
|
1199
1272
|
|
|
1200
1273
|
<!-- Terminal context menu -->
|
|
1201
|
-
<div id="term-ctx-menu" role="menu" style="position:fixed;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 20px var(--shadow-color);z-index:500;display:none;min-width:
|
|
1274
|
+
<div id="term-ctx-menu" role="menu" style="position:fixed;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 20px var(--shadow-color);z-index:500;display:none;min-width:180px;overflow:hidden">
|
|
1275
|
+
<!-- Clipboard -->
|
|
1202
1276
|
<div class="ctx-item" id="term-ctx-copy" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy</div>
|
|
1203
1277
|
<div class="ctx-item" id="term-ctx-paste" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="14" y="2" width="8" height="8" rx="2"/><path d="M16 14H8M8 18h8"/><path d="M5 4h4"/><rect x="2" y="6" width="16" height="16" rx="2"/></svg> Paste</div>
|
|
1278
|
+
<div class="ctx-sep"></div>
|
|
1204
1279
|
<div class="ctx-item" id="term-ctx-select-all" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/><path d="M3 9h18"/><path d="M3 15h18"/></svg> Select All</div>
|
|
1280
|
+
<!-- Tab / Path -->
|
|
1281
|
+
<div class="ctx-sep"></div>
|
|
1282
|
+
<div class="ctx-item" id="term-ctx-new-tab" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> New Tab Here</div>
|
|
1283
|
+
<div class="ctx-item" id="term-ctx-copy-cwd" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy Working Directory</div>
|
|
1284
|
+
<!-- Control -->
|
|
1205
1285
|
<div class="ctx-sep"></div>
|
|
1206
|
-
<div class="ctx-item" id="term-ctx-
|
|
1286
|
+
<div class="ctx-item" id="term-ctx-interrupt" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="9" x2="15" y2="15"/><line x1="15" y1="9" x2="9" y2="15"/></svg> Interrupt (Ctrl+C)</div>
|
|
1287
|
+
<!-- More Options -->
|
|
1288
|
+
<div class="ctx-sep"></div>
|
|
1289
|
+
<div class="ctx-submenu-wrap">
|
|
1290
|
+
<div class="ctx-item" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg> More Options<span class="ctx-submenu-arrow">▸</span></div>
|
|
1291
|
+
<div class="ctx-submenu-items">
|
|
1292
|
+
<div class="ctx-item" id="term-ctx-find" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Find in Terminal</div>
|
|
1293
|
+
<div class="ctx-item" id="term-ctx-zoom-in" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg> Zoom In</div>
|
|
1294
|
+
<div class="ctx-item" id="term-ctx-zoom-out" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg> Zoom Out</div>
|
|
1295
|
+
<div class="ctx-item" id="term-ctx-select-line" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"/></svg> Select Line</div>
|
|
1296
|
+
<div class="ctx-item" id="term-ctx-scroll-top" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg> Scroll to Top</div>
|
|
1297
|
+
<div class="ctx-item" id="term-ctx-scroll-bottom" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg> Scroll to Bottom</div>
|
|
1298
|
+
<div class="ctx-item" id="term-ctx-rename" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg> Rename Tab</div>
|
|
1299
|
+
<div class="ctx-item" id="term-ctx-bookmark" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg> Bookmark Directory</div>
|
|
1300
|
+
</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
<!-- Danger -->
|
|
1303
|
+
<div class="ctx-sep"></div>
|
|
1304
|
+
<div class="ctx-item danger" id="term-ctx-clear" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/></svg> Clear</div>
|
|
1207
1305
|
</div>
|
|
1208
1306
|
|
|
1209
1307
|
<!-- Fuzzy Finder Modal -->
|
|
@@ -1804,6 +1902,25 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1804
1902
|
|
|
1805
1903
|
const sendInput = data => {
|
|
1806
1904
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
1905
|
+
// On Enter, read command from terminal screen buffer (avoids echo-back pollution)
|
|
1906
|
+
for (let i = 0; i < data.length; i++) {
|
|
1907
|
+
const ch = data.charCodeAt(i);
|
|
1908
|
+
if (ch === 13 || ch === 10) { // Enter
|
|
1909
|
+
try {
|
|
1910
|
+
const buf = tab.term.buffer.active;
|
|
1911
|
+
const line = buf.getLine(buf.baseY + buf.cursorY);
|
|
1912
|
+
if (line) {
|
|
1913
|
+
let text = line.translateToString(true).trim();
|
|
1914
|
+
// Strip prompt prefix: find last $ # or % and take text after it
|
|
1915
|
+
const promptEnd = Math.max(text.lastIndexOf('$'), text.lastIndexOf('#'), text.lastIndexOf('%'));
|
|
1916
|
+
if (promptEnd >= 0 && promptEnd < text.length - 1) {
|
|
1917
|
+
text = text.substring(promptEnd + 1).trim();
|
|
1918
|
+
}
|
|
1919
|
+
if (text) addToCmdHist(text);
|
|
1920
|
+
}
|
|
1921
|
+
} catch {}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1807
1924
|
const enc = new TextEncoder().encode(data);
|
|
1808
1925
|
const buf = new Uint8Array(1 + enc.length);
|
|
1809
1926
|
buf[0] = 0x00; buf.set(enc, 1);
|
|
@@ -2097,9 +2214,23 @@ function initTerminal(tab) {
|
|
|
2097
2214
|
const menu = document.getElementById('term-ctx-menu');
|
|
2098
2215
|
const sel = term.getSelection();
|
|
2099
2216
|
document.getElementById('term-ctx-copy').style.opacity = sel ? '' : '0.5';
|
|
2100
|
-
|
|
2101
|
-
menu.style.top = Math.min(e.clientY, window.innerHeight - 160) + 'px';
|
|
2217
|
+
// Show first to measure actual dimensions
|
|
2102
2218
|
menu.style.display = 'block';
|
|
2219
|
+
const mw = menu.offsetWidth;
|
|
2220
|
+
const mh = menu.offsetHeight;
|
|
2221
|
+
const vw = window.innerWidth;
|
|
2222
|
+
const vh = window.innerHeight;
|
|
2223
|
+
let left = e.clientX;
|
|
2224
|
+
let top = e.clientY;
|
|
2225
|
+
// Flip horizontally if overflowing right
|
|
2226
|
+
if (left + mw > vw) left = Math.max(0, e.clientX - mw);
|
|
2227
|
+
// Flip vertically if overflowing bottom
|
|
2228
|
+
if (top + mh > vh) top = Math.max(0, e.clientY - mh);
|
|
2229
|
+
// Clamp to viewport
|
|
2230
|
+
left = Math.max(0, Math.min(left, vw - mw));
|
|
2231
|
+
top = Math.max(0, Math.min(top, vh - mh));
|
|
2232
|
+
menu.style.left = left + 'px';
|
|
2233
|
+
menu.style.top = top + 'px';
|
|
2103
2234
|
});
|
|
2104
2235
|
|
|
2105
2236
|
// Touch-to-mouse translation for TUI apps (mobile)
|
|
@@ -2686,24 +2817,115 @@ function renderBreadcrumb(fullPath) {
|
|
|
2686
2817
|
let fsClipboard = null;
|
|
2687
2818
|
|
|
2688
2819
|
// Terminal context menu actions (registered once)
|
|
2820
|
+
function hideTermCtxMenu() {
|
|
2821
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2822
|
+
document.querySelectorAll('#term-ctx-menu .ctx-submenu-wrap.open').forEach(el => el.classList.remove('open'));
|
|
2823
|
+
}
|
|
2689
2824
|
document.getElementById('term-ctx-copy').addEventListener('click', () => {
|
|
2690
2825
|
const t = getActiveTab();
|
|
2691
|
-
if (t?.term) {
|
|
2692
|
-
|
|
2826
|
+
if (t?.term) {
|
|
2827
|
+
const sel = t.term.getSelection();
|
|
2828
|
+
if (sel) {
|
|
2829
|
+
navigator.clipboard.writeText(sel).then(() => {
|
|
2830
|
+
toast('Copied to clipboard', 'success');
|
|
2831
|
+
}).catch(() => {
|
|
2832
|
+
document.execCommand('copy');
|
|
2833
|
+
toast('Copied to clipboard', 'success');
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
hideTermCtxMenu();
|
|
2693
2838
|
});
|
|
2694
2839
|
document.getElementById('term-ctx-paste').addEventListener('click', () => {
|
|
2695
2840
|
pasteToTerminal();
|
|
2696
|
-
|
|
2841
|
+
hideTermCtxMenu();
|
|
2697
2842
|
});
|
|
2698
2843
|
document.getElementById('term-ctx-select-all').addEventListener('click', () => {
|
|
2699
2844
|
const t = getActiveTab();
|
|
2700
2845
|
if (t?.term) t.term.selectAll();
|
|
2701
|
-
|
|
2846
|
+
hideTermCtxMenu();
|
|
2847
|
+
});
|
|
2848
|
+
document.getElementById('term-ctx-select-line').addEventListener('click', () => {
|
|
2849
|
+
selectTermLine();
|
|
2850
|
+
hideTermCtxMenu();
|
|
2851
|
+
});
|
|
2852
|
+
document.getElementById('term-ctx-find').addEventListener('click', () => {
|
|
2853
|
+
toggleSearch();
|
|
2854
|
+
hideTermCtxMenu();
|
|
2855
|
+
});
|
|
2856
|
+
document.getElementById('term-ctx-zoom-in').addEventListener('click', () => {
|
|
2857
|
+
applyFontSize((settings.fontSize || 14) + 1);
|
|
2858
|
+
hideTermCtxMenu();
|
|
2859
|
+
});
|
|
2860
|
+
document.getElementById('term-ctx-zoom-out').addEventListener('click', () => {
|
|
2861
|
+
applyFontSize((settings.fontSize || 14) - 1);
|
|
2862
|
+
hideTermCtxMenu();
|
|
2863
|
+
});
|
|
2864
|
+
document.getElementById('term-ctx-new-tab').addEventListener('click', () => {
|
|
2865
|
+
const t = getActiveTab();
|
|
2866
|
+
newTab(null, null, t?.cwd || currentPath);
|
|
2867
|
+
hideTermCtxMenu();
|
|
2868
|
+
});
|
|
2869
|
+
document.getElementById('term-ctx-copy-cwd').addEventListener('click', () => {
|
|
2870
|
+
const t = getActiveTab();
|
|
2871
|
+
const cwd = t?.cwd || currentPath;
|
|
2872
|
+
if (cwd) {
|
|
2873
|
+
navigator.clipboard.writeText(cwd).then(() => {
|
|
2874
|
+
toast('Path copied', 'success');
|
|
2875
|
+
}).catch(() => {
|
|
2876
|
+
document.execCommand('copy');
|
|
2877
|
+
toast('Path copied', 'success');
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
hideTermCtxMenu();
|
|
2881
|
+
});
|
|
2882
|
+
document.getElementById('term-ctx-bookmark').addEventListener('click', () => {
|
|
2883
|
+
const t = getActiveTab();
|
|
2884
|
+
const cwd = t?.cwd || currentPath;
|
|
2885
|
+
if (cwd) {
|
|
2886
|
+
const bm = getBookmarks();
|
|
2887
|
+
if (bm.some(b => b.path === cwd)) { toast('Already bookmarked', 'info'); }
|
|
2888
|
+
else {
|
|
2889
|
+
const sep = cwd.includes('\\') ? '\\' : '/';
|
|
2890
|
+
bm.push({ name: cwd.split(sep).filter(Boolean).pop() || cwd, path: cwd });
|
|
2891
|
+
saveBookmarks(bm);
|
|
2892
|
+
toast('Bookmarked', 'success');
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
hideTermCtxMenu();
|
|
2896
|
+
});
|
|
2897
|
+
document.getElementById('term-ctx-interrupt').addEventListener('click', () => {
|
|
2898
|
+
sendKey('\x03');
|
|
2899
|
+
hideTermCtxMenu();
|
|
2900
|
+
});
|
|
2901
|
+
document.getElementById('term-ctx-rename').addEventListener('click', () => {
|
|
2902
|
+
const t = getActiveTab();
|
|
2903
|
+
if (t?.el) {
|
|
2904
|
+
const span = t.el.querySelector('.tab-title');
|
|
2905
|
+
if (span) span.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
|
|
2906
|
+
}
|
|
2907
|
+
hideTermCtxMenu();
|
|
2908
|
+
});
|
|
2909
|
+
document.getElementById('term-ctx-scroll-top').addEventListener('click', () => {
|
|
2910
|
+
const t = getActiveTab();
|
|
2911
|
+
if (t?.term) t.term.scrollToTop();
|
|
2912
|
+
hideTermCtxMenu();
|
|
2913
|
+
});
|
|
2914
|
+
document.getElementById('term-ctx-scroll-bottom').addEventListener('click', () => {
|
|
2915
|
+
const t = getActiveTab();
|
|
2916
|
+
if (t?.term) t.term.scrollToBottom();
|
|
2917
|
+
hideTermCtxMenu();
|
|
2702
2918
|
});
|
|
2703
2919
|
document.getElementById('term-ctx-clear').addEventListener('click', () => {
|
|
2704
2920
|
const t = getActiveTab();
|
|
2705
2921
|
if (t?.term) t.term.clear();
|
|
2706
|
-
|
|
2922
|
+
hideTermCtxMenu();
|
|
2923
|
+
});
|
|
2924
|
+
// Submenu toggle on click
|
|
2925
|
+
document.querySelector('#term-ctx-menu .ctx-submenu-wrap > .ctx-item').addEventListener('click', e => {
|
|
2926
|
+
e.stopPropagation();
|
|
2927
|
+
const wrap = e.currentTarget.closest('.ctx-submenu-wrap');
|
|
2928
|
+
wrap.classList.toggle('open');
|
|
2707
2929
|
});
|
|
2708
2930
|
|
|
2709
2931
|
function showCtxMenu(e, file) {
|
|
@@ -2718,24 +2940,21 @@ function showCtxMenu(e, file) {
|
|
|
2718
2940
|
document.getElementById('ctx-zip').style.display = '';
|
|
2719
2941
|
document.getElementById('ctx-folder-size').style.display = file.isDir ? '' : 'none';
|
|
2720
2942
|
document.getElementById('ctx-extract').style.display = file.isDir ? 'none' : (file.ext === '.zip' ? '' : 'none');
|
|
2943
|
+
// Measure after content is set
|
|
2721
2944
|
const mw = menu.offsetWidth, mh = menu.offsetHeight;
|
|
2722
|
-
|
|
2723
|
-
let
|
|
2724
|
-
|
|
2725
|
-
if (
|
|
2945
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
2946
|
+
let left = e.clientX, top = e.clientY;
|
|
2947
|
+
// Flip horizontally if overflowing right
|
|
2948
|
+
if (left + mw > vw) left = Math.max(0, e.clientX - mw);
|
|
2949
|
+
// Flip vertically if overflowing bottom
|
|
2950
|
+
if (top + mh > vh) top = Math.max(0, e.clientY - mh);
|
|
2951
|
+
// Clamp to viewport
|
|
2952
|
+
left = Math.max(0, Math.min(left, vw - mw));
|
|
2953
|
+
top = Math.max(0, Math.min(top, vh - mh));
|
|
2726
2954
|
menu.style.left = left + 'px';
|
|
2727
2955
|
menu.style.top = top + 'px';
|
|
2728
2956
|
menu.classList.add('open');
|
|
2729
2957
|
menu.style.display = '';
|
|
2730
|
-
requestAnimationFrame(() => {
|
|
2731
|
-
const rect = menu.getBoundingClientRect();
|
|
2732
|
-
if (rect.right > window.innerWidth) {
|
|
2733
|
-
menu.style.left = Math.max(0, window.innerWidth - rect.width - 8) + 'px';
|
|
2734
|
-
}
|
|
2735
|
-
if (rect.bottom > window.innerHeight) {
|
|
2736
|
-
menu.style.top = Math.max(0, window.innerHeight - rect.height - 8) + 'px';
|
|
2737
|
-
}
|
|
2738
|
-
});
|
|
2739
2958
|
}
|
|
2740
2959
|
|
|
2741
2960
|
document.addEventListener('click', () => {
|
|
@@ -4191,6 +4410,11 @@ function setupKeyboardShortcuts() {
|
|
|
4191
4410
|
if (ctrl && e.shiftKey && e.key === 'ArrowLeft') { e.preventDefault(); cycleTab(-1); }
|
|
4192
4411
|
if (ctrl && e.shiftKey && e.key === 'r') { e.preventDefault(); refreshPreview(); }
|
|
4193
4412
|
if (e.key === 'Escape') {
|
|
4413
|
+
const termMenu = document.getElementById('term-ctx-menu');
|
|
4414
|
+
if (termMenu && termMenu.style.display !== 'none') {
|
|
4415
|
+
termMenu.style.display = 'none';
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4194
4418
|
if (document.getElementById('ctx-menu').classList.contains('open')) {
|
|
4195
4419
|
document.getElementById('ctx-menu').classList.remove('open');
|
|
4196
4420
|
return;
|
|
@@ -4350,6 +4574,7 @@ function pasteToTerminal() {
|
|
|
4350
4574
|
const buf = new Uint8Array(1 + enc.length);
|
|
4351
4575
|
buf[0] = 0x00; buf.set(enc, 1);
|
|
4352
4576
|
tab.ws.send(buf.buffer);
|
|
4577
|
+
tab.term?.focus();
|
|
4353
4578
|
toast('Pasted to terminal', 'success');
|
|
4354
4579
|
}
|
|
4355
4580
|
};
|
|
@@ -4366,14 +4591,16 @@ function fallbackPaste(callback) {
|
|
|
4366
4591
|
_pasteInput = document.createElement('textarea');
|
|
4367
4592
|
_pasteInput.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;z-index:-1';
|
|
4368
4593
|
document.body.appendChild(_pasteInput);
|
|
4369
|
-
_pasteInput.addEventListener('
|
|
4370
|
-
|
|
4371
|
-
|
|
4594
|
+
_pasteInput.addEventListener('paste', () => {
|
|
4595
|
+
setTimeout(() => {
|
|
4596
|
+
callback(_pasteInput.value);
|
|
4597
|
+
_pasteInput.value = '';
|
|
4598
|
+
}, 0);
|
|
4372
4599
|
});
|
|
4373
4600
|
}
|
|
4374
4601
|
_pasteInput.value = '';
|
|
4375
4602
|
_pasteInput.focus();
|
|
4376
|
-
|
|
4603
|
+
document.execCommand('paste');
|
|
4377
4604
|
}
|
|
4378
4605
|
|
|
4379
4606
|
function selectAllTerm() {
|
|
@@ -4915,9 +5142,8 @@ function toggleCmdLib() {
|
|
|
4915
5142
|
panel.classList.toggle('open');
|
|
4916
5143
|
document.getElementById('cmd-lib-toggle').classList.toggle('active', !isOpen);
|
|
4917
5144
|
if (!isOpen) {
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
document.getElementById('cmd-lib-search').focus();
|
|
5145
|
+
loadHistMax();
|
|
5146
|
+
switchCmdTab('library');
|
|
4921
5147
|
setTimeout(() => {
|
|
4922
5148
|
document.addEventListener('click', closeCmdLibOnClickOutside, true);
|
|
4923
5149
|
}, 0);
|
|
@@ -4999,7 +5225,13 @@ function renderCmdLib(filter) {
|
|
|
4999
5225
|
item.appendChild(delBtn);
|
|
5000
5226
|
}
|
|
5001
5227
|
|
|
5002
|
-
item.addEventListener('click', () =>
|
|
5228
|
+
item.addEventListener('click', () => {
|
|
5229
|
+
const tab = getActiveTab();
|
|
5230
|
+
if (!tab?.term) return;
|
|
5231
|
+
tab.term.focus();
|
|
5232
|
+
document.execCommand('insertText', false, c.cmd);
|
|
5233
|
+
toggleCmdLib();
|
|
5234
|
+
});
|
|
5003
5235
|
section.appendChild(item);
|
|
5004
5236
|
});
|
|
5005
5237
|
list.appendChild(section);
|
|
@@ -5023,6 +5255,7 @@ function runCmdLib(cmd) {
|
|
|
5023
5255
|
buf.set(enc, 1);
|
|
5024
5256
|
tab.ws.send(buf.buffer);
|
|
5025
5257
|
tab.term?.focus();
|
|
5258
|
+
addToCmdHist(cmd);
|
|
5026
5259
|
toast('Running: ' + cmd, 'info');
|
|
5027
5260
|
}
|
|
5028
5261
|
|
|
@@ -5053,6 +5286,155 @@ function deleteCustomCmd(name, cmd) {
|
|
|
5053
5286
|
toast('Command removed', 'info');
|
|
5054
5287
|
}
|
|
5055
5288
|
|
|
5289
|
+
// ═══════════════════════════════════════════════════════
|
|
5290
|
+
// COMMAND HISTORY (server-side storage)
|
|
5291
|
+
// ═══════════════════════════════════════════════════════
|
|
5292
|
+
let cmdHistMax = 50;
|
|
5293
|
+
let _cmdHistCache = [];
|
|
5294
|
+
|
|
5295
|
+
function histHeaders() {
|
|
5296
|
+
return { 'Content-Type': 'application/json', 'x-pin-token': localStorage.getItem('wt-pin') || '' };
|
|
5297
|
+
}
|
|
5298
|
+
function histHeadersDelete() {
|
|
5299
|
+
return { 'x-pin-token': localStorage.getItem('wt-pin') || '' };
|
|
5300
|
+
}
|
|
5301
|
+
async function getCmdHist() {
|
|
5302
|
+
try {
|
|
5303
|
+
const r = await fetch('/api/history', { headers: histHeadersDelete() });
|
|
5304
|
+
if (!r.ok) return _cmdHistCache;
|
|
5305
|
+
const data = await r.json();
|
|
5306
|
+
_cmdHistCache = data.history || [];
|
|
5307
|
+
if (data.max) cmdHistMax = data.max;
|
|
5308
|
+
return _cmdHistCache;
|
|
5309
|
+
} catch { return _cmdHistCache; }
|
|
5310
|
+
}
|
|
5311
|
+
function loadHistMax() {
|
|
5312
|
+
getCmdHist().then(() => {
|
|
5313
|
+
const el = document.getElementById('cmd-hist-max');
|
|
5314
|
+
if (el) el.value = cmdHistMax;
|
|
5315
|
+
});
|
|
5316
|
+
}
|
|
5317
|
+
async function addToCmdHist(cmd) {
|
|
5318
|
+
if (!cmd || !cmd.trim()) return;
|
|
5319
|
+
// Strip ANSI escapes and control characters, keep only printable + space + tab
|
|
5320
|
+
const clean = cmd.replace(/\x1b\[[0-9;]*[a-zA-Z~]/g, '')
|
|
5321
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
5322
|
+
.replace(/[\x00-\x08\x0e-\x1f]/g, '')
|
|
5323
|
+
.trim();
|
|
5324
|
+
if (!clean) return;
|
|
5325
|
+
try {
|
|
5326
|
+
await fetch('/api/history', { method: 'POST', headers: histHeaders(), body: JSON.stringify({ cmd: clean, max: cmdHistMax }) });
|
|
5327
|
+
await getCmdHist();
|
|
5328
|
+
} catch {}
|
|
5329
|
+
}
|
|
5330
|
+
async function removeCmdHistItem(idx) {
|
|
5331
|
+
try {
|
|
5332
|
+
await fetch('/api/history/' + idx, { method: 'DELETE', headers: histHeadersDelete() });
|
|
5333
|
+
await getCmdHist();
|
|
5334
|
+
renderCmdHist();
|
|
5335
|
+
} catch {}
|
|
5336
|
+
}
|
|
5337
|
+
async function clearCmdHist() {
|
|
5338
|
+
try {
|
|
5339
|
+
await fetch('/api/history', { method: 'DELETE', headers: histHeadersDelete() });
|
|
5340
|
+
_cmdHistCache = [];
|
|
5341
|
+
renderCmdHist();
|
|
5342
|
+
toast('History cleared', 'info');
|
|
5343
|
+
} catch {}
|
|
5344
|
+
}
|
|
5345
|
+
async function updateHistMax(val) {
|
|
5346
|
+
cmdHistMax = Math.max(10, Math.min(500, val || 50));
|
|
5347
|
+
try {
|
|
5348
|
+
await fetch('/api/history', { method: 'POST', headers: histHeaders(), body: JSON.stringify({ cmd: '', max: cmdHistMax }) });
|
|
5349
|
+
} catch {}
|
|
5350
|
+
}
|
|
5351
|
+
function switchCmdTab(tab) {
|
|
5352
|
+
const libBtn = document.getElementById('cmd-tab-lib');
|
|
5353
|
+
const histBtn = document.getElementById('cmd-tab-hist');
|
|
5354
|
+
const libList = document.getElementById('cmd-lib-list');
|
|
5355
|
+
const histList = document.getElementById('cmd-hist-list');
|
|
5356
|
+
const histHeader = document.getElementById('cmd-hist-header');
|
|
5357
|
+
const footer = document.getElementById('cmd-lib-footer');
|
|
5358
|
+
const search = document.getElementById('cmd-lib-search');
|
|
5359
|
+
if (tab === 'library') {
|
|
5360
|
+
libBtn.classList.add('active');
|
|
5361
|
+
histBtn.classList.remove('active');
|
|
5362
|
+
libList.style.display = '';
|
|
5363
|
+
histList.style.display = 'none';
|
|
5364
|
+
histHeader.style.display = 'none';
|
|
5365
|
+
footer.style.display = '';
|
|
5366
|
+
search.placeholder = 'Filter commands…';
|
|
5367
|
+
search.oninput = () => filterCmdLib();
|
|
5368
|
+
filterCmdLib();
|
|
5369
|
+
} else {
|
|
5370
|
+
histBtn.classList.add('active');
|
|
5371
|
+
libBtn.classList.remove('active');
|
|
5372
|
+
histList.style.display = 'flex';
|
|
5373
|
+
libList.style.display = 'none';
|
|
5374
|
+
histHeader.style.display = '';
|
|
5375
|
+
footer.style.display = 'none';
|
|
5376
|
+
search.placeholder = 'Filter history…';
|
|
5377
|
+
search.oninput = () => renderCmdHist();
|
|
5378
|
+
renderCmdHist();
|
|
5379
|
+
}
|
|
5380
|
+
search.value = '';
|
|
5381
|
+
search.focus();
|
|
5382
|
+
}
|
|
5383
|
+
async function renderCmdHist() {
|
|
5384
|
+
const list = document.getElementById('cmd-hist-list');
|
|
5385
|
+
const hist = await getCmdHist();
|
|
5386
|
+
const q = (document.getElementById('cmd-lib-search').value || '').toLowerCase();
|
|
5387
|
+
const filtered = q ? hist.filter(h => h.cmd.toLowerCase().includes(q)) : hist;
|
|
5388
|
+
|
|
5389
|
+
list.innerHTML = '';
|
|
5390
|
+
if (!filtered.length) {
|
|
5391
|
+
list.innerHTML = '<div id="cmd-hist-empty">No commands run yet</div>';
|
|
5392
|
+
return;
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
filtered.forEach((h, i) => {
|
|
5396
|
+
const realIdx = q ? hist.indexOf(h) : i;
|
|
5397
|
+
const item = document.createElement('div');
|
|
5398
|
+
item.className = 'cmd-hist-item';
|
|
5399
|
+
|
|
5400
|
+
const cmdSpan = document.createElement('span');
|
|
5401
|
+
cmdSpan.className = 'cmd-hist-cmd';
|
|
5402
|
+
cmdSpan.textContent = h.cmd;
|
|
5403
|
+
cmdSpan.title = h.cmd;
|
|
5404
|
+
item.appendChild(cmdSpan);
|
|
5405
|
+
|
|
5406
|
+
const timeSpan = document.createElement('span');
|
|
5407
|
+
timeSpan.className = 'cmd-hist-time';
|
|
5408
|
+
const ago = Date.now() - h.time;
|
|
5409
|
+
timeSpan.textContent = ago < 60000 ? 'just now' : ago < 3600000 ? Math.floor(ago / 60000) + 'm ago' : ago < 86400000 ? Math.floor(ago / 3600000) + 'h ago' : new Date(h.time).toLocaleDateString();
|
|
5410
|
+
if (h.count > 1) timeSpan.textContent = '×' + h.count + ' ' + timeSpan.textContent;
|
|
5411
|
+
item.appendChild(timeSpan);
|
|
5412
|
+
|
|
5413
|
+
const runBtn = document.createElement('button');
|
|
5414
|
+
runBtn.className = 'cmd-hist-run';
|
|
5415
|
+
runBtn.title = 'Run in terminal';
|
|
5416
|
+
runBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6,3 20,12 6,21"/></svg>';
|
|
5417
|
+
runBtn.addEventListener('click', e => { e.stopPropagation(); runCmdLib(h.cmd); });
|
|
5418
|
+
item.appendChild(runBtn);
|
|
5419
|
+
|
|
5420
|
+
const delBtn = document.createElement('button');
|
|
5421
|
+
delBtn.className = 'cmd-hist-del';
|
|
5422
|
+
delBtn.title = 'Remove';
|
|
5423
|
+
delBtn.textContent = '×';
|
|
5424
|
+
delBtn.addEventListener('click', e => { e.stopPropagation(); removeCmdHistItem(realIdx); });
|
|
5425
|
+
item.appendChild(delBtn);
|
|
5426
|
+
|
|
5427
|
+
item.addEventListener('click', () => {
|
|
5428
|
+
const tab = getActiveTab();
|
|
5429
|
+
if (!tab?.term) return;
|
|
5430
|
+
tab.term.focus();
|
|
5431
|
+
document.execCommand('insertText', false, h.cmd);
|
|
5432
|
+
toggleCmdLib();
|
|
5433
|
+
});
|
|
5434
|
+
list.appendChild(item);
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5056
5438
|
// ═══════════════════════════════════════════════════════
|
|
5057
5439
|
// SYSTEM STATS
|
|
5058
5440
|
// ═══════════════════════════════════════════════════════
|
package/server.js
CHANGED
|
@@ -127,6 +127,14 @@ const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT ? path.resolve(process.env.WOR
|
|
|
127
127
|
app.use(express.json({ limit: '50mb' }));
|
|
128
128
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
129
129
|
|
|
130
|
+
// Handle malformed JSON gracefully
|
|
131
|
+
app.use((err, req, res, next) => {
|
|
132
|
+
if (err.type === 'entity.parse.failed') {
|
|
133
|
+
return res.status(400).json({ error: 'Invalid JSON' });
|
|
134
|
+
}
|
|
135
|
+
next(err);
|
|
136
|
+
});
|
|
137
|
+
|
|
130
138
|
// Security headers
|
|
131
139
|
app.use((req, res, next) => {
|
|
132
140
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
@@ -1205,6 +1213,65 @@ app.delete('/api/clipboard', checkPin, (req, res) => {
|
|
|
1205
1213
|
res.json({ success: true });
|
|
1206
1214
|
});
|
|
1207
1215
|
|
|
1216
|
+
// ── Command history (server-side, persists across sessions) ────────────
|
|
1217
|
+
const HISTORY_FILE = path.join(__dirname, '.cmdhist.json');
|
|
1218
|
+
let cmdHistory = [];
|
|
1219
|
+
let cmdHistMax = 50;
|
|
1220
|
+
|
|
1221
|
+
function loadCmdHistory() {
|
|
1222
|
+
try { cmdHistory = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8')); } catch { cmdHistory = []; }
|
|
1223
|
+
}
|
|
1224
|
+
function saveCmdHistory() {
|
|
1225
|
+
try { fs.writeFileSync(HISTORY_FILE, JSON.stringify(cmdHistory)); } catch {}
|
|
1226
|
+
}
|
|
1227
|
+
loadCmdHistory();
|
|
1228
|
+
|
|
1229
|
+
app.get('/api/history', checkPin, (req, res) => {
|
|
1230
|
+
res.json({ history: cmdHistory, max: cmdHistMax });
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
app.post('/api/history', checkPin, (req, res) => {
|
|
1234
|
+
try {
|
|
1235
|
+
const { cmd, max } = req.body;
|
|
1236
|
+
if (typeof max === 'number' && max >= 10 && max <= 500) {
|
|
1237
|
+
cmdHistMax = max;
|
|
1238
|
+
}
|
|
1239
|
+
if (!cmd || typeof cmd !== 'string' || !cmd.trim()) {
|
|
1240
|
+
// No command — just updating max
|
|
1241
|
+
saveCmdHistory();
|
|
1242
|
+
return res.json({ success: true, history: cmdHistory, max: cmdHistMax });
|
|
1243
|
+
}
|
|
1244
|
+
const clean = cmd.trim();
|
|
1245
|
+
if (cmdHistory.length && cmdHistory[0].cmd === clean) {
|
|
1246
|
+
cmdHistory[0].time = Date.now();
|
|
1247
|
+
cmdHistory[0].count = (cmdHistory[0].count || 1) + 1;
|
|
1248
|
+
} else {
|
|
1249
|
+
cmdHistory.unshift({ cmd: clean, time: Date.now(), count: 1 });
|
|
1250
|
+
}
|
|
1251
|
+
if (cmdHistory.length > cmdHistMax) cmdHistory.length = cmdHistMax;
|
|
1252
|
+
saveCmdHistory();
|
|
1253
|
+
res.json({ success: true, history: cmdHistory });
|
|
1254
|
+
} catch (e) {
|
|
1255
|
+
res.status(500).json({ error: e.message });
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
app.delete('/api/history', checkPin, (req, res) => {
|
|
1260
|
+
cmdHistory = [];
|
|
1261
|
+
saveCmdHistory();
|
|
1262
|
+
res.json({ success: true });
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
app.delete('/api/history/:index', checkPin, (req, res) => {
|
|
1266
|
+
const idx = parseInt(req.params.index);
|
|
1267
|
+
if (isNaN(idx) || idx < 0 || idx >= cmdHistory.length) {
|
|
1268
|
+
return res.status(400).json({ error: 'invalid index' });
|
|
1269
|
+
}
|
|
1270
|
+
cmdHistory.splice(idx, 1);
|
|
1271
|
+
saveCmdHistory();
|
|
1272
|
+
res.json({ success: true, history: cmdHistory });
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1208
1275
|
// ── Session persistence via tmux ──────────────────────────────────────
|
|
1209
1276
|
const TMUX = (() => { try { return execSync('command -v tmux', { stdio: ['ignore','pipe','ignore'] }).toString().trim(); } catch { return null; } })();
|
|
1210
1277
|
|