@co0ontty/wand 1.15.1 → 1.17.4
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 +950 -517
- package/dist/web-ui/content/styles.css +312 -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",
|
|
@@ -199,11 +201,13 @@
|
|
|
199
201
|
claudeHistoryLoaded: false,
|
|
200
202
|
claudeHistoryExpanded: true,
|
|
201
203
|
claudeHistoryExpandedDirs: {},
|
|
204
|
+
archivedExpanded: false,
|
|
202
205
|
sessionsManageMode: false,
|
|
203
206
|
selectedSessionIds: {},
|
|
204
207
|
selectedClaudeHistoryIds: {},
|
|
205
208
|
askUserSelections: {}, // { toolUseId: { 0: [optIdx...], submitted: false } }
|
|
206
209
|
queueEpoch: 0, // Monotonic counter for queue state freshness
|
|
210
|
+
pendingAttachments: [], // [{ file, previewUrl, name, size }]
|
|
207
211
|
// Load last used working directory from localStorage
|
|
208
212
|
workingDir: (function() {
|
|
209
213
|
try {
|
|
@@ -889,16 +893,21 @@
|
|
|
889
893
|
try {
|
|
890
894
|
render({ skipShellChrome: true });
|
|
891
895
|
} catch (_e) {
|
|
892
|
-
// render() may fail if external scripts (
|
|
896
|
+
// render() may fail if external scripts (wterm) failed to load;
|
|
893
897
|
// continue with polling and session loading so the app remains functional
|
|
894
898
|
}
|
|
895
899
|
bindForegroundSyncListeners();
|
|
896
900
|
startPolling();
|
|
897
901
|
refreshAll();
|
|
902
|
+
fetchAvailableModels();
|
|
898
903
|
requestNotificationPermission();
|
|
899
904
|
if (config.updateAvailable && config.latestVersion) {
|
|
900
905
|
notifyUpdateAvailable(config.currentVersion || "-", config.latestVersion);
|
|
901
906
|
}
|
|
907
|
+
// APK auto-update check on startup
|
|
908
|
+
if (_apkVersion) {
|
|
909
|
+
checkApkAutoUpdate();
|
|
910
|
+
}
|
|
902
911
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
903
912
|
loadClaudeHistory();
|
|
904
913
|
}
|
|
@@ -952,6 +961,7 @@
|
|
|
952
961
|
attachEventListeners();
|
|
953
962
|
updateDrawerState();
|
|
954
963
|
syncComposerModeSelect();
|
|
964
|
+
syncComposerModelSelect(getSelectedSession());
|
|
955
965
|
applyCurrentView();
|
|
956
966
|
if (!skipShellChrome) {
|
|
957
967
|
updateShellChrome();
|
|
@@ -1106,12 +1116,21 @@
|
|
|
1106
1116
|
'<span class="session-count" id="session-count">' + String(state.sessions.length) + '</span>' +
|
|
1107
1117
|
'</div>' +
|
|
1108
1118
|
'<div class="sidebar-header-actions">' +
|
|
1109
|
-
'<
|
|
1110
|
-
'<
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
'<
|
|
1114
|
-
|
|
1119
|
+
'<div class="sidebar-header-more">' +
|
|
1120
|
+
'<button id="sidebar-more-btn" class="btn btn-ghost btn-sm" type="button" title="更多操作">' +
|
|
1121
|
+
'<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>' +
|
|
1122
|
+
'</button>' +
|
|
1123
|
+
'<div class="sidebar-header-overflow" id="sidebar-overflow-menu">' +
|
|
1124
|
+
'<button class="overflow-item" id="sidebar-home-btn" type="button">' +
|
|
1125
|
+
'<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>' +
|
|
1126
|
+
'<span>回到首页</span>' +
|
|
1127
|
+
'</button>' +
|
|
1128
|
+
'<button class="overflow-item" id="sidebar-refresh-btn" type="button">' +
|
|
1129
|
+
'<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>' +
|
|
1130
|
+
'<span>刷新页面</span>' +
|
|
1131
|
+
'</button>' +
|
|
1132
|
+
'</div>' +
|
|
1133
|
+
'</div>' +
|
|
1115
1134
|
'<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
|
|
1116
1135
|
'<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
1136
|
'</button>' +
|
|
@@ -1126,15 +1145,21 @@
|
|
|
1126
1145
|
'<div class="sidebar-footer">' +
|
|
1127
1146
|
'<button id="drawer-new-session-button" class="btn btn-primary btn-block"><span>+</span> 新会话</button>' +
|
|
1128
1147
|
'<div class="sidebar-footer-actions">' +
|
|
1129
|
-
'<button id="file-panel-toggle-btn" class="btn btn-ghost btn-sm' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件"
|
|
1148
|
+
'<button id="file-panel-toggle-btn" class="btn btn-ghost btn-sm' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件">' +
|
|
1149
|
+
'<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>' +
|
|
1150
|
+
'<span>文件</span>' +
|
|
1151
|
+
'</button>' +
|
|
1130
1152
|
'<button id="settings-button" class="btn btn-ghost btn-sm" type="button" title="设置">' +
|
|
1131
|
-
'<svg width="
|
|
1153
|
+
'<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>' +
|
|
1154
|
+
'<span>设置</span>' +
|
|
1132
1155
|
'</button>' +
|
|
1133
1156
|
'<button id="pwa-install-button" class="btn btn-ghost btn-sm hidden" title="安装应用">' +
|
|
1134
|
-
'<svg width="
|
|
1157
|
+
'<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>' +
|
|
1158
|
+
'<span>安装</span>' +
|
|
1135
1159
|
'</button>' +
|
|
1136
1160
|
'<button id="logout-button" class="btn btn-ghost btn-sm sidebar-logout" type="button" title="退出登录">' +
|
|
1137
|
-
'<svg width="
|
|
1161
|
+
'<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>' +
|
|
1162
|
+
'<span>退出</span>' +
|
|
1138
1163
|
'</button>' +
|
|
1139
1164
|
'</div>' +
|
|
1140
1165
|
'</div>' +
|
|
@@ -1226,11 +1251,19 @@
|
|
|
1226
1251
|
'</div>' +
|
|
1227
1252
|
'<div class="input-composer">' +
|
|
1228
1253
|
'<textarea id="input-box" class="input-textarea" placeholder="' + getComposerPlaceholder(selectedSession, state.terminalInteractive) + '" rows="1">' + escapeHtml(currentDraft) + '</textarea>' +
|
|
1254
|
+
'<div id="attachment-preview" class="attachment-preview hidden"></div>' +
|
|
1229
1255
|
'<div class="input-composer-bar">' +
|
|
1230
1256
|
'<div class="input-composer-left">' +
|
|
1257
|
+
'<button id="attach-btn" class="btn-circle btn-circle-attach" type="button" title="附加文件">' +
|
|
1258
|
+
'<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>' +
|
|
1259
|
+
'</button>' +
|
|
1260
|
+
'<input type="file" id="file-upload-input" multiple style="display:none">' +
|
|
1231
1261
|
'<select id="chat-mode-select" class="chat-mode-select" title="仅对新建会话生效">' +
|
|
1232
1262
|
renderModeOptions(preferredTool, composerMode) +
|
|
1233
1263
|
'</select>' +
|
|
1264
|
+
'<select id="chat-model-select" class="chat-mode-select chat-model-select" title="切换模型(对运行中会话发送 /model,对新会话作为 --model 启动)">' +
|
|
1265
|
+
renderChatModelOptions(getEffectiveModel(selectedSession)) +
|
|
1266
|
+
'</select>' +
|
|
1234
1267
|
'<button id="terminal-interactive-toggle-top" class="composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨</button>' +
|
|
1235
1268
|
'<span class="permission-actions hidden" id="permission-actions">' +
|
|
1236
1269
|
'<span class="permission-actions-divider"></span>' +
|
|
@@ -1387,6 +1420,14 @@
|
|
|
1387
1420
|
'<button id="do-restart-button" class="btn btn-success btn-sm hidden">\u91cd\u542f\u751f\u6548</button>' +
|
|
1388
1421
|
'</div>' +
|
|
1389
1422
|
'<p id="update-message" class="hint hidden"></p>' +
|
|
1423
|
+
'<div class="settings-auto-update-row" style="display:flex;align-items:center;justify-content:space-between;margin-top:10px">' +
|
|
1424
|
+
'<span class="settings-label">自动更新</span>' +
|
|
1425
|
+
'<label style="position:relative;cursor:pointer">' +
|
|
1426
|
+
'<input type="checkbox" id="auto-update-web-toggle" class="switch-toggle">' +
|
|
1427
|
+
'<span class="switch-slider"></span>' +
|
|
1428
|
+
'</label>' +
|
|
1429
|
+
'</div>' +
|
|
1430
|
+
'<p class="hint" style="margin-top:2px">开启后,检测到新版本将自动下载安装并重启服务。</p>' +
|
|
1390
1431
|
'</div>' +
|
|
1391
1432
|
'<div class="settings-update-section" id="android-apk-section">' +
|
|
1392
1433
|
'<div id="android-apk-current-row" class="settings-about-row hidden">' +
|
|
@@ -1403,6 +1444,14 @@
|
|
|
1403
1444
|
'<span class="settings-value" id="settings-android-apk-local" style="flex:1">-</span>' +
|
|
1404
1445
|
'<button id="download-local-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
|
|
1405
1446
|
'</div>' +
|
|
1447
|
+
'<div id="android-auto-update-row" class="hidden" style="display:flex;align-items:center;justify-content:space-between;margin-top:10px">' +
|
|
1448
|
+
'<span class="settings-label">自动更新</span>' +
|
|
1449
|
+
'<label style="position:relative;cursor:pointer">' +
|
|
1450
|
+
'<input type="checkbox" id="auto-update-apk-toggle" class="switch-toggle">' +
|
|
1451
|
+
'<span class="switch-slider"></span>' +
|
|
1452
|
+
'</label>' +
|
|
1453
|
+
'</div>' +
|
|
1454
|
+
'<p id="android-auto-update-hint" class="hint hidden" style="margin-top:2px">开启后,检测到新版 APK 将自动下载安装。</p>' +
|
|
1406
1455
|
'<p id="android-apk-message" class="hint hidden"></p>' +
|
|
1407
1456
|
'</div>' +
|
|
1408
1457
|
'<div class="settings-update-section" id="android-connect-section">' +
|
|
@@ -1516,6 +1565,16 @@
|
|
|
1516
1565
|
'</div>' +
|
|
1517
1566
|
'</div>' +
|
|
1518
1567
|
'<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
1568
|
+
'<div class="field">' +
|
|
1569
|
+
'<label class="field-label" for="cfg-default-model">默认模型</label>' +
|
|
1570
|
+
'<div style="display:flex;gap:8px;align-items:center;">' +
|
|
1571
|
+
'<select id="cfg-default-model" class="field-input" style="flex:1;">' +
|
|
1572
|
+
'<option value="">跟随 Claude Code 默认</option>' +
|
|
1573
|
+
'</select>' +
|
|
1574
|
+
'<button type="button" id="cfg-default-model-refresh" class="btn btn-ghost btn-sm" title="刷新模型列表">刷新</button>' +
|
|
1575
|
+
'</div>' +
|
|
1576
|
+
'<p class="field-hint" id="cfg-default-model-version" style="margin-top:4px;">新建会话时默认使用该模型;运行中的会话可在输入框切换。</p>' +
|
|
1577
|
+
'</div>' +
|
|
1519
1578
|
'<div class="field">' +
|
|
1520
1579
|
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
1521
1580
|
'<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
|
|
@@ -1675,7 +1734,7 @@
|
|
|
1675
1734
|
groups.push(renderRecentGroup(activeSessions, recentHistorySessions));
|
|
1676
1735
|
}
|
|
1677
1736
|
if (archivedSessions.length > 0) {
|
|
1678
|
-
groups.push(
|
|
1737
|
+
groups.push(renderArchivedGroup(archivedSessions));
|
|
1679
1738
|
}
|
|
1680
1739
|
groups.push(renderClaudeHistorySection());
|
|
1681
1740
|
if (activeSessions.length === 0 && archivedSessions.length === 0 && recentHistorySessions.length === 0) {
|
|
@@ -1696,12 +1755,17 @@
|
|
|
1696
1755
|
var historyCount = getSelectedClaudeHistoryIds().length;
|
|
1697
1756
|
var totalCount = sessionCount + historyCount;
|
|
1698
1757
|
var hasAny = totalCount > 0;
|
|
1758
|
+
var selectable = countSelectableItems();
|
|
1759
|
+
var allSelected = selectable > 0 && totalCount >= selectable;
|
|
1760
|
+
var selectAllLabel = allSelected ? "取消全选" : "全选";
|
|
1761
|
+
var selectAllAction = allSelected ? "clear-selection" : "select-all-visible";
|
|
1762
|
+
var selectAllDisabled = selectable === 0 ? ' disabled' : '';
|
|
1699
1763
|
|
|
1700
1764
|
return '<div class="session-manage-bar active">' +
|
|
1701
1765
|
'<div class="session-manage-summary">已选择 ' + totalCount + ' 项</div>' +
|
|
1702
1766
|
'<div class="session-manage-actions">' +
|
|
1703
|
-
'<button class="session-manage-btn" data-action="
|
|
1704
|
-
'<button class="session-manage-btn" data-action="clear-selection" type="button">清空</button>' +
|
|
1767
|
+
'<button class="session-manage-btn" data-action="' + selectAllAction + '" type="button"' + selectAllDisabled + '>' + selectAllLabel + '</button>' +
|
|
1768
|
+
'<button class="session-manage-btn" data-action="clear-selection" type="button"' + (hasAny ? '' : ' disabled') + '>清空</button>' +
|
|
1705
1769
|
'<button class="session-manage-btn danger" data-action="delete-selected" type="button"' + (hasAny ? '' : ' disabled') + '>删除所选</button>' +
|
|
1706
1770
|
'<button class="session-manage-btn" data-action="toggle-manage-mode" type="button">完成</button>' +
|
|
1707
1771
|
'</div>' +
|
|
@@ -1715,6 +1779,20 @@
|
|
|
1715
1779
|
'</section>';
|
|
1716
1780
|
}
|
|
1717
1781
|
|
|
1782
|
+
function renderArchivedGroup(archivedSessions) {
|
|
1783
|
+
var expanded = !!state.archivedExpanded;
|
|
1784
|
+
var chevron = expanded ? "▾" : "▸";
|
|
1785
|
+
var header = '<div class="session-group-title claude-history-toggle" data-action="toggle-archived-group">' +
|
|
1786
|
+
'<span class="chevron">' + chevron + '</span> 已归档 ' +
|
|
1787
|
+
'<span class="history-count">' + archivedSessions.length + '</span>' +
|
|
1788
|
+
'</div>';
|
|
1789
|
+
if (!expanded) {
|
|
1790
|
+
return '<section class="session-group">' + header + '</section>';
|
|
1791
|
+
}
|
|
1792
|
+
var items = archivedSessions.map(function(session) { return renderSessionItem(session, "sessions"); }).join("");
|
|
1793
|
+
return '<section class="session-group">' + header + items + '</section>';
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1718
1796
|
function renderRecentGroup(activeSessions, recentHistorySessions) {
|
|
1719
1797
|
var html = '<section class="session-group">' +
|
|
1720
1798
|
'<div class="session-group-title">最近</div>';
|
|
@@ -1806,11 +1884,19 @@
|
|
|
1806
1884
|
updateSessionsList();
|
|
1807
1885
|
}
|
|
1808
1886
|
|
|
1887
|
+
function getSelectableSessions() {
|
|
1888
|
+
return state.sessions.filter(function(session) {
|
|
1889
|
+
return session.archived || !session.resumedToSessionId;
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function countSelectableItems() {
|
|
1894
|
+
return getSelectableSessions().length + getVisibleClaudeHistorySessions().length;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1809
1897
|
function selectAllVisibleItems() {
|
|
1810
1898
|
var nextSessionIds = {};
|
|
1811
|
-
|
|
1812
|
-
return !session.resumedToSessionId;
|
|
1813
|
-
}).forEach(function(session) {
|
|
1899
|
+
getSelectableSessions().forEach(function(session) {
|
|
1814
1900
|
nextSessionIds[session.id] = true;
|
|
1815
1901
|
});
|
|
1816
1902
|
var nextHistoryIds = {};
|
|
@@ -2078,19 +2164,18 @@
|
|
|
2078
2164
|
} catch (e) {}
|
|
2079
2165
|
applyTerminalScale();
|
|
2080
2166
|
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
2167
|
}
|
|
2086
2168
|
|
|
2087
2169
|
function applyTerminalScale() {
|
|
2088
|
-
if (!state.terminal) return;
|
|
2089
|
-
|
|
2090
|
-
state.
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2170
|
+
if (!state.terminal || !state.terminal.element) return;
|
|
2171
|
+
var newSize = (state.terminalBaseFontSize * state.terminalScale) + "px";
|
|
2172
|
+
var newRowHeight = (state.terminalBaseFontSize * state.terminalScale * 1.5) + "px";
|
|
2173
|
+
state.terminal.element.style.setProperty("--term-font-size", newSize);
|
|
2174
|
+
state.terminal.element.style.setProperty("--term-row-height", newRowHeight);
|
|
2175
|
+
if (typeof state.terminal.remeasure === "function") {
|
|
2176
|
+
requestAnimationFrame(function() {
|
|
2177
|
+
if (state.terminal) state.terminal.remeasure();
|
|
2178
|
+
});
|
|
2094
2179
|
}
|
|
2095
2180
|
}
|
|
2096
2181
|
|
|
@@ -2450,15 +2535,8 @@
|
|
|
2450
2535
|
// Ordered lists
|
|
2451
2536
|
escaped = escaped.replace(/^\d+\.\s+(.*)$/gm, '<li>$1</li>');
|
|
2452
2537
|
|
|
2453
|
-
// Tables
|
|
2454
|
-
escaped = escaped
|
|
2455
|
-
var cells = match.split("|").slice(1, -1);
|
|
2456
|
-
if (cells.every(function(c) { return /^[\-:]+$/.test(c.trim()); })) {
|
|
2457
|
-
return "";
|
|
2458
|
-
}
|
|
2459
|
-
return '<tr>' + cells.map(function(c) { return '<td>' + c.trim() + '</td>'; }).join("") + '</tr>';
|
|
2460
|
-
});
|
|
2461
|
-
escaped = escaped.replace(/(<tr>.*<\/tr>\n?)+/g, '<table>$&</table>');
|
|
2538
|
+
// Tables (GFM)
|
|
2539
|
+
escaped = parseMarkdownTables(escaped);
|
|
2462
2540
|
|
|
2463
2541
|
// Paragraphs
|
|
2464
2542
|
var paragraphs = escaped.split(/\n{2,}/);
|
|
@@ -3217,6 +3295,17 @@
|
|
|
3217
3295
|
if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
|
|
3218
3296
|
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
3219
3297
|
if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
|
|
3298
|
+
var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
|
|
3299
|
+
var sidebarOverflow = document.getElementById("sidebar-overflow-menu");
|
|
3300
|
+
if (sidebarMoreBtn && sidebarOverflow) {
|
|
3301
|
+
sidebarMoreBtn.addEventListener("click", function(e) {
|
|
3302
|
+
e.stopPropagation();
|
|
3303
|
+
sidebarOverflow.classList.toggle("open");
|
|
3304
|
+
});
|
|
3305
|
+
document.addEventListener("click", function() {
|
|
3306
|
+
sidebarOverflow.classList.remove("open");
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3220
3309
|
var homeBtn = document.getElementById("sidebar-home-btn");
|
|
3221
3310
|
if (homeBtn) homeBtn.addEventListener("click", function() {
|
|
3222
3311
|
state.selectedId = null;
|
|
@@ -3245,11 +3334,15 @@
|
|
|
3245
3334
|
var settingsTabs = document.querySelectorAll(".settings-tab");
|
|
3246
3335
|
for (var ti = 0; ti < settingsTabs.length; ti++) {
|
|
3247
3336
|
settingsTabs[ti].addEventListener("click", function(e) {
|
|
3248
|
-
|
|
3337
|
+
var btn = e.currentTarget || this;
|
|
3338
|
+
var tabName = btn && btn.getAttribute ? btn.getAttribute("data-tab") : null;
|
|
3339
|
+
if (tabName) switchSettingsTab(tabName);
|
|
3249
3340
|
});
|
|
3250
3341
|
}
|
|
3251
3342
|
var saveConfigBtn = document.getElementById("save-config-button");
|
|
3252
3343
|
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
3344
|
+
var defaultModelRefreshBtn = document.getElementById("cfg-default-model-refresh");
|
|
3345
|
+
if (defaultModelRefreshBtn) defaultModelRefreshBtn.addEventListener("click", refreshAvailableModels);
|
|
3253
3346
|
var saveDisplayBtn = document.getElementById("save-display-button");
|
|
3254
3347
|
if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
|
|
3255
3348
|
// App icon picker (APK only)
|
|
@@ -3282,6 +3375,14 @@
|
|
|
3282
3375
|
if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
|
|
3283
3376
|
var doRestartBtn = document.getElementById("do-restart-button");
|
|
3284
3377
|
if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
|
|
3378
|
+
var autoUpdateWebToggle = document.getElementById("auto-update-web-toggle");
|
|
3379
|
+
if (autoUpdateWebToggle) autoUpdateWebToggle.addEventListener("change", function() {
|
|
3380
|
+
toggleAutoUpdate("web", autoUpdateWebToggle.checked);
|
|
3381
|
+
});
|
|
3382
|
+
var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
|
|
3383
|
+
if (autoUpdateApkToggle) autoUpdateApkToggle.addEventListener("change", function() {
|
|
3384
|
+
toggleAutoUpdate("apk", autoUpdateApkToggle.checked);
|
|
3385
|
+
});
|
|
3285
3386
|
var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
|
|
3286
3387
|
if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
|
|
3287
3388
|
var text = document.getElementById("android-connect-code");
|
|
@@ -3414,6 +3515,10 @@
|
|
|
3414
3515
|
state.chatMode = this.value;
|
|
3415
3516
|
showToast("新会话模式已切换为:" + getModeLabel(this.value), "info");
|
|
3416
3517
|
});
|
|
3518
|
+
var modelSelect = document.getElementById("chat-model-select");
|
|
3519
|
+
if (modelSelect) modelSelect.addEventListener("change", function() {
|
|
3520
|
+
onChatModelChange(this.value);
|
|
3521
|
+
});
|
|
3417
3522
|
|
|
3418
3523
|
var sessionModal = document.getElementById("session-modal");
|
|
3419
3524
|
if (sessionModal) sessionModal.addEventListener("click", function(e) {
|
|
@@ -3441,6 +3546,42 @@
|
|
|
3441
3546
|
inputBox.addEventListener("blur", handleInputBoxBlur);
|
|
3442
3547
|
}
|
|
3443
3548
|
|
|
3549
|
+
// Attach button & drag-drop
|
|
3550
|
+
var attachBtn = document.getElementById("attach-btn");
|
|
3551
|
+
var fileInput = document.getElementById("file-upload-input");
|
|
3552
|
+
if (attachBtn && fileInput) {
|
|
3553
|
+
attachBtn.addEventListener("click", function() { fileInput.click(); });
|
|
3554
|
+
fileInput.addEventListener("change", function() {
|
|
3555
|
+
var files = fileInput.files;
|
|
3556
|
+
if (files) {
|
|
3557
|
+
for (var i = 0; i < files.length; i++) addPendingAttachment(files[i]);
|
|
3558
|
+
}
|
|
3559
|
+
fileInput.value = "";
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
var composer = document.querySelector(".input-composer");
|
|
3563
|
+
if (composer) {
|
|
3564
|
+
composer.addEventListener("dragover", function(e) {
|
|
3565
|
+
e.preventDefault();
|
|
3566
|
+
e.stopPropagation();
|
|
3567
|
+
composer.classList.add("drag-over");
|
|
3568
|
+
});
|
|
3569
|
+
composer.addEventListener("dragleave", function(e) {
|
|
3570
|
+
e.preventDefault();
|
|
3571
|
+
e.stopPropagation();
|
|
3572
|
+
composer.classList.remove("drag-over");
|
|
3573
|
+
});
|
|
3574
|
+
composer.addEventListener("drop", function(e) {
|
|
3575
|
+
e.preventDefault();
|
|
3576
|
+
e.stopPropagation();
|
|
3577
|
+
composer.classList.remove("drag-over");
|
|
3578
|
+
var files = e.dataTransfer && e.dataTransfer.files;
|
|
3579
|
+
if (files) {
|
|
3580
|
+
for (var i = 0; i < files.length; i++) addPendingAttachment(files[i]);
|
|
3581
|
+
}
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3444
3585
|
// Terminal interactive toggle (both topbar and terminal-header)
|
|
3445
3586
|
var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
|
|
3446
3587
|
terminalInteractiveToggles.forEach(function(id) {
|
|
@@ -3515,7 +3656,16 @@
|
|
|
3515
3656
|
if (scaleDownBtn) scaleDownBtn.addEventListener("click", function() { adjustTerminalScale(-0.25); });
|
|
3516
3657
|
if (scaleUpBtn) scaleUpBtn.addEventListener("click", function() { adjustTerminalScale(0.25); });
|
|
3517
3658
|
var pageRefreshBtn = document.getElementById("page-refresh-btn");
|
|
3518
|
-
if (pageRefreshBtn) pageRefreshBtn.addEventListener("click", function() {
|
|
3659
|
+
if (pageRefreshBtn) pageRefreshBtn.addEventListener("click", function(ev) {
|
|
3660
|
+
// Soft refresh: replay terminal buffer + rebuild chat view.
|
|
3661
|
+
// Fixes residual DOM from CSI cursor-jump sequences without losing page state.
|
|
3662
|
+
// Hold Shift to force a full page reload as an escape hatch.
|
|
3663
|
+
if (ev && ev.shiftKey) {
|
|
3664
|
+
location.reload();
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
softRefreshCurrentView();
|
|
3668
|
+
});
|
|
3519
3669
|
var jumpBottomBtn = document.getElementById("terminal-jump-bottom");
|
|
3520
3670
|
if (jumpBottomBtn) jumpBottomBtn.addEventListener("click", function() {
|
|
3521
3671
|
maybeScrollTerminalToBottom("force");
|
|
@@ -4005,6 +4155,9 @@
|
|
|
4005
4155
|
}
|
|
4006
4156
|
} else if (actionButton.dataset.action === "clear-all-history") {
|
|
4007
4157
|
clearAllClaudeHistory();
|
|
4158
|
+
} else if (actionButton.dataset.action === "toggle-archived-group") {
|
|
4159
|
+
state.archivedExpanded = !state.archivedExpanded;
|
|
4160
|
+
updateSessionsList();
|
|
4008
4161
|
} else if (actionButton.dataset.action === "resume" && actionButton.dataset.sessionId) {
|
|
4009
4162
|
handleResumeAction(actionButton);
|
|
4010
4163
|
} else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
|
|
@@ -4107,10 +4260,7 @@
|
|
|
4107
4260
|
|
|
4108
4261
|
function getTerminalViewport() {
|
|
4109
4262
|
if (!state.terminal || !state.terminal.element) return null;
|
|
4110
|
-
|
|
4111
|
-
return state.terminalViewportEl;
|
|
4112
|
-
}
|
|
4113
|
-
state.terminalViewportEl = state.terminal.element.querySelector(".xterm-viewport");
|
|
4263
|
+
state.terminalViewportEl = state.terminal.element;
|
|
4114
4264
|
return state.terminalViewportEl;
|
|
4115
4265
|
}
|
|
4116
4266
|
|
|
@@ -4136,11 +4286,6 @@
|
|
|
4136
4286
|
}
|
|
4137
4287
|
|
|
4138
4288
|
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
4289
|
var viewport = getTerminalViewport();
|
|
4145
4290
|
if (!viewport) return true;
|
|
4146
4291
|
var distance = viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop;
|
|
@@ -4149,25 +4294,13 @@
|
|
|
4149
4294
|
|
|
4150
4295
|
function scrollTerminalToBottom(smooth) {
|
|
4151
4296
|
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
|
-
}
|
|
4297
|
+
var viewport = getTerminalViewport();
|
|
4298
|
+
if (!viewport) return;
|
|
4160
4299
|
if (smooth) {
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
setTimeout(function() {
|
|
4165
|
-
if (state.terminal) state.terminal.scrollToBottom();
|
|
4166
|
-
}, 160);
|
|
4167
|
-
return;
|
|
4168
|
-
}
|
|
4300
|
+
viewport.scrollTo({ top: viewport.scrollHeight, behavior: "smooth" });
|
|
4301
|
+
} else {
|
|
4302
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
4169
4303
|
}
|
|
4170
|
-
state.terminal.scrollToBottom();
|
|
4171
4304
|
}
|
|
4172
4305
|
|
|
4173
4306
|
function scheduleTerminalResumeFollow() {
|
|
@@ -4388,6 +4521,43 @@
|
|
|
4388
4521
|
requestSyncScrollbar();
|
|
4389
4522
|
}
|
|
4390
4523
|
|
|
4524
|
+
function resetTerminal() {
|
|
4525
|
+
if (!state.terminal || typeof state.terminal.reset !== "function") return;
|
|
4526
|
+
state.terminal.reset();
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
// Soft resync terminal: reset WASM grid and replay full output buffer.
|
|
4530
|
+
// Clears any stale DOM rows left over from CSI cursor-jump sequences
|
|
4531
|
+
// (e.g. Claude permission menus redrawing in place while user holds arrow keys).
|
|
4532
|
+
function softResyncTerminal() {
|
|
4533
|
+
if (!state.terminal || !state.terminalOutput) return false;
|
|
4534
|
+
resetTerminal();
|
|
4535
|
+
state.terminal.write(state.terminalOutput);
|
|
4536
|
+
state.lastTerminalResyncAt = Date.now();
|
|
4537
|
+
maybeScrollTerminalToBottom("output");
|
|
4538
|
+
return true;
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
// Soft refresh the whole current view without losing page state:
|
|
4542
|
+
// - Replays terminal buffer to clear residue
|
|
4543
|
+
// - Clears chat render cache and forces a full rebuild
|
|
4544
|
+
// Used by the refresh button and by automatic triggers
|
|
4545
|
+
// (e.g. permission escalation appearing/disappearing).
|
|
4546
|
+
function softRefreshCurrentView() {
|
|
4547
|
+
softResyncTerminal();
|
|
4548
|
+
if (typeof resetChatRenderCache === "function") resetChatRenderCache();
|
|
4549
|
+
if (typeof scheduleChatRender === "function") scheduleChatRender(true);
|
|
4550
|
+
else if (typeof render === "function") render();
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
function scheduleSoftResyncTerminal(delayMs) {
|
|
4554
|
+
if (state.softResyncTimer) clearTimeout(state.softResyncTimer);
|
|
4555
|
+
state.softResyncTimer = setTimeout(function() {
|
|
4556
|
+
state.softResyncTimer = null;
|
|
4557
|
+
softResyncTerminal();
|
|
4558
|
+
}, typeof delayMs === "number" ? delayMs : 150);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4391
4561
|
function syncTerminalBuffer(sessionId, output, options) {
|
|
4392
4562
|
if (!state.terminal) return false;
|
|
4393
4563
|
var normalizedOutput = normalizeTerminalOutput(output || "");
|
|
@@ -4407,7 +4577,7 @@
|
|
|
4407
4577
|
}
|
|
4408
4578
|
|
|
4409
4579
|
if (sessionChanged) {
|
|
4410
|
-
|
|
4580
|
+
resetTerminal();
|
|
4411
4581
|
currentOutput = "";
|
|
4412
4582
|
state.terminalOutput = "";
|
|
4413
4583
|
state.terminalAutoFollow = true;
|
|
@@ -4417,18 +4587,15 @@
|
|
|
4417
4587
|
|
|
4418
4588
|
if (mode === "replace") {
|
|
4419
4589
|
if (normalizedOutput !== currentOutput) {
|
|
4420
|
-
|
|
4590
|
+
resetTerminal();
|
|
4421
4591
|
if (normalizedOutput) {
|
|
4422
4592
|
state.terminal.write(normalizedOutput);
|
|
4423
4593
|
}
|
|
4424
4594
|
wrote = true;
|
|
4425
4595
|
}
|
|
4426
4596
|
} else if (normalizedOutput.length < currentOutput.length && !sessionChanged) {
|
|
4427
|
-
// Ignore regressive snapshots for the active session; wait for an explicit replace.
|
|
4428
4597
|
return false;
|
|
4429
4598
|
} 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
4599
|
return false;
|
|
4433
4600
|
} else if (normalizedOutput.startsWith(currentOutput)) {
|
|
4434
4601
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
@@ -4437,10 +4604,9 @@
|
|
|
4437
4604
|
wrote = true;
|
|
4438
4605
|
}
|
|
4439
4606
|
} else if (currentOutput && currentOutput.startsWith(normalizedOutput)) {
|
|
4440
|
-
// Ignore shorter/stale snapshots from polling or reconnect races.
|
|
4441
4607
|
return false;
|
|
4442
4608
|
} else {
|
|
4443
|
-
|
|
4609
|
+
resetTerminal();
|
|
4444
4610
|
if (normalizedOutput) {
|
|
4445
4611
|
state.terminal.write(normalizedOutput);
|
|
4446
4612
|
}
|
|
@@ -4449,40 +4615,21 @@
|
|
|
4449
4615
|
|
|
4450
4616
|
state.terminalSessionId = nextSessionId;
|
|
4451
4617
|
state.terminalOutput = normalizedOutput;
|
|
4452
|
-
scheduleMobileDomUpdate();
|
|
4453
4618
|
if (shouldScroll && (wrote || sessionChanged || mode === "replace")) {
|
|
4454
4619
|
maybeScrollTerminalToBottom(sessionChanged || mode === "replace" ? "force" : "output");
|
|
4455
4620
|
} else {
|
|
4456
4621
|
updateTerminalJumpToBottomButton();
|
|
4457
4622
|
}
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
if (sessionChanged && state.fitAddon) {
|
|
4461
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
4462
|
-
scheduleTerminalResize(true);
|
|
4623
|
+
if (sessionChanged) {
|
|
4624
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
4463
4625
|
}
|
|
4464
4626
|
return wrote || sessionChanged;
|
|
4465
4627
|
}
|
|
4466
4628
|
|
|
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
4629
|
function initTerminal() {
|
|
4482
4630
|
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
|
|
4631
|
+
if (!container || state.terminal || state.terminalInitializing) return;
|
|
4632
|
+
if (typeof WTermLib === "undefined" || !WTermLib.WTerm) {
|
|
4486
4633
|
if (!state.terminalInitRetries) state.terminalInitRetries = 0;
|
|
4487
4634
|
if (state.terminalInitRetries < 10) {
|
|
4488
4635
|
state.terminalInitRetries++;
|
|
@@ -4491,160 +4638,80 @@
|
|
|
4491
4638
|
return;
|
|
4492
4639
|
}
|
|
4493
4640
|
state.terminalInitRetries = 0;
|
|
4641
|
+
state.terminalInitializing = true;
|
|
4642
|
+
|
|
4643
|
+
var termWrap = document.createElement("div");
|
|
4644
|
+
termWrap.className = "terminal-scroll-wrap";
|
|
4645
|
+
container.appendChild(termWrap);
|
|
4494
4646
|
|
|
4495
|
-
|
|
4647
|
+
var term = new WTermLib.WTerm(termWrap, {
|
|
4496
4648
|
cols: 120,
|
|
4497
4649
|
rows: 36,
|
|
4498
|
-
|
|
4499
|
-
disableStdin: false,
|
|
4650
|
+
autoResize: true,
|
|
4500
4651
|
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"
|
|
4652
|
+
onData: function(data) {
|
|
4653
|
+
if (state.terminalInteractive) return;
|
|
4654
|
+
queueDirectInput(data);
|
|
4655
|
+
},
|
|
4656
|
+
onResize: function(cols, rows) {
|
|
4657
|
+
sendTerminalResize(cols, rows);
|
|
4528
4658
|
}
|
|
4529
4659
|
});
|
|
4530
4660
|
|
|
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
|
-
}
|
|
4661
|
+
term.init().then(function() {
|
|
4662
|
+
state.terminal = term;
|
|
4663
|
+
state.terminalInitializing = false;
|
|
4664
|
+
applyTerminalScale();
|
|
4665
|
+
state.terminalAutoFollow = true;
|
|
4666
|
+
clearTerminalScrollIdleTimer();
|
|
4567
4667
|
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
});
|
|
4668
|
+
var viewport = getTerminalViewport();
|
|
4669
|
+
if (viewport) {
|
|
4670
|
+
state.terminalViewportScrollHandler = function() {
|
|
4671
|
+
if (isTerminalNearBottom()) {
|
|
4672
|
+
state.terminalAutoFollow = true;
|
|
4673
|
+
clearTerminalScrollIdleTimer();
|
|
4674
|
+
updateTerminalJumpToBottomButton();
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
setTerminalManualScrollActive();
|
|
4678
|
+
};
|
|
4679
|
+
state.terminalViewportTouchHandler = function() {
|
|
4680
|
+
setTerminalManualScrollActive();
|
|
4681
|
+
};
|
|
4682
|
+
viewport.addEventListener("scroll", state.terminalViewportScrollHandler, { passive: true });
|
|
4683
|
+
viewport.addEventListener("touchmove", state.terminalViewportTouchHandler, { passive: true });
|
|
4585
4684
|
}
|
|
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
4685
|
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
if (isTerminalNearBottom()) {
|
|
4600
|
-
state.terminalAutoFollow = true;
|
|
4601
|
-
clearTerminalScrollIdleTimer();
|
|
4602
|
-
updateTerminalJumpToBottomButton();
|
|
4603
|
-
return;
|
|
4686
|
+
state.terminalWheelHandler = function(e) {
|
|
4687
|
+
if (!isTerminalNearBottom() || e.deltaY < 0) {
|
|
4688
|
+
setTerminalManualScrollActive();
|
|
4604
4689
|
}
|
|
4605
|
-
|
|
4606
|
-
};
|
|
4607
|
-
state.terminalViewportTouchHandler = function() {
|
|
4608
|
-
setTerminalManualScrollActive();
|
|
4690
|
+
e.stopPropagation();
|
|
4609
4691
|
};
|
|
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 });
|
|
4692
|
+
container.addEventListener("wheel", state.terminalWheelHandler, { passive: true });
|
|
4620
4693
|
|
|
4621
|
-
|
|
4622
|
-
initTerminalScrollbar(container);
|
|
4694
|
+
initTerminalScrollbar(container);
|
|
4623
4695
|
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4696
|
+
if (state.selectedId) {
|
|
4697
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
4698
|
+
if (session) {
|
|
4699
|
+
syncTerminalBuffer(session.id, session.output || "", { mode: "append", scroll: false });
|
|
4700
|
+
}
|
|
4701
|
+
} else {
|
|
4702
|
+
term.write("点击上方「新对话」开始你的第一次对话。\r\n");
|
|
4631
4703
|
}
|
|
4632
|
-
} else {
|
|
4633
|
-
state.terminal.writeln("点击上方「新对话」开始你的第一次对话。");
|
|
4634
|
-
}
|
|
4635
4704
|
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4705
|
+
state.terminalClickHandler = focusInputBox;
|
|
4706
|
+
container.addEventListener("click", state.terminalClickHandler);
|
|
4707
|
+
updateTerminalJumpToBottomButton();
|
|
4708
|
+
initTerminalResizeHandle();
|
|
4709
|
+
observeTerminalResize();
|
|
4710
|
+
startTerminalHealthCheck();
|
|
4711
|
+
}).catch(function(err) {
|
|
4712
|
+
state.terminalInitializing = false;
|
|
4713
|
+
console.error("[wand] wterm init failed:", err);
|
|
4639
4714
|
});
|
|
4640
|
-
|
|
4641
|
-
container.addEventListener("click", focusInputBox);
|
|
4642
|
-
updateTerminalJumpToBottomButton();
|
|
4643
|
-
|
|
4644
|
-
// 初始化拖动调整大小
|
|
4645
|
-
initTerminalResizeHandle();
|
|
4646
|
-
|
|
4647
|
-
observeTerminalResize();
|
|
4648
4715
|
}
|
|
4649
4716
|
|
|
4650
4717
|
function login() {
|
|
@@ -4869,13 +4936,138 @@
|
|
|
4869
4936
|
if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
|
|
4870
4937
|
}
|
|
4871
4938
|
|
|
4939
|
+
function getEffectiveModel(session) {
|
|
4940
|
+
if (session && session.selectedModel) return session.selectedModel;
|
|
4941
|
+
if (state.chatModel) return state.chatModel;
|
|
4942
|
+
if (state.config && state.config.defaultModel) return state.config.defaultModel;
|
|
4943
|
+
return "";
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
function renderChatModelOptions(selected) {
|
|
4947
|
+
var models = state.availableModels || [];
|
|
4948
|
+
var html = '<option value="">默认(跟随设置)</option>';
|
|
4949
|
+
for (var i = 0; i < models.length; i++) {
|
|
4950
|
+
var m = models[i];
|
|
4951
|
+
var label = m.label || m.id;
|
|
4952
|
+
html += '<option value="' + escapeHtml(m.id) + '"' + (m.id === selected ? " selected" : "") + '>' + escapeHtml(label) + '</option>';
|
|
4953
|
+
}
|
|
4954
|
+
// If selected is unknown (custom value), prepend it as a sticky option
|
|
4955
|
+
if (selected && !models.some(function(m) { return m.id === selected; })) {
|
|
4956
|
+
html += '<option value="' + escapeHtml(selected) + '" selected>' + escapeHtml(selected) + '(自定义)</option>';
|
|
4957
|
+
}
|
|
4958
|
+
return html;
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
function syncComposerModelSelect(session) {
|
|
4962
|
+
var select = document.getElementById("chat-model-select");
|
|
4963
|
+
if (!select) return;
|
|
4964
|
+
var effective = getEffectiveModel(session);
|
|
4965
|
+
select.innerHTML = renderChatModelOptions(effective);
|
|
4966
|
+
select.value = effective;
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
function fetchAvailableModels() {
|
|
4970
|
+
return fetch("/api/models", { credentials: "same-origin" })
|
|
4971
|
+
.then(function(res) { return res.json(); })
|
|
4972
|
+
.then(function(data) {
|
|
4973
|
+
if (data && Array.isArray(data.models)) {
|
|
4974
|
+
state.availableModels = data.models;
|
|
4975
|
+
syncComposerModelSelect(getSelectedSession());
|
|
4976
|
+
updateSettingsDefaultModelSelect(data);
|
|
4977
|
+
}
|
|
4978
|
+
return data;
|
|
4979
|
+
})
|
|
4980
|
+
.catch(function() { return null; });
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
function refreshAvailableModels() {
|
|
4984
|
+
if (state.modelsRefreshing) return Promise.resolve(null);
|
|
4985
|
+
state.modelsRefreshing = true;
|
|
4986
|
+
var btn = document.getElementById("cfg-default-model-refresh");
|
|
4987
|
+
if (btn) { btn.disabled = true; btn.textContent = "刷新中..."; }
|
|
4988
|
+
return fetch("/api/models/refresh", { method: "POST", credentials: "same-origin" })
|
|
4989
|
+
.then(function(res) { return res.json(); })
|
|
4990
|
+
.then(function(data) {
|
|
4991
|
+
if (data && Array.isArray(data.models)) {
|
|
4992
|
+
state.availableModels = data.models;
|
|
4993
|
+
syncComposerModelSelect(getSelectedSession());
|
|
4994
|
+
updateSettingsDefaultModelSelect(data);
|
|
4995
|
+
if (typeof showToast === "function") {
|
|
4996
|
+
showToast("模型列表已刷新" + (data.claudeVersion ? "(claude " + data.claudeVersion + ")" : ""), "success");
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
return data;
|
|
5000
|
+
})
|
|
5001
|
+
.catch(function() {
|
|
5002
|
+
if (typeof showToast === "function") showToast("刷新模型列表失败", "error");
|
|
5003
|
+
return null;
|
|
5004
|
+
})
|
|
5005
|
+
.finally(function() {
|
|
5006
|
+
state.modelsRefreshing = false;
|
|
5007
|
+
if (btn) { btn.disabled = false; btn.textContent = "刷新"; }
|
|
5008
|
+
});
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
function updateSettingsDefaultModelSelect(data) {
|
|
5012
|
+
var select = document.getElementById("cfg-default-model");
|
|
5013
|
+
if (!select) return;
|
|
5014
|
+
var previous = select.value;
|
|
5015
|
+
var current = previous || state.configDefaultModel || (state.config && state.config.defaultModel) || "";
|
|
5016
|
+
select.innerHTML = renderChatModelOptions(current);
|
|
5017
|
+
select.value = current;
|
|
5018
|
+
var versionEl = document.getElementById("cfg-default-model-version");
|
|
5019
|
+
if (versionEl && data) {
|
|
5020
|
+
versionEl.textContent = data.claudeVersion ? "已检测到 claude " + data.claudeVersion : "新建会话时默认使用该模型。";
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
function getSelectedSession() {
|
|
5025
|
+
if (!state.selectedId) return null;
|
|
5026
|
+
for (var i = 0; i < state.sessions.length; i++) {
|
|
5027
|
+
if (state.sessions[i].id === state.selectedId) return state.sessions[i];
|
|
5028
|
+
}
|
|
5029
|
+
return null;
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
function onChatModelChange(value) {
|
|
5033
|
+
var normalized = (value || "").trim();
|
|
5034
|
+
state.chatModel = normalized;
|
|
5035
|
+
try { localStorage.setItem("wand-chat-model", normalized); } catch (e) {}
|
|
5036
|
+
var session = getSelectedSession();
|
|
5037
|
+
if (!session) return;
|
|
5038
|
+
if (session.provider && session.provider !== "claude") return;
|
|
5039
|
+
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/model", {
|
|
5040
|
+
method: "POST",
|
|
5041
|
+
headers: { "Content-Type": "application/json" },
|
|
5042
|
+
credentials: "same-origin",
|
|
5043
|
+
body: JSON.stringify({ model: normalized || null })
|
|
5044
|
+
})
|
|
5045
|
+
.then(function(res) { return res.json(); })
|
|
5046
|
+
.then(function(data) {
|
|
5047
|
+
if (data && data.error) {
|
|
5048
|
+
showToast(data.error, "error");
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
5051
|
+
if (data && data.id) {
|
|
5052
|
+
updateSessionSnapshot(data);
|
|
5053
|
+
if (typeof showToast === "function") {
|
|
5054
|
+
var display = normalized || "默认";
|
|
5055
|
+
showToast("已切换模型 → " + display, "success");
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
})
|
|
5059
|
+
.catch(function() { showToast("切换模型失败", "error"); });
|
|
5060
|
+
}
|
|
5061
|
+
|
|
4872
5062
|
function createStructuredSession(prompt, cwdOverride, modeOverride, worktreeEnabled) {
|
|
5063
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
4873
5064
|
var payload = {
|
|
4874
5065
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
4875
5066
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
4876
5067
|
runner: state.structuredRunner || "claude-cli-print",
|
|
4877
5068
|
prompt: prompt || undefined,
|
|
4878
|
-
worktreeEnabled: worktreeEnabled === true
|
|
5069
|
+
worktreeEnabled: worktreeEnabled === true,
|
|
5070
|
+
model: modelPref || undefined
|
|
4879
5071
|
};
|
|
4880
5072
|
console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
|
|
4881
5073
|
return fetch("/api/structured-sessions", {
|
|
@@ -5004,6 +5196,17 @@
|
|
|
5004
5196
|
if (normalizedSnapshot.id === state.selectedId) {
|
|
5005
5197
|
reconcileInteractiveState();
|
|
5006
5198
|
updateTaskDisplay();
|
|
5199
|
+
// Escalation/permission toggles are the common trigger for CSI cursor-jump
|
|
5200
|
+
// redraw sequences from Claude CLI. When they appear or dismiss, schedule a
|
|
5201
|
+
// debounced terminal resync so residual DOM rows get cleaned up automatically
|
|
5202
|
+
// — same fix the user used to have to reach for via the refresh button.
|
|
5203
|
+
var prevEsc = prevSession && prevSession.pendingEscalation ? 1 : 0;
|
|
5204
|
+
var nextEsc = updatedSession && updatedSession.pendingEscalation ? 1 : 0;
|
|
5205
|
+
var prevBlocked = prevSession && prevSession.permissionBlocked ? 1 : 0;
|
|
5206
|
+
var nextBlocked = updatedSession && updatedSession.permissionBlocked ? 1 : 0;
|
|
5207
|
+
if (prevEsc !== nextEsc || prevBlocked !== nextBlocked) {
|
|
5208
|
+
scheduleSoftResyncTerminal(200);
|
|
5209
|
+
}
|
|
5007
5210
|
}
|
|
5008
5211
|
// When a session transitions to a non-running state, try flushing cross-session queue
|
|
5009
5212
|
if (normalizedSnapshot.status && normalizedSnapshot.status !== "running" && state.crossSessionQueue.length > 0) {
|
|
@@ -5254,10 +5457,8 @@
|
|
|
5254
5457
|
initTerminal();
|
|
5255
5458
|
}
|
|
5256
5459
|
if (state.terminal && terminalContainer && !terminalContainer.contains(state.terminal.element)) {
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
5260
|
-
scheduleTerminalResize();
|
|
5460
|
+
teardownTerminal();
|
|
5461
|
+
initTerminal();
|
|
5261
5462
|
}
|
|
5262
5463
|
|
|
5263
5464
|
if (selectedSession && state.terminal) {
|
|
@@ -5286,6 +5487,7 @@
|
|
|
5286
5487
|
if (inputPanel) inputPanel.classList.add("hidden");
|
|
5287
5488
|
}
|
|
5288
5489
|
syncComposerModeSelect();
|
|
5490
|
+
syncComposerModelSelect(getSelectedSession());
|
|
5289
5491
|
applyCurrentView();
|
|
5290
5492
|
reconcileInteractiveState();
|
|
5291
5493
|
}
|
|
@@ -5316,6 +5518,15 @@
|
|
|
5316
5518
|
updateSessionSnapshot(data);
|
|
5317
5519
|
updateShellChrome();
|
|
5318
5520
|
|
|
5521
|
+
if (state.terminal && id === state.selectedId && data.output !== undefined) {
|
|
5522
|
+
syncTerminalBuffer(id, data.output, { mode: "append" });
|
|
5523
|
+
if (state.terminal.remeasure) {
|
|
5524
|
+
requestAnimationFrame(function() {
|
|
5525
|
+
if (state.terminal) state.terminal.remeasure();
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5319
5530
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
5320
5531
|
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, data.output, false));
|
|
5321
5532
|
|
|
@@ -5944,6 +6155,13 @@
|
|
|
5944
6155
|
}
|
|
5945
6156
|
}
|
|
5946
6157
|
|
|
6158
|
+
// Auto-update toggles
|
|
6159
|
+
var autoUpdate = data.autoUpdate || {};
|
|
6160
|
+
var autoUpdateWebToggle = document.getElementById("auto-update-web-toggle");
|
|
6161
|
+
if (autoUpdateWebToggle) autoUpdateWebToggle.checked = !!autoUpdate.web;
|
|
6162
|
+
var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
|
|
6163
|
+
if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!autoUpdate.apk;
|
|
6164
|
+
|
|
5947
6165
|
// ── Android APK version display ──
|
|
5948
6166
|
var apkSection = document.getElementById("android-apk-section");
|
|
5949
6167
|
var apkCurrentRow = document.getElementById("android-apk-current-row");
|
|
@@ -6005,6 +6223,11 @@
|
|
|
6005
6223
|
apkMessageEl.textContent = "暂无可用更新";
|
|
6006
6224
|
apkMessageEl.classList.remove("hidden");
|
|
6007
6225
|
}
|
|
6226
|
+
// Show APK auto-update toggle in APK mode
|
|
6227
|
+
var apkAutoRow = document.getElementById("android-auto-update-row");
|
|
6228
|
+
var apkAutoHint = document.getElementById("android-auto-update-hint");
|
|
6229
|
+
if (apkAutoRow) apkAutoRow.classList.remove("hidden");
|
|
6230
|
+
if (apkAutoHint) apkAutoHint.classList.remove("hidden");
|
|
6008
6231
|
} else {
|
|
6009
6232
|
// ── 浏览器模式:显示线上版本 + 本地版本 + 下载按钮 ──
|
|
6010
6233
|
if (androidApk.github && apkGithubRow && apkGithubEl) {
|
|
@@ -6067,6 +6290,13 @@
|
|
|
6067
6290
|
var langEl = document.getElementById("cfg-language");
|
|
6068
6291
|
if (langEl) langEl.value = cfg.language || "";
|
|
6069
6292
|
|
|
6293
|
+
// Default model
|
|
6294
|
+
state.configDefaultModel = cfg.defaultModel || "";
|
|
6295
|
+
updateSettingsDefaultModelSelect();
|
|
6296
|
+
fetchAvailableModels().then(function() {
|
|
6297
|
+
updateSettingsDefaultModelSelect();
|
|
6298
|
+
}).catch(function() {});
|
|
6299
|
+
|
|
6070
6300
|
// Cert status
|
|
6071
6301
|
var certStatus = document.getElementById("cert-status");
|
|
6072
6302
|
if (certStatus) {
|
|
@@ -6117,8 +6347,12 @@
|
|
|
6117
6347
|
defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
|
|
6118
6348
|
shell: (document.getElementById("cfg-shell") || {}).value,
|
|
6119
6349
|
language: (document.getElementById("cfg-language") || {}).value || "",
|
|
6350
|
+
defaultModel: (document.getElementById("cfg-default-model") || {}).value || "",
|
|
6120
6351
|
};
|
|
6121
6352
|
|
|
6353
|
+
var previousDefaultModel = (state.config && state.config.defaultModel) || "";
|
|
6354
|
+
var nextDefaultModel = body.defaultModel || "";
|
|
6355
|
+
|
|
6122
6356
|
fetch("/api/settings/config", {
|
|
6123
6357
|
method: "POST",
|
|
6124
6358
|
headers: { "Content-Type": "application/json" },
|
|
@@ -6137,6 +6371,15 @@
|
|
|
6137
6371
|
}
|
|
6138
6372
|
msgEl.classList.remove("hidden");
|
|
6139
6373
|
}
|
|
6374
|
+
if (!data || !data.error) {
|
|
6375
|
+
if (state.config) state.config.defaultModel = nextDefaultModel;
|
|
6376
|
+
state.configDefaultModel = nextDefaultModel;
|
|
6377
|
+
if (nextDefaultModel !== previousDefaultModel) {
|
|
6378
|
+
state.chatModel = "";
|
|
6379
|
+
try { localStorage.removeItem("wand-chat-model"); } catch (e) {}
|
|
6380
|
+
syncComposerModelSelect(getSelectedSession());
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6140
6383
|
})
|
|
6141
6384
|
.catch(function() {
|
|
6142
6385
|
if (msgEl) {
|
|
@@ -6330,6 +6573,48 @@
|
|
|
6330
6573
|
performRestart(restartBtn, msgEl);
|
|
6331
6574
|
}
|
|
6332
6575
|
|
|
6576
|
+
function checkApkAutoUpdate() {
|
|
6577
|
+
fetch("/api/auto-update", { credentials: "same-origin" })
|
|
6578
|
+
.then(function(res) { return res.json(); })
|
|
6579
|
+
.then(function(autoData) {
|
|
6580
|
+
if (!autoData.apk) return;
|
|
6581
|
+
// Auto-update is enabled, check for APK update
|
|
6582
|
+
return fetch("/api/android-apk-update?currentVersion=" + encodeURIComponent(_apkVersion), { credentials: "same-origin" })
|
|
6583
|
+
.then(function(res) { return res.json(); })
|
|
6584
|
+
.then(function(data) {
|
|
6585
|
+
if (!data.updateAvailable || !data.downloadUrl) return;
|
|
6586
|
+
try {
|
|
6587
|
+
WandNative.downloadUpdate(data.downloadUrl, data.fileName || "wand-update.apk", data.source || "local");
|
|
6588
|
+
} catch (_e) {}
|
|
6589
|
+
});
|
|
6590
|
+
})
|
|
6591
|
+
.catch(function() {});
|
|
6592
|
+
}
|
|
6593
|
+
|
|
6594
|
+
function toggleAutoUpdate(type, enabled) {
|
|
6595
|
+
var body = {};
|
|
6596
|
+
body[type] = enabled;
|
|
6597
|
+
fetch("/api/auto-update", {
|
|
6598
|
+
method: "POST",
|
|
6599
|
+
headers: { "Content-Type": "application/json" },
|
|
6600
|
+
credentials: "same-origin",
|
|
6601
|
+
body: JSON.stringify(body),
|
|
6602
|
+
})
|
|
6603
|
+
.then(function(res) { return res.json(); })
|
|
6604
|
+
.then(function(data) {
|
|
6605
|
+
// Sync toggle state with server response
|
|
6606
|
+
var webToggle = document.getElementById("auto-update-web-toggle");
|
|
6607
|
+
var apkToggle = document.getElementById("auto-update-apk-toggle");
|
|
6608
|
+
if (webToggle) webToggle.checked = !!data.web;
|
|
6609
|
+
if (apkToggle) apkToggle.checked = !!data.apk;
|
|
6610
|
+
})
|
|
6611
|
+
.catch(function() {
|
|
6612
|
+
// Revert toggle on failure
|
|
6613
|
+
var toggle = document.getElementById("auto-update-" + type + "-toggle");
|
|
6614
|
+
if (toggle) toggle.checked = !enabled;
|
|
6615
|
+
});
|
|
6616
|
+
}
|
|
6617
|
+
|
|
6333
6618
|
// ── Notification Settings Helpers ──
|
|
6334
6619
|
|
|
6335
6620
|
function _updateAppIconSelection(activeIcon) {
|
|
@@ -6639,6 +6924,7 @@
|
|
|
6639
6924
|
state.sessionTool = "claude";
|
|
6640
6925
|
state.preferredCommand = "claude";
|
|
6641
6926
|
syncComposerModeSelect();
|
|
6927
|
+
syncComposerModelSelect(getSelectedSession());
|
|
6642
6928
|
return createStructuredSession(undefined, cwd, mode, worktreeEnabled)
|
|
6643
6929
|
.then(function(data) {
|
|
6644
6930
|
saveWorkingDir(cwd);
|
|
@@ -6989,6 +7275,107 @@
|
|
|
6989
7275
|
}
|
|
6990
7276
|
}
|
|
6991
7277
|
|
|
7278
|
+
// ── Attachment helpers ──
|
|
7279
|
+
|
|
7280
|
+
var ATTACH_MAX_SIZE = 10 * 1024 * 1024;
|
|
7281
|
+
|
|
7282
|
+
function formatFileSize(bytes) {
|
|
7283
|
+
if (bytes < 1024) return bytes + " B";
|
|
7284
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
7285
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
function isImageType(type) {
|
|
7289
|
+
return /^image\/(png|jpe?g|gif|webp|bmp|svg\+xml)/.test(type);
|
|
7290
|
+
}
|
|
7291
|
+
|
|
7292
|
+
function addPendingAttachment(file) {
|
|
7293
|
+
if (!file) return;
|
|
7294
|
+
if (file.size > ATTACH_MAX_SIZE) {
|
|
7295
|
+
showToast("文件过大(上限 10 MB): " + file.name, "error");
|
|
7296
|
+
return;
|
|
7297
|
+
}
|
|
7298
|
+
var entry = { file: file, name: file.name, size: file.size, previewUrl: null };
|
|
7299
|
+
if (isImageType(file.type)) {
|
|
7300
|
+
entry.previewUrl = URL.createObjectURL(file);
|
|
7301
|
+
}
|
|
7302
|
+
state.pendingAttachments.push(entry);
|
|
7303
|
+
renderAttachmentPreview();
|
|
7304
|
+
}
|
|
7305
|
+
|
|
7306
|
+
function removePendingAttachment(index) {
|
|
7307
|
+
var removed = state.pendingAttachments.splice(index, 1);
|
|
7308
|
+
if (removed.length && removed[0].previewUrl) {
|
|
7309
|
+
URL.revokeObjectURL(removed[0].previewUrl);
|
|
7310
|
+
}
|
|
7311
|
+
renderAttachmentPreview();
|
|
7312
|
+
}
|
|
7313
|
+
|
|
7314
|
+
function clearAttachments() {
|
|
7315
|
+
state.pendingAttachments.forEach(function(a) {
|
|
7316
|
+
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
|
7317
|
+
});
|
|
7318
|
+
state.pendingAttachments = [];
|
|
7319
|
+
renderAttachmentPreview();
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
function renderAttachmentPreview() {
|
|
7323
|
+
var bar = document.getElementById("attachment-preview");
|
|
7324
|
+
if (!bar) return;
|
|
7325
|
+
var items = state.pendingAttachments;
|
|
7326
|
+
if (items.length === 0) {
|
|
7327
|
+
bar.classList.add("hidden");
|
|
7328
|
+
bar.innerHTML = "";
|
|
7329
|
+
return;
|
|
7330
|
+
}
|
|
7331
|
+
bar.classList.remove("hidden");
|
|
7332
|
+
var html = "";
|
|
7333
|
+
for (var i = 0; i < items.length; i++) {
|
|
7334
|
+
var a = items[i];
|
|
7335
|
+
var thumb = a.previewUrl
|
|
7336
|
+
? '<img src="' + escapeHtml(a.previewUrl) + '" alt="">'
|
|
7337
|
+
: '<span class="att-icon">📄</span>';
|
|
7338
|
+
html += '<span class="attachment-pill" data-index="' + i + '">' +
|
|
7339
|
+
thumb +
|
|
7340
|
+
'<span class="att-name" title="' + escapeHtml(a.name) + '">' + escapeHtml(a.name) + '</span>' +
|
|
7341
|
+
'<span class="att-size">' + formatFileSize(a.size) + '</span>' +
|
|
7342
|
+
'<button class="att-remove" data-index="' + i + '" title="移除">×</button>' +
|
|
7343
|
+
'</span>';
|
|
7344
|
+
}
|
|
7345
|
+
bar.innerHTML = html;
|
|
7346
|
+
bar.querySelectorAll(".att-remove").forEach(function(btn) {
|
|
7347
|
+
btn.addEventListener("click", function(e) {
|
|
7348
|
+
e.preventDefault();
|
|
7349
|
+
e.stopPropagation();
|
|
7350
|
+
removePendingAttachment(parseInt(btn.getAttribute("data-index"), 10));
|
|
7351
|
+
});
|
|
7352
|
+
});
|
|
7353
|
+
}
|
|
7354
|
+
|
|
7355
|
+
function uploadAttachments(sessionId) {
|
|
7356
|
+
if (!state.pendingAttachments.length) return Promise.resolve([]);
|
|
7357
|
+
var formData = new FormData();
|
|
7358
|
+
state.pendingAttachments.forEach(function(a) {
|
|
7359
|
+
formData.append("files", a.file, a.name);
|
|
7360
|
+
});
|
|
7361
|
+
return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/upload", {
|
|
7362
|
+
method: "POST",
|
|
7363
|
+
body: formData,
|
|
7364
|
+
credentials: "same-origin"
|
|
7365
|
+
}).then(function(resp) {
|
|
7366
|
+
if (!resp.ok) return resp.json().then(function(e) { throw new Error(e.error || "上传失败"); });
|
|
7367
|
+
return resp.json();
|
|
7368
|
+
}).then(function(data) {
|
|
7369
|
+
return data.files || [];
|
|
7370
|
+
});
|
|
7371
|
+
}
|
|
7372
|
+
|
|
7373
|
+
function buildAttachmentPrefix(uploadedFiles) {
|
|
7374
|
+
if (!uploadedFiles || !uploadedFiles.length) return "";
|
|
7375
|
+
var paths = uploadedFiles.map(function(f) { return f.savedPath; });
|
|
7376
|
+
return "[附件已上传,请查看以下文件:\n" + paths.join("\n") + "]\n\n";
|
|
7377
|
+
}
|
|
7378
|
+
|
|
6992
7379
|
function handleInteractiveTextInput(inputBox) {
|
|
6993
7380
|
if (!state.terminalInteractive || !inputBox) return false;
|
|
6994
7381
|
var value = inputBox.value || "";
|
|
@@ -7001,6 +7388,17 @@
|
|
|
7001
7388
|
}
|
|
7002
7389
|
|
|
7003
7390
|
function handleInputPaste(event) {
|
|
7391
|
+
var items = event.clipboardData && event.clipboardData.items;
|
|
7392
|
+
if (items && !state.terminalInteractive) {
|
|
7393
|
+
for (var i = 0; i < items.length; i++) {
|
|
7394
|
+
if (items[i].type.indexOf("image/") === 0) {
|
|
7395
|
+
event.preventDefault();
|
|
7396
|
+
var file = items[i].getAsFile();
|
|
7397
|
+
if (file) addPendingAttachment(file);
|
|
7398
|
+
return;
|
|
7399
|
+
}
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7004
7402
|
var pasted = event.clipboardData && event.clipboardData.getData("text");
|
|
7005
7403
|
if (!pasted) return;
|
|
7006
7404
|
event.preventDefault();
|
|
@@ -7483,12 +7881,9 @@
|
|
|
7483
7881
|
|
|
7484
7882
|
if (!structured) {
|
|
7485
7883
|
if (!state.terminal) initTerminal();
|
|
7486
|
-
if (state.terminal && state.fitAddon) {
|
|
7487
|
-
ensureTerminalFit();
|
|
7488
|
-
}
|
|
7489
7884
|
}
|
|
7490
7885
|
applyCurrentView();
|
|
7491
|
-
focusInputBox();
|
|
7886
|
+
focusInputBox(true);
|
|
7492
7887
|
}
|
|
7493
7888
|
|
|
7494
7889
|
|
|
@@ -7501,7 +7896,9 @@
|
|
|
7501
7896
|
var inputBox = document.getElementById("input-box");
|
|
7502
7897
|
var value = inputBox ? inputBox.value : "";
|
|
7503
7898
|
var selectedSession = getSelectedSession();
|
|
7504
|
-
|
|
7899
|
+
var hasAttachments = state.pendingAttachments.length > 0;
|
|
7900
|
+
|
|
7901
|
+
if (value || hasAttachments) {
|
|
7505
7902
|
console.log("[WAND] sendInputFromBox", {
|
|
7506
7903
|
sessionId: state.selectedId,
|
|
7507
7904
|
sessionStatus: selectedSession ? selectedSession.status : null,
|
|
@@ -7511,50 +7908,62 @@
|
|
|
7511
7908
|
view: state.currentView,
|
|
7512
7909
|
wsConnected: state.wsConnected,
|
|
7513
7910
|
terminalInteractive: state.terminalInteractive,
|
|
7514
|
-
inputLength: value.length
|
|
7911
|
+
inputLength: value.length,
|
|
7912
|
+
attachments: state.pendingAttachments.length
|
|
7515
7913
|
});
|
|
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
7914
|
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7915
|
+
var attachUpload = hasAttachments && state.selectedId
|
|
7916
|
+
? uploadAttachments(state.selectedId)
|
|
7917
|
+
: Promise.resolve([]);
|
|
7523
7918
|
|
|
7524
|
-
|
|
7525
|
-
|
|
7919
|
+
return attachUpload.then(function(uploadedFiles) {
|
|
7920
|
+
var prefix = buildAttachmentPrefix(uploadedFiles);
|
|
7921
|
+
var finalValue = prefix + (value || (uploadedFiles.length ? "请查看附件。" : ""));
|
|
7922
|
+
if (uploadedFiles.length) clearAttachments();
|
|
7526
7923
|
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
if (inputBox) {
|
|
7531
|
-
inputBox.value = "";
|
|
7532
|
-
autoResizeInput(inputBox);
|
|
7533
|
-
}
|
|
7534
|
-
setDraftValue("");
|
|
7535
|
-
return Promise.resolve();
|
|
7536
|
-
}
|
|
7924
|
+
// Clear todo progress bar at the start of a new user turn
|
|
7925
|
+
var todoEl = document.getElementById("todo-progress");
|
|
7926
|
+
if (todoEl) todoEl.classList.add("hidden");
|
|
7537
7927
|
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
if (!readySession) {
|
|
7541
|
-
showToast("会话未就绪,将稍后重试。", "info");
|
|
7542
|
-
return null;
|
|
7928
|
+
if (isStructuredSession(selectedSession)) {
|
|
7929
|
+
return postStructuredInput(finalValue, inputBox, selectedSession);
|
|
7543
7930
|
}
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
if (inputBox
|
|
7931
|
+
|
|
7932
|
+
var submitChunks = getTerminalSubmitChunks(selectedSession, finalValue);
|
|
7933
|
+
var isOffline = !state.wsConnected;
|
|
7934
|
+
|
|
7935
|
+
if (isOffline) {
|
|
7936
|
+
queueOfflineTerminalChunks(submitChunks);
|
|
7937
|
+
if (inputBox) {
|
|
7551
7938
|
inputBox.value = "";
|
|
7552
7939
|
autoResizeInput(inputBox);
|
|
7553
7940
|
}
|
|
7554
7941
|
setDraftValue("");
|
|
7942
|
+
return Promise.resolve();
|
|
7943
|
+
}
|
|
7944
|
+
|
|
7945
|
+
return ensureSessionReadyForInput(selectedSession).then(function(readySession) {
|
|
7946
|
+
if (!readySession) {
|
|
7947
|
+
showToast("会话未就绪,将稍后重试。", "info");
|
|
7948
|
+
return null;
|
|
7949
|
+
}
|
|
7950
|
+
var submitView = state.currentView;
|
|
7951
|
+
if (readySession && readySession.provider === "codex" && state.selectedId !== readySession.id) {
|
|
7952
|
+
throw new Error("Codex session changed before input send.");
|
|
7953
|
+
}
|
|
7954
|
+
return sendTerminalChunks(submitChunks, "enter_text", 30, submitView).then(function() {
|
|
7955
|
+
if (inputBox && inputBox.value === value) {
|
|
7956
|
+
inputBox.value = "";
|
|
7957
|
+
autoResizeInput(inputBox);
|
|
7958
|
+
}
|
|
7959
|
+
setDraftValue("");
|
|
7960
|
+
});
|
|
7961
|
+
}).catch(function(err) {
|
|
7962
|
+
showToast(getInputErrorMessage(err), "error");
|
|
7963
|
+
throw err;
|
|
7555
7964
|
});
|
|
7556
7965
|
}).catch(function(err) {
|
|
7557
|
-
showToast(
|
|
7966
|
+
showToast("附件上传失败: " + (err.message || err), "error");
|
|
7558
7967
|
throw err;
|
|
7559
7968
|
});
|
|
7560
7969
|
}
|
|
@@ -7805,10 +8214,9 @@
|
|
|
7805
8214
|
}
|
|
7806
8215
|
|
|
7807
8216
|
function getTerminalSubmitChunks(session, text) {
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
return [text + String.fromCharCode(13)];
|
|
8217
|
+
// 文本与回车分两个 chunk 发,避免 CLI 的 bracketed paste 检测把末尾
|
|
8218
|
+
// \r 并入粘贴内容导致只换行不提交。
|
|
8219
|
+
return [text, String.fromCharCode(13)];
|
|
7812
8220
|
}
|
|
7813
8221
|
|
|
7814
8222
|
function sendTerminalChunks(chunks, shortcutKey, delayMs, viewOverride) {
|
|
@@ -7848,7 +8256,6 @@
|
|
|
7848
8256
|
return postInput(input, shortcutKey, viewOverride).finally(function() {
|
|
7849
8257
|
var idx = state.messageQueue.indexOf(input);
|
|
7850
8258
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
7851
|
-
scheduleMobileDomUpdate();
|
|
7852
8259
|
});
|
|
7853
8260
|
});
|
|
7854
8261
|
return state.inputQueue;
|
|
@@ -8491,6 +8898,7 @@
|
|
|
8491
8898
|
state.preferredCommand = command;
|
|
8492
8899
|
state.chatMode = getSafeModeForTool(command, state.chatMode);
|
|
8493
8900
|
}
|
|
8901
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8494
8902
|
return fetch("/api/commands", {
|
|
8495
8903
|
method: "POST",
|
|
8496
8904
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8498,7 +8906,8 @@
|
|
|
8498
8906
|
body: JSON.stringify({
|
|
8499
8907
|
command: command,
|
|
8500
8908
|
cwd: cwd || "",
|
|
8501
|
-
mode: state.chatMode || state.config.defaultMode || "default"
|
|
8909
|
+
mode: state.chatMode || state.config.defaultMode || "default",
|
|
8910
|
+
model: modelPref || undefined
|
|
8502
8911
|
})
|
|
8503
8912
|
})
|
|
8504
8913
|
.then(function(res) { return res.json(); })
|
|
@@ -8618,6 +9027,7 @@
|
|
|
8618
9027
|
var mode = state.chatMode || "managed";
|
|
8619
9028
|
var defaultCwd = getEffectiveCwd();
|
|
8620
9029
|
var preferredTool = getPreferredTool();
|
|
9030
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8621
9031
|
fetch("/api/commands", {
|
|
8622
9032
|
method: "POST",
|
|
8623
9033
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8626,7 +9036,8 @@
|
|
|
8626
9036
|
command: preferredTool,
|
|
8627
9037
|
cwd: defaultCwd,
|
|
8628
9038
|
mode: mode,
|
|
8629
|
-
initialInput: value
|
|
9039
|
+
initialInput: value,
|
|
9040
|
+
model: modelPref || undefined
|
|
8630
9041
|
})
|
|
8631
9042
|
})
|
|
8632
9043
|
.then(function(res) { return res.json(); })
|
|
@@ -8654,6 +9065,7 @@
|
|
|
8654
9065
|
var mode = state.chatMode || "managed";
|
|
8655
9066
|
var defaultCwd = getEffectiveCwd();
|
|
8656
9067
|
var preferredTool = getPreferredTool();
|
|
9068
|
+
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8657
9069
|
fetch("/api/commands", {
|
|
8658
9070
|
method: "POST",
|
|
8659
9071
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8662,7 +9074,8 @@
|
|
|
8662
9074
|
command: preferredTool,
|
|
8663
9075
|
cwd: defaultCwd,
|
|
8664
9076
|
mode: mode,
|
|
8665
|
-
initialInput: value || undefined
|
|
9077
|
+
initialInput: value || undefined,
|
|
9078
|
+
model: modelPref || undefined
|
|
8666
9079
|
})
|
|
8667
9080
|
})
|
|
8668
9081
|
.then(function(res) { return res.json(); })
|
|
@@ -8818,7 +9231,7 @@
|
|
|
8818
9231
|
setTimeout(function() {
|
|
8819
9232
|
window.scrollTo(0, 0);
|
|
8820
9233
|
// On mobile, force terminal refit + scroll after keyboard dismissal.
|
|
8821
|
-
// The container height restores but
|
|
9234
|
+
// The container height restores but terminal needs time to
|
|
8822
9235
|
// fill the expanded space, and the scroll position needs resetting.
|
|
8823
9236
|
if (isTouchDevice()) {
|
|
8824
9237
|
ensureTerminalFit();
|
|
@@ -9487,9 +9900,8 @@
|
|
|
9487
9900
|
if (!output) return;
|
|
9488
9901
|
output.setAttribute("tabindex", "0");
|
|
9489
9902
|
output.focus();
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
terminalTextarea.focus();
|
|
9903
|
+
if (state.terminal && state.terminal.focus) {
|
|
9904
|
+
state.terminal.focus();
|
|
9493
9905
|
}
|
|
9494
9906
|
}
|
|
9495
9907
|
|
|
@@ -9655,11 +10067,6 @@
|
|
|
9655
10067
|
function observeTerminalResize() {
|
|
9656
10068
|
var output = document.getElementById("output");
|
|
9657
10069
|
if (!output) return;
|
|
9658
|
-
|
|
9659
|
-
if (typeof ResizeObserver === "function") {
|
|
9660
|
-
state.resizeObserver = new ResizeObserver(function() { scheduleTerminalResize(true); });
|
|
9661
|
-
state.resizeObserver.observe(output);
|
|
9662
|
-
}
|
|
9663
10070
|
var lastKnownDesktop = !isMobileLayout();
|
|
9664
10071
|
state.resizeHandler = function() {
|
|
9665
10072
|
scheduleTerminalResize(true);
|
|
@@ -9685,7 +10092,36 @@
|
|
|
9685
10092
|
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
9686
10093
|
}
|
|
9687
10094
|
|
|
10095
|
+
function startTerminalHealthCheck() {
|
|
10096
|
+
if (state.terminalHealthTimer) return;
|
|
10097
|
+
state.terminalHealthTimer = setInterval(function() {
|
|
10098
|
+
if (!state.terminal || state.currentView !== "terminal" || document.hidden) return;
|
|
10099
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
10100
|
+
if (!selectedSession || selectedSession.sessionKind === "structured") return;
|
|
10101
|
+
// Lightweight remeasure every 5s
|
|
10102
|
+
if (state.terminal.remeasure) state.terminal.remeasure();
|
|
10103
|
+
// Full re-sync every 30s during output pauses
|
|
10104
|
+
var now = Date.now();
|
|
10105
|
+
var chunkPause = state.lastChunkAt > 0 && (now - state.lastChunkAt > 300);
|
|
10106
|
+
var resyncDue = (now - state.lastTerminalResyncAt) > 30000;
|
|
10107
|
+
if (resyncDue && (chunkPause || selectedSession.status !== "running") && state.terminalOutput) {
|
|
10108
|
+
state.lastTerminalResyncAt = now;
|
|
10109
|
+
state.terminal.reset();
|
|
10110
|
+
state.terminal.write(state.terminalOutput);
|
|
10111
|
+
maybeScrollTerminalToBottom("output");
|
|
10112
|
+
}
|
|
10113
|
+
}, 5000);
|
|
10114
|
+
}
|
|
10115
|
+
|
|
10116
|
+
function stopTerminalHealthCheck() {
|
|
10117
|
+
if (state.terminalHealthTimer) {
|
|
10118
|
+
clearInterval(state.terminalHealthTimer);
|
|
10119
|
+
state.terminalHealthTimer = null;
|
|
10120
|
+
}
|
|
10121
|
+
}
|
|
10122
|
+
|
|
9688
10123
|
function teardownTerminal() {
|
|
10124
|
+
stopTerminalHealthCheck();
|
|
9689
10125
|
if (state.resizeTimer) {
|
|
9690
10126
|
clearTimeout(state.resizeTimer);
|
|
9691
10127
|
state.resizeTimer = null;
|
|
@@ -9711,6 +10147,7 @@
|
|
|
9711
10147
|
}
|
|
9712
10148
|
});
|
|
9713
10149
|
clearTerminalScrollIdleTimer();
|
|
10150
|
+
var output = document.getElementById("output");
|
|
9714
10151
|
if (state.terminalViewportEl) {
|
|
9715
10152
|
if (state.terminalViewportScrollHandler) {
|
|
9716
10153
|
state.terminalViewportEl.removeEventListener("scroll", state.terminalViewportScrollHandler);
|
|
@@ -9719,27 +10156,30 @@
|
|
|
9719
10156
|
state.terminalViewportEl.removeEventListener("touchmove", state.terminalViewportTouchHandler);
|
|
9720
10157
|
}
|
|
9721
10158
|
}
|
|
10159
|
+
if (output) {
|
|
10160
|
+
if (state.terminalWheelHandler) {
|
|
10161
|
+
output.removeEventListener("wheel", state.terminalWheelHandler);
|
|
10162
|
+
}
|
|
10163
|
+
if (state.terminalClickHandler) {
|
|
10164
|
+
output.removeEventListener("click", state.terminalClickHandler);
|
|
10165
|
+
}
|
|
10166
|
+
}
|
|
9722
10167
|
state.terminalViewportEl = null;
|
|
9723
10168
|
state.terminalViewportScrollHandler = null;
|
|
9724
10169
|
state.terminalViewportTouchHandler = null;
|
|
10170
|
+
state.terminalWheelHandler = null;
|
|
10171
|
+
state.terminalClickHandler = null;
|
|
10172
|
+
if (state.terminalScrollbarEl && state.terminalScrollbarEl.parentNode) {
|
|
10173
|
+
state.terminalScrollbarEl.parentNode.removeChild(state.terminalScrollbarEl);
|
|
10174
|
+
}
|
|
10175
|
+
state.terminalScrollbarEl = null;
|
|
10176
|
+
state.terminalScrollbarThumbEl = null;
|
|
9725
10177
|
if (state.terminal) {
|
|
9726
|
-
state.terminal.
|
|
10178
|
+
state.terminal.destroy();
|
|
9727
10179
|
state.terminal = null;
|
|
9728
10180
|
}
|
|
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
10181
|
state.terminalSessionId = null;
|
|
9741
10182
|
state.terminalOutput = "";
|
|
9742
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
9743
10183
|
state.terminalAutoFollow = true;
|
|
9744
10184
|
state.showTerminalJumpToBottom = false;
|
|
9745
10185
|
updateTerminalJumpToBottomButton();
|
|
@@ -9762,39 +10202,9 @@
|
|
|
9762
10202
|
}
|
|
9763
10203
|
|
|
9764
10204
|
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);
|
|
10205
|
+
if (!state.terminal) return;
|
|
10206
|
+
maybeScrollTerminalToBottom("resize");
|
|
10207
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
9798
10208
|
}
|
|
9799
10209
|
|
|
9800
10210
|
function scheduleTerminalResize(immediate) {
|
|
@@ -9810,16 +10220,11 @@
|
|
|
9810
10220
|
}
|
|
9811
10221
|
|
|
9812
10222
|
function syncTerminalSize() {
|
|
9813
|
-
|
|
9814
|
-
if (!state.terminal || !state.fitAddon || !output) return;
|
|
9815
|
-
if (!shouldResizeTerminalViewport()) return;
|
|
9816
|
-
|
|
10223
|
+
if (!state.terminal) return;
|
|
9817
10224
|
var shouldFollow = state.terminalAutoFollow || isTerminalNearBottom();
|
|
9818
|
-
state.fitAddon.fit();
|
|
9819
10225
|
if (shouldFollow) {
|
|
9820
10226
|
maybeScrollTerminalToBottom("resize");
|
|
9821
10227
|
}
|
|
9822
|
-
|
|
9823
10228
|
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
9824
10229
|
}
|
|
9825
10230
|
|
|
@@ -9865,9 +10270,8 @@
|
|
|
9865
10270
|
flushPendingMessages();
|
|
9866
10271
|
// Re-fit terminal on reconnect — the viewport may have changed
|
|
9867
10272
|
// while disconnected, and the PTY needs up-to-date dimensions.
|
|
9868
|
-
if (state.terminal
|
|
9869
|
-
state.
|
|
9870
|
-
ensureTerminalFit();
|
|
10273
|
+
if (state.terminal) {
|
|
10274
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
9871
10275
|
}
|
|
9872
10276
|
};
|
|
9873
10277
|
|
|
@@ -9947,8 +10351,22 @@
|
|
|
9947
10351
|
snapshot.messages = msg.data.messages;
|
|
9948
10352
|
}
|
|
9949
10353
|
|
|
9950
|
-
//
|
|
9951
|
-
|
|
10354
|
+
// Fast path: chunk-only incremental events skip expensive chat update
|
|
10355
|
+
var isChunkOnly = isIncremental && msg.data.chunk
|
|
10356
|
+
&& !msg.data.lastMessage && !snapshot.messages
|
|
10357
|
+
&& snapshot.output === undefined
|
|
10358
|
+
&& !msg.data.structuredState && !msg.data.sessionKind;
|
|
10359
|
+
|
|
10360
|
+
if (isChunkOnly) {
|
|
10361
|
+
// Only update permissionBlocked if it actually changed
|
|
10362
|
+
if (msg.data.permissionBlocked !== undefined) {
|
|
10363
|
+
var existingPB = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
10364
|
+
if (existingPB && !!existingPB.permissionBlocked !== !!msg.data.permissionBlocked) {
|
|
10365
|
+
updateSessionSnapshot(snapshot);
|
|
10366
|
+
if (msg.sessionId === state.selectedId) updateTaskDisplay();
|
|
10367
|
+
}
|
|
10368
|
+
}
|
|
10369
|
+
} else if (snapshot.output !== undefined || snapshot.messages || isIncremental || msg.data.permissionBlocked !== undefined) {
|
|
9952
10370
|
updateSessionSnapshot(snapshot);
|
|
9953
10371
|
if (msg.sessionId === state.selectedId) {
|
|
9954
10372
|
var updatedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; }) || snapshot;
|
|
@@ -9966,21 +10384,20 @@
|
|
|
9966
10384
|
// Real-time terminal output
|
|
9967
10385
|
if (msg.sessionId === state.selectedId && state.terminal && msg.data) {
|
|
9968
10386
|
if (msg.data.chunk && (!state.terminalSessionId || state.terminalSessionId === msg.sessionId)) {
|
|
9969
|
-
// Fast path: write chunk directly to avoid full-output comparison
|
|
9970
|
-
|
|
10387
|
+
// Fast path: write chunk directly to avoid full-output comparison.
|
|
10388
|
+
state.lastChunkAt = Date.now();
|
|
9971
10389
|
state.terminalLiveStreamSessions[msg.sessionId] = true;
|
|
9972
10390
|
state.terminal.write(msg.data.chunk);
|
|
9973
10391
|
state.terminalSessionId = msg.sessionId;
|
|
9974
10392
|
if (msg.data.output) {
|
|
9975
10393
|
state.terminalOutput = normalizeTerminalOutput(msg.data.output);
|
|
9976
10394
|
} else {
|
|
9977
|
-
state.terminalOutput =
|
|
10395
|
+
state.terminalOutput = (state.terminalOutput || "") + normalizeTerminalOutput(msg.data.chunk);
|
|
9978
10396
|
}
|
|
9979
10397
|
maybeScrollTerminalToBottom("output");
|
|
9980
10398
|
updateTerminalJumpToBottomButton();
|
|
9981
|
-
scheduleMobileDomUpdate();
|
|
9982
10399
|
} else if (!msg.data.incremental && Object.prototype.hasOwnProperty.call(msg.data, "output")) {
|
|
9983
|
-
// Fallback: no chunk available, use full-output comparison
|
|
10400
|
+
// Fallback: no chunk available, use full-output comparison.
|
|
9984
10401
|
syncTerminalBuffer(msg.sessionId, msg.data.output || "", { mode: "append" });
|
|
9985
10402
|
}
|
|
9986
10403
|
}
|
|
@@ -10030,6 +10447,7 @@
|
|
|
10030
10447
|
endedNotifBody = endedSession ? (endedSession.command || msg.sessionId) : msg.sessionId;
|
|
10031
10448
|
}
|
|
10032
10449
|
notifyTaskEnded(msg.sessionId, endedNotifTitle, endedNotifBody);
|
|
10450
|
+
clearSessionProgressNative(msg.sessionId);
|
|
10033
10451
|
if (msg.sessionId !== state.selectedId || document.hidden) {
|
|
10034
10452
|
showNotificationBubble({
|
|
10035
10453
|
title: endedNotifTitle,
|
|
@@ -10104,6 +10522,11 @@
|
|
|
10104
10522
|
updateTerminalOutput(msg.data.output || "", msg.sessionId, "append");
|
|
10105
10523
|
// Ensure terminal is properly fitted after receiving initial data
|
|
10106
10524
|
scheduleTerminalResize(true);
|
|
10525
|
+
if (state.terminal && state.terminal.remeasure) {
|
|
10526
|
+
requestAnimationFrame(function() {
|
|
10527
|
+
if (state.terminal) state.terminal.remeasure();
|
|
10528
|
+
});
|
|
10529
|
+
}
|
|
10107
10530
|
}
|
|
10108
10531
|
break;
|
|
10109
10532
|
case 'usage':
|
|
@@ -10116,6 +10539,7 @@
|
|
|
10116
10539
|
updateTaskDisplay();
|
|
10117
10540
|
}
|
|
10118
10541
|
notifyTaskProgress(msg.sessionId, msg.data || null);
|
|
10542
|
+
syncSessionProgressToNative(msg.sessionId);
|
|
10119
10543
|
// Update session list to reflect current activity (debounced)
|
|
10120
10544
|
scheduleSessionListUpdate();
|
|
10121
10545
|
break;
|
|
@@ -10185,6 +10609,7 @@
|
|
|
10185
10609
|
statusUpdate.approvalStats = msg.data.approvalStats;
|
|
10186
10610
|
}
|
|
10187
10611
|
updateSessionSnapshot(statusUpdate);
|
|
10612
|
+
syncSessionProgressToNative(msg.sessionId);
|
|
10188
10613
|
if (msg.sessionId === state.selectedId) {
|
|
10189
10614
|
updateTaskDisplay();
|
|
10190
10615
|
if (msg.data.approvalStats) {
|
|
@@ -10207,6 +10632,10 @@
|
|
|
10207
10632
|
if (msg.data) {
|
|
10208
10633
|
if (msg.data.kind === "update") {
|
|
10209
10634
|
notifyUpdateAvailable(msg.data.current || "-", msg.data.latest || "-");
|
|
10635
|
+
} else if (msg.data.kind === "auto-update-start") {
|
|
10636
|
+
showAutoUpdateOverlay(msg.data.current || "-", msg.data.latest || "-");
|
|
10637
|
+
} else if (msg.data.kind === "auto-update-restart") {
|
|
10638
|
+
showRestartOverlay();
|
|
10210
10639
|
} else if (msg.data.kind === "restart") {
|
|
10211
10640
|
showRestartOverlay();
|
|
10212
10641
|
}
|
|
@@ -10440,8 +10869,12 @@
|
|
|
10440
10869
|
}
|
|
10441
10870
|
updateTerminalJumpToBottomButton();
|
|
10442
10871
|
if (state.currentView === "terminal") {
|
|
10443
|
-
state.terminalViewportSize = { width: 0, height: 0 };
|
|
10444
10872
|
scheduleTerminalResize(true);
|
|
10873
|
+
if (state.terminal && state.terminal.remeasure) {
|
|
10874
|
+
requestAnimationFrame(function() {
|
|
10875
|
+
if (state.terminal) state.terminal.remeasure();
|
|
10876
|
+
});
|
|
10877
|
+
}
|
|
10445
10878
|
}
|
|
10446
10879
|
}
|
|
10447
10880
|
|
|
@@ -10467,13 +10900,15 @@
|
|
|
10467
10900
|
if (chatRenderTimer) clearTimeout(chatRenderTimer);
|
|
10468
10901
|
if (immediate) {
|
|
10469
10902
|
chatRenderTimer = null;
|
|
10470
|
-
// Messages already updated in handleWebSocketMessage, just render
|
|
10471
10903
|
renderChat();
|
|
10472
10904
|
return;
|
|
10473
10905
|
}
|
|
10906
|
+
var selectedForDelay = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
10907
|
+
var isActiveStream = selectedForDelay && selectedForDelay.status === "running"
|
|
10908
|
+
&& selectedForDelay.sessionKind !== "structured";
|
|
10909
|
+
var delay = isActiveStream ? 150 : 30;
|
|
10474
10910
|
chatRenderTimer = setTimeout(function() {
|
|
10475
10911
|
chatRenderTimer = null;
|
|
10476
|
-
// Re-parse messages from the latest session output (fallback for edge cases)
|
|
10477
10912
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
10478
10913
|
if (selectedSession) {
|
|
10479
10914
|
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, selectedSession.output, true));
|
|
@@ -11016,6 +11451,10 @@
|
|
|
11016
11451
|
list.innerHTML = html;
|
|
11017
11452
|
}
|
|
11018
11453
|
|
|
11454
|
+
// Sync todo progress to native notification
|
|
11455
|
+
if (state.selectedId) {
|
|
11456
|
+
syncSessionProgressToNative(state.selectedId);
|
|
11457
|
+
}
|
|
11019
11458
|
}
|
|
11020
11459
|
|
|
11021
11460
|
|
|
@@ -11143,161 +11582,6 @@
|
|
|
11143
11582
|
})();
|
|
11144
11583
|
|
|
11145
11584
|
// ===== 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
11585
|
|
|
11302
11586
|
function isNoiseLine(line) {
|
|
11303
11587
|
if (!line) return false;
|
|
@@ -12917,6 +13201,65 @@
|
|
|
12917
13201
|
return deduped.join(newline);
|
|
12918
13202
|
}
|
|
12919
13203
|
|
|
13204
|
+
function parseMarkdownTables(source) {
|
|
13205
|
+
var NL = "\n";
|
|
13206
|
+
var lines = source.split(NL);
|
|
13207
|
+
var out = [];
|
|
13208
|
+
var i = 0;
|
|
13209
|
+
|
|
13210
|
+
function splitRow(line) {
|
|
13211
|
+
var s = line.trim();
|
|
13212
|
+
if (s.charAt(0) === "|") s = s.slice(1);
|
|
13213
|
+
if (s.charAt(s.length - 1) === "|") s = s.slice(0, -1);
|
|
13214
|
+
return s.split("|");
|
|
13215
|
+
}
|
|
13216
|
+
function styleAttr(a) { return a ? ' style="text-align:' + a + '"' : ""; }
|
|
13217
|
+
function buildTable(headers, aligns, rows) {
|
|
13218
|
+
var thead = "<thead><tr>" + headers.map(function(c, idx) {
|
|
13219
|
+
return "<th" + styleAttr(aligns[idx]) + ">" + c.trim() + "</th>";
|
|
13220
|
+
}).join("") + "</tr></thead>";
|
|
13221
|
+
var tbody = rows.length ? ("<tbody>" + rows.map(function(r) {
|
|
13222
|
+
return "<tr>" + r.map(function(c, idx) {
|
|
13223
|
+
return "<td" + styleAttr(aligns[idx]) + ">" + c.trim() + "</td>";
|
|
13224
|
+
}).join("") + "</tr>";
|
|
13225
|
+
}).join("") + "</tbody>") : "";
|
|
13226
|
+
return '<div class="md-table-wrap"><table class="md-table">' + thead + tbody + "</table></div>";
|
|
13227
|
+
}
|
|
13228
|
+
|
|
13229
|
+
while (i < lines.length) {
|
|
13230
|
+
var header = lines[i];
|
|
13231
|
+
if (header.indexOf("|") !== -1 && i + 1 < lines.length) {
|
|
13232
|
+
var sep = lines[i + 1].trim();
|
|
13233
|
+
if (/^\|?\s*:?-+:?(\s*\|\s*:?-+:?)+\s*\|?$/.test(sep)) {
|
|
13234
|
+
var headers = splitRow(header);
|
|
13235
|
+
var aligns = splitRow(sep).map(function(c) {
|
|
13236
|
+
var t = c.trim();
|
|
13237
|
+
var L = t.charAt(0) === ":";
|
|
13238
|
+
var R = t.charAt(t.length - 1) === ":";
|
|
13239
|
+
if (L && R) return "center";
|
|
13240
|
+
if (R) return "right";
|
|
13241
|
+
if (L) return "left";
|
|
13242
|
+
return "";
|
|
13243
|
+
});
|
|
13244
|
+
var rows = [];
|
|
13245
|
+
var j = i + 2;
|
|
13246
|
+
while (j < lines.length) {
|
|
13247
|
+
var trimmed = lines[j].trim();
|
|
13248
|
+
if (!trimmed || trimmed.indexOf("|") === -1) break;
|
|
13249
|
+
rows.push(splitRow(lines[j]));
|
|
13250
|
+
j += 1;
|
|
13251
|
+
}
|
|
13252
|
+
out.push("", buildTable(headers, aligns, rows), "");
|
|
13253
|
+
i = j;
|
|
13254
|
+
continue;
|
|
13255
|
+
}
|
|
13256
|
+
}
|
|
13257
|
+
out.push(header);
|
|
13258
|
+
i += 1;
|
|
13259
|
+
}
|
|
13260
|
+
return out.join(NL);
|
|
13261
|
+
}
|
|
13262
|
+
|
|
12920
13263
|
function renderMarkdown(text) {
|
|
12921
13264
|
if (!text) return "";
|
|
12922
13265
|
|
|
@@ -13041,6 +13384,7 @@
|
|
|
13041
13384
|
result = replaceLinePrefix(result, "- ", '<li>', '</li>');
|
|
13042
13385
|
result = replaceLinePrefix(result, "* ", '<li>', '</li>');
|
|
13043
13386
|
result = replaceOrderedList(result);
|
|
13387
|
+
result = parseMarkdownTables(result);
|
|
13044
13388
|
|
|
13045
13389
|
var lines = result.split(newline);
|
|
13046
13390
|
var grouped = [];
|
|
@@ -13387,7 +13731,79 @@
|
|
|
13387
13731
|
);
|
|
13388
13732
|
}
|
|
13389
13733
|
|
|
13390
|
-
|
|
13734
|
+
// ── Native Live Progress Sync ──
|
|
13735
|
+
|
|
13736
|
+
var _progressSyncTimers = {};
|
|
13737
|
+
var _PROGRESS_SYNC_DEBOUNCE_MS = 300;
|
|
13738
|
+
|
|
13739
|
+
function syncSessionProgressToNative(sessionId) {
|
|
13740
|
+
if (!_hasNativeBridge || typeof WandNative.updateSessionProgress !== "function") return;
|
|
13741
|
+
if (!sessionId) return;
|
|
13742
|
+
if (_progressSyncTimers[sessionId]) {
|
|
13743
|
+
clearTimeout(_progressSyncTimers[sessionId]);
|
|
13744
|
+
}
|
|
13745
|
+
_progressSyncTimers[sessionId] = setTimeout(function() {
|
|
13746
|
+
delete _progressSyncTimers[sessionId];
|
|
13747
|
+
_doSyncSessionProgress(sessionId);
|
|
13748
|
+
}, _PROGRESS_SYNC_DEBOUNCE_MS);
|
|
13749
|
+
}
|
|
13750
|
+
|
|
13751
|
+
function _doSyncSessionProgress(sessionId) {
|
|
13752
|
+
var session = state.sessions.find(function(s) { return s.id === sessionId; });
|
|
13753
|
+
if (!session) return;
|
|
13754
|
+
|
|
13755
|
+
var sessionLabel = session.summary || session.command || sessionId;
|
|
13756
|
+
var sessionStatus = session.status || "running";
|
|
13757
|
+
|
|
13758
|
+
// Clear notification for inactive sessions
|
|
13759
|
+
if (sessionStatus === "idle" || sessionStatus === "archived" || sessionStatus === "exited") {
|
|
13760
|
+
clearSessionProgressNative(sessionId);
|
|
13761
|
+
return;
|
|
13762
|
+
}
|
|
13763
|
+
|
|
13764
|
+
// Get latest todos from session messages
|
|
13765
|
+
var todos = null;
|
|
13766
|
+
var messages = session.messages || [];
|
|
13767
|
+
for (var i = messages.length - 1; i >= 0; i--) {
|
|
13768
|
+
var msg = messages[i];
|
|
13769
|
+
if (!msg.content || !Array.isArray(msg.content)) continue;
|
|
13770
|
+
for (var j = msg.content.length - 1; j >= 0; j--) {
|
|
13771
|
+
var block = msg.content[j];
|
|
13772
|
+
if (block.type === "tool_use" && block.name === "TodoWrite"
|
|
13773
|
+
&& block.input && block.input.todos) {
|
|
13774
|
+
todos = block.input.todos;
|
|
13775
|
+
break;
|
|
13776
|
+
}
|
|
13777
|
+
}
|
|
13778
|
+
if (todos) break;
|
|
13779
|
+
}
|
|
13780
|
+
|
|
13781
|
+
// Get current task
|
|
13782
|
+
var currentTask = "";
|
|
13783
|
+
if (sessionId === state.selectedId && state.currentTask && state.currentTask.title) {
|
|
13784
|
+
currentTask = state.currentTask.title;
|
|
13785
|
+
}
|
|
13786
|
+
|
|
13787
|
+
var data = {
|
|
13788
|
+
sessionLabel: sessionLabel,
|
|
13789
|
+
status: sessionStatus,
|
|
13790
|
+
currentTask: currentTask,
|
|
13791
|
+
todos: todos || []
|
|
13792
|
+
};
|
|
13793
|
+
|
|
13794
|
+
try {
|
|
13795
|
+
WandNative.updateSessionProgress(sessionId, JSON.stringify(data));
|
|
13796
|
+
} catch (_e) {}
|
|
13797
|
+
}
|
|
13798
|
+
|
|
13799
|
+
function clearSessionProgressNative(sessionId) {
|
|
13800
|
+
if (!_hasNativeBridge || typeof WandNative.clearSessionProgress !== "function") return;
|
|
13801
|
+
if (_progressSyncTimers[sessionId]) {
|
|
13802
|
+
clearTimeout(_progressSyncTimers[sessionId]);
|
|
13803
|
+
delete _progressSyncTimers[sessionId];
|
|
13804
|
+
}
|
|
13805
|
+
try { WandNative.clearSessionProgress(sessionId); } catch (_e) {}
|
|
13806
|
+
}
|
|
13391
13807
|
|
|
13392
13808
|
/**
|
|
13393
13809
|
* Play a soft, rounded notification chime using Web Audio API.
|
|
@@ -13606,6 +14022,23 @@
|
|
|
13606
14022
|
}, 2000);
|
|
13607
14023
|
}
|
|
13608
14024
|
|
|
14025
|
+
function showAutoUpdateOverlay(currentVer, latestVer) {
|
|
14026
|
+
if (document.getElementById("restart-overlay")) return;
|
|
14027
|
+
var overlay = document.createElement("div");
|
|
14028
|
+
overlay.id = "restart-overlay";
|
|
14029
|
+
overlay.className = "restart-overlay";
|
|
14030
|
+
overlay.innerHTML =
|
|
14031
|
+
'<div class="restart-overlay-content">' +
|
|
14032
|
+
'<div class="restart-spinner"></div>' +
|
|
14033
|
+
'<div class="restart-title">\u81ea\u52a8\u66f4\u65b0\u4e2d</div>' +
|
|
14034
|
+
'<div class="restart-subtitle">' +
|
|
14035
|
+
escapeHtml(currentVer) + ' \u2192 ' + escapeHtml(latestVer) +
|
|
14036
|
+
'<br>\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\uff0c\u7a0d\u540e\u5c06\u81ea\u52a8\u91cd\u542f\u2026' +
|
|
14037
|
+
'</div>' +
|
|
14038
|
+
'</div>';
|
|
14039
|
+
document.body.appendChild(overlay);
|
|
14040
|
+
}
|
|
14041
|
+
|
|
13609
14042
|
function escapeHtml(value) {
|
|
13610
14043
|
return String(value)
|
|
13611
14044
|
.replace(/&/g, "&")
|