@co0ontty/wand 1.39.0 → 1.39.1
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/dist/web-ui/content/scripts.js +270 -60
- package/dist/web-ui/content/styles.css +40 -18
- package/package.json +1 -1
|
@@ -1912,7 +1912,7 @@
|
|
|
1912
1912
|
'</div>' +
|
|
1913
1913
|
'<div class="file-search-box">' +
|
|
1914
1914
|
'<span class="file-search-icon">' + wandFileIcon("search", { size: 14 }) + '</span>' +
|
|
1915
|
-
'<input type="text" id="file-search-input" class="file-search-input" placeholder="搜索当前目录…" autocomplete="off" />' +
|
|
1915
|
+
'<input type="text" id="file-search-input" class="file-search-input" placeholder="搜索当前目录…" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />' +
|
|
1916
1916
|
'<button class="file-search-clear" id="file-search-clear" type="button" aria-label="清除搜索" title="清除">' +
|
|
1917
1917
|
wandFileIcon("x", { size: 13 }) +
|
|
1918
1918
|
'</button>' +
|
|
@@ -2095,7 +2095,7 @@
|
|
|
2095
2095
|
'<div id="folder-breadcrumb" class="folder-breadcrumb"></div>' +
|
|
2096
2096
|
'<div class="folder-picker">' +
|
|
2097
2097
|
'<span class="folder-picker-icon">' + iconSvg("folder", { size: 15, strokeWidth: 1.7 }) + '</span>' +
|
|
2098
|
-
'<input type="text" id="folder-picker-input" class="folder-picker-input" value="" placeholder="输入或选择工作目录..." autocomplete="off" />' +
|
|
2098
|
+
'<input type="text" id="folder-picker-input" class="folder-picker-input" value="" placeholder="输入或选择工作目录..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />' +
|
|
2099
2099
|
'</div>' +
|
|
2100
2100
|
'<div id="folder-picker-dropdown" class="folder-picker-dropdown hidden"></div>' +
|
|
2101
2101
|
'<div id="folder-picker-validation" class="folder-picker-validation"></div>' +
|
|
@@ -3538,9 +3538,6 @@
|
|
|
3538
3538
|
|
|
3539
3539
|
function renderCollapsedSessionTiles() {
|
|
3540
3540
|
var entries = getRecentEntries();
|
|
3541
|
-
if (entries.length === 0) {
|
|
3542
|
-
return '<div class="sidebar-collapsed-empty" title="无会话">—</div>';
|
|
3543
|
-
}
|
|
3544
3541
|
var tiles = entries.map(function(e, i) {
|
|
3545
3542
|
var idx = i + 1;
|
|
3546
3543
|
if (e.kind === "session") {
|
|
@@ -3554,7 +3551,11 @@
|
|
|
3554
3551
|
var hTitle = preview + " · " + formatHistoryTime(h.timestamp);
|
|
3555
3552
|
return '<button class="sidebar-collapsed-tile history" type="button" data-collapsed-history-id="' + escapeHtml(h.claudeSessionId) + '" data-cwd="' + escapeHtml(h.cwd || "") + '" title="' + escapeHtml(hTitle) + '">' + idx + '</button>';
|
|
3556
3553
|
}).join("");
|
|
3557
|
-
|
|
3554
|
+
// 窄条底部固定一个「+」快速新建会话方块,替代被隐藏的 footer 新会话入口。
|
|
3555
|
+
var addTile = '<button class="sidebar-collapsed-tile add" type="button" data-collapsed-new-session="1" title="新建会话" aria-label="新建会话">' +
|
|
3556
|
+
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' +
|
|
3557
|
+
'</button>';
|
|
3558
|
+
return '<div class="sidebar-collapsed-tiles">' + tiles + addTile + '</div>';
|
|
3558
3559
|
}
|
|
3559
3560
|
|
|
3560
3561
|
function renderSessionsListContent() {
|
|
@@ -7248,6 +7249,12 @@
|
|
|
7248
7249
|
|
|
7249
7250
|
var collapsedTile = target.closest(".sidebar-collapsed-tile");
|
|
7250
7251
|
if (collapsedTile && collapsedTile instanceof HTMLElement) {
|
|
7252
|
+
if (collapsedTile.dataset.collapsedNewSession) {
|
|
7253
|
+
event.preventDefault();
|
|
7254
|
+
event.stopPropagation();
|
|
7255
|
+
openSessionModal();
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7251
7258
|
if (collapsedTile.dataset.collapsedSessionId) {
|
|
7252
7259
|
event.preventDefault();
|
|
7253
7260
|
event.stopPropagation();
|
|
@@ -13941,24 +13948,24 @@
|
|
|
13941
13948
|
{ key: "down", label: "↓" },
|
|
13942
13949
|
{ key: "left", label: "←" }
|
|
13943
13950
|
];
|
|
13944
|
-
//
|
|
13951
|
+
// 外圈功能键:N 个均分扇区,i=0 正上方起顺时针。
|
|
13952
|
+
// 数组长度即扇区数 —— buildJoystickRingSvg / joystickHitTest 都是按
|
|
13953
|
+
// OUTER_KEYS.length 动态算角度,所以这里随便加减都不需要改几何。
|
|
13954
|
+
// 当前 4 键: 上=Enter 右=Ctrl+C 下=Esc 左=Shift+Tab。
|
|
13955
|
+
// 选这 4 个的原因: Claude / Codex 交互里只有方向键 + Enter + Esc +
|
|
13956
|
+
// Shift+Tab (back-tab) + Ctrl+C (abort) 有真实用途, 之前的 Tab /
|
|
13957
|
+
// Ctrl+Z/D/L 在结构化 / chat / PTY claude 里都拿不到效果, 留着只是
|
|
13958
|
+
// 占位 + 误点。
|
|
13945
13959
|
var JOYSTICK_OUTER_KEYS = [
|
|
13946
13960
|
{ key: "enter", label: "Enter" },
|
|
13947
|
-
{ key: "escape", label: "Esc" },
|
|
13948
|
-
{ key: "tab", label: "Tab" },
|
|
13949
|
-
{ key: "shift_tab", label: "Shift+Tab" },
|
|
13950
13961
|
{ key: "ctrl_c", label: "Ctrl+C" },
|
|
13951
|
-
{ key: "
|
|
13952
|
-
{ key: "
|
|
13953
|
-
{ key: "ctrl_l", label: "Ctrl+L" }
|
|
13954
|
-
];
|
|
13955
|
-
// 钉住面板四角翻页键
|
|
13956
|
-
var JOYSTICK_CORNER_KEYS = [
|
|
13957
|
-
{ key: "pageup", label: "PgUp" },
|
|
13958
|
-
{ key: "home", label: "Home" },
|
|
13959
|
-
{ key: "pagedown", label: "PgDn" },
|
|
13960
|
-
{ key: "end", label: "End" }
|
|
13962
|
+
{ key: "escape", label: "Esc" },
|
|
13963
|
+
{ key: "shift_tab", label: "Shift+Tab" }
|
|
13961
13964
|
];
|
|
13965
|
+
// 钉住面板四角翻页键 —— 已弃用 (PgUp/Home/PgDn/End 在 Claude TUI 里
|
|
13966
|
+
// 不是常用导航, 也跟终端历史回滚冲突)。留空数组让面板渲染逻辑自然
|
|
13967
|
+
// 跳过这一排, 不删数组以保 ringSvg 之外别处 reference 安全。
|
|
13968
|
+
var JOYSTICK_CORNER_KEYS = [];
|
|
13962
13969
|
|
|
13963
13970
|
var ignoredInteractiveTargetIds = new Set([
|
|
13964
13971
|
"mini-keyboard-fab",
|
|
@@ -14947,7 +14954,8 @@
|
|
|
14947
14954
|
function handleInputBoxBlur() {
|
|
14948
14955
|
resetInputPanelViewportSpacing();
|
|
14949
14956
|
setTimeout(function() {
|
|
14950
|
-
|
|
14957
|
+
resetRootViewportScroll();
|
|
14958
|
+
syncAppViewportHeight(false);
|
|
14951
14959
|
// On mobile, force terminal refit + scroll after keyboard dismissal.
|
|
14952
14960
|
// The container height restores but terminal needs time to
|
|
14953
14961
|
// fill the expanded space, and the scroll position needs resetting.
|
|
@@ -14959,6 +14967,10 @@
|
|
|
14959
14967
|
maybeScrollTerminalToBottom("keyboard");
|
|
14960
14968
|
}
|
|
14961
14969
|
}, 100);
|
|
14970
|
+
setTimeout(function() {
|
|
14971
|
+
resetRootViewportScroll();
|
|
14972
|
+
syncAppViewportHeight(false);
|
|
14973
|
+
}, 360);
|
|
14962
14974
|
}
|
|
14963
14975
|
|
|
14964
14976
|
function adjustInputBoxSelection(inputBox) {
|
|
@@ -15669,18 +15681,74 @@
|
|
|
15669
15681
|
// adjustResize 不再自动 resize WebView 内容;同时仅给 input-panel
|
|
15670
15682
|
// 加 padding-bottom 只是把 panel 内部底部撑空,并不会让 panel 自身
|
|
15671
15683
|
// 上移。这里通过 CSS 变量驱动整层高度,是跨 WebView/Chrome/PWA 的
|
|
15672
|
-
//
|
|
15673
|
-
//
|
|
15674
|
-
function
|
|
15684
|
+
// 统一兜底。iOS 的 100dvh 在键盘动画后会短暂滞后,所以这里持续用
|
|
15685
|
+
// visualViewport 的实测高度驱动布局,桌面场景下该值基本等于窗口高度。
|
|
15686
|
+
function getRootViewportScrollTop(vv) {
|
|
15687
|
+
var values = [
|
|
15688
|
+
window.scrollY || window.pageYOffset || 0,
|
|
15689
|
+
document.documentElement ? document.documentElement.scrollTop || 0 : 0,
|
|
15690
|
+
document.body ? document.body.scrollTop || 0 : 0
|
|
15691
|
+
];
|
|
15692
|
+
if (vv) {
|
|
15693
|
+
// pageTop is the visual viewport's top edge in layout coordinates.
|
|
15694
|
+
// On iOS it captures the focus pan that can survive keyboard close.
|
|
15695
|
+
if (typeof vv.pageTop === "number") {
|
|
15696
|
+
values.push(vv.pageTop);
|
|
15697
|
+
} else if (typeof vv.offsetTop === "number") {
|
|
15698
|
+
values.push(vv.offsetTop);
|
|
15699
|
+
}
|
|
15700
|
+
}
|
|
15701
|
+
return Math.max.apply(Math, values);
|
|
15702
|
+
}
|
|
15703
|
+
|
|
15704
|
+
function resetRootViewportScroll() {
|
|
15705
|
+
try { window.scrollTo(0, 0); } catch (e) {}
|
|
15706
|
+
if (document.scrollingElement) document.scrollingElement.scrollTop = 0;
|
|
15707
|
+
if (document.documentElement) document.documentElement.scrollTop = 0;
|
|
15708
|
+
if (document.body) document.body.scrollTop = 0;
|
|
15709
|
+
}
|
|
15710
|
+
|
|
15711
|
+
function syncAppViewportHeight(isKeyboardOpen) {
|
|
15675
15712
|
var vv = window.visualViewport;
|
|
15676
15713
|
if (!vv) return;
|
|
15677
|
-
var diff = window.innerHeight - vv.height - vv.offsetTop;
|
|
15678
15714
|
var root = document.documentElement;
|
|
15679
|
-
|
|
15680
|
-
|
|
15681
|
-
|
|
15682
|
-
|
|
15715
|
+
var visualTop = window.__wandImeNative ? 0 : getRootViewportScrollTop(vv);
|
|
15716
|
+
// iOS Safari 上 100dvh 在键盘 / 地址栏切换后有更新延迟, 经常"卡"在
|
|
15717
|
+
// 上一刻的小值 -> body 比真实可见区还短一截, 输入框下方留出一大段
|
|
15718
|
+
// 奶油色 html 背景。改成直接拿 visualViewport.height 当 body 高度的
|
|
15719
|
+
// 权威值, 每帧实时跟随 (vv.resize/scroll 触发), 不再依赖 dvh。
|
|
15720
|
+
// 桌面浏览器上 vv.height ≈ window.innerHeight, 同样无副作用。
|
|
15721
|
+
// 之前的 diff > 50 阈值现在只用来判断"是不是真键盘上来了"以做
|
|
15722
|
+
// iOS html 滚动复位 (offsetTop hack), 不再控制 body 高度。
|
|
15723
|
+
//
|
|
15724
|
+
// 但 iOS 还有一个更隐蔽的状态: 键盘收起后 visual viewport 已经变高,
|
|
15725
|
+
// 根页面却仍停在键盘弹起时的 pageTop/scrollY。此时如果只写 vv.height,
|
|
15726
|
+
// .app-container 的底边落在 visualViewport 顶点之前, input-panel 会悬在
|
|
15727
|
+
// 屏幕底部上方。把 visualTop 临时加回高度, 再滚回 0; 后续 settle timer
|
|
15728
|
+
// 会用新的 visualTop=0 覆盖回来。
|
|
15729
|
+
root.style.setProperty('--app-viewport-height', Math.ceil(vv.height + Math.max(0, visualTop)) + 'px');
|
|
15730
|
+
// iOS Safari: 当 textarea 获得焦点 / 键盘弹起时, 浏览器会主动把
|
|
15731
|
+
// <html> 向上滚一段, 让焦点元素进可见区 —— 体现为 vv.offsetTop > 0。
|
|
15732
|
+
// 但 body 已经被收缩到 vv.height, 这一段 offsetTop 就变成 body 底部
|
|
15733
|
+
// (= .input-panel) 与键盘上沿之间的"空洞", 用户看到的就是
|
|
15734
|
+
// "输入框离键盘还有很远一截"。这里强行把 html 滚回 0, 让 body 底部
|
|
15735
|
+
// 重新贴回键盘上沿。Wand APK 内 (window.__wandImeNative=true) 走
|
|
15736
|
+
// 原生 IME 回调精确 resize, 这里跳过避免双重补偿。
|
|
15737
|
+
if (!window.__wandImeNative && (isKeyboardOpen || visualTop > 1)) {
|
|
15738
|
+
resetRootViewportScroll();
|
|
15739
|
+
}
|
|
15740
|
+
}
|
|
15741
|
+
|
|
15742
|
+
function isEditableFocusTarget(el) {
|
|
15743
|
+
if (!el) return false;
|
|
15744
|
+
var tag = el.tagName;
|
|
15745
|
+
if (tag === "TEXTAREA") return true;
|
|
15746
|
+
if (tag === "SELECT") return true;
|
|
15747
|
+
if (tag === "INPUT") {
|
|
15748
|
+
var type = (el.getAttribute("type") || "text").toLowerCase();
|
|
15749
|
+
return !/^(button|checkbox|color|file|hidden|image|radio|range|reset|submit)$/i.test(type);
|
|
15683
15750
|
}
|
|
15751
|
+
return !!el.isContentEditable;
|
|
15684
15752
|
}
|
|
15685
15753
|
|
|
15686
15754
|
// Visual viewport handling for better mobile keyboard support
|
|
@@ -15690,17 +15758,66 @@
|
|
|
15690
15758
|
var vv = window.visualViewport;
|
|
15691
15759
|
var lastHeight = vv.height;
|
|
15692
15760
|
var keyboardOpen = false;
|
|
15761
|
+
var lastViewportWidth = Math.max(window.innerWidth || 0, vv.width || 0);
|
|
15762
|
+
var largestViewportHeight = Math.max(window.innerHeight || 0, vv.height || 0);
|
|
15763
|
+
var viewportSettleTimers = [];
|
|
15764
|
+
|
|
15765
|
+
function getCurrentViewportHeightBaseline() {
|
|
15766
|
+
return Math.max(window.innerHeight || 0, vv.height || 0);
|
|
15767
|
+
}
|
|
15768
|
+
|
|
15769
|
+
function refreshViewportBaseline() {
|
|
15770
|
+
var width = Math.max(window.innerWidth || 0, vv.width || 0);
|
|
15771
|
+
var height = getCurrentViewportHeightBaseline();
|
|
15772
|
+
if (Math.abs(width - lastViewportWidth) > 8) {
|
|
15773
|
+
lastViewportWidth = width;
|
|
15774
|
+
largestViewportHeight = height;
|
|
15775
|
+
return;
|
|
15776
|
+
}
|
|
15777
|
+
if (height > largestViewportHeight) {
|
|
15778
|
+
largestViewportHeight = height;
|
|
15779
|
+
}
|
|
15780
|
+
}
|
|
15781
|
+
|
|
15782
|
+
function detectKeyboardOpen(inputBox, offsetBottom) {
|
|
15783
|
+
var activeEl = document.activeElement;
|
|
15784
|
+
var hasEditableFocus = activeEl === inputBox || isEditableFocusTarget(activeEl);
|
|
15785
|
+
var shrinkFromLargest = largestViewportHeight - vv.height;
|
|
15786
|
+
var innerShrinkFromLargest = largestViewportHeight - (window.innerHeight || vv.height || 0);
|
|
15787
|
+
if (offsetBottom > 80) return true;
|
|
15788
|
+
// iOS/Chrome iOS sometimes resize window.innerHeight together with
|
|
15789
|
+
// visualViewport.height, so offsetBottom stays near zero. The
|
|
15790
|
+
// focused-editable + baseline shrink path catches that case.
|
|
15791
|
+
if (hasEditableFocus && (shrinkFromLargest > 120 || innerShrinkFromLargest > 120)) return true;
|
|
15792
|
+
// During close animation focus can disappear before viewport height
|
|
15793
|
+
// is fully restored. Keep the "open" state until the shrink is small.
|
|
15794
|
+
if (keyboardOpen && (shrinkFromLargest > 80 || offsetBottom > 32)) return true;
|
|
15795
|
+
return false;
|
|
15796
|
+
}
|
|
15797
|
+
|
|
15798
|
+
function scheduleViewportSettle() {
|
|
15799
|
+
viewportSettleTimers.forEach(function(timer) { clearTimeout(timer); });
|
|
15800
|
+
viewportSettleTimers = [60, 180, 360, 620].map(function(delay) {
|
|
15801
|
+
return setTimeout(function() {
|
|
15802
|
+
if (!window.__wandImeNative) {
|
|
15803
|
+
resetRootViewportScroll();
|
|
15804
|
+
}
|
|
15805
|
+
syncAppViewportHeight(keyboardOpen);
|
|
15806
|
+
}, delay);
|
|
15807
|
+
});
|
|
15808
|
+
}
|
|
15693
15809
|
|
|
15694
15810
|
function updateViewport() {
|
|
15695
15811
|
if (!vv) return;
|
|
15696
15812
|
var inputBox = document.getElementById('input-box');
|
|
15697
15813
|
var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
|
|
15698
|
-
|
|
15814
|
+
refreshViewportBaseline();
|
|
15815
|
+
var isKeyboardOpen = detectKeyboardOpen(inputBox, offsetBottom);
|
|
15699
15816
|
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
15700
15817
|
|
|
15701
15818
|
// 键盘开/关与视口尺寸变化时同步 --app-viewport-height,
|
|
15702
15819
|
// 让 body 高度跟随可见区域,input-panel 自然贴键盘上沿。
|
|
15703
|
-
syncAppViewportHeight();
|
|
15820
|
+
syncAppViewportHeight(isKeyboardOpen);
|
|
15704
15821
|
|
|
15705
15822
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
15706
15823
|
syncInputBoxScroll(inputBox);
|
|
@@ -15720,6 +15837,21 @@
|
|
|
15720
15837
|
// final scroll lands AFTER the animation settles.
|
|
15721
15838
|
var wasStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
15722
15839
|
ensureTerminalFit("keyboard-open", { forceReplay: true });
|
|
15840
|
+
// iOS Safari 二次复位: 第一次 syncAppViewportHeight 在键盘动画
|
|
15841
|
+
// 起始帧把 html 滚回 0, 但 iOS 在键盘动画收尾时还会再尝试一次
|
|
15842
|
+
// "把焦点元素拽进可见区", 把 html 重新推上去 —— 留下用户报的
|
|
15843
|
+
// "输入框距离键盘还有很长距离"。镜像 keyboard-close 的 200ms 兜底,
|
|
15844
|
+
// 等键盘动画完整跑完后再清一次 scrollTop + 重算 viewport 高度,
|
|
15845
|
+
// 让 input-panel 最终稳定贴在键盘上沿。
|
|
15846
|
+
// Wand APK (__wandImeNative=true) 跳过, 原生 IME callback 已经在
|
|
15847
|
+
// WebView 层精确 resize, 这里再 scroll 反而抖。
|
|
15848
|
+
if (!window.__wandImeNative) {
|
|
15849
|
+
setTimeout(function() {
|
|
15850
|
+
resetRootViewportScroll();
|
|
15851
|
+
syncAppViewportHeight(true);
|
|
15852
|
+
}, 220);
|
|
15853
|
+
}
|
|
15854
|
+
scheduleViewportSettle();
|
|
15723
15855
|
// Mirror the keyboard-close 200ms delay: by then the iOS / Android
|
|
15724
15856
|
// keyboard slide-in animation is done, vv.height is final, and
|
|
15725
15857
|
// scrollHeight reflects the post-replay grid. One more force
|
|
@@ -15742,8 +15874,8 @@
|
|
|
15742
15874
|
// window.scrollTo(0,0) 不跑,页面停在键盘抬起时被 iOS 推上去的
|
|
15743
15875
|
// 偏移位置,input-panel 看起来"没回到底"。
|
|
15744
15876
|
// 这里在 visualViewport 检测到键盘收起的瞬间直接强制复位一次,
|
|
15745
|
-
// 并把 --app-viewport-height
|
|
15746
|
-
//
|
|
15877
|
+
// 并把 --app-viewport-height 同步到键盘收起后的实测高度,确保
|
|
15878
|
+
// input-panel 重新贴屏幕底部。
|
|
15747
15879
|
//
|
|
15748
15880
|
// Android APK (window.__wandImeNative=true) 跳过这段 iOS hack —
|
|
15749
15881
|
// MainActivity 已经在 IME 动画 callback 里逐帧把 root setPadding,
|
|
@@ -15753,21 +15885,22 @@
|
|
|
15753
15885
|
var rootEl = document.documentElement;
|
|
15754
15886
|
var imeIsNative = !!window.__wandImeNative;
|
|
15755
15887
|
if (!imeIsNative) {
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15888
|
+
// 不要 removeProperty('--app-viewport-height') —— 那样会让 body 退回
|
|
15889
|
+
// 到 100dvh, 而 iOS Safari 的 100dvh 在键盘动画跑完前经常还停留
|
|
15890
|
+
// 在小值, body 立刻短一截 -> 输入框下方露出大段奶油色 html 背景。
|
|
15891
|
+
// 直接让 syncAppViewportHeight 把它更新为新的 vv.height (键盘已收
|
|
15892
|
+
// 起所以是全可见高度), body 平滑过渡, 不出现空洞。
|
|
15893
|
+
syncAppViewportHeight(false);
|
|
15894
|
+
resetRootViewportScroll();
|
|
15761
15895
|
}
|
|
15896
|
+
scheduleViewportSettle();
|
|
15762
15897
|
setTimeout(function() {
|
|
15763
15898
|
if (!imeIsNative) {
|
|
15764
15899
|
// 二次复位:等键盘收起动画 + iOS 自身的回滚跑完后再清一次,
|
|
15765
15900
|
// 防止 iOS 在动画过程中又把 scrollTop 推上去。
|
|
15766
|
-
|
|
15767
|
-
if (
|
|
15768
|
-
|
|
15769
|
-
if (document.body) document.body.scrollTop = 0;
|
|
15770
|
-
syncAppViewportHeight();
|
|
15901
|
+
resetRootViewportScroll();
|
|
15902
|
+
if (rootEl) rootEl.scrollTop = 0;
|
|
15903
|
+
syncAppViewportHeight(false);
|
|
15771
15904
|
}
|
|
15772
15905
|
ensureTerminalFit("keyboard-close", { forceReplay: true });
|
|
15773
15906
|
// 同 handleInputBoxBlur:尊重 terminalAutoFollow,避免把上滚
|
|
@@ -15883,11 +16016,18 @@
|
|
|
15883
16016
|
// 不改动终端背景的 touch/scroll/wheel —— 单指空白处仍是原生滚动看历史。
|
|
15884
16017
|
|
|
15885
16018
|
function isJoystickAvailable() {
|
|
15886
|
-
// 触屏与桌面网页端都显示(球球用 Pointer Events
|
|
15887
|
-
|
|
16019
|
+
// 触屏与桌面网页端都显示(球球用 Pointer Events,鼠标拖拽同样可用)。
|
|
16020
|
+
// 不再用 currentView/isStructuredSession 关掉:
|
|
16021
|
+
// - chat 视图 (含 PTY Claude 的对话视图): 用户偶尔要给底层 PTY 发
|
|
16022
|
+
// 方向键 / Esc / Shift+Tab 选权限菜单, 但只能切到 terminal 视图才点
|
|
16023
|
+
// 摇杆 —— 现在 chat 视图直接可用。sendJoystickKey 已经走 /input
|
|
16024
|
+
// 接口, 服务端不挑视图。
|
|
16025
|
+
// - 结构化会话: 大多数键 (方向 / Tab) 在 SDK runner 里没真实 effect,
|
|
16026
|
+
// 但 Ctrl+C / Esc 都映射到 query.interrupt() 中断当前回复, 用户
|
|
16027
|
+
// 场景是"等不及当前回答, 想停掉重发"。sendJoystickKey 里按 session
|
|
16028
|
+
// 类型分支处理: PTY 走原本 sequence, 结构化只接受中断意图键。
|
|
15888
16029
|
var session = getSelectedSession();
|
|
15889
16030
|
if (!session) return false;
|
|
15890
|
-
if (isStructuredSession(session)) return false;
|
|
15891
16031
|
return true;
|
|
15892
16032
|
}
|
|
15893
16033
|
|
|
@@ -15934,16 +16074,21 @@
|
|
|
15934
16074
|
for (i = 0; i < JOYSTICK_OUTER_KEYS.length; i++) {
|
|
15935
16075
|
fnRow += keyBtn(JOYSTICK_OUTER_KEYS[i].key, JOYSTICK_OUTER_KEYS[i].label, "");
|
|
15936
16076
|
}
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
var
|
|
15942
|
-
return '<div class="wjp-title">遥控面板</div>' +
|
|
16077
|
+
// 角键 (PgUp/Home/PgDn/End) 与修饰键 (Ctrl/Alt) 排已被裁剪 ——
|
|
16078
|
+
// 当前外圈 4 键全是独立功能键 (Enter / Ctrl+C / Esc / Shift+Tab),
|
|
16079
|
+
// 没有"先按 Ctrl 再按字母"的复合组合, 所以修饰键 toggle 没意义。
|
|
16080
|
+
// CORNER_KEYS 为空时, 对应的 grid 不渲染, 面板高度自动收缩。
|
|
16081
|
+
var html = '<div class="wjp-title">遥控面板</div>' +
|
|
15943
16082
|
dpad +
|
|
15944
|
-
'<div class="wjp-grid wjp-fnkeys">' + fnRow + "</div>"
|
|
15945
|
-
|
|
15946
|
-
|
|
16083
|
+
'<div class="wjp-grid wjp-fnkeys">' + fnRow + "</div>";
|
|
16084
|
+
if (JOYSTICK_CORNER_KEYS.length > 0) {
|
|
16085
|
+
var cornerRow = "";
|
|
16086
|
+
for (i = 0; i < JOYSTICK_CORNER_KEYS.length; i++) {
|
|
16087
|
+
cornerRow += keyBtn(JOYSTICK_CORNER_KEYS[i].key, JOYSTICK_CORNER_KEYS[i].label, "");
|
|
16088
|
+
}
|
|
16089
|
+
html += '<div class="wjp-grid wjp-corners">' + cornerRow + "</div>";
|
|
16090
|
+
}
|
|
16091
|
+
return html;
|
|
15947
16092
|
}
|
|
15948
16093
|
|
|
15949
16094
|
function joystickPolar(r, deg) {
|
|
@@ -15993,12 +16138,17 @@
|
|
|
15993
16138
|
'<path d="' + joystickSectorPath(JOYSTICK_R0, JOYSTICK_R1, center - 45 + gap, center + 45 - gap) + '"/>' +
|
|
15994
16139
|
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
15995
16140
|
}
|
|
15996
|
-
|
|
16141
|
+
// 外圈扇区宽度跟随 OUTER_KEYS.length 动态计算: 4 键 → 90° 每片,
|
|
16142
|
+
// 8 键 → 45° 每片。half 是单片半宽 (扇区中心两侧各延半个 step)。
|
|
16143
|
+
var outerCount = JOYSTICK_OUTER_KEYS.length;
|
|
16144
|
+
var outerStep = outerCount > 0 ? 360 / outerCount : 360;
|
|
16145
|
+
var outerHalf = outerStep / 2;
|
|
16146
|
+
for (i = 0; i < outerCount; i++) {
|
|
15997
16147
|
k = JOYSTICK_OUTER_KEYS[i];
|
|
15998
|
-
center = -90 + i *
|
|
16148
|
+
center = -90 + i * outerStep;
|
|
15999
16149
|
lp = joystickPolar((JOYSTICK_R1 + JOYSTICK_R2) / 2, center);
|
|
16000
16150
|
svg += '<g class="wjr-sector wjr-outer" data-key="' + k.key + '">' +
|
|
16001
|
-
'<path d="' + joystickSectorPath(JOYSTICK_R1, JOYSTICK_R2, center -
|
|
16151
|
+
'<path d="' + joystickSectorPath(JOYSTICK_R1, JOYSTICK_R2, center - outerHalf + gap, center + outerHalf - gap) + '"/>' +
|
|
16002
16152
|
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
16003
16153
|
}
|
|
16004
16154
|
svg += '<circle class="wjr-hub" cx="0" cy="0" r="' + (JOYSTICK_R0 - 1) + '"/>';
|
|
@@ -16248,10 +16398,14 @@
|
|
|
16248
16398
|
if (Math.abs(dy) >= Math.abs(dx)) return { zone: "inner", key: dy < 0 ? "up" : "down" };
|
|
16249
16399
|
return { zone: "inner", key: dx < 0 ? "left" : "right" };
|
|
16250
16400
|
}
|
|
16251
|
-
// 外圈:
|
|
16401
|
+
// 外圈:OUTER_KEYS.length 等分扇区,正上方为 0,顺时针递增;
|
|
16402
|
+
// +halfStep 让扇区中心对准按钮 (原本 N=8 时是 +π/8)。
|
|
16252
16403
|
var ang = Math.atan2(dx, -dy);
|
|
16253
16404
|
if (ang < 0) ang += Math.PI * 2;
|
|
16254
|
-
var
|
|
16405
|
+
var outerCount = JOYSTICK_OUTER_KEYS.length;
|
|
16406
|
+
if (outerCount === 0) return { zone: "dead", key: null };
|
|
16407
|
+
var outerStepRad = (Math.PI * 2) / outerCount;
|
|
16408
|
+
var idx = Math.floor((ang + outerStepRad / 2) / outerStepRad) % outerCount;
|
|
16255
16409
|
return { zone: "outer", key: JOYSTICK_OUTER_KEYS[idx].key };
|
|
16256
16410
|
}
|
|
16257
16411
|
|
|
@@ -16356,6 +16510,24 @@
|
|
|
16356
16510
|
updateJoystickPanelUI();
|
|
16357
16511
|
return;
|
|
16358
16512
|
}
|
|
16513
|
+
var session = getSelectedSession();
|
|
16514
|
+
// ── 结构化会话分支 ──
|
|
16515
|
+
// SDK / claude -p 通道没有 PTY 可写, 把原始 escape 序列丢给
|
|
16516
|
+
// /api/sessions/:id/input 会被结构化 sendMessage 当成对话文本 (例如
|
|
16517
|
+
// 把 "\x1b[A" 作为 prompt 发出去), 既无效又污染上下文。
|
|
16518
|
+
// 这里按"中断意图"白名单转发: Ctrl+C / Esc → query.interrupt()。
|
|
16519
|
+
// 其他键 (方向 / Enter / Shift+Tab) 在结构化里没有合理 mapping, 静默
|
|
16520
|
+
// no-op, 同时震一下做反馈。
|
|
16521
|
+
if (session && isStructuredSession(session)) {
|
|
16522
|
+
if (key === "ctrl_c" || key === "escape") {
|
|
16523
|
+
interruptStructuredSessionFromJoystick(session, key);
|
|
16524
|
+
}
|
|
16525
|
+
// 不论是否真发出去, 都消化掉修饰键 + 更新 UI, 避免下次发送残留状态
|
|
16526
|
+
clearModifiers();
|
|
16527
|
+
updateJoystickPanelUI();
|
|
16528
|
+
return;
|
|
16529
|
+
}
|
|
16530
|
+
// ── PTY 会话原路径 ──
|
|
16359
16531
|
var seq = buildPtySequence(key, {
|
|
16360
16532
|
ctrl: state.modifiers.ctrl,
|
|
16361
16533
|
alt: state.modifiers.alt,
|
|
@@ -16367,6 +16539,44 @@
|
|
|
16367
16539
|
scheduleShortcutResync();
|
|
16368
16540
|
}
|
|
16369
16541
|
|
|
16542
|
+
// 摇杆触发的结构化会话中断: 复用 /api/structured-sessions/:id/messages
|
|
16543
|
+
// 的 interrupt=true 路径 (sendMessage 内部走 query.interrupt 优雅停止,
|
|
16544
|
+
// 失败 fallback 到 abortController.abort)。空 input + interrupt=true =
|
|
16545
|
+
// "停掉当前回复但不发新消息", 跟用户从摇杆按 Ctrl+C/Esc 的预期一致。
|
|
16546
|
+
function interruptStructuredSessionFromJoystick(session, key) {
|
|
16547
|
+
if (!session || !session.id) return;
|
|
16548
|
+
fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
16549
|
+
method: "POST",
|
|
16550
|
+
headers: { "Content-Type": "application/json" },
|
|
16551
|
+
credentials: "same-origin",
|
|
16552
|
+
body: JSON.stringify({ input: "", interrupt: true, preserveQueue: true }),
|
|
16553
|
+
})
|
|
16554
|
+
.then(function(res) {
|
|
16555
|
+
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(p) {
|
|
16556
|
+
throw new Error((p && p.error) || ("中断失败 (key=" + key + ")"));
|
|
16557
|
+
});
|
|
16558
|
+
return res.json();
|
|
16559
|
+
})
|
|
16560
|
+
.then(function(snapshot) {
|
|
16561
|
+
if (snapshot && snapshot.id) {
|
|
16562
|
+
updateSessionSnapshot(snapshot);
|
|
16563
|
+
if (snapshot.id === state.selectedId) {
|
|
16564
|
+
var refreshed = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
16565
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, snapshot.output, false));
|
|
16566
|
+
renderChat(true);
|
|
16567
|
+
if (typeof updateQueueBar === "function") updateQueueBar();
|
|
16568
|
+
}
|
|
16569
|
+
}
|
|
16570
|
+
})
|
|
16571
|
+
.catch(function(err) {
|
|
16572
|
+
// 已经在 SDK 内部完成 / 没有 pending query 时, 服务端会返回 400,
|
|
16573
|
+
// 这里静默吃掉, 避免给用户冒出"中断失败"toast (按了也是想停, 没东西可停就当成功)。
|
|
16574
|
+
if (window && window.console && err && err.message) {
|
|
16575
|
+
console.debug("[wand] joystick interrupt no-op:", err.message);
|
|
16576
|
+
}
|
|
16577
|
+
});
|
|
16578
|
+
}
|
|
16579
|
+
|
|
16370
16580
|
function toggleJoystickPanel() {
|
|
16371
16581
|
if (state.joystickPinnedOpen) closeJoystickPanel();
|
|
16372
16582
|
else openJoystickPanel();
|
|
@@ -258,9 +258,9 @@
|
|
|
258
258
|
min-height: 100dvh;
|
|
259
259
|
height: 100vh;
|
|
260
260
|
height: 100dvh;
|
|
261
|
-
/*
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
/* JS 注入 --app-viewport-height = visualViewport.height,
|
|
262
|
+
键盘开关和 iOS 地址栏动画时整体 flex column 跟随可见视口;
|
|
263
|
+
不支持 visualViewport 时退回 100dvh。 */
|
|
264
264
|
height: var(--app-viewport-height, 100dvh);
|
|
265
265
|
line-height: var(--line-height-base);
|
|
266
266
|
overflow: hidden;
|
|
@@ -310,7 +310,7 @@
|
|
|
310
310
|
min-height: 100dvh;
|
|
311
311
|
height: 100vh;
|
|
312
312
|
height: 100dvh;
|
|
313
|
-
/* 与 body
|
|
313
|
+
/* 与 body 同步:键盘开关时跟随可见视口收缩/恢复。 */
|
|
314
314
|
height: var(--app-viewport-height, 100dvh);
|
|
315
315
|
overflow: hidden;
|
|
316
316
|
}
|
|
@@ -547,7 +547,8 @@
|
|
|
547
547
|
}
|
|
548
548
|
.sidebar.pinned.collapsed .sidebar-header-main,
|
|
549
549
|
.sidebar.pinned.collapsed .sidebar-header-more,
|
|
550
|
-
.sidebar.pinned.collapsed .sidebar-footer
|
|
550
|
+
.sidebar.pinned.collapsed .sidebar-footer,
|
|
551
|
+
.sidebar.pinned.collapsed .sidebar-history-region {
|
|
551
552
|
display: none;
|
|
552
553
|
}
|
|
553
554
|
.sidebar.pinned.collapsed .sidebar-header-actions {
|
|
@@ -613,6 +614,24 @@
|
|
|
613
614
|
0 4px 10px -3px rgba(120, 88, 56, 0.3),
|
|
614
615
|
inset 0 1px 0 rgba(255, 255, 255, 0.48);
|
|
615
616
|
}
|
|
617
|
+
/* 快速新建会话 tile —— 虚线描边 + 加号,区别于实心会话方块 */
|
|
618
|
+
.sidebar-collapsed-tile.add {
|
|
619
|
+
background: rgba(184, 92, 55, 0.06);
|
|
620
|
+
border: 1.5px dashed rgba(184, 92, 55, 0.5);
|
|
621
|
+
color: #a04a2e;
|
|
622
|
+
box-shadow: none;
|
|
623
|
+
margin-top: 2px;
|
|
624
|
+
}
|
|
625
|
+
.sidebar-collapsed-tile.add:hover {
|
|
626
|
+
filter: none;
|
|
627
|
+
transform: translateY(-1px);
|
|
628
|
+
background: rgba(184, 92, 55, 0.12);
|
|
629
|
+
border-color: rgba(184, 92, 55, 0.72);
|
|
630
|
+
box-shadow: 0 3px 8px -3px rgba(184, 92, 55, 0.32);
|
|
631
|
+
}
|
|
632
|
+
.sidebar-collapsed-tile.add:active {
|
|
633
|
+
transform: translateY(0) scale(0.94);
|
|
634
|
+
}
|
|
616
635
|
/* Active — accent ring + 一层柔和暖投影 + 顶高光。原先 7 层叠加在 36px 小块上
|
|
617
636
|
糊成一团,精简到 3 层即可表达「升起 + 选中」。 */
|
|
618
637
|
.sidebar-collapsed-tile.active {
|
|
@@ -627,13 +646,6 @@
|
|
|
627
646
|
transform: translateY(0) scale(0.94);
|
|
628
647
|
transition-duration: 0.08s;
|
|
629
648
|
}
|
|
630
|
-
.sidebar-collapsed-empty {
|
|
631
|
-
color: var(--text-muted, rgba(89, 58, 32, 0.45));
|
|
632
|
-
text-align: center;
|
|
633
|
-
font-size: 16px;
|
|
634
|
-
padding: 12px 0;
|
|
635
|
-
user-select: none;
|
|
636
|
-
}
|
|
637
649
|
/* Hover tooltip bubble for narrow tiles — chat-bubble look */
|
|
638
650
|
.sidebar-tile-bubble {
|
|
639
651
|
position: fixed;
|
|
@@ -4045,6 +4057,12 @@
|
|
|
4045
4057
|
min-height: 0;
|
|
4046
4058
|
min-width: 0;
|
|
4047
4059
|
scrollbar-gutter: stable;
|
|
4060
|
+
/* iOS Safari: 聊天到顶 / 到底再用力滑, scroll 会从 .chat-messages 冒到
|
|
4061
|
+
body, 即使父级有 overflow:hidden 也拦不住 (Safari 已知行为)。这一冒
|
|
4062
|
+
有两个用户可见的副作用: 1) 整页跟着一起被推一帧, 输入框跳动;
|
|
4063
|
+
2) 偶尔触发地址栏 / 工具栏的折叠动画。contain 把惯性吃在容器内,
|
|
4064
|
+
保留 iOS 那种橡皮筋反弹手感, 但不向外溢出。 */
|
|
4065
|
+
overscroll-behavior: contain;
|
|
4048
4066
|
}
|
|
4049
4067
|
|
|
4050
4068
|
/* ===== 历史消息懒加载 ===== */
|
|
@@ -10169,8 +10187,8 @@
|
|
|
10169
10187
|
}
|
|
10170
10188
|
|
|
10171
10189
|
html, body {
|
|
10172
|
-
min-height: 100dvh;
|
|
10173
|
-
height:
|
|
10190
|
+
min-height: var(--app-viewport-height, 100dvh);
|
|
10191
|
+
height: var(--app-viewport-height, 100dvh);
|
|
10174
10192
|
}
|
|
10175
10193
|
|
|
10176
10194
|
body {
|
|
@@ -10181,9 +10199,9 @@
|
|
|
10181
10199
|
.app-container {
|
|
10182
10200
|
min-height: 100dvh;
|
|
10183
10201
|
height: 100dvh;
|
|
10184
|
-
/* 与 body
|
|
10185
|
-
app-container 跟随 visualViewport
|
|
10186
|
-
|
|
10202
|
+
/* 与 body 同步:JS 持续注入 --app-viewport-height,
|
|
10203
|
+
app-container 跟随 visualViewport 收缩/恢复,input-panel 自动贴键盘
|
|
10204
|
+
上沿或屏幕底部。 */
|
|
10187
10205
|
height: var(--app-viewport-height, 100dvh);
|
|
10188
10206
|
overflow: hidden;
|
|
10189
10207
|
}
|
|
@@ -10740,7 +10758,11 @@
|
|
|
10740
10758
|
.file-search-box { padding: 6px 10px 8px; }
|
|
10741
10759
|
.file-search-icon { left: 20px; }
|
|
10742
10760
|
.file-search-clear { right: 16px; }
|
|
10743
|
-
|
|
10761
|
+
/* 16px 兜底 iOS Safari 聚焦自动放大:
|
|
10762
|
+
即使设了 user-scalable=no, 部分 iOS 版本 (尤其 15 / 16 早期补丁) 仍会
|
|
10763
|
+
在 input font-size < 16px 时强制 zoom in, 之后回不到原 scale, 用户得
|
|
10764
|
+
手动双指捏回。所有移动端会被聚焦的 input 都统一到 16px。 */
|
|
10765
|
+
.file-search-input { min-height: 34px; font-size: 16px; padding: 7px 32px 7px 30px; }
|
|
10744
10766
|
.file-item { padding: 6px 8px; min-height: 32px; }
|
|
10745
10767
|
|
|
10746
10768
|
/* 欢迎页移动端 */
|