@co0ontty/wand 1.15.1 → 1.17.2
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-pty-bridge.d.ts +3 -0
- package/dist/claude-pty-bridge.js +35 -1
- package/dist/config.js +2 -0
- package/dist/models.d.ts +13 -0
- package/dist/models.js +54 -0
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +47 -15
- package/dist/pty-text-utils.d.ts +7 -0
- package/dist/pty-text-utils.js +14 -0
- package/dist/pwa.js +2 -4
- package/dist/server-session-routes.js +25 -1
- package/dist/server.js +113 -15
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +30 -1
- package/dist/types.d.ts +16 -0
- package/dist/upload-routes.d.ts +3 -0
- package/dist/upload-routes.js +53 -0
- package/dist/web-ui/content/scripts.js +786 -501
- package/dist/web-ui/content/styles.css +285 -118
- package/dist/web-ui/content/vendor/wterm/terminal.css +162 -0
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -0
- package/dist/web-ui/index.js +2 -4
- package/dist/ws-broadcast.js +12 -7
- package/package.json +6 -5
|
@@ -70,16 +70,13 @@
|
|
|
70
70
|
sessions: [],
|
|
71
71
|
suggestionTimer: null,
|
|
72
72
|
terminal: null,
|
|
73
|
-
fitAddon: null,
|
|
74
73
|
terminalFitInProgress: false,
|
|
75
|
-
serializeAddon: null,
|
|
76
|
-
terminalDomView: null,
|
|
77
|
-
terminalDomUpdateTimer: null,
|
|
78
|
-
_lastDomHtml: "",
|
|
79
74
|
terminalSessionId: null,
|
|
80
75
|
terminalOutput: "",
|
|
81
76
|
terminalLiveStreamSessions: {},
|
|
82
|
-
|
|
77
|
+
lastChunkAt: 0,
|
|
78
|
+
terminalHealthTimer: null,
|
|
79
|
+
lastTerminalResyncAt: 0,
|
|
83
80
|
terminalAutoFollow: true,
|
|
84
81
|
terminalScrollIdleTimer: null,
|
|
85
82
|
terminalScrollIdleMs: 1800,
|
|
@@ -118,6 +115,11 @@
|
|
|
118
115
|
cwdValue: "",
|
|
119
116
|
modeValue: "managed",
|
|
120
117
|
chatMode: "managed",
|
|
118
|
+
chatModel: (function() {
|
|
119
|
+
try { return localStorage.getItem("wand-chat-model") || ""; } catch (e) { return ""; }
|
|
120
|
+
})(),
|
|
121
|
+
availableModels: [],
|
|
122
|
+
modelsRefreshing: false,
|
|
121
123
|
sessionCreateKind: "structured",
|
|
122
124
|
sessionCreateWorktree: false,
|
|
123
125
|
sessionTool: "claude",
|
|
@@ -204,6 +206,7 @@
|
|
|
204
206
|
selectedClaudeHistoryIds: {},
|
|
205
207
|
askUserSelections: {}, // { toolUseId: { 0: [optIdx...], submitted: false } }
|
|
206
208
|
queueEpoch: 0, // Monotonic counter for queue state freshness
|
|
209
|
+
pendingAttachments: [], // [{ file, previewUrl, name, size }]
|
|
207
210
|
// Load last used working directory from localStorage
|
|
208
211
|
workingDir: (function() {
|
|
209
212
|
try {
|
|
@@ -889,16 +892,21 @@
|
|
|
889
892
|
try {
|
|
890
893
|
render({ skipShellChrome: true });
|
|
891
894
|
} catch (_e) {
|
|
892
|
-
// render() may fail if external scripts (
|
|
895
|
+
// render() may fail if external scripts (wterm) failed to load;
|
|
893
896
|
// continue with polling and session loading so the app remains functional
|
|
894
897
|
}
|
|
895
898
|
bindForegroundSyncListeners();
|
|
896
899
|
startPolling();
|
|
897
900
|
refreshAll();
|
|
901
|
+
fetchAvailableModels();
|
|
898
902
|
requestNotificationPermission();
|
|
899
903
|
if (config.updateAvailable && config.latestVersion) {
|
|
900
904
|
notifyUpdateAvailable(config.currentVersion || "-", config.latestVersion);
|
|
901
905
|
}
|
|
906
|
+
// APK auto-update check on startup
|
|
907
|
+
if (_apkVersion) {
|
|
908
|
+
checkApkAutoUpdate();
|
|
909
|
+
}
|
|
902
910
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
903
911
|
loadClaudeHistory();
|
|
904
912
|
}
|
|
@@ -952,6 +960,7 @@
|
|
|
952
960
|
attachEventListeners();
|
|
953
961
|
updateDrawerState();
|
|
954
962
|
syncComposerModeSelect();
|
|
963
|
+
syncComposerModelSelect(getSelectedSession());
|
|
955
964
|
applyCurrentView();
|
|
956
965
|
if (!skipShellChrome) {
|
|
957
966
|
updateShellChrome();
|
|
@@ -1106,12 +1115,21 @@
|
|
|
1106
1115
|
'<span class="session-count" id="session-count">' + String(state.sessions.length) + '</span>' +
|
|
1107
1116
|
'</div>' +
|
|
1108
1117
|
'<div class="sidebar-header-actions">' +
|
|
1109
|
-
'<
|
|
1110
|
-
'<
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
'<
|
|
1114
|
-
|
|
1118
|
+
'<div class="sidebar-header-more">' +
|
|
1119
|
+
'<button id="sidebar-more-btn" class="btn btn-ghost btn-sm" type="button" title="更多操作">' +
|
|
1120
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>' +
|
|
1121
|
+
'</button>' +
|
|
1122
|
+
'<div class="sidebar-header-overflow" id="sidebar-overflow-menu">' +
|
|
1123
|
+
'<button class="overflow-item" id="sidebar-home-btn" type="button">' +
|
|
1124
|
+
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>' +
|
|
1125
|
+
'<span>回到首页</span>' +
|
|
1126
|
+
'</button>' +
|
|
1127
|
+
'<button class="overflow-item" id="sidebar-refresh-btn" type="button">' +
|
|
1128
|
+
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>' +
|
|
1129
|
+
'<span>刷新页面</span>' +
|
|
1130
|
+
'</button>' +
|
|
1131
|
+
'</div>' +
|
|
1132
|
+
'</div>' +
|
|
1115
1133
|
'<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
|
|
1116
1134
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg>' +
|
|
1117
1135
|
'</button>' +
|
|
@@ -1126,15 +1144,21 @@
|
|
|
1126
1144
|
'<div class="sidebar-footer">' +
|
|
1127
1145
|
'<button id="drawer-new-session-button" class="btn btn-primary btn-block"><span>+</span> 新会话</button>' +
|
|
1128
1146
|
'<div class="sidebar-footer-actions">' +
|
|
1129
|
-
'<button id="file-panel-toggle-btn" class="btn btn-ghost btn-sm' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件"
|
|
1147
|
+
'<button id="file-panel-toggle-btn" class="btn btn-ghost btn-sm' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件">' +
|
|
1148
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>' +
|
|
1149
|
+
'<span>文件</span>' +
|
|
1150
|
+
'</button>' +
|
|
1130
1151
|
'<button id="settings-button" class="btn btn-ghost btn-sm" type="button" title="设置">' +
|
|
1131
|
-
'<svg width="
|
|
1152
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>' +
|
|
1153
|
+
'<span>设置</span>' +
|
|
1132
1154
|
'</button>' +
|
|
1133
1155
|
'<button id="pwa-install-button" class="btn btn-ghost btn-sm hidden" title="安装应用">' +
|
|
1134
|
-
'<svg width="
|
|
1156
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>' +
|
|
1157
|
+
'<span>安装</span>' +
|
|
1135
1158
|
'</button>' +
|
|
1136
1159
|
'<button id="logout-button" class="btn btn-ghost btn-sm sidebar-logout" type="button" title="退出登录">' +
|
|
1137
|
-
'<svg width="
|
|
1160
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>' +
|
|
1161
|
+
'<span>退出</span>' +
|
|
1138
1162
|
'</button>' +
|
|
1139
1163
|
'</div>' +
|
|
1140
1164
|
'</div>' +
|
|
@@ -1226,11 +1250,19 @@
|
|
|
1226
1250
|
'</div>' +
|
|
1227
1251
|
'<div class="input-composer">' +
|
|
1228
1252
|
'<textarea id="input-box" class="input-textarea" placeholder="' + getComposerPlaceholder(selectedSession, state.terminalInteractive) + '" rows="1">' + escapeHtml(currentDraft) + '</textarea>' +
|
|
1253
|
+
'<div id="attachment-preview" class="attachment-preview hidden"></div>' +
|
|
1229
1254
|
'<div class="input-composer-bar">' +
|
|
1230
1255
|
'<div class="input-composer-left">' +
|
|
1256
|
+
'<button id="attach-btn" class="btn-circle btn-circle-attach" type="button" title="附加文件">' +
|
|
1257
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>' +
|
|
1258
|
+
'</button>' +
|
|
1259
|
+
'<input type="file" id="file-upload-input" multiple style="display:none">' +
|
|
1231
1260
|
'<select id="chat-mode-select" class="chat-mode-select" title="仅对新建会话生效">' +
|
|
1232
1261
|
renderModeOptions(preferredTool, composerMode) +
|
|
1233
1262
|
'</select>' +
|
|
1263
|
+
'<select id="chat-model-select" class="chat-mode-select chat-model-select" title="切换模型(对运行中会话发送 /model,对新会话作为 --model 启动)">' +
|
|
1264
|
+
renderChatModelOptions(getEffectiveModel(selectedSession)) +
|
|
1265
|
+
'</select>' +
|
|
1234
1266
|
'<button id="terminal-interactive-toggle-top" class="composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨</button>' +
|
|
1235
1267
|
'<span class="permission-actions hidden" id="permission-actions">' +
|
|
1236
1268
|
'<span class="permission-actions-divider"></span>' +
|
|
@@ -1387,6 +1419,14 @@
|
|
|
1387
1419
|
'<button id="do-restart-button" class="btn btn-success btn-sm hidden">\u91cd\u542f\u751f\u6548</button>' +
|
|
1388
1420
|
'</div>' +
|
|
1389
1421
|
'<p id="update-message" class="hint hidden"></p>' +
|
|
1422
|
+
'<div class="settings-auto-update-row" style="display:flex;align-items:center;justify-content:space-between;margin-top:10px">' +
|
|
1423
|
+
'<span class="settings-label">自动更新</span>' +
|
|
1424
|
+
'<label style="position:relative;cursor:pointer">' +
|
|
1425
|
+
'<input type="checkbox" id="auto-update-web-toggle" class="switch-toggle">' +
|
|
1426
|
+
'<span class="switch-slider"></span>' +
|
|
1427
|
+
'</label>' +
|
|
1428
|
+
'</div>' +
|
|
1429
|
+
'<p class="hint" style="margin-top:2px">开启后,检测到新版本将自动下载安装并重启服务。</p>' +
|
|
1390
1430
|
'</div>' +
|
|
1391
1431
|
'<div class="settings-update-section" id="android-apk-section">' +
|
|
1392
1432
|
'<div id="android-apk-current-row" class="settings-about-row hidden">' +
|
|
@@ -1403,6 +1443,14 @@
|
|
|
1403
1443
|
'<span class="settings-value" id="settings-android-apk-local" style="flex:1">-</span>' +
|
|
1404
1444
|
'<button id="download-local-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
|
|
1405
1445
|
'</div>' +
|
|
1446
|
+
'<div id="android-auto-update-row" class="hidden" style="display:flex;align-items:center;justify-content:space-between;margin-top:10px">' +
|
|
1447
|
+
'<span class="settings-label">自动更新</span>' +
|
|
1448
|
+
'<label style="position:relative;cursor:pointer">' +
|
|
1449
|
+
'<input type="checkbox" id="auto-update-apk-toggle" class="switch-toggle">' +
|
|
1450
|
+
'<span class="switch-slider"></span>' +
|
|
1451
|
+
'</label>' +
|
|
1452
|
+
'</div>' +
|
|
1453
|
+
'<p id="android-auto-update-hint" class="hint hidden" style="margin-top:2px">开启后,检测到新版 APK 将自动下载安装。</p>' +
|
|
1406
1454
|
'<p id="android-apk-message" class="hint hidden"></p>' +
|
|
1407
1455
|
'</div>' +
|
|
1408
1456
|
'<div class="settings-update-section" id="android-connect-section">' +
|
|
@@ -1516,6 +1564,16 @@
|
|
|
1516
1564
|
'</div>' +
|
|
1517
1565
|
'</div>' +
|
|
1518
1566
|
'<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
1567
|
+
'<div class="field">' +
|
|
1568
|
+
'<label class="field-label" for="cfg-default-model">默认模型</label>' +
|
|
1569
|
+
'<div style="display:flex;gap:8px;align-items:center;">' +
|
|
1570
|
+
'<select id="cfg-default-model" class="field-input" style="flex:1;">' +
|
|
1571
|
+
'<option value="">跟随 Claude Code 默认</option>' +
|
|
1572
|
+
'</select>' +
|
|
1573
|
+
'<button type="button" id="cfg-default-model-refresh" class="btn btn-ghost btn-sm" title="刷新模型列表">刷新</button>' +
|
|
1574
|
+
'</div>' +
|
|
1575
|
+
'<p class="field-hint" id="cfg-default-model-version" style="margin-top:4px;">新建会话时默认使用该模型;运行中的会话可在输入框切换。</p>' +
|
|
1576
|
+
'</div>' +
|
|
1519
1577
|
'<div class="field">' +
|
|
1520
1578
|
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
1521
1579
|
'<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
|
|
@@ -2078,19 +2136,18 @@
|
|
|
2078
2136
|
} catch (e) {}
|
|
2079
2137
|
applyTerminalScale();
|
|
2080
2138
|
updateScaleLabel();
|
|
2081
|
-
// Force refit: font size changed but container dimensions didn't,
|
|
2082
|
-
// so ensureTerminalFit (which resets viewport tracking) is needed
|
|
2083
|
-
// instead of scheduleTerminalResize (which skips when size unchanged).
|
|
2084
|
-
ensureTerminalFit();
|
|
2085
2139
|
}
|
|
2086
2140
|
|
|
2087
2141
|
function applyTerminalScale() {
|
|
2088
|
-
if (!state.terminal) return;
|
|
2089
|
-
|
|
2090
|
-
state.
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2142
|
+
if (!state.terminal || !state.terminal.element) return;
|
|
2143
|
+
var newSize = (state.terminalBaseFontSize * state.terminalScale) + "px";
|
|
2144
|
+
var newRowHeight = (state.terminalBaseFontSize * state.terminalScale * 1.5) + "px";
|
|
2145
|
+
state.terminal.element.style.setProperty("--term-font-size", newSize);
|
|
2146
|
+
state.terminal.element.style.setProperty("--term-row-height", newRowHeight);
|
|
2147
|
+
if (typeof state.terminal.remeasure === "function") {
|
|
2148
|
+
requestAnimationFrame(function() {
|
|
2149
|
+
if (state.terminal) state.terminal.remeasure();
|
|
2150
|
+
});
|
|
2094
2151
|
}
|
|
2095
2152
|
}
|
|
2096
2153
|
|
|
@@ -3217,6 +3274,17 @@
|
|
|
3217
3274
|
if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
|
|
3218
3275
|
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
3219
3276
|
if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
|
|
3277
|
+
var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
|
|
3278
|
+
var sidebarOverflow = document.getElementById("sidebar-overflow-menu");
|
|
3279
|
+
if (sidebarMoreBtn && sidebarOverflow) {
|
|
3280
|
+
sidebarMoreBtn.addEventListener("click", function(e) {
|
|
3281
|
+
e.stopPropagation();
|
|
3282
|
+
sidebarOverflow.classList.toggle("open");
|
|
3283
|
+
});
|
|
3284
|
+
document.addEventListener("click", function() {
|
|
3285
|
+
sidebarOverflow.classList.remove("open");
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3220
3288
|
var homeBtn = document.getElementById("sidebar-home-btn");
|
|
3221
3289
|
if (homeBtn) homeBtn.addEventListener("click", function() {
|
|
3222
3290
|
state.selectedId = null;
|
|
@@ -3245,11 +3313,15 @@
|
|
|
3245
3313
|
var settingsTabs = document.querySelectorAll(".settings-tab");
|
|
3246
3314
|
for (var ti = 0; ti < settingsTabs.length; ti++) {
|
|
3247
3315
|
settingsTabs[ti].addEventListener("click", function(e) {
|
|
3248
|
-
|
|
3316
|
+
var btn = e.currentTarget || this;
|
|
3317
|
+
var tabName = btn && btn.getAttribute ? btn.getAttribute("data-tab") : null;
|
|
3318
|
+
if (tabName) switchSettingsTab(tabName);
|
|
3249
3319
|
});
|
|
3250
3320
|
}
|
|
3251
3321
|
var saveConfigBtn = document.getElementById("save-config-button");
|
|
3252
3322
|
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
3323
|
+
var defaultModelRefreshBtn = document.getElementById("cfg-default-model-refresh");
|
|
3324
|
+
if (defaultModelRefreshBtn) defaultModelRefreshBtn.addEventListener("click", refreshAvailableModels);
|
|
3253
3325
|
var saveDisplayBtn = document.getElementById("save-display-button");
|
|
3254
3326
|
if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
|
|
3255
3327
|
// App icon picker (APK only)
|
|
@@ -3282,6 +3354,14 @@
|
|
|
3282
3354
|
if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
|
|
3283
3355
|
var doRestartBtn = document.getElementById("do-restart-button");
|
|
3284
3356
|
if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
|
|
3357
|
+
var autoUpdateWebToggle = document.getElementById("auto-update-web-toggle");
|
|
3358
|
+
if (autoUpdateWebToggle) autoUpdateWebToggle.addEventListener("change", function() {
|
|
3359
|
+
toggleAutoUpdate("web", autoUpdateWebToggle.checked);
|
|
3360
|
+
});
|
|
3361
|
+
var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
|
|
3362
|
+
if (autoUpdateApkToggle) autoUpdateApkToggle.addEventListener("change", function() {
|
|
3363
|
+
toggleAutoUpdate("apk", autoUpdateApkToggle.checked);
|
|
3364
|
+
});
|
|
3285
3365
|
var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
|
|
3286
3366
|
if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
|
|
3287
3367
|
var text = document.getElementById("android-connect-code");
|
|
@@ -3414,6 +3494,10 @@
|
|
|
3414
3494
|
state.chatMode = this.value;
|
|
3415
3495
|
showToast("新会话模式已切换为:" + getModeLabel(this.value), "info");
|
|
3416
3496
|
});
|
|
3497
|
+
var modelSelect = document.getElementById("chat-model-select");
|
|
3498
|
+
if (modelSelect) modelSelect.addEventListener("change", function() {
|
|
3499
|
+
onChatModelChange(this.value);
|
|
3500
|
+
});
|
|
3417
3501
|
|
|
3418
3502
|
var sessionModal = document.getElementById("session-modal");
|
|
3419
3503
|
if (sessionModal) sessionModal.addEventListener("click", function(e) {
|
|
@@ -3441,6 +3525,42 @@
|
|
|
3441
3525
|
inputBox.addEventListener("blur", handleInputBoxBlur);
|
|
3442
3526
|
}
|
|
3443
3527
|
|
|
3528
|
+
// Attach button & drag-drop
|
|
3529
|
+
var attachBtn = document.getElementById("attach-btn");
|
|
3530
|
+
var fileInput = document.getElementById("file-upload-input");
|
|
3531
|
+
if (attachBtn && fileInput) {
|
|
3532
|
+
attachBtn.addEventListener("click", function() { fileInput.click(); });
|
|
3533
|
+
fileInput.addEventListener("change", function() {
|
|
3534
|
+
var files = fileInput.files;
|
|
3535
|
+
if (files) {
|
|
3536
|
+
for (var i = 0; i < files.length; i++) addPendingAttachment(files[i]);
|
|
3537
|
+
}
|
|
3538
|
+
fileInput.value = "";
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
var composer = document.querySelector(".input-composer");
|
|
3542
|
+
if (composer) {
|
|
3543
|
+
composer.addEventListener("dragover", function(e) {
|
|
3544
|
+
e.preventDefault();
|
|
3545
|
+
e.stopPropagation();
|
|
3546
|
+
composer.classList.add("drag-over");
|
|
3547
|
+
});
|
|
3548
|
+
composer.addEventListener("dragleave", function(e) {
|
|
3549
|
+
e.preventDefault();
|
|
3550
|
+
e.stopPropagation();
|
|
3551
|
+
composer.classList.remove("drag-over");
|
|
3552
|
+
});
|
|
3553
|
+
composer.addEventListener("drop", function(e) {
|
|
3554
|
+
e.preventDefault();
|
|
3555
|
+
e.stopPropagation();
|
|
3556
|
+
composer.classList.remove("drag-over");
|
|
3557
|
+
var files = e.dataTransfer && e.dataTransfer.files;
|
|
3558
|
+
if (files) {
|
|
3559
|
+
for (var i = 0; i < files.length; i++) addPendingAttachment(files[i]);
|
|
3560
|
+
}
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3444
3564
|
// Terminal interactive toggle (both topbar and terminal-header)
|
|
3445
3565
|
var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
|
|
3446
3566
|
terminalInteractiveToggles.forEach(function(id) {
|
|
@@ -4107,10 +4227,7 @@
|
|
|
4107
4227
|
|
|
4108
4228
|
function getTerminalViewport() {
|
|
4109
4229
|
if (!state.terminal || !state.terminal.element) return null;
|
|
4110
|
-
|
|
4111
|
-
return state.terminalViewportEl;
|
|
4112
|
-
}
|
|
4113
|
-
state.terminalViewportEl = state.terminal.element.querySelector(".xterm-viewport");
|
|
4230
|
+
state.terminalViewportEl = state.terminal.element;
|
|
4114
4231
|
return state.terminalViewportEl;
|
|
4115
4232
|
}
|
|
4116
4233
|
|
|
@@ -4136,11 +4253,6 @@
|
|
|
4136
4253
|
}
|
|
4137
4254
|
|
|
4138
4255
|
function isTerminalNearBottom() {
|
|
4139
|
-
// On mobile, check DOM view scroll position
|
|
4140
|
-
if (state.terminalDomView) {
|
|
4141
|
-
var d = state.terminalDomView.scrollHeight - state.terminalDomView.clientHeight - state.terminalDomView.scrollTop;
|
|
4142
|
-
return d <= state.terminalScrollThreshold;
|
|
4143
|
-
}
|
|
4144
4256
|
var viewport = getTerminalViewport();
|
|
4145
4257
|
if (!viewport) return true;
|
|
4146
4258
|
var distance = viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop;
|
|
@@ -4149,25 +4261,13 @@
|
|
|
4149
4261
|
|
|
4150
4262
|
function scrollTerminalToBottom(smooth) {
|
|
4151
4263
|
if (!state.terminal) return;
|
|
4152
|
-
|
|
4153
|
-
if (
|
|
4154
|
-
if (smooth) {
|
|
4155
|
-
state.terminalDomView.scrollTo({ top: state.terminalDomView.scrollHeight, behavior: "smooth" });
|
|
4156
|
-
} else {
|
|
4157
|
-
state.terminalDomView.scrollTop = state.terminalDomView.scrollHeight;
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4264
|
+
var viewport = getTerminalViewport();
|
|
4265
|
+
if (!viewport) return;
|
|
4160
4266
|
if (smooth) {
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
setTimeout(function() {
|
|
4165
|
-
if (state.terminal) state.terminal.scrollToBottom();
|
|
4166
|
-
}, 160);
|
|
4167
|
-
return;
|
|
4168
|
-
}
|
|
4267
|
+
viewport.scrollTo({ top: viewport.scrollHeight, behavior: "smooth" });
|
|
4268
|
+
} else {
|
|
4269
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
4169
4270
|
}
|
|
4170
|
-
state.terminal.scrollToBottom();
|
|
4171
4271
|
}
|
|
4172
4272
|
|
|
4173
4273
|
function scheduleTerminalResumeFollow() {
|
|
@@ -4388,6 +4488,11 @@
|
|
|
4388
4488
|
requestSyncScrollbar();
|
|
4389
4489
|
}
|
|
4390
4490
|
|
|
4491
|
+
function resetTerminal() {
|
|
4492
|
+
if (!state.terminal || typeof state.terminal.reset !== "function") return;
|
|
4493
|
+
state.terminal.reset();
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4391
4496
|
function syncTerminalBuffer(sessionId, output, options) {
|
|
4392
4497
|
if (!state.terminal) return false;
|
|
4393
4498
|
var normalizedOutput = normalizeTerminalOutput(output || "");
|
|
@@ -4407,7 +4512,7 @@
|
|
|
4407
4512
|
}
|
|
4408
4513
|
|
|
4409
4514
|
if (sessionChanged) {
|
|
4410
|
-
|
|
4515
|
+
resetTerminal();
|
|
4411
4516
|
currentOutput = "";
|
|
4412
4517
|
state.terminalOutput = "";
|
|
4413
4518
|
state.terminalAutoFollow = true;
|
|
@@ -4417,18 +4522,15 @@
|
|
|
4417
4522
|
|
|
4418
4523
|
if (mode === "replace") {
|
|
4419
4524
|
if (normalizedOutput !== currentOutput) {
|
|
4420
|
-
|
|
4525
|
+
resetTerminal();
|
|
4421
4526
|
if (normalizedOutput) {
|
|
4422
4527
|
state.terminal.write(normalizedOutput);
|
|
4423
4528
|
}
|
|
4424
4529
|
wrote = true;
|
|
4425
4530
|
}
|
|
4426
4531
|
} else if (normalizedOutput.length < currentOutput.length && !sessionChanged) {
|
|
4427
|
-
// Ignore regressive snapshots for the active session; wait for an explicit replace.
|
|
4428
4532
|
return false;
|
|
4429
4533
|
} else if (liveChunkStream && !sessionChanged && mode !== "replace" && currentOutput && !normalizedOutput.startsWith(currentOutput)) {
|
|
4430
|
-
// When a session is already streaming live chunks, do not let polled snapshots
|
|
4431
|
-
// rewrite the terminal unless they are strict appends of what we've rendered.
|
|
4432
4534
|
return false;
|
|
4433
4535
|
} else if (normalizedOutput.startsWith(currentOutput)) {
|
|
4434
4536
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
@@ -4437,10 +4539,9 @@
|
|
|
4437
4539
|
wrote = true;
|
|
4438
4540
|
}
|
|
4439
4541
|
} else if (currentOutput && currentOutput.startsWith(normalizedOutput)) {
|
|
4440
|
-
// Ignore shorter/stale snapshots from polling or reconnect races.
|
|
4441
4542
|
return false;
|
|
4442
4543
|
} else {
|
|
4443
|
-
|
|
4544
|
+
resetTerminal();
|
|
4444
4545
|
if (normalizedOutput) {
|
|
4445
4546
|
state.terminal.write(normalizedOutput);
|
|
4446
4547
|
}
|
|
@@ -4449,40 +4550,21 @@
|
|
|
4449
4550
|
|
|
4450
4551
|
state.terminalSessionId = nextSessionId;
|
|
4451
4552
|
state.terminalOutput = normalizedOutput;
|
|
4452
|
-
scheduleMobileDomUpdate();
|
|
4453
4553
|
if (shouldScroll && (wrote || sessionChanged || mode === "replace")) {
|
|
4454
4554
|
maybeScrollTerminalToBottom(sessionChanged || mode === "replace" ? "force" : "output");
|
|
4455
4555
|
} else {
|
|
4456
4556
|
updateTerminalJumpToBottomButton();
|
|
4457
4557
|
}
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
if (sessionChanged && state.fitAddon) {
|
|
4461
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
4462
|
-
scheduleTerminalResize(true);
|
|
4558
|
+
if (sessionChanged) {
|
|
4559
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
4463
4560
|
}
|
|
4464
4561
|
return wrote || sessionChanged;
|
|
4465
4562
|
}
|
|
4466
4563
|
|
|
4467
|
-
function shouldResizeTerminalViewport() {
|
|
4468
|
-
var output = document.getElementById("output");
|
|
4469
|
-
if (!output) return false;
|
|
4470
|
-
var rect = output.getBoundingClientRect();
|
|
4471
|
-
var width = Math.round(rect.width);
|
|
4472
|
-
var height = Math.round(rect.height);
|
|
4473
|
-
if (!width || !height) return false;
|
|
4474
|
-
if (state.terminalViewportSize.width === width && state.terminalViewportSize.height === height) {
|
|
4475
|
-
return false;
|
|
4476
|
-
}
|
|
4477
|
-
state.terminalViewportSize = { width: width, height: height };
|
|
4478
|
-
return true;
|
|
4479
|
-
}
|
|
4480
|
-
|
|
4481
4564
|
function initTerminal() {
|
|
4482
4565
|
var container = document.getElementById("output");
|
|
4483
|
-
if (!container || state.terminal) return;
|
|
4484
|
-
if (typeof
|
|
4485
|
-
// xterm.js not yet loaded — retry after a short delay
|
|
4566
|
+
if (!container || state.terminal || state.terminalInitializing) return;
|
|
4567
|
+
if (typeof WTermLib === "undefined" || !WTermLib.WTerm) {
|
|
4486
4568
|
if (!state.terminalInitRetries) state.terminalInitRetries = 0;
|
|
4487
4569
|
if (state.terminalInitRetries < 10) {
|
|
4488
4570
|
state.terminalInitRetries++;
|
|
@@ -4491,160 +4573,80 @@
|
|
|
4491
4573
|
return;
|
|
4492
4574
|
}
|
|
4493
4575
|
state.terminalInitRetries = 0;
|
|
4576
|
+
state.terminalInitializing = true;
|
|
4577
|
+
|
|
4578
|
+
var termWrap = document.createElement("div");
|
|
4579
|
+
termWrap.className = "terminal-scroll-wrap";
|
|
4580
|
+
container.appendChild(termWrap);
|
|
4494
4581
|
|
|
4495
|
-
|
|
4582
|
+
var term = new WTermLib.WTerm(termWrap, {
|
|
4496
4583
|
cols: 120,
|
|
4497
4584
|
rows: 36,
|
|
4498
|
-
|
|
4499
|
-
disableStdin: false,
|
|
4585
|
+
autoResize: true,
|
|
4500
4586
|
cursorBlink: false,
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
theme: {
|
|
4508
|
-
background: "#1f1b17",
|
|
4509
|
-
foreground: "#f5eadc",
|
|
4510
|
-
cursor: "#d67b52",
|
|
4511
|
-
selectionBackground: "rgba(214, 123, 82, 0.28)",
|
|
4512
|
-
black: "#1f1b17",
|
|
4513
|
-
red: "#d27766",
|
|
4514
|
-
green: "#7fa36f",
|
|
4515
|
-
yellow: "#d5a35b",
|
|
4516
|
-
blue: "#87a9d9",
|
|
4517
|
-
magenta: "#c595c7",
|
|
4518
|
-
cyan: "#7fb3b1",
|
|
4519
|
-
white: "#f5eadc",
|
|
4520
|
-
brightBlack: "#625347",
|
|
4521
|
-
brightRed: "#e39a89",
|
|
4522
|
-
brightGreen: "#9cc08a",
|
|
4523
|
-
brightYellow: "#ebbb6e",
|
|
4524
|
-
brightBlue: "#a8c1ea",
|
|
4525
|
-
brightMagenta: "#dbb1dc",
|
|
4526
|
-
brightCyan: "#9acbca",
|
|
4527
|
-
brightWhite: "#fff7ef"
|
|
4587
|
+
onData: function(data) {
|
|
4588
|
+
if (state.terminalInteractive) return;
|
|
4589
|
+
queueDirectInput(data);
|
|
4590
|
+
},
|
|
4591
|
+
onResize: function(cols, rows) {
|
|
4592
|
+
sendTerminalResize(cols, rows);
|
|
4528
4593
|
}
|
|
4529
4594
|
});
|
|
4530
4595
|
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
state.terminal.loadAddon(state.fitAddon);
|
|
4538
|
-
// Patch: FitAddon subtracts 14px for a scrollbar that CSS hides;
|
|
4539
|
-
// recalculate cols without the scrollbar deduction.
|
|
4540
|
-
var _origPropose = state.fitAddon.proposeDimensions;
|
|
4541
|
-
state.fitAddon.proposeDimensions = function() {
|
|
4542
|
-
var result = _origPropose.call(state.fitAddon);
|
|
4543
|
-
if (result && state.terminal) {
|
|
4544
|
-
try {
|
|
4545
|
-
var core = state.terminal._core;
|
|
4546
|
-
var cellW = core._renderService.dimensions.css.cell.width;
|
|
4547
|
-
var el = state.terminal.element;
|
|
4548
|
-
if (cellW > 0 && el && el.parentElement) {
|
|
4549
|
-
var pw = Math.max(0, parseInt(window.getComputedStyle(el.parentElement).getPropertyValue("width")));
|
|
4550
|
-
var es = window.getComputedStyle(el);
|
|
4551
|
-
var ePad = parseInt(es.getPropertyValue("padding-left")) + parseInt(es.getPropertyValue("padding-right"));
|
|
4552
|
-
result.cols = Math.max(2, Math.floor((pw - ePad) / cellW));
|
|
4553
|
-
}
|
|
4554
|
-
} catch(e) {}
|
|
4555
|
-
}
|
|
4556
|
-
return result;
|
|
4557
|
-
};
|
|
4558
|
-
} else {
|
|
4559
|
-
console.error("[wand] xterm fit addon failed to load; continuing without fit support.");
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
|
-
// Load serialize addon for mobile DOM rendering
|
|
4563
|
-
if (typeof SerializeAddon !== "undefined" && SerializeAddon && typeof SerializeAddon.SerializeAddon === "function") {
|
|
4564
|
-
state.serializeAddon = new SerializeAddon.SerializeAddon();
|
|
4565
|
-
state.terminal.loadAddon(state.serializeAddon);
|
|
4566
|
-
}
|
|
4596
|
+
term.init().then(function() {
|
|
4597
|
+
state.terminal = term;
|
|
4598
|
+
state.terminalInitializing = false;
|
|
4599
|
+
applyTerminalScale();
|
|
4600
|
+
state.terminalAutoFollow = true;
|
|
4601
|
+
clearTerminalScrollIdleTimer();
|
|
4567
4602
|
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
});
|
|
4603
|
+
var viewport = getTerminalViewport();
|
|
4604
|
+
if (viewport) {
|
|
4605
|
+
state.terminalViewportScrollHandler = function() {
|
|
4606
|
+
if (isTerminalNearBottom()) {
|
|
4607
|
+
state.terminalAutoFollow = true;
|
|
4608
|
+
clearTerminalScrollIdleTimer();
|
|
4609
|
+
updateTerminalJumpToBottomButton();
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
setTerminalManualScrollActive();
|
|
4613
|
+
};
|
|
4614
|
+
state.terminalViewportTouchHandler = function() {
|
|
4615
|
+
setTerminalManualScrollActive();
|
|
4616
|
+
};
|
|
4617
|
+
viewport.addEventListener("scroll", state.terminalViewportScrollHandler, { passive: true });
|
|
4618
|
+
viewport.addEventListener("touchmove", state.terminalViewportTouchHandler, { passive: true });
|
|
4585
4619
|
}
|
|
4586
|
-
// Safety-net fit after layout has fully stabilised (CSS transitions,
|
|
4587
|
-
// deferred reflows, late font loads, etc.)
|
|
4588
|
-
setTimeout(function() {
|
|
4589
|
-
if (state.terminal && state.fitAddon) {
|
|
4590
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
4591
|
-
ensureTerminalFit();
|
|
4592
|
-
}
|
|
4593
|
-
}, 500);
|
|
4594
|
-
}
|
|
4595
4620
|
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
if (isTerminalNearBottom()) {
|
|
4600
|
-
state.terminalAutoFollow = true;
|
|
4601
|
-
clearTerminalScrollIdleTimer();
|
|
4602
|
-
updateTerminalJumpToBottomButton();
|
|
4603
|
-
return;
|
|
4621
|
+
state.terminalWheelHandler = function(e) {
|
|
4622
|
+
if (!isTerminalNearBottom() || e.deltaY < 0) {
|
|
4623
|
+
setTerminalManualScrollActive();
|
|
4604
4624
|
}
|
|
4605
|
-
|
|
4606
|
-
};
|
|
4607
|
-
state.terminalViewportTouchHandler = function() {
|
|
4608
|
-
setTerminalManualScrollActive();
|
|
4625
|
+
e.stopPropagation();
|
|
4609
4626
|
};
|
|
4610
|
-
|
|
4611
|
-
viewport.addEventListener("touchmove", state.terminalViewportTouchHandler, { passive: true });
|
|
4612
|
-
}
|
|
4613
|
-
|
|
4614
|
-
container.addEventListener('wheel', function(e) {
|
|
4615
|
-
if (!isTerminalNearBottom() || e.deltaY < 0) {
|
|
4616
|
-
setTerminalManualScrollActive();
|
|
4617
|
-
}
|
|
4618
|
-
e.stopPropagation();
|
|
4619
|
-
}, { passive: true });
|
|
4620
|
-
|
|
4621
|
-
// Create custom scrollbar overlay
|
|
4622
|
-
initTerminalScrollbar(container);
|
|
4627
|
+
container.addEventListener("wheel", state.terminalWheelHandler, { passive: true });
|
|
4623
4628
|
|
|
4624
|
-
|
|
4625
|
-
initMobileDomTerminal(container);
|
|
4629
|
+
initTerminalScrollbar(container);
|
|
4626
4630
|
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
+
if (state.selectedId) {
|
|
4632
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
4633
|
+
if (session) {
|
|
4634
|
+
syncTerminalBuffer(session.id, session.output || "", { mode: "append", scroll: false });
|
|
4635
|
+
}
|
|
4636
|
+
} else {
|
|
4637
|
+
term.write("点击上方「新对话」开始你的第一次对话。\r\n");
|
|
4631
4638
|
}
|
|
4632
|
-
} else {
|
|
4633
|
-
state.terminal.writeln("点击上方「新对话」开始你的第一次对话。");
|
|
4634
|
-
}
|
|
4635
4639
|
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4640
|
+
state.terminalClickHandler = focusInputBox;
|
|
4641
|
+
container.addEventListener("click", state.terminalClickHandler);
|
|
4642
|
+
updateTerminalJumpToBottomButton();
|
|
4643
|
+
initTerminalResizeHandle();
|
|
4644
|
+
observeTerminalResize();
|
|
4645
|
+
startTerminalHealthCheck();
|
|
4646
|
+
}).catch(function(err) {
|
|
4647
|
+
state.terminalInitializing = false;
|
|
4648
|
+
console.error("[wand] wterm init failed:", err);
|
|
4639
4649
|
});
|
|
4640
|
-
|
|
4641
|
-
container.addEventListener("click", focusInputBox);
|
|
4642
|
-
updateTerminalJumpToBottomButton();
|
|
4643
|
-
|
|
4644
|
-
// 初始化拖动调整大小
|
|
4645
|
-
initTerminalResizeHandle();
|
|
4646
|
-
|
|
4647
|
-
observeTerminalResize();
|
|
4648
4650
|
}
|
|
4649
4651
|
|
|
4650
4652
|
function login() {
|
|
@@ -4869,13 +4871,138 @@
|
|
|
4869
4871
|
if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
|
|
4870
4872
|
}
|
|
4871
4873
|
|
|
4874
|
+
function getEffectiveModel(session) {
|
|
4875
|
+
if (session && session.selectedModel) return session.selectedModel;
|
|
4876
|
+
if (state.chatModel) return state.chatModel;
|
|
4877
|
+
if (state.config && state.config.defaultModel) return state.config.defaultModel;
|
|
4878
|
+
return "";
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
function renderChatModelOptions(selected) {
|
|
4882
|
+
var models = state.availableModels || [];
|
|
4883
|
+
var html = '<option value="">默认(跟随设置)</option>';
|
|
4884
|
+
for (var i = 0; i < models.length; i++) {
|
|
4885
|
+
var m = models[i];
|
|
4886
|
+
var label = m.label || m.id;
|
|
4887
|
+
html += '<option value="' + escapeHtml(m.id) + '"' + (m.id === selected ? " selected" : "") + '>' + escapeHtml(label) + '</option>';
|
|
4888
|
+
}
|
|
4889
|
+
// If selected is unknown (custom value), prepend it as a sticky option
|
|
4890
|
+
if (selected && !models.some(function(m) { return m.id === selected; })) {
|
|
4891
|
+
html += '<option value="' + escapeHtml(selected) + '" selected>' + escapeHtml(selected) + '(自定义)</option>';
|
|
4892
|
+
}
|
|
4893
|
+
return html;
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4896
|
+
function syncComposerModelSelect(session) {
|
|
4897
|
+
var select = document.getElementById("chat-model-select");
|
|
4898
|
+
if (!select) return;
|
|
4899
|
+
var effective = getEffectiveModel(session);
|
|
4900
|
+
select.innerHTML = renderChatModelOptions(effective);
|
|
4901
|
+
select.value = effective;
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
function fetchAvailableModels() {
|
|
4905
|
+
return fetch("/api/models", { credentials: "same-origin" })
|
|
4906
|
+
.then(function(res) { return res.json(); })
|
|
4907
|
+
.then(function(data) {
|
|
4908
|
+
if (data && Array.isArray(data.models)) {
|
|
4909
|
+
state.availableModels = data.models;
|
|
4910
|
+
syncComposerModelSelect(getSelectedSession());
|
|
4911
|
+
updateSettingsDefaultModelSelect(data);
|
|
4912
|
+
}
|
|
4913
|
+
return data;
|
|
4914
|
+
})
|
|
4915
|
+
.catch(function() { return null; });
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4918
|
+
function refreshAvailableModels() {
|
|
4919
|
+
if (state.modelsRefreshing) return Promise.resolve(null);
|
|
4920
|
+
state.modelsRefreshing = true;
|
|
4921
|
+
var btn = document.getElementById("cfg-default-model-refresh");
|
|
4922
|
+
if (btn) { btn.disabled = true; btn.textContent = "刷新中..."; }
|
|
4923
|
+
return fetch("/api/models/refresh", { method: "POST", credentials: "same-origin" })
|
|
4924
|
+
.then(function(res) { return res.json(); })
|
|
4925
|
+
.then(function(data) {
|
|
4926
|
+
if (data && Array.isArray(data.models)) {
|
|
4927
|
+
state.availableModels = data.models;
|
|
4928
|
+
syncComposerModelSelect(getSelectedSession());
|
|
4929
|
+
updateSettingsDefaultModelSelect(data);
|
|
4930
|
+
if (typeof showToast === "function") {
|
|
4931
|
+
showToast("模型列表已刷新" + (data.claudeVersion ? "(claude " + data.claudeVersion + ")" : ""), "success");
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
return data;
|
|
4935
|
+
})
|
|
4936
|
+
.catch(function() {
|
|
4937
|
+
if (typeof showToast === "function") showToast("刷新模型列表失败", "error");
|
|
4938
|
+
return null;
|
|
4939
|
+
})
|
|
4940
|
+
.finally(function() {
|
|
4941
|
+
state.modelsRefreshing = false;
|
|
4942
|
+
if (btn) { btn.disabled = false; btn.textContent = "刷新"; }
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
function updateSettingsDefaultModelSelect(data) {
|
|
4947
|
+
var select = document.getElementById("cfg-default-model");
|
|
4948
|
+
if (!select) return;
|
|
4949
|
+
var previous = select.value;
|
|
4950
|
+
var current = previous || state.configDefaultModel || (state.config && state.config.defaultModel) || "";
|
|
4951
|
+
select.innerHTML = renderChatModelOptions(current);
|
|
4952
|
+
select.value = current;
|
|
4953
|
+
var versionEl = document.getElementById("cfg-default-model-version");
|
|
4954
|
+
if (versionEl && data) {
|
|
4955
|
+
versionEl.textContent = data.claudeVersion ? "已检测到 claude " + data.claudeVersion : "新建会话时默认使用该模型。";
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
function getSelectedSession() {
|
|
4960
|
+
if (!state.selectedId) return null;
|
|
4961
|
+
for (var i = 0; i < state.sessions.length; i++) {
|
|
4962
|
+
if (state.sessions[i].id === state.selectedId) return state.sessions[i];
|
|
4963
|
+
}
|
|
4964
|
+
return null;
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
function onChatModelChange(value) {
|
|
4968
|
+
var normalized = (value || "").trim();
|
|
4969
|
+
state.chatModel = normalized;
|
|
4970
|
+
try { localStorage.setItem("wand-chat-model", normalized); } catch (e) {}
|
|
4971
|
+
var session = getSelectedSession();
|
|
4972
|
+
if (!session) return;
|
|
4973
|
+
if (session.provider && session.provider !== "claude") return;
|
|
4974
|
+
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/model", {
|
|
4975
|
+
method: "POST",
|
|
4976
|
+
headers: { "Content-Type": "application/json" },
|
|
4977
|
+
credentials: "same-origin",
|
|
4978
|
+
body: JSON.stringify({ model: normalized || null })
|
|
4979
|
+
})
|
|
4980
|
+
.then(function(res) { return res.json(); })
|
|
4981
|
+
.then(function(data) {
|
|
4982
|
+
if (data && data.error) {
|
|
4983
|
+
showToast(data.error, "error");
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
if (data && data.id) {
|
|
4987
|
+
updateSessionSnapshot(data);
|
|
4988
|
+
if (typeof showToast === "function") {
|
|
4989
|
+
var display = normalized || "默认";
|
|
4990
|
+
showToast("已切换模型 → " + display, "success");
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
})
|
|
4994
|
+
.catch(function() { showToast("切换模型失败", "error"); });
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4872
4997
|
function createStructuredSession(prompt, cwdOverride, modeOverride, worktreeEnabled) {
|
|
4998
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
4873
4999
|
var payload = {
|
|
4874
5000
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
4875
5001
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
4876
5002
|
runner: state.structuredRunner || "claude-cli-print",
|
|
4877
5003
|
prompt: prompt || undefined,
|
|
4878
|
-
worktreeEnabled: worktreeEnabled === true
|
|
5004
|
+
worktreeEnabled: worktreeEnabled === true,
|
|
5005
|
+
model: modelPref || undefined
|
|
4879
5006
|
};
|
|
4880
5007
|
console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
|
|
4881
5008
|
return fetch("/api/structured-sessions", {
|
|
@@ -5254,10 +5381,8 @@
|
|
|
5254
5381
|
initTerminal();
|
|
5255
5382
|
}
|
|
5256
5383
|
if (state.terminal && terminalContainer && !terminalContainer.contains(state.terminal.element)) {
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
5260
|
-
scheduleTerminalResize();
|
|
5384
|
+
teardownTerminal();
|
|
5385
|
+
initTerminal();
|
|
5261
5386
|
}
|
|
5262
5387
|
|
|
5263
5388
|
if (selectedSession && state.terminal) {
|
|
@@ -5286,6 +5411,7 @@
|
|
|
5286
5411
|
if (inputPanel) inputPanel.classList.add("hidden");
|
|
5287
5412
|
}
|
|
5288
5413
|
syncComposerModeSelect();
|
|
5414
|
+
syncComposerModelSelect(getSelectedSession());
|
|
5289
5415
|
applyCurrentView();
|
|
5290
5416
|
reconcileInteractiveState();
|
|
5291
5417
|
}
|
|
@@ -5316,6 +5442,15 @@
|
|
|
5316
5442
|
updateSessionSnapshot(data);
|
|
5317
5443
|
updateShellChrome();
|
|
5318
5444
|
|
|
5445
|
+
if (state.terminal && id === state.selectedId && data.output !== undefined) {
|
|
5446
|
+
syncTerminalBuffer(id, data.output, { mode: "append" });
|
|
5447
|
+
if (state.terminal.remeasure) {
|
|
5448
|
+
requestAnimationFrame(function() {
|
|
5449
|
+
if (state.terminal) state.terminal.remeasure();
|
|
5450
|
+
});
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5319
5454
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
5320
5455
|
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, data.output, false));
|
|
5321
5456
|
|
|
@@ -5944,6 +6079,13 @@
|
|
|
5944
6079
|
}
|
|
5945
6080
|
}
|
|
5946
6081
|
|
|
6082
|
+
// Auto-update toggles
|
|
6083
|
+
var autoUpdate = data.autoUpdate || {};
|
|
6084
|
+
var autoUpdateWebToggle = document.getElementById("auto-update-web-toggle");
|
|
6085
|
+
if (autoUpdateWebToggle) autoUpdateWebToggle.checked = !!autoUpdate.web;
|
|
6086
|
+
var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
|
|
6087
|
+
if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!autoUpdate.apk;
|
|
6088
|
+
|
|
5947
6089
|
// ── Android APK version display ──
|
|
5948
6090
|
var apkSection = document.getElementById("android-apk-section");
|
|
5949
6091
|
var apkCurrentRow = document.getElementById("android-apk-current-row");
|
|
@@ -6005,6 +6147,11 @@
|
|
|
6005
6147
|
apkMessageEl.textContent = "暂无可用更新";
|
|
6006
6148
|
apkMessageEl.classList.remove("hidden");
|
|
6007
6149
|
}
|
|
6150
|
+
// Show APK auto-update toggle in APK mode
|
|
6151
|
+
var apkAutoRow = document.getElementById("android-auto-update-row");
|
|
6152
|
+
var apkAutoHint = document.getElementById("android-auto-update-hint");
|
|
6153
|
+
if (apkAutoRow) apkAutoRow.classList.remove("hidden");
|
|
6154
|
+
if (apkAutoHint) apkAutoHint.classList.remove("hidden");
|
|
6008
6155
|
} else {
|
|
6009
6156
|
// ── 浏览器模式:显示线上版本 + 本地版本 + 下载按钮 ──
|
|
6010
6157
|
if (androidApk.github && apkGithubRow && apkGithubEl) {
|
|
@@ -6067,6 +6214,13 @@
|
|
|
6067
6214
|
var langEl = document.getElementById("cfg-language");
|
|
6068
6215
|
if (langEl) langEl.value = cfg.language || "";
|
|
6069
6216
|
|
|
6217
|
+
// Default model
|
|
6218
|
+
state.configDefaultModel = cfg.defaultModel || "";
|
|
6219
|
+
updateSettingsDefaultModelSelect();
|
|
6220
|
+
fetchAvailableModels().then(function() {
|
|
6221
|
+
updateSettingsDefaultModelSelect();
|
|
6222
|
+
}).catch(function() {});
|
|
6223
|
+
|
|
6070
6224
|
// Cert status
|
|
6071
6225
|
var certStatus = document.getElementById("cert-status");
|
|
6072
6226
|
if (certStatus) {
|
|
@@ -6117,6 +6271,7 @@
|
|
|
6117
6271
|
defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
|
|
6118
6272
|
shell: (document.getElementById("cfg-shell") || {}).value,
|
|
6119
6273
|
language: (document.getElementById("cfg-language") || {}).value || "",
|
|
6274
|
+
defaultModel: (document.getElementById("cfg-default-model") || {}).value || "",
|
|
6120
6275
|
};
|
|
6121
6276
|
|
|
6122
6277
|
fetch("/api/settings/config", {
|
|
@@ -6330,6 +6485,48 @@
|
|
|
6330
6485
|
performRestart(restartBtn, msgEl);
|
|
6331
6486
|
}
|
|
6332
6487
|
|
|
6488
|
+
function checkApkAutoUpdate() {
|
|
6489
|
+
fetch("/api/auto-update", { credentials: "same-origin" })
|
|
6490
|
+
.then(function(res) { return res.json(); })
|
|
6491
|
+
.then(function(autoData) {
|
|
6492
|
+
if (!autoData.apk) return;
|
|
6493
|
+
// Auto-update is enabled, check for APK update
|
|
6494
|
+
return fetch("/api/android-apk-update?currentVersion=" + encodeURIComponent(_apkVersion), { credentials: "same-origin" })
|
|
6495
|
+
.then(function(res) { return res.json(); })
|
|
6496
|
+
.then(function(data) {
|
|
6497
|
+
if (!data.updateAvailable || !data.downloadUrl) return;
|
|
6498
|
+
try {
|
|
6499
|
+
WandNative.downloadUpdate(data.downloadUrl, data.fileName || "wand-update.apk", data.source || "local");
|
|
6500
|
+
} catch (_e) {}
|
|
6501
|
+
});
|
|
6502
|
+
})
|
|
6503
|
+
.catch(function() {});
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
function toggleAutoUpdate(type, enabled) {
|
|
6507
|
+
var body = {};
|
|
6508
|
+
body[type] = enabled;
|
|
6509
|
+
fetch("/api/auto-update", {
|
|
6510
|
+
method: "POST",
|
|
6511
|
+
headers: { "Content-Type": "application/json" },
|
|
6512
|
+
credentials: "same-origin",
|
|
6513
|
+
body: JSON.stringify(body),
|
|
6514
|
+
})
|
|
6515
|
+
.then(function(res) { return res.json(); })
|
|
6516
|
+
.then(function(data) {
|
|
6517
|
+
// Sync toggle state with server response
|
|
6518
|
+
var webToggle = document.getElementById("auto-update-web-toggle");
|
|
6519
|
+
var apkToggle = document.getElementById("auto-update-apk-toggle");
|
|
6520
|
+
if (webToggle) webToggle.checked = !!data.web;
|
|
6521
|
+
if (apkToggle) apkToggle.checked = !!data.apk;
|
|
6522
|
+
})
|
|
6523
|
+
.catch(function() {
|
|
6524
|
+
// Revert toggle on failure
|
|
6525
|
+
var toggle = document.getElementById("auto-update-" + type + "-toggle");
|
|
6526
|
+
if (toggle) toggle.checked = !enabled;
|
|
6527
|
+
});
|
|
6528
|
+
}
|
|
6529
|
+
|
|
6333
6530
|
// ── Notification Settings Helpers ──
|
|
6334
6531
|
|
|
6335
6532
|
function _updateAppIconSelection(activeIcon) {
|
|
@@ -6639,6 +6836,7 @@
|
|
|
6639
6836
|
state.sessionTool = "claude";
|
|
6640
6837
|
state.preferredCommand = "claude";
|
|
6641
6838
|
syncComposerModeSelect();
|
|
6839
|
+
syncComposerModelSelect(getSelectedSession());
|
|
6642
6840
|
return createStructuredSession(undefined, cwd, mode, worktreeEnabled)
|
|
6643
6841
|
.then(function(data) {
|
|
6644
6842
|
saveWorkingDir(cwd);
|
|
@@ -6989,6 +7187,107 @@
|
|
|
6989
7187
|
}
|
|
6990
7188
|
}
|
|
6991
7189
|
|
|
7190
|
+
// ── Attachment helpers ──
|
|
7191
|
+
|
|
7192
|
+
var ATTACH_MAX_SIZE = 10 * 1024 * 1024;
|
|
7193
|
+
|
|
7194
|
+
function formatFileSize(bytes) {
|
|
7195
|
+
if (bytes < 1024) return bytes + " B";
|
|
7196
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
7197
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
|
7198
|
+
}
|
|
7199
|
+
|
|
7200
|
+
function isImageType(type) {
|
|
7201
|
+
return /^image\/(png|jpe?g|gif|webp|bmp|svg\+xml)/.test(type);
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
function addPendingAttachment(file) {
|
|
7205
|
+
if (!file) return;
|
|
7206
|
+
if (file.size > ATTACH_MAX_SIZE) {
|
|
7207
|
+
showToast("文件过大(上限 10 MB): " + file.name, "error");
|
|
7208
|
+
return;
|
|
7209
|
+
}
|
|
7210
|
+
var entry = { file: file, name: file.name, size: file.size, previewUrl: null };
|
|
7211
|
+
if (isImageType(file.type)) {
|
|
7212
|
+
entry.previewUrl = URL.createObjectURL(file);
|
|
7213
|
+
}
|
|
7214
|
+
state.pendingAttachments.push(entry);
|
|
7215
|
+
renderAttachmentPreview();
|
|
7216
|
+
}
|
|
7217
|
+
|
|
7218
|
+
function removePendingAttachment(index) {
|
|
7219
|
+
var removed = state.pendingAttachments.splice(index, 1);
|
|
7220
|
+
if (removed.length && removed[0].previewUrl) {
|
|
7221
|
+
URL.revokeObjectURL(removed[0].previewUrl);
|
|
7222
|
+
}
|
|
7223
|
+
renderAttachmentPreview();
|
|
7224
|
+
}
|
|
7225
|
+
|
|
7226
|
+
function clearAttachments() {
|
|
7227
|
+
state.pendingAttachments.forEach(function(a) {
|
|
7228
|
+
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
|
7229
|
+
});
|
|
7230
|
+
state.pendingAttachments = [];
|
|
7231
|
+
renderAttachmentPreview();
|
|
7232
|
+
}
|
|
7233
|
+
|
|
7234
|
+
function renderAttachmentPreview() {
|
|
7235
|
+
var bar = document.getElementById("attachment-preview");
|
|
7236
|
+
if (!bar) return;
|
|
7237
|
+
var items = state.pendingAttachments;
|
|
7238
|
+
if (items.length === 0) {
|
|
7239
|
+
bar.classList.add("hidden");
|
|
7240
|
+
bar.innerHTML = "";
|
|
7241
|
+
return;
|
|
7242
|
+
}
|
|
7243
|
+
bar.classList.remove("hidden");
|
|
7244
|
+
var html = "";
|
|
7245
|
+
for (var i = 0; i < items.length; i++) {
|
|
7246
|
+
var a = items[i];
|
|
7247
|
+
var thumb = a.previewUrl
|
|
7248
|
+
? '<img src="' + escapeHtml(a.previewUrl) + '" alt="">'
|
|
7249
|
+
: '<span class="att-icon">📄</span>';
|
|
7250
|
+
html += '<span class="attachment-pill" data-index="' + i + '">' +
|
|
7251
|
+
thumb +
|
|
7252
|
+
'<span class="att-name" title="' + escapeHtml(a.name) + '">' + escapeHtml(a.name) + '</span>' +
|
|
7253
|
+
'<span class="att-size">' + formatFileSize(a.size) + '</span>' +
|
|
7254
|
+
'<button class="att-remove" data-index="' + i + '" title="移除">×</button>' +
|
|
7255
|
+
'</span>';
|
|
7256
|
+
}
|
|
7257
|
+
bar.innerHTML = html;
|
|
7258
|
+
bar.querySelectorAll(".att-remove").forEach(function(btn) {
|
|
7259
|
+
btn.addEventListener("click", function(e) {
|
|
7260
|
+
e.preventDefault();
|
|
7261
|
+
e.stopPropagation();
|
|
7262
|
+
removePendingAttachment(parseInt(btn.getAttribute("data-index"), 10));
|
|
7263
|
+
});
|
|
7264
|
+
});
|
|
7265
|
+
}
|
|
7266
|
+
|
|
7267
|
+
function uploadAttachments(sessionId) {
|
|
7268
|
+
if (!state.pendingAttachments.length) return Promise.resolve([]);
|
|
7269
|
+
var formData = new FormData();
|
|
7270
|
+
state.pendingAttachments.forEach(function(a) {
|
|
7271
|
+
formData.append("files", a.file, a.name);
|
|
7272
|
+
});
|
|
7273
|
+
return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/upload", {
|
|
7274
|
+
method: "POST",
|
|
7275
|
+
body: formData,
|
|
7276
|
+
credentials: "same-origin"
|
|
7277
|
+
}).then(function(resp) {
|
|
7278
|
+
if (!resp.ok) return resp.json().then(function(e) { throw new Error(e.error || "上传失败"); });
|
|
7279
|
+
return resp.json();
|
|
7280
|
+
}).then(function(data) {
|
|
7281
|
+
return data.files || [];
|
|
7282
|
+
});
|
|
7283
|
+
}
|
|
7284
|
+
|
|
7285
|
+
function buildAttachmentPrefix(uploadedFiles) {
|
|
7286
|
+
if (!uploadedFiles || !uploadedFiles.length) return "";
|
|
7287
|
+
var paths = uploadedFiles.map(function(f) { return f.savedPath; });
|
|
7288
|
+
return "[附件已上传,请查看以下文件:\n" + paths.join("\n") + "]\n\n";
|
|
7289
|
+
}
|
|
7290
|
+
|
|
6992
7291
|
function handleInteractiveTextInput(inputBox) {
|
|
6993
7292
|
if (!state.terminalInteractive || !inputBox) return false;
|
|
6994
7293
|
var value = inputBox.value || "";
|
|
@@ -7001,6 +7300,17 @@
|
|
|
7001
7300
|
}
|
|
7002
7301
|
|
|
7003
7302
|
function handleInputPaste(event) {
|
|
7303
|
+
var items = event.clipboardData && event.clipboardData.items;
|
|
7304
|
+
if (items && !state.terminalInteractive) {
|
|
7305
|
+
for (var i = 0; i < items.length; i++) {
|
|
7306
|
+
if (items[i].type.indexOf("image/") === 0) {
|
|
7307
|
+
event.preventDefault();
|
|
7308
|
+
var file = items[i].getAsFile();
|
|
7309
|
+
if (file) addPendingAttachment(file);
|
|
7310
|
+
return;
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
}
|
|
7004
7314
|
var pasted = event.clipboardData && event.clipboardData.getData("text");
|
|
7005
7315
|
if (!pasted) return;
|
|
7006
7316
|
event.preventDefault();
|
|
@@ -7483,12 +7793,9 @@
|
|
|
7483
7793
|
|
|
7484
7794
|
if (!structured) {
|
|
7485
7795
|
if (!state.terminal) initTerminal();
|
|
7486
|
-
if (state.terminal && state.fitAddon) {
|
|
7487
|
-
ensureTerminalFit();
|
|
7488
|
-
}
|
|
7489
7796
|
}
|
|
7490
7797
|
applyCurrentView();
|
|
7491
|
-
focusInputBox();
|
|
7798
|
+
focusInputBox(true);
|
|
7492
7799
|
}
|
|
7493
7800
|
|
|
7494
7801
|
|
|
@@ -7501,7 +7808,9 @@
|
|
|
7501
7808
|
var inputBox = document.getElementById("input-box");
|
|
7502
7809
|
var value = inputBox ? inputBox.value : "";
|
|
7503
7810
|
var selectedSession = getSelectedSession();
|
|
7504
|
-
|
|
7811
|
+
var hasAttachments = state.pendingAttachments.length > 0;
|
|
7812
|
+
|
|
7813
|
+
if (value || hasAttachments) {
|
|
7505
7814
|
console.log("[WAND] sendInputFromBox", {
|
|
7506
7815
|
sessionId: state.selectedId,
|
|
7507
7816
|
sessionStatus: selectedSession ? selectedSession.status : null,
|
|
@@ -7511,50 +7820,62 @@
|
|
|
7511
7820
|
view: state.currentView,
|
|
7512
7821
|
wsConnected: state.wsConnected,
|
|
7513
7822
|
terminalInteractive: state.terminalInteractive,
|
|
7514
|
-
inputLength: value.length
|
|
7823
|
+
inputLength: value.length,
|
|
7824
|
+
attachments: state.pendingAttachments.length
|
|
7515
7825
|
});
|
|
7516
|
-
// Clear todo progress bar at the start of a new user turn
|
|
7517
|
-
var todoEl = document.getElementById("todo-progress");
|
|
7518
|
-
if (todoEl) todoEl.classList.add("hidden");
|
|
7519
7826
|
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7827
|
+
var attachUpload = hasAttachments && state.selectedId
|
|
7828
|
+
? uploadAttachments(state.selectedId)
|
|
7829
|
+
: Promise.resolve([]);
|
|
7523
7830
|
|
|
7524
|
-
|
|
7525
|
-
|
|
7831
|
+
return attachUpload.then(function(uploadedFiles) {
|
|
7832
|
+
var prefix = buildAttachmentPrefix(uploadedFiles);
|
|
7833
|
+
var finalValue = prefix + (value || (uploadedFiles.length ? "请查看附件。" : ""));
|
|
7834
|
+
if (uploadedFiles.length) clearAttachments();
|
|
7526
7835
|
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
if (inputBox) {
|
|
7531
|
-
inputBox.value = "";
|
|
7532
|
-
autoResizeInput(inputBox);
|
|
7533
|
-
}
|
|
7534
|
-
setDraftValue("");
|
|
7535
|
-
return Promise.resolve();
|
|
7536
|
-
}
|
|
7836
|
+
// Clear todo progress bar at the start of a new user turn
|
|
7837
|
+
var todoEl = document.getElementById("todo-progress");
|
|
7838
|
+
if (todoEl) todoEl.classList.add("hidden");
|
|
7537
7839
|
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
if (!readySession) {
|
|
7541
|
-
showToast("会话未就绪,将稍后重试。", "info");
|
|
7542
|
-
return null;
|
|
7840
|
+
if (isStructuredSession(selectedSession)) {
|
|
7841
|
+
return postStructuredInput(finalValue, inputBox, selectedSession);
|
|
7543
7842
|
}
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
if (inputBox
|
|
7843
|
+
|
|
7844
|
+
var submitChunks = getTerminalSubmitChunks(selectedSession, finalValue);
|
|
7845
|
+
var isOffline = !state.wsConnected;
|
|
7846
|
+
|
|
7847
|
+
if (isOffline) {
|
|
7848
|
+
queueOfflineTerminalChunks(submitChunks);
|
|
7849
|
+
if (inputBox) {
|
|
7551
7850
|
inputBox.value = "";
|
|
7552
7851
|
autoResizeInput(inputBox);
|
|
7553
7852
|
}
|
|
7554
7853
|
setDraftValue("");
|
|
7854
|
+
return Promise.resolve();
|
|
7855
|
+
}
|
|
7856
|
+
|
|
7857
|
+
return ensureSessionReadyForInput(selectedSession).then(function(readySession) {
|
|
7858
|
+
if (!readySession) {
|
|
7859
|
+
showToast("会话未就绪,将稍后重试。", "info");
|
|
7860
|
+
return null;
|
|
7861
|
+
}
|
|
7862
|
+
var submitView = state.currentView;
|
|
7863
|
+
if (readySession && readySession.provider === "codex" && state.selectedId !== readySession.id) {
|
|
7864
|
+
throw new Error("Codex session changed before input send.");
|
|
7865
|
+
}
|
|
7866
|
+
return sendTerminalChunks(submitChunks, "enter_text", 30, submitView).then(function() {
|
|
7867
|
+
if (inputBox && inputBox.value === value) {
|
|
7868
|
+
inputBox.value = "";
|
|
7869
|
+
autoResizeInput(inputBox);
|
|
7870
|
+
}
|
|
7871
|
+
setDraftValue("");
|
|
7872
|
+
});
|
|
7873
|
+
}).catch(function(err) {
|
|
7874
|
+
showToast(getInputErrorMessage(err), "error");
|
|
7875
|
+
throw err;
|
|
7555
7876
|
});
|
|
7556
7877
|
}).catch(function(err) {
|
|
7557
|
-
showToast(
|
|
7878
|
+
showToast("附件上传失败: " + (err.message || err), "error");
|
|
7558
7879
|
throw err;
|
|
7559
7880
|
});
|
|
7560
7881
|
}
|
|
@@ -7805,10 +8126,9 @@
|
|
|
7805
8126
|
}
|
|
7806
8127
|
|
|
7807
8128
|
function getTerminalSubmitChunks(session, text) {
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
return [text + String.fromCharCode(13)];
|
|
8129
|
+
// 文本与回车分两个 chunk 发,避免 CLI 的 bracketed paste 检测把末尾
|
|
8130
|
+
// \r 并入粘贴内容导致只换行不提交。
|
|
8131
|
+
return [text, String.fromCharCode(13)];
|
|
7812
8132
|
}
|
|
7813
8133
|
|
|
7814
8134
|
function sendTerminalChunks(chunks, shortcutKey, delayMs, viewOverride) {
|
|
@@ -7848,7 +8168,6 @@
|
|
|
7848
8168
|
return postInput(input, shortcutKey, viewOverride).finally(function() {
|
|
7849
8169
|
var idx = state.messageQueue.indexOf(input);
|
|
7850
8170
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
7851
|
-
scheduleMobileDomUpdate();
|
|
7852
8171
|
});
|
|
7853
8172
|
});
|
|
7854
8173
|
return state.inputQueue;
|
|
@@ -8491,6 +8810,7 @@
|
|
|
8491
8810
|
state.preferredCommand = command;
|
|
8492
8811
|
state.chatMode = getSafeModeForTool(command, state.chatMode);
|
|
8493
8812
|
}
|
|
8813
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8494
8814
|
return fetch("/api/commands", {
|
|
8495
8815
|
method: "POST",
|
|
8496
8816
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8498,7 +8818,8 @@
|
|
|
8498
8818
|
body: JSON.stringify({
|
|
8499
8819
|
command: command,
|
|
8500
8820
|
cwd: cwd || "",
|
|
8501
|
-
mode: state.chatMode || state.config.defaultMode || "default"
|
|
8821
|
+
mode: state.chatMode || state.config.defaultMode || "default",
|
|
8822
|
+
model: modelPref || undefined
|
|
8502
8823
|
})
|
|
8503
8824
|
})
|
|
8504
8825
|
.then(function(res) { return res.json(); })
|
|
@@ -8618,6 +8939,7 @@
|
|
|
8618
8939
|
var mode = state.chatMode || "managed";
|
|
8619
8940
|
var defaultCwd = getEffectiveCwd();
|
|
8620
8941
|
var preferredTool = getPreferredTool();
|
|
8942
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8621
8943
|
fetch("/api/commands", {
|
|
8622
8944
|
method: "POST",
|
|
8623
8945
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8626,7 +8948,8 @@
|
|
|
8626
8948
|
command: preferredTool,
|
|
8627
8949
|
cwd: defaultCwd,
|
|
8628
8950
|
mode: mode,
|
|
8629
|
-
initialInput: value
|
|
8951
|
+
initialInput: value,
|
|
8952
|
+
model: modelPref || undefined
|
|
8630
8953
|
})
|
|
8631
8954
|
})
|
|
8632
8955
|
.then(function(res) { return res.json(); })
|
|
@@ -8654,6 +8977,7 @@
|
|
|
8654
8977
|
var mode = state.chatMode || "managed";
|
|
8655
8978
|
var defaultCwd = getEffectiveCwd();
|
|
8656
8979
|
var preferredTool = getPreferredTool();
|
|
8980
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8657
8981
|
fetch("/api/commands", {
|
|
8658
8982
|
method: "POST",
|
|
8659
8983
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8662,7 +8986,8 @@
|
|
|
8662
8986
|
command: preferredTool,
|
|
8663
8987
|
cwd: defaultCwd,
|
|
8664
8988
|
mode: mode,
|
|
8665
|
-
initialInput: value || undefined
|
|
8989
|
+
initialInput: value || undefined,
|
|
8990
|
+
model: modelPref || undefined
|
|
8666
8991
|
})
|
|
8667
8992
|
})
|
|
8668
8993
|
.then(function(res) { return res.json(); })
|
|
@@ -8818,7 +9143,7 @@
|
|
|
8818
9143
|
setTimeout(function() {
|
|
8819
9144
|
window.scrollTo(0, 0);
|
|
8820
9145
|
// On mobile, force terminal refit + scroll after keyboard dismissal.
|
|
8821
|
-
// The container height restores but
|
|
9146
|
+
// The container height restores but terminal needs time to
|
|
8822
9147
|
// fill the expanded space, and the scroll position needs resetting.
|
|
8823
9148
|
if (isTouchDevice()) {
|
|
8824
9149
|
ensureTerminalFit();
|
|
@@ -9487,9 +9812,8 @@
|
|
|
9487
9812
|
if (!output) return;
|
|
9488
9813
|
output.setAttribute("tabindex", "0");
|
|
9489
9814
|
output.focus();
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
terminalTextarea.focus();
|
|
9815
|
+
if (state.terminal && state.terminal.focus) {
|
|
9816
|
+
state.terminal.focus();
|
|
9493
9817
|
}
|
|
9494
9818
|
}
|
|
9495
9819
|
|
|
@@ -9655,11 +9979,6 @@
|
|
|
9655
9979
|
function observeTerminalResize() {
|
|
9656
9980
|
var output = document.getElementById("output");
|
|
9657
9981
|
if (!output) return;
|
|
9658
|
-
|
|
9659
|
-
if (typeof ResizeObserver === "function") {
|
|
9660
|
-
state.resizeObserver = new ResizeObserver(function() { scheduleTerminalResize(true); });
|
|
9661
|
-
state.resizeObserver.observe(output);
|
|
9662
|
-
}
|
|
9663
9982
|
var lastKnownDesktop = !isMobileLayout();
|
|
9664
9983
|
state.resizeHandler = function() {
|
|
9665
9984
|
scheduleTerminalResize(true);
|
|
@@ -9685,7 +10004,36 @@
|
|
|
9685
10004
|
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
9686
10005
|
}
|
|
9687
10006
|
|
|
10007
|
+
function startTerminalHealthCheck() {
|
|
10008
|
+
if (state.terminalHealthTimer) return;
|
|
10009
|
+
state.terminalHealthTimer = setInterval(function() {
|
|
10010
|
+
if (!state.terminal || state.currentView !== "terminal" || document.hidden) return;
|
|
10011
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
10012
|
+
if (!selectedSession || selectedSession.sessionKind === "structured") return;
|
|
10013
|
+
// Lightweight remeasure every 5s
|
|
10014
|
+
if (state.terminal.remeasure) state.terminal.remeasure();
|
|
10015
|
+
// Full re-sync every 30s during output pauses
|
|
10016
|
+
var now = Date.now();
|
|
10017
|
+
var chunkPause = state.lastChunkAt > 0 && (now - state.lastChunkAt > 300);
|
|
10018
|
+
var resyncDue = (now - state.lastTerminalResyncAt) > 30000;
|
|
10019
|
+
if (resyncDue && (chunkPause || selectedSession.status !== "running") && state.terminalOutput) {
|
|
10020
|
+
state.lastTerminalResyncAt = now;
|
|
10021
|
+
state.terminal.reset();
|
|
10022
|
+
state.terminal.write(state.terminalOutput);
|
|
10023
|
+
maybeScrollTerminalToBottom("output");
|
|
10024
|
+
}
|
|
10025
|
+
}, 5000);
|
|
10026
|
+
}
|
|
10027
|
+
|
|
10028
|
+
function stopTerminalHealthCheck() {
|
|
10029
|
+
if (state.terminalHealthTimer) {
|
|
10030
|
+
clearInterval(state.terminalHealthTimer);
|
|
10031
|
+
state.terminalHealthTimer = null;
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
|
|
9688
10035
|
function teardownTerminal() {
|
|
10036
|
+
stopTerminalHealthCheck();
|
|
9689
10037
|
if (state.resizeTimer) {
|
|
9690
10038
|
clearTimeout(state.resizeTimer);
|
|
9691
10039
|
state.resizeTimer = null;
|
|
@@ -9711,6 +10059,7 @@
|
|
|
9711
10059
|
}
|
|
9712
10060
|
});
|
|
9713
10061
|
clearTerminalScrollIdleTimer();
|
|
10062
|
+
var output = document.getElementById("output");
|
|
9714
10063
|
if (state.terminalViewportEl) {
|
|
9715
10064
|
if (state.terminalViewportScrollHandler) {
|
|
9716
10065
|
state.terminalViewportEl.removeEventListener("scroll", state.terminalViewportScrollHandler);
|
|
@@ -9719,27 +10068,30 @@
|
|
|
9719
10068
|
state.terminalViewportEl.removeEventListener("touchmove", state.terminalViewportTouchHandler);
|
|
9720
10069
|
}
|
|
9721
10070
|
}
|
|
10071
|
+
if (output) {
|
|
10072
|
+
if (state.terminalWheelHandler) {
|
|
10073
|
+
output.removeEventListener("wheel", state.terminalWheelHandler);
|
|
10074
|
+
}
|
|
10075
|
+
if (state.terminalClickHandler) {
|
|
10076
|
+
output.removeEventListener("click", state.terminalClickHandler);
|
|
10077
|
+
}
|
|
10078
|
+
}
|
|
9722
10079
|
state.terminalViewportEl = null;
|
|
9723
10080
|
state.terminalViewportScrollHandler = null;
|
|
9724
10081
|
state.terminalViewportTouchHandler = null;
|
|
10082
|
+
state.terminalWheelHandler = null;
|
|
10083
|
+
state.terminalClickHandler = null;
|
|
10084
|
+
if (state.terminalScrollbarEl && state.terminalScrollbarEl.parentNode) {
|
|
10085
|
+
state.terminalScrollbarEl.parentNode.removeChild(state.terminalScrollbarEl);
|
|
10086
|
+
}
|
|
10087
|
+
state.terminalScrollbarEl = null;
|
|
10088
|
+
state.terminalScrollbarThumbEl = null;
|
|
9725
10089
|
if (state.terminal) {
|
|
9726
|
-
state.terminal.
|
|
10090
|
+
state.terminal.destroy();
|
|
9727
10091
|
state.terminal = null;
|
|
9728
10092
|
}
|
|
9729
|
-
state.fitAddon = null;
|
|
9730
|
-
state.serializeAddon = null;
|
|
9731
|
-
if (state.terminalDomView && state.terminalDomView.parentNode) {
|
|
9732
|
-
state.terminalDomView.parentNode.removeChild(state.terminalDomView);
|
|
9733
|
-
}
|
|
9734
|
-
state.terminalDomView = null;
|
|
9735
|
-
state._lastDomHtml = "";
|
|
9736
|
-
if (state.terminalDomUpdateTimer) {
|
|
9737
|
-
clearTimeout(state.terminalDomUpdateTimer);
|
|
9738
|
-
state.terminalDomUpdateTimer = null;
|
|
9739
|
-
}
|
|
9740
10093
|
state.terminalSessionId = null;
|
|
9741
10094
|
state.terminalOutput = "";
|
|
9742
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
9743
10095
|
state.terminalAutoFollow = true;
|
|
9744
10096
|
state.showTerminalJumpToBottom = false;
|
|
9745
10097
|
updateTerminalJumpToBottomButton();
|
|
@@ -9762,39 +10114,9 @@
|
|
|
9762
10114
|
}
|
|
9763
10115
|
|
|
9764
10116
|
function ensureTerminalFit() {
|
|
9765
|
-
if (state.
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
var attempt = 0;
|
|
9769
|
-
function finishFit() {
|
|
9770
|
-
state.terminalFitInProgress = false;
|
|
9771
|
-
}
|
|
9772
|
-
function tryFit() {
|
|
9773
|
-
attempt++;
|
|
9774
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
9775
|
-
if (shouldResizeTerminalViewport() && state.fitAddon) {
|
|
9776
|
-
state.fitAddon.fit();
|
|
9777
|
-
maybeScrollTerminalToBottom("resize");
|
|
9778
|
-
if (state.terminal) {
|
|
9779
|
-
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
9780
|
-
}
|
|
9781
|
-
var output = document.getElementById("output");
|
|
9782
|
-
if (output && state.terminal) {
|
|
9783
|
-
var containerW = output.getBoundingClientRect().width;
|
|
9784
|
-
var expectedMinCols = Math.floor(containerW / 20);
|
|
9785
|
-
if (state.terminal.cols < expectedMinCols && attempt < maxAttempts) {
|
|
9786
|
-
requestAnimationFrame(tryFit);
|
|
9787
|
-
return;
|
|
9788
|
-
}
|
|
9789
|
-
}
|
|
9790
|
-
finishFit();
|
|
9791
|
-
} else if (attempt < maxAttempts) {
|
|
9792
|
-
requestAnimationFrame(tryFit);
|
|
9793
|
-
} else {
|
|
9794
|
-
finishFit();
|
|
9795
|
-
}
|
|
9796
|
-
}
|
|
9797
|
-
requestAnimationFrame(tryFit);
|
|
10117
|
+
if (!state.terminal) return;
|
|
10118
|
+
maybeScrollTerminalToBottom("resize");
|
|
10119
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
9798
10120
|
}
|
|
9799
10121
|
|
|
9800
10122
|
function scheduleTerminalResize(immediate) {
|
|
@@ -9810,16 +10132,11 @@
|
|
|
9810
10132
|
}
|
|
9811
10133
|
|
|
9812
10134
|
function syncTerminalSize() {
|
|
9813
|
-
|
|
9814
|
-
if (!state.terminal || !state.fitAddon || !output) return;
|
|
9815
|
-
if (!shouldResizeTerminalViewport()) return;
|
|
9816
|
-
|
|
10135
|
+
if (!state.terminal) return;
|
|
9817
10136
|
var shouldFollow = state.terminalAutoFollow || isTerminalNearBottom();
|
|
9818
|
-
state.fitAddon.fit();
|
|
9819
10137
|
if (shouldFollow) {
|
|
9820
10138
|
maybeScrollTerminalToBottom("resize");
|
|
9821
10139
|
}
|
|
9822
|
-
|
|
9823
10140
|
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
9824
10141
|
}
|
|
9825
10142
|
|
|
@@ -9865,9 +10182,8 @@
|
|
|
9865
10182
|
flushPendingMessages();
|
|
9866
10183
|
// Re-fit terminal on reconnect — the viewport may have changed
|
|
9867
10184
|
// while disconnected, and the PTY needs up-to-date dimensions.
|
|
9868
|
-
if (state.terminal
|
|
9869
|
-
state.
|
|
9870
|
-
ensureTerminalFit();
|
|
10185
|
+
if (state.terminal) {
|
|
10186
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
9871
10187
|
}
|
|
9872
10188
|
};
|
|
9873
10189
|
|
|
@@ -9947,8 +10263,22 @@
|
|
|
9947
10263
|
snapshot.messages = msg.data.messages;
|
|
9948
10264
|
}
|
|
9949
10265
|
|
|
9950
|
-
//
|
|
9951
|
-
|
|
10266
|
+
// Fast path: chunk-only incremental events skip expensive chat update
|
|
10267
|
+
var isChunkOnly = isIncremental && msg.data.chunk
|
|
10268
|
+
&& !msg.data.lastMessage && !snapshot.messages
|
|
10269
|
+
&& snapshot.output === undefined
|
|
10270
|
+
&& !msg.data.structuredState && !msg.data.sessionKind;
|
|
10271
|
+
|
|
10272
|
+
if (isChunkOnly) {
|
|
10273
|
+
// Only update permissionBlocked if it actually changed
|
|
10274
|
+
if (msg.data.permissionBlocked !== undefined) {
|
|
10275
|
+
var existingPB = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
10276
|
+
if (existingPB && !!existingPB.permissionBlocked !== !!msg.data.permissionBlocked) {
|
|
10277
|
+
updateSessionSnapshot(snapshot);
|
|
10278
|
+
if (msg.sessionId === state.selectedId) updateTaskDisplay();
|
|
10279
|
+
}
|
|
10280
|
+
}
|
|
10281
|
+
} else if (snapshot.output !== undefined || snapshot.messages || isIncremental || msg.data.permissionBlocked !== undefined) {
|
|
9952
10282
|
updateSessionSnapshot(snapshot);
|
|
9953
10283
|
if (msg.sessionId === state.selectedId) {
|
|
9954
10284
|
var updatedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; }) || snapshot;
|
|
@@ -9966,21 +10296,20 @@
|
|
|
9966
10296
|
// Real-time terminal output
|
|
9967
10297
|
if (msg.sessionId === state.selectedId && state.terminal && msg.data) {
|
|
9968
10298
|
if (msg.data.chunk && (!state.terminalSessionId || state.terminalSessionId === msg.sessionId)) {
|
|
9969
|
-
// Fast path: write chunk directly to avoid full-output comparison
|
|
9970
|
-
|
|
10299
|
+
// Fast path: write chunk directly to avoid full-output comparison.
|
|
10300
|
+
state.lastChunkAt = Date.now();
|
|
9971
10301
|
state.terminalLiveStreamSessions[msg.sessionId] = true;
|
|
9972
10302
|
state.terminal.write(msg.data.chunk);
|
|
9973
10303
|
state.terminalSessionId = msg.sessionId;
|
|
9974
10304
|
if (msg.data.output) {
|
|
9975
10305
|
state.terminalOutput = normalizeTerminalOutput(msg.data.output);
|
|
9976
10306
|
} else {
|
|
9977
|
-
state.terminalOutput =
|
|
10307
|
+
state.terminalOutput = (state.terminalOutput || "") + normalizeTerminalOutput(msg.data.chunk);
|
|
9978
10308
|
}
|
|
9979
10309
|
maybeScrollTerminalToBottom("output");
|
|
9980
10310
|
updateTerminalJumpToBottomButton();
|
|
9981
|
-
scheduleMobileDomUpdate();
|
|
9982
10311
|
} else if (!msg.data.incremental && Object.prototype.hasOwnProperty.call(msg.data, "output")) {
|
|
9983
|
-
// Fallback: no chunk available, use full-output comparison
|
|
10312
|
+
// Fallback: no chunk available, use full-output comparison.
|
|
9984
10313
|
syncTerminalBuffer(msg.sessionId, msg.data.output || "", { mode: "append" });
|
|
9985
10314
|
}
|
|
9986
10315
|
}
|
|
@@ -10030,6 +10359,7 @@
|
|
|
10030
10359
|
endedNotifBody = endedSession ? (endedSession.command || msg.sessionId) : msg.sessionId;
|
|
10031
10360
|
}
|
|
10032
10361
|
notifyTaskEnded(msg.sessionId, endedNotifTitle, endedNotifBody);
|
|
10362
|
+
clearSessionProgressNative(msg.sessionId);
|
|
10033
10363
|
if (msg.sessionId !== state.selectedId || document.hidden) {
|
|
10034
10364
|
showNotificationBubble({
|
|
10035
10365
|
title: endedNotifTitle,
|
|
@@ -10104,6 +10434,11 @@
|
|
|
10104
10434
|
updateTerminalOutput(msg.data.output || "", msg.sessionId, "append");
|
|
10105
10435
|
// Ensure terminal is properly fitted after receiving initial data
|
|
10106
10436
|
scheduleTerminalResize(true);
|
|
10437
|
+
if (state.terminal && state.terminal.remeasure) {
|
|
10438
|
+
requestAnimationFrame(function() {
|
|
10439
|
+
if (state.terminal) state.terminal.remeasure();
|
|
10440
|
+
});
|
|
10441
|
+
}
|
|
10107
10442
|
}
|
|
10108
10443
|
break;
|
|
10109
10444
|
case 'usage':
|
|
@@ -10116,6 +10451,7 @@
|
|
|
10116
10451
|
updateTaskDisplay();
|
|
10117
10452
|
}
|
|
10118
10453
|
notifyTaskProgress(msg.sessionId, msg.data || null);
|
|
10454
|
+
syncSessionProgressToNative(msg.sessionId);
|
|
10119
10455
|
// Update session list to reflect current activity (debounced)
|
|
10120
10456
|
scheduleSessionListUpdate();
|
|
10121
10457
|
break;
|
|
@@ -10185,6 +10521,7 @@
|
|
|
10185
10521
|
statusUpdate.approvalStats = msg.data.approvalStats;
|
|
10186
10522
|
}
|
|
10187
10523
|
updateSessionSnapshot(statusUpdate);
|
|
10524
|
+
syncSessionProgressToNative(msg.sessionId);
|
|
10188
10525
|
if (msg.sessionId === state.selectedId) {
|
|
10189
10526
|
updateTaskDisplay();
|
|
10190
10527
|
if (msg.data.approvalStats) {
|
|
@@ -10207,6 +10544,10 @@
|
|
|
10207
10544
|
if (msg.data) {
|
|
10208
10545
|
if (msg.data.kind === "update") {
|
|
10209
10546
|
notifyUpdateAvailable(msg.data.current || "-", msg.data.latest || "-");
|
|
10547
|
+
} else if (msg.data.kind === "auto-update-start") {
|
|
10548
|
+
showAutoUpdateOverlay(msg.data.current || "-", msg.data.latest || "-");
|
|
10549
|
+
} else if (msg.data.kind === "auto-update-restart") {
|
|
10550
|
+
showRestartOverlay();
|
|
10210
10551
|
} else if (msg.data.kind === "restart") {
|
|
10211
10552
|
showRestartOverlay();
|
|
10212
10553
|
}
|
|
@@ -10440,8 +10781,12 @@
|
|
|
10440
10781
|
}
|
|
10441
10782
|
updateTerminalJumpToBottomButton();
|
|
10442
10783
|
if (state.currentView === "terminal") {
|
|
10443
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
10444
10784
|
scheduleTerminalResize(true);
|
|
10785
|
+
if (state.terminal && state.terminal.remeasure) {
|
|
10786
|
+
requestAnimationFrame(function() {
|
|
10787
|
+
if (state.terminal) state.terminal.remeasure();
|
|
10788
|
+
});
|
|
10789
|
+
}
|
|
10445
10790
|
}
|
|
10446
10791
|
}
|
|
10447
10792
|
|
|
@@ -10467,13 +10812,15 @@
|
|
|
10467
10812
|
if (chatRenderTimer) clearTimeout(chatRenderTimer);
|
|
10468
10813
|
if (immediate) {
|
|
10469
10814
|
chatRenderTimer = null;
|
|
10470
|
-
// Messages already updated in handleWebSocketMessage, just render
|
|
10471
10815
|
renderChat();
|
|
10472
10816
|
return;
|
|
10473
10817
|
}
|
|
10818
|
+
var selectedForDelay = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
10819
|
+
var isActiveStream = selectedForDelay && selectedForDelay.status === "running"
|
|
10820
|
+
&& selectedForDelay.sessionKind !== "structured";
|
|
10821
|
+
var delay = isActiveStream ? 150 : 30;
|
|
10474
10822
|
chatRenderTimer = setTimeout(function() {
|
|
10475
10823
|
chatRenderTimer = null;
|
|
10476
|
-
// Re-parse messages from the latest session output (fallback for edge cases)
|
|
10477
10824
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
10478
10825
|
if (selectedSession) {
|
|
10479
10826
|
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, selectedSession.output, true));
|
|
@@ -11016,6 +11363,10 @@
|
|
|
11016
11363
|
list.innerHTML = html;
|
|
11017
11364
|
}
|
|
11018
11365
|
|
|
11366
|
+
// Sync todo progress to native notification
|
|
11367
|
+
if (state.selectedId) {
|
|
11368
|
+
syncSessionProgressToNative(state.selectedId);
|
|
11369
|
+
}
|
|
11019
11370
|
}
|
|
11020
11371
|
|
|
11021
11372
|
|
|
@@ -11143,161 +11494,6 @@
|
|
|
11143
11494
|
})();
|
|
11144
11495
|
|
|
11145
11496
|
// ===== Terminal copy button for mobile =====
|
|
11146
|
-
// ===== Mobile DOM terminal view =====
|
|
11147
|
-
function initMobileDomTerminal(container) {
|
|
11148
|
-
// DOM terminal feature removed — always return
|
|
11149
|
-
return;
|
|
11150
|
-
|
|
11151
|
-
// Create DOM view container
|
|
11152
|
-
var domView = document.createElement("div");
|
|
11153
|
-
domView.className = "terminal-dom-view active";
|
|
11154
|
-
container.appendChild(domView);
|
|
11155
|
-
|
|
11156
|
-
// Always set font-size explicitly to match xterm
|
|
11157
|
-
domView.style.fontSize = (state.terminalBaseFontSize * state.terminalScale) + "px";
|
|
11158
|
-
|
|
11159
|
-
// Hide xterm canvas but keep it in layout for FitAddon sizing.
|
|
11160
|
-
// Use opacity:0 + pointer-events:none so the element still occupies
|
|
11161
|
-
// space in the flex container and fit() can compute cols/rows correctly.
|
|
11162
|
-
setTimeout(function() {
|
|
11163
|
-
var xtermEl = container.querySelector(".xterm");
|
|
11164
|
-
if (xtermEl) {
|
|
11165
|
-
xtermEl.style.opacity = "0";
|
|
11166
|
-
xtermEl.style.pointerEvents = "none";
|
|
11167
|
-
}
|
|
11168
|
-
}, 100);
|
|
11169
|
-
|
|
11170
|
-
// Save reference
|
|
11171
|
-
state.terminalDomView = domView;
|
|
11172
|
-
state.terminalDomUpdateTimer = null;
|
|
11173
|
-
|
|
11174
|
-
// Scroll events for auto-follow
|
|
11175
|
-
domView.addEventListener("scroll", function() {
|
|
11176
|
-
var distance = domView.scrollHeight - domView.clientHeight - domView.scrollTop;
|
|
11177
|
-
if (distance <= state.terminalScrollThreshold) {
|
|
11178
|
-
state.terminalAutoFollow = true;
|
|
11179
|
-
clearTerminalScrollIdleTimer();
|
|
11180
|
-
updateTerminalJumpToBottomButton();
|
|
11181
|
-
} else {
|
|
11182
|
-
setTerminalManualScrollActive();
|
|
11183
|
-
}
|
|
11184
|
-
}, { passive: true });
|
|
11185
|
-
|
|
11186
|
-
domView.addEventListener("touchmove", function() {
|
|
11187
|
-
setTerminalManualScrollActive();
|
|
11188
|
-
}, { passive: true });
|
|
11189
|
-
|
|
11190
|
-
// Trigger initial render
|
|
11191
|
-
scheduleMobileDomUpdate();
|
|
11192
|
-
}
|
|
11193
|
-
|
|
11194
|
-
function updateMobileDomView() {
|
|
11195
|
-
if (!state.terminalDomView || !state.serializeAddon) return;
|
|
11196
|
-
|
|
11197
|
-
try {
|
|
11198
|
-
// Serialize the entire buffer including scrollback history
|
|
11199
|
-
var buf = state.terminal.buffer.active;
|
|
11200
|
-
var totalRows = buf.length;
|
|
11201
|
-
var html = state.serializeAddon.serializeAsHTML({
|
|
11202
|
-
includeGlobalBackground: true,
|
|
11203
|
-
range: { start: 0, end: totalRows }
|
|
11204
|
-
});
|
|
11205
|
-
|
|
11206
|
-
// Extract the <pre>...</pre> portion
|
|
11207
|
-
var match = html.match(/<pre[\s\S]*<\/pre>/);
|
|
11208
|
-
var preHtml = match ? match[0] : "";
|
|
11209
|
-
|
|
11210
|
-
// Strip inline font-size/font-family from the serialized HTML
|
|
11211
|
-
// so our CSS controls sizing and font consistently
|
|
11212
|
-
preHtml = preHtml.replace(/font-size:\s*[^;"']+;?/g, "");
|
|
11213
|
-
preHtml = preHtml.replace(/font-family:\s*[^;"']+;?/g, "");
|
|
11214
|
-
|
|
11215
|
-
// Fix colors for dark background
|
|
11216
|
-
preHtml = fixDarkTerminalColors(preHtml);
|
|
11217
|
-
|
|
11218
|
-
// Skip update if content unchanged
|
|
11219
|
-
if (preHtml === state._lastDomHtml) return;
|
|
11220
|
-
state._lastDomHtml = preHtml;
|
|
11221
|
-
|
|
11222
|
-
// Preserve scroll position for non-auto-follow mode
|
|
11223
|
-
var wasAtBottom = state.terminalAutoFollow;
|
|
11224
|
-
var scrollTop = state.terminalDomView.scrollTop;
|
|
11225
|
-
|
|
11226
|
-
state.terminalDomView.innerHTML = preHtml;
|
|
11227
|
-
|
|
11228
|
-
if (wasAtBottom) {
|
|
11229
|
-
state.terminalDomView.scrollTop = state.terminalDomView.scrollHeight;
|
|
11230
|
-
} else {
|
|
11231
|
-
state.terminalDomView.scrollTop = scrollTop;
|
|
11232
|
-
}
|
|
11233
|
-
} catch (e) {
|
|
11234
|
-
// Fallback: plain text if serialize fails
|
|
11235
|
-
if (state.terminal && state.terminal.buffer) {
|
|
11236
|
-
var buf = state.terminal.buffer.active;
|
|
11237
|
-
var lines = [];
|
|
11238
|
-
for (var i = 0; i < buf.length; i++) {
|
|
11239
|
-
var line = buf.getLine(i);
|
|
11240
|
-
if (line) lines.push(line.translateToString(true));
|
|
11241
|
-
}
|
|
11242
|
-
var text = lines.join("\n").replace(/\n+$/, "");
|
|
11243
|
-
state.terminalDomView.innerHTML = '<pre><div style="padding:8px 12px">' + escapeHtml(text) + '</div></pre>';
|
|
11244
|
-
if (state.terminalAutoFollow) {
|
|
11245
|
-
state.terminalDomView.scrollTop = state.terminalDomView.scrollHeight;
|
|
11246
|
-
}
|
|
11247
|
-
}
|
|
11248
|
-
}
|
|
11249
|
-
}
|
|
11250
|
-
|
|
11251
|
-
// Fix serialize addon's color issues on dark terminal background
|
|
11252
|
-
function fixDarkTerminalColors(html) {
|
|
11253
|
-
// Theme reference: bg=#1f1b17, fg=#f5eadc, black=#1f1b17, brightBlack=#625347
|
|
11254
|
-
// 1. Hardcoded inverse: black text on gray → theme fg on theme brightBlack bg
|
|
11255
|
-
html = html.replace(
|
|
11256
|
-
/color:\s*#000000;\s*background-color:\s*#BFBFBF/g,
|
|
11257
|
-
"color: #f5eadc; background-color: #625347"
|
|
11258
|
-
);
|
|
11259
|
-
// 2. Fix foreground colors that are too dark to read on #1f1b17 background.
|
|
11260
|
-
// Process each style attribute: split into declarations, fix only "color:" (not "background-color:").
|
|
11261
|
-
html = html.replace(/style='([^']*)'/g, function(_match, styles) {
|
|
11262
|
-
var parts = styles.split(";");
|
|
11263
|
-
for (var i = 0; i < parts.length; i++) {
|
|
11264
|
-
var decl = parts[i].trim();
|
|
11265
|
-
// Skip background-color declarations
|
|
11266
|
-
if (/^background-color\s*:/.test(decl)) continue;
|
|
11267
|
-
// Match standalone color declaration
|
|
11268
|
-
if (/^color\s*:/.test(decl)) {
|
|
11269
|
-
var hexMatch = decl.match(/#([0-9a-fA-F]{6})\b/);
|
|
11270
|
-
if (hexMatch && isColorTooDark("#" + hexMatch[1])) {
|
|
11271
|
-
parts[i] = parts[i].replace(/#[0-9a-fA-F]{6}/, "#625347");
|
|
11272
|
-
}
|
|
11273
|
-
}
|
|
11274
|
-
}
|
|
11275
|
-
return "style='" + parts.join(";") + "'";
|
|
11276
|
-
});
|
|
11277
|
-
return html;
|
|
11278
|
-
}
|
|
11279
|
-
|
|
11280
|
-
function isColorTooDark(hex) {
|
|
11281
|
-
// Parse hex color and check relative luminance
|
|
11282
|
-
var r = parseInt(hex.substring(1, 3), 16);
|
|
11283
|
-
var g = parseInt(hex.substring(3, 5), 16);
|
|
11284
|
-
var b = parseInt(hex.substring(5, 7), 16);
|
|
11285
|
-
// Simple perceived brightness: if below threshold, it's too dark for #1f1b17 bg
|
|
11286
|
-
var brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
|
11287
|
-
return brightness < 45; // #1f1b17 has brightness ~22, threshold catches colors close to it
|
|
11288
|
-
}
|
|
11289
|
-
|
|
11290
|
-
function scheduleMobileDomUpdate() {
|
|
11291
|
-
if (!state.terminalDomView) return;
|
|
11292
|
-
// Trailing-edge debounce: reset timer on each call to batch rapid updates
|
|
11293
|
-
if (state.terminalDomUpdateTimer) {
|
|
11294
|
-
clearTimeout(state.terminalDomUpdateTimer);
|
|
11295
|
-
}
|
|
11296
|
-
state.terminalDomUpdateTimer = setTimeout(function() {
|
|
11297
|
-
state.terminalDomUpdateTimer = null;
|
|
11298
|
-
updateMobileDomView();
|
|
11299
|
-
}, 150);
|
|
11300
|
-
}
|
|
11301
11497
|
|
|
11302
11498
|
function isNoiseLine(line) {
|
|
11303
11499
|
if (!line) return false;
|
|
@@ -13387,7 +13583,79 @@
|
|
|
13387
13583
|
);
|
|
13388
13584
|
}
|
|
13389
13585
|
|
|
13390
|
-
|
|
13586
|
+
// ── Native Live Progress Sync ──
|
|
13587
|
+
|
|
13588
|
+
var _progressSyncTimers = {};
|
|
13589
|
+
var _PROGRESS_SYNC_DEBOUNCE_MS = 300;
|
|
13590
|
+
|
|
13591
|
+
function syncSessionProgressToNative(sessionId) {
|
|
13592
|
+
if (!_hasNativeBridge || typeof WandNative.updateSessionProgress !== "function") return;
|
|
13593
|
+
if (!sessionId) return;
|
|
13594
|
+
if (_progressSyncTimers[sessionId]) {
|
|
13595
|
+
clearTimeout(_progressSyncTimers[sessionId]);
|
|
13596
|
+
}
|
|
13597
|
+
_progressSyncTimers[sessionId] = setTimeout(function() {
|
|
13598
|
+
delete _progressSyncTimers[sessionId];
|
|
13599
|
+
_doSyncSessionProgress(sessionId);
|
|
13600
|
+
}, _PROGRESS_SYNC_DEBOUNCE_MS);
|
|
13601
|
+
}
|
|
13602
|
+
|
|
13603
|
+
function _doSyncSessionProgress(sessionId) {
|
|
13604
|
+
var session = state.sessions.find(function(s) { return s.id === sessionId; });
|
|
13605
|
+
if (!session) return;
|
|
13606
|
+
|
|
13607
|
+
var sessionLabel = session.summary || session.command || sessionId;
|
|
13608
|
+
var sessionStatus = session.status || "running";
|
|
13609
|
+
|
|
13610
|
+
// Clear notification for inactive sessions
|
|
13611
|
+
if (sessionStatus === "idle" || sessionStatus === "archived" || sessionStatus === "exited") {
|
|
13612
|
+
clearSessionProgressNative(sessionId);
|
|
13613
|
+
return;
|
|
13614
|
+
}
|
|
13615
|
+
|
|
13616
|
+
// Get latest todos from session messages
|
|
13617
|
+
var todos = null;
|
|
13618
|
+
var messages = session.messages || [];
|
|
13619
|
+
for (var i = messages.length - 1; i >= 0; i--) {
|
|
13620
|
+
var msg = messages[i];
|
|
13621
|
+
if (!msg.content || !Array.isArray(msg.content)) continue;
|
|
13622
|
+
for (var j = msg.content.length - 1; j >= 0; j--) {
|
|
13623
|
+
var block = msg.content[j];
|
|
13624
|
+
if (block.type === "tool_use" && block.name === "TodoWrite"
|
|
13625
|
+
&& block.input && block.input.todos) {
|
|
13626
|
+
todos = block.input.todos;
|
|
13627
|
+
break;
|
|
13628
|
+
}
|
|
13629
|
+
}
|
|
13630
|
+
if (todos) break;
|
|
13631
|
+
}
|
|
13632
|
+
|
|
13633
|
+
// Get current task
|
|
13634
|
+
var currentTask = "";
|
|
13635
|
+
if (sessionId === state.selectedId && state.currentTask && state.currentTask.title) {
|
|
13636
|
+
currentTask = state.currentTask.title;
|
|
13637
|
+
}
|
|
13638
|
+
|
|
13639
|
+
var data = {
|
|
13640
|
+
sessionLabel: sessionLabel,
|
|
13641
|
+
status: sessionStatus,
|
|
13642
|
+
currentTask: currentTask,
|
|
13643
|
+
todos: todos || []
|
|
13644
|
+
};
|
|
13645
|
+
|
|
13646
|
+
try {
|
|
13647
|
+
WandNative.updateSessionProgress(sessionId, JSON.stringify(data));
|
|
13648
|
+
} catch (_e) {}
|
|
13649
|
+
}
|
|
13650
|
+
|
|
13651
|
+
function clearSessionProgressNative(sessionId) {
|
|
13652
|
+
if (!_hasNativeBridge || typeof WandNative.clearSessionProgress !== "function") return;
|
|
13653
|
+
if (_progressSyncTimers[sessionId]) {
|
|
13654
|
+
clearTimeout(_progressSyncTimers[sessionId]);
|
|
13655
|
+
delete _progressSyncTimers[sessionId];
|
|
13656
|
+
}
|
|
13657
|
+
try { WandNative.clearSessionProgress(sessionId); } catch (_e) {}
|
|
13658
|
+
}
|
|
13391
13659
|
|
|
13392
13660
|
/**
|
|
13393
13661
|
* Play a soft, rounded notification chime using Web Audio API.
|
|
@@ -13606,6 +13874,23 @@
|
|
|
13606
13874
|
}, 2000);
|
|
13607
13875
|
}
|
|
13608
13876
|
|
|
13877
|
+
function showAutoUpdateOverlay(currentVer, latestVer) {
|
|
13878
|
+
if (document.getElementById("restart-overlay")) return;
|
|
13879
|
+
var overlay = document.createElement("div");
|
|
13880
|
+
overlay.id = "restart-overlay";
|
|
13881
|
+
overlay.className = "restart-overlay";
|
|
13882
|
+
overlay.innerHTML =
|
|
13883
|
+
'<div class="restart-overlay-content">' +
|
|
13884
|
+
'<div class="restart-spinner"></div>' +
|
|
13885
|
+
'<div class="restart-title">\u81ea\u52a8\u66f4\u65b0\u4e2d</div>' +
|
|
13886
|
+
'<div class="restart-subtitle">' +
|
|
13887
|
+
escapeHtml(currentVer) + ' \u2192 ' + escapeHtml(latestVer) +
|
|
13888
|
+
'<br>\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\uff0c\u7a0d\u540e\u5c06\u81ea\u52a8\u91cd\u542f\u2026' +
|
|
13889
|
+
'</div>' +
|
|
13890
|
+
'</div>';
|
|
13891
|
+
document.body.appendChild(overlay);
|
|
13892
|
+
}
|
|
13893
|
+
|
|
13609
13894
|
function escapeHtml(value) {
|
|
13610
13895
|
return String(value)
|
|
13611
13896
|
.replace(/&/g, "&")
|