@co0ontty/wand 1.35.2 → 1.37.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/dist/claude-sdk-runner.d.ts +7 -0
- package/dist/claude-sdk-runner.js +3 -0
- package/dist/git-quick-commit.js +5 -5
- package/dist/language-prompt.d.ts +17 -0
- package/dist/language-prompt.js +68 -0
- package/dist/process-manager.js +8 -5
- package/dist/prompt-optimizer.js +3 -3
- package/dist/pwa.js +36 -5
- package/dist/server.js +3 -0
- package/dist/structured-session-manager.js +4 -3
- package/dist/web-ui/content/scripts.js +1009 -106
- package/dist/web-ui/content/styles.css +669 -93
- package/dist/web-ui/styles.js +18 -4
- package/package.json +1 -1
|
@@ -326,6 +326,36 @@
|
|
|
326
326
|
miniKeyboardVisible: false,
|
|
327
327
|
shortcutsExpanded: false,
|
|
328
328
|
modifiers: { ctrl: false, alt: false, shift: false },
|
|
329
|
+
// ── 终端悬浮摇杆遥控器(手机端 PTY 遥控)状态 ──
|
|
330
|
+
// joystickPos 持久化球球位置 {right, bottom}(localStorage wand-ball-pos)
|
|
331
|
+
joystickPos: (function() {
|
|
332
|
+
try {
|
|
333
|
+
var saved = localStorage.getItem("wand-ball-pos");
|
|
334
|
+
if (!saved) return null;
|
|
335
|
+
var parsed = JSON.parse(saved);
|
|
336
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
337
|
+
} catch (e) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
})(),
|
|
341
|
+
joystickPinnedOpen: false, // 钉住面板是否展开(不持久化,切会话复位)
|
|
342
|
+
joystickRootEl: null, // 以下均为运行期句柄,teardown 复位
|
|
343
|
+
joystickRingEl: null,
|
|
344
|
+
joystickPanelEl: null,
|
|
345
|
+
joystickBackdropEl: null,
|
|
346
|
+
joystickBallEl: null,
|
|
347
|
+
joystickPointerId: null,
|
|
348
|
+
joystickGesture: null, // null|'pending'|'ring'|'move'
|
|
349
|
+
joystickPressStart: null, // {x, y, t}
|
|
350
|
+
joystickCenter: null, // 手势开始时球球中心,用于径向命中
|
|
351
|
+
joystickLongPressTimer: null,
|
|
352
|
+
joystickRepeatTimer: null,
|
|
353
|
+
joystickRepeatKey: null,
|
|
354
|
+
joystickHoverOuter: null, // 外圈当前高亮键(松手发送)
|
|
355
|
+
joystickLastHoverKey: null, // 上一次悬停的扇区键(用于切换震动反馈)
|
|
356
|
+
joystickMoveHandler: null,
|
|
357
|
+
joystickUpHandler: null,
|
|
358
|
+
joystickResizeHandler: null,
|
|
329
359
|
fileSearchQuery: "",
|
|
330
360
|
fileExplorerLoading: false,
|
|
331
361
|
allFiles: [],
|
|
@@ -334,7 +364,7 @@
|
|
|
334
364
|
fileExplorerTotal: 0,
|
|
335
365
|
claudeHistory: [],
|
|
336
366
|
claudeHistoryLoaded: false,
|
|
337
|
-
claudeHistoryExpanded:
|
|
367
|
+
claudeHistoryExpanded: false,
|
|
338
368
|
claudeHistoryExpandedDirs: {},
|
|
339
369
|
archivedExpanded: false,
|
|
340
370
|
sessionsManageMode: false,
|
|
@@ -354,6 +384,81 @@
|
|
|
354
384
|
})()
|
|
355
385
|
};
|
|
356
386
|
|
|
387
|
+
// ── 前端 i18n(最小化)──
|
|
388
|
+
// 后端 config.language 是给 Claude 用的"回答语言"偏好("中文" / "English" / 任意字符串),
|
|
389
|
+
// 之前 frontend 完全没收 → UI label 一直 hardcoded 中文 + 个别英文("SUBAGENT" 那个 tag)。
|
|
390
|
+
// 用户设的是中文时,"SUBAGENT" 这类英文残留就和"配置语言不一致"。
|
|
391
|
+
//
|
|
392
|
+
// 设计取舍:
|
|
393
|
+
// - 只维护两套:中文(默认) + 英文。其它取值("日本語"、"Français"等)回退到英文,
|
|
394
|
+
// 因为 Claude 会按用户语言回答,UI 至少不卡在中文上让英语圈用户看不懂。
|
|
395
|
+
// - 不引入 i18n 库,几十个 key 用平铺对象,t(key, params) 是个十行 helper。
|
|
396
|
+
// - params 支持 "{name}" 占位符替换,避免在调用点拼字符串。
|
|
397
|
+
// - 缺 key 时回退到中文表,再没有就返回 key 本身(debug 友好)。
|
|
398
|
+
var I18N_DEFAULT_LANG = "中文";
|
|
399
|
+
var I18N = {
|
|
400
|
+
"中文": {
|
|
401
|
+
"subagent.tag": "子代理",
|
|
402
|
+
"subagent.handoff": "{parent} 让 {sub} 帮忙",
|
|
403
|
+
"subagent.handoff.with_desc": "{parent} 让 {sub} 帮忙:",
|
|
404
|
+
"subagent.continued": "继续输出",
|
|
405
|
+
"subagent.task.done": "任务完成",
|
|
406
|
+
"subagent.task.failed": "任务失败",
|
|
407
|
+
"subagent.running": "运行中",
|
|
408
|
+
"subagent.no_output": "(无输出)",
|
|
409
|
+
"subagent.helper_fallback_prefix": "协作猫·",
|
|
410
|
+
"subagent.title_aria": "点击展开 / 收起子代理输出",
|
|
411
|
+
"subagent.tag_title": "子代理 / subagent",
|
|
412
|
+
"ui.expand": "展开",
|
|
413
|
+
"ui.collapse": "收起",
|
|
414
|
+
"ui.expand_panel_aria": "展开子代理输出",
|
|
415
|
+
"ui.collapse_panel_aria": "收起子代理输出"
|
|
416
|
+
},
|
|
417
|
+
"English": {
|
|
418
|
+
"subagent.tag": "Subagent",
|
|
419
|
+
"subagent.handoff": "{parent} asked {sub} for help",
|
|
420
|
+
"subagent.handoff.with_desc": "{parent} asked {sub} for help with: ",
|
|
421
|
+
"subagent.continued": "continued",
|
|
422
|
+
"subagent.task.done": "Task complete",
|
|
423
|
+
"subagent.task.failed": "Task failed",
|
|
424
|
+
"subagent.running": "Running",
|
|
425
|
+
"subagent.no_output": "(no output)",
|
|
426
|
+
"subagent.helper_fallback_prefix": "Helper·",
|
|
427
|
+
"subagent.title_aria": "Click to expand / collapse subagent output",
|
|
428
|
+
"subagent.tag_title": "Subagent",
|
|
429
|
+
"ui.expand": "Expand",
|
|
430
|
+
"ui.collapse": "Collapse",
|
|
431
|
+
"ui.expand_panel_aria": "Expand subagent output",
|
|
432
|
+
"ui.collapse_panel_aria": "Collapse subagent output"
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
function getActiveLang() {
|
|
436
|
+
var raw = state.config && typeof state.config.language === "string" ? state.config.language.trim() : "";
|
|
437
|
+
if (!raw) return I18N_DEFAULT_LANG;
|
|
438
|
+
if (I18N[raw]) return raw;
|
|
439
|
+
// 模糊匹配:用户可能写 "english" / "en" / "ENG"
|
|
440
|
+
var lower = raw.toLowerCase();
|
|
441
|
+
if (lower === "english" || lower === "en" || lower.indexOf("english") === 0 || lower.indexOf("英") === 0) return "English";
|
|
442
|
+
if (lower === "中文" || lower === "zh" || lower.indexOf("zh") === 0 || lower.indexOf("中") === 0 || lower.indexOf("chinese") === 0) return "中文";
|
|
443
|
+
return "English"; // 其它语言走英文 fallback(Claude 会按 raw 回答,UI 至少英文不卡)
|
|
444
|
+
}
|
|
445
|
+
function t(key, params) {
|
|
446
|
+
var lang = getActiveLang();
|
|
447
|
+
var table = I18N[lang] || I18N[I18N_DEFAULT_LANG];
|
|
448
|
+
var template = table && key in table ? table[key] : null;
|
|
449
|
+
if (template == null) {
|
|
450
|
+
var def = I18N[I18N_DEFAULT_LANG];
|
|
451
|
+
template = def && key in def ? def[key] : key;
|
|
452
|
+
}
|
|
453
|
+
if (params && typeof template === "string") {
|
|
454
|
+
for (var k in params) {
|
|
455
|
+
if (!Object.prototype.hasOwnProperty.call(params, k)) continue;
|
|
456
|
+
template = template.split("{" + k + "}").join(params[k]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return template;
|
|
460
|
+
}
|
|
461
|
+
|
|
357
462
|
// ── 统一线性图标库 ──
|
|
358
463
|
// 替代页面里散落的 emoji(🛡 / ⌨ / 📁 / 🔔 …)。这些 emoji 在系统字体里渲染成
|
|
359
464
|
// 彩色卡通形态,与项目温暖米色 + 棕橙的复古主题视觉冲突明显。这里集中维护
|
|
@@ -1040,6 +1145,10 @@
|
|
|
1040
1145
|
}
|
|
1041
1146
|
case "tool-group":
|
|
1042
1147
|
return el.getAttribute("data-expanded") === "true";
|
|
1148
|
+
case "subagent-reply":
|
|
1149
|
+
return el.getAttribute("data-expanded") === "true";
|
|
1150
|
+
case "subagent-panel":
|
|
1151
|
+
return el.getAttribute("data-expanded") === "true";
|
|
1043
1152
|
default:
|
|
1044
1153
|
return false;
|
|
1045
1154
|
}
|
|
@@ -1088,6 +1197,35 @@
|
|
|
1088
1197
|
if (chevron) chevron.style.transform = expanded ? "rotate(180deg)" : "";
|
|
1089
1198
|
break;
|
|
1090
1199
|
}
|
|
1200
|
+
case "subagent-reply": {
|
|
1201
|
+
el.setAttribute("data-expanded", expanded ? "true" : "false");
|
|
1202
|
+
var subLabel = el.querySelector(".subagent-reply-toggle-label");
|
|
1203
|
+
if (subLabel) subLabel.textContent = expanded ? "收起" : "展开";
|
|
1204
|
+
var subToggleBtn = el.querySelector(".subagent-reply-toggle");
|
|
1205
|
+
if (subToggleBtn) {
|
|
1206
|
+
subToggleBtn.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1207
|
+
subToggleBtn.setAttribute("aria-label", expanded ? "收起子代理回复" : "展开子代理回复全文");
|
|
1208
|
+
}
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
case "subagent-panel": {
|
|
1212
|
+
el.setAttribute("data-expanded", expanded ? "true" : "false");
|
|
1213
|
+
// 头/尾两个按钮都得同步——label、aria-expanded、aria-label
|
|
1214
|
+
var panelBtns = el.querySelectorAll(".subagent-panel-toggle");
|
|
1215
|
+
for (var pbi = 0; pbi < panelBtns.length; pbi++) {
|
|
1216
|
+
var pb = panelBtns[pbi];
|
|
1217
|
+
pb.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
1218
|
+
pb.setAttribute("aria-label", expanded ? "收起子代理输出" : "展开子代理输出");
|
|
1219
|
+
var pblbl = pb.querySelector(".subagent-panel-toggle-label");
|
|
1220
|
+
if (pblbl) pblbl.textContent = expanded ? "收起" : "展开";
|
|
1221
|
+
}
|
|
1222
|
+
// 展开时把 body 滚到顶,避免延续上次的滚动位置造成"展开后看到一半"
|
|
1223
|
+
if (expanded) {
|
|
1224
|
+
var pbody = el.querySelector(".subagent-panel-body");
|
|
1225
|
+
if (pbody) pbody.scrollTop = 0;
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1091
1229
|
}
|
|
1092
1230
|
}
|
|
1093
1231
|
|
|
@@ -1685,6 +1823,7 @@
|
|
|
1685
1823
|
'<div class="sessions-list" id="sessions-list">' + renderSessionsListContent() + '</div>' +
|
|
1686
1824
|
'</div>' +
|
|
1687
1825
|
'</div>' +
|
|
1826
|
+
'<div id="sidebar-history-region" class="sidebar-history-region">' + renderClaudeHistoryRegion() + '</div>' +
|
|
1688
1827
|
'<div class="sidebar-footer">' +
|
|
1689
1828
|
'<button id="drawer-new-session-button" class="btn btn-primary btn-block"><span>+</span> 新会话</button>' +
|
|
1690
1829
|
'<div class="sidebar-footer-actions">' +
|
|
@@ -3361,6 +3500,9 @@
|
|
|
3361
3500
|
}
|
|
3362
3501
|
|
|
3363
3502
|
function renderSessions() {
|
|
3503
|
+
// Claude history is no longer inlined here — it lives in its own
|
|
3504
|
+
// collapsible region between .sidebar-body and .sidebar-footer, so
|
|
3505
|
+
// the scrolling sessions list focuses on recent / archived sessions.
|
|
3364
3506
|
var archivedSessions = state.sessions.filter(function(session) { return session.archived; });
|
|
3365
3507
|
var groups = [];
|
|
3366
3508
|
groups.push(renderSessionManageBar());
|
|
@@ -3373,9 +3515,8 @@
|
|
|
3373
3515
|
if (archivedSessions.length > 0) {
|
|
3374
3516
|
groups.push(renderArchivedGroup(archivedSessions));
|
|
3375
3517
|
}
|
|
3376
|
-
groups.push(renderClaudeHistorySection());
|
|
3377
3518
|
if (recentEntries.length === 0 && archivedSessions.length === 0) {
|
|
3378
|
-
return renderSessionManageBar() + '<div class="empty-state"><strong>还没有会话记录</strong><br>点击上方「新对话」开始你的第一次对话。</div>'
|
|
3519
|
+
return renderSessionManageBar() + '<div class="empty-state"><strong>还没有会话记录</strong><br>点击上方「新对话」开始你的第一次对话。</div>';
|
|
3379
3520
|
}
|
|
3380
3521
|
return groups.join("");
|
|
3381
3522
|
}
|
|
@@ -3482,37 +3623,62 @@
|
|
|
3482
3623
|
return html;
|
|
3483
3624
|
}
|
|
3484
3625
|
|
|
3485
|
-
|
|
3486
|
-
|
|
3626
|
+
// Compute the items eligible for the history region (older than 24h —
|
|
3627
|
+
// the recent-24h ones already show in the recent group above).
|
|
3628
|
+
function getClaudeHistoryRegionItems() {
|
|
3487
3629
|
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
3488
|
-
|
|
3630
|
+
return getVisibleClaudeHistorySessions().filter(function(s) {
|
|
3489
3631
|
return !s.timestamp || new Date(s.timestamp).getTime() <= cutoff;
|
|
3490
3632
|
});
|
|
3491
|
-
|
|
3492
|
-
var countBadge = state.claudeHistoryLoaded && visibleHistory.length > 0
|
|
3493
|
-
? ' <span class="history-count">' + visibleHistory.length + '</span>'
|
|
3494
|
-
: '';
|
|
3495
|
-
var clearAllButton = state.claudeHistoryExpanded && state.claudeHistoryLoaded && visibleHistory.length > 0
|
|
3496
|
-
? '<button class="btn btn-danger btn-xs session-history-clear" data-action="clear-all-history" type="button">清空</button>'
|
|
3497
|
-
: '';
|
|
3498
|
-
var header = '<div class="session-group-title claude-history-toggle" id="claude-history-toggle">' +
|
|
3499
|
-
'<span class="chevron">' + chevron + '</span> Claude 历史' + countBadge +
|
|
3500
|
-
'</div>' + clearAllButton;
|
|
3633
|
+
}
|
|
3501
3634
|
|
|
3502
|
-
|
|
3503
|
-
|
|
3635
|
+
// Render the docked Claude-history region that lives between
|
|
3636
|
+
// `.sidebar-body` and `.sidebar-footer`. Collapsed by default — only
|
|
3637
|
+
// shows a slim header ("历史消息" + count bubble). Expanded reveals the
|
|
3638
|
+
// grouped-by-cwd list inside a scroll cap.
|
|
3639
|
+
function renderClaudeHistoryRegion() {
|
|
3640
|
+
var visibleHistory = getClaudeHistoryRegionItems();
|
|
3641
|
+
var expanded = !!state.claudeHistoryExpanded;
|
|
3642
|
+
var loaded = !!state.claudeHistoryLoaded;
|
|
3643
|
+
var count = loaded ? visibleHistory.length : 0;
|
|
3644
|
+
|
|
3645
|
+
var badgeCls = "history-bubble";
|
|
3646
|
+
var badgeContent;
|
|
3647
|
+
if (!loaded) {
|
|
3648
|
+
badgeCls += " loading";
|
|
3649
|
+
badgeContent = "···";
|
|
3650
|
+
} else if (count === 0) {
|
|
3651
|
+
badgeCls += " empty";
|
|
3652
|
+
badgeContent = "0";
|
|
3653
|
+
} else {
|
|
3654
|
+
badgeContent = count > 999 ? "999+" : String(count);
|
|
3504
3655
|
}
|
|
3656
|
+
var badge = '<span class="' + badgeCls + '">' + badgeContent + '</span>';
|
|
3657
|
+
|
|
3658
|
+
// Chevron rotates: collapsed → up (▲, suggests "expand upward"),
|
|
3659
|
+
// expanded → down (▼, suggests "collapse downward").
|
|
3660
|
+
var chevronSvg = '<svg class="sidebar-history-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 15 12 9 18 15"/></svg>';
|
|
3661
|
+
|
|
3662
|
+
var headerCls = "sidebar-history-header" + (expanded ? " expanded" : "");
|
|
3663
|
+
var header = '<button type="button" class="' + headerCls + '" id="claude-history-toggle" aria-expanded="' + expanded + '" aria-controls="sidebar-history-body" title="' + (expanded ? "收起历史消息" : "展开历史消息") + '">' +
|
|
3664
|
+
'<span class="sidebar-history-label">历史消息</span>' +
|
|
3665
|
+
'<span class="sidebar-history-right">' + badge + chevronSvg + '</span>' +
|
|
3666
|
+
'</button>';
|
|
3667
|
+
|
|
3668
|
+
var body = expanded
|
|
3669
|
+
? '<div class="sidebar-history-body" id="sidebar-history-body">' + renderClaudeHistoryBodyContent(visibleHistory) + '</div>'
|
|
3670
|
+
: '';
|
|
3671
|
+
|
|
3672
|
+
return header + body;
|
|
3673
|
+
}
|
|
3505
3674
|
|
|
3675
|
+
function renderClaudeHistoryBodyContent(visibleHistory) {
|
|
3506
3676
|
if (!state.claudeHistoryLoaded) {
|
|
3507
|
-
return '<
|
|
3508
|
-
'<div class="claude-history-loading">扫描历史会话中…</div></section>';
|
|
3677
|
+
return '<div class="claude-history-loading">扫描历史会话中…</div>';
|
|
3509
3678
|
}
|
|
3510
|
-
|
|
3511
3679
|
if (visibleHistory.length === 0) {
|
|
3512
|
-
return '<
|
|
3513
|
-
'<div class="claude-history-loading">没有更早的 Claude 历史会话</div></section>';
|
|
3680
|
+
return '<div class="claude-history-loading">没有更早的 Claude 历史会话</div>';
|
|
3514
3681
|
}
|
|
3515
|
-
|
|
3516
3682
|
var groups = {};
|
|
3517
3683
|
var groupOrder = [];
|
|
3518
3684
|
visibleHistory.forEach(function(s) {
|
|
@@ -3522,18 +3688,27 @@
|
|
|
3522
3688
|
}
|
|
3523
3689
|
groups[s.cwd].push(s);
|
|
3524
3690
|
});
|
|
3525
|
-
|
|
3526
|
-
|
|
3691
|
+
var toolbar = '<div class="sidebar-history-toolbar">' +
|
|
3692
|
+
'<button class="btn btn-ghost btn-xs sidebar-history-clear" data-action="clear-all-history" type="button">清空全部</button>' +
|
|
3693
|
+
'</div>';
|
|
3694
|
+
var listHtml = '';
|
|
3527
3695
|
groupOrder.forEach(function(cwd) {
|
|
3528
3696
|
var cwdShort = cwd.split("/").filter(Boolean).slice(-3).join("/");
|
|
3529
3697
|
var isDirExpanded = !!state.claudeHistoryExpandedDirs[cwd];
|
|
3530
|
-
|
|
3698
|
+
listHtml += renderClaudeHistoryDirectoryHeader(cwd, cwdShort, groups[cwd].length, isDirExpanded);
|
|
3531
3699
|
if (isDirExpanded) {
|
|
3532
|
-
|
|
3700
|
+
listHtml += groups[cwd].map(function(session) { return renderClaudeHistoryItem(session, "history"); }).join("");
|
|
3533
3701
|
}
|
|
3534
3702
|
});
|
|
3703
|
+
return toolbar + '<div class="sidebar-history-scroll">' + listHtml + '</div>';
|
|
3704
|
+
}
|
|
3535
3705
|
|
|
3536
|
-
|
|
3706
|
+
// Re-render only the docked history region in place. Called by
|
|
3707
|
+
// updateSessionsList() so existing callers (load complete, delete, etc.)
|
|
3708
|
+
// keep working without changes.
|
|
3709
|
+
function updateClaudeHistoryRegion() {
|
|
3710
|
+
var region = document.getElementById("sidebar-history-region");
|
|
3711
|
+
if (region) region.innerHTML = renderClaudeHistoryRegion();
|
|
3537
3712
|
}
|
|
3538
3713
|
|
|
3539
3714
|
function getVisibleClaudeHistorySessions() {
|
|
@@ -5591,35 +5766,28 @@
|
|
|
5591
5766
|
}
|
|
5592
5767
|
persistElementExpandState(el, "thinking");
|
|
5593
5768
|
};
|
|
5594
|
-
// Toggle function for subagent reply bubbles —
|
|
5595
|
-
//
|
|
5596
|
-
|
|
5769
|
+
// Toggle function for subagent reply bubbles — simple two-state preview/expanded.
|
|
5770
|
+
// 参考 opencode 的折叠面板:默认固定高度预览(含底部渐隐 mask),点击切到全文展开。
|
|
5771
|
+
// 状态写在 data-expanded 上,配套 CSS 控制 max-height + mask;用 data-expand-key
|
|
5772
|
+
// 走通用持久化通道(applyPersistedExpandState 会自动恢复用户上次的选择)。
|
|
5773
|
+
window.__subagentReplyToggle = function(e, target) {
|
|
5597
5774
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
5598
|
-
var bubble =
|
|
5775
|
+
var bubble = target && target.closest ? target.closest(".subagent-reply") : null;
|
|
5599
5776
|
if (!bubble) return;
|
|
5600
|
-
var
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
if (
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
icon.textContent = next === "collapsed" ? "▸"
|
|
5615
|
-
: next === "expanded" ? "▴"
|
|
5616
|
-
: "▾";
|
|
5617
|
-
}
|
|
5618
|
-
btn.setAttribute("aria-label",
|
|
5619
|
-
next === "preview" ? "点击展开全部" :
|
|
5620
|
-
next === "expanded" ? "点击完全收起" :
|
|
5621
|
-
"点击切回预览"
|
|
5622
|
-
);
|
|
5777
|
+
var expanded = bubble.getAttribute("data-expanded") === "true";
|
|
5778
|
+
applyExpandedState(bubble, "subagent-reply", !expanded);
|
|
5779
|
+
persistElementExpandState(bubble, "subagent-reply");
|
|
5780
|
+
};
|
|
5781
|
+
// Toggle function for whole subagent panel (handoff + body + footer)。
|
|
5782
|
+
// 头部 / 尾部按钮、整条 header 都绑这个;data-expanded 一变,CSS 切 body max-height
|
|
5783
|
+
// + 旋转 chevron,applyExpandedState 走通用通道写持久化。
|
|
5784
|
+
window.__subagentPanelToggle = function(e, target) {
|
|
5785
|
+
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
5786
|
+
var panel = target && target.closest ? target.closest(".subagent-panel") : null;
|
|
5787
|
+
if (!panel) return;
|
|
5788
|
+
var expanded = panel.getAttribute("data-expanded") === "true";
|
|
5789
|
+
applyExpandedState(panel, "subagent-panel", !expanded);
|
|
5790
|
+
persistElementExpandState(panel, "subagent-panel");
|
|
5623
5791
|
};
|
|
5624
5792
|
// Toggle function for inline tool rows (Read, Glob, Grep, etc.)
|
|
5625
5793
|
window.__inlineToolToggle = function(el) {
|
|
@@ -5858,6 +6026,15 @@
|
|
|
5858
6026
|
sessionsList.addEventListener("mouseout", handleCollapsedTileLeave);
|
|
5859
6027
|
initSwipeToDelete(sessionsList);
|
|
5860
6028
|
}
|
|
6029
|
+
// The docked history region lives outside #sessions-list now, but it
|
|
6030
|
+
// still wants the same delegated handlers (toggle button, directory
|
|
6031
|
+
// expand/collapse, history-item clicks, clear-all, etc.). Reuse the
|
|
6032
|
+
// same callbacks so behavior stays identical.
|
|
6033
|
+
var historyRegion = document.getElementById("sidebar-history-region");
|
|
6034
|
+
if (historyRegion) {
|
|
6035
|
+
historyRegion.addEventListener("click", handleSessionItemClick);
|
|
6036
|
+
historyRegion.addEventListener("keydown", handleSessionItemKeydown);
|
|
6037
|
+
}
|
|
5861
6038
|
window.addEventListener("scroll", hideCollapsedTileBubble, true);
|
|
5862
6039
|
window.addEventListener("resize", hideCollapsedTileBubble);
|
|
5863
6040
|
|
|
@@ -5942,7 +6119,8 @@
|
|
|
5942
6119
|
state.selectedId = null;
|
|
5943
6120
|
persistSelectedId();
|
|
5944
6121
|
resetChatRenderCache();
|
|
5945
|
-
|
|
6122
|
+
// 回到首页是导航语义,不是「收侧栏」。桌面常驻栏保留;手机只把 overlay 收掉。
|
|
6123
|
+
dismissDrawerIfOverlay();
|
|
5946
6124
|
render();
|
|
5947
6125
|
});
|
|
5948
6126
|
var refreshBtn = document.getElementById("sidebar-refresh-btn");
|
|
@@ -6188,7 +6366,8 @@
|
|
|
6188
6366
|
if (autoApproveToggle) autoApproveToggle.addEventListener("click", toggleAutoApprove);
|
|
6189
6367
|
var sendBtn = document.getElementById("send-input-button");
|
|
6190
6368
|
if (sendBtn) sendBtn.addEventListener("click", function() {
|
|
6191
|
-
|
|
6369
|
+
// 与 input focus 同理:手机 drawer 盖在上面才收起,桌面常驻栏保持原状。
|
|
6370
|
+
dismissDrawerIfOverlay();
|
|
6192
6371
|
sendOrStart();
|
|
6193
6372
|
});
|
|
6194
6373
|
var stopBtn = document.getElementById("stop-button");
|
|
@@ -6241,8 +6420,10 @@
|
|
|
6241
6420
|
if (state.terminalInteractive) handleInteractiveTextInput(inputBox);
|
|
6242
6421
|
});
|
|
6243
6422
|
inputBox.addEventListener("focus", function() {
|
|
6244
|
-
//
|
|
6245
|
-
|
|
6423
|
+
// 只在手机 drawer 真的盖在输入区上面时才收起,避免 backdrop 挡点击。
|
|
6424
|
+
// 桌面 pinned/窄条形态下 drawer 是常驻并列布局,不会挡输入,调
|
|
6425
|
+
// closeSessionsDrawer 会把 sidebarPinned 一起清掉、侧栏整个不见。
|
|
6426
|
+
dismissDrawerIfOverlay();
|
|
6246
6427
|
handleInputBoxFocus({ target: inputBox });
|
|
6247
6428
|
});
|
|
6248
6429
|
inputBox.addEventListener("blur", handleInputBoxBlur);
|
|
@@ -6960,9 +7141,11 @@
|
|
|
6960
7141
|
} else {
|
|
6961
7142
|
selectSession(sessionId);
|
|
6962
7143
|
}
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
7144
|
+
// 桌面常驻栏与窄条形态都保留;只在手机端真的有 overlay drawer 时才收。
|
|
7145
|
+
// (旧条件 !sidebarPinned || isMobileLayout() 在桌面 not-pinned 状态下也会
|
|
7146
|
+
// 调 closeSessionsDrawer,靠内部 early-return 才不至于出错——含义不清晰,
|
|
7147
|
+
// 统一走 dismissDrawerIfOverlay 反过来表达"只收 overlay 不撤常驻"。)
|
|
7148
|
+
dismissDrawerIfOverlay();
|
|
6966
7149
|
}
|
|
6967
7150
|
|
|
6968
7151
|
function handleSessionItemClick(event) {
|
|
@@ -7097,7 +7280,8 @@
|
|
|
7097
7280
|
state.drafts[data.id] = "";
|
|
7098
7281
|
loadSessions().then(function() {
|
|
7099
7282
|
selectSession(data.id);
|
|
7100
|
-
|
|
7283
|
+
// 桌面常驻/窄条形态不要撤掉,只把手机端 overlay 收掉。
|
|
7284
|
+
dismissDrawerIfOverlay();
|
|
7101
7285
|
});
|
|
7102
7286
|
}
|
|
7103
7287
|
});
|
|
@@ -7131,7 +7315,8 @@
|
|
|
7131
7315
|
state.drafts[data.id] = "";
|
|
7132
7316
|
loadSessions().then(function() {
|
|
7133
7317
|
selectSession(data.id);
|
|
7134
|
-
|
|
7318
|
+
// 桌面常驻/窄条形态不要撤掉,只把手机端 overlay 收掉。
|
|
7319
|
+
dismissDrawerIfOverlay();
|
|
7135
7320
|
});
|
|
7136
7321
|
}
|
|
7137
7322
|
});
|
|
@@ -8197,6 +8382,7 @@
|
|
|
8197
8382
|
container.addEventListener("click", state.terminalClickHandler);
|
|
8198
8383
|
updateTerminalJumpToBottomButton();
|
|
8199
8384
|
initTerminalResizeHandle();
|
|
8385
|
+
initTerminalJoystick();
|
|
8200
8386
|
observeTerminalResize();
|
|
8201
8387
|
startTerminalHealthCheck();
|
|
8202
8388
|
// Container may have been hidden / zero-width at construction
|
|
@@ -9225,6 +9411,10 @@
|
|
|
9225
9411
|
var countEl = document.getElementById("session-count");
|
|
9226
9412
|
if (listEl) listEl.innerHTML = renderSessionsListContent();
|
|
9227
9413
|
if (countEl) countEl.textContent = String(state.sessions.length);
|
|
9414
|
+
// The docked history region lives outside #sessions-list — refresh it
|
|
9415
|
+
// too so callers that mutate state.claudeHistory (load complete,
|
|
9416
|
+
// delete, clear) don't need to know about it.
|
|
9417
|
+
updateClaudeHistoryRegion();
|
|
9228
9418
|
if (typeof hideCollapsedTileBubble === "function") hideCollapsedTileBubble();
|
|
9229
9419
|
updateShellChrome();
|
|
9230
9420
|
// Re-render cross-session queue (container may have been destroyed by DOM rebuild)
|
|
@@ -9519,6 +9709,19 @@
|
|
|
9519
9709
|
updateLayoutState();
|
|
9520
9710
|
}
|
|
9521
9711
|
|
|
9712
|
+
// 把"浮在内容上的 drawer/backdrop"关掉,但保留桌面常驻栏与窄条形态。
|
|
9713
|
+
// 用法:从 input focus / send 按钮 / 选中会话 / 新建会话回调里调,这些场
|
|
9714
|
+
// 景只想避免遮罩挡住内容,并不想撤掉用户主动开启的常驻侧栏。
|
|
9715
|
+
//
|
|
9716
|
+
// 直接调 closeSessionsDrawer() 会在桌面把 state.sidebarPinned 置 false,
|
|
9717
|
+
// 进而让 .pinned/.collapsed 这两个类一起脱落,窄条整体消失 —— 这是
|
|
9718
|
+
// sidebar-collapsed-tile 点击后侧栏整个不见的根因。
|
|
9719
|
+
function dismissDrawerIfOverlay() {
|
|
9720
|
+
if (isMobileLayout() && state.sessionsDrawerOpen) {
|
|
9721
|
+
closeSessionsDrawer();
|
|
9722
|
+
}
|
|
9723
|
+
}
|
|
9724
|
+
|
|
9522
9725
|
// 桌面 padding-left transition 结束后重新拟合终端尺寸。
|
|
9523
9726
|
// 抽出来给 toggleSessionsDrawer / closeSessionsDrawer / toggleSidebarCollapsed 复用。
|
|
9524
9727
|
function scheduleTerminalRefitAfterPaddingTransition() {
|
|
@@ -11336,7 +11539,9 @@
|
|
|
11336
11539
|
.then(function(data) {
|
|
11337
11540
|
saveWorkingDir(cwd);
|
|
11338
11541
|
closeSessionModal();
|
|
11339
|
-
|
|
11542
|
+
// 桌面常驻栏要保留:用户刚建完会话,希望左侧栏继续看到列表里的新条目,
|
|
11543
|
+
// 不能因为模态关闭顺手把 sidebarPinned 抹掉让侧栏整体消失。
|
|
11544
|
+
dismissDrawerIfOverlay();
|
|
11340
11545
|
return data;
|
|
11341
11546
|
})
|
|
11342
11547
|
.then(function() { focusInputBox(true); })
|
|
@@ -11382,7 +11587,8 @@
|
|
|
11382
11587
|
state.drafts[data.id] = "";
|
|
11383
11588
|
resetChatRenderCache();
|
|
11384
11589
|
closeSessionModal();
|
|
11385
|
-
|
|
11590
|
+
// 同 structured 路径:模态关闭后只收手机端的 overlay,保留桌面常驻侧栏。
|
|
11591
|
+
dismissDrawerIfOverlay();
|
|
11386
11592
|
return refreshAll();
|
|
11387
11593
|
})
|
|
11388
11594
|
.then(function() {
|
|
@@ -13610,6 +13816,48 @@
|
|
|
13610
13816
|
"_": 31
|
|
13611
13817
|
};
|
|
13612
13818
|
|
|
13819
|
+
// ── 终端悬浮摇杆遥控器常量与布局表 ──
|
|
13820
|
+
var JOYSTICK_LONG_PRESS_MS = 400; // 按住不动多久进入移动模式
|
|
13821
|
+
var JOYSTICK_MOVE_THRESHOLD = 10; // px:区分"拖动选键"与"静止长按"
|
|
13822
|
+
var JOYSTICK_TAP_THRESHOLD = 8; // px:快速点击的最大位移
|
|
13823
|
+
var JOYSTICK_REPEAT_MS = 130; // 内圈方向键连发间隔
|
|
13824
|
+
var JOYSTICK_R0 = 24; // 扇形中心空洞 = 死区半径
|
|
13825
|
+
var JOYSTICK_R1 = 60; // 内圈(方向)/外圈(功能)分界半径
|
|
13826
|
+
var JOYSTICK_R2 = 104; // 外圈外缘半径
|
|
13827
|
+
var JOYSTICK_DEADZONE_R = 24; // 命中死区(= R0)
|
|
13828
|
+
var JOYSTICK_RING_SPLIT_R = 60; // 命中分界(= R1):< 内圈方向,>= 外圈功能
|
|
13829
|
+
var JOYSTICK_MOVE_OUT_R = 140; // 拖出此半径(超出外圈区域)→ 切"正在移动"
|
|
13830
|
+
var JOYSTICK_BALL_SIZE = 52; // 球球直径(与 CSS 一致)
|
|
13831
|
+
var JOYSTICK_EDGE_MARGIN = 8; // 球球钳进视口的留白
|
|
13832
|
+
var JOYSTICK_RING_RADIUS = JOYSTICK_R2 + 8; // 环整体半径(含标签外延),用于把圆心钳进视口
|
|
13833
|
+
var JOYSTICK_RING_VIEW_PAD = 6; // 环外缘与视口边的最小留白
|
|
13834
|
+
var JOYSTICK_SECTOR_GAP_DEG = 2; // 外圈扇区之间的角度细缝(°),读成独立按钮
|
|
13835
|
+
// 内圈 4 方向:i=0 上、1 右、2 下、3 左(与渲染角 -90° 起顺时针一致)
|
|
13836
|
+
var JOYSTICK_INNER_KEYS = [
|
|
13837
|
+
{ key: "up", label: "↑" },
|
|
13838
|
+
{ key: "right", label: "→" },
|
|
13839
|
+
{ key: "down", label: "↓" },
|
|
13840
|
+
{ key: "left", label: "←" }
|
|
13841
|
+
];
|
|
13842
|
+
// 外圈 8 功能:i=0 正上方起顺时针,与八分扇区 idx 对应
|
|
13843
|
+
var JOYSTICK_OUTER_KEYS = [
|
|
13844
|
+
{ key: "enter", label: "Enter" },
|
|
13845
|
+
{ key: "escape", label: "Esc" },
|
|
13846
|
+
{ key: "tab", label: "Tab" },
|
|
13847
|
+
{ key: "shift_tab", label: "Shift+Tab" },
|
|
13848
|
+
{ key: "ctrl_c", label: "Ctrl+C" },
|
|
13849
|
+
{ key: "ctrl_z", label: "Ctrl+Z" },
|
|
13850
|
+
{ key: "ctrl_d", label: "Ctrl+D" },
|
|
13851
|
+
{ key: "ctrl_l", label: "Ctrl+L" }
|
|
13852
|
+
];
|
|
13853
|
+
// 钉住面板四角翻页键
|
|
13854
|
+
var JOYSTICK_CORNER_KEYS = [
|
|
13855
|
+
{ key: "pageup", label: "PgUp" },
|
|
13856
|
+
{ key: "home", label: "Home" },
|
|
13857
|
+
{ key: "pagedown", label: "PgDn" },
|
|
13858
|
+
{ key: "end", label: "End" }
|
|
13859
|
+
];
|
|
13860
|
+
|
|
13613
13861
|
var ignoredInteractiveTargetIds = new Set([
|
|
13614
13862
|
"mini-keyboard-fab",
|
|
13615
13863
|
"mini-keyboard-toggle",
|
|
@@ -13783,6 +14031,7 @@
|
|
|
13783
14031
|
}
|
|
13784
14032
|
var container = document.getElementById("output");
|
|
13785
14033
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
14034
|
+
updateJoystickVisibility();
|
|
13786
14035
|
}
|
|
13787
14036
|
|
|
13788
14037
|
// COPY-2/COPY-4: 是否存在落在终端输出区(#output)内的活动文本选区。用于:
|
|
@@ -14411,7 +14660,8 @@
|
|
|
14411
14660
|
persistSelectedId();
|
|
14412
14661
|
state.drafts[data.id] = "";
|
|
14413
14662
|
activateSession(data).then(function() {
|
|
14414
|
-
|
|
14663
|
+
// 桌面常驻/窄条形态不要撤掉,仅手机端 overlay 需要收。
|
|
14664
|
+
dismissDrawerIfOverlay();
|
|
14415
14665
|
});
|
|
14416
14666
|
}
|
|
14417
14667
|
})
|
|
@@ -15450,6 +15700,588 @@
|
|
|
15450
15700
|
document.addEventListener("touchend", state.resizeTouchEnd);
|
|
15451
15701
|
}
|
|
15452
15702
|
|
|
15703
|
+
// ====== 终端悬浮摇杆遥控器(手机端 PTY 遥控) ======
|
|
15704
|
+
// 纯前端覆盖层:fixed 挂到 body,绕开 #output 的 overflow:hidden 裁切。
|
|
15705
|
+
// 用 Pointer Events 统一鼠标/触摸;球球 touch-action:none 让 preventDefault 稳定。
|
|
15706
|
+
// 不改动终端背景的 touch/scroll/wheel —— 单指空白处仍是原生滚动看历史。
|
|
15707
|
+
|
|
15708
|
+
function isJoystickAvailable() {
|
|
15709
|
+
// 触屏与桌面网页端都显示(球球用 Pointer Events,鼠标拖拽同样可用)
|
|
15710
|
+
if (state.currentView !== "terminal") return false;
|
|
15711
|
+
var session = getSelectedSession();
|
|
15712
|
+
if (!session) return false;
|
|
15713
|
+
if (isStructuredSession(session)) return false;
|
|
15714
|
+
return true;
|
|
15715
|
+
}
|
|
15716
|
+
|
|
15717
|
+
function clampJoystickPos(pos) {
|
|
15718
|
+
var maxRight = Math.max(JOYSTICK_EDGE_MARGIN, window.innerWidth - JOYSTICK_BALL_SIZE - JOYSTICK_EDGE_MARGIN);
|
|
15719
|
+
var maxBottom = Math.max(JOYSTICK_EDGE_MARGIN, window.innerHeight - JOYSTICK_BALL_SIZE - JOYSTICK_EDGE_MARGIN);
|
|
15720
|
+
return {
|
|
15721
|
+
right: Math.min(Math.max(JOYSTICK_EDGE_MARGIN, pos.right), maxRight),
|
|
15722
|
+
bottom: Math.min(Math.max(JOYSTICK_EDGE_MARGIN, pos.bottom), maxBottom)
|
|
15723
|
+
};
|
|
15724
|
+
}
|
|
15725
|
+
|
|
15726
|
+
function applyJoystickPosition() {
|
|
15727
|
+
if (!state.joystickBallEl) return;
|
|
15728
|
+
var pos = clampJoystickPos(state.joystickPos || { right: 18, bottom: 96 });
|
|
15729
|
+
state.joystickBallEl.style.right = pos.right + "px";
|
|
15730
|
+
state.joystickBallEl.style.bottom = pos.bottom + "px";
|
|
15731
|
+
}
|
|
15732
|
+
|
|
15733
|
+
function saveJoystickPosition(right, bottom) {
|
|
15734
|
+
var pos = clampJoystickPos({ right: right, bottom: bottom });
|
|
15735
|
+
state.joystickPos = pos;
|
|
15736
|
+
try {
|
|
15737
|
+
localStorage.setItem("wand-ball-pos", JSON.stringify(pos));
|
|
15738
|
+
} catch (e) {
|
|
15739
|
+
// Ignore localStorage errors
|
|
15740
|
+
}
|
|
15741
|
+
}
|
|
15742
|
+
|
|
15743
|
+
function renderJoystickPanel() {
|
|
15744
|
+
function keyBtn(key, label, cls) {
|
|
15745
|
+
return '<button type="button" class="wjp-key' + (cls ? " " + cls : "") +
|
|
15746
|
+
'" data-key="' + key + '">' + label + "</button>";
|
|
15747
|
+
}
|
|
15748
|
+
var dpad =
|
|
15749
|
+
'<div class="wjp-dpad">' +
|
|
15750
|
+
'<div class="wjp-dpad-row">' + keyBtn("up", "↑", "wjp-dir") + "</div>" +
|
|
15751
|
+
'<div class="wjp-dpad-row">' +
|
|
15752
|
+
keyBtn("left", "←", "wjp-dir") + keyBtn("down", "↓", "wjp-dir") + keyBtn("right", "→", "wjp-dir") +
|
|
15753
|
+
"</div>" +
|
|
15754
|
+
"</div>";
|
|
15755
|
+
var fnRow = "";
|
|
15756
|
+
var i;
|
|
15757
|
+
for (i = 0; i < JOYSTICK_OUTER_KEYS.length; i++) {
|
|
15758
|
+
fnRow += keyBtn(JOYSTICK_OUTER_KEYS[i].key, JOYSTICK_OUTER_KEYS[i].label, "");
|
|
15759
|
+
}
|
|
15760
|
+
var cornerRow = "";
|
|
15761
|
+
for (i = 0; i < JOYSTICK_CORNER_KEYS.length; i++) {
|
|
15762
|
+
cornerRow += keyBtn(JOYSTICK_CORNER_KEYS[i].key, JOYSTICK_CORNER_KEYS[i].label, "");
|
|
15763
|
+
}
|
|
15764
|
+
var modRow = keyBtn("ctrl", "Ctrl", "wjp-mod") + keyBtn("alt", "Alt", "wjp-mod");
|
|
15765
|
+
return '<div class="wjp-title">遥控面板</div>' +
|
|
15766
|
+
dpad +
|
|
15767
|
+
'<div class="wjp-grid wjp-fnkeys">' + fnRow + "</div>" +
|
|
15768
|
+
'<div class="wjp-grid wjp-corners">' + cornerRow + "</div>" +
|
|
15769
|
+
'<div class="wjp-grid wjp-mods">' + modRow + "</div>";
|
|
15770
|
+
}
|
|
15771
|
+
|
|
15772
|
+
function joystickPolar(r, deg) {
|
|
15773
|
+
var a = deg * Math.PI / 180;
|
|
15774
|
+
return { x: +(r * Math.cos(a)).toFixed(2), y: +(r * Math.sin(a)).toFixed(2) };
|
|
15775
|
+
}
|
|
15776
|
+
|
|
15777
|
+
// 环形扇区(annular sector)路径:外弧顺时针、内弧逆时针闭合
|
|
15778
|
+
function joystickSectorPath(rIn, rOut, startDeg, endDeg) {
|
|
15779
|
+
var a = joystickPolar(rOut, startDeg), b = joystickPolar(rOut, endDeg);
|
|
15780
|
+
var c = joystickPolar(rIn, endDeg), d = joystickPolar(rIn, startDeg);
|
|
15781
|
+
return "M" + a.x + " " + a.y +
|
|
15782
|
+
"A" + rOut + " " + rOut + " 0 0 1 " + b.x + " " + b.y +
|
|
15783
|
+
"L" + c.x + " " + c.y +
|
|
15784
|
+
"A" + rIn + " " + rIn + " 0 0 0 " + d.x + " " + d.y + "Z";
|
|
15785
|
+
}
|
|
15786
|
+
|
|
15787
|
+
// 标签渲染:组合键(含 "+")拆成两行,前缀在上、+键在下,省横向空间更清晰。
|
|
15788
|
+
function joystickLabelMarkup(label, x, y) {
|
|
15789
|
+
var plus = label.indexOf("+");
|
|
15790
|
+
if (plus > 0) {
|
|
15791
|
+
var top = label.slice(0, plus);
|
|
15792
|
+
var bot = "+" + label.slice(plus + 1);
|
|
15793
|
+
return '<text class="wjr-2line" x="' + x + '" y="' + y + '">' +
|
|
15794
|
+
'<tspan x="' + x + '" dy="-0.42em">' + top + '</tspan>' +
|
|
15795
|
+
'<tspan x="' + x + '" dy="0.95em">' + bot + '</tspan></text>';
|
|
15796
|
+
}
|
|
15797
|
+
return '<text x="' + x + '" y="' + y + '">' + label + "</text>";
|
|
15798
|
+
}
|
|
15799
|
+
|
|
15800
|
+
// 构建两圈扇形 pie 菜单 SVG:内圈 4×90° 方向、外圈 8×45° 功能 + 中心选中提示。
|
|
15801
|
+
// 扇区角度与 joystickHitTest 完全对应(正上=0、顺时针)。
|
|
15802
|
+
function buildJoystickRingSvg() {
|
|
15803
|
+
var size = (JOYSTICK_R2 + 6) * 2;
|
|
15804
|
+
var half = size / 2;
|
|
15805
|
+
var gap = JOYSTICK_SECTOR_GAP_DEG / 2; // 每个扇区起止各内缩半个细缝
|
|
15806
|
+
var svg = '<svg class="wjr-svg" width="' + size + '" height="' + size +
|
|
15807
|
+
'" viewBox="' + (-half) + " " + (-half) + " " + size + " " + size + '">';
|
|
15808
|
+
// 底盘:所有扇区之下的一整块玻璃圆盘,细缝/外缘透出它作分隔与外圈光环
|
|
15809
|
+
svg += '<circle class="wjr-base" cx="0" cy="0" r="' + (JOYSTICK_R2 + 4) + '"/>';
|
|
15810
|
+
var i, k, center, lp;
|
|
15811
|
+
for (i = 0; i < JOYSTICK_INNER_KEYS.length; i++) {
|
|
15812
|
+
k = JOYSTICK_INNER_KEYS[i];
|
|
15813
|
+
center = -90 + i * 90;
|
|
15814
|
+
lp = joystickPolar((JOYSTICK_R0 + JOYSTICK_R1) / 2, center);
|
|
15815
|
+
svg += '<g class="wjr-sector wjr-inner" data-key="' + k.key + '">' +
|
|
15816
|
+
'<path d="' + joystickSectorPath(JOYSTICK_R0, JOYSTICK_R1, center - 45 + gap, center + 45 - gap) + '"/>' +
|
|
15817
|
+
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
15818
|
+
}
|
|
15819
|
+
for (i = 0; i < JOYSTICK_OUTER_KEYS.length; i++) {
|
|
15820
|
+
k = JOYSTICK_OUTER_KEYS[i];
|
|
15821
|
+
center = -90 + i * 45;
|
|
15822
|
+
lp = joystickPolar((JOYSTICK_R1 + JOYSTICK_R2) / 2, center);
|
|
15823
|
+
svg += '<g class="wjr-sector wjr-outer" data-key="' + k.key + '">' +
|
|
15824
|
+
'<path d="' + joystickSectorPath(JOYSTICK_R1, JOYSTICK_R2, center - 22.5 + gap, center + 22.5 - gap) + '"/>' +
|
|
15825
|
+
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
15826
|
+
}
|
|
15827
|
+
svg += '<circle class="wjr-hub" cx="0" cy="0" r="' + (JOYSTICK_R0 - 1) + '"/>';
|
|
15828
|
+
svg += '<text class="wjr-hub-label" x="0" y="0"></text>';
|
|
15829
|
+
return svg + "</svg>";
|
|
15830
|
+
}
|
|
15831
|
+
|
|
15832
|
+
function joystickLabelForKey(key) {
|
|
15833
|
+
var i;
|
|
15834
|
+
for (i = 0; i < JOYSTICK_INNER_KEYS.length; i++) {
|
|
15835
|
+
if (JOYSTICK_INNER_KEYS[i].key === key) return JOYSTICK_INNER_KEYS[i].label;
|
|
15836
|
+
}
|
|
15837
|
+
for (i = 0; i < JOYSTICK_OUTER_KEYS.length; i++) {
|
|
15838
|
+
if (JOYSTICK_OUTER_KEYS[i].key === key) return JOYSTICK_OUTER_KEYS[i].label;
|
|
15839
|
+
}
|
|
15840
|
+
return "";
|
|
15841
|
+
}
|
|
15842
|
+
|
|
15843
|
+
function setJoystickCenterLabel(text) {
|
|
15844
|
+
if (!state.joystickRingEl) return;
|
|
15845
|
+
var el = state.joystickRingEl.querySelector(".wjr-hub-label");
|
|
15846
|
+
if (el) el.textContent = text || "";
|
|
15847
|
+
}
|
|
15848
|
+
|
|
15849
|
+
function joystickHaptic(ms) {
|
|
15850
|
+
try { if (navigator.vibrate) navigator.vibrate(ms); } catch (e) {}
|
|
15851
|
+
}
|
|
15852
|
+
|
|
15853
|
+
function initTerminalJoystick() {
|
|
15854
|
+
if (state.joystickRootEl) return; // 已存在不重复建(触屏/桌面均构建)
|
|
15855
|
+
|
|
15856
|
+
var root = document.createElement("div");
|
|
15857
|
+
root.className = "wand-joystick-root";
|
|
15858
|
+
|
|
15859
|
+
var backdrop = document.createElement("div");
|
|
15860
|
+
backdrop.className = "wand-joystick-backdrop";
|
|
15861
|
+
root.appendChild(backdrop);
|
|
15862
|
+
|
|
15863
|
+
// 环形菜单容器(圆心运行期对齐球球中心)—— 扇形 pie 菜单(SVG,带文字 + 中心提示)
|
|
15864
|
+
var ring = document.createElement("div");
|
|
15865
|
+
ring.className = "wand-joystick-ring";
|
|
15866
|
+
ring.innerHTML = buildJoystickRingSvg();
|
|
15867
|
+
root.appendChild(ring);
|
|
15868
|
+
|
|
15869
|
+
// 钉住面板
|
|
15870
|
+
var panel = document.createElement("div");
|
|
15871
|
+
panel.className = "wand-joystick-panel";
|
|
15872
|
+
panel.innerHTML = renderJoystickPanel();
|
|
15873
|
+
panel.addEventListener("click", onJoystickPanelClick);
|
|
15874
|
+
root.appendChild(panel);
|
|
15875
|
+
|
|
15876
|
+
// 球球本体
|
|
15877
|
+
var ball = document.createElement("div");
|
|
15878
|
+
ball.className = "wand-joystick-ball";
|
|
15879
|
+
ball.setAttribute("role", "button");
|
|
15880
|
+
ball.setAttribute("aria-label", "终端摇杆遥控");
|
|
15881
|
+
ball.innerHTML = '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" ' +
|
|
15882
|
+
'stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
15883
|
+
'<circle cx="12" cy="12" r="3"/><path d="M12 5V3M12 21v-2M5 12H3M21 12h-2"/></svg>';
|
|
15884
|
+
root.appendChild(ball);
|
|
15885
|
+
|
|
15886
|
+
document.body.appendChild(root);
|
|
15887
|
+
|
|
15888
|
+
state.joystickRootEl = root;
|
|
15889
|
+
state.joystickBackdropEl = backdrop;
|
|
15890
|
+
state.joystickRingEl = ring;
|
|
15891
|
+
state.joystickPanelEl = panel;
|
|
15892
|
+
state.joystickBallEl = ball;
|
|
15893
|
+
|
|
15894
|
+
applyJoystickPosition();
|
|
15895
|
+
|
|
15896
|
+
ball.addEventListener("pointerdown", onJoystickPointerDown);
|
|
15897
|
+
backdrop.addEventListener("pointerdown", function(e) {
|
|
15898
|
+
// 钉住面板开着且无进行中手势时,点遮罩收起面板
|
|
15899
|
+
if (state.joystickPinnedOpen && state.joystickGesture == null) {
|
|
15900
|
+
e.preventDefault();
|
|
15901
|
+
closeJoystickPanel();
|
|
15902
|
+
}
|
|
15903
|
+
});
|
|
15904
|
+
|
|
15905
|
+
// 旋转/窗口尺寸变化时重新钳制球球位置
|
|
15906
|
+
state.joystickResizeHandler = function() { applyJoystickPosition(); };
|
|
15907
|
+
window.addEventListener("resize", state.joystickResizeHandler);
|
|
15908
|
+
window.addEventListener("orientationchange", state.joystickResizeHandler);
|
|
15909
|
+
|
|
15910
|
+
updateJoystickVisibility();
|
|
15911
|
+
}
|
|
15912
|
+
|
|
15913
|
+
function getJoystickCenter() {
|
|
15914
|
+
if (!state.joystickBallEl) return { x: 0, y: 0 };
|
|
15915
|
+
var r = state.joystickBallEl.getBoundingClientRect();
|
|
15916
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
15917
|
+
}
|
|
15918
|
+
|
|
15919
|
+
function onJoystickPointerDown(e) {
|
|
15920
|
+
if (!isJoystickAvailable()) return;
|
|
15921
|
+
if (state.joystickPointerId !== null) return; // 已有手势在进行
|
|
15922
|
+
e.preventDefault();
|
|
15923
|
+
e.stopPropagation();
|
|
15924
|
+
state.joystickPointerId = e.pointerId;
|
|
15925
|
+
state.joystickPressStart = { x: e.clientX, y: e.clientY, t: Date.now() };
|
|
15926
|
+
state.joystickGesture = "pending";
|
|
15927
|
+
state.joystickHoverOuter = null;
|
|
15928
|
+
state.joystickCenter = getJoystickCenter();
|
|
15929
|
+
try { state.joystickBallEl.setPointerCapture(e.pointerId); } catch (err) {}
|
|
15930
|
+
// 起长按定时器:不动到 400ms → 移动模式
|
|
15931
|
+
state.joystickLongPressTimer = setTimeout(function() {
|
|
15932
|
+
if (state.joystickGesture === "pending") enterJoystickMoveMode();
|
|
15933
|
+
}, JOYSTICK_LONG_PRESS_MS);
|
|
15934
|
+
state.joystickMoveHandler = onJoystickPointerMove;
|
|
15935
|
+
state.joystickUpHandler = onJoystickPointerUp;
|
|
15936
|
+
document.addEventListener("pointermove", state.joystickMoveHandler);
|
|
15937
|
+
document.addEventListener("pointerup", state.joystickUpHandler);
|
|
15938
|
+
document.addEventListener("pointercancel", state.joystickUpHandler);
|
|
15939
|
+
}
|
|
15940
|
+
|
|
15941
|
+
function enterJoystickMoveMode() {
|
|
15942
|
+
state.joystickGesture = "move";
|
|
15943
|
+
if (state.joystickPinnedOpen) closeJoystickPanel();
|
|
15944
|
+
if (state.joystickBallEl) state.joystickBallEl.classList.add("dragging");
|
|
15945
|
+
if (state.joystickBackdropEl) state.joystickBackdropEl.classList.add("active");
|
|
15946
|
+
}
|
|
15947
|
+
|
|
15948
|
+
function moveJoystickBallTo(clientX, clientY) {
|
|
15949
|
+
if (!state.joystickBallEl) return;
|
|
15950
|
+
var pos = clampJoystickPos({
|
|
15951
|
+
right: window.innerWidth - clientX - JOYSTICK_BALL_SIZE / 2,
|
|
15952
|
+
bottom: window.innerHeight - clientY - JOYSTICK_BALL_SIZE / 2
|
|
15953
|
+
});
|
|
15954
|
+
state.joystickBallEl.style.right = pos.right + "px";
|
|
15955
|
+
state.joystickBallEl.style.bottom = pos.bottom + "px";
|
|
15956
|
+
}
|
|
15957
|
+
|
|
15958
|
+
// 环形手势里把手指往外拖出外圈区域时调用:收起环,切到"正在移动"状态,
|
|
15959
|
+
// 球球立刻挪到手指下,之后跟手慢慢移动,松手保存位置。
|
|
15960
|
+
function switchJoystickToMoveMode(e) {
|
|
15961
|
+
stopJoystickRepeat();
|
|
15962
|
+
state.joystickHoverOuter = null;
|
|
15963
|
+
closeJoystickRing();
|
|
15964
|
+
state.joystickGesture = "move";
|
|
15965
|
+
if (state.joystickBallEl) state.joystickBallEl.classList.add("dragging");
|
|
15966
|
+
if (state.joystickBackdropEl) state.joystickBackdropEl.classList.add("active");
|
|
15967
|
+
joystickHaptic(18);
|
|
15968
|
+
moveJoystickBallTo(e.clientX, e.clientY);
|
|
15969
|
+
}
|
|
15970
|
+
|
|
15971
|
+
function onJoystickPointerMove(e) {
|
|
15972
|
+
if (e.pointerId !== state.joystickPointerId) return;
|
|
15973
|
+
if (!state.joystickBallEl) return;
|
|
15974
|
+
e.preventDefault();
|
|
15975
|
+
var dxStart = e.clientX - state.joystickPressStart.x;
|
|
15976
|
+
var dyStart = e.clientY - state.joystickPressStart.y;
|
|
15977
|
+
if (state.joystickGesture === "pending") {
|
|
15978
|
+
if (Math.sqrt(dxStart * dxStart + dyStart * dyStart) > JOYSTICK_MOVE_THRESHOLD) {
|
|
15979
|
+
// 先动 → 选键手势
|
|
15980
|
+
if (state.joystickLongPressTimer) {
|
|
15981
|
+
clearTimeout(state.joystickLongPressTimer);
|
|
15982
|
+
state.joystickLongPressTimer = null;
|
|
15983
|
+
}
|
|
15984
|
+
if (state.joystickPinnedOpen) closeJoystickPanel();
|
|
15985
|
+
state.joystickGesture = "ring";
|
|
15986
|
+
state.joystickCenter = getJoystickCenter();
|
|
15987
|
+
openJoystickRing();
|
|
15988
|
+
} else {
|
|
15989
|
+
return;
|
|
15990
|
+
}
|
|
15991
|
+
}
|
|
15992
|
+
if (state.joystickGesture === "ring") {
|
|
15993
|
+
var c = state.joystickCenter || getJoystickCenter();
|
|
15994
|
+
var rdx = e.clientX - c.x;
|
|
15995
|
+
var rdy = e.clientY - c.y;
|
|
15996
|
+
// 往外拖超出外圈区域 → 切到"正在移动"状态,球球开始跟手
|
|
15997
|
+
if (Math.sqrt(rdx * rdx + rdy * rdy) > JOYSTICK_MOVE_OUT_R) {
|
|
15998
|
+
switchJoystickToMoveMode(e);
|
|
15999
|
+
return;
|
|
16000
|
+
}
|
|
16001
|
+
applyJoystickRingHit(joystickHitTest(rdx, rdy));
|
|
16002
|
+
return;
|
|
16003
|
+
}
|
|
16004
|
+
if (state.joystickGesture === "move") {
|
|
16005
|
+
moveJoystickBallTo(e.clientX, e.clientY);
|
|
16006
|
+
return;
|
|
16007
|
+
}
|
|
16008
|
+
}
|
|
16009
|
+
|
|
16010
|
+
function onJoystickPointerUp(e) {
|
|
16011
|
+
if (e.pointerId !== state.joystickPointerId) return;
|
|
16012
|
+
if (state.joystickLongPressTimer) {
|
|
16013
|
+
clearTimeout(state.joystickLongPressTimer);
|
|
16014
|
+
state.joystickLongPressTimer = null;
|
|
16015
|
+
}
|
|
16016
|
+
var gesture = state.joystickGesture;
|
|
16017
|
+
if (gesture === "ring") {
|
|
16018
|
+
stopJoystickRepeat();
|
|
16019
|
+
if (state.joystickHoverOuter) {
|
|
16020
|
+
joystickHaptic(18);
|
|
16021
|
+
sendJoystickKey(state.joystickHoverOuter);
|
|
16022
|
+
}
|
|
16023
|
+
} else if (gesture === "pending") {
|
|
16024
|
+
var dx = e.clientX - state.joystickPressStart.x;
|
|
16025
|
+
var dy = e.clientY - state.joystickPressStart.y;
|
|
16026
|
+
if (Math.sqrt(dx * dx + dy * dy) <= JOYSTICK_TAP_THRESHOLD) toggleJoystickPanel();
|
|
16027
|
+
} else if (gesture === "move") {
|
|
16028
|
+
var r = state.joystickBallEl ? state.joystickBallEl.getBoundingClientRect() : null;
|
|
16029
|
+
if (r) saveJoystickPosition(window.innerWidth - r.right, window.innerHeight - r.bottom);
|
|
16030
|
+
}
|
|
16031
|
+
endJoystickGesture();
|
|
16032
|
+
}
|
|
16033
|
+
|
|
16034
|
+
function endJoystickGesture() {
|
|
16035
|
+
stopJoystickRepeat();
|
|
16036
|
+
if (state.joystickLongPressTimer) {
|
|
16037
|
+
clearTimeout(state.joystickLongPressTimer);
|
|
16038
|
+
state.joystickLongPressTimer = null;
|
|
16039
|
+
}
|
|
16040
|
+
if (state.joystickBallEl && state.joystickPointerId !== null) {
|
|
16041
|
+
try { state.joystickBallEl.releasePointerCapture(state.joystickPointerId); } catch (err) {}
|
|
16042
|
+
}
|
|
16043
|
+
if (state.joystickMoveHandler) {
|
|
16044
|
+
document.removeEventListener("pointermove", state.joystickMoveHandler);
|
|
16045
|
+
state.joystickMoveHandler = null;
|
|
16046
|
+
}
|
|
16047
|
+
if (state.joystickUpHandler) {
|
|
16048
|
+
document.removeEventListener("pointerup", state.joystickUpHandler);
|
|
16049
|
+
document.removeEventListener("pointercancel", state.joystickUpHandler);
|
|
16050
|
+
state.joystickUpHandler = null;
|
|
16051
|
+
}
|
|
16052
|
+
closeJoystickRing();
|
|
16053
|
+
if (state.joystickBallEl) state.joystickBallEl.classList.remove("dragging");
|
|
16054
|
+
// 钉住面板若仍开着则保留遮罩,否则移除
|
|
16055
|
+
if (state.joystickBackdropEl && !state.joystickPinnedOpen) {
|
|
16056
|
+
state.joystickBackdropEl.classList.remove("active");
|
|
16057
|
+
}
|
|
16058
|
+
state.joystickPointerId = null;
|
|
16059
|
+
state.joystickGesture = null;
|
|
16060
|
+
state.joystickHoverOuter = null;
|
|
16061
|
+
state.joystickLastHoverKey = null;
|
|
16062
|
+
state.joystickPressStart = null;
|
|
16063
|
+
state.joystickCenter = null;
|
|
16064
|
+
}
|
|
16065
|
+
|
|
16066
|
+
function joystickHitTest(dx, dy) {
|
|
16067
|
+
var r = Math.sqrt(dx * dx + dy * dy);
|
|
16068
|
+
if (r < JOYSTICK_DEADZONE_R) return { zone: "dead", key: null };
|
|
16069
|
+
if (r < JOYSTICK_RING_SPLIT_R) {
|
|
16070
|
+
// 内圈:主轴象限(往上滑 dy<0 = up)
|
|
16071
|
+
if (Math.abs(dy) >= Math.abs(dx)) return { zone: "inner", key: dy < 0 ? "up" : "down" };
|
|
16072
|
+
return { zone: "inner", key: dx < 0 ? "left" : "right" };
|
|
16073
|
+
}
|
|
16074
|
+
// 外圈:8 等分扇区,正上方为 0,顺时针递增;+π/8 让扇区中心对准按钮
|
|
16075
|
+
var ang = Math.atan2(dx, -dy);
|
|
16076
|
+
if (ang < 0) ang += Math.PI * 2;
|
|
16077
|
+
var idx = Math.floor((ang + Math.PI / 8) / (Math.PI / 4)) % 8;
|
|
16078
|
+
return { zone: "outer", key: JOYSTICK_OUTER_KEYS[idx].key };
|
|
16079
|
+
}
|
|
16080
|
+
|
|
16081
|
+
function applyJoystickRingHit(hit) {
|
|
16082
|
+
if (!state.joystickRingEl) return;
|
|
16083
|
+
var key = hit.zone === "dead" ? null : hit.key;
|
|
16084
|
+
if (key !== state.joystickLastHoverKey) { // 切换扇区 → 轻震反馈
|
|
16085
|
+
state.joystickLastHoverKey = key;
|
|
16086
|
+
joystickHaptic(8);
|
|
16087
|
+
}
|
|
16088
|
+
if (hit.zone === "inner") {
|
|
16089
|
+
state.joystickHoverOuter = null;
|
|
16090
|
+
setJoystickOuterHighlight(null);
|
|
16091
|
+
startJoystickRepeat(hit.key);
|
|
16092
|
+
setJoystickInnerHighlight(hit.key);
|
|
16093
|
+
setJoystickCenterLabel(joystickLabelForKey(hit.key));
|
|
16094
|
+
} else if (hit.zone === "outer") {
|
|
16095
|
+
stopJoystickRepeat();
|
|
16096
|
+
setJoystickInnerHighlight(null);
|
|
16097
|
+
state.joystickHoverOuter = hit.key;
|
|
16098
|
+
setJoystickOuterHighlight(hit.key);
|
|
16099
|
+
setJoystickCenterLabel(joystickLabelForKey(hit.key));
|
|
16100
|
+
} else {
|
|
16101
|
+
stopJoystickRepeat();
|
|
16102
|
+
setJoystickInnerHighlight(null);
|
|
16103
|
+
state.joystickHoverOuter = null;
|
|
16104
|
+
setJoystickOuterHighlight(null);
|
|
16105
|
+
setJoystickCenterLabel("取消");
|
|
16106
|
+
}
|
|
16107
|
+
}
|
|
16108
|
+
|
|
16109
|
+
function setJoystickInnerHighlight(key) {
|
|
16110
|
+
if (!state.joystickRingEl) return;
|
|
16111
|
+
var btns = state.joystickRingEl.querySelectorAll(".wjr-inner");
|
|
16112
|
+
for (var i = 0; i < btns.length; i++) {
|
|
16113
|
+
btns[i].classList.toggle("is-repeating", btns[i].getAttribute("data-key") === key);
|
|
16114
|
+
}
|
|
16115
|
+
}
|
|
16116
|
+
|
|
16117
|
+
function setJoystickOuterHighlight(key) {
|
|
16118
|
+
if (!state.joystickRingEl) return;
|
|
16119
|
+
var btns = state.joystickRingEl.querySelectorAll(".wjr-outer");
|
|
16120
|
+
for (var i = 0; i < btns.length; i++) {
|
|
16121
|
+
btns[i].classList.toggle("is-hover", btns[i].getAttribute("data-key") === key);
|
|
16122
|
+
}
|
|
16123
|
+
}
|
|
16124
|
+
|
|
16125
|
+
// 把环圆心钳进视口,保证整圈(含标签)不被屏幕边裁掉。视口比环还小则回退到正中。
|
|
16126
|
+
function clampJoystickRingCenter(c) {
|
|
16127
|
+
var pad = JOYSTICK_RING_RADIUS + JOYSTICK_RING_VIEW_PAD;
|
|
16128
|
+
var vw = window.innerWidth, vh = window.innerHeight;
|
|
16129
|
+
return {
|
|
16130
|
+
x: vw < pad * 2 ? vw / 2 : Math.min(Math.max(pad, c.x), vw - pad),
|
|
16131
|
+
y: vh < pad * 2 ? vh / 2 : Math.min(Math.max(pad, c.y), vh - pad)
|
|
16132
|
+
};
|
|
16133
|
+
}
|
|
16134
|
+
|
|
16135
|
+
function openJoystickRing() {
|
|
16136
|
+
if (!state.joystickRingEl) return;
|
|
16137
|
+
// 圆心钳进视口后写回 state.joystickCenter:球球此刻已 is-ringing(opacity:0),
|
|
16138
|
+
// 圆心内移不露馅,且命中测试与可见环始终对齐。
|
|
16139
|
+
var c = clampJoystickRingCenter(state.joystickCenter || getJoystickCenter());
|
|
16140
|
+
state.joystickCenter = c;
|
|
16141
|
+
state.joystickRingEl.style.left = c.x + "px";
|
|
16142
|
+
state.joystickRingEl.style.top = c.y + "px";
|
|
16143
|
+
state.joystickRingEl.classList.add("active");
|
|
16144
|
+
state.joystickLastHoverKey = null;
|
|
16145
|
+
setJoystickCenterLabel("取消"); // 初始在死区,提示松手取消
|
|
16146
|
+
if (state.joystickBallEl) state.joystickBallEl.classList.add("is-ringing"); // 隐球球露中心
|
|
16147
|
+
if (state.joystickBackdropEl) state.joystickBackdropEl.classList.add("active");
|
|
16148
|
+
joystickHaptic(10);
|
|
16149
|
+
}
|
|
16150
|
+
|
|
16151
|
+
function closeJoystickRing() {
|
|
16152
|
+
if (state.joystickRingEl) state.joystickRingEl.classList.remove("active");
|
|
16153
|
+
if (state.joystickBallEl) state.joystickBallEl.classList.remove("is-ringing");
|
|
16154
|
+
setJoystickInnerHighlight(null);
|
|
16155
|
+
setJoystickOuterHighlight(null);
|
|
16156
|
+
}
|
|
16157
|
+
|
|
16158
|
+
function startJoystickRepeat(key) {
|
|
16159
|
+
if (state.joystickRepeatKey === key) return; // 同方向不重启,保持节奏
|
|
16160
|
+
stopJoystickRepeat();
|
|
16161
|
+
state.joystickRepeatKey = key;
|
|
16162
|
+
sendJoystickKey(key); // 立即发一次
|
|
16163
|
+
state.joystickRepeatTimer = setInterval(function() {
|
|
16164
|
+
if (state.joystickRepeatKey) sendJoystickKey(state.joystickRepeatKey);
|
|
16165
|
+
}, JOYSTICK_REPEAT_MS);
|
|
16166
|
+
}
|
|
16167
|
+
|
|
16168
|
+
function stopJoystickRepeat() {
|
|
16169
|
+
if (state.joystickRepeatTimer) {
|
|
16170
|
+
clearInterval(state.joystickRepeatTimer);
|
|
16171
|
+
state.joystickRepeatTimer = null;
|
|
16172
|
+
}
|
|
16173
|
+
state.joystickRepeatKey = null;
|
|
16174
|
+
}
|
|
16175
|
+
|
|
16176
|
+
function sendJoystickKey(key) {
|
|
16177
|
+
if (key === "ctrl" || key === "alt" || key === "shift") {
|
|
16178
|
+
state.modifiers[key] = !state.modifiers[key];
|
|
16179
|
+
updateJoystickPanelUI();
|
|
16180
|
+
return;
|
|
16181
|
+
}
|
|
16182
|
+
var seq = buildPtySequence(key, {
|
|
16183
|
+
ctrl: state.modifiers.ctrl,
|
|
16184
|
+
alt: state.modifiers.alt,
|
|
16185
|
+
shift: state.modifiers.shift
|
|
16186
|
+
});
|
|
16187
|
+
if (seq) sendTerminalSequence(seq, key);
|
|
16188
|
+
clearModifiers(); // 发后自动清修饰键(应用到下一个发送的键)
|
|
16189
|
+
updateJoystickPanelUI();
|
|
16190
|
+
scheduleShortcutResync();
|
|
16191
|
+
}
|
|
16192
|
+
|
|
16193
|
+
function toggleJoystickPanel() {
|
|
16194
|
+
if (state.joystickPinnedOpen) closeJoystickPanel();
|
|
16195
|
+
else openJoystickPanel();
|
|
16196
|
+
}
|
|
16197
|
+
|
|
16198
|
+
function openJoystickPanel() {
|
|
16199
|
+
if (!state.joystickPanelEl || !state.joystickBallEl) return;
|
|
16200
|
+
state.joystickPinnedOpen = true;
|
|
16201
|
+
var r = state.joystickBallEl.getBoundingClientRect();
|
|
16202
|
+
// 面板锚定在球球上方(球球在右下→面板往左上展开),贴右/底边对齐
|
|
16203
|
+
state.joystickPanelEl.style.right = Math.max(JOYSTICK_EDGE_MARGIN, window.innerWidth - r.right) + "px";
|
|
16204
|
+
state.joystickPanelEl.style.bottom = Math.max(JOYSTICK_EDGE_MARGIN, window.innerHeight - r.top + 10) + "px";
|
|
16205
|
+
state.joystickPanelEl.classList.add("active");
|
|
16206
|
+
if (state.joystickBackdropEl) state.joystickBackdropEl.classList.add("active");
|
|
16207
|
+
updateJoystickPanelUI();
|
|
16208
|
+
}
|
|
16209
|
+
|
|
16210
|
+
function closeJoystickPanel() {
|
|
16211
|
+
state.joystickPinnedOpen = false;
|
|
16212
|
+
if (state.joystickPanelEl) state.joystickPanelEl.classList.remove("active");
|
|
16213
|
+
if (state.joystickBackdropEl && state.joystickGesture == null) {
|
|
16214
|
+
state.joystickBackdropEl.classList.remove("active");
|
|
16215
|
+
}
|
|
16216
|
+
}
|
|
16217
|
+
|
|
16218
|
+
function updateJoystickPanelUI() {
|
|
16219
|
+
if (!state.joystickPanelEl) return;
|
|
16220
|
+
["ctrl", "alt"].forEach(function(name) {
|
|
16221
|
+
var btn = state.joystickPanelEl.querySelector('.wjp-mod[data-key="' + name + '"]');
|
|
16222
|
+
if (btn) btn.classList.toggle("active", !!state.modifiers[name]);
|
|
16223
|
+
});
|
|
16224
|
+
}
|
|
16225
|
+
|
|
16226
|
+
function onJoystickPanelClick(e) {
|
|
16227
|
+
var btn = e.target && e.target.closest ? e.target.closest(".wjp-key") : null;
|
|
16228
|
+
if (!btn) return;
|
|
16229
|
+
e.preventDefault();
|
|
16230
|
+
e.stopPropagation();
|
|
16231
|
+
var key = btn.getAttribute("data-key");
|
|
16232
|
+
if (key) sendJoystickKey(key);
|
|
16233
|
+
}
|
|
16234
|
+
|
|
16235
|
+
function updateJoystickVisibility() {
|
|
16236
|
+
var root = state.joystickRootEl;
|
|
16237
|
+
if (!root) return;
|
|
16238
|
+
var available = isJoystickAvailable();
|
|
16239
|
+
root.classList.toggle("visible", available);
|
|
16240
|
+
if (!available) {
|
|
16241
|
+
// 不可用:强制收手势 + 收面板 + 停连发 + 清修饰键,杜绝残留
|
|
16242
|
+
if (state.joystickPointerId !== null || state.joystickGesture) endJoystickGesture();
|
|
16243
|
+
stopJoystickRepeat();
|
|
16244
|
+
if (state.joystickPinnedOpen) closeJoystickPanel();
|
|
16245
|
+
if (state.joystickBackdropEl) state.joystickBackdropEl.classList.remove("active");
|
|
16246
|
+
}
|
|
16247
|
+
}
|
|
16248
|
+
|
|
16249
|
+
function teardownJoystick() {
|
|
16250
|
+
stopJoystickRepeat();
|
|
16251
|
+
if (state.joystickLongPressTimer) {
|
|
16252
|
+
clearTimeout(state.joystickLongPressTimer);
|
|
16253
|
+
state.joystickLongPressTimer = null;
|
|
16254
|
+
}
|
|
16255
|
+
if (state.joystickMoveHandler) {
|
|
16256
|
+
document.removeEventListener("pointermove", state.joystickMoveHandler);
|
|
16257
|
+
state.joystickMoveHandler = null;
|
|
16258
|
+
}
|
|
16259
|
+
if (state.joystickUpHandler) {
|
|
16260
|
+
document.removeEventListener("pointerup", state.joystickUpHandler);
|
|
16261
|
+
document.removeEventListener("pointercancel", state.joystickUpHandler);
|
|
16262
|
+
state.joystickUpHandler = null;
|
|
16263
|
+
}
|
|
16264
|
+
if (state.joystickResizeHandler) {
|
|
16265
|
+
window.removeEventListener("resize", state.joystickResizeHandler);
|
|
16266
|
+
window.removeEventListener("orientationchange", state.joystickResizeHandler);
|
|
16267
|
+
state.joystickResizeHandler = null;
|
|
16268
|
+
}
|
|
16269
|
+
if (state.joystickRootEl && state.joystickRootEl.parentNode) {
|
|
16270
|
+
state.joystickRootEl.parentNode.removeChild(state.joystickRootEl);
|
|
16271
|
+
}
|
|
16272
|
+
state.joystickRootEl = null;
|
|
16273
|
+
state.joystickRingEl = null;
|
|
16274
|
+
state.joystickPanelEl = null;
|
|
16275
|
+
state.joystickBackdropEl = null;
|
|
16276
|
+
state.joystickBallEl = null;
|
|
16277
|
+
state.joystickPointerId = null;
|
|
16278
|
+
state.joystickGesture = null;
|
|
16279
|
+
state.joystickPressStart = null;
|
|
16280
|
+
state.joystickHoverOuter = null;
|
|
16281
|
+
state.joystickCenter = null;
|
|
16282
|
+
state.joystickPinnedOpen = false;
|
|
16283
|
+
}
|
|
16284
|
+
|
|
15453
16285
|
function observeTerminalResize() {
|
|
15454
16286
|
var output = document.getElementById("output");
|
|
15455
16287
|
if (!output) return;
|
|
@@ -15644,6 +16476,7 @@
|
|
|
15644
16476
|
// 尺寸下创建的会话时,若新算出的 cols/rows 恰好等于上次值会被去重跳过,导致
|
|
15645
16477
|
// 后端该会话列宽停在旧值、整段折行。teardown 重置后新会话首次 resize 必发出。
|
|
15646
16478
|
state.lastResize = { cols: 0, rows: 0 };
|
|
16479
|
+
teardownJoystick();
|
|
15647
16480
|
}
|
|
15648
16481
|
|
|
15649
16482
|
function sendTerminalResize(cols, rows) {
|
|
@@ -18173,8 +19006,8 @@
|
|
|
18173
19006
|
var agentType = sub.agentType || "";
|
|
18174
19007
|
if (agentType && SUBAGENT_NAME_MAP[agentType]) return SUBAGENT_NAME_MAP[agentType];
|
|
18175
19008
|
if (agentType) return agentType;
|
|
18176
|
-
var tail = (sub.taskId || "").slice(-4) || "未知";
|
|
18177
|
-
return "
|
|
19009
|
+
var tail = (sub.taskId || "").slice(-4) || (getActiveLang() === "English" ? "?" : "未知");
|
|
19010
|
+
return t("subagent.helper_fallback_prefix") + tail;
|
|
18178
19011
|
}
|
|
18179
19012
|
function getSubagentPalette(sub) {
|
|
18180
19013
|
// 哈希优先用 agentType,让同类型 agent 跨 turn 颜色稳定;没有 agentType 时
|
|
@@ -18192,33 +19025,35 @@
|
|
|
18192
19025
|
'</div>';
|
|
18193
19026
|
}
|
|
18194
19027
|
|
|
18195
|
-
// subagent
|
|
18196
|
-
//
|
|
19028
|
+
// subagent 最终回复(父 Task 的 tool_result)——现在外层 .subagent-panel 已经
|
|
19029
|
+
// 负责整段折叠 / 滚动,这里只需把"任务完成 / 失败"做个轻量标记块,markdown
|
|
19030
|
+
// 内容平铺,让 panel 的 body 滚动条统一接管。
|
|
18197
19031
|
function renderSubagentReplyBubble(block, role) {
|
|
18198
19032
|
if (!block || block.type !== "tool_result") return "";
|
|
18199
19033
|
var text = extractToolResultText(block.content);
|
|
18200
19034
|
var isError = block.is_error === true;
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
19035
|
+
var rawText = typeof text === "string" ? text : (text == null ? "" : String(text));
|
|
19036
|
+
|
|
19037
|
+
// pending:subagent 还在跑,没收到结果。在 panel body 里画一个 typing 指示器
|
|
19038
|
+
// 占位,告诉用户"还在跑"。
|
|
19039
|
+
if (!isError && !rawText.trim()) {
|
|
19040
|
+
return '<div class="subagent-reply pending">' +
|
|
19041
|
+
'<span class="subagent-reply-marker pending">' + escapeHtml(t("subagent.running")) + '</span>' +
|
|
19042
|
+
'<span class="typing-indicator"><span></span><span></span><span></span></span>' +
|
|
18206
19043
|
'</div>';
|
|
18207
19044
|
}
|
|
18208
19045
|
|
|
18209
|
-
|
|
18210
|
-
|
|
18211
|
-
|
|
19046
|
+
var displayText = rawText.trim() ? rawText : t("subagent.no_output");
|
|
19047
|
+
var bodyHtml = rawText.trim() ? renderMarkdown(displayText) : escapeHtml(displayText);
|
|
19048
|
+
var markerLabel = isError ? t("subagent.task.failed") : t("subagent.task.done");
|
|
19049
|
+
var markerSymbol = isError ? "✗" : "✓";
|
|
18212
19050
|
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
'
|
|
18218
|
-
'<
|
|
18219
|
-
'<span class="subagent-reply-cycle-label">展开</span>' +
|
|
18220
|
-
'<span class="subagent-reply-cycle-icon" aria-hidden="true">▾</span>' +
|
|
18221
|
-
'</button>' +
|
|
19051
|
+
return '<div class="subagent-reply final' + (isError ? ' error' : '') + '">' +
|
|
19052
|
+
'<div class="subagent-reply-marker ' + (isError ? 'error' : 'done') + '">' +
|
|
19053
|
+
'<span class="subagent-reply-marker-icon" aria-hidden="true">' + markerSymbol + '</span>' +
|
|
19054
|
+
'<span class="subagent-reply-marker-label">' + escapeHtml(markerLabel) + '</span>' +
|
|
19055
|
+
'</div>' +
|
|
19056
|
+
'<div class="subagent-reply-content">' + bodyHtml + '</div>' +
|
|
18222
19057
|
'</div>';
|
|
18223
19058
|
}
|
|
18224
19059
|
var PIXEL_AVATAR = {
|
|
@@ -18569,23 +19404,12 @@
|
|
|
18569
19404
|
// 跳过整段。否则会渲染出"只有头像没内容"的空气泡。
|
|
18570
19405
|
if (!segHtml || !segHtml.trim()) continue;
|
|
18571
19406
|
if (seg.subagent) {
|
|
18572
|
-
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
html += '<div class="chat-handoff" style="--agent-color:' + subPalette.primary + '">' +
|
|
18579
|
-
'<span class="chat-handoff-arrow">↳</span> ' +
|
|
18580
|
-
escapeHtml(parentPersonaName) + ' 让 <strong>' + escapeHtml(subName) + '</strong>' +
|
|
18581
|
-
'<span class="chat-handoff-tag" title="子代理 / subagent">subagent</span>' +
|
|
18582
|
-
'帮忙' + desc +
|
|
18583
|
-
'</div>';
|
|
18584
|
-
}
|
|
18585
|
-
html += '<div class="chat-message-segment subagent" data-agent-id="' + escapeHtml(seg.subagent.taskId) + '" style="--agent-color:' + subPalette.primary + '">' +
|
|
18586
|
-
subagentAvatarHtml(seg.subagent) +
|
|
18587
|
-
'<div class="chat-message-content">' + segHtml + '</div>' +
|
|
18588
|
-
'</div>';
|
|
19407
|
+
// 整段 subagent 输出包成一个统一的可折叠面板:
|
|
19408
|
+
// 头部 = handoff title + 展开按钮;body = segHtml(所有工具卡、文本、最终回复
|
|
19409
|
+
// 都在 body 里);footer = 同款按钮。同一 taskId 的连续段(极少出现的
|
|
19410
|
+
// parent→sub→parent→sub 交错)只在第一次露 handoff title。
|
|
19411
|
+
var includeHandoff = showHandoff && lastSubId !== seg.subagent.taskId;
|
|
19412
|
+
html += buildSubagentPanelHtml(seg, parentPersonaName, segHtml, messageKey, includeHandoff);
|
|
18589
19413
|
lastSubId = seg.subagent.taskId;
|
|
18590
19414
|
} else {
|
|
18591
19415
|
html += '<div class="chat-message-segment parent">' +
|
|
@@ -18598,6 +19422,85 @@
|
|
|
18598
19422
|
return html;
|
|
18599
19423
|
}
|
|
18600
19424
|
|
|
19425
|
+
// 渲染整段 subagent 输出为一个可折叠面板:
|
|
19426
|
+
// ┌─ subagent-panel ──────────────────────────────────┐
|
|
19427
|
+
// │ [🐱] ↳ 勤劳初二 让 协作猫 帮忙:xxx [展开 ▾] │ ← header (always visible)
|
|
19428
|
+
// ├───────────────────────────────────────────────────┤
|
|
19429
|
+
// │ <tool 卡 1> │
|
|
19430
|
+
// │ <text> │ ← body
|
|
19431
|
+
// │ <tool 卡 2> │ 默认 max-height 22em
|
|
19432
|
+
// │ <... 最终回复 ...> │ + 内部 overflow-y:auto
|
|
19433
|
+
// ├───────────────────────────────────────────────────┤
|
|
19434
|
+
// │ [展开 ▾] │ ← footer (always visible)
|
|
19435
|
+
// └───────────────────────────────────────────────────┘
|
|
19436
|
+
// 状态写在 .subagent-panel[data-expanded],按 messageKey + taskId 持久化。
|
|
19437
|
+
// 默认折叠(preview);用户点头/尾按钮或头部标题条都能切。
|
|
19438
|
+
function buildSubagentPanelHtml(seg, parentPersonaName, segHtml, messageKey, includeHandoff) {
|
|
19439
|
+
var sub = seg.subagent;
|
|
19440
|
+
var subPalette = getSubagentPalette(sub);
|
|
19441
|
+
var subName = getSubagentDisplayName(sub);
|
|
19442
|
+
var taskId = sub.taskId || "";
|
|
19443
|
+
var avatarSvg = buildPixelSvg(buildCatGrid(subPalette));
|
|
19444
|
+
|
|
19445
|
+
var titleHtml;
|
|
19446
|
+
if (includeHandoff) {
|
|
19447
|
+
// 用 i18n 模板拼 handoff title。{parent}/{sub} 用占位符避免在调用点拼字符串导致
|
|
19448
|
+
// 顺序错乱(中文 "X 让 Y 帮忙",英文 "X asked Y for help",词序完全不同)。
|
|
19449
|
+
// tag 是单独的彩色 chip,所以把模板里的 {sub} 替换成空 span 占位,然后再 splice
|
|
19450
|
+
// 进 <strong> + chip——保证模板可读、调用点不脏。
|
|
19451
|
+
var subInlineHtml = '<strong class="subagent-panel-name">' + escapeHtml(subName) + '</strong>' +
|
|
19452
|
+
'<span class="subagent-panel-tag" title="' + escapeHtml(t("subagent.tag_title")) + '">' + escapeHtml(t("subagent.tag")) + '</span>';
|
|
19453
|
+
var hasDesc = !!(sub.taskDescription && String(sub.taskDescription).trim());
|
|
19454
|
+
var handoffTpl = hasDesc ? t("subagent.handoff.with_desc", { parent: escapeHtml(parentPersonaName), sub: subInlineHtml })
|
|
19455
|
+
: t("subagent.handoff", { parent: escapeHtml(parentPersonaName), sub: subInlineHtml });
|
|
19456
|
+
var descSpan = hasDesc
|
|
19457
|
+
? '<span class="subagent-panel-task-desc">' + escapeHtml(sub.taskDescription) + '</span>'
|
|
19458
|
+
: '';
|
|
19459
|
+
titleHtml = '<span class="subagent-panel-arrow" aria-hidden="true">↳</span>' +
|
|
19460
|
+
'<span class="subagent-panel-attribution">' + handoffTpl + descSpan + '</span>';
|
|
19461
|
+
} else {
|
|
19462
|
+
titleHtml = '<span class="subagent-panel-attribution">' +
|
|
19463
|
+
'<strong class="subagent-panel-name">' + escapeHtml(subName) + '</strong>' +
|
|
19464
|
+
'<span class="subagent-panel-task-desc"> ' + escapeHtml(t("subagent.continued")) + '</span>' +
|
|
19465
|
+
'</span>';
|
|
19466
|
+
}
|
|
19467
|
+
|
|
19468
|
+
var expandKey = buildExpandKey("subagent-panel", [messageKey, taskId]);
|
|
19469
|
+
var persisted = getPersistedExpandState(expandKey);
|
|
19470
|
+
var expanded = persisted === null ? false : !!persisted;
|
|
19471
|
+
|
|
19472
|
+
function toggleBtnHtml(position) {
|
|
19473
|
+
return '<button type="button" class="subagent-panel-toggle" ' +
|
|
19474
|
+
'data-position="' + position + '" ' +
|
|
19475
|
+
'onclick="__subagentPanelToggle(event, this)" ' +
|
|
19476
|
+
'aria-expanded="' + (expanded ? "true" : "false") + '" ' +
|
|
19477
|
+
'aria-label="' + escapeHtml(expanded ? t("ui.collapse_panel_aria") : t("ui.expand_panel_aria")) + '">' +
|
|
19478
|
+
'<span class="subagent-panel-toggle-label">' + escapeHtml(expanded ? t("ui.collapse") : t("ui.expand")) + '</span>' +
|
|
19479
|
+
'<svg class="subagent-panel-toggle-icon" width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">' +
|
|
19480
|
+
'<path d="M2.5 3.75L5 6.25L7.5 3.75" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
19481
|
+
'</svg>' +
|
|
19482
|
+
'</button>';
|
|
19483
|
+
}
|
|
19484
|
+
|
|
19485
|
+
return '<div class="subagent-panel" ' +
|
|
19486
|
+
'data-expand-kind="subagent-panel" ' +
|
|
19487
|
+
'data-expand-key="' + escapeHtml(expandKey) + '" ' +
|
|
19488
|
+
'data-agent-id="' + escapeHtml(taskId) + '" ' +
|
|
19489
|
+
'data-expanded="' + (expanded ? "true" : "false") + '" ' +
|
|
19490
|
+
'style="--agent-color:' + subPalette.primary + '">' +
|
|
19491
|
+
'<div class="subagent-panel-header" onclick="__subagentPanelToggle(event, this)" role="button" tabindex="0" aria-label="' + escapeHtml(t("subagent.title_aria")) + '">' +
|
|
19492
|
+
'<span class="subagent-panel-avatar" aria-hidden="true">' + avatarSvg + '</span>' +
|
|
19493
|
+
titleHtml +
|
|
19494
|
+
toggleBtnHtml("top") +
|
|
19495
|
+
'</div>' +
|
|
19496
|
+
'<div class="subagent-panel-body">' + segHtml + '</div>' +
|
|
19497
|
+
'<div class="subagent-panel-footer">' +
|
|
19498
|
+
'<span class="subagent-panel-footer-hint" aria-hidden="true">— ' + escapeHtml(subName) + ' —</span>' +
|
|
19499
|
+
toggleBtnHtml("bottom") +
|
|
19500
|
+
'</div>' +
|
|
19501
|
+
'</div>';
|
|
19502
|
+
}
|
|
19503
|
+
|
|
18601
19504
|
function renderStructuredMessage(msg, roundUsage, messageIndex, legacyTaskMap) {
|
|
18602
19505
|
var role = msg.role;
|
|
18603
19506
|
var messageKey = getMessageKey(msg, messageIndex);
|