@co0ontty/wand 1.0.1 → 1.1.0
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/README.md +7 -1
- package/dist/avatar.d.ts +2 -1
- package/dist/avatar.js +29 -37
- package/dist/process-manager.d.ts +1 -1
- package/dist/process-manager.js +16 -5
- package/dist/pwa.js +6 -2
- package/dist/server.js +155 -3
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +15 -0
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +614 -32
- package/dist/web-ui/content/styles.css +223 -0
- package/dist/web-ui/index.js +2 -2
- package/package.json +1 -1
|
@@ -21,9 +21,12 @@
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
// Auto-reload when a new service worker takes control (e.g. after update)
|
|
24
|
+
// But skip reload during initial page load to avoid breaking initialization
|
|
24
25
|
var reloading = false;
|
|
26
|
+
var pageReady = false;
|
|
27
|
+
setTimeout(function() { pageReady = true; }, 3000);
|
|
25
28
|
navigator.serviceWorker.addEventListener('controllerchange', function() {
|
|
26
|
-
if (reloading) return;
|
|
29
|
+
if (reloading || !pageReady) return;
|
|
27
30
|
reloading = true;
|
|
28
31
|
location.reload();
|
|
29
32
|
});
|
|
@@ -254,6 +257,23 @@
|
|
|
254
257
|
})
|
|
255
258
|
.catch(function() {
|
|
256
259
|
state.loginChecked = true;
|
|
260
|
+
// If offline (no network), show a friendly offline message instead of login
|
|
261
|
+
if (!navigator.onLine) {
|
|
262
|
+
var app = document.getElementById("app");
|
|
263
|
+
if (app) {
|
|
264
|
+
app.innerHTML =
|
|
265
|
+
'<div class="boot-loading">' +
|
|
266
|
+
'<div class="boot-loading-card">' +
|
|
267
|
+
'<div class="boot-loading-text" style="font-size:1.3em;margin-bottom:12px">📡 无法连接到服务器</div>' +
|
|
268
|
+
'<div class="boot-loading-text" style="opacity:0.7;font-size:0.95em">请检查网络连接或确认 Wand 服务正在运行。</div>' +
|
|
269
|
+
'<button onclick="location.reload()" style="margin-top:18px;padding:8px 24px;border-radius:8px;border:1px solid rgba(150,118,85,0.3);background:rgba(255,255,255,0.8);cursor:pointer;font-size:0.95em">重试</button>' +
|
|
270
|
+
'</div>' +
|
|
271
|
+
'</div>';
|
|
272
|
+
}
|
|
273
|
+
// Retry when network comes back
|
|
274
|
+
window.addEventListener('online', function() { location.reload(); }, { once: true });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
257
277
|
render();
|
|
258
278
|
});
|
|
259
279
|
}
|
|
@@ -419,6 +439,9 @@
|
|
|
419
439
|
'<button id="drawer-new-session-button" class="btn btn-primary btn-block"><span>+</span> 新会话</button>' +
|
|
420
440
|
'<div class="sidebar-footer-actions">' +
|
|
421
441
|
'<button id="file-panel-toggle-btn" class="btn btn-ghost btn-sm' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件">📁 文件</button>' +
|
|
442
|
+
'<button id="settings-button" class="btn btn-ghost btn-sm" type="button" title="设置">' +
|
|
443
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg> 设置' +
|
|
444
|
+
'</button>' +
|
|
422
445
|
'<button id="pwa-install-button" class="btn btn-ghost btn-sm hidden" title="安装应用">' +
|
|
423
446
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> 安装' +
|
|
424
447
|
'</button>' +
|
|
@@ -562,23 +585,113 @@
|
|
|
562
585
|
|
|
563
586
|
function renderSettingsModal() {
|
|
564
587
|
return '<section id="settings-modal" class="modal-backdrop hidden">' +
|
|
565
|
-
'<div class="modal">' +
|
|
588
|
+
'<div class="modal settings-modal">' +
|
|
566
589
|
'<div class="modal-header">' +
|
|
567
590
|
'<h2 class="modal-title">设置</h2>' +
|
|
568
591
|
'<button id="close-settings-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
569
592
|
'</div>' +
|
|
570
593
|
'<div class="modal-body">' +
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
'<
|
|
594
|
+
// Tabs
|
|
595
|
+
'<div class="settings-tabs">' +
|
|
596
|
+
'<button class="settings-tab active" data-tab="about">关于</button>' +
|
|
597
|
+
'<button class="settings-tab" data-tab="general">基本配置</button>' +
|
|
598
|
+
'<button class="settings-tab" data-tab="security">安全</button>' +
|
|
599
|
+
'<button class="settings-tab" data-tab="presets">命令预设</button>' +
|
|
574
600
|
'</div>' +
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
601
|
+
|
|
602
|
+
// About tab
|
|
603
|
+
'<div class="settings-panel active" id="settings-tab-about">' +
|
|
604
|
+
'<div class="settings-about-info">' +
|
|
605
|
+
'<div class="settings-about-row"><span class="settings-label">包名</span><span class="settings-value" id="settings-pkg-name">-</span></div>' +
|
|
606
|
+
'<div class="settings-about-row"><span class="settings-label">当前版本</span><span class="settings-value" id="settings-version">-</span></div>' +
|
|
607
|
+
'<div class="settings-about-row"><span class="settings-label">Node.js 要求</span><span class="settings-value" id="settings-node-req">-</span></div>' +
|
|
608
|
+
'<div class="settings-about-row"><span class="settings-label">仓库地址</span><span class="settings-value" id="settings-repo-url"><a href="#" target="_blank" rel="noopener">-</a></span></div>' +
|
|
609
|
+
'</div>' +
|
|
610
|
+
'<div class="settings-update-section">' +
|
|
611
|
+
'<div class="settings-about-row">' +
|
|
612
|
+
'<span class="settings-label">最新版本</span>' +
|
|
613
|
+
'<span class="settings-value" id="settings-latest-version">-</span>' +
|
|
614
|
+
'</div>' +
|
|
615
|
+
'<div class="settings-update-actions">' +
|
|
616
|
+
'<button id="check-update-button" class="btn btn-ghost btn-sm">检查更新</button>' +
|
|
617
|
+
'<button id="do-update-button" class="btn btn-primary btn-sm hidden">更新到最新版</button>' +
|
|
618
|
+
'</div>' +
|
|
619
|
+
'<p id="update-message" class="hint hidden"></p>' +
|
|
620
|
+
'</div>' +
|
|
621
|
+
'</div>' +
|
|
622
|
+
|
|
623
|
+
// General config tab
|
|
624
|
+
'<div class="settings-panel" id="settings-tab-general">' +
|
|
625
|
+
'<div class="field">' +
|
|
626
|
+
'<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
|
|
627
|
+
'<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
|
|
628
|
+
'</div>' +
|
|
629
|
+
'<div class="field">' +
|
|
630
|
+
'<label class="field-label" for="cfg-port">端口 (port)</label>' +
|
|
631
|
+
'<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
|
|
632
|
+
'</div>' +
|
|
633
|
+
'<div class="field field-inline">' +
|
|
634
|
+
'<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
|
|
635
|
+
'<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
|
|
636
|
+
'</div>' +
|
|
637
|
+
'<div class="field">' +
|
|
638
|
+
'<label class="field-label" for="cfg-mode">默认执行模式</label>' +
|
|
639
|
+
'<select id="cfg-mode" class="field-input">' +
|
|
640
|
+
'<option value="default">default</option>' +
|
|
641
|
+
'<option value="assist">assist</option>' +
|
|
642
|
+
'<option value="agent">agent</option>' +
|
|
643
|
+
'<option value="agent-max">agent-max</option>' +
|
|
644
|
+
'<option value="auto-edit">auto-edit</option>' +
|
|
645
|
+
'<option value="full-access">full-access</option>' +
|
|
646
|
+
'<option value="native">native</option>' +
|
|
647
|
+
'<option value="managed">managed</option>' +
|
|
648
|
+
'</select>' +
|
|
649
|
+
'</div>' +
|
|
650
|
+
'<div class="field">' +
|
|
651
|
+
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
652
|
+
'<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
|
|
653
|
+
'</div>' +
|
|
654
|
+
'<div class="field">' +
|
|
655
|
+
'<label class="field-label" for="cfg-shell">Shell</label>' +
|
|
656
|
+
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
657
|
+
'</div>' +
|
|
658
|
+
'<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
|
|
659
|
+
'<p id="config-message" class="hint hidden"></p>' +
|
|
660
|
+
'</div>' +
|
|
661
|
+
|
|
662
|
+
// Security tab
|
|
663
|
+
'<div class="settings-panel" id="settings-tab-security">' +
|
|
664
|
+
'<h3 class="settings-section-title">修改密码</h3>' +
|
|
665
|
+
'<div class="field">' +
|
|
666
|
+
'<label class="field-label" for="new-password">新密码</label>' +
|
|
667
|
+
'<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
|
|
668
|
+
'</div>' +
|
|
669
|
+
'<div class="field">' +
|
|
670
|
+
'<label class="field-label" for="confirm-password">确认密码</label>' +
|
|
671
|
+
'<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
|
|
672
|
+
'</div>' +
|
|
673
|
+
'<button id="save-password-button" class="btn btn-primary btn-block">保存密码</button>' +
|
|
674
|
+
'<p id="settings-error" class="error-message hidden"></p>' +
|
|
675
|
+
'<p id="settings-success" class="hint hidden" style="color: var(--success);"></p>' +
|
|
676
|
+
'<hr class="settings-divider" />' +
|
|
677
|
+
'<h3 class="settings-section-title">SSL 证书</h3>' +
|
|
678
|
+
'<p class="settings-hint" id="cert-status">加载中...</p>' +
|
|
679
|
+
'<div class="field">' +
|
|
680
|
+
'<label class="field-label" for="cert-key-file">私钥文件 (.key)</label>' +
|
|
681
|
+
'<input id="cert-key-file" type="file" class="field-input field-file" accept=".key,.pem" />' +
|
|
682
|
+
'</div>' +
|
|
683
|
+
'<div class="field">' +
|
|
684
|
+
'<label class="field-label" for="cert-cert-file">证书文件 (.crt/.pem)</label>' +
|
|
685
|
+
'<input id="cert-cert-file" type="file" class="field-input field-file" accept=".crt,.pem,.cert" />' +
|
|
686
|
+
'</div>' +
|
|
687
|
+
'<button id="upload-cert-button" class="btn btn-primary btn-block">上传证书</button>' +
|
|
688
|
+
'<p id="cert-message" class="hint hidden"></p>' +
|
|
689
|
+
'</div>' +
|
|
690
|
+
|
|
691
|
+
// Command presets tab
|
|
692
|
+
'<div class="settings-panel" id="settings-tab-presets">' +
|
|
693
|
+
'<div id="presets-list" class="presets-list"></div>' +
|
|
578
694
|
'</div>' +
|
|
579
|
-
'<button id="save-password-button" class="btn btn-primary btn-block">保存密码</button>' +
|
|
580
|
-
'<p id="settings-error" class="error-message hidden"></p>' +
|
|
581
|
-
'<p id="settings-success" class="hint hidden" style="color: var(--success);"></p>' +
|
|
582
695
|
'</div>' +
|
|
583
696
|
'</div>' +
|
|
584
697
|
'</section>';
|
|
@@ -1748,6 +1861,21 @@
|
|
|
1748
1861
|
});
|
|
1749
1862
|
var savePassBtn = document.getElementById("save-password-button");
|
|
1750
1863
|
if (savePassBtn) savePassBtn.addEventListener("click", savePassword);
|
|
1864
|
+
// Settings tab clicks
|
|
1865
|
+
var settingsTabs = document.querySelectorAll(".settings-tab");
|
|
1866
|
+
for (var ti = 0; ti < settingsTabs.length; ti++) {
|
|
1867
|
+
settingsTabs[ti].addEventListener("click", function(e) {
|
|
1868
|
+
switchSettingsTab(e.target.getAttribute("data-tab"));
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
var saveConfigBtn = document.getElementById("save-config-button");
|
|
1872
|
+
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
1873
|
+
var uploadCertBtn = document.getElementById("upload-cert-button");
|
|
1874
|
+
if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
|
|
1875
|
+
var checkUpdateBtn = document.getElementById("check-update-button");
|
|
1876
|
+
if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
|
|
1877
|
+
var doUpdateBtn = document.getElementById("do-update-button");
|
|
1878
|
+
if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
|
|
1751
1879
|
var newSessBtn = document.getElementById("topbar-new-session-button");
|
|
1752
1880
|
if (newSessBtn) newSessBtn.addEventListener("click", openSessionModal);
|
|
1753
1881
|
var drawerNewSessBtn = document.getElementById("drawer-new-session-button");
|
|
@@ -2508,6 +2636,186 @@
|
|
|
2508
2636
|
updateTerminalJumpToBottomButton();
|
|
2509
2637
|
}
|
|
2510
2638
|
|
|
2639
|
+
// ===== Custom terminal scrollbar =====
|
|
2640
|
+
function initTerminalScrollbar(container) {
|
|
2641
|
+
var scrollbar = document.createElement("div");
|
|
2642
|
+
scrollbar.className = "terminal-scrollbar";
|
|
2643
|
+
var track = document.createElement("div");
|
|
2644
|
+
track.className = "terminal-scrollbar-track";
|
|
2645
|
+
var thumb = document.createElement("div");
|
|
2646
|
+
thumb.className = "terminal-scrollbar-thumb";
|
|
2647
|
+
track.appendChild(thumb);
|
|
2648
|
+
scrollbar.appendChild(track);
|
|
2649
|
+
container.appendChild(scrollbar);
|
|
2650
|
+
|
|
2651
|
+
state.terminalScrollbarEl = scrollbar;
|
|
2652
|
+
state.terminalScrollbarThumbEl = thumb;
|
|
2653
|
+
state.terminalScrollbarHideTimer = null;
|
|
2654
|
+
state.terminalScrollbarDragging = false;
|
|
2655
|
+
state.terminalScrollbarRafPending = false;
|
|
2656
|
+
|
|
2657
|
+
// Show/hide logic
|
|
2658
|
+
function showScrollbar() {
|
|
2659
|
+
if (state.terminalScrollbarHideTimer) {
|
|
2660
|
+
clearTimeout(state.terminalScrollbarHideTimer);
|
|
2661
|
+
state.terminalScrollbarHideTimer = null;
|
|
2662
|
+
}
|
|
2663
|
+
scrollbar.classList.add("visible");
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
function scheduleHideScrollbar() {
|
|
2667
|
+
if (state.terminalScrollbarDragging) return;
|
|
2668
|
+
if (state.terminalScrollbarHideTimer) clearTimeout(state.terminalScrollbarHideTimer);
|
|
2669
|
+
state.terminalScrollbarHideTimer = setTimeout(function() {
|
|
2670
|
+
state.terminalScrollbarHideTimer = null;
|
|
2671
|
+
if (!state.terminalScrollbarDragging) {
|
|
2672
|
+
scrollbar.classList.remove("visible");
|
|
2673
|
+
}
|
|
2674
|
+
}, 1500);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// Sync thumb position/size from viewport
|
|
2678
|
+
function syncScrollbarThumb() {
|
|
2679
|
+
state.terminalScrollbarRafPending = false;
|
|
2680
|
+
var viewport = getTerminalViewport();
|
|
2681
|
+
if (!viewport) return;
|
|
2682
|
+
var sh = viewport.scrollHeight;
|
|
2683
|
+
var ch = viewport.clientHeight;
|
|
2684
|
+
if (sh <= ch) {
|
|
2685
|
+
scrollbar.classList.remove("visible");
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
var trackH = track.clientHeight;
|
|
2689
|
+
var thumbH = Math.max(28, (ch / sh) * trackH);
|
|
2690
|
+
var maxScroll = sh - ch;
|
|
2691
|
+
var scrollRatio = viewport.scrollTop / maxScroll;
|
|
2692
|
+
var thumbTop = scrollRatio * (trackH - thumbH);
|
|
2693
|
+
thumb.style.height = thumbH + "px";
|
|
2694
|
+
thumb.style.top = thumbTop + "px";
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
function requestSyncScrollbar() {
|
|
2698
|
+
if (state.terminalScrollbarRafPending) return;
|
|
2699
|
+
state.terminalScrollbarRafPending = true;
|
|
2700
|
+
requestAnimationFrame(syncScrollbarThumb);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// Listen to viewport scroll
|
|
2704
|
+
var viewport = getTerminalViewport();
|
|
2705
|
+
if (viewport) {
|
|
2706
|
+
viewport.addEventListener("scroll", function() {
|
|
2707
|
+
showScrollbar();
|
|
2708
|
+
requestSyncScrollbar();
|
|
2709
|
+
scheduleHideScrollbar();
|
|
2710
|
+
}, { passive: true });
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
// Track click → jump to position
|
|
2714
|
+
track.addEventListener("mousedown", function(e) {
|
|
2715
|
+
if (e.target === thumb) return;
|
|
2716
|
+
e.preventDefault();
|
|
2717
|
+
var viewport = getTerminalViewport();
|
|
2718
|
+
if (!viewport) return;
|
|
2719
|
+
var rect = track.getBoundingClientRect();
|
|
2720
|
+
var clickRatio = (e.clientY - rect.top) / rect.height;
|
|
2721
|
+
var maxScroll = viewport.scrollHeight - viewport.clientHeight;
|
|
2722
|
+
viewport.scrollTop = clickRatio * maxScroll;
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2725
|
+
// Thumb drag — mouse
|
|
2726
|
+
var dragStartY = 0;
|
|
2727
|
+
var dragStartScrollTop = 0;
|
|
2728
|
+
|
|
2729
|
+
thumb.addEventListener("mousedown", function(e) {
|
|
2730
|
+
e.preventDefault();
|
|
2731
|
+
e.stopPropagation();
|
|
2732
|
+
state.terminalScrollbarDragging = true;
|
|
2733
|
+
thumb.classList.add("dragging");
|
|
2734
|
+
dragStartY = e.clientY;
|
|
2735
|
+
var viewport = getTerminalViewport();
|
|
2736
|
+
dragStartScrollTop = viewport ? viewport.scrollTop : 0;
|
|
2737
|
+
document.addEventListener("mousemove", onDragMove);
|
|
2738
|
+
document.addEventListener("mouseup", onDragEnd);
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2741
|
+
function onDragMove(e) {
|
|
2742
|
+
e.preventDefault();
|
|
2743
|
+
var viewport = getTerminalViewport();
|
|
2744
|
+
if (!viewport) return;
|
|
2745
|
+
var trackH = track.clientHeight;
|
|
2746
|
+
var sh = viewport.scrollHeight;
|
|
2747
|
+
var ch = viewport.clientHeight;
|
|
2748
|
+
var maxScroll = sh - ch;
|
|
2749
|
+
if (maxScroll <= 0) return;
|
|
2750
|
+
var thumbH = Math.max(28, (ch / sh) * trackH);
|
|
2751
|
+
var scrollableTrack = trackH - thumbH;
|
|
2752
|
+
if (scrollableTrack <= 0) return;
|
|
2753
|
+
var deltaY = e.clientY - dragStartY;
|
|
2754
|
+
var scrollDelta = (deltaY / scrollableTrack) * maxScroll;
|
|
2755
|
+
viewport.scrollTop = dragStartScrollTop + scrollDelta;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function onDragEnd() {
|
|
2759
|
+
state.terminalScrollbarDragging = false;
|
|
2760
|
+
thumb.classList.remove("dragging");
|
|
2761
|
+
document.removeEventListener("mousemove", onDragMove);
|
|
2762
|
+
document.removeEventListener("mouseup", onDragEnd);
|
|
2763
|
+
scheduleHideScrollbar();
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// Thumb drag — touch
|
|
2767
|
+
thumb.addEventListener("touchstart", function(e) {
|
|
2768
|
+
if (e.touches.length !== 1) return;
|
|
2769
|
+
e.stopPropagation();
|
|
2770
|
+
state.terminalScrollbarDragging = true;
|
|
2771
|
+
thumb.classList.add("dragging");
|
|
2772
|
+
dragStartY = e.touches[0].clientY;
|
|
2773
|
+
var viewport = getTerminalViewport();
|
|
2774
|
+
dragStartScrollTop = viewport ? viewport.scrollTop : 0;
|
|
2775
|
+
document.addEventListener("touchmove", onTouchDragMove, { passive: false });
|
|
2776
|
+
document.addEventListener("touchend", onTouchDragEnd);
|
|
2777
|
+
document.addEventListener("touchcancel", onTouchDragEnd);
|
|
2778
|
+
}, { passive: false });
|
|
2779
|
+
|
|
2780
|
+
function onTouchDragMove(e) {
|
|
2781
|
+
if (e.touches.length !== 1) return;
|
|
2782
|
+
e.preventDefault();
|
|
2783
|
+
var viewport = getTerminalViewport();
|
|
2784
|
+
if (!viewport) return;
|
|
2785
|
+
var trackH = track.clientHeight;
|
|
2786
|
+
var sh = viewport.scrollHeight;
|
|
2787
|
+
var ch = viewport.clientHeight;
|
|
2788
|
+
var maxScroll = sh - ch;
|
|
2789
|
+
if (maxScroll <= 0) return;
|
|
2790
|
+
var thumbH = Math.max(28, (ch / sh) * trackH);
|
|
2791
|
+
var scrollableTrack = trackH - thumbH;
|
|
2792
|
+
if (scrollableTrack <= 0) return;
|
|
2793
|
+
var deltaY = e.touches[0].clientY - dragStartY;
|
|
2794
|
+
var scrollDelta = (deltaY / scrollableTrack) * maxScroll;
|
|
2795
|
+
viewport.scrollTop = dragStartScrollTop + scrollDelta;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function onTouchDragEnd() {
|
|
2799
|
+
state.terminalScrollbarDragging = false;
|
|
2800
|
+
thumb.classList.remove("dragging");
|
|
2801
|
+
document.removeEventListener("touchmove", onTouchDragMove);
|
|
2802
|
+
document.removeEventListener("touchend", onTouchDragEnd);
|
|
2803
|
+
document.removeEventListener("touchcancel", onTouchDragEnd);
|
|
2804
|
+
scheduleHideScrollbar();
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// Hover on scrollbar area shows it
|
|
2808
|
+
scrollbar.addEventListener("mouseenter", function() {
|
|
2809
|
+
showScrollbar();
|
|
2810
|
+
});
|
|
2811
|
+
scrollbar.addEventListener("mouseleave", function() {
|
|
2812
|
+
if (!state.terminalScrollbarDragging) scheduleHideScrollbar();
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
// Initial sync
|
|
2816
|
+
requestSyncScrollbar();
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2511
2819
|
function syncTerminalBuffer(sessionId, output, options) {
|
|
2512
2820
|
if (!state.terminal) return false;
|
|
2513
2821
|
var normalizedOutput = normalizeTerminalOutput(output || "");
|
|
@@ -2590,9 +2898,15 @@
|
|
|
2590
2898
|
var container = document.getElementById("output");
|
|
2591
2899
|
if (!container || state.terminal) return;
|
|
2592
2900
|
if (typeof Terminal === "undefined") {
|
|
2593
|
-
// xterm.js
|
|
2901
|
+
// xterm.js not yet loaded — retry after a short delay
|
|
2902
|
+
if (!state.terminalInitRetries) state.terminalInitRetries = 0;
|
|
2903
|
+
if (state.terminalInitRetries < 10) {
|
|
2904
|
+
state.terminalInitRetries++;
|
|
2905
|
+
setTimeout(initTerminal, 200);
|
|
2906
|
+
}
|
|
2594
2907
|
return;
|
|
2595
2908
|
}
|
|
2909
|
+
state.terminalInitRetries = 0;
|
|
2596
2910
|
|
|
2597
2911
|
state.terminal = new Terminal({
|
|
2598
2912
|
cols: 120,
|
|
@@ -2682,6 +2996,9 @@
|
|
|
2682
2996
|
e.stopPropagation();
|
|
2683
2997
|
}, { passive: true });
|
|
2684
2998
|
|
|
2999
|
+
// Create custom scrollbar overlay
|
|
3000
|
+
initTerminalScrollbar(container);
|
|
3001
|
+
|
|
2685
3002
|
if (state.selectedId) {
|
|
2686
3003
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
2687
3004
|
if (session) {
|
|
@@ -2974,6 +3291,9 @@
|
|
|
2974
3291
|
}
|
|
2975
3292
|
}
|
|
2976
3293
|
updateShellChrome();
|
|
3294
|
+
})
|
|
3295
|
+
.catch(function(e) {
|
|
3296
|
+
console.error("[wand] loadSessions failed:", e);
|
|
2977
3297
|
});
|
|
2978
3298
|
}
|
|
2979
3299
|
|
|
@@ -3231,10 +3551,16 @@
|
|
|
3231
3551
|
if (modal) {
|
|
3232
3552
|
modal.classList.remove("hidden");
|
|
3233
3553
|
lastFocusedElement = document.activeElement;
|
|
3234
|
-
document.getElementById("new-password")
|
|
3235
|
-
document.getElementById("confirm-password")
|
|
3554
|
+
var passEl = document.getElementById("new-password");
|
|
3555
|
+
var confirmEl = document.getElementById("confirm-password");
|
|
3556
|
+
if (passEl) passEl.value = "";
|
|
3557
|
+
if (confirmEl) confirmEl.value = "";
|
|
3236
3558
|
hideSettingsMessages();
|
|
3237
3559
|
setupFocusTrap(modal);
|
|
3560
|
+
// Activate first tab
|
|
3561
|
+
switchSettingsTab("about");
|
|
3562
|
+
// Load settings data
|
|
3563
|
+
loadSettingsData();
|
|
3238
3564
|
}
|
|
3239
3565
|
}
|
|
3240
3566
|
|
|
@@ -3305,6 +3631,247 @@
|
|
|
3305
3631
|
});
|
|
3306
3632
|
}
|
|
3307
3633
|
|
|
3634
|
+
// ── Settings tab/panel logic ──
|
|
3635
|
+
|
|
3636
|
+
function switchSettingsTab(tabName) {
|
|
3637
|
+
var tabs = document.querySelectorAll(".settings-tab");
|
|
3638
|
+
var panels = document.querySelectorAll(".settings-panel");
|
|
3639
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
3640
|
+
if (tabs[i].getAttribute("data-tab") === tabName) {
|
|
3641
|
+
tabs[i].classList.add("active");
|
|
3642
|
+
} else {
|
|
3643
|
+
tabs[i].classList.remove("active");
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
for (var j = 0; j < panels.length; j++) {
|
|
3647
|
+
if (panels[j].id === "settings-tab-" + tabName) {
|
|
3648
|
+
panels[j].classList.add("active");
|
|
3649
|
+
} else {
|
|
3650
|
+
panels[j].classList.remove("active");
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
function loadSettingsData() {
|
|
3656
|
+
fetch("/api/settings", { credentials: "same-origin" })
|
|
3657
|
+
.then(function(res) { return res.json(); })
|
|
3658
|
+
.then(function(data) {
|
|
3659
|
+
// About
|
|
3660
|
+
var nameEl = document.getElementById("settings-pkg-name");
|
|
3661
|
+
var verEl = document.getElementById("settings-version");
|
|
3662
|
+
var nodeEl = document.getElementById("settings-node-req");
|
|
3663
|
+
var repoEl = document.getElementById("settings-repo-url");
|
|
3664
|
+
if (nameEl) nameEl.textContent = data.packageName || "-";
|
|
3665
|
+
if (verEl) verEl.textContent = data.version || "-";
|
|
3666
|
+
if (nodeEl) nodeEl.textContent = data.nodeVersion || "-";
|
|
3667
|
+
if (repoEl && data.repoUrl) {
|
|
3668
|
+
repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// Config fields
|
|
3672
|
+
var cfg = data.config || {};
|
|
3673
|
+
var hostEl = document.getElementById("cfg-host");
|
|
3674
|
+
var portEl = document.getElementById("cfg-port");
|
|
3675
|
+
var httpsEl = document.getElementById("cfg-https");
|
|
3676
|
+
var modeEl = document.getElementById("cfg-mode");
|
|
3677
|
+
var cwdEl = document.getElementById("cfg-cwd");
|
|
3678
|
+
var shellEl = document.getElementById("cfg-shell");
|
|
3679
|
+
if (hostEl) hostEl.value = cfg.host || "";
|
|
3680
|
+
if (portEl) portEl.value = cfg.port || "";
|
|
3681
|
+
if (httpsEl) httpsEl.checked = cfg.https === true;
|
|
3682
|
+
if (modeEl) modeEl.value = cfg.defaultMode || "default";
|
|
3683
|
+
if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
|
|
3684
|
+
if (shellEl) shellEl.value = cfg.shell || "";
|
|
3685
|
+
|
|
3686
|
+
// Cert status
|
|
3687
|
+
var certStatus = document.getElementById("cert-status");
|
|
3688
|
+
if (certStatus) {
|
|
3689
|
+
certStatus.textContent = data.hasCert ? "已安装 SSL 证书" : "未安装证书(使用自签名或 HTTP)";
|
|
3690
|
+
certStatus.style.color = data.hasCert ? "var(--success)" : "var(--text-secondary)";
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// Presets
|
|
3694
|
+
var presetsList = document.getElementById("presets-list");
|
|
3695
|
+
if (presetsList && cfg.commandPresets) {
|
|
3696
|
+
var html = "";
|
|
3697
|
+
for (var i = 0; i < cfg.commandPresets.length; i++) {
|
|
3698
|
+
var p = cfg.commandPresets[i];
|
|
3699
|
+
html += '<div class="preset-item">' +
|
|
3700
|
+
'<span class="preset-label">' + escapeHtml(p.label) + '</span>' +
|
|
3701
|
+
'<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
|
|
3702
|
+
'</div>';
|
|
3703
|
+
}
|
|
3704
|
+
if (!html) html = '<p class="settings-hint">没有命令预设。</p>';
|
|
3705
|
+
presetsList.innerHTML = html;
|
|
3706
|
+
}
|
|
3707
|
+
})
|
|
3708
|
+
.catch(function() {});
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
function saveConfigSettings() {
|
|
3712
|
+
var msgEl = document.getElementById("config-message");
|
|
3713
|
+
if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
|
|
3714
|
+
|
|
3715
|
+
var body = {
|
|
3716
|
+
host: (document.getElementById("cfg-host") || {}).value,
|
|
3717
|
+
port: Number((document.getElementById("cfg-port") || {}).value),
|
|
3718
|
+
https: (document.getElementById("cfg-https") || {}).checked,
|
|
3719
|
+
defaultMode: (document.getElementById("cfg-mode") || {}).value,
|
|
3720
|
+
defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
|
|
3721
|
+
shell: (document.getElementById("cfg-shell") || {}).value,
|
|
3722
|
+
};
|
|
3723
|
+
|
|
3724
|
+
fetch("/api/settings/config", {
|
|
3725
|
+
method: "POST",
|
|
3726
|
+
headers: { "Content-Type": "application/json" },
|
|
3727
|
+
credentials: "same-origin",
|
|
3728
|
+
body: JSON.stringify(body)
|
|
3729
|
+
})
|
|
3730
|
+
.then(function(res) { return res.json(); })
|
|
3731
|
+
.then(function(data) {
|
|
3732
|
+
if (msgEl) {
|
|
3733
|
+
if (data.error) {
|
|
3734
|
+
msgEl.textContent = data.error;
|
|
3735
|
+
msgEl.style.color = "var(--error)";
|
|
3736
|
+
} else {
|
|
3737
|
+
msgEl.textContent = "配置已保存,部分更改需要重启后生效。";
|
|
3738
|
+
msgEl.style.color = "var(--success)";
|
|
3739
|
+
}
|
|
3740
|
+
msgEl.classList.remove("hidden");
|
|
3741
|
+
}
|
|
3742
|
+
})
|
|
3743
|
+
.catch(function() {
|
|
3744
|
+
if (msgEl) {
|
|
3745
|
+
msgEl.textContent = "保存失败。";
|
|
3746
|
+
msgEl.style.color = "var(--error)";
|
|
3747
|
+
msgEl.classList.remove("hidden");
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
function uploadCertificates() {
|
|
3753
|
+
var keyFile = document.getElementById("cert-key-file");
|
|
3754
|
+
var certFile = document.getElementById("cert-cert-file");
|
|
3755
|
+
var msgEl = document.getElementById("cert-message");
|
|
3756
|
+
if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
|
|
3757
|
+
|
|
3758
|
+
if (!keyFile || !keyFile.files[0] || !certFile || !certFile.files[0]) {
|
|
3759
|
+
if (msgEl) {
|
|
3760
|
+
msgEl.textContent = "请选择私钥和证书文件。";
|
|
3761
|
+
msgEl.style.color = "var(--error)";
|
|
3762
|
+
msgEl.classList.remove("hidden");
|
|
3763
|
+
}
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
var keyReader = new FileReader();
|
|
3768
|
+
keyReader.onload = function() {
|
|
3769
|
+
var keyContent = keyReader.result;
|
|
3770
|
+
var certReader = new FileReader();
|
|
3771
|
+
certReader.onload = function() {
|
|
3772
|
+
var certContent = certReader.result;
|
|
3773
|
+
fetch("/api/settings/upload-cert", {
|
|
3774
|
+
method: "POST",
|
|
3775
|
+
headers: { "Content-Type": "application/json" },
|
|
3776
|
+
credentials: "same-origin",
|
|
3777
|
+
body: JSON.stringify({ key: keyContent, cert: certContent })
|
|
3778
|
+
})
|
|
3779
|
+
.then(function(res) { return res.json(); })
|
|
3780
|
+
.then(function(data) {
|
|
3781
|
+
if (msgEl) {
|
|
3782
|
+
if (data.error) {
|
|
3783
|
+
msgEl.textContent = data.error;
|
|
3784
|
+
msgEl.style.color = "var(--error)";
|
|
3785
|
+
} else {
|
|
3786
|
+
msgEl.textContent = "证书已上传,重启后生效。";
|
|
3787
|
+
msgEl.style.color = "var(--success)";
|
|
3788
|
+
// Update cert status
|
|
3789
|
+
var certStatus = document.getElementById("cert-status");
|
|
3790
|
+
if (certStatus) {
|
|
3791
|
+
certStatus.textContent = "已安装 SSL 证书";
|
|
3792
|
+
certStatus.style.color = "var(--success)";
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
msgEl.classList.remove("hidden");
|
|
3796
|
+
}
|
|
3797
|
+
})
|
|
3798
|
+
.catch(function() {
|
|
3799
|
+
if (msgEl) {
|
|
3800
|
+
msgEl.textContent = "上传失败。";
|
|
3801
|
+
msgEl.style.color = "var(--error)";
|
|
3802
|
+
msgEl.classList.remove("hidden");
|
|
3803
|
+
}
|
|
3804
|
+
});
|
|
3805
|
+
};
|
|
3806
|
+
certReader.readAsText(certFile.files[0]);
|
|
3807
|
+
};
|
|
3808
|
+
keyReader.readAsText(keyFile.files[0]);
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
function checkForUpdate() {
|
|
3812
|
+
var latestEl = document.getElementById("settings-latest-version");
|
|
3813
|
+
var updateBtn = document.getElementById("do-update-button");
|
|
3814
|
+
var msgEl = document.getElementById("update-message");
|
|
3815
|
+
if (latestEl) latestEl.textContent = "检查中...";
|
|
3816
|
+
if (msgEl) msgEl.classList.add("hidden");
|
|
3817
|
+
if (updateBtn) updateBtn.classList.add("hidden");
|
|
3818
|
+
|
|
3819
|
+
fetch("/api/check-update", { credentials: "same-origin" })
|
|
3820
|
+
.then(function(res) { return res.json(); })
|
|
3821
|
+
.then(function(data) {
|
|
3822
|
+
if (data.error) {
|
|
3823
|
+
if (latestEl) latestEl.textContent = "检查失败";
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
if (latestEl) latestEl.textContent = data.latest;
|
|
3827
|
+
if (data.updateAvailable && updateBtn) {
|
|
3828
|
+
updateBtn.classList.remove("hidden");
|
|
3829
|
+
}
|
|
3830
|
+
if (!data.updateAvailable && msgEl) {
|
|
3831
|
+
msgEl.textContent = "已是最新版本。";
|
|
3832
|
+
msgEl.style.color = "var(--success)";
|
|
3833
|
+
msgEl.classList.remove("hidden");
|
|
3834
|
+
}
|
|
3835
|
+
})
|
|
3836
|
+
.catch(function() {
|
|
3837
|
+
if (latestEl) latestEl.textContent = "检查失败";
|
|
3838
|
+
});
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
function performUpdate() {
|
|
3842
|
+
var msgEl = document.getElementById("update-message");
|
|
3843
|
+
var updateBtn = document.getElementById("do-update-button");
|
|
3844
|
+
if (updateBtn) updateBtn.disabled = true;
|
|
3845
|
+
if (msgEl) {
|
|
3846
|
+
msgEl.textContent = "正在更新,请稍候...";
|
|
3847
|
+
msgEl.style.color = "var(--text-secondary)";
|
|
3848
|
+
msgEl.classList.remove("hidden");
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
fetch("/api/update", {
|
|
3852
|
+
method: "POST",
|
|
3853
|
+
headers: { "Content-Type": "application/json" },
|
|
3854
|
+
credentials: "same-origin"
|
|
3855
|
+
})
|
|
3856
|
+
.then(function(res) { return res.json(); })
|
|
3857
|
+
.then(function(data) {
|
|
3858
|
+
if (msgEl) {
|
|
3859
|
+
msgEl.textContent = data.message || data.error || "更新请求已发送。";
|
|
3860
|
+
msgEl.style.color = data.error ? "var(--error)" : "var(--success)";
|
|
3861
|
+
msgEl.classList.remove("hidden");
|
|
3862
|
+
}
|
|
3863
|
+
if (updateBtn) updateBtn.disabled = false;
|
|
3864
|
+
})
|
|
3865
|
+
.catch(function() {
|
|
3866
|
+
if (msgEl) {
|
|
3867
|
+
msgEl.textContent = "更新失败。";
|
|
3868
|
+
msgEl.style.color = "var(--error)";
|
|
3869
|
+
msgEl.classList.remove("hidden");
|
|
3870
|
+
}
|
|
3871
|
+
if (updateBtn) updateBtn.disabled = false;
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3308
3875
|
function quickStartSession() {
|
|
3309
3876
|
var command = getPreferredTool();
|
|
3310
3877
|
var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
|
|
@@ -3524,7 +4091,7 @@
|
|
|
3524
4091
|
|
|
3525
4092
|
if (event.key === "Escape") {
|
|
3526
4093
|
event.preventDefault();
|
|
3527
|
-
queueDirectInput(getControlInput("escape"));
|
|
4094
|
+
queueDirectInput(getControlInput("escape"), "escape");
|
|
3528
4095
|
return;
|
|
3529
4096
|
}
|
|
3530
4097
|
|
|
@@ -3536,7 +4103,7 @@
|
|
|
3536
4103
|
return; // Let browser handle copy
|
|
3537
4104
|
}
|
|
3538
4105
|
event.preventDefault();
|
|
3539
|
-
queueDirectInput(getControlInput("ctrl_c"));
|
|
4106
|
+
queueDirectInput(getControlInput("ctrl_c"), "ctrl_c");
|
|
3540
4107
|
return;
|
|
3541
4108
|
}
|
|
3542
4109
|
|
|
@@ -3548,13 +4115,13 @@
|
|
|
3548
4115
|
return; // Let browser handle copy
|
|
3549
4116
|
}
|
|
3550
4117
|
event.preventDefault();
|
|
3551
|
-
queueDirectInput(getControlInput("ctrl_d"));
|
|
4118
|
+
queueDirectInput(getControlInput("ctrl_d"), "ctrl_d");
|
|
3552
4119
|
return;
|
|
3553
4120
|
}
|
|
3554
4121
|
|
|
3555
4122
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
|
|
3556
4123
|
event.preventDefault();
|
|
3557
|
-
queueDirectInput(getControlInput("ctrl_l"));
|
|
4124
|
+
queueDirectInput(getControlInput("ctrl_l"), "ctrl_l");
|
|
3558
4125
|
return;
|
|
3559
4126
|
}
|
|
3560
4127
|
|
|
@@ -3580,7 +4147,7 @@
|
|
|
3580
4147
|
}
|
|
3581
4148
|
// No selection: send Ctrl+X to terminal (rare case)
|
|
3582
4149
|
event.preventDefault();
|
|
3583
|
-
queueDirectInput(String.fromCharCode(24)); // Ctrl+X = 0x18
|
|
4150
|
+
queueDirectInput(String.fromCharCode(24), "ctrl_x"); // Ctrl+X = 0x18
|
|
3584
4151
|
return;
|
|
3585
4152
|
}
|
|
3586
4153
|
|
|
@@ -3877,7 +4444,7 @@
|
|
|
3877
4444
|
showToast("会话未就绪,将稍后重试。", "info");
|
|
3878
4445
|
return null;
|
|
3879
4446
|
}
|
|
3880
|
-
return queueDirectInput(combinedInput).then(function() {
|
|
4447
|
+
return queueDirectInput(combinedInput, "enter_text").then(function() {
|
|
3881
4448
|
// Clear input only after the send succeeds
|
|
3882
4449
|
if (inputBox && inputBox.value === value) {
|
|
3883
4450
|
inputBox.value = "";
|
|
@@ -3972,12 +4539,12 @@
|
|
|
3972
4539
|
});
|
|
3973
4540
|
}
|
|
3974
4541
|
|
|
3975
|
-
function queueDirectInput(input) {
|
|
4542
|
+
function queueDirectInput(input, shortcutKey) {
|
|
3976
4543
|
if (!input || !state.selectedId) return Promise.resolve();
|
|
3977
4544
|
state.messageQueue.push(input);
|
|
3978
4545
|
updateQueueCounter();
|
|
3979
4546
|
state.inputQueue = state.inputQueue.then(function() {
|
|
3980
|
-
return postInput(input).finally(function() {
|
|
4547
|
+
return postInput(input, shortcutKey).finally(function() {
|
|
3981
4548
|
var idx = state.messageQueue.indexOf(input);
|
|
3982
4549
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
3983
4550
|
updateQueueCounter();
|
|
@@ -3986,7 +4553,7 @@
|
|
|
3986
4553
|
return state.inputQueue;
|
|
3987
4554
|
}
|
|
3988
4555
|
|
|
3989
|
-
function postInput(input) {
|
|
4556
|
+
function postInput(input, shortcutKey) {
|
|
3990
4557
|
if (!state.selectedId) return Promise.resolve();
|
|
3991
4558
|
|
|
3992
4559
|
// Pre-check: don't send if session is not running
|
|
@@ -4034,7 +4601,7 @@
|
|
|
4034
4601
|
method: "POST",
|
|
4035
4602
|
headers: { "Content-Type": "application/json" },
|
|
4036
4603
|
credentials: "same-origin",
|
|
4037
|
-
body: JSON.stringify({ input: input, view: state.currentView })
|
|
4604
|
+
body: JSON.stringify({ input: input, view: state.currentView, shortcutKey: shortcutKey || undefined })
|
|
4038
4605
|
})
|
|
4039
4606
|
.then(function(res) {
|
|
4040
4607
|
if (!res.ok) {
|
|
@@ -4169,9 +4736,9 @@
|
|
|
4169
4736
|
};
|
|
4170
4737
|
}
|
|
4171
4738
|
|
|
4172
|
-
function sendTerminalSequence(sequence) {
|
|
4739
|
+
function sendTerminalSequence(sequence, shortcutKey) {
|
|
4173
4740
|
if (!sequence) return;
|
|
4174
|
-
queueDirectInput(sequence).catch(function() {});
|
|
4741
|
+
queueDirectInput(sequence, shortcutKey).catch(function() {});
|
|
4175
4742
|
}
|
|
4176
4743
|
|
|
4177
4744
|
function focusTerminalInteractionTarget() {
|
|
@@ -4265,7 +4832,7 @@
|
|
|
4265
4832
|
var mods = getModifierStateFromEvent(event, key);
|
|
4266
4833
|
if (isModifierKey(key)) return;
|
|
4267
4834
|
var sequence = buildPtySequence(key, mods);
|
|
4268
|
-
if (sequence) sendTerminalSequence(sequence);
|
|
4835
|
+
if (sequence) sendTerminalSequence(sequence, key);
|
|
4269
4836
|
}
|
|
4270
4837
|
|
|
4271
4838
|
function handleMiniKeyboardClick(event) {
|
|
@@ -4280,7 +4847,7 @@
|
|
|
4280
4847
|
return;
|
|
4281
4848
|
}
|
|
4282
4849
|
var sequence = buildPtySequence(key, { ctrl: state.modifiers.ctrl, alt: state.modifiers.alt, shift: state.modifiers.shift });
|
|
4283
|
-
if (sequence) sendTerminalSequence(sequence);
|
|
4850
|
+
if (sequence) sendTerminalSequence(sequence, key);
|
|
4284
4851
|
clearModifiers();
|
|
4285
4852
|
}
|
|
4286
4853
|
|
|
@@ -4297,11 +4864,11 @@
|
|
|
4297
4864
|
}
|
|
4298
4865
|
if (key === "ctrl_enter") {
|
|
4299
4866
|
var sequence = buildPtySequence("enter", { ctrl: true, alt: false, shift: false });
|
|
4300
|
-
if (sequence) sendTerminalSequence(sequence);
|
|
4867
|
+
if (sequence) sendTerminalSequence(sequence, "ctrl_enter");
|
|
4301
4868
|
return;
|
|
4302
4869
|
}
|
|
4303
4870
|
var sequence = buildPtySequence(key, { ctrl: state.modifiers.ctrl, alt: state.modifiers.alt, shift: false });
|
|
4304
|
-
if (sequence) sendTerminalSequence(sequence);
|
|
4871
|
+
if (sequence) sendTerminalSequence(sequence, key);
|
|
4305
4872
|
clearModifiers();
|
|
4306
4873
|
updateKeyboardPopupUI();
|
|
4307
4874
|
}
|
|
@@ -4905,6 +5472,11 @@
|
|
|
4905
5472
|
|
|
4906
5473
|
function handleInputBoxBlur() {
|
|
4907
5474
|
resetInputPanelViewportSpacing();
|
|
5475
|
+
// Restore app container height when keyboard closes
|
|
5476
|
+
var appContainer = document.querySelector('.app-container');
|
|
5477
|
+
if (appContainer) {
|
|
5478
|
+
appContainer.style.height = '';
|
|
5479
|
+
}
|
|
4908
5480
|
}
|
|
4909
5481
|
|
|
4910
5482
|
function adjustInputBoxSelection(inputBox) {
|
|
@@ -5610,17 +6182,27 @@
|
|
|
5610
6182
|
if (!('visualViewport' in window)) return;
|
|
5611
6183
|
|
|
5612
6184
|
var vv = window.visualViewport;
|
|
5613
|
-
var inputPanel = document.querySelector('.input-panel');
|
|
5614
6185
|
var lastHeight = vv.height;
|
|
5615
6186
|
var keyboardOpen = false;
|
|
5616
6187
|
|
|
5617
6188
|
function updateViewport() {
|
|
5618
|
-
if (!
|
|
6189
|
+
if (!vv) return;
|
|
5619
6190
|
var inputBox = document.getElementById('input-box');
|
|
5620
6191
|
var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
|
|
5621
6192
|
var isKeyboardOpen = offsetBottom > 50;
|
|
5622
6193
|
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
5623
6194
|
|
|
6195
|
+
// In PWA standalone mode, dynamically resize the app container
|
|
6196
|
+
// because 100dvh does NOT shrink when keyboard appears in standalone PWA
|
|
6197
|
+
var appContainer = document.querySelector('.app-container');
|
|
6198
|
+
if (appContainer) {
|
|
6199
|
+
if (isKeyboardOpen) {
|
|
6200
|
+
appContainer.style.height = vv.height + 'px';
|
|
6201
|
+
} else {
|
|
6202
|
+
appContainer.style.height = '';
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
|
|
5624
6206
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
5625
6207
|
scrollLatestMessageIntoView();
|
|
5626
6208
|
syncInputBoxScroll(inputBox);
|