@co0ontty/wand 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/dist/acp-protocol.d.ts +67 -0
- package/dist/acp-protocol.js +291 -0
- package/dist/claude-pty-bridge.d.ts +139 -0
- package/dist/claude-pty-bridge.js +649 -0
- package/dist/claude-stream-adapter.d.ts +35 -0
- package/dist/claude-stream-adapter.js +153 -0
- package/dist/claude-structured-runner.d.ts +27 -0
- package/dist/claude-structured-runner.js +106 -0
- package/dist/config.js +2 -2
- package/dist/message-parser.js +12 -66
- package/dist/message-queue.d.ts +57 -0
- package/dist/message-queue.js +127 -0
- package/dist/process-manager.d.ts +32 -25
- package/dist/process-manager.js +503 -780
- package/dist/server.js +366 -51
- package/dist/session-lifecycle.d.ts +81 -0
- package/dist/session-lifecycle.js +176 -0
- package/dist/storage.js +12 -1
- package/dist/types.d.ts +105 -5
- package/dist/web-ui/content/scripts.js +2307 -658
- package/dist/web-ui/content/styles.css +5284 -2771
- package/dist/web-ui/index.js +8 -5
- package/dist/web-ui/scripts.js +8 -1
- package/package.json +2 -9
- package/dist/web-ui/utils.d.ts +0 -4
- package/dist/web-ui/utils.js +0 -12
- package/dist/web-ui.d.ts +0 -1
- package/dist/web-ui.js +0 -2
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
// Register Service Worker for PWA
|
|
2
|
+
// For self-signed certificates, we need to handle certificate errors gracefully
|
|
2
3
|
if ('serviceWorker' in navigator) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// First, try to fetch the service worker script with a custom handler for certificate errors
|
|
5
|
+
fetch('/sw.js', { cache: 'no-cache' })
|
|
6
|
+
.then(function(response) {
|
|
7
|
+
if (response.ok) {
|
|
8
|
+
return navigator.serviceWorker.register('/sw.js');
|
|
9
|
+
}
|
|
10
|
+
// If fetch fails (e.g., certificate error), skip service worker registration
|
|
11
|
+
console.log('SW fetch failed, skipping service worker registration');
|
|
12
|
+
return Promise.reject('Service worker script not available');
|
|
13
|
+
})
|
|
14
|
+
.catch(function(e) {
|
|
15
|
+
// Distinguish between certificate errors and other failures
|
|
16
|
+
if (e.name === 'TypeError' || e.message.includes('certificate')) {
|
|
17
|
+
console.log('SW registration skipped: likely self-signed certificate issue');
|
|
18
|
+
} else {
|
|
19
|
+
console.log('SW registration failed:', e.message || e);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
6
22
|
}
|
|
7
23
|
|
|
8
24
|
(function() {
|
|
@@ -30,10 +46,9 @@
|
|
|
30
46
|
isSyncingInputBox: false,
|
|
31
47
|
loginPending: false,
|
|
32
48
|
loginChecked: false,
|
|
33
|
-
sessionsDrawerOpen:
|
|
49
|
+
sessionsDrawerOpen: false,
|
|
34
50
|
modalOpen: false,
|
|
35
51
|
presetValue: "",
|
|
36
|
-
commandValue: "",
|
|
37
52
|
cwdValue: "",
|
|
38
53
|
modeValue: "full-access",
|
|
39
54
|
chatMode: "full-access",
|
|
@@ -45,7 +60,17 @@
|
|
|
45
60
|
showInstallPrompt: false,
|
|
46
61
|
ws: null,
|
|
47
62
|
wsConnected: false,
|
|
48
|
-
currentView: "
|
|
63
|
+
currentView: "terminal",
|
|
64
|
+
terminalScale: (function() {
|
|
65
|
+
try {
|
|
66
|
+
var saved = localStorage.getItem("wand-terminal-scale");
|
|
67
|
+
return saved ? parseFloat(saved) : 1;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
})(),
|
|
72
|
+
terminalBaseFontSize: 13,
|
|
73
|
+
keyboardPopupOpen: false,
|
|
49
74
|
filePanelOpen: (function() {
|
|
50
75
|
try {
|
|
51
76
|
return localStorage.getItem("wand-file-panel-open") === "true";
|
|
@@ -58,6 +83,10 @@
|
|
|
58
83
|
lastRenderedMsgCount: 0,
|
|
59
84
|
lastRenderedEmpty: null,
|
|
60
85
|
renderPending: false,
|
|
86
|
+
currentTask: null, // Current task title from Claude
|
|
87
|
+
terminalInteractive: false,
|
|
88
|
+
miniKeyboardVisible: false,
|
|
89
|
+
modifiers: { ctrl: false, alt: false, shift: false },
|
|
61
90
|
fileSearchQuery: "",
|
|
62
91
|
allFiles: [],
|
|
63
92
|
// Load last used working directory from localStorage
|
|
@@ -116,34 +145,14 @@
|
|
|
116
145
|
}
|
|
117
146
|
|
|
118
147
|
function updateInstallPrompt() {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
'
|
|
126
|
-
|
|
127
|
-
'<div class="prompt-title">Install Wand</div>' +
|
|
128
|
-
'<div class="prompt-desc">Add to home screen for quick access</div>' +
|
|
129
|
-
'</div>' +
|
|
130
|
-
'<div class="prompt-actions">' +
|
|
131
|
-
'<button id="pwa-install-dismiss" class="btn btn-ghost btn-sm">Later</button>' +
|
|
132
|
-
'<button id="pwa-install-accept" class="btn btn-primary btn-sm">Install</button>' +
|
|
133
|
-
'</div>';
|
|
134
|
-
document.body.appendChild(el);
|
|
135
|
-
document.getElementById('pwa-install-dismiss').addEventListener('click', function() {
|
|
136
|
-
el.remove();
|
|
137
|
-
state.showInstallPrompt = false;
|
|
138
|
-
});
|
|
139
|
-
document.getElementById('pwa-install-accept').addEventListener('click', function() {
|
|
140
|
-
state.deferredPrompt.prompt();
|
|
141
|
-
state.deferredPrompt.userChoice.then(function(result) {
|
|
142
|
-
state.deferredPrompt = null;
|
|
143
|
-
state.showInstallPrompt = false;
|
|
144
|
-
el.remove();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
148
|
+
// 显示或隐藏菜单栏中的安装按钮
|
|
149
|
+
var installBtn = document.getElementById('pwa-install-button');
|
|
150
|
+
if (installBtn) {
|
|
151
|
+
if (state.showInstallPrompt && state.deferredPrompt) {
|
|
152
|
+
installBtn.classList.remove('hidden');
|
|
153
|
+
} else {
|
|
154
|
+
installBtn.classList.add('hidden');
|
|
155
|
+
}
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
@@ -205,21 +214,59 @@
|
|
|
205
214
|
var modal = document.getElementById("session-modal");
|
|
206
215
|
if (modal) {
|
|
207
216
|
modal.classList.remove("hidden");
|
|
208
|
-
var commandEl = document.getElementById("command");
|
|
209
217
|
var cwdEl = document.getElementById("cwd");
|
|
210
|
-
var modeEl = document.getElementById("mode");
|
|
211
|
-
if (commandEl) commandEl.value = state.commandValue;
|
|
212
218
|
if (cwdEl) cwdEl.value = state.cwdValue;
|
|
213
|
-
if (modeEl) modeEl.value = state.modeValue;
|
|
214
219
|
syncSessionModalUI();
|
|
215
220
|
}
|
|
216
221
|
}
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
function renderInlineKeyboard() {
|
|
225
|
+
if (!state.selectedId) return "";
|
|
226
|
+
// Keyboard toggle button + popup panel
|
|
227
|
+
var isActive = state.currentView === "terminal";
|
|
228
|
+
return '<button id="keyboard-toggle" class="keyboard-toggle-btn' + (isActive ? "" : " hidden") + '" type="button" title="快捷键盘">' +
|
|
229
|
+
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
230
|
+
'<rect x="2" y="4" width="20" height="16" rx="2" ry="2"/>' +
|
|
231
|
+
'<line x1="6" y1="8" x2="6.01" y2="8"/>' +
|
|
232
|
+
'<line x1="10" y1="8" x2="10.01" y2="8"/>' +
|
|
233
|
+
'<line x1="14" y1="8" x2="14.01" y2="8"/>' +
|
|
234
|
+
'<line x1="18" y1="8" x2="18.01" y2="8"/>' +
|
|
235
|
+
'<line x1="6" y1="12" x2="18" y2="12"/>' +
|
|
236
|
+
'</svg>' +
|
|
237
|
+
'</button>' +
|
|
238
|
+
'<div id="keyboard-popup" class="keyboard-popup' + (state.keyboardPopupOpen ? '' : ' hidden') + '">' +
|
|
239
|
+
'<div class="keyboard-popup-row modifiers">' +
|
|
240
|
+
'<button class="kp-key' + (state.modifiers.ctrl ? ' active' : '') + '" data-key="ctrl" type="button">Ctrl</button>' +
|
|
241
|
+
'<button class="kp-key' + (state.modifiers.alt ? ' active' : '') + '" data-key="alt" type="button">Alt</button>' +
|
|
242
|
+
'</div>' +
|
|
243
|
+
'<div class="keyboard-popup-row directions">' +
|
|
244
|
+
'<div class="kp-dir-grid">' +
|
|
245
|
+
'<div class="kp-dir-up"><button class="kp-key kp-dir" data-key="up" type="button">↑</button></div>' +
|
|
246
|
+
'<div class="kp-dir-lr">' +
|
|
247
|
+
'<button class="kp-key kp-dir" data-key="left" type="button">←</button>' +
|
|
248
|
+
'<button class="kp-key kp-dir" data-key="down" type="button">↓</button>' +
|
|
249
|
+
'<button class="kp-key kp-dir" data-key="right" type="button">→</button>' +
|
|
250
|
+
'</div>' +
|
|
251
|
+
'</div>' +
|
|
252
|
+
'</div>' +
|
|
253
|
+
'<div class="keyboard-popup-row actions">' +
|
|
254
|
+
'<button class="kp-key kp-action" data-key="enter" type="button">↵ 回车</button>' +
|
|
255
|
+
'<button class="kp-key kp-action" data-key="ctrl_enter" type="button">C-↵</button>' +
|
|
256
|
+
'<button class="kp-key kp-action kp-escape" data-key="escape" type="button">Esc</button>' +
|
|
257
|
+
'</div>' +
|
|
258
|
+
'</div>';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderMiniKeyboard() {
|
|
262
|
+
// Mini keyboard is now inline, rendered in input-composer-right
|
|
263
|
+
return "";
|
|
264
|
+
}
|
|
265
|
+
|
|
219
266
|
function renderLogin() {
|
|
220
267
|
if (!state.loginChecked) {
|
|
221
268
|
return '<div class="login-container">' +
|
|
222
|
-
'<div class="login-card">' +
|
|
269
|
+
'<div class="login-card login-card-loading">' +
|
|
223
270
|
'<div class="login-header">' +
|
|
224
271
|
'<div class="login-logo">' +
|
|
225
272
|
'<div class="login-logo-icon">W</div>' +
|
|
@@ -228,7 +275,13 @@
|
|
|
228
275
|
'<div class="login-subtitle">正在恢复登录状态</div>' +
|
|
229
276
|
'</div>' +
|
|
230
277
|
'<div class="login-body">' +
|
|
231
|
-
'<
|
|
278
|
+
'<div class="login-status">' +
|
|
279
|
+
'<span class="login-spinner" aria-hidden="true"></span>' +
|
|
280
|
+
'<div>' +
|
|
281
|
+
'<p class="login-hint">正在检查本地登录会话,请稍候。</p>' +
|
|
282
|
+
'<p class="login-muted">如果你刚刷新页面,这是正常现象。</p>' +
|
|
283
|
+
'</div>' +
|
|
284
|
+
'</div>' +
|
|
232
285
|
'</div>' +
|
|
233
286
|
'</div>' +
|
|
234
287
|
'</div>';
|
|
@@ -243,15 +296,18 @@
|
|
|
243
296
|
'<div class="login-subtitle">在浏览器中运行本机终端</div>' +
|
|
244
297
|
'</div>' +
|
|
245
298
|
'<div class="login-body">' +
|
|
246
|
-
'<p class="login-hint"
|
|
247
|
-
'<p class="login-tip"
|
|
299
|
+
'<p class="login-hint">输入 Wand 访问密码以进入控制台。</p>' +
|
|
300
|
+
'<p class="login-tip">如果页面是通过 <strong>https://</strong> 打开的,请改用 <strong>http://</strong> 访问本地服务。</p>' +
|
|
248
301
|
'<div class="field">' +
|
|
249
302
|
'<label class="field-label" for="password">密码</label>' +
|
|
250
|
-
'<
|
|
251
|
-
|
|
303
|
+
'<div class="password-field">' +
|
|
304
|
+
'<input id="password" type="password" class="field-input password-input" placeholder="输入访问密码" autocomplete="current-password" data-error="false" aria-describedby="password-hint login-error" aria-invalid="false" />' +
|
|
305
|
+
'<button id="toggle-password-button" type="button" class="password-toggle" aria-label="显示密码" aria-pressed="false">显示</button>' +
|
|
306
|
+
'</div>' +
|
|
307
|
+
'<p id="password-hint" class="hint">使用你在 Wand 中设置的访问密码。</p>' +
|
|
308
|
+
'<p id="login-error" class="error-message hidden" role="alert"></p>' +
|
|
252
309
|
'</div>' +
|
|
253
310
|
'<button id="login-button" class="btn btn-primary btn-block">进入控制台</button>' +
|
|
254
|
-
'<p id="login-error" class="error-message hidden"></p>' +
|
|
255
311
|
'</div>' +
|
|
256
312
|
'</div>' +
|
|
257
313
|
'</div>';
|
|
@@ -275,26 +331,30 @@
|
|
|
275
331
|
'<span></span><span></span><span></span>' +
|
|
276
332
|
'</span>' +
|
|
277
333
|
'</button>' +
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
'<div class="logo">' +
|
|
281
|
-
'<div class="logo-icon">W</div>' +
|
|
282
|
-
'<span class="logo-text">Wand</span>' +
|
|
334
|
+
'<div class="topbar-logo">' +
|
|
335
|
+
'<div class="topbar-logo-icon">W</div>' +
|
|
283
336
|
'</div>' +
|
|
284
337
|
'</div>' +
|
|
285
338
|
'<div class="topbar-center">' +
|
|
286
|
-
'<div class="session-
|
|
287
|
-
'<span class="
|
|
339
|
+
'<div class="topbar-session-meta">' +
|
|
340
|
+
'<span class="topbar-title" id="terminal-title">' + escapeHtml(terminalTitle) + '</span>' +
|
|
341
|
+
'<span class="terminal-info topbar-terminal-info" id="terminal-info">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '开始对话') + '</span>' +
|
|
288
342
|
'</div>' +
|
|
343
|
+
'<span class="current-task hidden" id="current-task"></span>' +
|
|
344
|
+
'<span class="permission-actions hidden" id="permission-actions"><button id="approve-permission-btn" class="btn btn-primary btn-small" type="button">批准</button><button id="deny-permission-btn" class="btn btn-ghost btn-small" type="button">拒绝</button></span>' +
|
|
289
345
|
'</div>' +
|
|
290
346
|
'<div class="topbar-right">' +
|
|
347
|
+
'<button id="file-panel-toggle-btn" class="topbar-btn square' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件">📁</button>' +
|
|
291
348
|
'<button id="topbar-new-session-button" class="topbar-new-btn" title="新对话">' +
|
|
292
349
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' +
|
|
293
350
|
'新对话' +
|
|
294
351
|
'</button>' +
|
|
295
|
-
'<button id="
|
|
296
|
-
|
|
297
|
-
'
|
|
352
|
+
'<button id="terminal-interactive-toggle-top" class="topbar-btn' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨ ' + (state.terminalInteractive ? '交互开' : '交互关') + '</button>' +
|
|
353
|
+
'<button id="pwa-install-button" class="topbar-btn square hidden" title="安装应用">' +
|
|
354
|
+
'<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>' +
|
|
355
|
+
'</button>' +
|
|
356
|
+
'<button id="logout-button" class="topbar-btn square" title="退出登录">' +
|
|
357
|
+
'<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>' +
|
|
298
358
|
'</button>' +
|
|
299
359
|
'</div>' +
|
|
300
360
|
'</header>' +
|
|
@@ -319,21 +379,9 @@
|
|
|
319
379
|
'</div>' +
|
|
320
380
|
'</aside>' +
|
|
321
381
|
'<main class="main-content">' +
|
|
322
|
-
'<div class="
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
'<span class="terminal-info" id="terminal-info">' + (selectedSession ? (getModeLabel(selectedSession.mode) + " | " + selectedSession.status) : "开始对话") + '</span>' +
|
|
326
|
-
'</div>' +
|
|
327
|
-
'<div class="terminal-header-actions">' +
|
|
328
|
-
'<div class="view-toggle" aria-label="视图切换">' +
|
|
329
|
-
'<button id="view-terminal-btn" class="view-toggle-btn' + (state.currentView === "terminal" ? " active" : "") + '" type="button">终端</button>' +
|
|
330
|
-
'<button id="view-chat-btn" class="view-toggle-btn' + (state.currentView === "chat" ? " active" : "") + '" type="button">对话</button>' +
|
|
331
|
-
'</div>' +
|
|
332
|
-
'<div class="file-panel-toggle" aria-label="文件浏览器">' +
|
|
333
|
-
'<button id="file-panel-toggle-btn" class="view-toggle-btn' + (state.filePanelOpen ? " active" : "") + '" type="button" title="文件浏览器">📁</button>' +
|
|
334
|
-
'</div>' +
|
|
335
|
-
'</div>' +
|
|
336
|
-
'</div>' +
|
|
382
|
+
'<div class="topbar-spacer"></div>' +
|
|
383
|
+
// File panel backdrop (mobile)
|
|
384
|
+
'<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
|
|
337
385
|
// File side panel
|
|
338
386
|
'<div id="file-side-panel" class="file-side-panel' + (state.filePanelOpen ? " open" : "") + '">' +
|
|
339
387
|
'<div class="file-side-panel-header">' +
|
|
@@ -352,30 +400,30 @@
|
|
|
352
400
|
'<div class="file-explorer" id="file-explorer">' + renderFileExplorer(selectedSession && selectedSession.cwd ? selectedSession.cwd : (state.config && state.config.defaultCwd ? state.config.defaultCwd : "")) + '</div>' +
|
|
353
401
|
'</div>' +
|
|
354
402
|
'</div>' +
|
|
355
|
-
|
|
403
|
+
'<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active">' +
|
|
404
|
+
'<div class="terminal-scale-overlay" aria-label="终端缩放控件">' +
|
|
405
|
+
'<button id="terminal-scale-down-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
|
|
406
|
+
'<span class="terminal-scale-overlay-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
|
|
407
|
+
'<button id="terminal-scale-up-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="放大">+</button>' +
|
|
408
|
+
'</div>' +
|
|
409
|
+
'</div>' +
|
|
410
|
+
'<div id="chat-output" class="chat-container hidden"></div>' +
|
|
356
411
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
357
412
|
'<div class="blank-chat-inner">' +
|
|
358
413
|
'<div class="blank-chat-logo">W</div>' +
|
|
359
414
|
'<h2 class="blank-chat-title">Wand</h2>' +
|
|
360
|
-
'<p class="blank-chat-subtitle"
|
|
361
|
-
'<div class="blank-chat-input-wrap">' +
|
|
362
|
-
'<input type="text" id="welcome-input" class="blank-chat-input" ' +
|
|
363
|
-
'placeholder="输入你的问题,按 Enter 发送..." autocomplete="off" spellcheck="false" />' +
|
|
364
|
-
'<button id="welcome-send-btn" class="blank-chat-send-btn" type="button">发送</button>' +
|
|
365
|
-
'</div>' +
|
|
415
|
+
'<p class="blank-chat-subtitle">当前仅保留原生终端模式,优先修复 PTY 交互与显示。</p>' +
|
|
366
416
|
'<div class="blank-chat-tools">' +
|
|
367
417
|
'<button class="blank-chat-tool-btn" id="welcome-tool-claude" type="button">' +
|
|
368
|
-
'<span class="tool-icon">🤖</span
|
|
418
|
+
'<span class="tool-icon">🤖</span>新建终端会话' +
|
|
369
419
|
'</button>' +
|
|
370
420
|
'<button class="blank-chat-tool-btn" id="welcome-tool-folder" type="button" title="选择工作目录">' +
|
|
371
421
|
'<span class="tool-icon">📎</span>目录' +
|
|
372
422
|
'</button>' +
|
|
373
423
|
'</div>' +
|
|
374
|
-
'<p class="blank-chat-hint">按 Enter 发送消息,或点击上方按钮快速开始</p>' +
|
|
375
424
|
'</div>' +
|
|
376
425
|
'</div>' +
|
|
377
|
-
'<div id="output" class="
|
|
378
|
-
'<div id="chat-output" class="chat-container' + (state.selectedId ? "" : " hidden") + (state.selectedId && state.currentView === "chat" ? " active" : "") + '"></div>' +
|
|
426
|
+
'<div id="chat-output" class="chat-container hidden"></div>' +
|
|
379
427
|
'<div class="input-panel' + (state.selectedId ? "" : " hidden") + '">' +
|
|
380
428
|
'<div id="todo-progress" class="todo-progress hidden">' +
|
|
381
429
|
'<div class="todo-progress-header" id="todo-progress-toggle">' +
|
|
@@ -392,7 +440,7 @@
|
|
|
392
440
|
'</div>' +
|
|
393
441
|
'</div>' +
|
|
394
442
|
'<div class="input-composer">' +
|
|
395
|
-
'<textarea id="input-box" class="input-textarea" placeholder="输入消息..." rows="1">' + escapeHtml(currentDraft) + '</textarea>' +
|
|
443
|
+
'<textarea id="input-box" class="input-textarea" placeholder="' + (state.terminalInteractive ? "终端交互模式开启中,请直接在终端中输入" : "输入消息...") + '" rows="1">' + escapeHtml(currentDraft) + '</textarea>' +
|
|
396
444
|
'<div class="input-composer-bar">' +
|
|
397
445
|
'<div class="input-composer-left">' +
|
|
398
446
|
'<select id="chat-mode-select" class="chat-mode-select" title="仅对新建会话生效">' +
|
|
@@ -401,7 +449,8 @@
|
|
|
401
449
|
'</div>' +
|
|
402
450
|
'<div class="input-composer-right">' +
|
|
403
451
|
'<span id="queue-counter" class="queue-counter hidden">队列: 0</span>' +
|
|
404
|
-
'<span class="input-hint">Enter 发送 · Shift+Enter
|
|
452
|
+
'<span class="input-hint' + (state.terminalInteractive ? ' terminal-interactive-hint' : state.currentView === "terminal" ? " hidden" : "") + '">' + (state.terminalInteractive ? '终端交互中 · Ctrl+C 中断 · Ctrl+L 清屏' : 'Enter 发送 · Shift+Enter 换行') + '</span>' +
|
|
453
|
+
renderInlineKeyboard() +
|
|
405
454
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
406
455
|
'<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="3" y="3" width="10" height="10" rx="2"/></svg>' +
|
|
407
456
|
'</button>' +
|
|
@@ -410,9 +459,16 @@
|
|
|
410
459
|
'</button>' +
|
|
411
460
|
'</div>' +
|
|
412
461
|
'</div>' +
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
462
|
+
// Session info bar at bottom
|
|
463
|
+
'<div class="input-session-info-bar">' +
|
|
464
|
+
'<span id="session-cwd-display" class="session-cwd-display">' + (selectedSession && selectedSession.cwd ? escapeHtml(selectedSession.cwd) : '未设置目录') + '</span>' +
|
|
465
|
+
'<span class="session-info-separator">|</span>' +
|
|
466
|
+
'<span id="session-mode-display" class="session-mode-display">' + (selectedSession ? getModeLabel(selectedSession.mode) : '默认') + '</span>' +
|
|
467
|
+
'<span class="session-info-separator">|</span>' +
|
|
468
|
+
'<span id="session-status-display" class="session-status-display">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '-') + '</span>' +
|
|
469
|
+
'<span class="session-info-separator">|</span>' +
|
|
470
|
+
'<span id="session-exit-display" class="session-exit-display">exit=' + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : 'n/a') + '</span>' +
|
|
471
|
+
'</div>' +
|
|
416
472
|
'</div>' +
|
|
417
473
|
'<p id="action-error" class="error-message hidden"></p>' +
|
|
418
474
|
'</div>' +
|
|
@@ -490,32 +546,52 @@
|
|
|
490
546
|
'</section>';
|
|
491
547
|
}
|
|
492
548
|
|
|
493
|
-
function
|
|
494
|
-
|
|
549
|
+
function isMobileLayout() {
|
|
550
|
+
return window.innerWidth <= 768;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function setFilePanelOpen(nextOpen) {
|
|
554
|
+
state.filePanelOpen = nextOpen;
|
|
495
555
|
try {
|
|
496
556
|
localStorage.setItem("wand-file-panel-open", String(state.filePanelOpen));
|
|
497
557
|
} catch (e) {}
|
|
498
|
-
|
|
558
|
+
if (state.filePanelOpen && isMobileLayout()) {
|
|
559
|
+
state.sessionsDrawerOpen = false;
|
|
560
|
+
}
|
|
561
|
+
updateLayoutState();
|
|
499
562
|
if (state.filePanelOpen) {
|
|
500
563
|
refreshFileExplorer();
|
|
501
564
|
}
|
|
502
565
|
}
|
|
503
566
|
|
|
567
|
+
function toggleFilePanel() {
|
|
568
|
+
setFilePanelOpen(!state.filePanelOpen);
|
|
569
|
+
}
|
|
570
|
+
|
|
504
571
|
function updateFilePanelState() {
|
|
505
572
|
var panel = document.getElementById("file-side-panel");
|
|
506
573
|
var mainContent = document.querySelector(".main-content");
|
|
507
574
|
var toggleBtn = document.getElementById("file-panel-toggle-btn");
|
|
575
|
+
var backdrop = document.getElementById("file-panel-backdrop");
|
|
508
576
|
if (panel) {
|
|
509
577
|
panel.classList.toggle("open", state.filePanelOpen);
|
|
510
578
|
}
|
|
511
579
|
if (mainContent) {
|
|
512
580
|
mainContent.classList.toggle("file-panel-open", state.filePanelOpen);
|
|
513
581
|
}
|
|
582
|
+
if (backdrop) {
|
|
583
|
+
backdrop.classList.toggle("open", state.filePanelOpen);
|
|
584
|
+
}
|
|
514
585
|
if (toggleBtn) {
|
|
515
586
|
toggleBtn.classList.toggle("active", state.filePanelOpen);
|
|
516
587
|
}
|
|
517
588
|
}
|
|
518
589
|
|
|
590
|
+
function updateLayoutState() {
|
|
591
|
+
updateDrawerState();
|
|
592
|
+
updateFilePanelState();
|
|
593
|
+
}
|
|
594
|
+
|
|
519
595
|
function updateFilePanelCwd(session) {
|
|
520
596
|
var cwdEl = document.getElementById("file-explorer-cwd");
|
|
521
597
|
if (!cwdEl) return;
|
|
@@ -525,11 +601,36 @@
|
|
|
525
601
|
|
|
526
602
|
function closeFilePanel() {
|
|
527
603
|
if (!state.filePanelOpen) return;
|
|
528
|
-
|
|
604
|
+
setFilePanelOpen(false);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function adjustTerminalScale(delta) {
|
|
608
|
+
var newScale = state.terminalScale + delta;
|
|
609
|
+
// Clamp scale between 0.5 and 2
|
|
610
|
+
newScale = Math.max(0.5, Math.min(2, newScale));
|
|
611
|
+
// Round to nearest 0.25
|
|
612
|
+
newScale = Math.round(newScale * 4) / 4;
|
|
613
|
+
if (newScale === state.terminalScale) return;
|
|
614
|
+
state.terminalScale = newScale;
|
|
529
615
|
try {
|
|
530
|
-
localStorage.setItem("wand-
|
|
616
|
+
localStorage.setItem("wand-terminal-scale", String(newScale));
|
|
531
617
|
} catch (e) {}
|
|
532
|
-
|
|
618
|
+
applyTerminalScale();
|
|
619
|
+
updateScaleLabel();
|
|
620
|
+
scheduleTerminalResize();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function applyTerminalScale() {
|
|
624
|
+
if (!state.terminal) return;
|
|
625
|
+
state.terminal.options.fontSize = state.terminalBaseFontSize * state.terminalScale;
|
|
626
|
+
state.terminal.refresh(0, state.terminal.rows - 1);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function updateScaleLabel() {
|
|
630
|
+
var label = document.getElementById("terminal-scale-label-top");
|
|
631
|
+
if (label) {
|
|
632
|
+
label.textContent = Math.round(state.terminalScale * 100) + "%";
|
|
633
|
+
}
|
|
533
634
|
}
|
|
534
635
|
|
|
535
636
|
function renderFileExplorer(cwd) {
|
|
@@ -656,6 +757,11 @@
|
|
|
656
757
|
toggleTreeNode(item);
|
|
657
758
|
});
|
|
658
759
|
});
|
|
760
|
+
tree.querySelectorAll(".tree-item[data-type='file']").forEach(function(item) {
|
|
761
|
+
item.addEventListener("dblclick", function() {
|
|
762
|
+
openFilePreview(item.dataset.path);
|
|
763
|
+
});
|
|
764
|
+
});
|
|
659
765
|
}
|
|
660
766
|
|
|
661
767
|
function toggleTreeNode(item) {
|
|
@@ -690,6 +796,205 @@
|
|
|
690
796
|
.catch(function() {});
|
|
691
797
|
}
|
|
692
798
|
|
|
799
|
+
function openFilePreview(filePath) {
|
|
800
|
+
var overlay = document.createElement("div");
|
|
801
|
+
overlay.className = "file-preview-overlay";
|
|
802
|
+
overlay.innerHTML =
|
|
803
|
+
'<div class="file-preview-modal">' +
|
|
804
|
+
'<div class="file-preview-header">' +
|
|
805
|
+
'<div class="file-preview-title">' +
|
|
806
|
+
'<span>📄</span>' +
|
|
807
|
+
'<span class="file-preview-filename">Loading...</span>' +
|
|
808
|
+
'</div>' +
|
|
809
|
+
'<div class="file-preview-path" title="' + escapeHtml(filePath) + '">' + escapeHtml(filePath) + '</div>' +
|
|
810
|
+
'<button class="file-preview-close" title="Close">✕</button>' +
|
|
811
|
+
'</div>' +
|
|
812
|
+
'<div class="file-preview-body">' +
|
|
813
|
+
'<div class="file-preview-loading">Loading preview...</div>' +
|
|
814
|
+
'</div>' +
|
|
815
|
+
'</div>';
|
|
816
|
+
document.body.appendChild(overlay);
|
|
817
|
+
|
|
818
|
+
var closeBtn = overlay.querySelector(".file-preview-close");
|
|
819
|
+
var closeModal = function() {
|
|
820
|
+
overlay.remove();
|
|
821
|
+
document.removeEventListener("keydown", escHandler);
|
|
822
|
+
};
|
|
823
|
+
closeBtn.addEventListener("click", closeModal);
|
|
824
|
+
overlay.addEventListener("click", function(e) {
|
|
825
|
+
if (e.target === overlay) closeModal();
|
|
826
|
+
});
|
|
827
|
+
var escHandler = function(e) {
|
|
828
|
+
if (e.key === "Escape") closeModal();
|
|
829
|
+
};
|
|
830
|
+
document.addEventListener("keydown", escHandler);
|
|
831
|
+
|
|
832
|
+
fetch("/api/file-preview?path=" + encodeURIComponent(filePath), { credentials: "same-origin" })
|
|
833
|
+
.then(function(res) { return res.json(); })
|
|
834
|
+
.then(function(data) {
|
|
835
|
+
if (data.error) {
|
|
836
|
+
var body = overlay.querySelector(".file-preview-body");
|
|
837
|
+
body.innerHTML = '<div class="file-preview-error"><span class="preview-error-icon">⚠</span><span>' + escapeHtml(data.error) + '</span></div>';
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
renderPreviewContent(overlay, data);
|
|
841
|
+
})
|
|
842
|
+
.catch(function(err) {
|
|
843
|
+
var body = overlay.querySelector(".file-preview-body");
|
|
844
|
+
body.innerHTML = '<div class="file-preview-error"><span class="preview-error-icon">⚠</span><span>Failed to load preview</span></div>';
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function renderPreviewContent(overlay, data) {
|
|
849
|
+
var filename = overlay.querySelector(".file-preview-filename");
|
|
850
|
+
filename.textContent = data.name;
|
|
851
|
+
|
|
852
|
+
var langBadge = document.createElement("span");
|
|
853
|
+
langBadge.className = "file-preview-lang";
|
|
854
|
+
langBadge.textContent = data.lang || data.ext.replace(".", "");
|
|
855
|
+
overlay.querySelector(".file-preview-title").appendChild(langBadge);
|
|
856
|
+
|
|
857
|
+
var body = overlay.querySelector(".file-preview-body");
|
|
858
|
+
|
|
859
|
+
if (data.lang === "markdown") {
|
|
860
|
+
body.innerHTML = '<div class="markdown-preview">' + renderMarkdownPreview(data.content) + '</div>';
|
|
861
|
+
} else {
|
|
862
|
+
var highlighted = highlightCodePreview(data.content, data.lang);
|
|
863
|
+
var lines = highlighted.split("\n");
|
|
864
|
+
var lineNums = lines.map(function(_, i) { return i + 1; });
|
|
865
|
+
|
|
866
|
+
body.innerHTML =
|
|
867
|
+
'<div class="code-preview-wrapper">' +
|
|
868
|
+
'<div class="code-preview-lines">' + lineNums.join("\n") + '</div>' +
|
|
869
|
+
'<div class="code-preview-content"><pre>' + lines.join("\n") + '</pre></div>' +
|
|
870
|
+
'</div>';
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function highlightCodePreview(code, lang) {
|
|
875
|
+
// Escape HTML first
|
|
876
|
+
var escaped = code
|
|
877
|
+
.replace(/&/g, "&")
|
|
878
|
+
.replace(/</g, "<")
|
|
879
|
+
.replace(/>/g, ">");
|
|
880
|
+
|
|
881
|
+
// Simple token-based syntax highlighting
|
|
882
|
+
var tokens = getSyntaxTokens();
|
|
883
|
+
if (!tokens) return escaped;
|
|
884
|
+
|
|
885
|
+
// Order matters: longer patterns first, then by priority
|
|
886
|
+
var patterns = [];
|
|
887
|
+
for (var category in tokens) {
|
|
888
|
+
var t = tokens[category];
|
|
889
|
+
if (t && t.pattern) {
|
|
890
|
+
patterns.push({ pattern: t.pattern, cls: t.cls, priority: t.priority || 5 });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
patterns.sort(function(a, b) { return b.priority - a.priority; });
|
|
894
|
+
|
|
895
|
+
// Build regex for all patterns
|
|
896
|
+
var allPatterns = patterns.map(function(p) { return "(" + p.pattern.source + ")"; });
|
|
897
|
+
var regex = new RegExp(allPatterns.join("|"), "gm");
|
|
898
|
+
|
|
899
|
+
return escaped.replace(regex, function(match) {
|
|
900
|
+
for (var i = 0; i < patterns.length; i++) {
|
|
901
|
+
var p = patterns[i];
|
|
902
|
+
var re = new RegExp("^" + p.pattern.source + "$", "gm");
|
|
903
|
+
if (re.test(match)) {
|
|
904
|
+
return '<span class="' + p.cls + '">' + match + '</span>';
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return match;
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function getSyntaxTokens() {
|
|
912
|
+
return {
|
|
913
|
+
comment: { pattern: /\/\/.*|#[^\n]*/y, cls: "syntax-comment", priority: 1 },
|
|
914
|
+
string: { pattern: /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/y, cls: "syntax-string", priority: 2 },
|
|
915
|
+
keyword: { pattern: /\b(?:async|await|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|module|namespace|new|null|of|override|private|protected|public|readonly|return|set|static|super|switch|this|throw|try|type|typeof|undefined|var|void|while|yield|abstract|as|base|bool|byte|char|decimal|double|event|explicit|extern|false|fixed|float|foreach|goto|implicit|in|int|internal|is|lock|long|object|operator|out|params|partial|readonly|ref|sbyte|sealed|short|sizeof|stackalloc|string|struct|switch|throw|true|try|uint|ulong|unchecked|unsafe|ushort|using|virtual|volatile|where|while|with|yield|def|elif|else|except|exec|finally|for|from|global|if|import|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield|True|False|None|and|in|is|lambda|not|or|fn|pub|use|mod|impl|trait|struct|enum|match|loop|while|for|if|else|return|self|super|crate|where|async|await|move|ref|mut|static|const|unsafe|extern|use|as|impl|struct|enum|type|fn|let|loop|if|else|match|return|self|Self|mod|pub|crate|macro|derive|where|async|await|dyn|self|package|func|go|return|defer|go|if|else|switch|case|default|for|range|select|break|continue|fallthrough|const|struct|enum|type|interface|map|chan|var|nil|true|false|iota|len|cap|append|make|new|panic|recover|select|else|if|elif|end|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while|end|and|begin|do|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/y, cls: "syntax-keyword", priority: 3 },
|
|
916
|
+
number: { pattern: /\b(?:0x[\da-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*(?:e[+-]?\d+)?)\b/y, cls: "syntax-number", priority: 2 },
|
|
917
|
+
function: { pattern: /\b[A-Z][a-zA-Z0-9]*[a-z]\w*(?=\s*\()/y, cls: "syntax-function", priority: 4 },
|
|
918
|
+
type: { pattern: /\b(?:string|number|boolean|void|any|unknown|never|object|symbol|bigint|Array|Object|String|Number|Boolean|Map|Set|WeakMap|WeakSet|Promise|Error|Type|Interface|Enum|Class|Struct|Impl|Trait|fn|fnc|func|function|def|proc|fun|pub|static|const|let|var|int|float|double|bool|char|byte|string|u8|u16|u32|u64|i8|i16|i32|i64|f32|f64|usize|isize|str|Vec|HashMap|Option|Result|Box|Rc|Arc|Cell|RefCell)\b/y, cls: "syntax-type", priority: 4 },
|
|
919
|
+
operator: { pattern: /[+\-*/%=<>!&|^~?:]+|\.\.\.?/y, cls: "syntax-operator", priority: 5 },
|
|
920
|
+
punctuation: { pattern: /[{}[\]();,\.]/y, cls: "syntax-punctuation", priority: 6 }
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderMarkdownPreview(text) {
|
|
925
|
+
if (!text) return "";
|
|
926
|
+
var escaped = escapeHtml(text);
|
|
927
|
+
|
|
928
|
+
// Code blocks with syntax highlighting
|
|
929
|
+
escaped = escaped.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
|
|
930
|
+
var highlighted = highlightCodePreview(code.trim(), lang);
|
|
931
|
+
return '<pre><code class="language-' + lang + '">' + highlighted + '</code></pre>';
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// Inline code
|
|
935
|
+
escaped = escaped.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
936
|
+
|
|
937
|
+
// Headers
|
|
938
|
+
escaped = escaped.replace(/^######\s+(.*)$/gm, '<h6>$1</h6>');
|
|
939
|
+
escaped = escaped.replace(/^#####\s+(.*)$/gm, '<h5>$1</h5>');
|
|
940
|
+
escaped = escaped.replace(/^####\s+(.*)$/gm, '<h4>$1</h4>');
|
|
941
|
+
escaped = escaped.replace(/^###\s+(.*)$/gm, '<h3>$1</h3>');
|
|
942
|
+
escaped = escaped.replace(/^##\s+(.*)$/gm, '<h2>$1</h2>');
|
|
943
|
+
escaped = escaped.replace(/^#\s+(.*)$/gm, '<h1>$1</h1>');
|
|
944
|
+
|
|
945
|
+
// Bold and italic
|
|
946
|
+
escaped = escaped.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
947
|
+
escaped = escaped.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
948
|
+
escaped = escaped.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
949
|
+
escaped = escaped.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>');
|
|
950
|
+
escaped = escaped.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
|
951
|
+
escaped = escaped.replace(/_(.+?)_/g, '<em>$1</em>');
|
|
952
|
+
|
|
953
|
+
// Strikethrough
|
|
954
|
+
escaped = escaped.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
|
955
|
+
|
|
956
|
+
// Links
|
|
957
|
+
escaped = escaped.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
958
|
+
|
|
959
|
+
// Images
|
|
960
|
+
escaped = escaped.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
961
|
+
|
|
962
|
+
// Blockquote
|
|
963
|
+
escaped = escaped.replace(/^>\s+(.*)$/gm, '<blockquote>$1</blockquote>');
|
|
964
|
+
|
|
965
|
+
// Horizontal rule
|
|
966
|
+
escaped = escaped.replace(/^---+$/gm, '<hr>');
|
|
967
|
+
escaped = escaped.replace(/^\*\*\*+$/gm, '<hr>');
|
|
968
|
+
|
|
969
|
+
// Unordered lists
|
|
970
|
+
escaped = escaped.replace(/^[\-\*]\s+(.*)$/gm, '<li>$1</li>');
|
|
971
|
+
escaped = escaped.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
|
|
972
|
+
|
|
973
|
+
// Ordered lists
|
|
974
|
+
escaped = escaped.replace(/^\d+\.\s+(.*)$/gm, '<li>$1</li>');
|
|
975
|
+
|
|
976
|
+
// Tables
|
|
977
|
+
escaped = escaped.replace(/\|(.+)\|/g, function(match) {
|
|
978
|
+
var cells = match.split("|").slice(1, -1);
|
|
979
|
+
if (cells.every(function(c) { return /^[\-:]+$/.test(c.trim()); })) {
|
|
980
|
+
return "";
|
|
981
|
+
}
|
|
982
|
+
return '<tr>' + cells.map(function(c) { return '<td>' + c.trim() + '</td>'; }).join("") + '</tr>';
|
|
983
|
+
});
|
|
984
|
+
escaped = escaped.replace(/(<tr>.*<\/tr>\n?)+/g, '<table>$&</table>');
|
|
985
|
+
|
|
986
|
+
// Paragraphs
|
|
987
|
+
var paragraphs = escaped.split(/\n{2,}/);
|
|
988
|
+
escaped = paragraphs.map(function(p) {
|
|
989
|
+
p = p.trim();
|
|
990
|
+
if (!p) return "";
|
|
991
|
+
if (/^<(h[1-6]|ul|ol|li|blockquote|pre|table|hr|div)/.test(p)) return p;
|
|
992
|
+
return '<p>' + p.replace(/\n/g, "<br>") + '</p>';
|
|
993
|
+
}).join("\n");
|
|
994
|
+
|
|
995
|
+
return escaped;
|
|
996
|
+
}
|
|
997
|
+
|
|
693
998
|
function renderFolderPicker(state) {
|
|
694
999
|
var currentDir = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "/tmp");
|
|
695
1000
|
|
|
@@ -738,9 +1043,24 @@
|
|
|
738
1043
|
'</div>';
|
|
739
1044
|
}
|
|
740
1045
|
|
|
1046
|
+
function getSessionStatusLabel(session) {
|
|
1047
|
+
if (!session) return "";
|
|
1048
|
+
if (session.archived) return "已归档";
|
|
1049
|
+
if (session.permissionBlocked) return "等待授权";
|
|
1050
|
+
return session.status;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function getSessionStatusClass(session) {
|
|
1054
|
+
if (!session) return "";
|
|
1055
|
+
if (session.archived) return "archived";
|
|
1056
|
+
if (session.permissionBlocked) return "permission-blocked";
|
|
1057
|
+
return session.status || "";
|
|
1058
|
+
}
|
|
1059
|
+
|
|
741
1060
|
function renderSessionItem(session) {
|
|
742
1061
|
var activeClass = session.id === state.selectedId ? " active" : "";
|
|
743
|
-
var metaStatus = session
|
|
1062
|
+
var metaStatus = getSessionStatusLabel(session);
|
|
1063
|
+
var metaStatusClass = getSessionStatusClass(session);
|
|
744
1064
|
var modeName = session.mode === "full-access" ? "全权限" : session.mode === "default" ? "默认" : session.mode === "native" ? "原生" : session.mode === "auto-edit" ? "自动编辑" : session.mode;
|
|
745
1065
|
var deleteButton = '<button class="btn btn-ghost btn-sm session-action-btn" data-action="delete" data-session-id="' + session.id + '" type="button" aria-label="删除会话">×</button>';
|
|
746
1066
|
var resumeButton = "";
|
|
@@ -761,7 +1081,7 @@
|
|
|
761
1081
|
'<div class="session-command">' + escapeHtml(session.command) + '</div>' +
|
|
762
1082
|
'<div class="session-meta">' +
|
|
763
1083
|
'<span>' + escapeHtml(modeName) + '</span>' +
|
|
764
|
-
'<span class="session-status ' +
|
|
1084
|
+
'<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
|
|
765
1085
|
sessionIdDisplay +
|
|
766
1086
|
'</div>' +
|
|
767
1087
|
'</div>' +
|
|
@@ -770,45 +1090,50 @@
|
|
|
770
1090
|
'</div>';
|
|
771
1091
|
}
|
|
772
1092
|
|
|
1093
|
+
function renderModeCards(selectedMode) {
|
|
1094
|
+
var modes = [
|
|
1095
|
+
{ id: "default", label: "标准", desc: "逐步确认操作" },
|
|
1096
|
+
{ id: "full-access", label: "全权限", desc: "自动确认权限" },
|
|
1097
|
+
{ id: "auto-edit", label: "自动编辑", desc: "自动确认修改" },
|
|
1098
|
+
{ id: "native", label: "原生", desc: "结构化单轮输出" },
|
|
1099
|
+
{ id: "managed", label: "托管", desc: "全自动完成任务" }
|
|
1100
|
+
];
|
|
1101
|
+
return modes.map(function(m) {
|
|
1102
|
+
var active = m.id === selectedMode ? " active" : "";
|
|
1103
|
+
return '<button type="button" class="mode-card' + active + '" data-mode="' + m.id + '">' +
|
|
1104
|
+
'<span class="mode-card-label">' + m.label + '</span>' +
|
|
1105
|
+
'<span class="mode-card-desc">' + m.desc + '</span>' +
|
|
1106
|
+
'</button>';
|
|
1107
|
+
}).join("");
|
|
1108
|
+
}
|
|
1109
|
+
|
|
773
1110
|
function renderSessionModal() {
|
|
774
|
-
var modalTool =
|
|
1111
|
+
var modalTool = getPreferredTool();
|
|
775
1112
|
var modalMode = getSafeModeForTool(modalTool, state.modeValue || state.chatMode || "default");
|
|
776
|
-
var commandValue = state.commandValue || modalTool;
|
|
777
1113
|
return '<section id="session-modal" class="modal-backdrop hidden">' +
|
|
778
|
-
'<div class="modal">' +
|
|
1114
|
+
'<div class="modal session-modal">' +
|
|
779
1115
|
'<div class="modal-header">' +
|
|
780
|
-
'<
|
|
781
|
-
|
|
1116
|
+
'<div>' +
|
|
1117
|
+
'<h2 class="modal-title">新对话</h2>' +
|
|
1118
|
+
'<p class="modal-subtitle">启动 Claude 会话,选择模式和工作目录。</p>' +
|
|
1119
|
+
'</div>' +
|
|
1120
|
+
'<button id="close-modal-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
782
1121
|
'</div>' +
|
|
783
1122
|
'<div class="modal-body">' +
|
|
784
|
-
'<div class="field">' +
|
|
785
|
-
'<label class="field-label">工具</label>' +
|
|
786
|
-
'<div class="tool-picker" id="tool-picker">' +
|
|
787
|
-
'<button class="tool-card active" type="button" data-tool="claude">' +
|
|
788
|
-
'<div class="tool-card-title"><span>Claude</span><span class="tool-chip">推荐</span></div>' +
|
|
789
|
-
'<div class="tool-card-desc">适合长会话、恢复上下文,以及 Claude 原生单轮回复。</div>' +
|
|
790
|
-
'</button>' +
|
|
791
|
-
'</div>' +
|
|
792
|
-
'<p id="tool-description" class="field-hint">' + escapeHtml(getSessionToolDescription(modalTool)) + '</p>' +
|
|
793
|
-
'</div>' +
|
|
794
|
-
'<div class="field">' +
|
|
795
|
-
'<label class="field-label" for="mode">模式</label>' +
|
|
796
|
-
'<select id="mode" class="field-input">' +
|
|
797
|
-
renderModeOptions(modalTool, modalMode) +
|
|
798
|
-
'</select>' +
|
|
799
|
-
'<p id="mode-description" class="field-hint">' + escapeHtml(getToolModeHint(modalTool, modalMode)) + '</p>' +
|
|
800
|
-
'</div>' +
|
|
801
|
-
'<div class="field">' +
|
|
802
|
-
'<label class="field-label" for="command">命令</label>' +
|
|
803
|
-
'<textarea id="command" class="field-input" placeholder="claude 任意 CLI 命令" rows="2">' + escapeHtml(commandValue) + '</textarea>' +
|
|
804
|
-
'<span id="session-command-preview" class="command-preview">' + escapeHtml(commandValue) + '</span>' +
|
|
805
|
-
'</div>' +
|
|
806
1123
|
'<div class="field">' +
|
|
807
1124
|
'<label class="field-label" for="cwd">工作目录</label>' +
|
|
808
1125
|
'<div class="suggestions-wrap">' +
|
|
809
1126
|
'<input id="cwd" type="text" class="field-input" autocomplete="off" placeholder="留空则使用默认目录" />' +
|
|
810
1127
|
'<div id="cwd-suggestions" class="suggestions hidden"></div>' +
|
|
811
1128
|
'</div>' +
|
|
1129
|
+
'<p class="field-hint">会话将在此目录启动,支持路径自动补全。</p>' +
|
|
1130
|
+
'</div>' +
|
|
1131
|
+
'<div class="field">' +
|
|
1132
|
+
'<label class="field-label">模式</label>' +
|
|
1133
|
+
'<div id="mode-cards" class="mode-cards">' +
|
|
1134
|
+
renderModeCards(modalMode) +
|
|
1135
|
+
'</div>' +
|
|
1136
|
+
'<p id="mode-description" class="field-hint">' + escapeHtml(getToolModeHint(modalTool, modalMode)) + '</p>' +
|
|
812
1137
|
'</div>' +
|
|
813
1138
|
'<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
|
|
814
1139
|
'<p id="modal-error" class="error-message hidden"></p>' +
|
|
@@ -817,40 +1142,6 @@
|
|
|
817
1142
|
'</section>';
|
|
818
1143
|
}
|
|
819
1144
|
|
|
820
|
-
function renderWelcomeView() {
|
|
821
|
-
var defaultCmd = (state.config && state.config.commandPresets && state.config.commandPresets.length > 0)
|
|
822
|
-
? state.config.commandPresets[0].command
|
|
823
|
-
: "claude";
|
|
824
|
-
var presets = state.config && state.config.commandPresets ? state.config.commandPresets : [];
|
|
825
|
-
var cards = presets.slice(0, 2).map(function(p) {
|
|
826
|
-
var icon = p.command.indexOf("claude") !== -1 ? "🤖" : "⌨";
|
|
827
|
-
var desc = p.command.indexOf("claude") !== -1 ? "Anthropic 编程助手" : "CLI 工具";
|
|
828
|
-
return '<div class="quick-card" data-command="' + escapeHtml(p.command) + '">' +
|
|
829
|
-
'<div class="quick-card-icon">' + icon + '</div>' +
|
|
830
|
-
'<div class="quick-card-body">' +
|
|
831
|
-
'<div class="quick-card-title">' + escapeHtml(p.label || p.command) + '</div>' +
|
|
832
|
-
'<div class="quick-card-desc">' + desc + '</div>' +
|
|
833
|
-
'</div>' +
|
|
834
|
-
'</div>';
|
|
835
|
-
}).join("");
|
|
836
|
-
|
|
837
|
-
return '<div class="welcome-view">' +
|
|
838
|
-
'<div class="welcome-header">' +
|
|
839
|
-
'<div class="welcome-logo">W</div>' +
|
|
840
|
-
'<h1 class="welcome-title">Wand</h1>' +
|
|
841
|
-
'<p class="welcome-subtitle">你的本地 AI 编程助手</p>' +
|
|
842
|
-
'</div>' +
|
|
843
|
-
'<div class="quick-start-grid" id="quick-start-grid">' +
|
|
844
|
-
cards +
|
|
845
|
-
'</div>' +
|
|
846
|
-
'<div class="welcome-custom-row">' +
|
|
847
|
-
'<input id="welcome-custom-command" class="welcome-custom-input" placeholder="或输入任意命令..." />' +
|
|
848
|
-
'<button id="welcome-custom-start" class="btn btn-primary">启动</button>' +
|
|
849
|
-
'</div>' +
|
|
850
|
-
'<p class="welcome-hint">从右侧菜单可查看历史会话</p>' +
|
|
851
|
-
'</div>';
|
|
852
|
-
}
|
|
853
|
-
|
|
854
1145
|
// Global toggle function for tool card headers — called via onclick attribute
|
|
855
1146
|
window.__tcToggle = function(e, headerEl) {
|
|
856
1147
|
var card = headerEl.closest(".tool-use-card");
|
|
@@ -951,10 +1242,27 @@
|
|
|
951
1242
|
if (loginButton) {
|
|
952
1243
|
loginButton.addEventListener("click", login);
|
|
953
1244
|
var passwordEl = document.getElementById("password");
|
|
1245
|
+
var togglePasswordButton = document.getElementById("toggle-password-button");
|
|
1246
|
+
if (togglePasswordButton && passwordEl) {
|
|
1247
|
+
togglePasswordButton.addEventListener("click", function() {
|
|
1248
|
+
var visible = passwordEl.type === "text";
|
|
1249
|
+
passwordEl.type = visible ? "password" : "text";
|
|
1250
|
+
togglePasswordButton.textContent = visible ? "显示" : "隐藏";
|
|
1251
|
+
togglePasswordButton.setAttribute("aria-label", visible ? "显示密码" : "隐藏密码");
|
|
1252
|
+
togglePasswordButton.setAttribute("aria-pressed", visible ? "false" : "true");
|
|
1253
|
+
passwordEl.focus();
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
954
1256
|
if (passwordEl) {
|
|
955
1257
|
passwordEl.addEventListener("keydown", function(e) {
|
|
956
1258
|
if (e.key === "Enter") login();
|
|
957
1259
|
});
|
|
1260
|
+
passwordEl.addEventListener("input", function() {
|
|
1261
|
+
passwordEl.dataset.error = "false";
|
|
1262
|
+
passwordEl.setAttribute("aria-invalid", "false");
|
|
1263
|
+
var errorEl = document.getElementById("login-error");
|
|
1264
|
+
if (errorEl) hideError(errorEl);
|
|
1265
|
+
});
|
|
958
1266
|
passwordEl.focus();
|
|
959
1267
|
}
|
|
960
1268
|
return;
|
|
@@ -980,7 +1288,7 @@
|
|
|
980
1288
|
var welcomeClaudeBtn = document.getElementById("welcome-tool-claude");
|
|
981
1289
|
if (welcomeClaudeBtn) {
|
|
982
1290
|
welcomeClaudeBtn.addEventListener("click", function() {
|
|
983
|
-
quickStartSession(
|
|
1291
|
+
quickStartSession();
|
|
984
1292
|
});
|
|
985
1293
|
}
|
|
986
1294
|
var welcomeFolderBtn = document.getElementById("welcome-tool-folder");
|
|
@@ -994,33 +1302,15 @@
|
|
|
994
1302
|
sessionsList.addEventListener("keydown", handleSessionItemKeydown);
|
|
995
1303
|
}
|
|
996
1304
|
|
|
997
|
-
var
|
|
998
|
-
if (
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
state.modeValue =
|
|
1305
|
+
var modeCardsEl = document.getElementById("mode-cards");
|
|
1306
|
+
if (modeCardsEl) modeCardsEl.addEventListener("click", function(e) {
|
|
1307
|
+
var card = e.target.closest(".mode-card");
|
|
1308
|
+
if (!card) return;
|
|
1309
|
+
var mode = card.getAttribute("data-mode");
|
|
1310
|
+
if (mode) {
|
|
1311
|
+
state.modeValue = mode;
|
|
1312
|
+
syncSessionModalUI();
|
|
1004
1313
|
}
|
|
1005
|
-
syncSessionModalUI();
|
|
1006
|
-
});
|
|
1007
|
-
var modalModeEl = document.getElementById("mode");
|
|
1008
|
-
if (modalModeEl) modalModeEl.addEventListener("change", function() {
|
|
1009
|
-
state.modeValue = this.value;
|
|
1010
|
-
syncSessionModalUI();
|
|
1011
|
-
});
|
|
1012
|
-
var toolPicker = document.getElementById("tool-picker");
|
|
1013
|
-
if (toolPicker) toolPicker.addEventListener("click", function(e) {
|
|
1014
|
-
var target = e.target;
|
|
1015
|
-
var card = target && target.closest ? target.closest(".tool-card") : null;
|
|
1016
|
-
if (!card || !card.dataset.tool) return;
|
|
1017
|
-
var nextTool = card.dataset.tool;
|
|
1018
|
-
state.sessionTool = nextTool;
|
|
1019
|
-
state.modeValue = getSafeModeForTool(nextTool, state.modeValue || state.chatMode);
|
|
1020
|
-
state.commandValue = replaceCommandBase(state.commandValue || nextTool, nextTool);
|
|
1021
|
-
var commandField = document.getElementById("command");
|
|
1022
|
-
if (commandField) commandField.value = state.commandValue;
|
|
1023
|
-
syncSessionModalUI();
|
|
1024
1314
|
});
|
|
1025
1315
|
var cwdEl = document.getElementById("cwd");
|
|
1026
1316
|
if (cwdEl) {
|
|
@@ -1056,6 +1346,10 @@
|
|
|
1056
1346
|
if (closeModalBtn) closeModalBtn.addEventListener("click", closeSessionModal);
|
|
1057
1347
|
var runBtn = document.getElementById("run-button");
|
|
1058
1348
|
if (runBtn) runBtn.addEventListener("click", runCommand);
|
|
1349
|
+
var approvePermissionBtn = document.getElementById("approve-permission-btn");
|
|
1350
|
+
if (approvePermissionBtn) approvePermissionBtn.addEventListener("click", approvePermission);
|
|
1351
|
+
var denyPermissionBtn = document.getElementById("deny-permission-btn");
|
|
1352
|
+
if (denyPermissionBtn) denyPermissionBtn.addEventListener("click", denyPermission);
|
|
1059
1353
|
var sendBtn = document.getElementById("send-input-button");
|
|
1060
1354
|
if (sendBtn) sendBtn.addEventListener("click", function() {
|
|
1061
1355
|
closeSessionsDrawer();
|
|
@@ -1074,56 +1368,63 @@
|
|
|
1074
1368
|
if (e.target.id === "session-modal") closeSessionModal();
|
|
1075
1369
|
});
|
|
1076
1370
|
|
|
1077
|
-
// Welcome view quick-start cards
|
|
1078
|
-
var quickGrid = document.getElementById("quick-start-grid");
|
|
1079
|
-
if (quickGrid) {
|
|
1080
|
-
quickGrid.addEventListener("click", function(e) {
|
|
1081
|
-
var target = e.target;
|
|
1082
|
-
var card = target.closest(".quick-card");
|
|
1083
|
-
if (!card) return;
|
|
1084
|
-
var cmd = card.dataset && card.dataset.command || "claude";
|
|
1085
|
-
quickStartSession(cmd);
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// Welcome view custom command button
|
|
1090
|
-
var customStartBtn = document.getElementById("welcome-custom-start");
|
|
1091
|
-
if (customStartBtn) {
|
|
1092
|
-
customStartBtn.addEventListener("click", function() {
|
|
1093
|
-
var inputEl = document.getElementById("welcome-custom-command");
|
|
1094
|
-
if (inputEl && inputEl.value.trim()) {
|
|
1095
|
-
quickStartSession(inputEl.value.trim());
|
|
1096
|
-
}
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
var customInput = document.getElementById("welcome-custom-command");
|
|
1100
|
-
if (customInput) {
|
|
1101
|
-
customInput.addEventListener("keydown", function(e) {
|
|
1102
|
-
if (e.key === "Enter") {
|
|
1103
|
-
var inputEl = e.target;
|
|
1104
|
-
if (inputEl.value.trim()) quickStartSession(inputEl.value.trim());
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
1371
|
var inputBox = document.getElementById("input-box");
|
|
1110
1372
|
if (inputBox) {
|
|
1373
|
+
bindInputTouchScroll(inputBox);
|
|
1111
1374
|
inputBox.addEventListener("keydown", handleInputBoxKeydown);
|
|
1112
1375
|
inputBox.addEventListener("paste", handleInputPaste);
|
|
1113
1376
|
inputBox.addEventListener("input", function() {
|
|
1114
|
-
|
|
1377
|
+
refreshInputBoxState(inputBox);
|
|
1378
|
+
setDraftValue(inputBox.value);
|
|
1115
1379
|
});
|
|
1116
1380
|
inputBox.addEventListener("focus", function() {
|
|
1117
1381
|
// Close drawer when user focuses input to avoid backdrop blocking clicks
|
|
1118
1382
|
closeSessionsDrawer();
|
|
1383
|
+
handleInputBoxFocus({ target: inputBox });
|
|
1119
1384
|
});
|
|
1385
|
+
inputBox.addEventListener("blur", handleInputBoxBlur);
|
|
1120
1386
|
}
|
|
1121
1387
|
|
|
1122
1388
|
// View toggle handlers
|
|
1123
1389
|
var viewTermBtn = document.getElementById("view-terminal-btn");
|
|
1124
1390
|
if (viewTermBtn) viewTermBtn.addEventListener("click", function() { setView("terminal"); });
|
|
1125
|
-
|
|
1126
|
-
|
|
1391
|
+
// Terminal interactive toggle (both topbar and terminal-header)
|
|
1392
|
+
var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
|
|
1393
|
+
terminalInteractiveToggles.forEach(function(id) {
|
|
1394
|
+
var toggle = document.getElementById(id);
|
|
1395
|
+
if (toggle) toggle.addEventListener("click", toggleTerminalInteractive);
|
|
1396
|
+
});
|
|
1397
|
+
// Keyboard popup handlers
|
|
1398
|
+
var keyboardToggle = document.getElementById("keyboard-toggle");
|
|
1399
|
+
if (keyboardToggle) keyboardToggle.addEventListener("click", handleKeyboardToggle);
|
|
1400
|
+
var keyboardPopup = document.getElementById("keyboard-popup");
|
|
1401
|
+
if (keyboardPopup) keyboardPopup.addEventListener("click", handleInlineKeyboardClick);
|
|
1402
|
+
// Close popup when clicking outside
|
|
1403
|
+
document.addEventListener("click", function(event) {
|
|
1404
|
+
var toggle = document.getElementById("keyboard-toggle");
|
|
1405
|
+
var popup = document.getElementById("keyboard-popup");
|
|
1406
|
+
var target = event.target;
|
|
1407
|
+
if (!popup || popup.classList.contains("hidden") || !target) return;
|
|
1408
|
+
var clickedPopup = popup.contains(target);
|
|
1409
|
+
var clickedToggle = !!toggle && toggle.contains(target);
|
|
1410
|
+
if (!clickedPopup && !clickedToggle) {
|
|
1411
|
+
closeKeyboardPopup();
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// PWA install button
|
|
1416
|
+
var pwaInstallBtn = document.getElementById("pwa-install-button");
|
|
1417
|
+
if (pwaInstallBtn) {
|
|
1418
|
+
pwaInstallBtn.addEventListener("click", function() {
|
|
1419
|
+
if (!state.deferredPrompt) return;
|
|
1420
|
+
state.deferredPrompt.prompt();
|
|
1421
|
+
state.deferredPrompt.userChoice.then(function() {
|
|
1422
|
+
state.deferredPrompt = null;
|
|
1423
|
+
state.showInstallPrompt = false;
|
|
1424
|
+
updateInstallPrompt();
|
|
1425
|
+
});
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1127
1428
|
|
|
1128
1429
|
// File panel toggle
|
|
1129
1430
|
var filePanelToggle = document.getElementById("file-panel-toggle-btn");
|
|
@@ -1131,6 +1432,16 @@
|
|
|
1131
1432
|
var filePanelClose = document.getElementById("file-side-panel-close");
|
|
1132
1433
|
if (filePanelClose) filePanelClose.addEventListener("click", closeFilePanel);
|
|
1133
1434
|
|
|
1435
|
+
// File panel backdrop click to close (mobile)
|
|
1436
|
+
var filePanelBackdrop = document.getElementById("file-panel-backdrop");
|
|
1437
|
+
if (filePanelBackdrop) filePanelBackdrop.addEventListener("click", closeFilePanel);
|
|
1438
|
+
|
|
1439
|
+
// Terminal scale controls (topbar)
|
|
1440
|
+
var scaleDownBtn = document.getElementById("terminal-scale-down-top");
|
|
1441
|
+
var scaleUpBtn = document.getElementById("terminal-scale-up-top");
|
|
1442
|
+
if (scaleDownBtn) scaleDownBtn.addEventListener("click", function() { adjustTerminalScale(-0.25); });
|
|
1443
|
+
if (scaleUpBtn) scaleUpBtn.addEventListener("click", function() { adjustTerminalScale(0.25); });
|
|
1444
|
+
|
|
1134
1445
|
// File explorer
|
|
1135
1446
|
var fileRefresh = document.getElementById("file-explorer-refresh");
|
|
1136
1447
|
if (fileRefresh) fileRefresh.addEventListener("click", refreshFileExplorer);
|
|
@@ -1292,6 +1603,7 @@
|
|
|
1292
1603
|
});
|
|
1293
1604
|
}
|
|
1294
1605
|
|
|
1606
|
+
|
|
1295
1607
|
// Drag and drop support
|
|
1296
1608
|
var folderPickerContainer = document.querySelector(".folder-picker-compact");
|
|
1297
1609
|
if (folderPickerContainer) {
|
|
@@ -1602,11 +1914,14 @@
|
|
|
1602
1914
|
cols: 120,
|
|
1603
1915
|
rows: 36,
|
|
1604
1916
|
convertEol: false,
|
|
1605
|
-
disableStdin:
|
|
1917
|
+
disableStdin: false,
|
|
1606
1918
|
cursorBlink: false,
|
|
1607
1919
|
fontFamily: '"Geist Mono", "SF Mono", monospace',
|
|
1608
1920
|
fontSize: 13,
|
|
1609
1921
|
lineHeight: 1.5,
|
|
1922
|
+
allowProposedApi: true,
|
|
1923
|
+
scrollback: 10000,
|
|
1924
|
+
wheelScrollMargin: 0,
|
|
1610
1925
|
theme: {
|
|
1611
1926
|
background: "#1f1b17",
|
|
1612
1927
|
foreground: "#f5eadc",
|
|
@@ -1635,6 +1950,7 @@
|
|
|
1635
1950
|
state.terminal.loadAddon(state.fitAddon);
|
|
1636
1951
|
|
|
1637
1952
|
state.terminal.open(container);
|
|
1953
|
+
applyTerminalScale();
|
|
1638
1954
|
state.fitAddon.fit();
|
|
1639
1955
|
|
|
1640
1956
|
if (state.selectedId) {
|
|
@@ -1648,8 +1964,22 @@
|
|
|
1648
1964
|
state.terminal.writeln("点击上方「新对话」开始你的第一次对话。");
|
|
1649
1965
|
}
|
|
1650
1966
|
|
|
1651
|
-
state.terminal.onData(function(data) {
|
|
1967
|
+
state.terminal.onData(function(data) {
|
|
1968
|
+
if (state.terminalInteractive) return;
|
|
1969
|
+
queueDirectInput(data);
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
// 鼠标滚轮支持 - 在终端容器上滚动
|
|
1973
|
+
container.addEventListener('wheel', function(e) {
|
|
1974
|
+
// 总是允许滚动,让 xterm 处理滚轮事件
|
|
1975
|
+
e.stopPropagation();
|
|
1976
|
+
}, { passive: true });
|
|
1977
|
+
|
|
1652
1978
|
container.addEventListener("click", focusInputBox);
|
|
1979
|
+
|
|
1980
|
+
// 初始化拖动调整大小
|
|
1981
|
+
initTerminalResizeHandle();
|
|
1982
|
+
|
|
1653
1983
|
observeTerminalResize();
|
|
1654
1984
|
}
|
|
1655
1985
|
|
|
@@ -1659,8 +1989,11 @@
|
|
|
1659
1989
|
var passwordEl = document.getElementById("password");
|
|
1660
1990
|
var loginButton = document.getElementById("login-button");
|
|
1661
1991
|
var errorEl = document.getElementById("login-error");
|
|
1992
|
+
if (!passwordEl || !loginButton || !errorEl) return;
|
|
1662
1993
|
|
|
1663
1994
|
hideError(errorEl);
|
|
1995
|
+
passwordEl.dataset.error = "false";
|
|
1996
|
+
passwordEl.setAttribute("aria-invalid", "false");
|
|
1664
1997
|
state.loginPending = true;
|
|
1665
1998
|
loginButton.disabled = true;
|
|
1666
1999
|
loginButton.textContent = "登录中...";
|
|
@@ -1673,6 +2006,8 @@
|
|
|
1673
2006
|
})
|
|
1674
2007
|
.then(function(res) {
|
|
1675
2008
|
if (!res.ok) {
|
|
2009
|
+
passwordEl.dataset.error = "true";
|
|
2010
|
+
passwordEl.setAttribute("aria-invalid", "true");
|
|
1676
2011
|
showError(errorEl, "密码错误,请重试。");
|
|
1677
2012
|
return Promise.reject("Invalid password");
|
|
1678
2013
|
}
|
|
@@ -1694,6 +2029,8 @@
|
|
|
1694
2029
|
.catch(function(error) {
|
|
1695
2030
|
console.error("[wand] Login error:", error);
|
|
1696
2031
|
if (error !== "Invalid password") {
|
|
2032
|
+
passwordEl.dataset.error = "true";
|
|
2033
|
+
passwordEl.setAttribute("aria-invalid", "true");
|
|
1697
2034
|
showError(errorEl, "登录失败,请重试。");
|
|
1698
2035
|
}
|
|
1699
2036
|
})
|
|
@@ -1707,6 +2044,8 @@
|
|
|
1707
2044
|
function logout() {
|
|
1708
2045
|
fetch("/api/logout", { method: "POST", credentials: "same-origin" }).catch(function() {});
|
|
1709
2046
|
stopPolling();
|
|
2047
|
+
setTerminalInteractive(false);
|
|
2048
|
+
hideMiniKeyboard();
|
|
1710
2049
|
teardownTerminal();
|
|
1711
2050
|
state.config = null;
|
|
1712
2051
|
state.selectedId = null;
|
|
@@ -1736,32 +2075,17 @@
|
|
|
1736
2075
|
: mode;
|
|
1737
2076
|
}
|
|
1738
2077
|
|
|
1739
|
-
function inferToolFromCommand(command) {
|
|
1740
|
-
var base = String(command || "").trim().split(/\s+/)[0] || "";
|
|
1741
|
-
if (base === "claude") return "claude";
|
|
1742
|
-
return "custom";
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
2078
|
function getPreferredTool() {
|
|
1746
2079
|
return "claude";
|
|
1747
2080
|
}
|
|
1748
2081
|
|
|
1749
2082
|
function getComposerTool() {
|
|
1750
|
-
|
|
1751
|
-
var selectedTool = inferToolFromCommand(selectedSession && selectedSession.command ? selectedSession.command : "");
|
|
1752
|
-
if (selectedTool === "claude") {
|
|
1753
|
-
return selectedTool;
|
|
1754
|
-
}
|
|
1755
|
-
return getPreferredTool();
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
function getSessionToolDescription(tool) {
|
|
1759
|
-
return "适合持续对话、恢复上下文,也支持原生单轮回复模式。";
|
|
2083
|
+
return "claude";
|
|
1760
2084
|
}
|
|
1761
2085
|
|
|
1762
2086
|
function getToolModeHint(tool, mode) {
|
|
1763
2087
|
if (mode === "full-access") {
|
|
1764
|
-
return "
|
|
2088
|
+
return "自动确认权限请求与高权限操作,适合你确认环境安全后的连续修改。";
|
|
1765
2089
|
}
|
|
1766
2090
|
if (mode === "auto-edit") {
|
|
1767
2091
|
return "保留交互式会话,同时更偏向直接编辑代码。";
|
|
@@ -1799,7 +2123,7 @@
|
|
|
1799
2123
|
function getModeHint(mode) {
|
|
1800
2124
|
var hints = {
|
|
1801
2125
|
'default': '标准模式 - 需要确认文件修改',
|
|
1802
|
-
'full-access': '完全访问 -
|
|
2126
|
+
'full-access': '完全访问 - 自动确认权限与操作',
|
|
1803
2127
|
'auto-edit': '自动编辑 - 自动确认文件修改',
|
|
1804
2128
|
'native': '原生模式 - 返回结构化输出',
|
|
1805
2129
|
'managed': '托管模式 - AI 自动完成所有工作'
|
|
@@ -1807,22 +2131,12 @@
|
|
|
1807
2131
|
return hints[mode] || '';
|
|
1808
2132
|
}
|
|
1809
2133
|
|
|
1810
|
-
function replaceCommandBase(command, nextBase) {
|
|
1811
|
-
var trimmed = String(command || "").trim();
|
|
1812
|
-
if (!trimmed) return nextBase;
|
|
1813
|
-
var parts = trimmed.split(/\s+/);
|
|
1814
|
-
parts[0] = nextBase;
|
|
1815
|
-
return parts.join(" ");
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
2134
|
function syncComposerModeSelect() {
|
|
1819
2135
|
var select = document.getElementById("chat-mode-select");
|
|
1820
2136
|
if (!select) return;
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
select.innerHTML = renderModeOptions(tool, state.chatMode);
|
|
2137
|
+
state.chatMode = getSafeModeForTool("claude", state.chatMode);
|
|
2138
|
+
select.innerHTML = renderModeOptions("claude", state.chatMode);
|
|
1824
2139
|
select.value = state.chatMode;
|
|
1825
|
-
// 更新模式提示
|
|
1826
2140
|
var modeHint = document.getElementById("mode-hint");
|
|
1827
2141
|
if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
|
|
1828
2142
|
}
|
|
@@ -1830,61 +2144,53 @@
|
|
|
1830
2144
|
function applyCurrentView() {
|
|
1831
2145
|
var hasSession = !!state.selectedId;
|
|
1832
2146
|
var terminalBtn = document.getElementById("view-terminal-btn");
|
|
1833
|
-
var chatBtn = document.getElementById("view-chat-btn");
|
|
1834
2147
|
var terminalContainer = document.getElementById("output");
|
|
1835
2148
|
var chatContainer = document.getElementById("chat-output");
|
|
1836
2149
|
|
|
1837
|
-
if (terminalBtn) terminalBtn.classList.
|
|
1838
|
-
if (
|
|
1839
|
-
if (
|
|
1840
|
-
|
|
2150
|
+
if (terminalBtn) terminalBtn.classList.add("active");
|
|
2151
|
+
if (terminalContainer) terminalContainer.classList.toggle("active", hasSession);
|
|
2152
|
+
if (chatContainer) {
|
|
2153
|
+
chatContainer.classList.remove("active");
|
|
2154
|
+
chatContainer.classList.add("hidden");
|
|
2155
|
+
}
|
|
2156
|
+
updateInteractiveControls();
|
|
1841
2157
|
}
|
|
1842
2158
|
|
|
1843
2159
|
function syncSessionModalUI() {
|
|
1844
|
-
var commandEl = document.getElementById("command");
|
|
1845
|
-
var modeEl = document.getElementById("mode");
|
|
1846
|
-
var toolHint = document.getElementById("tool-description");
|
|
1847
2160
|
var modeHint = document.getElementById("mode-description");
|
|
1848
|
-
var previewEl = document.getElementById("session-command-preview");
|
|
1849
2161
|
var tool = "claude";
|
|
1850
2162
|
|
|
1851
2163
|
state.sessionTool = tool;
|
|
1852
2164
|
state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
|
|
1853
2165
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
commandEl.value = tool;
|
|
1861
|
-
state.commandValue = tool;
|
|
1862
|
-
}
|
|
1863
|
-
commandEl.placeholder = "claude --model sonnet";
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
if (modeEl) {
|
|
1867
|
-
modeEl.innerHTML = renderModeOptions(tool, state.modeValue);
|
|
1868
|
-
modeEl.value = state.modeValue;
|
|
2166
|
+
// Update mode cards active state
|
|
2167
|
+
var modeCards = document.querySelectorAll("#mode-cards .mode-card");
|
|
2168
|
+
if (modeCards.length) {
|
|
2169
|
+
modeCards.forEach(function(card) {
|
|
2170
|
+
card.classList.toggle("active", card.getAttribute("data-mode") === state.modeValue);
|
|
2171
|
+
});
|
|
1869
2172
|
}
|
|
1870
2173
|
|
|
1871
|
-
if (toolHint) toolHint.textContent = getSessionToolDescription(tool);
|
|
1872
2174
|
if (modeHint) modeHint.textContent = getToolModeHint(tool, state.modeValue);
|
|
1873
|
-
if (previewEl) previewEl.textContent = (commandEl && commandEl.value.trim()) || tool;
|
|
1874
2175
|
}
|
|
1875
2176
|
|
|
1876
2177
|
function updateSessionSnapshot(snapshot) {
|
|
1877
2178
|
if (!snapshot || !snapshot.id) return;
|
|
1878
2179
|
var updated = false;
|
|
2180
|
+
var prevSession = null;
|
|
1879
2181
|
state.sessions = state.sessions.map(function(session) {
|
|
1880
2182
|
if (session.id !== snapshot.id) return session;
|
|
2183
|
+
prevSession = session;
|
|
1881
2184
|
updated = true;
|
|
1882
|
-
// Merge snapshot fields into existing session to preserve all fields
|
|
1883
2185
|
return Object.assign({}, session, snapshot);
|
|
1884
2186
|
});
|
|
1885
2187
|
if (!updated) {
|
|
1886
2188
|
state.sessions.unshift(snapshot);
|
|
1887
2189
|
}
|
|
2190
|
+
if (snapshot.id === state.selectedId) {
|
|
2191
|
+
reconcileInteractiveState();
|
|
2192
|
+
updateTaskDisplay();
|
|
2193
|
+
}
|
|
1888
2194
|
}
|
|
1889
2195
|
|
|
1890
2196
|
function getPreferredSessionId(sessions) {
|
|
@@ -1961,6 +2267,11 @@
|
|
|
1961
2267
|
|
|
1962
2268
|
function updateShellChrome() {
|
|
1963
2269
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
2270
|
+
if (!selectedSession) {
|
|
2271
|
+
setTerminalInteractive(false);
|
|
2272
|
+
hideMiniKeyboard();
|
|
2273
|
+
closeKeyboardPopup();
|
|
2274
|
+
}
|
|
1964
2275
|
var terminalTitle = selectedSession ? shortCommand(selectedSession.command) : "Wand";
|
|
1965
2276
|
var summaryEl = document.querySelector(".session-summary-value");
|
|
1966
2277
|
var titleEl = document.getElementById("terminal-title");
|
|
@@ -1973,9 +2284,19 @@
|
|
|
1973
2284
|
if (summaryEl) summaryEl.textContent = terminalTitle;
|
|
1974
2285
|
if (titleEl) titleEl.textContent = terminalTitle;
|
|
1975
2286
|
if (infoEl) {
|
|
1976
|
-
infoEl.textContent = selectedSession ? (
|
|
2287
|
+
infoEl.textContent = selectedSession ? getSessionStatusLabel(selectedSession) : "开始对话";
|
|
1977
2288
|
}
|
|
1978
2289
|
|
|
2290
|
+
// Update session info bar at bottom
|
|
2291
|
+
var cwdEl = document.getElementById("session-cwd-display");
|
|
2292
|
+
var modeEl = document.getElementById("session-mode-display");
|
|
2293
|
+
var statusEl = document.getElementById("session-status-display");
|
|
2294
|
+
var exitEl = document.getElementById("session-exit-display");
|
|
2295
|
+
if (cwdEl) cwdEl.textContent = selectedSession && selectedSession.cwd ? escapeHtml(selectedSession.cwd) : '未设置目录';
|
|
2296
|
+
if (modeEl) modeEl.textContent = selectedSession ? getModeLabel(selectedSession.mode) : '默认';
|
|
2297
|
+
if (statusEl) statusEl.textContent = selectedSession ? getSessionStatusLabel(selectedSession) : '-';
|
|
2298
|
+
if (exitEl) exitEl.textContent = 'exit=' + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : 'n/a');
|
|
2299
|
+
|
|
1979
2300
|
var inputPanel = document.querySelector(".input-panel");
|
|
1980
2301
|
if (selectedSession) {
|
|
1981
2302
|
if (blankChat) blankChat.classList.add("hidden");
|
|
@@ -1992,6 +2313,7 @@
|
|
|
1992
2313
|
}
|
|
1993
2314
|
syncComposerModeSelect();
|
|
1994
2315
|
applyCurrentView();
|
|
2316
|
+
reconcileInteractiveState();
|
|
1995
2317
|
}
|
|
1996
2318
|
|
|
1997
2319
|
function loadOutput(id) {
|
|
@@ -2005,18 +2327,9 @@
|
|
|
2005
2327
|
.then(function(data) {
|
|
2006
2328
|
updateSessionSnapshot(data);
|
|
2007
2329
|
updateShellChrome();
|
|
2008
|
-
var terminalInfo = document.getElementById("terminal-info");
|
|
2009
|
-
if (terminalInfo) {
|
|
2010
|
-
terminalInfo.textContent = data.cwd + " | " + getModeLabel(data.mode) + " | " + data.status + " | exit=" + (data.exitCode ?? "n/a");
|
|
2011
|
-
}
|
|
2012
2330
|
|
|
2013
|
-
// Use structured messages if available (JSON chat mode), otherwise parse from PTY output
|
|
2014
2331
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
2015
|
-
|
|
2016
|
-
state.currentMessages = selectedSession.messages;
|
|
2017
|
-
} else {
|
|
2018
|
-
state.currentMessages = parseMessages(selectedSession ? selectedSession.output : "", selectedSession ? selectedSession.command : "");
|
|
2019
|
-
}
|
|
2332
|
+
state.currentMessages = [];
|
|
2020
2333
|
|
|
2021
2334
|
if (state.terminal) {
|
|
2022
2335
|
if (state.terminalSessionId !== id) {
|
|
@@ -2033,9 +2346,10 @@
|
|
|
2033
2346
|
state.terminalSessionId = id;
|
|
2034
2347
|
state.terminalOutput = newOutput;
|
|
2035
2348
|
state.terminal.scrollToBottom();
|
|
2349
|
+
scheduleTerminalResize();
|
|
2036
2350
|
}
|
|
2037
2351
|
|
|
2038
|
-
renderChat(
|
|
2352
|
+
renderChat(false);
|
|
2039
2353
|
});
|
|
2040
2354
|
}
|
|
2041
2355
|
|
|
@@ -2051,10 +2365,10 @@
|
|
|
2051
2365
|
var todoEl = document.getElementById("todo-progress");
|
|
2052
2366
|
if (todoEl) todoEl.classList.add("hidden");
|
|
2053
2367
|
var session = state.sessions.find(function(item) { return item.id === id; });
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2368
|
+
state.preferredCommand = getPreferredTool();
|
|
2369
|
+
state.chatMode = getSafeModeForTool("claude", session && session.mode ? session.mode : state.chatMode);
|
|
2370
|
+
if (state.terminalInteractive && session && session.status !== "running") {
|
|
2371
|
+
setTerminalInteractive(false);
|
|
2058
2372
|
}
|
|
2059
2373
|
updateSessionsList();
|
|
2060
2374
|
switchToSessionView(id);
|
|
@@ -2087,13 +2401,19 @@
|
|
|
2087
2401
|
|
|
2088
2402
|
function toggleSessionsDrawer() {
|
|
2089
2403
|
state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
|
|
2090
|
-
|
|
2404
|
+
if (state.sessionsDrawerOpen && isMobileLayout()) {
|
|
2405
|
+
state.filePanelOpen = false;
|
|
2406
|
+
try {
|
|
2407
|
+
localStorage.setItem("wand-file-panel-open", "false");
|
|
2408
|
+
} catch (e) {}
|
|
2409
|
+
}
|
|
2410
|
+
updateLayoutState();
|
|
2091
2411
|
}
|
|
2092
2412
|
|
|
2093
2413
|
function closeSessionsDrawer() {
|
|
2094
2414
|
if (!state.sessionsDrawerOpen) return;
|
|
2095
2415
|
state.sessionsDrawerOpen = false;
|
|
2096
|
-
|
|
2416
|
+
updateLayoutState();
|
|
2097
2417
|
}
|
|
2098
2418
|
|
|
2099
2419
|
// Store last focused element for focus trap
|
|
@@ -2107,18 +2427,14 @@
|
|
|
2107
2427
|
var modal = document.getElementById("session-modal");
|
|
2108
2428
|
if (modal) {
|
|
2109
2429
|
modal.classList.remove("hidden");
|
|
2110
|
-
// Store last focused element to restore on close
|
|
2111
2430
|
lastFocusedElement = document.activeElement;
|
|
2112
|
-
|
|
2113
|
-
var defaultTool = getPreferredTool();
|
|
2114
|
-
var fallbackCommand = state.commandValue || state.preferredCommand || defaultTool;
|
|
2115
|
-
state.sessionTool = defaultTool;
|
|
2116
|
-
state.commandValue = fallbackCommand || state.sessionTool;
|
|
2431
|
+
state.sessionTool = getPreferredTool();
|
|
2117
2432
|
state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
|
|
2118
|
-
if (commandEl) commandEl.value = state.commandValue;
|
|
2119
2433
|
syncSessionModalUI();
|
|
2120
|
-
setTimeout(function() {
|
|
2121
|
-
|
|
2434
|
+
setTimeout(function() {
|
|
2435
|
+
var modeCardsEl = document.getElementById("mode-cards");
|
|
2436
|
+
if (modeCardsEl) modeCardsEl.focus();
|
|
2437
|
+
}, 20);
|
|
2122
2438
|
setupFocusTrap(modal);
|
|
2123
2439
|
}
|
|
2124
2440
|
}
|
|
@@ -2251,43 +2567,12 @@
|
|
|
2251
2567
|
});
|
|
2252
2568
|
}
|
|
2253
2569
|
|
|
2254
|
-
function
|
|
2255
|
-
var
|
|
2256
|
-
if (!select || !state.config) return;
|
|
2257
|
-
|
|
2258
|
-
select.innerHTML = '<option value="">Custom command</option>';
|
|
2259
|
-
(state.config.commandPresets || []).forEach(function(preset, i) {
|
|
2260
|
-
var opt = document.createElement("option");
|
|
2261
|
-
opt.value = String(i);
|
|
2262
|
-
opt.textContent = preset.label + " — " + preset.command;
|
|
2263
|
-
select.appendChild(opt);
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
function applyPreset() {
|
|
2268
|
-
var select = document.getElementById("preset-select");
|
|
2269
|
-
var commandEl = document.getElementById("command");
|
|
2270
|
-
var modeEl = document.getElementById("mode");
|
|
2271
|
-
|
|
2272
|
-
if (!select || !commandEl || !state.config || select.value === "") return;
|
|
2273
|
-
|
|
2274
|
-
var preset = state.config.commandPresets[Number(select.value)];
|
|
2275
|
-
if (!preset) return;
|
|
2276
|
-
|
|
2277
|
-
commandEl.value = preset.command;
|
|
2278
|
-
modeEl.value = preset.mode || state.config.defaultMode || "default";
|
|
2279
|
-
state.commandValue = commandEl.value;
|
|
2280
|
-
state.modeValue = modeEl.value;
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
function quickStartSession(command) {
|
|
2570
|
+
function quickStartSession() {
|
|
2571
|
+
var command = getPreferredTool();
|
|
2284
2572
|
var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
|
|
2285
2573
|
var defaultMode = (state.config && state.config.defaultMode) ? state.config.defaultMode : "default";
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
state.preferredCommand = inferredTool;
|
|
2289
|
-
state.chatMode = getSafeModeForTool(inferredTool, state.chatMode);
|
|
2290
|
-
}
|
|
2574
|
+
state.preferredCommand = command;
|
|
2575
|
+
state.chatMode = getSafeModeForTool(command, state.chatMode);
|
|
2291
2576
|
fetch("/api/commands", {
|
|
2292
2577
|
method: "POST",
|
|
2293
2578
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2310,31 +2595,23 @@
|
|
|
2310
2595
|
})
|
|
2311
2596
|
.then(function() { focusInputBox(true); })
|
|
2312
2597
|
.catch(function() {
|
|
2313
|
-
showToast("
|
|
2598
|
+
showToast("无法启动会话。", "error");
|
|
2314
2599
|
});
|
|
2315
2600
|
}
|
|
2316
2601
|
|
|
2317
2602
|
function runCommand() {
|
|
2318
|
-
var commandEl = document.getElementById("command");
|
|
2319
2603
|
var cwdEl = document.getElementById("cwd");
|
|
2320
|
-
var modeEl = document.getElementById("mode");
|
|
2321
2604
|
var errorEl = document.getElementById("modal-error");
|
|
2605
|
+
var command = getPreferredTool();
|
|
2322
2606
|
|
|
2323
2607
|
hideError(errorEl);
|
|
2324
2608
|
|
|
2325
|
-
var command = commandEl.value.trim();
|
|
2326
|
-
if (!command) {
|
|
2327
|
-
showError(errorEl, "请输入要执行的命令。");
|
|
2328
|
-
return;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
2609
|
var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
|
|
2332
|
-
var
|
|
2333
|
-
var selectedMode = getSafeModeForTool(selectedTool, modeEl && modeEl.value ? modeEl.value : state.modeValue);
|
|
2610
|
+
var selectedMode = getSafeModeForTool(command, state.modeValue);
|
|
2334
2611
|
state.modeValue = selectedMode;
|
|
2335
2612
|
state.chatMode = selectedMode;
|
|
2336
|
-
state.sessionTool =
|
|
2337
|
-
state.preferredCommand =
|
|
2613
|
+
state.sessionTool = command;
|
|
2614
|
+
state.preferredCommand = command;
|
|
2338
2615
|
syncComposerModeSelect();
|
|
2339
2616
|
|
|
2340
2617
|
fetch("/api/commands", {
|
|
@@ -2361,12 +2638,11 @@
|
|
|
2361
2638
|
state.lastRenderedEmpty = null;
|
|
2362
2639
|
closeSessionModal();
|
|
2363
2640
|
closeSessionsDrawer();
|
|
2364
|
-
state.commandValue = command;
|
|
2365
2641
|
return refreshAll();
|
|
2366
2642
|
})
|
|
2367
2643
|
.then(function() { focusInputBox(true); })
|
|
2368
2644
|
.catch(function() {
|
|
2369
|
-
showError(errorEl, "
|
|
2645
|
+
showError(errorEl, "无法启动会话,请确认 Claude 已正确安装。");
|
|
2370
2646
|
});
|
|
2371
2647
|
}
|
|
2372
2648
|
|
|
@@ -2427,6 +2703,11 @@
|
|
|
2427
2703
|
function handleInputBoxKeydown(event) {
|
|
2428
2704
|
if (event.isComposing) return;
|
|
2429
2705
|
|
|
2706
|
+
if (shouldCaptureTerminalEvent(event)) {
|
|
2707
|
+
captureTerminalInput(event);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2430
2711
|
if (event.key === "Enter") {
|
|
2431
2712
|
if (event.shiftKey) {
|
|
2432
2713
|
event.preventDefault();
|
|
@@ -2445,7 +2726,7 @@
|
|
|
2445
2726
|
return;
|
|
2446
2727
|
}
|
|
2447
2728
|
event.preventDefault();
|
|
2448
|
-
sendInputFromBox(
|
|
2729
|
+
sendInputFromBox();
|
|
2449
2730
|
return;
|
|
2450
2731
|
}
|
|
2451
2732
|
|
|
@@ -2594,22 +2875,35 @@
|
|
|
2594
2875
|
|
|
2595
2876
|
function autoResizeInput(el) {
|
|
2596
2877
|
if (!el) return;
|
|
2878
|
+
var minHeight = 36;
|
|
2879
|
+
var maxHeight = 120;
|
|
2880
|
+
var touchDevice = isTouchDevice();
|
|
2881
|
+
// For empty content, reset to minimum height immediately
|
|
2882
|
+
if (!el.value || el.value.trim() === "") {
|
|
2883
|
+
el.style.height = minHeight + "px";
|
|
2884
|
+
el.style.minHeight = minHeight + "px";
|
|
2885
|
+
el.style.overflowY = touchDevice ? "auto" : "hidden";
|
|
2886
|
+
el.scrollTop = 0;
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2597
2889
|
// Force synchronous reflow so scrollHeight reflects current content
|
|
2598
2890
|
void el.offsetHeight;
|
|
2599
|
-
// Temporarily
|
|
2600
|
-
el.style.minHeight = "0";
|
|
2891
|
+
// Temporarily collapse to measure true content height
|
|
2601
2892
|
el.style.height = "0";
|
|
2602
|
-
|
|
2893
|
+
el.style.minHeight = "0";
|
|
2603
2894
|
void el.offsetHeight;
|
|
2604
|
-
var maxHeight = 160;
|
|
2605
|
-
var minHeight = 44;
|
|
2606
2895
|
var contentHeight = el.scrollHeight;
|
|
2607
2896
|
var newHeight = Math.max(minHeight, Math.min(contentHeight, maxHeight));
|
|
2897
|
+
var shouldScrollInside = contentHeight > maxHeight;
|
|
2608
2898
|
el.style.height = newHeight + "px";
|
|
2609
|
-
// Keep inline minHeight to override CSS min-height
|
|
2610
2899
|
el.style.minHeight = minHeight + "px";
|
|
2611
|
-
el.style.overflowY =
|
|
2612
|
-
|
|
2900
|
+
el.style.overflowY = shouldScrollInside || touchDevice ? "auto" : "hidden";
|
|
2901
|
+
if (shouldScrollInside) {
|
|
2902
|
+
syncInputBoxScroll(el);
|
|
2903
|
+
} else {
|
|
2904
|
+
el.scrollTop = 0;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2613
2907
|
|
|
2614
2908
|
function isSelectedSessionRunning() {
|
|
2615
2909
|
if (!state.selectedId) return false;
|
|
@@ -2684,7 +2978,7 @@
|
|
|
2684
2978
|
// If we have a selected ID, try to send input to it
|
|
2685
2979
|
if (state.selectedId) {
|
|
2686
2980
|
if (value) {
|
|
2687
|
-
sendInputFromBox(
|
|
2981
|
+
sendInputFromBox();
|
|
2688
2982
|
}
|
|
2689
2983
|
return;
|
|
2690
2984
|
}
|
|
@@ -2750,8 +3044,7 @@
|
|
|
2750
3044
|
if (stopBtn) stopBtn.classList.remove("hidden");
|
|
2751
3045
|
|
|
2752
3046
|
var title = session ? shortCommand(session.command) : "Wand";
|
|
2753
|
-
var
|
|
2754
|
-
var info = session ? (modeName + " | " + session.status) : "";
|
|
3047
|
+
var info = session ? getSessionStatusLabel(session) : "开始对话";
|
|
2755
3048
|
if (terminalTitle) terminalTitle.textContent = title;
|
|
2756
3049
|
if (terminalInfo) terminalInfo.textContent = info;
|
|
2757
3050
|
if (sessionSummary) sessionSummary.textContent = title;
|
|
@@ -2768,10 +3061,32 @@
|
|
|
2768
3061
|
}
|
|
2769
3062
|
|
|
2770
3063
|
|
|
2771
|
-
function sendInputFromBox(
|
|
3064
|
+
function sendInputFromBox() {
|
|
3065
|
+
if (state.terminalInteractive) {
|
|
3066
|
+
showToast("终端交互模式开启时,请直接在终端中输入。", "info");
|
|
3067
|
+
return Promise.resolve();
|
|
3068
|
+
}
|
|
3069
|
+
|
|
2772
3070
|
var inputBox = document.getElementById("input-box");
|
|
2773
3071
|
var value = inputBox ? inputBox.value : "";
|
|
3072
|
+
var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; }) || null;
|
|
2774
3073
|
if (value) {
|
|
3074
|
+
console.log("[wand] sendInputFromBox", {
|
|
3075
|
+
sessionId: state.selectedId,
|
|
3076
|
+
sessionStatus: selectedSession ? selectedSession.status : null,
|
|
3077
|
+
view: state.currentView,
|
|
3078
|
+
wsConnected: state.wsConnected,
|
|
3079
|
+
terminalInteractive: state.terminalInteractive,
|
|
3080
|
+
inputLength: value.length
|
|
3081
|
+
});
|
|
3082
|
+
if (!isSelectedSessionRunning()) {
|
|
3083
|
+
console.warn("[wand] Prevented send because selected session is not running", {
|
|
3084
|
+
sessionId: state.selectedId,
|
|
3085
|
+
sessionStatus: selectedSession ? selectedSession.status : null
|
|
3086
|
+
});
|
|
3087
|
+
showToast("会话已结束,请重新启动会话。", "error");
|
|
3088
|
+
return Promise.resolve();
|
|
3089
|
+
}
|
|
2775
3090
|
// Clear todo progress bar at the start of a new user turn
|
|
2776
3091
|
var todoEl = document.getElementById("todo-progress");
|
|
2777
3092
|
if (todoEl) todoEl.classList.add("hidden");
|
|
@@ -2780,30 +3095,444 @@
|
|
|
2780
3095
|
// Clear the input box immediately to prevent double-sending
|
|
2781
3096
|
if (inputBox) {
|
|
2782
3097
|
inputBox.value = "";
|
|
2783
|
-
|
|
2784
|
-
inputBox.style.height = "44px";
|
|
2785
|
-
inputBox.style.minHeight = "44px";
|
|
2786
|
-
inputBox.style.overflowY = "hidden";
|
|
3098
|
+
autoResizeInput(inputBox);
|
|
2787
3099
|
}
|
|
2788
3100
|
setDraftValue("");
|
|
2789
3101
|
return queueDirectInput(combinedInput).catch(function(err) {
|
|
2790
|
-
showToast(err
|
|
3102
|
+
showToast(getInputErrorMessage(err), "error");
|
|
2791
3103
|
throw err;
|
|
2792
3104
|
});
|
|
2793
3105
|
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3106
|
+
return Promise.resolve();
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
function getInputErrorMessage(error) {
|
|
3110
|
+
if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
|
|
3111
|
+
return "会话已结束,请重新启动会话。";
|
|
3112
|
+
}
|
|
3113
|
+
if (error && error.errorCode === "SESSION_NOT_FOUND") {
|
|
3114
|
+
return "会话不存在,请重新启动会话。";
|
|
3115
|
+
}
|
|
3116
|
+
return (error && error.message) || "会话已结束,请重启会话。";
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function buildInputError(payload) {
|
|
3120
|
+
var err = new Error((payload && payload.error) || "会话已结束。");
|
|
3121
|
+
if (payload && typeof payload === "object") {
|
|
3122
|
+
err.errorCode = payload.errorCode || null;
|
|
3123
|
+
err.sessionId = payload.sessionId || state.selectedId || null;
|
|
3124
|
+
err.sessionStatus = Object.prototype.hasOwnProperty.call(payload, "sessionStatus") ? payload.sessionStatus : null;
|
|
3125
|
+
}
|
|
3126
|
+
return err;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
function isSessionUnavailableError(error) {
|
|
3130
|
+
return error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY" || error.errorCode === "SESSION_NOT_FOUND");
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
function markSessionStopped(sessionId, status) {
|
|
3134
|
+
if (!sessionId) return;
|
|
3135
|
+
updateSessionSnapshot({ id: sessionId, status: status || "exited" });
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
function queueDirectInput(input) {
|
|
3139
|
+
if (!input || !state.selectedId) return Promise.resolve();
|
|
3140
|
+
state.messageQueue.push(input);
|
|
3141
|
+
updateQueueCounter();
|
|
3142
|
+
state.inputQueue = state.inputQueue.then(function() {
|
|
3143
|
+
return postInput(input).finally(function() {
|
|
3144
|
+
var idx = state.messageQueue.indexOf(input);
|
|
3145
|
+
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
3146
|
+
updateQueueCounter();
|
|
3147
|
+
});
|
|
3148
|
+
});
|
|
3149
|
+
return state.inputQueue;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
function postInput(input) {
|
|
3153
|
+
if (!state.selectedId) return Promise.resolve();
|
|
3154
|
+
|
|
3155
|
+
// Pre-check: don't send if session is not running
|
|
3156
|
+
if (!isSelectedSessionRunning()) {
|
|
3157
|
+
console.warn("[wand] postInput: session not running, skipping send", {
|
|
3158
|
+
sessionId: state.selectedId
|
|
2798
3159
|
});
|
|
3160
|
+
showToast("会话已结束,请重新启动会话。", "error");
|
|
3161
|
+
return Promise.resolve();
|
|
2799
3162
|
}
|
|
2800
|
-
|
|
3163
|
+
|
|
3164
|
+
// If WebSocket is disconnected, queue the message
|
|
3165
|
+
if (!state.wsConnected) {
|
|
3166
|
+
if (state.pendingMessages.length >= 100) {
|
|
3167
|
+
state.pendingMessages.shift();
|
|
3168
|
+
}
|
|
3169
|
+
state.pendingMessages.push(input);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
console.log("[wand] postInput: sending", {
|
|
3173
|
+
sessionId: state.selectedId,
|
|
3174
|
+
inputLength: input.length,
|
|
3175
|
+
view: state.currentView,
|
|
3176
|
+
wsConnected: state.wsConnected
|
|
3177
|
+
});
|
|
3178
|
+
|
|
3179
|
+
return fetch("/api/sessions/" + state.selectedId + "/input", {
|
|
3180
|
+
method: "POST",
|
|
3181
|
+
headers: { "Content-Type": "application/json" },
|
|
3182
|
+
credentials: "same-origin",
|
|
3183
|
+
body: JSON.stringify({ input: input, view: state.currentView })
|
|
3184
|
+
})
|
|
3185
|
+
.then(function(res) {
|
|
3186
|
+
if (!res.ok) {
|
|
3187
|
+
return res.json().catch(function() { return { error: "请求失败" }; }).then(function(payload) {
|
|
3188
|
+
var error = buildInputError(payload);
|
|
3189
|
+
error.httpStatus = res.status;
|
|
3190
|
+
console.error("[wand] postInput: request failed", {
|
|
3191
|
+
status: res.status,
|
|
3192
|
+
errorCode: error.errorCode,
|
|
3193
|
+
message: error.message,
|
|
3194
|
+
sessionId: state.selectedId
|
|
3195
|
+
});
|
|
3196
|
+
// Mark session as stopped for unavailable errors
|
|
3197
|
+
if (isSessionUnavailableError(error)) {
|
|
3198
|
+
markSessionStopped(state.selectedId, error.sessionStatus || "exited");
|
|
3199
|
+
}
|
|
3200
|
+
throw error;
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
return res.json();
|
|
3204
|
+
})
|
|
3205
|
+
.then(function(snapshot) {
|
|
3206
|
+
if (snapshot && snapshot.id) {
|
|
3207
|
+
updateSessionSnapshot(snapshot);
|
|
3208
|
+
if (snapshot.messages && snapshot.messages.length > 0) {
|
|
3209
|
+
state.currentMessages = snapshot.messages;
|
|
3210
|
+
}
|
|
3211
|
+
renderChat(true);
|
|
3212
|
+
}
|
|
3213
|
+
return snapshot;
|
|
3214
|
+
});
|
|
2801
3215
|
}
|
|
2802
3216
|
|
|
2803
3217
|
function sendDirectInput(input) {
|
|
2804
3218
|
return queueDirectInput(input);
|
|
2805
3219
|
}
|
|
2806
3220
|
|
|
3221
|
+
function isTerminalInteractionAvailable() {
|
|
3222
|
+
return !!state.selectedId && state.currentView === "terminal";
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
function shouldCaptureTerminalEvent(event) {
|
|
3226
|
+
if (!state.terminalInteractive || !isTerminalInteractionAvailable()) return false;
|
|
3227
|
+
if (event.defaultPrevented || event.isComposing) return false;
|
|
3228
|
+
var target = event.target;
|
|
3229
|
+
if (!target) return true;
|
|
3230
|
+
if (target.closest && target.closest("#mini-keyboard")) return false;
|
|
3231
|
+
if (shouldIgnoreInteractiveTarget(target)) return false;
|
|
3232
|
+
return true;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
var keyboardEventKeyMap = {
|
|
3236
|
+
Esc: "escape",
|
|
3237
|
+
ArrowUp: "up",
|
|
3238
|
+
ArrowDown: "down",
|
|
3239
|
+
ArrowLeft: "left",
|
|
3240
|
+
ArrowRight: "right",
|
|
3241
|
+
Enter: "enter",
|
|
3242
|
+
Tab: "tab",
|
|
3243
|
+
Backspace: "backspace",
|
|
3244
|
+
Home: "home",
|
|
3245
|
+
End: "end",
|
|
3246
|
+
PageUp: "pageup",
|
|
3247
|
+
PageDown: "pagedown",
|
|
3248
|
+
Delete: "delete",
|
|
3249
|
+
Insert: "insert",
|
|
3250
|
+
" ": "space"
|
|
3251
|
+
};
|
|
3252
|
+
|
|
3253
|
+
var ptySpecialKeyMap = {
|
|
3254
|
+
space: " ",
|
|
3255
|
+
tab: String.fromCharCode(9),
|
|
3256
|
+
backspace: String.fromCharCode(127),
|
|
3257
|
+
home: String.fromCharCode(27) + "[H",
|
|
3258
|
+
end: String.fromCharCode(27) + "[F",
|
|
3259
|
+
pageup: String.fromCharCode(27) + "[5~",
|
|
3260
|
+
pagedown: String.fromCharCode(27) + "[6~",
|
|
3261
|
+
delete: String.fromCharCode(27) + "[3~",
|
|
3262
|
+
insert: String.fromCharCode(27) + "[2~"
|
|
3263
|
+
};
|
|
3264
|
+
|
|
3265
|
+
var ctrlSymbolMap = {
|
|
3266
|
+
" ": 0,
|
|
3267
|
+
"[": 27,
|
|
3268
|
+
"\\": 28,
|
|
3269
|
+
"]": 29,
|
|
3270
|
+
"^": 30,
|
|
3271
|
+
"_": 31
|
|
3272
|
+
};
|
|
3273
|
+
|
|
3274
|
+
var ignoredInteractiveTargetIds = new Set([
|
|
3275
|
+
"mini-keyboard-fab",
|
|
3276
|
+
"mini-keyboard-toggle",
|
|
3277
|
+
"terminal-interactive-toggle"
|
|
3278
|
+
]);
|
|
3279
|
+
|
|
3280
|
+
function shouldIgnoreInteractiveTarget(target) {
|
|
3281
|
+
return !!(target && ignoredInteractiveTargetIds.has(target.id));
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
var modifierKeySet = new Set(["ctrl", "alt", "shift"]);
|
|
3285
|
+
|
|
3286
|
+
function isModifierKey(key) {
|
|
3287
|
+
return modifierKeySet.has(key);
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function getPtySpecialSequence(key) {
|
|
3291
|
+
return ptySpecialKeyMap[key] || "";
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function getCtrlSequence(text) {
|
|
3295
|
+
var lower = text.toLowerCase();
|
|
3296
|
+
if (lower >= "a" && lower <= "z") {
|
|
3297
|
+
return String.fromCharCode(lower.charCodeAt(0) - 96);
|
|
3298
|
+
}
|
|
3299
|
+
if (Object.prototype.hasOwnProperty.call(ctrlSymbolMap, lower)) {
|
|
3300
|
+
return String.fromCharCode(ctrlSymbolMap[lower]);
|
|
3301
|
+
}
|
|
3302
|
+
return "";
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
function keyFromKeyboardEvent(event) {
|
|
3306
|
+
return keyboardEventKeyMap[event.key] || event.key;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function getModifierStateFromEvent(event, key) {
|
|
3310
|
+
return {
|
|
3311
|
+
ctrl: event.ctrlKey,
|
|
3312
|
+
alt: event.altKey,
|
|
3313
|
+
shift: event.shiftKey && key.length === 1,
|
|
3314
|
+
meta: event.metaKey
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
function sendTerminalSequence(sequence) {
|
|
3319
|
+
if (!sequence) return;
|
|
3320
|
+
queueDirectInput(sequence).catch(function() {});
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
function focusTerminalInteractionTarget() {
|
|
3324
|
+
focusTerminalContainer();
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
function setMiniKeyboardVisible(visible, clearModifiersOnHide) {
|
|
3328
|
+
// Inline keyboard visibility is now based on view, not state
|
|
3329
|
+
state.miniKeyboardVisible = !!visible;
|
|
3330
|
+
if (!state.miniKeyboardVisible && clearModifiersOnHide !== false) {
|
|
3331
|
+
clearModifiers();
|
|
3332
|
+
}
|
|
3333
|
+
updateKeyboardPopupUI();
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
function hideMiniKeyboard(clearModifiersOnHide) {
|
|
3337
|
+
// Just clear modifiers, inline keyboard visibility follows view
|
|
3338
|
+
state.keyboardPopupOpen = false;
|
|
3339
|
+
if (clearModifiersOnHide !== false) {
|
|
3340
|
+
clearModifiers();
|
|
3341
|
+
}
|
|
3342
|
+
updateKeyboardPopupUI();
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function showMiniKeyboard() {
|
|
3346
|
+
// Inline keyboard shows automatically in terminal view
|
|
3347
|
+
updateKeyboardPopupUI();
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
function toggleMiniKeyboard() {
|
|
3351
|
+
// No longer needed, keyboard is inline
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
function toggleTerminalInteractive() {
|
|
3355
|
+
if (!isTerminalInteractionAvailable()) return;
|
|
3356
|
+
setTerminalInteractive(!state.terminalInteractive);
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function setTerminalInteractive(enabled) {
|
|
3360
|
+
var next = !!enabled && isTerminalInteractionAvailable();
|
|
3361
|
+
if (state.terminalInteractive === next) return;
|
|
3362
|
+
state.terminalInteractive = next;
|
|
3363
|
+
if (next) {
|
|
3364
|
+
enableTerminalCapture();
|
|
3365
|
+
hideMiniKeyboard(false);
|
|
3366
|
+
focusTerminalInteractionTarget();
|
|
3367
|
+
showToast("终端交互模式已开启", "info");
|
|
3368
|
+
} else {
|
|
3369
|
+
disableTerminalCapture();
|
|
3370
|
+
clearModifiers();
|
|
3371
|
+
}
|
|
3372
|
+
updateInteractiveControls();
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
function reconcileInteractiveState() {
|
|
3376
|
+
var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
|
|
3377
|
+
var shouldDisableInteractive = !selectedSession || selectedSession.status !== "running" || state.currentView !== "terminal";
|
|
3378
|
+
if (shouldDisableInteractive && state.terminalInteractive) {
|
|
3379
|
+
setTerminalInteractive(false);
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
if ((!selectedSession || state.currentView !== "terminal") && state.keyboardPopupOpen) {
|
|
3383
|
+
state.keyboardPopupOpen = false;
|
|
3384
|
+
}
|
|
3385
|
+
updateInteractiveControls();
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function updateInteractiveControls() {
|
|
3389
|
+
// Update both toggle buttons (topbar and terminal-header)
|
|
3390
|
+
var toggles = ["terminal-interactive-toggle-top"];
|
|
3391
|
+
toggles.forEach(function(id) {
|
|
3392
|
+
var toggle = document.getElementById(id);
|
|
3393
|
+
if (toggle) {
|
|
3394
|
+
toggle.classList.toggle("active", state.terminalInteractive);
|
|
3395
|
+
toggle.textContent = state.terminalInteractive ? "⌨ 交互开" : "⌨ 交互关";
|
|
3396
|
+
}
|
|
3397
|
+
});
|
|
3398
|
+
// Inline keyboard visibility follows current view
|
|
3399
|
+
var inlineKeyboard = document.getElementById("inline-keyboard");
|
|
3400
|
+
if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", state.currentView !== "terminal");
|
|
3401
|
+
var inputHint = document.querySelector(".input-hint");
|
|
3402
|
+
if (inputHint) inputHint.classList.toggle("hidden", state.currentView === "terminal");
|
|
3403
|
+
var container = document.getElementById("output");
|
|
3404
|
+
if (container) container.classList.toggle("interactive", state.terminalInteractive);
|
|
3405
|
+
var keyboardToggle = document.getElementById("keyboard-toggle");
|
|
3406
|
+
if (keyboardToggle) {
|
|
3407
|
+
keyboardToggle.classList.toggle("hidden", state.currentView !== "terminal" || !state.selectedId);
|
|
3408
|
+
keyboardToggle.classList.toggle("active", state.keyboardPopupOpen);
|
|
3409
|
+
}
|
|
3410
|
+
var popup = document.getElementById("keyboard-popup");
|
|
3411
|
+
if (popup) {
|
|
3412
|
+
var shouldShowPopup = state.keyboardPopupOpen && state.currentView === "terminal" && !!state.selectedId;
|
|
3413
|
+
popup.classList.toggle("hidden", !shouldShowPopup);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
function captureTerminalInput(event) {
|
|
3418
|
+
if (!shouldCaptureTerminalEvent(event)) return;
|
|
3419
|
+
var key = keyFromKeyboardEvent(event);
|
|
3420
|
+
if (!key) return;
|
|
3421
|
+
event.preventDefault();
|
|
3422
|
+
var mods = getModifierStateFromEvent(event, key);
|
|
3423
|
+
if (isModifierKey(key)) return;
|
|
3424
|
+
var sequence = buildPtySequence(key, mods);
|
|
3425
|
+
if (sequence) sendTerminalSequence(sequence);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
function handleMiniKeyboardClick(event) {
|
|
3429
|
+
var btn = event.target.closest(".mk-key");
|
|
3430
|
+
if (!btn) return;
|
|
3431
|
+
var key = btn.getAttribute("data-key");
|
|
3432
|
+
if (!key) return;
|
|
3433
|
+
event.preventDefault();
|
|
3434
|
+
if (key === "ctrl" || key === "alt" || key === "shift") {
|
|
3435
|
+
state.modifiers[key] = !state.modifiers[key];
|
|
3436
|
+
updateModifierUI();
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
var sequence = buildPtySequence(key, { ctrl: state.modifiers.ctrl, alt: state.modifiers.alt, shift: state.modifiers.shift });
|
|
3440
|
+
if (sequence) sendTerminalSequence(sequence);
|
|
3441
|
+
clearModifiers();
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function handleInlineKeyboardClick(event) {
|
|
3445
|
+
// Support both old .ik-key and new .kp-key buttons
|
|
3446
|
+
var btn = event.target.closest(".ik-key, .kp-key");
|
|
3447
|
+
if (!btn) return;
|
|
3448
|
+
var key = btn.getAttribute("data-key");
|
|
3449
|
+
if (!key) return;
|
|
3450
|
+
event.preventDefault();
|
|
3451
|
+
if (key === "ctrl" || key === "alt") {
|
|
3452
|
+
state.modifiers[key] = !state.modifiers[key];
|
|
3453
|
+
updateKeyboardPopupUI();
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
if (key === "ctrl_enter") {
|
|
3457
|
+
// Ctrl+Enter for confirm/approve in terminal
|
|
3458
|
+
var sequence = buildPtySequence("enter", { ctrl: true, alt: false, shift: false });
|
|
3459
|
+
if (sequence) sendTerminalSequence(sequence);
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
var sequence = buildPtySequence(key, { ctrl: state.modifiers.ctrl, alt: state.modifiers.alt, shift: false });
|
|
3463
|
+
if (sequence) sendTerminalSequence(sequence);
|
|
3464
|
+
clearModifiers();
|
|
3465
|
+
updateKeyboardPopupUI();
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function updateKeyboardPopupUI() {
|
|
3469
|
+
var popup = document.getElementById("keyboard-popup");
|
|
3470
|
+
if (!popup) return;
|
|
3471
|
+
["ctrl", "alt"].forEach(function(name) {
|
|
3472
|
+
var btn = popup.querySelector('[data-key="' + name + '"]');
|
|
3473
|
+
if (btn) btn.classList.toggle("active", !!state.modifiers[name]);
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
function handleKeyboardToggle(event) {
|
|
3478
|
+
event.preventDefault();
|
|
3479
|
+
event.stopPropagation();
|
|
3480
|
+
if (state.currentView !== "terminal" || !state.selectedId) return;
|
|
3481
|
+
state.keyboardPopupOpen = !state.keyboardPopupOpen;
|
|
3482
|
+
updateInteractiveControls();
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function closeKeyboardPopup() {
|
|
3486
|
+
state.keyboardPopupOpen = false;
|
|
3487
|
+
updateInteractiveControls();
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
function enableTerminalCapture() {
|
|
3491
|
+
document.addEventListener("keydown", captureTerminalInput, true);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
function disableTerminalCapture() {
|
|
3495
|
+
document.removeEventListener("keydown", captureTerminalInput, true);
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
function buildPtySequence(key, modifiers) {
|
|
3499
|
+
var mods = modifiers || { ctrl: false, alt: false, shift: false };
|
|
3500
|
+
if (isModifierKey(key)) return "";
|
|
3501
|
+
var specialSequence = getPtySpecialSequence(key);
|
|
3502
|
+
if (specialSequence) return specialSequence;
|
|
3503
|
+
if (key.indexOf("ctrl_") === 0) {
|
|
3504
|
+
return String.fromCharCode(key.charCodeAt(key.length - 1) - 96);
|
|
3505
|
+
}
|
|
3506
|
+
var mapped = getControlInput(key);
|
|
3507
|
+
if (mapped) return mapped;
|
|
3508
|
+
if (!key) return "";
|
|
3509
|
+
var text = key.length === 1 ? key : "";
|
|
3510
|
+
if (!text) return "";
|
|
3511
|
+
if (mods.shift) text = text.toUpperCase();
|
|
3512
|
+
if (mods.ctrl) {
|
|
3513
|
+
return getCtrlSequence(text);
|
|
3514
|
+
}
|
|
3515
|
+
if (mods.alt) return String.fromCharCode(27) + text;
|
|
3516
|
+
return text;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
|
|
3520
|
+
function clearModifiers() {
|
|
3521
|
+
state.modifiers.ctrl = false;
|
|
3522
|
+
state.modifiers.alt = false;
|
|
3523
|
+
state.modifiers.shift = false;
|
|
3524
|
+
updateModifierUI();
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
function updateModifierUI() {
|
|
3528
|
+
var keyboard = document.getElementById("mini-keyboard");
|
|
3529
|
+
if (!keyboard) return;
|
|
3530
|
+
["ctrl", "alt", "shift"].forEach(function(name) {
|
|
3531
|
+
var btn = keyboard.querySelector('[data-key="' + name + '"]');
|
|
3532
|
+
if (btn) btn.classList.toggle("active", !!state.modifiers[name]);
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
|
|
2807
3536
|
function getControlInput(key) {
|
|
2808
3537
|
switch (key) {
|
|
2809
3538
|
case "yes":
|
|
@@ -2832,6 +3561,8 @@
|
|
|
2832
3561
|
return String.fromCharCode(11);
|
|
2833
3562
|
case "ctrl_w":
|
|
2834
3563
|
return String.fromCharCode(23);
|
|
3564
|
+
case "ctrl_z":
|
|
3565
|
+
return String.fromCharCode(26);
|
|
2835
3566
|
case "escape":
|
|
2836
3567
|
return String.fromCharCode(27);
|
|
2837
3568
|
default:
|
|
@@ -2839,144 +3570,813 @@
|
|
|
2839
3570
|
}
|
|
2840
3571
|
}
|
|
2841
3572
|
|
|
2842
|
-
function
|
|
2843
|
-
if (
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
state.
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3573
|
+
function flushPendingMessages() {
|
|
3574
|
+
if (state.pendingMessages.length === 0) return;
|
|
3575
|
+
|
|
3576
|
+
// Send queued messages in order
|
|
3577
|
+
var queue = state.pendingMessages.slice();
|
|
3578
|
+
state.pendingMessages = [];
|
|
3579
|
+
|
|
3580
|
+
queue.forEach(function(input) {
|
|
3581
|
+
postInput(input).catch(function() {
|
|
3582
|
+
// Ignore errors during flush
|
|
3583
|
+
});
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
function stopSession() {
|
|
3588
|
+
if (!state.selectedId) return;
|
|
3589
|
+
fetch("/api/sessions/" + state.selectedId + "/stop", { method: "POST", credentials: "same-origin" })
|
|
3590
|
+
.then(refreshAll);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
function deleteSession(id) {
|
|
3594
|
+
// 二次确认
|
|
3595
|
+
if (!confirm("确定要删除这个会话吗?此操作无法撤销。")) {
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
fetch("/api/sessions/" + id, { method: "DELETE", credentials: "same-origin" })
|
|
3599
|
+
.then(function(res) { return res.json(); })
|
|
3600
|
+
.then(function(data) {
|
|
3601
|
+
if (data && data.error) {
|
|
3602
|
+
throw new Error(data.error);
|
|
3603
|
+
}
|
|
3604
|
+
if (state.selectedId === id) {
|
|
3605
|
+
state.selectedId = null;
|
|
3606
|
+
persistSelectedId();
|
|
3607
|
+
}
|
|
3608
|
+
return refreshAll();
|
|
3609
|
+
})
|
|
3610
|
+
.catch(function() {
|
|
3611
|
+
var errorEl = document.getElementById("action-error");
|
|
3612
|
+
showError(errorEl, "无法删除会话。");
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
function startCommand(command, cwd, errorEl) {
|
|
3617
|
+
if (command === "claude") {
|
|
3618
|
+
state.preferredCommand = command;
|
|
3619
|
+
state.chatMode = getSafeModeForTool(command, state.chatMode);
|
|
3620
|
+
}
|
|
3621
|
+
return fetch("/api/commands", {
|
|
3622
|
+
method: "POST",
|
|
3623
|
+
headers: { "Content-Type": "application/json" },
|
|
3624
|
+
credentials: "same-origin",
|
|
3625
|
+
body: JSON.stringify({
|
|
3626
|
+
command: command,
|
|
3627
|
+
cwd: cwd || "",
|
|
3628
|
+
mode: state.chatMode || state.config.defaultMode || "default"
|
|
3629
|
+
})
|
|
3630
|
+
})
|
|
3631
|
+
.then(function(res) { return res.json(); })
|
|
3632
|
+
.then(function(data) {
|
|
3633
|
+
if (data.error) {
|
|
3634
|
+
if (errorEl) showError(errorEl, data.error);
|
|
3635
|
+
return null;
|
|
3636
|
+
}
|
|
3637
|
+
state.selectedId = data.id;
|
|
3638
|
+
persistSelectedId();
|
|
3639
|
+
state.drafts[data.id] = "";
|
|
3640
|
+
return data;
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
function isTouchDevice() {
|
|
3645
|
+
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
function focusInputBox(skipMobile) {
|
|
3649
|
+
if (state.terminalInteractive) return;
|
|
3650
|
+
var inputBox = document.getElementById("input-box");
|
|
3651
|
+
if (!inputBox || !state.selectedId) return;
|
|
3652
|
+
if (document.activeElement === inputBox) return;
|
|
3653
|
+
// Skip focus on mobile/touch devices for auto-triggered calls to avoid opening keyboard
|
|
3654
|
+
if (skipMobile && isTouchDevice()) return;
|
|
3655
|
+
focusInputWithSelection(inputBox);
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
function scrollLatestMessageIntoView() {
|
|
3659
|
+
var chatMessages = document.querySelector('.chat-messages');
|
|
3660
|
+
if (!chatMessages) return;
|
|
3661
|
+
var firstMsg = chatMessages.querySelector(".chat-message");
|
|
3662
|
+
if (!firstMsg) return;
|
|
3663
|
+
firstMsg.scrollIntoView({ block: "end", inline: "nearest", behavior: isTouchDevice() ? "auto" : "smooth" });
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
function updateInputPanelViewportSpacing() {
|
|
3667
|
+
var inputPanel = document.querySelector('.input-panel');
|
|
3668
|
+
if (!inputPanel) return;
|
|
3669
|
+
if (!('visualViewport' in window) || !isTouchDevice()) {
|
|
3670
|
+
inputPanel.style.removeProperty('--keyboard-offset');
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
var vv = window.visualViewport;
|
|
3674
|
+
var offsetBottom = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
3675
|
+
inputPanel.style.setProperty('--keyboard-offset', offsetBottom + 'px');
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
function resetInputPanelViewportSpacing() {
|
|
3679
|
+
var inputPanel = document.querySelector('.input-panel');
|
|
3680
|
+
if (!inputPanel) return;
|
|
3681
|
+
inputPanel.style.removeProperty('--keyboard-offset');
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
function restoreInputBoxViewport(inputBox) {
|
|
3685
|
+
if (!inputBox) return;
|
|
3686
|
+
var start = inputBox.selectionStart;
|
|
3687
|
+
var end = inputBox.selectionEnd;
|
|
3688
|
+
syncInputBoxScroll(inputBox);
|
|
3689
|
+
if (typeof start === 'number' && typeof end === 'number') {
|
|
3690
|
+
inputBox.setSelectionRange(start, end);
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
function bindInputTouchScroll(inputBox) {
|
|
3695
|
+
if (!inputBox || inputBox.dataset.touchScrollBound === 'true') return;
|
|
3696
|
+
inputBox.dataset.touchScrollBound = 'true';
|
|
3697
|
+
inputBox.addEventListener('touchstart', function() {
|
|
3698
|
+
if (inputBox.scrollHeight <= inputBox.clientHeight + 1) return;
|
|
3699
|
+
if (inputBox.scrollTop <= 0) {
|
|
3700
|
+
inputBox.scrollTop = 1;
|
|
3701
|
+
} else if (inputBox.scrollTop + inputBox.clientHeight >= inputBox.scrollHeight) {
|
|
3702
|
+
inputBox.scrollTop = Math.max(1, inputBox.scrollHeight - inputBox.clientHeight - 1);
|
|
3703
|
+
}
|
|
3704
|
+
}, { passive: true });
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
function syncInputBoxLayout(inputBox) {
|
|
3708
|
+
if (!inputBox) return;
|
|
3709
|
+
autoResizeInput(inputBox);
|
|
3710
|
+
restoreInputBoxViewport(inputBox);
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
function handleInputBoxFocus(event) {
|
|
3714
|
+
var inputBox = event && event.target ? event.target : document.getElementById('input-box');
|
|
3715
|
+
if (!inputBox) return;
|
|
3716
|
+
updateInputPanelViewportSpacing();
|
|
3717
|
+
syncInputBoxLayout(inputBox);
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
function handleInputBoxBlur() {
|
|
3721
|
+
resetInputPanelViewportSpacing();
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
function adjustInputBoxSelection(inputBox) {
|
|
3725
|
+
if (!inputBox) return;
|
|
3726
|
+
inputBox.setSelectionRange(inputBox.value.length, inputBox.value.length);
|
|
3727
|
+
restoreInputBoxViewport(inputBox);
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
function focusInputWithSelection(inputBox) {
|
|
3731
|
+
if (!inputBox) return;
|
|
3732
|
+
inputBox.focus({ preventScroll: true });
|
|
3733
|
+
adjustInputBoxSelection(inputBox);
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
function syncInputBoxForCurrentState(inputBox) {
|
|
3737
|
+
bindInputTouchScroll(inputBox);
|
|
3738
|
+
syncInputBoxLayout(inputBox);
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
function focusInputCaret(inputBox) {
|
|
3742
|
+
focusInputWithSelection(inputBox);
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
function updateInputViewportState(inputBox) {
|
|
3746
|
+
updateInputPanelViewportSpacing();
|
|
3747
|
+
restoreInputBoxViewport(inputBox);
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
function resetInputViewport() {
|
|
3751
|
+
resetInputPanelViewportSpacing();
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
function settleInputViewport(inputBox) {
|
|
3755
|
+
restoreInputBoxViewport(inputBox);
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
function focusInputBoxFromTap(inputBox) {
|
|
3759
|
+
focusInputCaret(inputBox);
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
function refreshInputBoxState(inputBox) {
|
|
3763
|
+
syncInputBoxForCurrentState(inputBox);
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
function clearInputViewportState() {
|
|
3767
|
+
resetInputViewport();
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
function finalizeInputViewportUpdate(inputBox) {
|
|
3771
|
+
settleInputViewport(inputBox);
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
function refreshInputViewportState(inputBox) {
|
|
3775
|
+
updateInputViewportState(inputBox);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
function clearInputBoxViewportState() {
|
|
3779
|
+
clearInputViewportState();
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
function syncInputBoxViewportState(inputBox) {
|
|
3783
|
+
refreshInputViewportState(inputBox);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
function resetInputBoxViewportState() {
|
|
3787
|
+
clearInputBoxViewportState();
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
function maintainInputBoxSelection(inputBox) {
|
|
3791
|
+
settleInputViewport(inputBox);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
function focusInputFromViewportTap(inputBox) {
|
|
3795
|
+
focusInputBoxFromTap(inputBox);
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
function stabilizeInputViewport(inputBox) {
|
|
3799
|
+
finalizeInputViewportUpdate(inputBox);
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
function syncInputBoxAfterFocus(inputBox) {
|
|
3803
|
+
handleInputBoxFocus({ target: inputBox });
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
function syncInputBoxAfterBlur() {
|
|
3807
|
+
handleInputBoxBlur();
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
function syncInputBoxAfterViewportChange(inputBox) {
|
|
3811
|
+
refreshInputViewportState(inputBox);
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
function syncInputBoxAfterValueChange(inputBox) {
|
|
3815
|
+
refreshInputBoxState(inputBox);
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
function keepInputBoxCursorVisible(inputBox) {
|
|
3819
|
+
maintainInputBoxSelection(inputBox);
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
function updateInputViewportAfterKeyboard(inputBox) {
|
|
3823
|
+
updateInputViewportState(inputBox);
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
function clearInputViewportAfterKeyboard() {
|
|
3827
|
+
clearInputViewportState();
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
function applyInputViewportState(inputBox) {
|
|
3831
|
+
updateInputViewportState(inputBox);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
function syncInputComposerAfterViewportChange(inputBox) {
|
|
3835
|
+
syncInputBoxAfterViewportChange(inputBox);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
function resetInputComposerAfterViewportChange() {
|
|
3839
|
+
clearInputViewportAfterKeyboard();
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
function ensureInputBoxViewportState(inputBox) {
|
|
3843
|
+
refreshInputBoxState(inputBox);
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
function syncInputBoxState(inputBox) {
|
|
3847
|
+
ensureInputBoxViewportState(inputBox);
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
function syncInputBoxOnTouch(inputBox) {
|
|
3851
|
+
bindInputTouchScroll(inputBox);
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
function clearInputViewport() {
|
|
3855
|
+
resetInputViewport();
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
function refreshInputViewport(inputBox) {
|
|
3859
|
+
updateInputViewportState(inputBox);
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
function stabilizeInputBoxViewport(inputBox) {
|
|
3863
|
+
settleInputViewport(inputBox);
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
function focusInputByTap(inputBox) {
|
|
3867
|
+
focusInputBoxFromTap(inputBox);
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
function finalizeInputBoxViewport(inputBox) {
|
|
3871
|
+
stabilizeInputBoxViewport(inputBox);
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
function updateInputViewport(inputBox) {
|
|
3875
|
+
refreshInputViewport(inputBox);
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
function resetInputViewportSpacing() {
|
|
3879
|
+
clearInputViewport();
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
function keepInputViewportStable(inputBox) {
|
|
3883
|
+
finalizeInputBoxViewport(inputBox);
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
function focusInputAtCaret(inputBox) {
|
|
3887
|
+
focusInputByTap(inputBox);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
function syncInputBoxViewport(inputBox) {
|
|
3891
|
+
updateInputViewport(inputBox);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
function clearInputBoxViewport() {
|
|
3895
|
+
resetInputViewportSpacing();
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function maintainInputViewport(inputBox) {
|
|
3899
|
+
keepInputViewportStable(inputBox);
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
function focusInputFromTapTarget(inputBox) {
|
|
3903
|
+
focusInputAtCaret(inputBox);
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
function settleInputBoxViewport(inputBox) {
|
|
3907
|
+
maintainInputViewport(inputBox);
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
function refreshInputViewportLayout(inputBox) {
|
|
3911
|
+
syncInputBoxViewport(inputBox);
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
function resetInputViewportLayout() {
|
|
3915
|
+
clearInputBoxViewport();
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
function keepCaretVisible(inputBox) {
|
|
3919
|
+
settleInputBoxViewport(inputBox);
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
function focusInputTarget(inputBox) {
|
|
3923
|
+
focusInputFromTapTarget(inputBox);
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
function finalizeInputLayout(inputBox) {
|
|
3927
|
+
refreshInputBoxState(inputBox);
|
|
3928
|
+
keepCaretVisible(inputBox);
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
function resetInputLayout() {
|
|
3932
|
+
resetInputViewportLayout();
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
function syncInputLayout(inputBox) {
|
|
3936
|
+
refreshInputViewportLayout(inputBox);
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
function focusInputSelection(inputBox) {
|
|
3940
|
+
focusInputTarget(inputBox);
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
function stabilizeInputLayout(inputBox) {
|
|
3944
|
+
finalizeInputLayout(inputBox);
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
function clearInputLayout() {
|
|
3948
|
+
resetInputLayout();
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
function applyInputLayout(inputBox) {
|
|
3952
|
+
syncInputLayout(inputBox);
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
function focusInputTapSelection(inputBox) {
|
|
3956
|
+
focusInputSelection(inputBox);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
function settleInputLayout(inputBox) {
|
|
3960
|
+
stabilizeInputLayout(inputBox);
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
function resetInputTapLayout() {
|
|
3964
|
+
clearInputLayout();
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function refreshInputTapLayout(inputBox) {
|
|
3968
|
+
applyInputLayout(inputBox);
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
function focusInputTap(inputBox) {
|
|
3972
|
+
focusInputTapSelection(inputBox);
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function keepInputTapStable(inputBox) {
|
|
3976
|
+
settleInputLayout(inputBox);
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
function clearInputTapState() {
|
|
3980
|
+
resetInputTapLayout();
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
function updateInputTapState(inputBox) {
|
|
3984
|
+
refreshInputTapLayout(inputBox);
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
function maintainInputTapState(inputBox) {
|
|
3988
|
+
keepInputTapStable(inputBox);
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
function focusInputTapTarget(inputBox) {
|
|
3992
|
+
focusInputTap(inputBox);
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
function syncInputTapState(inputBox) {
|
|
3996
|
+
updateInputTapState(inputBox);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
function resetInputTapState() {
|
|
4000
|
+
clearInputTapState();
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
function stabilizeInputTapState(inputBox) {
|
|
4004
|
+
maintainInputTapState(inputBox);
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
function activateInputTapTarget(inputBox) {
|
|
4008
|
+
focusInputTapTarget(inputBox);
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
function refreshInputTapViewport(inputBox) {
|
|
4012
|
+
syncInputTapState(inputBox);
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
function clearInputTapViewport() {
|
|
4016
|
+
resetInputTapState();
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
function keepInputTapViewportStable(inputBox) {
|
|
4020
|
+
stabilizeInputTapState(inputBox);
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
function focusInputTapViewport(inputBox) {
|
|
4024
|
+
activateInputTapTarget(inputBox);
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
function settleInputTapViewport(inputBox) {
|
|
4028
|
+
keepInputTapViewportStable(inputBox);
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
function updateInputTapViewport(inputBox) {
|
|
4032
|
+
refreshInputTapViewport(inputBox);
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function resetInputTapViewport() {
|
|
4036
|
+
clearInputTapViewport();
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
function maintainInputTapViewport(inputBox) {
|
|
4040
|
+
settleInputTapViewport(inputBox);
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
function focusInputTapViewportTarget(inputBox) {
|
|
4044
|
+
focusInputTapViewport(inputBox);
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
function refreshInputPanelState(inputBox) {
|
|
4048
|
+
updateInputTapViewport(inputBox);
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
function clearInputPanelState() {
|
|
4052
|
+
resetInputTapViewport();
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
function stabilizeInputPanelState(inputBox) {
|
|
4056
|
+
maintainInputTapViewport(inputBox);
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
function focusInputPanelTarget(inputBox) {
|
|
4060
|
+
focusInputTapViewportTarget(inputBox);
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
function finalizeInputPanelState(inputBox) {
|
|
4064
|
+
stabilizeInputPanelState(inputBox);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
function refreshInputPanelViewport(inputBox) {
|
|
4068
|
+
refreshInputPanelState(inputBox);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function clearInputPanelViewport() {
|
|
4072
|
+
clearInputPanelState();
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
function settleInputPanelViewport(inputBox) {
|
|
4076
|
+
finalizeInputPanelState(inputBox);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
function focusInputPanelViewport(inputBox) {
|
|
4080
|
+
focusInputPanelTarget(inputBox);
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
function syncInputPanelViewport(inputBox) {
|
|
4084
|
+
refreshInputPanelViewport(inputBox);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
function resetInputPanelViewport() {
|
|
4088
|
+
clearInputPanelViewport();
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
function stabilizeInputPanelViewport(inputBox) {
|
|
4092
|
+
settleInputPanelViewport(inputBox);
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
function focusInputPanelTap(inputBox) {
|
|
4096
|
+
focusInputPanelViewport(inputBox);
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
function updateInputPanelLayout(inputBox) {
|
|
4100
|
+
syncInputPanelViewport(inputBox);
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
function clearInputPanelLayout() {
|
|
4104
|
+
resetInputPanelViewport();
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
function keepInputPanelLayoutStable(inputBox) {
|
|
4108
|
+
stabilizeInputPanelViewport(inputBox);
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
function focusInputPanelSelection(inputBox) {
|
|
4112
|
+
focusInputPanelTap(inputBox);
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
function finalizeInputPanelLayout(inputBox) {
|
|
4116
|
+
keepInputPanelLayoutStable(inputBox);
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
function refreshInputComposerState(inputBox) {
|
|
4120
|
+
updateInputPanelLayout(inputBox);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
function clearInputComposerState() {
|
|
4124
|
+
clearInputPanelLayout();
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
function settleInputComposerState(inputBox) {
|
|
4128
|
+
finalizeInputPanelLayout(inputBox);
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
function focusInputComposerSelection(inputBox) {
|
|
4132
|
+
focusInputPanelSelection(inputBox);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
function syncInputComposerState(inputBox) {
|
|
4136
|
+
refreshInputComposerState(inputBox);
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
function resetInputComposerState() {
|
|
4140
|
+
clearInputComposerState();
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
function stabilizeInputComposerState(inputBox) {
|
|
4144
|
+
settleInputComposerState(inputBox);
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
function focusInputComposerTap(inputBox) {
|
|
4148
|
+
focusInputComposerSelection(inputBox);
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
function updateInputComposerLayout(inputBox) {
|
|
4152
|
+
syncInputComposerState(inputBox);
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
function clearComposerLayout() {
|
|
4156
|
+
resetInputComposerState();
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
function keepComposerLayoutStable(inputBox) {
|
|
4160
|
+
stabilizeInputComposerState(inputBox);
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
function focusComposerTap(inputBox) {
|
|
4164
|
+
focusInputComposerTap(inputBox);
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
function finalizeComposerLayout(inputBox) {
|
|
4168
|
+
keepComposerLayoutStable(inputBox);
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
function refreshComposerLayout(inputBox) {
|
|
4172
|
+
updateInputComposerLayout(inputBox);
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
function resetComposerLayout() {
|
|
4176
|
+
clearComposerLayout();
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
function stabilizeComposerLayout(inputBox) {
|
|
4180
|
+
finalizeComposerLayout(inputBox);
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
function focusComposerSelection(inputBox) {
|
|
4184
|
+
focusComposerTap(inputBox);
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
function updateComposerViewport(inputBox) {
|
|
4188
|
+
refreshComposerLayout(inputBox);
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
function clearComposerViewport() {
|
|
4192
|
+
resetComposerLayout();
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
function keepComposerViewportStable(inputBox) {
|
|
4196
|
+
stabilizeComposerLayout(inputBox);
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
function focusComposerViewport(inputBox) {
|
|
4200
|
+
focusComposerSelection(inputBox);
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
function finalizeComposerViewport(inputBox) {
|
|
4204
|
+
keepComposerViewportStable(inputBox);
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
function refreshComposerViewport(inputBox) {
|
|
4208
|
+
updateComposerViewport(inputBox);
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
function resetComposerViewport() {
|
|
4212
|
+
clearComposerViewport();
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
function stabilizeComposerViewport(inputBox) {
|
|
4216
|
+
finalizeComposerViewport(inputBox);
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
function focusComposerViewportTap(inputBox) {
|
|
4220
|
+
focusComposerViewport(inputBox);
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
function syncComposerViewport(inputBox) {
|
|
4224
|
+
refreshComposerViewport(inputBox);
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
function clearComposerViewportState() {
|
|
4228
|
+
resetComposerViewport();
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
function keepComposerViewportStateStable(inputBox) {
|
|
4232
|
+
stabilizeComposerViewport(inputBox);
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
function focusComposerViewportTarget(inputBox) {
|
|
4236
|
+
focusComposerViewportTap(inputBox);
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
function finalizeComposerViewportState(inputBox) {
|
|
4240
|
+
keepComposerViewportStateStable(inputBox);
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
function refreshComposerViewportState(inputBox) {
|
|
4244
|
+
syncComposerViewport(inputBox);
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
function resetComposerViewportState() {
|
|
4248
|
+
clearComposerViewportState();
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
function stabilizeComposerViewportState(inputBox) {
|
|
4252
|
+
finalizeComposerViewportState(inputBox);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
function focusComposerViewportState(inputBox) {
|
|
4256
|
+
focusComposerViewportTarget(inputBox);
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
function syncComposerLayoutState(inputBox) {
|
|
4260
|
+
refreshComposerViewportState(inputBox);
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
function clearComposerLayoutState() {
|
|
4264
|
+
resetComposerViewportState();
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
function keepComposerLayoutStateStable(inputBox) {
|
|
4268
|
+
stabilizeComposerViewportState(inputBox);
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
function focusComposerLayoutState(inputBox) {
|
|
4272
|
+
focusComposerViewportState(inputBox);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
function finalizeComposerLayoutState(inputBox) {
|
|
4276
|
+
keepComposerLayoutStateStable(inputBox);
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
function refreshInputFocusState(inputBox) {
|
|
4280
|
+
syncComposerLayoutState(inputBox);
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
function clearInputFocusState() {
|
|
4284
|
+
clearComposerLayoutState();
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
function stabilizeInputFocusState(inputBox) {
|
|
4288
|
+
finalizeComposerLayoutState(inputBox);
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
function focusInputFocusState(inputBox) {
|
|
4292
|
+
focusComposerLayoutState(inputBox);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
function keepInputFocusStable(inputBox) {
|
|
4296
|
+
stabilizeInputFocusState(inputBox);
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
function updateInputFocusState(inputBox) {
|
|
4300
|
+
refreshInputFocusState(inputBox);
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
function resetInputFocusState() {
|
|
4304
|
+
clearInputFocusState();
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
function focusInputTargetState(inputBox) {
|
|
4308
|
+
focusInputFocusState(inputBox);
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
function settleInputFocusState(inputBox) {
|
|
4312
|
+
keepInputFocusStable(inputBox);
|
|
2856
4313
|
}
|
|
2857
4314
|
|
|
2858
|
-
function
|
|
2859
|
-
|
|
4315
|
+
function syncInputFocusState(inputBox) {
|
|
4316
|
+
updateInputFocusState(inputBox);
|
|
4317
|
+
}
|
|
2860
4318
|
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
if (state.pendingMessages.length >= 100) {
|
|
2865
|
-
state.pendingMessages.shift(); // Remove oldest
|
|
2866
|
-
}
|
|
2867
|
-
state.pendingMessages.push(input);
|
|
2868
|
-
// Still try HTTP fallback
|
|
2869
|
-
}
|
|
4319
|
+
function clearFocusState() {
|
|
4320
|
+
resetInputFocusState();
|
|
4321
|
+
}
|
|
2870
4322
|
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
headers: { "Content-Type": "application/json" },
|
|
2874
|
-
credentials: "same-origin",
|
|
2875
|
-
body: JSON.stringify({ input: input, view: state.currentView })
|
|
2876
|
-
})
|
|
2877
|
-
.then(function(res) {
|
|
2878
|
-
if (!res.ok) {
|
|
2879
|
-
return res.json().then(function(data) {
|
|
2880
|
-
throw new Error(data.error || "会话已结束。");
|
|
2881
|
-
});
|
|
2882
|
-
}
|
|
2883
|
-
return res.json();
|
|
2884
|
-
})
|
|
2885
|
-
.then(function(snapshot) {
|
|
2886
|
-
// Use the response snapshot to immediately update session state
|
|
2887
|
-
// This ensures user messages appear in chat without waiting for WebSocket echo
|
|
2888
|
-
if (snapshot && snapshot.id) {
|
|
2889
|
-
updateSessionSnapshot(snapshot);
|
|
2890
|
-
if (snapshot.messages && snapshot.messages.length > 0) {
|
|
2891
|
-
state.currentMessages = snapshot.messages;
|
|
2892
|
-
}
|
|
2893
|
-
// Immediate render to show user message quickly
|
|
2894
|
-
renderChat(true);
|
|
2895
|
-
}
|
|
2896
|
-
return snapshot;
|
|
2897
|
-
});
|
|
4323
|
+
function maintainFocusState(inputBox) {
|
|
4324
|
+
settleInputFocusState(inputBox);
|
|
2898
4325
|
}
|
|
2899
4326
|
|
|
2900
|
-
function
|
|
2901
|
-
|
|
4327
|
+
function activateInputTargetState(inputBox) {
|
|
4328
|
+
focusInputTargetState(inputBox);
|
|
4329
|
+
}
|
|
2902
4330
|
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
4331
|
+
function updateInputFocusViewport(inputBox) {
|
|
4332
|
+
syncInputFocusState(inputBox);
|
|
4333
|
+
}
|
|
2906
4334
|
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
// Ignore errors during flush
|
|
2910
|
-
});
|
|
2911
|
-
});
|
|
4335
|
+
function clearInputFocusViewport() {
|
|
4336
|
+
clearFocusState();
|
|
2912
4337
|
}
|
|
2913
4338
|
|
|
2914
|
-
function
|
|
2915
|
-
|
|
2916
|
-
fetch("/api/sessions/" + state.selectedId + "/stop", { method: "POST", credentials: "same-origin" })
|
|
2917
|
-
.then(refreshAll);
|
|
4339
|
+
function stabilizeInputFocusViewport(inputBox) {
|
|
4340
|
+
maintainFocusState(inputBox);
|
|
2918
4341
|
}
|
|
2919
4342
|
|
|
2920
|
-
function
|
|
2921
|
-
|
|
2922
|
-
|
|
4343
|
+
function focusInputViewportTarget(inputBox) {
|
|
4344
|
+
activateInputTargetState(inputBox);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
function finalizeInputFocusViewport(inputBox) {
|
|
4348
|
+
stabilizeInputFocusViewport(inputBox);
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
function shouldAdjustForKeyboard(vv, inputBox) {
|
|
4352
|
+
if (!vv || !inputBox || document.activeElement !== inputBox) return false;
|
|
4353
|
+
var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
|
|
4354
|
+
if (offsetBottom <= 50) return false;
|
|
4355
|
+
var rect = inputBox.getBoundingClientRect();
|
|
4356
|
+
return rect.bottom > vv.height - 12;
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
function syncInputBoxScroll(inputBox) {
|
|
4360
|
+
if (!inputBox) return;
|
|
4361
|
+
var isScrollable = inputBox.scrollHeight > inputBox.clientHeight + 1;
|
|
4362
|
+
if (!isScrollable) {
|
|
4363
|
+
inputBox.scrollTop = 0;
|
|
2923
4364
|
return;
|
|
2924
4365
|
}
|
|
2925
|
-
|
|
2926
|
-
.then(function(res) { return res.json(); })
|
|
2927
|
-
.then(function(data) {
|
|
2928
|
-
if (data && data.error) {
|
|
2929
|
-
throw new Error(data.error);
|
|
2930
|
-
}
|
|
2931
|
-
if (state.selectedId === id) {
|
|
2932
|
-
state.selectedId = null;
|
|
2933
|
-
persistSelectedId();
|
|
2934
|
-
}
|
|
2935
|
-
return refreshAll();
|
|
2936
|
-
})
|
|
2937
|
-
.catch(function() {
|
|
2938
|
-
var errorEl = document.getElementById("action-error");
|
|
2939
|
-
showError(errorEl, "无法删除会话。");
|
|
2940
|
-
});
|
|
4366
|
+
inputBox.scrollTop = inputBox.scrollHeight;
|
|
2941
4367
|
}
|
|
2942
4368
|
|
|
2943
|
-
function
|
|
2944
|
-
var
|
|
2945
|
-
if (
|
|
2946
|
-
|
|
2947
|
-
state.chatMode = getSafeModeForTool(inferredTool, state.chatMode);
|
|
2948
|
-
}
|
|
2949
|
-
return fetch("/api/commands", {
|
|
2950
|
-
method: "POST",
|
|
2951
|
-
headers: { "Content-Type": "application/json" },
|
|
2952
|
-
credentials: "same-origin",
|
|
2953
|
-
body: JSON.stringify({
|
|
2954
|
-
command: command,
|
|
2955
|
-
cwd: cwd || "",
|
|
2956
|
-
mode: state.chatMode || state.config.defaultMode || "default"
|
|
2957
|
-
})
|
|
2958
|
-
})
|
|
2959
|
-
.then(function(res) { return res.json(); })
|
|
2960
|
-
.then(function(data) {
|
|
2961
|
-
if (data.error) {
|
|
2962
|
-
if (errorEl) showError(errorEl, data.error);
|
|
2963
|
-
return null;
|
|
2964
|
-
}
|
|
2965
|
-
state.selectedId = data.id;
|
|
2966
|
-
persistSelectedId();
|
|
2967
|
-
state.drafts[data.id] = "";
|
|
2968
|
-
return data;
|
|
2969
|
-
});
|
|
4369
|
+
function focusInputFromTap() {
|
|
4370
|
+
var inputBox = document.getElementById('input-box');
|
|
4371
|
+
if (!inputBox || !state.selectedId || document.activeElement === inputBox) return;
|
|
4372
|
+
focusInputWithSelection(inputBox);
|
|
2970
4373
|
}
|
|
2971
4374
|
|
|
2972
|
-
function
|
|
2973
|
-
var
|
|
2974
|
-
if (!
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
if (skipMobile && ("ontouchstart" in window || navigator.maxTouchPoints > 0)) return;
|
|
2978
|
-
inputBox.focus();
|
|
2979
|
-
inputBox.setSelectionRange(inputBox.value.length, inputBox.value.length);
|
|
4375
|
+
function focusTerminalContainer() {
|
|
4376
|
+
var output = document.getElementById("output");
|
|
4377
|
+
if (!output) return;
|
|
4378
|
+
output.setAttribute("tabindex", "0");
|
|
4379
|
+
output.focus();
|
|
2980
4380
|
}
|
|
2981
4381
|
|
|
2982
4382
|
// Mobile keyboard handling
|
|
@@ -2990,14 +4390,12 @@
|
|
|
2990
4390
|
var vk = navigator.virtualKeyboard;
|
|
2991
4391
|
|
|
2992
4392
|
vk.addEventListener('geometrychange', function() {
|
|
2993
|
-
if (inputPanel)
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
3000
|
-
}
|
|
4393
|
+
if (!inputPanel) return;
|
|
4394
|
+
var rect = vk.boundingRect;
|
|
4395
|
+
var kbHeight = rect ? rect.height : 0;
|
|
4396
|
+
inputPanel.style.paddingBottom = kbHeight > 0 ? kbHeight + 'px' : '';
|
|
4397
|
+
if (kbHeight > 0 && document.activeElement === document.getElementById('input-box')) {
|
|
4398
|
+
scrollLatestMessageIntoView();
|
|
3001
4399
|
}
|
|
3002
4400
|
});
|
|
3003
4401
|
}
|
|
@@ -3006,10 +4404,7 @@
|
|
|
3006
4404
|
var output = document.getElementById('output');
|
|
3007
4405
|
if (output) {
|
|
3008
4406
|
output.addEventListener('click', function() {
|
|
3009
|
-
|
|
3010
|
-
var inputBox = document.getElementById('input-box');
|
|
3011
|
-
if (inputBox) inputBox.focus();
|
|
3012
|
-
}
|
|
4407
|
+
focusInputFromTap();
|
|
3013
4408
|
});
|
|
3014
4409
|
}
|
|
3015
4410
|
|
|
@@ -3018,8 +4413,7 @@
|
|
|
3018
4413
|
chatMessages.addEventListener('click', function(e) {
|
|
3019
4414
|
// Only focus if not clicking on a link, button, or tool card header
|
|
3020
4415
|
if (e.target.tagName !== 'A' && e.target.tagName !== 'BUTTON' && !e.target.closest('button') && !e.target.closest('[data-tool-toggle]')) {
|
|
3021
|
-
|
|
3022
|
-
if (inputBox && state.selectedId) inputBox.focus();
|
|
4416
|
+
focusInputFromTap();
|
|
3023
4417
|
}
|
|
3024
4418
|
});
|
|
3025
4419
|
}
|
|
@@ -3031,41 +4425,108 @@
|
|
|
3031
4425
|
|
|
3032
4426
|
var vv = window.visualViewport;
|
|
3033
4427
|
var inputPanel = document.querySelector('.input-panel');
|
|
3034
|
-
var chatMessages = document.querySelector('.chat-messages');
|
|
3035
4428
|
var lastHeight = vv.height;
|
|
4429
|
+
var keyboardOpen = false;
|
|
3036
4430
|
|
|
3037
4431
|
function updateViewport() {
|
|
3038
4432
|
if (!inputPanel || !vv) return;
|
|
3039
|
-
|
|
4433
|
+
var inputBox = document.getElementById('input-box');
|
|
3040
4434
|
var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
|
|
3041
4435
|
var isKeyboardOpen = offsetBottom > 50;
|
|
4436
|
+
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
3042
4437
|
|
|
3043
|
-
if (isKeyboardOpen) {
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
setTimeout(function() {
|
|
3047
|
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
3048
|
-
}, 100);
|
|
3049
|
-
}
|
|
4438
|
+
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
4439
|
+
scrollLatestMessageIntoView();
|
|
4440
|
+
syncInputBoxScroll(inputBox);
|
|
3050
4441
|
}
|
|
3051
4442
|
|
|
4443
|
+
keyboardOpen = isKeyboardOpen;
|
|
3052
4444
|
lastHeight = vv.height;
|
|
3053
4445
|
}
|
|
3054
4446
|
|
|
3055
|
-
|
|
3056
|
-
var viewportTimer = null;
|
|
4447
|
+
var viewportFrame = null;
|
|
3057
4448
|
function debouncedUpdate() {
|
|
3058
|
-
if (
|
|
3059
|
-
|
|
4449
|
+
if (viewportFrame !== null) cancelAnimationFrame(viewportFrame);
|
|
4450
|
+
viewportFrame = requestAnimationFrame(function() {
|
|
4451
|
+
viewportFrame = null;
|
|
4452
|
+
updateViewport();
|
|
4453
|
+
});
|
|
3060
4454
|
}
|
|
3061
4455
|
|
|
3062
4456
|
vv.addEventListener('resize', debouncedUpdate);
|
|
3063
4457
|
vv.addEventListener('scroll', debouncedUpdate);
|
|
3064
4458
|
|
|
3065
|
-
// Initial update
|
|
3066
4459
|
updateViewport();
|
|
3067
4460
|
}
|
|
3068
4461
|
|
|
4462
|
+
function initTerminalResizeHandle() {
|
|
4463
|
+
// 终端容器拖动调整大小功能
|
|
4464
|
+
var container = document.getElementById("terminal-container");
|
|
4465
|
+
if (!container) return;
|
|
4466
|
+
|
|
4467
|
+
// 创建拖动手柄
|
|
4468
|
+
var resizeHandle = document.createElement("div");
|
|
4469
|
+
resizeHandle.className = "terminal-resize-handle";
|
|
4470
|
+
resizeHandle.innerHTML = "⋮"; // 垂直省略号,表示可拖动
|
|
4471
|
+
container.appendChild(resizeHandle);
|
|
4472
|
+
|
|
4473
|
+
var isResizing = false;
|
|
4474
|
+
var startY = 0;
|
|
4475
|
+
var startHeight = 0;
|
|
4476
|
+
|
|
4477
|
+
resizeHandle.addEventListener("mousedown", function(e) {
|
|
4478
|
+
isResizing = true;
|
|
4479
|
+
startY = e.clientY;
|
|
4480
|
+
startHeight = container.getBoundingClientRect().height;
|
|
4481
|
+
document.body.style.cursor = "ns-resize";
|
|
4482
|
+
document.body.style.userSelect = "none";
|
|
4483
|
+
e.preventDefault();
|
|
4484
|
+
});
|
|
4485
|
+
|
|
4486
|
+
document.addEventListener("mousemove", function(e) {
|
|
4487
|
+
if (!isResizing) return;
|
|
4488
|
+
var deltaY = e.clientY - startY;
|
|
4489
|
+
var newHeight = Math.max(200, Math.min(startHeight + deltaY, window.innerHeight - 200));
|
|
4490
|
+
container.style.height = newHeight + "px";
|
|
4491
|
+
container.style.flex = "none";
|
|
4492
|
+
scheduleTerminalResize();
|
|
4493
|
+
});
|
|
4494
|
+
|
|
4495
|
+
document.addEventListener("mouseup", function() {
|
|
4496
|
+
if (isResizing) {
|
|
4497
|
+
isResizing = false;
|
|
4498
|
+
document.body.style.cursor = "";
|
|
4499
|
+
document.body.style.userSelect = "";
|
|
4500
|
+
scheduleTerminalResize();
|
|
4501
|
+
}
|
|
4502
|
+
});
|
|
4503
|
+
|
|
4504
|
+
// 触摸设备支持
|
|
4505
|
+
resizeHandle.addEventListener("touchstart", function(e) {
|
|
4506
|
+
isResizing = true;
|
|
4507
|
+
startY = e.touches[0].clientY;
|
|
4508
|
+
startHeight = container.getBoundingClientRect().height;
|
|
4509
|
+
e.preventDefault();
|
|
4510
|
+
}, { passive: false });
|
|
4511
|
+
|
|
4512
|
+
document.addEventListener("touchmove", function(e) {
|
|
4513
|
+
if (!isResizing) return;
|
|
4514
|
+
var deltaY = e.touches[0].clientY - startY;
|
|
4515
|
+
var newHeight = Math.max(200, Math.min(startHeight + deltaY, window.innerHeight - 200));
|
|
4516
|
+
container.style.height = newHeight + "px";
|
|
4517
|
+
container.style.flex = "none";
|
|
4518
|
+
scheduleTerminalResize();
|
|
4519
|
+
e.preventDefault();
|
|
4520
|
+
}, { passive: false });
|
|
4521
|
+
|
|
4522
|
+
document.addEventListener("touchend", function() {
|
|
4523
|
+
if (isResizing) {
|
|
4524
|
+
isResizing = false;
|
|
4525
|
+
scheduleTerminalResize();
|
|
4526
|
+
}
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
|
|
3069
4530
|
function observeTerminalResize() {
|
|
3070
4531
|
var output = document.getElementById("output");
|
|
3071
4532
|
if (!output) return;
|
|
@@ -3194,36 +4655,18 @@
|
|
|
3194
4655
|
// Update session output (for terminal display and local message parsing)
|
|
3195
4656
|
if (msg.data && msg.data.output && msg.sessionId) {
|
|
3196
4657
|
var snapshot = { id: msg.sessionId, output: msg.data.output };
|
|
4658
|
+
if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
|
|
4659
|
+
snapshot.permissionBlocked = !!msg.data.permissionBlocked;
|
|
4660
|
+
}
|
|
3197
4661
|
// Pass structured messages if available from JSON chat mode
|
|
3198
4662
|
if (msg.data.messages) {
|
|
3199
4663
|
snapshot.messages = msg.data.messages;
|
|
3200
4664
|
}
|
|
3201
4665
|
updateSessionSnapshot(snapshot);
|
|
3202
|
-
|
|
3203
|
-
// Only process if this is the selected session
|
|
3204
4666
|
if (msg.sessionId === state.selectedId) {
|
|
3205
|
-
|
|
3206
|
-
var updatedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
3207
|
-
if (updatedSession) {
|
|
3208
|
-
if (updatedSession.messages && updatedSession.messages.length > 0) {
|
|
3209
|
-
state.currentMessages = updatedSession.messages;
|
|
3210
|
-
} else if (updatedSession.output) {
|
|
3211
|
-
state.currentMessages = parseMessages(updatedSession.output, updatedSession.command);
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
// Check if this is a new message (not just streaming update)
|
|
3216
|
-
var prevMsgCount = state.lastRenderedMsgCount;
|
|
3217
|
-
var currMsgCount = state.currentMessages.length;
|
|
3218
|
-
|
|
3219
|
-
// Streaming thinking update: update the thinking element in-place
|
|
3220
|
-
if (msg.data.thinkingContent !== undefined) {
|
|
3221
|
-
updateStreamingThinking(msg.data.thinkingContent);
|
|
3222
|
-
}
|
|
3223
|
-
|
|
3224
|
-
// Immediate render for new messages, debounced for streaming updates
|
|
3225
|
-
scheduleChatRender(currMsgCount > prevMsgCount);
|
|
4667
|
+
updateTaskDisplay();
|
|
3226
4668
|
}
|
|
4669
|
+
|
|
3227
4670
|
}
|
|
3228
4671
|
// Real-time terminal output
|
|
3229
4672
|
if (msg.sessionId === state.selectedId && state.terminal && msg.data && msg.data.output) {
|
|
@@ -3242,33 +4685,139 @@
|
|
|
3242
4685
|
// New session started
|
|
3243
4686
|
loadSessions();
|
|
3244
4687
|
break;
|
|
3245
|
-
case 'ended':
|
|
3246
|
-
//
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
4688
|
+
case 'ended': {
|
|
4689
|
+
// Build snapshot from server data; use updateSessionSnapshot so the
|
|
4690
|
+
// local update is not lost when loadSessions() later replaces
|
|
4691
|
+
// state.sessions entirely.
|
|
4692
|
+
var endedStatus = (msg.data && msg.data.status) ? msg.data.status : "exited";
|
|
4693
|
+
var endedPermBlocked = (msg.data && Object.prototype.hasOwnProperty.call(msg.data, "permissionBlocked")) ? !!msg.data.permissionBlocked : false;
|
|
4694
|
+
var endedSnapshot = { id: msg.sessionId, status: endedStatus, permissionBlocked: endedPermBlocked };
|
|
4695
|
+
if (msg.data && msg.data.messages) {
|
|
4696
|
+
endedSnapshot.messages = msg.data.messages;
|
|
4697
|
+
}
|
|
4698
|
+
updateSessionSnapshot(endedSnapshot);
|
|
4699
|
+
|
|
4700
|
+
// Clear stale queued inputs so they cannot race with the ended session.
|
|
4701
|
+
// Each queued item's postInput will hit the server and get an error, but
|
|
4702
|
+
// clearing the queues here prevents them from growing unbounded.
|
|
4703
|
+
state.messageQueue = [];
|
|
4704
|
+
state.pendingMessages = [];
|
|
4705
|
+
|
|
4706
|
+
// Disable terminal interactive mode immediately so the terminal stops
|
|
4707
|
+
// capturing keystrokes before loadSessions() completes.
|
|
3250
4708
|
if (msg.sessionId === state.selectedId) {
|
|
3251
|
-
|
|
4709
|
+
setTerminalInteractive(false);
|
|
4710
|
+
state.currentTask = null;
|
|
4711
|
+
updateTaskDisplay();
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
// Update UI chrome immediately; loadSessions() will refresh the sessions
|
|
4715
|
+
// list asynchronously (which may already be in-flight from a poll tick).
|
|
4716
|
+
if (msg.sessionId === state.selectedId) {
|
|
4717
|
+
updateShellChrome();
|
|
3252
4718
|
}
|
|
3253
|
-
|
|
4719
|
+
|
|
4720
|
+
loadSessions();
|
|
3254
4721
|
if (msg.sessionId === state.selectedId) {
|
|
3255
|
-
|
|
4722
|
+
loadOutput(msg.sessionId);
|
|
3256
4723
|
}
|
|
3257
4724
|
break;
|
|
4725
|
+
}
|
|
3258
4726
|
case 'init':
|
|
3259
4727
|
// Initial state for subscribed session (after reconnect or subscription)
|
|
3260
4728
|
if (msg.sessionId === state.selectedId && msg.data) {
|
|
3261
4729
|
if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
|
|
3262
4730
|
updateTerminalOutput(msg.data.output || "");
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
4731
|
+
scheduleTerminalResize();
|
|
4732
|
+
}
|
|
4733
|
+
break;
|
|
4734
|
+
case 'usage':
|
|
4735
|
+
// Token usage events are processed server-side; per-message usage is read from msg.usage
|
|
4736
|
+
break;
|
|
4737
|
+
case 'task':
|
|
4738
|
+
// Current task update from Claude's tool execution
|
|
4739
|
+
if (msg.sessionId === state.selectedId) {
|
|
4740
|
+
state.currentTask = msg.data || null;
|
|
4741
|
+
updateTaskDisplay();
|
|
3267
4742
|
}
|
|
3268
4743
|
break;
|
|
3269
4744
|
}
|
|
3270
4745
|
}
|
|
3271
4746
|
|
|
4747
|
+
function updateTaskDisplay() {
|
|
4748
|
+
var taskEl = document.getElementById("current-task");
|
|
4749
|
+
var permissionActionsEl = document.getElementById("permission-actions");
|
|
4750
|
+
if (!taskEl) return;
|
|
4751
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
4752
|
+
var pendingEscalation = selectedSession && selectedSession.pendingEscalation ? selectedSession.pendingEscalation : null;
|
|
4753
|
+
if (pendingEscalation) {
|
|
4754
|
+
var reason = pendingEscalation.reason || "等待 Claude 权限授权";
|
|
4755
|
+
var target = pendingEscalation.target ? " · " + pendingEscalation.target : "";
|
|
4756
|
+
taskEl.textContent = reason + target;
|
|
4757
|
+
taskEl.classList.remove("hidden");
|
|
4758
|
+
taskEl.classList.add("permission-blocked");
|
|
4759
|
+
if (permissionActionsEl) permissionActionsEl.classList.remove("hidden");
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
if (selectedSession && selectedSession.permissionBlocked) {
|
|
4763
|
+
taskEl.textContent = "等待 Claude 权限授权";
|
|
4764
|
+
taskEl.classList.remove("hidden");
|
|
4765
|
+
taskEl.classList.add("permission-blocked");
|
|
4766
|
+
if (permissionActionsEl) permissionActionsEl.classList.remove("hidden");
|
|
4767
|
+
return;
|
|
4768
|
+
}
|
|
4769
|
+
taskEl.classList.remove("permission-blocked");
|
|
4770
|
+
if (permissionActionsEl) permissionActionsEl.classList.add("hidden");
|
|
4771
|
+
var task = state.currentTask;
|
|
4772
|
+
if (task && task.title) {
|
|
4773
|
+
taskEl.textContent = task.title;
|
|
4774
|
+
taskEl.classList.remove("hidden");
|
|
4775
|
+
} else {
|
|
4776
|
+
taskEl.textContent = "";
|
|
4777
|
+
taskEl.classList.add("hidden");
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
function approvePermission() {
|
|
4782
|
+
if (!state.selectedId) return;
|
|
4783
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/approve-permission", {
|
|
4784
|
+
method: "POST",
|
|
4785
|
+
credentials: "same-origin"
|
|
4786
|
+
})
|
|
4787
|
+
.then(function(res) { return res.json(); })
|
|
4788
|
+
.then(function(data) {
|
|
4789
|
+
if (data && data.error) {
|
|
4790
|
+
showToast(data.error, "error");
|
|
4791
|
+
return;
|
|
4792
|
+
}
|
|
4793
|
+
updateSessionSnapshot(data);
|
|
4794
|
+
updateTaskDisplay();
|
|
4795
|
+
})
|
|
4796
|
+
.catch(function(error) {
|
|
4797
|
+
showToast((error && error.message) || "无法批准授权。", "error");
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
function denyPermission() {
|
|
4802
|
+
if (!state.selectedId) return;
|
|
4803
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/deny-permission", {
|
|
4804
|
+
method: "POST",
|
|
4805
|
+
credentials: "same-origin"
|
|
4806
|
+
})
|
|
4807
|
+
.then(function(res) { return res.json(); })
|
|
4808
|
+
.then(function(data) {
|
|
4809
|
+
if (data && data.error) {
|
|
4810
|
+
showToast(data.error, "error");
|
|
4811
|
+
return;
|
|
4812
|
+
}
|
|
4813
|
+
updateSessionSnapshot(data);
|
|
4814
|
+
updateTaskDisplay();
|
|
4815
|
+
})
|
|
4816
|
+
.catch(function(error) {
|
|
4817
|
+
showToast((error && error.message) || "无法拒绝授权。", "error");
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
|
|
3272
4821
|
function updateTerminalOutput(output) {
|
|
3273
4822
|
if (!state.terminal) return;
|
|
3274
4823
|
var normalized = normalizeTerminalOutput(output);
|
|
@@ -3290,17 +4839,16 @@
|
|
|
3290
4839
|
}
|
|
3291
4840
|
|
|
3292
4841
|
function setView(view) {
|
|
3293
|
-
|
|
3294
|
-
state.currentView
|
|
4842
|
+
state.currentView = view || "terminal";
|
|
4843
|
+
if (state.currentView !== "terminal") {
|
|
4844
|
+
setTerminalInteractive(false);
|
|
4845
|
+
closeKeyboardPopup();
|
|
4846
|
+
}
|
|
3295
4847
|
applyCurrentView();
|
|
3296
|
-
|
|
4848
|
+
reconcileInteractiveState();
|
|
4849
|
+
if (state.currentView === "terminal") {
|
|
3297
4850
|
setTimeout(scheduleTerminalResize, 40);
|
|
3298
4851
|
}
|
|
3299
|
-
|
|
3300
|
-
// Render chat if switching to chat view - force full render
|
|
3301
|
-
if (view === "chat") {
|
|
3302
|
-
renderChat(true);
|
|
3303
|
-
}
|
|
3304
4852
|
}
|
|
3305
4853
|
|
|
3306
4854
|
function renderChat(forceFullRender) {
|
|
@@ -3345,6 +4893,90 @@
|
|
|
3345
4893
|
}, 30);
|
|
3346
4894
|
}
|
|
3347
4895
|
|
|
4896
|
+
// Extract system info from PTY output that's not in structured messages
|
|
4897
|
+
function extractPtySystemInfo(output, messages) {
|
|
4898
|
+
if (!output || !messages || messages.length === 0) return [];
|
|
4899
|
+
|
|
4900
|
+
// Strip ANSI escape sequences
|
|
4901
|
+
function stripAnsi(text) {
|
|
4902
|
+
return text.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
var clean = stripAnsi(output);
|
|
4906
|
+
var systemInfo = [];
|
|
4907
|
+
|
|
4908
|
+
// Find user input positions in output
|
|
4909
|
+
var userInputs = [];
|
|
4910
|
+
for (var i = 0; i < messages.length; i++) {
|
|
4911
|
+
if (messages[i].role === 'user') {
|
|
4912
|
+
var userText = '';
|
|
4913
|
+
var content = messages[i].content;
|
|
4914
|
+
if (typeof content === 'string') {
|
|
4915
|
+
userText = content;
|
|
4916
|
+
} else if (Array.isArray(content)) {
|
|
4917
|
+
for (var j = 0; j < content.length; j++) {
|
|
4918
|
+
if (content[j].type === 'text') {
|
|
4919
|
+
userText = content[j].text;
|
|
4920
|
+
break;
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
if (userText) {
|
|
4925
|
+
userInputs.push({ text: userText, index: i });
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
// Extract content before each user input
|
|
4931
|
+
var lastPos = 0;
|
|
4932
|
+
for (var i = 0; i < userInputs.length; i++) {
|
|
4933
|
+
var userInput = userInputs[i];
|
|
4934
|
+
var pos = clean.indexOf('❯ ' + userInput.text, lastPos);
|
|
4935
|
+
if (pos === -1) {
|
|
4936
|
+
// Try with newline
|
|
4937
|
+
pos = clean.indexOf('\n❯ ' + userInput.text, lastPos);
|
|
4938
|
+
if (pos !== -1) pos += 1;
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4941
|
+
if (pos > lastPos) {
|
|
4942
|
+
var segment = clean.substring(lastPos, pos);
|
|
4943
|
+
// Extract meaningful system info
|
|
4944
|
+
var lines = segment.split('\n');
|
|
4945
|
+
var infoLines = [];
|
|
4946
|
+
for (var j = 0; j < lines.length; j++) {
|
|
4947
|
+
var line = lines[j].trim();
|
|
4948
|
+
// Skip empty lines, separators, prompts, UI noise
|
|
4949
|
+
if (!line || line.startsWith('────') || line === '❯' || line === '?' || line === '') continue;
|
|
4950
|
+
|
|
4951
|
+
// Skip Claude Code UI elements
|
|
4952
|
+
if (line.includes('Claude Code v') ||
|
|
4953
|
+
(line.includes('Opus') && line.includes('with')) ||
|
|
4954
|
+
(line.includes('Sonnet') && line.includes('with')) ||
|
|
4955
|
+
line.includes('API Usage') || line.includes('Billing') ||
|
|
4956
|
+
line.includes('for shortcuts') || line.includes('/effort') ||
|
|
4957
|
+
line.match(/^[▸▐▝▘▗▖█▌▍▎▏▔▁▂▃▄▅▆▇██]/) ||
|
|
4958
|
+
line.match(/^[▸▐▝▘▗▖█▌▍▎▏▔▁▂▃▄▅▆▇██]{3,}/)) {
|
|
4959
|
+
continue;
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4962
|
+
// Keep meaningful system messages
|
|
4963
|
+
if (line.length > 3) {
|
|
4964
|
+
infoLines.push(line);
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
if (infoLines.length > 0) {
|
|
4968
|
+
systemInfo.push({
|
|
4969
|
+
beforeMessage: userInput.index,
|
|
4970
|
+
content: infoLines.join('\n')
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
lastPos = pos + userInput.text.length + 2; // +2 for '❯ '
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
return systemInfo;
|
|
4978
|
+
}
|
|
4979
|
+
|
|
3348
4980
|
function doRenderChat(forceFullRender) {
|
|
3349
4981
|
var chatOutput = document.getElementById("chat-output");
|
|
3350
4982
|
if (!chatOutput) return;
|
|
@@ -3424,10 +5056,45 @@
|
|
|
3424
5056
|
var needsFullRender = forceRender || existingCount === 0 || msgCount !== existingCount;
|
|
3425
5057
|
|
|
3426
5058
|
function fullRenderChat() {
|
|
3427
|
-
|
|
5059
|
+
// Extract system info from PTY output
|
|
5060
|
+
var systemInfo = extractPtySystemInfo(selectedSession.output, messages);
|
|
5061
|
+
|
|
5062
|
+
// Build HTML with system info cards interleaved
|
|
5063
|
+
var html = '';
|
|
5064
|
+
var reversedMessages = messages.slice().reverse();
|
|
5065
|
+
var msgCount = messages.length;
|
|
5066
|
+
|
|
5067
|
+
for (var i = 0; i < reversedMessages.length; i++) {
|
|
5068
|
+
var msg = reversedMessages[i];
|
|
5069
|
+
var originalIndex = msgCount - 1 - i; // Original index in messages array
|
|
5070
|
+
|
|
5071
|
+
// Find system info for this message position
|
|
5072
|
+
var sysInfo = null;
|
|
5073
|
+
for (var j = 0; j < systemInfo.length; j++) {
|
|
5074
|
+
if (systemInfo[j].beforeMessage === originalIndex) {
|
|
5075
|
+
sysInfo = systemInfo[j];
|
|
5076
|
+
break;
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
|
|
5080
|
+
// Render system info card if exists
|
|
5081
|
+
if (sysInfo) {
|
|
5082
|
+
html += '<div class="chat-message system-info">' +
|
|
5083
|
+
'<div class="system-info-card">' +
|
|
5084
|
+
'<div class="system-info-header">ℹ️ 系统信息</div>' +
|
|
5085
|
+
'<div class="system-info-content">' + escapeHtml(sysInfo.content) + '</div>' +
|
|
5086
|
+
'</div>' +
|
|
5087
|
+
'</div>';
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
// Render message
|
|
5091
|
+
html += renderChatMessage(msg);
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
chatMessages.innerHTML = html;
|
|
3428
5095
|
attachAllCopyHandlers(chatMessages);
|
|
3429
5096
|
// Only expand the single newest tool card (first chat-message = newest due to column-reverse)
|
|
3430
|
-
var firstMsg = chatMessages.querySelector(".chat-message");
|
|
5097
|
+
var firstMsg = chatMessages.querySelector(".chat-message:not(.system-info)");
|
|
3431
5098
|
if (firstMsg) {
|
|
3432
5099
|
var cards = firstMsg.querySelectorAll(".tool-use-card");
|
|
3433
5100
|
if (cards.length > 0) {
|
|
@@ -3437,9 +5104,9 @@
|
|
|
3437
5104
|
}
|
|
3438
5105
|
}
|
|
3439
5106
|
}
|
|
3440
|
-
// Scroll to bottom (newest message) -
|
|
5107
|
+
// Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
|
|
3441
5108
|
requestAnimationFrame(function() {
|
|
3442
|
-
chatMessages
|
|
5109
|
+
smartScrollToBottom(chatMessages);
|
|
3443
5110
|
});
|
|
3444
5111
|
}
|
|
3445
5112
|
|
|
@@ -3466,67 +5133,65 @@
|
|
|
3466
5133
|
// Reverse so the newest ends up at the bottom
|
|
3467
5134
|
newMessages.reverse();
|
|
3468
5135
|
var fragment = document.createDocumentFragment();
|
|
5136
|
+
var insertedEls = [];
|
|
3469
5137
|
for (var i = 0; i < newMessages.length; i++) {
|
|
3470
5138
|
var div = document.createElement("div");
|
|
3471
5139
|
div.innerHTML = renderChatMessage(newMessages[i]);
|
|
3472
5140
|
var el = div.firstElementChild;
|
|
3473
5141
|
if (el) {
|
|
3474
5142
|
el.classList.add("animate-in");
|
|
5143
|
+
insertedEls.push(el);
|
|
3475
5144
|
fragment.appendChild(el);
|
|
3476
5145
|
}
|
|
3477
5146
|
}
|
|
3478
5147
|
chatMessages.insertBefore(fragment, chatMessages.firstChild);
|
|
3479
5148
|
attachAllCopyHandlers(chatMessages);
|
|
3480
5149
|
// Collapse all existing cards; new cards (with animate-in) stay expanded
|
|
3481
|
-
collapseOldToolCards(chatMessages,
|
|
3482
|
-
// Scroll to bottom (newest message) -
|
|
5150
|
+
collapseOldToolCards(chatMessages, insertedEls);
|
|
5151
|
+
// Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
|
|
3483
5152
|
requestAnimationFrame(function() {
|
|
3484
|
-
chatMessages
|
|
5153
|
+
smartScrollToBottom(chatMessages);
|
|
3485
5154
|
});
|
|
3486
5155
|
} else if (msgCount === existingCount && outputHash !== prevHash) {
|
|
3487
|
-
// Same message count but content changed (streaming update)
|
|
3488
|
-
//
|
|
3489
|
-
var
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
var
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
if (newBubble && currentContent.innerHTML !== newBubble.innerHTML) {
|
|
3503
|
-
chatMessages.replaceChild(newEl, firstEl);
|
|
3504
|
-
attachCopyHandler(newEl);
|
|
3505
|
-
// Keep only the single newest tool card expanded, collapse all others
|
|
3506
|
-
var newestMsgEl = chatMessages.querySelector(".chat-message");
|
|
3507
|
-
var allCards = chatMessages.querySelectorAll(".tool-use-card");
|
|
3508
|
-
var newestCard = null;
|
|
3509
|
-
allCards.forEach(function(c) {
|
|
3510
|
-
if (newestMsgEl && newestMsgEl.contains(c)) {
|
|
3511
|
-
if (!newestCard) newestCard = c;
|
|
3512
|
-
else c.classList.add("collapsed");
|
|
3513
|
-
} else {
|
|
3514
|
-
c.classList.add("collapsed");
|
|
3515
|
-
}
|
|
3516
|
-
});
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
5156
|
+
// Same message count but content changed (streaming update). Re-render in place
|
|
5157
|
+
// by index so assistant growth, tool cards, and retroactive message fixes all show up.
|
|
5158
|
+
var existingEls = Array.from(chatMessages.querySelectorAll(".chat-message"));
|
|
5159
|
+
var reversedMessages = messages.slice().reverse();
|
|
5160
|
+
var replacedAny = false;
|
|
5161
|
+
for (var mi = 0; mi < reversedMessages.length && mi < existingEls.length; mi++) {
|
|
5162
|
+
var currentEl = existingEls[mi];
|
|
5163
|
+
var tmpWrap = document.createElement("div");
|
|
5164
|
+
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi]);
|
|
5165
|
+
var replacementEl = tmpWrap.firstElementChild;
|
|
5166
|
+
if (!replacementEl) continue;
|
|
5167
|
+
if (currentEl.innerHTML !== replacementEl.innerHTML || currentEl.className !== replacementEl.className) {
|
|
5168
|
+
chatMessages.replaceChild(replacementEl, currentEl);
|
|
5169
|
+
attachCopyHandler(replacementEl);
|
|
5170
|
+
replacedAny = true;
|
|
3519
5171
|
}
|
|
3520
5172
|
}
|
|
5173
|
+
if (replacedAny) {
|
|
5174
|
+
requestAnimationFrame(function() {
|
|
5175
|
+
smartScrollToBottom(chatMessages);
|
|
5176
|
+
});
|
|
5177
|
+
var newestMsgEl = chatMessages.querySelector(".chat-message");
|
|
5178
|
+
var allCards = chatMessages.querySelectorAll(".tool-use-card");
|
|
5179
|
+
var newestCard = null;
|
|
5180
|
+
allCards.forEach(function(c) {
|
|
5181
|
+
if (newestMsgEl && newestMsgEl.contains(c)) {
|
|
5182
|
+
if (!newestCard) newestCard = c;
|
|
5183
|
+
else c.classList.add("collapsed");
|
|
5184
|
+
} else {
|
|
5185
|
+
c.classList.add("collapsed");
|
|
5186
|
+
}
|
|
5187
|
+
});
|
|
5188
|
+
}
|
|
3521
5189
|
} else if (msgCount < existingCount) {
|
|
3522
5190
|
fullRenderChat();
|
|
3523
5191
|
}
|
|
3524
5192
|
|
|
3525
5193
|
// Update todo progress bar from latest messages
|
|
3526
5194
|
updateTodoProgress(messages);
|
|
3527
|
-
|
|
3528
|
-
// Update real-time token usage display
|
|
3529
|
-
updateTokenUsageDisplay(messages);
|
|
3530
5195
|
}
|
|
3531
5196
|
|
|
3532
5197
|
// Smart scroll: only auto-scroll if user is near bottom
|
|
@@ -3534,7 +5199,7 @@
|
|
|
3534
5199
|
function smartScrollToBottom(container) {
|
|
3535
5200
|
var chatMsgs = container.querySelector ? container.querySelector(".chat-messages") : container;
|
|
3536
5201
|
if (!chatMsgs) chatMsgs = container;
|
|
3537
|
-
var threshold =
|
|
5202
|
+
var threshold = 200;
|
|
3538
5203
|
// column-reverse: scrollTop=0 is the visual bottom; positive = scrolled up
|
|
3539
5204
|
var isNearBottom = chatMsgs.scrollTop < threshold;
|
|
3540
5205
|
if (isNearBottom) {
|
|
@@ -3566,42 +5231,6 @@
|
|
|
3566
5231
|
}
|
|
3567
5232
|
});
|
|
3568
5233
|
|
|
3569
|
-
function updateTokenUsageDisplay(messages) {
|
|
3570
|
-
var display = document.getElementById("token-usage-display");
|
|
3571
|
-
var textEl = document.getElementById("token-usage-text");
|
|
3572
|
-
if (!display || !textEl) return;
|
|
3573
|
-
|
|
3574
|
-
// Calculate total token usage from current messages
|
|
3575
|
-
var totalInput = 0;
|
|
3576
|
-
var totalOutput = 0;
|
|
3577
|
-
var totalCache = 0;
|
|
3578
|
-
var totalCost = 0;
|
|
3579
|
-
|
|
3580
|
-
for (var i = 0; i < messages.length; i++) {
|
|
3581
|
-
var msg = messages[i];
|
|
3582
|
-
if (msg.usage) {
|
|
3583
|
-
if (msg.usage.inputTokens) totalInput += msg.usage.inputTokens;
|
|
3584
|
-
if (msg.usage.outputTokens) totalOutput += msg.usage.outputTokens;
|
|
3585
|
-
if (msg.usage.cacheReadInputTokens) totalCache += msg.usage.cacheReadInputTokens;
|
|
3586
|
-
if (msg.usage.totalCostUsd) totalCost += msg.usage.totalCostUsd;
|
|
3587
|
-
}
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
// Build token usage string
|
|
3591
|
-
var parts = [];
|
|
3592
|
-
if (totalInput > 0) parts.push("输入 " + totalInput);
|
|
3593
|
-
if (totalOutput > 0) parts.push("输出 " + totalOutput);
|
|
3594
|
-
if (totalCache > 0) parts.push("缓存 " + totalCache);
|
|
3595
|
-
if (totalCost > 0) parts.push("$" + totalCost.toFixed(4));
|
|
3596
|
-
|
|
3597
|
-
if (parts.length > 0) {
|
|
3598
|
-
textEl.textContent = parts.join(" · ");
|
|
3599
|
-
display.classList.remove("hidden");
|
|
3600
|
-
} else {
|
|
3601
|
-
display.classList.add("hidden");
|
|
3602
|
-
}
|
|
3603
|
-
}
|
|
3604
|
-
|
|
3605
5234
|
function updateTodoProgress(messages) {
|
|
3606
5235
|
var todos = null;
|
|
3607
5236
|
// Scan all messages for latest TodoWrite tool_use
|
|
@@ -3868,7 +5497,6 @@
|
|
|
3868
5497
|
if (line.indexOf("audited ") !== -1) continue;
|
|
3869
5498
|
if (line.indexOf("found ") !== -1 && line.indexOf(" vulnerabilities") !== -1) continue;
|
|
3870
5499
|
if (line.indexOf("Using ") !== -1 && line.indexOf(" for ") !== -1 && line.indexOf("session") !== -1) continue;
|
|
3871
|
-
if (line.indexOf("Permissions") !== -1 && line.indexOf("mode") !== -1) continue;
|
|
3872
5500
|
if (line.indexOf("You can use") !== -1) continue;
|
|
3873
5501
|
if (line.indexOf("Press ") !== -1 && line.indexOf(" for") !== -1) continue;
|
|
3874
5502
|
if (line.indexOf("type ") === 0 && line.indexOf(" to ") !== -1) continue;
|
|
@@ -4211,8 +5839,8 @@
|
|
|
4211
5839
|
// Format result preview
|
|
4212
5840
|
if (hasResult) {
|
|
4213
5841
|
var lines = resultContent.split("\n");
|
|
4214
|
-
if (lines.length >
|
|
4215
|
-
preview = lines.slice(0,
|
|
5842
|
+
if (lines.length > 10) {
|
|
5843
|
+
preview = lines.slice(0, 10).join("\n") + "\n…";
|
|
4216
5844
|
} else {
|
|
4217
5845
|
preview = resultContent;
|
|
4218
5846
|
}
|
|
@@ -4223,20 +5851,23 @@
|
|
|
4223
5851
|
var fullResult = resultContent;
|
|
4224
5852
|
|
|
4225
5853
|
var expandedHtml = "";
|
|
5854
|
+
var shouldExpand = hasResult;
|
|
4226
5855
|
if (hasResult) {
|
|
4227
|
-
expandedHtml = '<div class="inline-tool-expanded">' +
|
|
5856
|
+
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
4228
5857
|
'<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
|
|
4229
5858
|
'</div>';
|
|
4230
5859
|
} else if (isError) {
|
|
4231
|
-
expandedHtml = '<div class="inline-tool-expanded"><div class="inline-tool-result inline-tool-error">' +
|
|
5860
|
+
expandedHtml = '<div class="inline-tool-expanded" style="display: none;"><div class="inline-tool-result inline-tool-error">' +
|
|
4232
5861
|
escapeHtml(resultContent || "操作失败") + '</div></div>';
|
|
4233
5862
|
} else if (!toolResult) {
|
|
4234
|
-
expandedHtml = '<div class="inline-tool-expanded"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
5863
|
+
expandedHtml = '<div class="inline-tool-expanded" style="display: none;"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
4235
5864
|
}
|
|
4236
5865
|
|
|
4237
5866
|
var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
|
|
5867
|
+
var extraClass = isError ? 'inline-tool-error-inline' : '';
|
|
5868
|
+
if (hasResult) extraClass += ' inline-tool-open';
|
|
4238
5869
|
|
|
4239
|
-
return '<div class="inline-tool ' +
|
|
5870
|
+
return '<div class="inline-tool ' + extraClass + '" ' +
|
|
4240
5871
|
'data-result="' + escapeHtml(fullResult) + '" ' +
|
|
4241
5872
|
'data-preview="' + previewDataAttr + '" ' +
|
|
4242
5873
|
'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
|
|
@@ -4305,6 +5936,23 @@
|
|
|
4305
5936
|
}
|
|
4306
5937
|
|
|
4307
5938
|
// GitHub-style diff display for Edit/Write/MultiEdit
|
|
5939
|
+
function extractToolResultText(content) {
|
|
5940
|
+
if (!content) return "";
|
|
5941
|
+
if (typeof content === "string") return content;
|
|
5942
|
+
if (Array.isArray(content)) {
|
|
5943
|
+
return content.map(function(item) {
|
|
5944
|
+
if (!item || typeof item !== "object") return "";
|
|
5945
|
+
if (item.type === "text" && typeof item.text === "string") return item.text;
|
|
5946
|
+
try {
|
|
5947
|
+
return JSON.stringify(item);
|
|
5948
|
+
} catch (e) {
|
|
5949
|
+
return "";
|
|
5950
|
+
}
|
|
5951
|
+
}).filter(Boolean).join("\n");
|
|
5952
|
+
}
|
|
5953
|
+
return "";
|
|
5954
|
+
}
|
|
5955
|
+
|
|
4308
5956
|
function renderDiffTool(block, toolResult, toolName) {
|
|
4309
5957
|
var inputData = block.input || {};
|
|
4310
5958
|
var path = inputData.file_path || inputData.path || "";
|
|
@@ -4317,7 +5965,8 @@
|
|
|
4317
5965
|
|
|
4318
5966
|
var isWrite = toolName === "Write" || toolName === "MultiEdit";
|
|
4319
5967
|
var isError = toolResult && toolResult.is_error;
|
|
4320
|
-
var
|
|
5968
|
+
var toolResultText = extractToolResultText(toolResult && toolResult.content);
|
|
5969
|
+
var hasResult = !!(toolResultText && toolResultText.trim().length > 0);
|
|
4321
5970
|
|
|
4322
5971
|
// Build side-by-side diff HTML (old | new columns)
|
|
4323
5972
|
var leftCol = "";
|
|
@@ -4340,7 +5989,9 @@
|
|
|
4340
5989
|
if (toolResult) {
|
|
4341
5990
|
if (isError) {
|
|
4342
5991
|
statusClass = "diff-error";
|
|
4343
|
-
statusText = "
|
|
5992
|
+
statusText = toolResultText.indexOf("haven't granted") !== -1 || toolResultText.indexOf("permission") !== -1
|
|
5993
|
+
? "⏸ 等待授权"
|
|
5994
|
+
: "❌ 修改失败";
|
|
4344
5995
|
} else {
|
|
4345
5996
|
statusClass = "diff-success";
|
|
4346
5997
|
statusText = "✅ 已修改";
|
|
@@ -4372,9 +6023,7 @@
|
|
|
4372
6023
|
|
|
4373
6024
|
function formatInlineResult(content, toolName) {
|
|
4374
6025
|
if (!content) return '<span class="inline-tool-empty">无输出</span>';
|
|
4375
|
-
|
|
4376
|
-
var displayLines = lines.length > 8 ? lines.slice(0, 8).join("\n") + "\n…" : content;
|
|
4377
|
-
return '<pre class="inline-tool-result-text">' + escapeHtml(displayLines) + '</pre>';
|
|
6026
|
+
return '<pre class="inline-tool-result-text" style="max-height: 300px; overflow-y: auto;">' + escapeHtml(content) + '</pre>';
|
|
4378
6027
|
}
|
|
4379
6028
|
|
|
4380
6029
|
function renderToolUseCard(block, toolResult, index) {
|