@co0ontty/wand 0.3.0 → 0.4.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 +1 -1
- package/dist/avatar.d.ts +14 -0
- package/dist/avatar.js +110 -0
- package/dist/claude-pty-bridge.d.ts +0 -2
- package/dist/claude-pty-bridge.js +63 -93
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +10 -2
- package/dist/config.js +6 -2
- package/dist/message-parser.js +9 -89
- package/dist/middleware/path-safety.d.ts +6 -0
- package/dist/middleware/path-safety.js +19 -0
- package/dist/middleware/rate-limit.d.ts +8 -0
- package/dist/middleware/rate-limit.js +37 -0
- package/dist/process-manager.d.ts +52 -4
- package/dist/process-manager.js +1025 -125
- package/dist/pty-text-utils.d.ts +13 -0
- package/dist/pty-text-utils.js +84 -0
- package/dist/pwa.d.ts +5 -0
- package/dist/pwa.js +118 -0
- package/dist/server.js +346 -559
- package/dist/session-lifecycle.js +17 -12
- package/dist/session-logger.d.ts +13 -3
- package/dist/session-logger.js +56 -5
- package/dist/storage.d.ts +9 -0
- package/dist/storage.js +62 -7
- package/dist/types.d.ts +8 -2
- package/dist/web-ui/content/icon-192.png +0 -0
- package/dist/web-ui/content/icon-512.png +0 -0
- package/dist/web-ui/content/scripts.js +1571 -302
- package/dist/web-ui/content/styles.css +882 -669
- package/dist/web-ui/index.js +2 -2
- package/dist/ws-broadcast.d.ts +27 -0
- package/dist/ws-broadcast.js +160 -0
- package/package.json +1 -1
|
@@ -19,8 +19,40 @@
|
|
|
19
19
|
console.log('SW registration failed:', e.message || e);
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
|
+
|
|
23
|
+
// Auto-reload when a new service worker takes control (e.g. after update)
|
|
24
|
+
var reloading = false;
|
|
25
|
+
navigator.serviceWorker.addEventListener('controllerchange', function() {
|
|
26
|
+
if (reloading) return;
|
|
27
|
+
reloading = true;
|
|
28
|
+
location.reload();
|
|
29
|
+
});
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
// PWA display mode detection
|
|
33
|
+
(function() {
|
|
34
|
+
function detectDisplayMode() {
|
|
35
|
+
var mode = 'browser';
|
|
36
|
+
if (window.matchMedia('(display-mode: window-controls-overlay)').matches) {
|
|
37
|
+
mode = 'window-controls-overlay';
|
|
38
|
+
} else if (window.matchMedia('(display-mode: standalone)').matches) {
|
|
39
|
+
mode = 'standalone';
|
|
40
|
+
} else if (window.matchMedia('(display-mode: fullscreen)').matches) {
|
|
41
|
+
mode = 'fullscreen';
|
|
42
|
+
} else if (navigator.standalone === true) {
|
|
43
|
+
mode = 'standalone'; // iOS Safari
|
|
44
|
+
}
|
|
45
|
+
document.documentElement.setAttribute('data-display-mode', mode);
|
|
46
|
+
document.documentElement.classList.toggle('is-pwa', mode !== 'browser');
|
|
47
|
+
return mode;
|
|
48
|
+
}
|
|
49
|
+
detectDisplayMode();
|
|
50
|
+
// Re-detect when display mode changes (e.g., user toggles WCO)
|
|
51
|
+
['standalone', 'window-controls-overlay', 'fullscreen'].forEach(function(m) {
|
|
52
|
+
window.matchMedia('(display-mode: ' + m + ')').addEventListener('change', detectDisplayMode);
|
|
53
|
+
});
|
|
54
|
+
})();
|
|
55
|
+
|
|
24
56
|
(function() {
|
|
25
57
|
var configPath = "${escapeHtml(configPath)}";
|
|
26
58
|
|
|
@@ -36,6 +68,15 @@
|
|
|
36
68
|
fitAddon: null,
|
|
37
69
|
terminalSessionId: null,
|
|
38
70
|
terminalOutput: "",
|
|
71
|
+
terminalViewportSize: { width: 0, height: 0 },
|
|
72
|
+
terminalAutoFollow: true,
|
|
73
|
+
terminalScrollIdleTimer: null,
|
|
74
|
+
terminalScrollIdleMs: 1800,
|
|
75
|
+
terminalScrollThreshold: 12,
|
|
76
|
+
showTerminalJumpToBottom: false,
|
|
77
|
+
terminalViewportEl: null,
|
|
78
|
+
terminalViewportScrollHandler: null,
|
|
79
|
+
terminalViewportTouchHandler: null,
|
|
39
80
|
resizeObserver: null,
|
|
40
81
|
resizeHandler: null,
|
|
41
82
|
resizeTimer: null,
|
|
@@ -86,9 +127,17 @@
|
|
|
86
127
|
currentTask: null, // Current task title from Claude
|
|
87
128
|
terminalInteractive: false,
|
|
88
129
|
miniKeyboardVisible: false,
|
|
130
|
+
shortcutsExpanded: false,
|
|
89
131
|
modifiers: { ctrl: false, alt: false, shift: false },
|
|
90
132
|
fileSearchQuery: "",
|
|
91
133
|
allFiles: [],
|
|
134
|
+
claudeHistory: [],
|
|
135
|
+
claudeHistoryLoaded: false,
|
|
136
|
+
claudeHistoryExpanded: true,
|
|
137
|
+
claudeHistoryExpandedDirs: {},
|
|
138
|
+
sessionsManageMode: false,
|
|
139
|
+
selectedSessionIds: {},
|
|
140
|
+
selectedClaudeHistoryIds: {},
|
|
92
141
|
// Load last used working directory from localStorage
|
|
93
142
|
workingDir: (function() {
|
|
94
143
|
try {
|
|
@@ -156,8 +205,21 @@
|
|
|
156
205
|
}
|
|
157
206
|
}
|
|
158
207
|
|
|
208
|
+
renderBootLoading();
|
|
159
209
|
restoreLoginSession();
|
|
160
210
|
|
|
211
|
+
function renderBootLoading() {
|
|
212
|
+
var app = document.getElementById("app");
|
|
213
|
+
if (!app) return;
|
|
214
|
+
app.innerHTML =
|
|
215
|
+
'<div class="boot-loading">' +
|
|
216
|
+
'<div class="boot-loading-card">' +
|
|
217
|
+
'<div class="boot-loading-spinner"></div>' +
|
|
218
|
+
'<div class="boot-loading-text">正在恢复会话…</div>' +
|
|
219
|
+
'</div>' +
|
|
220
|
+
'</div>';
|
|
221
|
+
}
|
|
222
|
+
|
|
161
223
|
function restoreLoginSession() {
|
|
162
224
|
fetch("/api/config", { credentials: "same-origin" })
|
|
163
225
|
.then(function(res) {
|
|
@@ -172,16 +234,23 @@
|
|
|
172
234
|
if (!config) return;
|
|
173
235
|
state.config = config;
|
|
174
236
|
state.loginChecked = true;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
237
|
+
requestAnimationFrame(function() {
|
|
238
|
+
// Render the app shell first, THEN load session data into it.
|
|
239
|
+
// Skip updateShellChrome() here — sessions aren't loaded yet.
|
|
240
|
+
// refreshAll() will call updateShellChrome() after sessions arrive.
|
|
241
|
+
try {
|
|
242
|
+
render({ skipShellChrome: true });
|
|
243
|
+
} catch (_e) {
|
|
244
|
+
// render() may fail if external scripts (xterm.js) failed to load;
|
|
245
|
+
// continue with polling and session loading so the app remains functional
|
|
246
|
+
}
|
|
247
|
+
startPolling();
|
|
248
|
+
refreshAll();
|
|
249
|
+
// Auto-load claude history since section defaults to expanded
|
|
250
|
+
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
251
|
+
loadClaudeHistory();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
185
254
|
})
|
|
186
255
|
.catch(function() {
|
|
187
256
|
state.loginChecked = true;
|
|
@@ -189,14 +258,19 @@
|
|
|
189
258
|
});
|
|
190
259
|
}
|
|
191
260
|
|
|
192
|
-
render()
|
|
193
|
-
|
|
194
|
-
function render() {
|
|
261
|
+
function render(options) {
|
|
262
|
+
var skipShellChrome = options && options.skipShellChrome;
|
|
195
263
|
var app = document.getElementById("app");
|
|
196
264
|
var isLoggedIn = state.config !== null;
|
|
197
265
|
var wasModalOpen = state.modalOpen;
|
|
266
|
+
var shouldResetShell = !isLoggedIn || !document.getElementById("output");
|
|
198
267
|
|
|
199
|
-
|
|
268
|
+
if (shouldResetShell) {
|
|
269
|
+
teardownTerminal();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Suppress CSS transitions during initial DOM build
|
|
273
|
+
document.documentElement.classList.add("no-transition");
|
|
200
274
|
|
|
201
275
|
app.innerHTML = isLoggedIn ? renderAppShell() : renderLogin();
|
|
202
276
|
// Reset chat render tracking since DOM was fully replaced
|
|
@@ -207,7 +281,15 @@
|
|
|
207
281
|
updateDrawerState();
|
|
208
282
|
syncComposerModeSelect();
|
|
209
283
|
applyCurrentView();
|
|
210
|
-
|
|
284
|
+
if (!skipShellChrome) {
|
|
285
|
+
updateShellChrome();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Force reflow then re-enable transitions after layout settles
|
|
289
|
+
void document.body.offsetHeight;
|
|
290
|
+
requestAnimationFrame(function() {
|
|
291
|
+
document.documentElement.classList.remove("no-transition");
|
|
292
|
+
});
|
|
211
293
|
|
|
212
294
|
// Restore modal state if it was open
|
|
213
295
|
if (wasModalOpen && state.modalOpen) {
|
|
@@ -223,38 +305,25 @@
|
|
|
223
305
|
|
|
224
306
|
function renderInlineKeyboard() {
|
|
225
307
|
if (!state.selectedId) return "";
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
'
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
'<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
'</
|
|
243
|
-
'<div class="
|
|
244
|
-
|
|
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>' +
|
|
308
|
+
var isTerminal = state.currentView === "terminal";
|
|
309
|
+
if (!isTerminal) return "";
|
|
310
|
+
var keys =
|
|
311
|
+
'<button class="shortcut-key' + (state.modifiers.ctrl ? ' active' : '') + '" data-key="ctrl" type="button">Ctrl</button>' +
|
|
312
|
+
'<button class="shortcut-key' + (state.modifiers.alt ? ' active' : '') + '" data-key="alt" type="button">Alt</button>' +
|
|
313
|
+
'<span class="shortcut-sep">·</span>' +
|
|
314
|
+
'<button class="shortcut-key shortcut-dir" data-key="up" type="button">↑</button>' +
|
|
315
|
+
'<button class="shortcut-key shortcut-dir" data-key="down" type="button">↓</button>' +
|
|
316
|
+
'<button class="shortcut-key shortcut-dir" data-key="left" type="button">←</button>' +
|
|
317
|
+
'<button class="shortcut-key shortcut-dir" data-key="right" type="button">→</button>' +
|
|
318
|
+
'<span class="shortcut-sep">·</span>' +
|
|
319
|
+
'<button class="shortcut-key" data-key="enter" type="button">↵</button>' +
|
|
320
|
+
'<button class="shortcut-key" data-key="ctrl_enter" type="button">C-↵</button>' +
|
|
321
|
+
'<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
|
|
322
|
+
var arrow = state.shortcutsExpanded ? '›' : '‹';
|
|
323
|
+
return '<div class="inline-shortcuts-wrap' + (state.shortcutsExpanded ? ' expanded' : '') + '">' +
|
|
324
|
+
'<button class="shortcuts-toggle' + (state.shortcutsExpanded ? ' active' : '') + '" type="button" title="快捷键">' + arrow + '</button>' +
|
|
325
|
+
'<div class="inline-shortcuts-strip">' + keys + '</div>' +
|
|
326
|
+
'<div class="inline-shortcuts-inline">' + keys + '</div>' +
|
|
258
327
|
'</div>';
|
|
259
328
|
}
|
|
260
329
|
|
|
@@ -297,7 +366,6 @@
|
|
|
297
366
|
'</div>' +
|
|
298
367
|
'<div class="login-body">' +
|
|
299
368
|
'<p class="login-hint">输入 Wand 访问密码以进入控制台。</p>' +
|
|
300
|
-
'<p class="login-tip">如果页面是通过 <strong>https://</strong> 打开的,请改用 <strong>http://</strong> 访问本地服务。</p>' +
|
|
301
369
|
'<div class="field">' +
|
|
302
370
|
'<label class="field-label" for="password">密码</label>' +
|
|
303
371
|
'<div class="password-field">' +
|
|
@@ -324,62 +392,45 @@
|
|
|
324
392
|
var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
|
|
325
393
|
|
|
326
394
|
return '<div class="app-container">' +
|
|
327
|
-
'<
|
|
328
|
-
'<
|
|
329
|
-
'<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
'</span>' +
|
|
333
|
-
'</button>' +
|
|
334
|
-
'<div class="topbar-logo">' +
|
|
335
|
-
'<div class="topbar-logo-icon">W</div>' +
|
|
336
|
-
'</div>' +
|
|
337
|
-
'</div>' +
|
|
338
|
-
'<div class="topbar-center">' +
|
|
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>' +
|
|
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>' +
|
|
345
|
-
'</div>' +
|
|
346
|
-
'<div class="topbar-right">' +
|
|
347
|
-
'<button id="file-panel-toggle-btn" class="topbar-btn square' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件">📁</button>' +
|
|
348
|
-
'<button id="topbar-new-session-button" class="topbar-new-btn" title="新对话">' +
|
|
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>' +
|
|
350
|
-
'新对话' +
|
|
351
|
-
'</button>' +
|
|
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>' +
|
|
358
|
-
'</button>' +
|
|
359
|
-
'</div>' +
|
|
360
|
-
'</header>' +
|
|
395
|
+
'<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="Toggle sidebar">' +
|
|
396
|
+
'<span class="hamburger-icon">' +
|
|
397
|
+
'<span></span><span></span><span></span>' +
|
|
398
|
+
'</span>' +
|
|
399
|
+
'</button>' +
|
|
361
400
|
'<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
|
|
362
401
|
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + '">' +
|
|
363
402
|
'<aside id="sessions-drawer" class="sidebar' + drawerClass + '">' +
|
|
364
403
|
'<div class="sidebar-header">' +
|
|
365
404
|
'<div class="sidebar-header-main">' +
|
|
405
|
+
'<div class="topbar-logo-icon">W</div>' +
|
|
366
406
|
'<span class="sidebar-title">会话</span>' +
|
|
367
407
|
'<span class="session-count" id="session-count">' + String(state.sessions.length) + '</span>' +
|
|
368
408
|
'</div>' +
|
|
369
|
-
'<
|
|
409
|
+
'<div class="sidebar-header-actions">' +
|
|
410
|
+
'<button id="close-drawer-button" class="btn btn-ghost btn-sm sidebar-close" type="button" aria-label="关闭菜单">×</button>' +
|
|
411
|
+
'</div>' +
|
|
370
412
|
'</div>' +
|
|
371
413
|
'<div class="sidebar-body">' +
|
|
372
414
|
'<div id="sessions-panel">' +
|
|
373
|
-
'<p class="sidebar-intro">最近的会话记录会显示在这里</p>' +
|
|
374
415
|
'<div class="sessions-list" id="sessions-list">' + renderSessions() + '</div>' +
|
|
375
416
|
'</div>' +
|
|
376
417
|
'</div>' +
|
|
377
418
|
'<div class="sidebar-footer">' +
|
|
378
419
|
'<button id="drawer-new-session-button" class="btn btn-primary btn-block"><span>+</span> 新会话</button>' +
|
|
420
|
+
'<div class="sidebar-footer-actions">' +
|
|
421
|
+
'<button id="file-panel-toggle-btn" class="btn btn-ghost btn-sm' + (state.filePanelOpen ? " active" : "") + '" type="button" title="查看文件">📁 文件</button>' +
|
|
422
|
+
'<button id="pwa-install-button" class="btn btn-ghost btn-sm hidden" title="安装应用">' +
|
|
423
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 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> 安装' +
|
|
424
|
+
'</button>' +
|
|
425
|
+
'<button id="logout-button" class="btn btn-ghost btn-sm sidebar-logout" type="button" title="退出登录">' +
|
|
426
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="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> 退出' +
|
|
427
|
+
'</button>' +
|
|
428
|
+
'</div>' +
|
|
379
429
|
'</div>' +
|
|
380
430
|
'</aside>' +
|
|
381
431
|
'<main class="main-content">' +
|
|
382
|
-
'<
|
|
432
|
+
'<span class="current-task hidden" id="current-task"></span>' +
|
|
433
|
+
'' +
|
|
383
434
|
// File panel backdrop (mobile)
|
|
384
435
|
'<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
|
|
385
436
|
// File side panel
|
|
@@ -405,7 +456,10 @@
|
|
|
405
456
|
'<button id="terminal-scale-down-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
|
|
406
457
|
'<span class="terminal-scale-overlay-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
|
|
407
458
|
'<button id="terminal-scale-up-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="放大">+</button>' +
|
|
459
|
+
'<span class="terminal-scale-overlay-divider"></span>' +
|
|
460
|
+
'<button id="page-refresh-btn" class="terminal-scale-overlay-btn" type="button" title="刷新页面">↻</button>' +
|
|
408
461
|
'</div>' +
|
|
462
|
+
'<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
|
|
409
463
|
'</div>' +
|
|
410
464
|
'<div id="chat-output" class="chat-container hidden"></div>' +
|
|
411
465
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
@@ -423,7 +477,6 @@
|
|
|
423
477
|
'</div>' +
|
|
424
478
|
'</div>' +
|
|
425
479
|
'</div>' +
|
|
426
|
-
'<div id="chat-output" class="chat-container hidden"></div>' +
|
|
427
480
|
'<div class="input-panel' + (state.selectedId ? "" : " hidden") + '">' +
|
|
428
481
|
'<div id="todo-progress" class="todo-progress hidden">' +
|
|
429
482
|
'<div class="todo-progress-header" id="todo-progress-toggle">' +
|
|
@@ -446,16 +499,23 @@
|
|
|
446
499
|
'<select id="chat-mode-select" class="chat-mode-select" title="仅对新建会话生效">' +
|
|
447
500
|
renderModeOptions(preferredTool, composerMode) +
|
|
448
501
|
'</select>' +
|
|
502
|
+
'<button id="terminal-interactive-toggle-top" class="composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨</button>' +
|
|
503
|
+
'<span class="permission-actions hidden" id="permission-actions">' +
|
|
504
|
+
'<span class="permission-actions-divider"></span>' +
|
|
505
|
+
'<span class="permission-actions-label" id="permission-actions-label">等待授权</span>' +
|
|
506
|
+
'<button id="approve-permission-btn" class="btn btn-permission btn-permission-approve" type="button">批准</button>' +
|
|
507
|
+
'<button id="deny-permission-btn" class="btn btn-permission btn-permission-deny" type="button">拒绝</button>' +
|
|
508
|
+
'</span>' +
|
|
449
509
|
'</div>' +
|
|
450
510
|
'<div class="input-composer-right">' +
|
|
451
511
|
'<span id="queue-counter" class="queue-counter hidden">队列: 0</span>' +
|
|
452
512
|
'<span class="input-hint' + (state.terminalInteractive ? ' terminal-interactive-hint' : state.currentView === "terminal" ? " hidden" : "") + '">' + (state.terminalInteractive ? '终端交互中 · Ctrl+C 中断 · Ctrl+L 清屏' : 'Enter 发送 · Shift+Enter 换行') + '</span>' +
|
|
453
513
|
renderInlineKeyboard() +
|
|
454
514
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
455
|
-
'<svg width="
|
|
515
|
+
'<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><rect x="3" y="3" width="10" height="10" rx="2"/></svg>' +
|
|
456
516
|
'</button>' +
|
|
457
517
|
'<button id="send-input-button" class="btn-circle btn-circle-send" title="发送">' +
|
|
458
|
-
'<svg width="
|
|
518
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' +
|
|
459
519
|
'</button>' +
|
|
460
520
|
'</div>' +
|
|
461
521
|
'</div>' +
|
|
@@ -466,6 +526,7 @@
|
|
|
466
526
|
'<span id="session-mode-display" class="session-mode-display">' + (selectedSession ? getModeLabel(selectedSession.mode) : '默认') + '</span>' +
|
|
467
527
|
'<span class="session-info-separator">|</span>' +
|
|
468
528
|
'<span id="session-status-display" class="session-status-display">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '-') + '</span>' +
|
|
529
|
+
(selectedSession && selectedSession.claudeSessionId ? '<span class="session-info-separator">|</span><span id="claude-session-id-badge" class="claude-session-id-badge" data-claude-id="' + escapeHtml(selectedSession.claudeSessionId) + '" title="点击复制 Claude 会话 ID">☁ ' + escapeHtml(selectedSession.claudeSessionId.slice(0, 8)) + '</span>' : '') +
|
|
469
530
|
'<span class="session-info-separator">|</span>' +
|
|
470
531
|
'<span id="session-exit-display" class="session-exit-display">exit=' + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : 'n/a') + '</span>' +
|
|
471
532
|
'</div>' +
|
|
@@ -524,28 +585,357 @@
|
|
|
524
585
|
}
|
|
525
586
|
|
|
526
587
|
function renderSessions() {
|
|
527
|
-
if (state.sessions.length === 0) {
|
|
528
|
-
return '<div class="empty-state"><strong>还没有会话记录</strong><br>点击上方「新对话」开始你的第一次对话。</div>';
|
|
529
|
-
}
|
|
530
588
|
var activeSessions = state.sessions.filter(function(session) { return !session.archived; });
|
|
531
589
|
var archivedSessions = state.sessions.filter(function(session) { return session.archived; });
|
|
532
590
|
var groups = [];
|
|
533
|
-
|
|
534
|
-
|
|
591
|
+
groups.push(renderSessionManageBar());
|
|
592
|
+
|
|
593
|
+
// Split claude history into recent (24h) and older
|
|
594
|
+
var recentHistorySessions = [];
|
|
595
|
+
if (state.claudeHistoryLoaded) {
|
|
596
|
+
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
597
|
+
recentHistorySessions = getVisibleClaudeHistorySessions().filter(function(s) {
|
|
598
|
+
return s.timestamp && new Date(s.timestamp).getTime() > cutoff;
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (activeSessions.length > 0 || recentHistorySessions.length > 0) {
|
|
603
|
+
groups.push(renderRecentGroup(activeSessions, recentHistorySessions));
|
|
535
604
|
}
|
|
536
605
|
if (archivedSessions.length > 0) {
|
|
537
|
-
groups.push(renderSessionGroup("已归档", archivedSessions));
|
|
606
|
+
groups.push(renderSessionGroup("已归档", archivedSessions, "sessions"));
|
|
607
|
+
}
|
|
608
|
+
groups.push(renderClaudeHistorySection());
|
|
609
|
+
if (activeSessions.length === 0 && archivedSessions.length === 0 && recentHistorySessions.length === 0) {
|
|
610
|
+
return renderSessionManageBar() + '<div class="empty-state"><strong>还没有会话记录</strong><br>点击上方「新对话」开始你的第一次对话。</div>' + renderClaudeHistorySection();
|
|
538
611
|
}
|
|
539
612
|
return groups.join("");
|
|
540
613
|
}
|
|
541
614
|
|
|
542
|
-
function
|
|
615
|
+
function renderSessionManageBar() {
|
|
616
|
+
if (!state.sessionsManageMode) {
|
|
617
|
+
return '<div class="session-manage-bar">' +
|
|
618
|
+
'<span class="sidebar-intro">最近的会话记录</span>' +
|
|
619
|
+
'<button class="session-manage-toggle" data-action="toggle-manage-mode" type="button">管理</button>' +
|
|
620
|
+
'</div>';
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
var sessionCount = getSelectedSessionIds().length;
|
|
624
|
+
var historyCount = getSelectedClaudeHistoryIds().length;
|
|
625
|
+
var totalCount = sessionCount + historyCount;
|
|
626
|
+
var hasAny = totalCount > 0;
|
|
627
|
+
|
|
628
|
+
return '<div class="session-manage-bar active">' +
|
|
629
|
+
'<div class="session-manage-summary">已选择 ' + totalCount + ' 项</div>' +
|
|
630
|
+
'<div class="session-manage-actions">' +
|
|
631
|
+
'<button class="session-manage-btn" data-action="select-all-visible" type="button">全选</button>' +
|
|
632
|
+
'<button class="session-manage-btn" data-action="clear-selection" type="button">清空</button>' +
|
|
633
|
+
'<button class="session-manage-btn danger" data-action="delete-selected" type="button"' + (hasAny ? '' : ' disabled') + '>删除所选</button>' +
|
|
634
|
+
'<button class="session-manage-btn" data-action="toggle-manage-mode" type="button">完成</button>' +
|
|
635
|
+
'</div>' +
|
|
636
|
+
'</div>';
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function renderSessionGroup(title, sessions, kind) {
|
|
543
640
|
return '<section class="session-group">' +
|
|
544
641
|
'<div class="session-group-title">' + escapeHtml(title) + '</div>' +
|
|
545
|
-
sessions.map(renderSessionItem).join("") +
|
|
642
|
+
sessions.map(function(session) { return renderSessionItem(session, kind); }).join("") +
|
|
546
643
|
'</section>';
|
|
547
644
|
}
|
|
548
645
|
|
|
646
|
+
function renderRecentGroup(activeSessions, recentHistorySessions) {
|
|
647
|
+
var html = '<section class="session-group">' +
|
|
648
|
+
'<div class="session-group-title">最近</div>';
|
|
649
|
+
html += activeSessions.map(function(session) { return renderSessionItem(session, "sessions"); }).join("");
|
|
650
|
+
html += recentHistorySessions.map(function(session) { return renderClaudeHistoryItem(session, "history"); }).join("");
|
|
651
|
+
html += '</section>';
|
|
652
|
+
return html;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function renderClaudeHistorySection() {
|
|
656
|
+
// Exclude recent 24h items from history section
|
|
657
|
+
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
658
|
+
var visibleHistory = getVisibleClaudeHistorySessions().filter(function(s) {
|
|
659
|
+
return !s.timestamp || new Date(s.timestamp).getTime() <= cutoff;
|
|
660
|
+
});
|
|
661
|
+
var chevron = state.claudeHistoryExpanded ? "▾" : "▸";
|
|
662
|
+
var countBadge = state.claudeHistoryLoaded && visibleHistory.length > 0
|
|
663
|
+
? ' <span class="history-count">' + visibleHistory.length + '</span>'
|
|
664
|
+
: '';
|
|
665
|
+
var clearAllButton = state.claudeHistoryExpanded && state.claudeHistoryLoaded && visibleHistory.length > 0
|
|
666
|
+
? '<button class="session-manage-btn danger compact" data-action="clear-all-history" type="button">清空历史</button>'
|
|
667
|
+
: '';
|
|
668
|
+
var header = '<div class="session-group-title claude-history-toggle" id="claude-history-toggle">' +
|
|
669
|
+
'<span class="chevron">' + chevron + '</span> Claude 历史' + countBadge +
|
|
670
|
+
'</div>' + clearAllButton;
|
|
671
|
+
|
|
672
|
+
if (!state.claudeHistoryExpanded) {
|
|
673
|
+
return '<section class="session-group">' + header + '</section>';
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!state.claudeHistoryLoaded) {
|
|
677
|
+
return '<section class="session-group">' + header +
|
|
678
|
+
'<div class="claude-history-loading">扫描历史会话中…</div></section>';
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (visibleHistory.length === 0) {
|
|
682
|
+
return '<section class="session-group">' + header +
|
|
683
|
+
'<div class="claude-history-loading">没有更早的 Claude 历史会话</div></section>';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
var groups = {};
|
|
687
|
+
var groupOrder = [];
|
|
688
|
+
visibleHistory.forEach(function(s) {
|
|
689
|
+
if (!groups[s.cwd]) {
|
|
690
|
+
groups[s.cwd] = [];
|
|
691
|
+
groupOrder.push(s.cwd);
|
|
692
|
+
}
|
|
693
|
+
groups[s.cwd].push(s);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
var html = '';
|
|
697
|
+
groupOrder.forEach(function(cwd) {
|
|
698
|
+
var cwdShort = cwd.split("/").filter(Boolean).slice(-3).join("/");
|
|
699
|
+
var isDirExpanded = !!state.claudeHistoryExpandedDirs[cwd];
|
|
700
|
+
html += renderClaudeHistoryDirectoryHeader(cwd, cwdShort, groups[cwd].length, isDirExpanded);
|
|
701
|
+
if (isDirExpanded) {
|
|
702
|
+
html += groups[cwd].map(function(session) { return renderClaudeHistoryItem(session, "history"); }).join("");
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return '<section class="session-group">' + header + html + '</section>';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function getVisibleClaudeHistorySessions() {
|
|
710
|
+
return state.claudeHistory.filter(function(s) {
|
|
711
|
+
return s.hasConversation && !s.managedByWand;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function getSelectedSessionIds() {
|
|
716
|
+
return Object.keys(state.selectedSessionIds).filter(function(id) { return !!state.selectedSessionIds[id]; });
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function getSelectedClaudeHistoryIds() {
|
|
720
|
+
return Object.keys(state.selectedClaudeHistoryIds).filter(function(id) { return !!state.selectedClaudeHistoryIds[id]; });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function clearManageSelections() {
|
|
724
|
+
state.selectedSessionIds = {};
|
|
725
|
+
state.selectedClaudeHistoryIds = {};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function toggleManageMode(force) {
|
|
729
|
+
state.sessionsManageMode = typeof force === "boolean" ? force : !state.sessionsManageMode;
|
|
730
|
+
if (!state.sessionsManageMode) {
|
|
731
|
+
clearManageSelections();
|
|
732
|
+
closeSwipedItem();
|
|
733
|
+
}
|
|
734
|
+
updateSessionsList();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function selectAllVisibleItems() {
|
|
738
|
+
var nextSessionIds = {};
|
|
739
|
+
state.sessions.forEach(function(session) {
|
|
740
|
+
nextSessionIds[session.id] = true;
|
|
741
|
+
});
|
|
742
|
+
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
743
|
+
var nextHistoryIds = {};
|
|
744
|
+
getVisibleClaudeHistorySessions().filter(function(session) {
|
|
745
|
+
return !session.timestamp || new Date(session.timestamp).getTime() <= cutoff;
|
|
746
|
+
}).forEach(function(session) {
|
|
747
|
+
nextHistoryIds[session.claudeSessionId] = true;
|
|
748
|
+
});
|
|
749
|
+
state.selectedSessionIds = nextSessionIds;
|
|
750
|
+
state.selectedClaudeHistoryIds = nextHistoryIds;
|
|
751
|
+
updateSessionsList();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function clearSelections() {
|
|
755
|
+
clearManageSelections();
|
|
756
|
+
updateSessionsList();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function toggleManagedItemSelection(kind, id) {
|
|
760
|
+
if (!state.sessionsManageMode || !id) return;
|
|
761
|
+
var target = kind === "history" ? state.selectedClaudeHistoryIds : state.selectedSessionIds;
|
|
762
|
+
if (target[id]) {
|
|
763
|
+
delete target[id];
|
|
764
|
+
} else {
|
|
765
|
+
target[id] = true;
|
|
766
|
+
}
|
|
767
|
+
updateSessionsList();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function renderManageCheckbox(kind, id, label) {
|
|
771
|
+
if (!state.sessionsManageMode) return '';
|
|
772
|
+
var selected = kind === "history" ? !!state.selectedClaudeHistoryIds[id] : !!state.selectedSessionIds[id];
|
|
773
|
+
return '<label class="session-manage-check">' +
|
|
774
|
+
'<input type="checkbox" data-action="toggle-selection" data-kind="' + escapeHtml(kind) + '" data-id="' + escapeHtml(id) + '"' + (selected ? ' checked' : '') + ' aria-label="' + escapeHtml(label) + '">' +
|
|
775
|
+
'<span></span>' +
|
|
776
|
+
'</label>';
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function confirmDelete(message) {
|
|
780
|
+
return window.confirm(message);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function batchDeleteSelected() {
|
|
784
|
+
var sessionIds = getSelectedSessionIds();
|
|
785
|
+
var historyIds = getSelectedClaudeHistoryIds();
|
|
786
|
+
var total = sessionIds.length + historyIds.length;
|
|
787
|
+
if (!total) return;
|
|
788
|
+
if (!confirmDelete('确认删除所选 ' + total + ' 项吗?')) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
var requests = [];
|
|
793
|
+
if (sessionIds.length > 0) {
|
|
794
|
+
requests.push(fetch('/api/sessions/batch-delete', {
|
|
795
|
+
method: 'POST',
|
|
796
|
+
headers: { 'Content-Type': 'application/json' },
|
|
797
|
+
credentials: 'same-origin',
|
|
798
|
+
body: JSON.stringify({ sessionIds: sessionIds })
|
|
799
|
+
}).then(function(res) { return res.json(); }));
|
|
800
|
+
}
|
|
801
|
+
if (historyIds.length > 0) {
|
|
802
|
+
requests.push(fetch('/api/claude-history/batch-delete', {
|
|
803
|
+
method: 'POST',
|
|
804
|
+
headers: { 'Content-Type': 'application/json' },
|
|
805
|
+
credentials: 'same-origin',
|
|
806
|
+
body: JSON.stringify({ claudeSessionIds: historyIds })
|
|
807
|
+
}).then(function(res) { return res.json(); }));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
Promise.all(requests)
|
|
811
|
+
.then(function() {
|
|
812
|
+
if (sessionIds.indexOf(state.selectedId) !== -1) {
|
|
813
|
+
state.selectedId = null;
|
|
814
|
+
persistSelectedId();
|
|
815
|
+
}
|
|
816
|
+
state.claudeHistory = state.claudeHistory.filter(function(session) {
|
|
817
|
+
return historyIds.indexOf(session.claudeSessionId) === -1;
|
|
818
|
+
});
|
|
819
|
+
clearManageSelections();
|
|
820
|
+
return refreshAll();
|
|
821
|
+
})
|
|
822
|
+
.catch(function() {
|
|
823
|
+
var errorEl = document.getElementById('action-error');
|
|
824
|
+
showError(errorEl, '无法批量删除所选项目。');
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function clearAllClaudeHistory() {
|
|
829
|
+
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
830
|
+
var visibleHistory = getVisibleClaudeHistorySessions().filter(function(s) {
|
|
831
|
+
return !s.timestamp || new Date(s.timestamp).getTime() <= cutoff;
|
|
832
|
+
});
|
|
833
|
+
if (!visibleHistory.length) return;
|
|
834
|
+
if (!confirmDelete('确认清空当前显示的 ' + visibleHistory.length + ' 条 Claude 历史吗?')) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
var deleteIds = visibleHistory.map(function(session) { return session.claudeSessionId; });
|
|
838
|
+
return fetch('/api/claude-history/batch-delete', {
|
|
839
|
+
method: 'POST',
|
|
840
|
+
headers: { 'Content-Type': 'application/json' },
|
|
841
|
+
credentials: 'same-origin',
|
|
842
|
+
body: JSON.stringify({ claudeSessionIds: deleteIds })
|
|
843
|
+
})
|
|
844
|
+
.then(function(res) { return res.json(); })
|
|
845
|
+
.then(function(data) {
|
|
846
|
+
if (data && data.error) {
|
|
847
|
+
throw new Error(data.error);
|
|
848
|
+
}
|
|
849
|
+
state.claudeHistory = state.claudeHistory.filter(function(s) {
|
|
850
|
+
return deleteIds.indexOf(s.claudeSessionId) === -1;
|
|
851
|
+
});
|
|
852
|
+
clearManageSelections();
|
|
853
|
+
updateSessionsList();
|
|
854
|
+
})
|
|
855
|
+
.catch(function() {
|
|
856
|
+
var errorEl = document.getElementById('action-error');
|
|
857
|
+
showError(errorEl, '无法清空历史会话。');
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function renderClaudeHistoryDirectoryHeader(cwd, cwdShort, count, isExpanded) {
|
|
862
|
+
var chevron = isExpanded ? "▾" : "▸";
|
|
863
|
+
return '<div class="claude-history-directory-header" data-action="toggle-history-directory" data-cwd="' + escapeHtml(cwd) + '" role="button" tabindex="0">' +
|
|
864
|
+
'<div class="session-group-title claude-history-directory-title">' +
|
|
865
|
+
'<span class="chevron">' + chevron + '</span>' +
|
|
866
|
+
'<span class="claude-history-directory-label">' + escapeHtml(cwdShort) + ' (' + count + ')</span>' +
|
|
867
|
+
'<button class="session-manage-btn danger compact claude-history-directory-clear-btn" data-action="delete-history-directory" data-cwd="' +
|
|
868
|
+
escapeHtml(cwd) + '" type="button" aria-label="清空此目录的历史会话" title="清空此目录的历史会话">清空此目录</button>' +
|
|
869
|
+
'</div>' +
|
|
870
|
+
'</div>';
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function renderClaudeHistoryItem(session, kind) {
|
|
874
|
+
var shortId = session.claudeSessionId.slice(0, 8);
|
|
875
|
+
var preview = session.firstUserMessage || "(空会话)";
|
|
876
|
+
var timeStr = formatHistoryTime(session.timestamp);
|
|
877
|
+
var checkbox = renderManageCheckbox(kind, session.claudeSessionId, "选择历史会话 " + preview);
|
|
878
|
+
var deleteButton = state.sessionsManageMode ? '' :
|
|
879
|
+
'<button class="session-action-btn delete-btn" data-action="delete-history" data-claude-session-id="' +
|
|
880
|
+
session.claudeSessionId + '" type="button" aria-label="删除会话" title="隐藏此历史会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
|
|
881
|
+
var resumeButton = state.sessionsManageMode ? '' :
|
|
882
|
+
'<button class="session-action-btn" data-action="resume-history" data-claude-session-id="' +
|
|
883
|
+
session.claudeSessionId + '" data-cwd="' + escapeHtml(session.cwd) +
|
|
884
|
+
'" type="button" aria-label="恢复会话" title="恢复此 Claude 历史会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
|
|
885
|
+
|
|
886
|
+
return '<div class="session-item claude-history-item' + (state.sessionsManageMode && state.selectedClaudeHistoryIds[session.claudeSessionId] ? ' selected' : '') + '" data-claude-history-id="' + session.claudeSessionId + '" data-cwd="' + escapeHtml(session.cwd) + '" role="button" tabindex="0">' +
|
|
887
|
+
'<div class="session-item-content">' +
|
|
888
|
+
'<div class="session-item-row">' +
|
|
889
|
+
checkbox +
|
|
890
|
+
'<div class="session-main">' +
|
|
891
|
+
'<div class="session-command claude-history-preview">' + escapeHtml(preview) + '</div>' +
|
|
892
|
+
'<div class="session-meta">' +
|
|
893
|
+
'<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>' +
|
|
894
|
+
'<span>' + escapeHtml(timeStr) + '</span>' +
|
|
895
|
+
'</div>' +
|
|
896
|
+
'</div>' +
|
|
897
|
+
'<span class="session-actions">' + resumeButton + deleteButton + '</span>' +
|
|
898
|
+
'</div>' +
|
|
899
|
+
'</div>' +
|
|
900
|
+
'</div>';
|
|
901
|
+
}
|
|
902
|
+
function formatHistoryTime(isoStr) {
|
|
903
|
+
if (!isoStr) return "";
|
|
904
|
+
try {
|
|
905
|
+
var d = new Date(isoStr);
|
|
906
|
+
var now = new Date();
|
|
907
|
+
var diffMs = now - d;
|
|
908
|
+
var diffMin = Math.floor(diffMs / 60000);
|
|
909
|
+
if (diffMin < 1) return "刚刚";
|
|
910
|
+
if (diffMin < 60) return diffMin + " 分钟前";
|
|
911
|
+
var diffHr = Math.floor(diffMin / 60);
|
|
912
|
+
if (diffHr < 24) return diffHr + " 小时前";
|
|
913
|
+
var diffDay = Math.floor(diffHr / 24);
|
|
914
|
+
if (diffDay < 30) return diffDay + " 天前";
|
|
915
|
+
return d.toLocaleDateString();
|
|
916
|
+
} catch (e) {
|
|
917
|
+
return "";
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function loadClaudeHistory() {
|
|
922
|
+
return fetch("/api/claude-history", { credentials: "same-origin" })
|
|
923
|
+
.then(function(res) {
|
|
924
|
+
if (!res.ok) return [];
|
|
925
|
+
return res.json();
|
|
926
|
+
})
|
|
927
|
+
.then(function(sessions) {
|
|
928
|
+
state.claudeHistory = sessions || [];
|
|
929
|
+
state.claudeHistoryLoaded = true;
|
|
930
|
+
updateSessionsList();
|
|
931
|
+
})
|
|
932
|
+
.catch(function() {
|
|
933
|
+
state.claudeHistoryLoaded = true;
|
|
934
|
+
state.claudeHistory = [];
|
|
935
|
+
updateSessionsList();
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
549
939
|
function isMobileLayout() {
|
|
550
940
|
return window.innerWidth <= 768;
|
|
551
941
|
}
|
|
@@ -1059,37 +1449,52 @@
|
|
|
1059
1449
|
|
|
1060
1450
|
function renderSessionItem(session) {
|
|
1061
1451
|
var activeClass = session.id === state.selectedId ? " active" : "";
|
|
1452
|
+
var selectedClass = state.sessionsManageMode && state.selectedSessionIds[session.id] ? " selected" : "";
|
|
1062
1453
|
var metaStatus = getSessionStatusLabel(session);
|
|
1063
1454
|
var metaStatusClass = getSessionStatusClass(session);
|
|
1064
1455
|
var modeName = session.mode === "full-access" ? "全权限" : session.mode === "default" ? "默认" : session.mode === "native" ? "原生" : session.mode === "auto-edit" ? "自动编辑" : session.mode;
|
|
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>';
|
|
1066
1456
|
var resumeButton = "";
|
|
1067
1457
|
var sessionIdDisplay = "";
|
|
1458
|
+
var recoveryHint = "";
|
|
1459
|
+
var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
|
|
1068
1460
|
|
|
1069
|
-
// 如果有 Claude 会话 ID,显示恢复按钮
|
|
1070
1461
|
if (session.claudeSessionId) {
|
|
1071
1462
|
var shortId = session.claudeSessionId.slice(0, 8);
|
|
1072
1463
|
sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
|
|
1073
|
-
if (session.status !== "running") {
|
|
1074
|
-
resumeButton = '<button class="
|
|
1464
|
+
if (session.status !== "running" && !state.sessionsManageMode) {
|
|
1465
|
+
resumeButton = '<button class="session-action-btn" data-action="resume" data-session-id="' + session.id + '" type="button" aria-label="恢复会话" title="恢复 Claude 会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
|
|
1075
1466
|
}
|
|
1076
1467
|
}
|
|
1077
1468
|
|
|
1078
|
-
|
|
1079
|
-
'<
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1469
|
+
if (session.autoRecovered) {
|
|
1470
|
+
recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
|
|
1471
|
+
} else if (session.resumedToSessionId) {
|
|
1472
|
+
recoveryHint = '<span class="session-id" title="已恢复到新会话">已恢复</span>';
|
|
1473
|
+
} else if (session.resumedFromSessionId) {
|
|
1474
|
+
recoveryHint = '<span class="session-id" title="从旧会话恢复而来">续接</span>';
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
var deleteButton = state.sessionsManageMode ? '' : '<button class="session-action-btn delete-btn" data-action="delete-session" data-session-id="' + session.id + '" type="button" aria-label="删除会话" title="删除此会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
|
|
1478
|
+
var actionsHtml = '<span class="session-actions">' + resumeButton + deleteButton + '</span>';
|
|
1479
|
+
|
|
1480
|
+
return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
|
|
1481
|
+
'<div class="session-item-content">' +
|
|
1482
|
+
'<div class="session-item-row">' +
|
|
1483
|
+
checkbox +
|
|
1484
|
+
'<div class="session-main">' +
|
|
1485
|
+
'<div class="session-command">' + escapeHtml(session.command) + '</div>' +
|
|
1486
|
+
'<div class="session-meta">' +
|
|
1487
|
+
'<span>' + escapeHtml(modeName) + '</span>' +
|
|
1488
|
+
'<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
|
|
1489
|
+
sessionIdDisplay +
|
|
1490
|
+
recoveryHint +
|
|
1491
|
+
'</div>' +
|
|
1086
1492
|
'</div>' +
|
|
1493
|
+
actionsHtml +
|
|
1087
1494
|
'</div>' +
|
|
1088
|
-
'<span class="session-actions">' + resumeButton + deleteButton + '</span>' +
|
|
1089
1495
|
'</div>' +
|
|
1090
1496
|
'</div>';
|
|
1091
1497
|
}
|
|
1092
|
-
|
|
1093
1498
|
function renderModeCards(selectedMode) {
|
|
1094
1499
|
var modes = [
|
|
1095
1500
|
{ id: "default", label: "标准", desc: "逐步确认操作" },
|
|
@@ -1120,6 +1525,13 @@
|
|
|
1120
1525
|
'<button id="close-modal-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
1121
1526
|
'</div>' +
|
|
1122
1527
|
'<div class="modal-body">' +
|
|
1528
|
+
'<div class="field">' +
|
|
1529
|
+
'<label class="field-label">模式</label>' +
|
|
1530
|
+
'<div id="mode-cards" class="mode-cards">' +
|
|
1531
|
+
renderModeCards(modalMode) +
|
|
1532
|
+
'</div>' +
|
|
1533
|
+
'<p id="mode-description" class="field-hint">' + escapeHtml(getToolModeHint(modalTool, modalMode)) + '</p>' +
|
|
1534
|
+
'</div>' +
|
|
1123
1535
|
'<div class="field">' +
|
|
1124
1536
|
'<label class="field-label" for="cwd">工作目录</label>' +
|
|
1125
1537
|
'<div class="suggestions-wrap">' +
|
|
@@ -1127,13 +1539,7 @@
|
|
|
1127
1539
|
'<div id="cwd-suggestions" class="suggestions hidden"></div>' +
|
|
1128
1540
|
'</div>' +
|
|
1129
1541
|
'<p class="field-hint">会话将在此目录启动,支持路径自动补全。</p>' +
|
|
1130
|
-
|
|
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>' +
|
|
1542
|
+
'<div id="recent-paths-bubbles" class="recent-paths-bubbles"></div>' +
|
|
1137
1543
|
'</div>' +
|
|
1138
1544
|
'<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
|
|
1139
1545
|
'<p id="modal-error" class="error-message hidden"></p>' +
|
|
@@ -1300,8 +1706,12 @@
|
|
|
1300
1706
|
if (sessionsList) {
|
|
1301
1707
|
sessionsList.addEventListener("click", handleSessionItemClick);
|
|
1302
1708
|
sessionsList.addEventListener("keydown", handleSessionItemKeydown);
|
|
1709
|
+
initSwipeToDelete(sessionsList);
|
|
1303
1710
|
}
|
|
1304
1711
|
|
|
1712
|
+
// Claude session ID badge click-to-copy (event delegation on document)
|
|
1713
|
+
document.addEventListener("click", handleClaudeIdCopy);
|
|
1714
|
+
|
|
1305
1715
|
var modeCardsEl = document.getElementById("mode-cards");
|
|
1306
1716
|
if (modeCardsEl) modeCardsEl.addEventListener("click", function(e) {
|
|
1307
1717
|
var card = e.target.closest(".mode-card");
|
|
@@ -1375,7 +1785,7 @@
|
|
|
1375
1785
|
inputBox.addEventListener("paste", handleInputPaste);
|
|
1376
1786
|
inputBox.addEventListener("input", function() {
|
|
1377
1787
|
refreshInputBoxState(inputBox);
|
|
1378
|
-
setDraftValue(inputBox.value);
|
|
1788
|
+
setDraftValue(inputBox.value, true);
|
|
1379
1789
|
});
|
|
1380
1790
|
inputBox.addEventListener("focus", function() {
|
|
1381
1791
|
// Close drawer when user focuses input to avoid backdrop blocking clicks
|
|
@@ -1394,21 +1804,34 @@
|
|
|
1394
1804
|
var toggle = document.getElementById(id);
|
|
1395
1805
|
if (toggle) toggle.addEventListener("click", toggleTerminalInteractive);
|
|
1396
1806
|
});
|
|
1397
|
-
//
|
|
1398
|
-
var
|
|
1399
|
-
if (
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
var
|
|
1406
|
-
var
|
|
1407
|
-
if (
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1807
|
+
// Inline shortcuts click handler
|
|
1808
|
+
var inlineShortcutsWrap = document.querySelector(".inline-shortcuts-wrap");
|
|
1809
|
+
if (inlineShortcutsWrap) inlineShortcutsWrap.addEventListener("click", handleInlineKeyboardClick);
|
|
1810
|
+
// Shortcuts toggle (mobile fold/unfold)
|
|
1811
|
+
var shortcutsToggleBtn = document.querySelector(".shortcuts-toggle");
|
|
1812
|
+
if (shortcutsToggleBtn) shortcutsToggleBtn.addEventListener("click", function(e) {
|
|
1813
|
+
e.stopPropagation();
|
|
1814
|
+
state.shortcutsExpanded = !state.shortcutsExpanded;
|
|
1815
|
+
var wrap = document.querySelector(".inline-shortcuts-wrap");
|
|
1816
|
+
var toggle = document.querySelector(".shortcuts-toggle");
|
|
1817
|
+
if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
|
|
1818
|
+
if (toggle) {
|
|
1819
|
+
toggle.classList.toggle("active", state.shortcutsExpanded);
|
|
1820
|
+
toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
// Close shortcuts strip on outside click
|
|
1824
|
+
document.addEventListener("click", function(e) {
|
|
1825
|
+
if (!state.shortcutsExpanded) return;
|
|
1826
|
+
var wrap = document.querySelector(".inline-shortcuts-wrap");
|
|
1827
|
+
if (wrap && !wrap.contains(e.target)) {
|
|
1828
|
+
state.shortcutsExpanded = false;
|
|
1829
|
+
wrap.classList.remove("expanded");
|
|
1830
|
+
var toggle = document.querySelector(".shortcuts-toggle");
|
|
1831
|
+
if (toggle) {
|
|
1832
|
+
toggle.classList.remove("active");
|
|
1833
|
+
toggle.textContent = "\u2039";
|
|
1834
|
+
}
|
|
1412
1835
|
}
|
|
1413
1836
|
});
|
|
1414
1837
|
|
|
@@ -1441,6 +1864,12 @@
|
|
|
1441
1864
|
var scaleUpBtn = document.getElementById("terminal-scale-up-top");
|
|
1442
1865
|
if (scaleDownBtn) scaleDownBtn.addEventListener("click", function() { adjustTerminalScale(-0.25); });
|
|
1443
1866
|
if (scaleUpBtn) scaleUpBtn.addEventListener("click", function() { adjustTerminalScale(0.25); });
|
|
1867
|
+
var pageRefreshBtn = document.getElementById("page-refresh-btn");
|
|
1868
|
+
if (pageRefreshBtn) pageRefreshBtn.addEventListener("click", function() { location.reload(); });
|
|
1869
|
+
var jumpBottomBtn = document.getElementById("terminal-jump-bottom");
|
|
1870
|
+
if (jumpBottomBtn) jumpBottomBtn.addEventListener("click", function() {
|
|
1871
|
+
maybeScrollTerminalToBottom("force");
|
|
1872
|
+
});
|
|
1444
1873
|
|
|
1445
1874
|
// File explorer
|
|
1446
1875
|
var fileRefresh = document.getElementById("file-explorer-refresh");
|
|
@@ -1874,34 +2303,289 @@
|
|
|
1874
2303
|
function handleSessionItemClick(event) {
|
|
1875
2304
|
var target = event.target;
|
|
1876
2305
|
if (!target || !(target instanceof Element)) return;
|
|
2306
|
+
|
|
2307
|
+
var historyToggle = target.closest("#claude-history-toggle");
|
|
2308
|
+
if (historyToggle) {
|
|
2309
|
+
event.preventDefault();
|
|
2310
|
+
event.stopPropagation();
|
|
2311
|
+
state.claudeHistoryExpanded = !state.claudeHistoryExpanded;
|
|
2312
|
+
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
2313
|
+
loadClaudeHistory();
|
|
2314
|
+
}
|
|
2315
|
+
updateSessionsList();
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
1877
2319
|
var actionButton = target.closest("[data-action]");
|
|
1878
2320
|
if (actionButton && actionButton instanceof HTMLElement) {
|
|
1879
2321
|
event.preventDefault();
|
|
1880
2322
|
event.stopPropagation();
|
|
1881
|
-
if (actionButton.dataset.action === "
|
|
1882
|
-
|
|
1883
|
-
} else if (actionButton.dataset.action === "
|
|
1884
|
-
|
|
2323
|
+
if (actionButton.dataset.action === "toggle-manage-mode") {
|
|
2324
|
+
toggleManageMode();
|
|
2325
|
+
} else if (actionButton.dataset.action === "select-all-visible") {
|
|
2326
|
+
selectAllVisibleItems();
|
|
2327
|
+
} else if (actionButton.dataset.action === "clear-selection") {
|
|
2328
|
+
clearSelections();
|
|
2329
|
+
} else if (actionButton.dataset.action === "delete-selected") {
|
|
2330
|
+
batchDeleteSelected();
|
|
2331
|
+
} else if (actionButton.dataset.action === "toggle-selection") {
|
|
2332
|
+
toggleManagedItemSelection(actionButton.dataset.kind, actionButton.dataset.id);
|
|
2333
|
+
} else if (actionButton.dataset.action === "delete-session" && actionButton.dataset.sessionId) {
|
|
2334
|
+
if (confirmDelete("确认删除这个会话吗?")) {
|
|
2335
|
+
deleteSession(actionButton.dataset.sessionId);
|
|
2336
|
+
}
|
|
2337
|
+
} else if (actionButton.dataset.action === "delete-history" && actionButton.dataset.claudeSessionId) {
|
|
2338
|
+
if (confirmDelete("确认隐藏这条 Claude 历史吗?")) {
|
|
2339
|
+
executeDeleteHistory(actionButton.dataset.claudeSessionId, actionButton.closest(".session-item"));
|
|
2340
|
+
}
|
|
2341
|
+
} else if (actionButton.dataset.action === "toggle-history-directory" && actionButton.dataset.cwd) {
|
|
2342
|
+
var dirCwd = actionButton.dataset.cwd;
|
|
2343
|
+
state.claudeHistoryExpandedDirs[dirCwd] = !state.claudeHistoryExpandedDirs[dirCwd];
|
|
2344
|
+
updateSessionsList();
|
|
2345
|
+
} else if (actionButton.dataset.action === "delete-history-directory" && actionButton.dataset.cwd) {
|
|
2346
|
+
var deleteCwd = actionButton.dataset.cwd;
|
|
2347
|
+
var items = getHistoryItemsByCwd(deleteCwd);
|
|
2348
|
+
var dirCount = getVisibleClaudeHistorySessions().filter(function(s) { return s.cwd === deleteCwd; }).length;
|
|
2349
|
+
if (confirmDelete("确认清空此目录下的 " + dirCount + " 条 Claude 历史吗?")) {
|
|
2350
|
+
setDeletingState(items, true);
|
|
2351
|
+
deleteClaudeHistoryDirectory(deleteCwd, actionButton, items);
|
|
2352
|
+
}
|
|
2353
|
+
} else if (actionButton.dataset.action === "clear-all-history") {
|
|
2354
|
+
clearAllClaudeHistory();
|
|
2355
|
+
} else if (actionButton.dataset.action === "resume" && actionButton.dataset.sessionId) {
|
|
2356
|
+
handleResumeAction(actionButton);
|
|
2357
|
+
} else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
|
|
2358
|
+
handleResumeHistoryAction(actionButton);
|
|
1885
2359
|
}
|
|
1886
2360
|
return;
|
|
1887
2361
|
}
|
|
2362
|
+
|
|
1888
2363
|
var item = target.closest(".session-item");
|
|
1889
|
-
if (item
|
|
1890
|
-
|
|
1891
|
-
|
|
2364
|
+
if (item) {
|
|
2365
|
+
if (state.sessionsManageMode) {
|
|
2366
|
+
if (item.dataset.sessionId) {
|
|
2367
|
+
toggleManagedItemSelection("sessions", item.dataset.sessionId);
|
|
2368
|
+
} else if (item.dataset.claudeHistoryId) {
|
|
2369
|
+
toggleManagedItemSelection("history", item.dataset.claudeHistoryId);
|
|
2370
|
+
}
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (item.classList.contains("swiped")) {
|
|
2374
|
+
closeSwipedItem();
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
if (_swipeState) return;
|
|
2378
|
+
if (item.dataset.sessionId) {
|
|
2379
|
+
selectSession(item.dataset.sessionId);
|
|
2380
|
+
closeSessionsDrawer();
|
|
2381
|
+
}
|
|
1892
2382
|
}
|
|
1893
2383
|
}
|
|
1894
2384
|
|
|
1895
2385
|
function handleSessionItemKeydown(event) {
|
|
1896
2386
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1897
2387
|
var item = event.target.closest(".session-item");
|
|
1898
|
-
if (item
|
|
1899
|
-
|
|
2388
|
+
if (!item) return;
|
|
2389
|
+
event.preventDefault();
|
|
2390
|
+
if (state.sessionsManageMode) {
|
|
2391
|
+
if (item.dataset.sessionId) {
|
|
2392
|
+
toggleManagedItemSelection("sessions", item.dataset.sessionId);
|
|
2393
|
+
} else if (item.dataset.claudeHistoryId) {
|
|
2394
|
+
toggleManagedItemSelection("history", item.dataset.claudeHistoryId);
|
|
2395
|
+
}
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (item.dataset.sessionId) {
|
|
1900
2399
|
selectSession(item.dataset.sessionId);
|
|
1901
2400
|
closeSessionsDrawer();
|
|
1902
2401
|
}
|
|
1903
2402
|
}
|
|
1904
2403
|
|
|
2404
|
+
/** Copy Claude session ID from badge to clipboard */
|
|
2405
|
+
function handleClaudeIdCopy(event) {
|
|
2406
|
+
var badge = event.target.closest("#claude-session-id-badge");
|
|
2407
|
+
if (!badge) return;
|
|
2408
|
+
var fullId = badge.dataset.claudeId;
|
|
2409
|
+
if (!fullId) return;
|
|
2410
|
+
navigator.clipboard.writeText(fullId).then(function() {
|
|
2411
|
+
var original = badge.textContent;
|
|
2412
|
+
badge.textContent = "\u2713 已复制";
|
|
2413
|
+
badge.classList.add("copied");
|
|
2414
|
+
setTimeout(function() {
|
|
2415
|
+
badge.textContent = original;
|
|
2416
|
+
badge.classList.remove("copied");
|
|
2417
|
+
}, 1200);
|
|
2418
|
+
}).catch(function() {
|
|
2419
|
+
showToast("复制失败", "error");
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function getTerminalViewport() {
|
|
2424
|
+
if (!state.terminal || !state.terminal.element) return null;
|
|
2425
|
+
if (state.terminalViewportEl && state.terminal.element.contains(state.terminalViewportEl)) {
|
|
2426
|
+
return state.terminalViewportEl;
|
|
2427
|
+
}
|
|
2428
|
+
state.terminalViewportEl = state.terminal.element.querySelector(".xterm-viewport");
|
|
2429
|
+
return state.terminalViewportEl;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function clearTerminalScrollIdleTimer() {
|
|
2433
|
+
if (state.terminalScrollIdleTimer) {
|
|
2434
|
+
clearTimeout(state.terminalScrollIdleTimer);
|
|
2435
|
+
state.terminalScrollIdleTimer = null;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function updateTerminalJumpToBottomButton() {
|
|
2440
|
+
var button = document.getElementById("terminal-jump-bottom");
|
|
2441
|
+
var shouldShow = !!state.selectedId
|
|
2442
|
+
&& state.currentView === "terminal"
|
|
2443
|
+
&& !state.terminalAutoFollow
|
|
2444
|
+
&& !isTerminalNearBottom();
|
|
2445
|
+
state.showTerminalJumpToBottom = shouldShow;
|
|
2446
|
+
if (button) {
|
|
2447
|
+
button.classList.toggle("visible", shouldShow);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function isTerminalNearBottom() {
|
|
2452
|
+
var viewport = getTerminalViewport();
|
|
2453
|
+
if (!viewport) return true;
|
|
2454
|
+
var distance = viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop;
|
|
2455
|
+
return distance <= state.terminalScrollThreshold;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function scrollTerminalToBottom(smooth) {
|
|
2459
|
+
if (!state.terminal) return;
|
|
2460
|
+
if (smooth) {
|
|
2461
|
+
var viewport = getTerminalViewport();
|
|
2462
|
+
if (viewport) {
|
|
2463
|
+
viewport.scrollTo({ top: viewport.scrollHeight, behavior: "smooth" });
|
|
2464
|
+
setTimeout(function() {
|
|
2465
|
+
if (state.terminal) state.terminal.scrollToBottom();
|
|
2466
|
+
}, 160);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
state.terminal.scrollToBottom();
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
function scheduleTerminalResumeFollow() {
|
|
2474
|
+
clearTerminalScrollIdleTimer();
|
|
2475
|
+
updateTerminalJumpToBottomButton();
|
|
2476
|
+
state.terminalScrollIdleTimer = setTimeout(function() {
|
|
2477
|
+
state.terminalScrollIdleTimer = null;
|
|
2478
|
+
state.terminalAutoFollow = true;
|
|
2479
|
+
if (!isTerminalNearBottom()) {
|
|
2480
|
+
scrollTerminalToBottom(true);
|
|
2481
|
+
}
|
|
2482
|
+
updateTerminalJumpToBottomButton();
|
|
2483
|
+
}, state.terminalScrollIdleMs);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
function setTerminalManualScrollActive() {
|
|
2487
|
+
state.terminalAutoFollow = false;
|
|
2488
|
+
updateTerminalJumpToBottomButton();
|
|
2489
|
+
scheduleTerminalResumeFollow();
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function maybeScrollTerminalToBottom(reason) {
|
|
2493
|
+
if (!state.terminal) return;
|
|
2494
|
+
var force = reason === "force";
|
|
2495
|
+
if (force) {
|
|
2496
|
+
state.terminalAutoFollow = true;
|
|
2497
|
+
clearTerminalScrollIdleTimer();
|
|
2498
|
+
scrollTerminalToBottom(false);
|
|
2499
|
+
updateTerminalJumpToBottomButton();
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
if (!state.terminalAutoFollow && !isTerminalNearBottom()) {
|
|
2503
|
+
updateTerminalJumpToBottomButton();
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
state.terminalAutoFollow = true;
|
|
2507
|
+
scrollTerminalToBottom(false);
|
|
2508
|
+
updateTerminalJumpToBottomButton();
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function syncTerminalBuffer(sessionId, output, options) {
|
|
2512
|
+
if (!state.terminal) return false;
|
|
2513
|
+
var normalizedOutput = normalizeTerminalOutput(output || "");
|
|
2514
|
+
var nextSessionId = sessionId || null;
|
|
2515
|
+
var opts = options || {};
|
|
2516
|
+
var mode = opts.mode || "append";
|
|
2517
|
+
var shouldScroll = opts.scroll !== false;
|
|
2518
|
+
var sessionChanged = state.terminalSessionId !== nextSessionId;
|
|
2519
|
+
var currentOutput = state.terminalOutput || "";
|
|
2520
|
+
var wrote = false;
|
|
2521
|
+
|
|
2522
|
+
if (normalizedOutput === currentOutput && !sessionChanged) {
|
|
2523
|
+
if (shouldScroll) maybeScrollTerminalToBottom("output");
|
|
2524
|
+
updateTerminalJumpToBottomButton();
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
if (sessionChanged) {
|
|
2529
|
+
state.terminal.reset();
|
|
2530
|
+
currentOutput = "";
|
|
2531
|
+
state.terminalOutput = "";
|
|
2532
|
+
state.terminalAutoFollow = true;
|
|
2533
|
+
clearTerminalScrollIdleTimer();
|
|
2534
|
+
updateTerminalJumpToBottomButton();
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
if (mode === "replace") {
|
|
2538
|
+
if (normalizedOutput !== currentOutput) {
|
|
2539
|
+
state.terminal.reset();
|
|
2540
|
+
if (normalizedOutput) {
|
|
2541
|
+
state.terminal.write(normalizedOutput);
|
|
2542
|
+
}
|
|
2543
|
+
wrote = true;
|
|
2544
|
+
}
|
|
2545
|
+
} else if (normalizedOutput.length < currentOutput.length && !sessionChanged) {
|
|
2546
|
+
// Ignore regressive snapshots for the active session; wait for an explicit replace.
|
|
2547
|
+
return false;
|
|
2548
|
+
} else if (normalizedOutput.startsWith(currentOutput)) {
|
|
2549
|
+
var delta = normalizedOutput.slice(currentOutput.length);
|
|
2550
|
+
if (delta) {
|
|
2551
|
+
state.terminal.write(delta);
|
|
2552
|
+
wrote = true;
|
|
2553
|
+
}
|
|
2554
|
+
} else if (currentOutput && currentOutput.startsWith(normalizedOutput)) {
|
|
2555
|
+
// Ignore shorter/stale snapshots from polling or reconnect races.
|
|
2556
|
+
return false;
|
|
2557
|
+
} else {
|
|
2558
|
+
state.terminal.reset();
|
|
2559
|
+
if (normalizedOutput) {
|
|
2560
|
+
state.terminal.write(normalizedOutput);
|
|
2561
|
+
}
|
|
2562
|
+
wrote = true;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
state.terminalSessionId = nextSessionId;
|
|
2566
|
+
state.terminalOutput = normalizedOutput;
|
|
2567
|
+
if (shouldScroll && (wrote || sessionChanged || mode === "replace")) {
|
|
2568
|
+
maybeScrollTerminalToBottom(sessionChanged || mode === "replace" ? "force" : "output");
|
|
2569
|
+
} else {
|
|
2570
|
+
updateTerminalJumpToBottomButton();
|
|
2571
|
+
}
|
|
2572
|
+
return wrote || sessionChanged;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function shouldResizeTerminalViewport() {
|
|
2576
|
+
var output = document.getElementById("output");
|
|
2577
|
+
if (!output) return false;
|
|
2578
|
+
var rect = output.getBoundingClientRect();
|
|
2579
|
+
var width = Math.round(rect.width);
|
|
2580
|
+
var height = Math.round(rect.height);
|
|
2581
|
+
if (!width || !height) return false;
|
|
2582
|
+
if (state.terminalViewportSize.width === width && state.terminalViewportSize.height === height) {
|
|
2583
|
+
return false;
|
|
2584
|
+
}
|
|
2585
|
+
state.terminalViewportSize = { width: width, height: height };
|
|
2586
|
+
return true;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
1905
2589
|
function initTerminal() {
|
|
1906
2590
|
var container = document.getElementById("output");
|
|
1907
2591
|
if (!container || state.terminal) return;
|
|
@@ -1913,7 +2597,7 @@
|
|
|
1913
2597
|
state.terminal = new Terminal({
|
|
1914
2598
|
cols: 120,
|
|
1915
2599
|
rows: 36,
|
|
1916
|
-
convertEol:
|
|
2600
|
+
convertEol: true,
|
|
1917
2601
|
disableStdin: false,
|
|
1918
2602
|
cursorBlink: false,
|
|
1919
2603
|
fontFamily: '"Geist Mono", "SF Mono", monospace',
|
|
@@ -1946,19 +2630,62 @@
|
|
|
1946
2630
|
}
|
|
1947
2631
|
});
|
|
1948
2632
|
|
|
1949
|
-
|
|
1950
|
-
|
|
2633
|
+
var fitAddonConstructor =
|
|
2634
|
+
typeof FitAddon !== "undefined" && FitAddon && typeof FitAddon.FitAddon === "function"
|
|
2635
|
+
? FitAddon.FitAddon
|
|
2636
|
+
: null;
|
|
2637
|
+
state.fitAddon = fitAddonConstructor ? new fitAddonConstructor() : null;
|
|
2638
|
+
if (state.fitAddon) {
|
|
2639
|
+
state.terminal.loadAddon(state.fitAddon);
|
|
2640
|
+
} else {
|
|
2641
|
+
console.error("[wand] xterm fit addon failed to load; continuing without fit support.");
|
|
2642
|
+
}
|
|
1951
2643
|
|
|
1952
2644
|
state.terminal.open(container);
|
|
1953
2645
|
applyTerminalScale();
|
|
1954
|
-
state.
|
|
2646
|
+
state.terminalViewportSize = { width: 0, height: 0 };
|
|
2647
|
+
state.terminalAutoFollow = true;
|
|
2648
|
+
clearTerminalScrollIdleTimer();
|
|
2649
|
+
// Double-rAF: wait for browser to complete layout before measuring and fitting
|
|
2650
|
+
if (state.fitAddon) {
|
|
2651
|
+
requestAnimationFrame(function() {
|
|
2652
|
+
requestAnimationFrame(function() {
|
|
2653
|
+
if (state.fitAddon && shouldResizeTerminalViewport()) {
|
|
2654
|
+
state.fitAddon.fit();
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
var viewport = getTerminalViewport();
|
|
2661
|
+
if (viewport) {
|
|
2662
|
+
state.terminalViewportScrollHandler = function() {
|
|
2663
|
+
if (isTerminalNearBottom()) {
|
|
2664
|
+
state.terminalAutoFollow = true;
|
|
2665
|
+
clearTerminalScrollIdleTimer();
|
|
2666
|
+
updateTerminalJumpToBottomButton();
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
setTerminalManualScrollActive();
|
|
2670
|
+
};
|
|
2671
|
+
state.terminalViewportTouchHandler = function() {
|
|
2672
|
+
setTerminalManualScrollActive();
|
|
2673
|
+
};
|
|
2674
|
+
viewport.addEventListener("scroll", state.terminalViewportScrollHandler, { passive: true });
|
|
2675
|
+
viewport.addEventListener("touchmove", state.terminalViewportTouchHandler, { passive: true });
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
container.addEventListener('wheel', function(e) {
|
|
2679
|
+
if (!isTerminalNearBottom() || e.deltaY < 0) {
|
|
2680
|
+
setTerminalManualScrollActive();
|
|
2681
|
+
}
|
|
2682
|
+
e.stopPropagation();
|
|
2683
|
+
}, { passive: true });
|
|
1955
2684
|
|
|
1956
2685
|
if (state.selectedId) {
|
|
1957
2686
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
1958
|
-
if (session
|
|
1959
|
-
|
|
1960
|
-
state.terminal.write(normalizedOutput);
|
|
1961
|
-
state.terminalOutput = normalizedOutput;
|
|
2687
|
+
if (session) {
|
|
2688
|
+
syncTerminalBuffer(session.id, session.output || "", { mode: "replace", scroll: false });
|
|
1962
2689
|
}
|
|
1963
2690
|
} else {
|
|
1964
2691
|
state.terminal.writeln("点击上方「新对话」开始你的第一次对话。");
|
|
@@ -1969,13 +2696,8 @@
|
|
|
1969
2696
|
queueDirectInput(data);
|
|
1970
2697
|
});
|
|
1971
2698
|
|
|
1972
|
-
// 鼠标滚轮支持 - 在终端容器上滚动
|
|
1973
|
-
container.addEventListener('wheel', function(e) {
|
|
1974
|
-
// 总是允许滚动,让 xterm 处理滚轮事件
|
|
1975
|
-
e.stopPropagation();
|
|
1976
|
-
}, { passive: true });
|
|
1977
|
-
|
|
1978
2699
|
container.addEventListener("click", focusInputBox);
|
|
2700
|
+
updateTerminalJumpToBottomButton();
|
|
1979
2701
|
|
|
1980
2702
|
// 初始化拖动调整大小
|
|
1981
2703
|
initTerminalResizeHandle();
|
|
@@ -2051,14 +2773,16 @@
|
|
|
2051
2773
|
state.selectedId = null;
|
|
2052
2774
|
persistSelectedId();
|
|
2053
2775
|
state.sessions = [];
|
|
2776
|
+
state.claudeHistory = [];
|
|
2777
|
+
state.claudeHistoryLoaded = false;
|
|
2778
|
+
state.claudeHistoryExpanded = true;
|
|
2779
|
+
state.claudeHistoryExpandedDirs = {};
|
|
2054
2780
|
state.sessionsDrawerOpen = false;
|
|
2055
2781
|
render();
|
|
2056
2782
|
}
|
|
2057
2783
|
|
|
2058
2784
|
function refreshAll() {
|
|
2059
|
-
return loadSessions()
|
|
2060
|
-
if (state.selectedId) return loadOutput(state.selectedId);
|
|
2061
|
-
});
|
|
2785
|
+
return loadSessions();
|
|
2062
2786
|
}
|
|
2063
2787
|
|
|
2064
2788
|
function getModeLabel(mode) {
|
|
@@ -2250,9 +2974,6 @@
|
|
|
2250
2974
|
}
|
|
2251
2975
|
}
|
|
2252
2976
|
updateShellChrome();
|
|
2253
|
-
if (state.selectedId) {
|
|
2254
|
-
loadOutput(state.selectedId);
|
|
2255
|
-
}
|
|
2256
2977
|
});
|
|
2257
2978
|
}
|
|
2258
2979
|
|
|
@@ -2273,6 +2994,7 @@
|
|
|
2273
2994
|
closeKeyboardPopup();
|
|
2274
2995
|
}
|
|
2275
2996
|
var terminalTitle = selectedSession ? shortCommand(selectedSession.command) : "Wand";
|
|
2997
|
+
var terminalInfo = selectedSession ? getSessionStatusLabel(selectedSession) : "开始对话";
|
|
2276
2998
|
var summaryEl = document.querySelector(".session-summary-value");
|
|
2277
2999
|
var titleEl = document.getElementById("terminal-title");
|
|
2278
3000
|
var infoEl = document.getElementById("terminal-info");
|
|
@@ -2281,10 +3003,10 @@
|
|
|
2281
3003
|
var chatContainer = document.getElementById("chat-output");
|
|
2282
3004
|
var stopBtn = document.getElementById("stop-button");
|
|
2283
3005
|
|
|
2284
|
-
if (summaryEl) summaryEl.textContent = terminalTitle;
|
|
2285
|
-
if (titleEl) titleEl.textContent = terminalTitle;
|
|
2286
|
-
if (infoEl) {
|
|
2287
|
-
infoEl.textContent =
|
|
3006
|
+
if (summaryEl && summaryEl.textContent !== terminalTitle) summaryEl.textContent = terminalTitle;
|
|
3007
|
+
if (titleEl && titleEl.textContent !== terminalTitle) titleEl.textContent = terminalTitle;
|
|
3008
|
+
if (infoEl && infoEl.textContent !== terminalInfo) {
|
|
3009
|
+
infoEl.textContent = terminalInfo;
|
|
2288
3010
|
}
|
|
2289
3011
|
|
|
2290
3012
|
// Update session info bar at bottom
|
|
@@ -2292,10 +3014,34 @@
|
|
|
2292
3014
|
var modeEl = document.getElementById("session-mode-display");
|
|
2293
3015
|
var statusEl = document.getElementById("session-status-display");
|
|
2294
3016
|
var exitEl = document.getElementById("session-exit-display");
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
if (
|
|
3017
|
+
var cwdText = selectedSession && selectedSession.cwd ? selectedSession.cwd : "未设置目录";
|
|
3018
|
+
var modeText = selectedSession ? getModeLabel(selectedSession.mode) : "默认";
|
|
3019
|
+
var exitText = "exit=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
|
|
3020
|
+
if (cwdEl && cwdEl.textContent !== cwdText) cwdEl.textContent = cwdText;
|
|
3021
|
+
if (modeEl && modeEl.textContent !== modeText) modeEl.textContent = modeText;
|
|
3022
|
+
if (statusEl && statusEl.textContent !== terminalInfo) statusEl.textContent = terminalInfo;
|
|
3023
|
+
if (exitEl && exitEl.textContent !== exitText) exitEl.textContent = exitText;
|
|
3024
|
+
|
|
3025
|
+
if (!state.terminal && terminalContainer && selectedSession) {
|
|
3026
|
+
initTerminal();
|
|
3027
|
+
}
|
|
3028
|
+
if (state.terminal && terminalContainer && !terminalContainer.contains(state.terminal.element)) {
|
|
3029
|
+
state.terminal.open(terminalContainer);
|
|
3030
|
+
applyTerminalScale();
|
|
3031
|
+
state.terminalViewportSize = { width: 0, height: 0 };
|
|
3032
|
+
scheduleTerminalResize();
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
if (selectedSession && state.terminal) {
|
|
3036
|
+
syncTerminalBuffer(selectedSession.id, selectedSession.output || "", { mode: "replace" });
|
|
3037
|
+
} else if (!selectedSession) {
|
|
3038
|
+
state.terminalSessionId = null;
|
|
3039
|
+
state.terminalOutput = "";
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
if (state.terminal && selectedSession && state.currentView === "terminal") {
|
|
3043
|
+
maybeScrollTerminalToBottom("view");
|
|
3044
|
+
}
|
|
2299
3045
|
|
|
2300
3046
|
var inputPanel = document.querySelector(".input-panel");
|
|
2301
3047
|
if (selectedSession) {
|
|
@@ -2332,21 +3078,7 @@
|
|
|
2332
3078
|
state.currentMessages = [];
|
|
2333
3079
|
|
|
2334
3080
|
if (state.terminal) {
|
|
2335
|
-
|
|
2336
|
-
state.terminal.reset();
|
|
2337
|
-
state.terminalOutput = "";
|
|
2338
|
-
}
|
|
2339
|
-
var newOutput = normalizeTerminalOutput(data.output || "");
|
|
2340
|
-
if (newOutput.startsWith(state.terminalOutput)) {
|
|
2341
|
-
state.terminal.write(newOutput.slice(state.terminalOutput.length));
|
|
2342
|
-
} else {
|
|
2343
|
-
state.terminal.reset();
|
|
2344
|
-
state.terminal.write(newOutput);
|
|
2345
|
-
}
|
|
2346
|
-
state.terminalSessionId = id;
|
|
2347
|
-
state.terminalOutput = newOutput;
|
|
2348
|
-
state.terminal.scrollToBottom();
|
|
2349
|
-
scheduleTerminalResize();
|
|
3081
|
+
syncTerminalBuffer(id, data.output || "", { mode: "replace" });
|
|
2350
3082
|
}
|
|
2351
3083
|
|
|
2352
3084
|
renderChat(false);
|
|
@@ -2412,6 +3144,7 @@
|
|
|
2412
3144
|
|
|
2413
3145
|
function closeSessionsDrawer() {
|
|
2414
3146
|
if (!state.sessionsDrawerOpen) return;
|
|
3147
|
+
closeSwipedItem();
|
|
2415
3148
|
state.sessionsDrawerOpen = false;
|
|
2416
3149
|
updateLayoutState();
|
|
2417
3150
|
}
|
|
@@ -2431,6 +3164,7 @@
|
|
|
2431
3164
|
state.sessionTool = getPreferredTool();
|
|
2432
3165
|
state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
|
|
2433
3166
|
syncSessionModalUI();
|
|
3167
|
+
loadRecentPathBubbles();
|
|
2434
3168
|
setTimeout(function() {
|
|
2435
3169
|
var modeCardsEl = document.getElementById("mode-cards");
|
|
2436
3170
|
if (modeCardsEl) modeCardsEl.focus();
|
|
@@ -2458,6 +3192,10 @@
|
|
|
2458
3192
|
}
|
|
2459
3193
|
|
|
2460
3194
|
function setupFocusTrap(modal) {
|
|
3195
|
+
if (focusTrapHandler) {
|
|
3196
|
+
document.removeEventListener("keydown", focusTrapHandler);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
2461
3199
|
// Focusable elements selector
|
|
2462
3200
|
var focusableSelector = 'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
|
|
2463
3201
|
|
|
@@ -2646,6 +3384,36 @@
|
|
|
2646
3384
|
});
|
|
2647
3385
|
}
|
|
2648
3386
|
|
|
3387
|
+
function loadRecentPathBubbles() {
|
|
3388
|
+
var container = document.getElementById("recent-paths-bubbles");
|
|
3389
|
+
if (!container) return;
|
|
3390
|
+
fetch("/api/recent-paths", { credentials: "same-origin" })
|
|
3391
|
+
.then(function(res) { return res.json(); })
|
|
3392
|
+
.then(function(items) {
|
|
3393
|
+
if (!items || !items.length) {
|
|
3394
|
+
container.innerHTML = "";
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
container.innerHTML = items.map(function(item) {
|
|
3398
|
+
return '<button class="recent-path-bubble" data-path="' + escapeHtml(item.path) + '" title="' + escapeHtml(item.path) + '">' +
|
|
3399
|
+
escapeHtml(item.name) +
|
|
3400
|
+
'</button>';
|
|
3401
|
+
}).join("");
|
|
3402
|
+
container.querySelectorAll(".recent-path-bubble").forEach(function(el) {
|
|
3403
|
+
el.addEventListener("click", function() {
|
|
3404
|
+
var cwdEl = document.getElementById("cwd");
|
|
3405
|
+
if (cwdEl) {
|
|
3406
|
+
cwdEl.value = el.dataset.path;
|
|
3407
|
+
state.cwdValue = el.dataset.path || "";
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
});
|
|
3411
|
+
})
|
|
3412
|
+
.catch(function() {
|
|
3413
|
+
if (container) container.innerHTML = "";
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
|
|
2649
3417
|
function schedulePathSuggestions() {
|
|
2650
3418
|
if (state.suggestionTimer) clearTimeout(state.suggestionTimer);
|
|
2651
3419
|
state.suggestionTimer = setTimeout(loadPathSuggestions, 120);
|
|
@@ -2720,7 +3488,7 @@
|
|
|
2720
3488
|
// Move cursor to after the inserted newline
|
|
2721
3489
|
inputBox.selectionStart = start + 1;
|
|
2722
3490
|
inputBox.selectionEnd = start + 1;
|
|
2723
|
-
setDraftValue(newValue);
|
|
3491
|
+
setDraftValue(newValue, true);
|
|
2724
3492
|
autoResizeInput(inputBox);
|
|
2725
3493
|
}
|
|
2726
3494
|
return;
|
|
@@ -2735,7 +3503,7 @@
|
|
|
2735
3503
|
setTimeout(function() {
|
|
2736
3504
|
var inputBox = document.getElementById("input-box");
|
|
2737
3505
|
if (inputBox) {
|
|
2738
|
-
setDraftValue(inputBox.value);
|
|
3506
|
+
setDraftValue(inputBox.value, true);
|
|
2739
3507
|
}
|
|
2740
3508
|
}, 0);
|
|
2741
3509
|
return;
|
|
@@ -2749,7 +3517,7 @@
|
|
|
2749
3517
|
var current = inputBox.value;
|
|
2750
3518
|
var newValue = current.slice(0, start) + String.fromCharCode(9) + current.slice(start);
|
|
2751
3519
|
inputBox.value = newValue;
|
|
2752
|
-
setDraftValue(newValue);
|
|
3520
|
+
setDraftValue(newValue, true);
|
|
2753
3521
|
}
|
|
2754
3522
|
return;
|
|
2755
3523
|
}
|
|
@@ -2862,15 +3630,17 @@
|
|
|
2862
3630
|
return "";
|
|
2863
3631
|
}
|
|
2864
3632
|
|
|
2865
|
-
function setDraftValue(value) {
|
|
3633
|
+
function setDraftValue(value, skipDom) {
|
|
2866
3634
|
if (!state.selectedId) return;
|
|
2867
3635
|
state.drafts[state.selectedId] = value;
|
|
2868
3636
|
// Persist to localStorage
|
|
2869
3637
|
try {
|
|
2870
3638
|
localStorage.setItem("wand-draft-" + state.selectedId, value);
|
|
2871
3639
|
} catch (e) { /* ignore */ }
|
|
2872
|
-
|
|
2873
|
-
|
|
3640
|
+
if (!skipDom) {
|
|
3641
|
+
var inputBox = document.getElementById("input-box");
|
|
3642
|
+
if (inputBox) inputBox.value = value;
|
|
3643
|
+
}
|
|
2874
3644
|
}
|
|
2875
3645
|
|
|
2876
3646
|
function autoResizeInput(el) {
|
|
@@ -3053,7 +3823,8 @@
|
|
|
3053
3823
|
if (!state.terminal) initTerminal();
|
|
3054
3824
|
applyCurrentView();
|
|
3055
3825
|
if (state.currentView === "terminal") {
|
|
3056
|
-
|
|
3826
|
+
state.terminalViewportSize = { width: 0, height: 0 };
|
|
3827
|
+
scheduleTerminalResize(true);
|
|
3057
3828
|
}
|
|
3058
3829
|
// Don't call renderChat() here — loadOutput() always calls renderChat() after it resolves.
|
|
3059
3830
|
// Calling renderChat() prematurely would render with stale/empty messages.
|
|
@@ -3079,26 +3850,42 @@
|
|
|
3079
3850
|
terminalInteractive: state.terminalInteractive,
|
|
3080
3851
|
inputLength: value.length
|
|
3081
3852
|
});
|
|
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
|
-
}
|
|
3090
3853
|
// Clear todo progress bar at the start of a new user turn
|
|
3091
3854
|
var todoEl = document.getElementById("todo-progress");
|
|
3092
3855
|
if (todoEl) todoEl.classList.add("hidden");
|
|
3093
3856
|
// Send text + Enter as a single call to avoid race conditions
|
|
3094
3857
|
var combinedInput = value + getControlInput("enter");
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3858
|
+
var isOffline = !state.wsConnected;
|
|
3859
|
+
|
|
3860
|
+
if (isOffline) {
|
|
3861
|
+
// Offline: queue for flush on reconnect, clear input immediately
|
|
3862
|
+
if (state.pendingMessages.length >= 100) {
|
|
3863
|
+
state.pendingMessages.shift();
|
|
3864
|
+
}
|
|
3865
|
+
state.pendingMessages.push(combinedInput);
|
|
3866
|
+
if (inputBox) {
|
|
3867
|
+
inputBox.value = "";
|
|
3868
|
+
autoResizeInput(inputBox);
|
|
3869
|
+
}
|
|
3870
|
+
setDraftValue("");
|
|
3871
|
+
return Promise.resolve();
|
|
3099
3872
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3873
|
+
|
|
3874
|
+
// Online: send via queue, only clear on success
|
|
3875
|
+
return ensureSessionReadyForInput(selectedSession).then(function(readySession) {
|
|
3876
|
+
if (!readySession) {
|
|
3877
|
+
showToast("会话未就绪,将稍后重试。", "info");
|
|
3878
|
+
return null;
|
|
3879
|
+
}
|
|
3880
|
+
return queueDirectInput(combinedInput).then(function() {
|
|
3881
|
+
// Clear input only after the send succeeds
|
|
3882
|
+
if (inputBox && inputBox.value === value) {
|
|
3883
|
+
inputBox.value = "";
|
|
3884
|
+
autoResizeInput(inputBox);
|
|
3885
|
+
}
|
|
3886
|
+
setDraftValue("");
|
|
3887
|
+
});
|
|
3888
|
+
}).catch(function(err) {
|
|
3102
3889
|
showToast(getInputErrorMessage(err), "error");
|
|
3103
3890
|
throw err;
|
|
3104
3891
|
});
|
|
@@ -3108,12 +3895,12 @@
|
|
|
3108
3895
|
|
|
3109
3896
|
function getInputErrorMessage(error) {
|
|
3110
3897
|
if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
|
|
3111
|
-
return "
|
|
3898
|
+
return "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
|
|
3112
3899
|
}
|
|
3113
3900
|
if (error && error.errorCode === "SESSION_NOT_FOUND") {
|
|
3114
|
-
return "
|
|
3901
|
+
return "会话不存在,请重新选择或新建会话。";
|
|
3115
3902
|
}
|
|
3116
|
-
return (error && error.message) || "
|
|
3903
|
+
return (error && error.message) || "会话暂不可用;若存在 Claude 历史会话,将自动尝试恢复。";
|
|
3117
3904
|
}
|
|
3118
3905
|
|
|
3119
3906
|
function buildInputError(payload) {
|
|
@@ -3135,6 +3922,56 @@
|
|
|
3135
3922
|
updateSessionSnapshot({ id: sessionId, status: status || "exited" });
|
|
3136
3923
|
}
|
|
3137
3924
|
|
|
3925
|
+
function hasRealConversationHistory(session) {
|
|
3926
|
+
if (!session || !Array.isArray(session.messages) || session.messages.length < 2) {
|
|
3927
|
+
return false;
|
|
3928
|
+
}
|
|
3929
|
+
var hasUser = session.messages.some(function(turn) {
|
|
3930
|
+
return turn && turn.role === "user" && Array.isArray(turn.content) && turn.content.some(function(block) {
|
|
3931
|
+
return block && block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0;
|
|
3932
|
+
});
|
|
3933
|
+
});
|
|
3934
|
+
var hasAssistant = session.messages.some(function(turn) {
|
|
3935
|
+
return turn && turn.role === "assistant" && Array.isArray(turn.content) && turn.content.some(function(block) {
|
|
3936
|
+
return block && block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0;
|
|
3937
|
+
});
|
|
3938
|
+
});
|
|
3939
|
+
return hasUser && hasAssistant;
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
function canAutoResumeSession(session) {
|
|
3943
|
+
return !!(session && session.status === "exited" && session.claudeSessionId && hasRealConversationHistory(session));
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
function ensureSessionReadyForInput(session, errorEl) {
|
|
3947
|
+
if (!session) {
|
|
3948
|
+
showToast("会话不存在,请重新选择或新建会话。", "error");
|
|
3949
|
+
return Promise.resolve(null);
|
|
3950
|
+
}
|
|
3951
|
+
if (session.status === "running") {
|
|
3952
|
+
return Promise.resolve(session);
|
|
3953
|
+
}
|
|
3954
|
+
if (!canAutoResumeSession(session)) {
|
|
3955
|
+
showToast("该会话没有可恢复的 Claude 历史上下文,请新建会话。", "error");
|
|
3956
|
+
return Promise.resolve(null);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
showToast("正在恢复历史会话…", "info");
|
|
3960
|
+
return resumeClaudeSessionById(session.claudeSessionId, errorEl).then(function(data) {
|
|
3961
|
+
if (!data) return null;
|
|
3962
|
+
updateSessionSnapshot(data);
|
|
3963
|
+
updateSessionsList();
|
|
3964
|
+
switchToSessionView(data.id);
|
|
3965
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
3966
|
+
state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
|
|
3967
|
+
}
|
|
3968
|
+
return loadOutput(data.id).then(function() {
|
|
3969
|
+
focusInputBox(true);
|
|
3970
|
+
return data;
|
|
3971
|
+
});
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3138
3975
|
function queueDirectInput(input) {
|
|
3139
3976
|
if (!input || !state.selectedId) return Promise.resolve();
|
|
3140
3977
|
state.messageQueue.push(input);
|
|
@@ -3154,19 +3991,36 @@
|
|
|
3154
3991
|
|
|
3155
3992
|
// Pre-check: don't send if session is not running
|
|
3156
3993
|
if (!isSelectedSessionRunning()) {
|
|
3994
|
+
// If WebSocket is disconnected, queue for flush on reconnect
|
|
3995
|
+
if (!state.wsConnected) {
|
|
3996
|
+
if (state.pendingMessages.length >= 100) {
|
|
3997
|
+
state.pendingMessages.shift();
|
|
3998
|
+
}
|
|
3999
|
+
state.pendingMessages.push(input);
|
|
4000
|
+
console.log("[wand] postInput: session not running, queued for reconnect", {
|
|
4001
|
+
sessionId: state.selectedId,
|
|
4002
|
+
inputLength: input.length
|
|
4003
|
+
});
|
|
4004
|
+
return Promise.resolve();
|
|
4005
|
+
}
|
|
3157
4006
|
console.warn("[wand] postInput: session not running, skipping send", {
|
|
3158
4007
|
sessionId: state.selectedId
|
|
3159
4008
|
});
|
|
3160
|
-
showToast("
|
|
4009
|
+
showToast("会话未运行,正在等待自动恢复后重试。", "info");
|
|
3161
4010
|
return Promise.resolve();
|
|
3162
4011
|
}
|
|
3163
4012
|
|
|
3164
|
-
// If WebSocket is disconnected, queue the message
|
|
4013
|
+
// If WebSocket is disconnected, queue the message (no HTTP fetch while offline)
|
|
3165
4014
|
if (!state.wsConnected) {
|
|
3166
4015
|
if (state.pendingMessages.length >= 100) {
|
|
3167
4016
|
state.pendingMessages.shift();
|
|
3168
4017
|
}
|
|
3169
4018
|
state.pendingMessages.push(input);
|
|
4019
|
+
console.log("[wand] postInput: WebSocket disconnected, queued message", {
|
|
4020
|
+
sessionId: state.selectedId,
|
|
4021
|
+
inputLength: input.length
|
|
4022
|
+
});
|
|
4023
|
+
return Promise.resolve();
|
|
3170
4024
|
}
|
|
3171
4025
|
|
|
3172
4026
|
console.log("[wand] postInput: sending", {
|
|
@@ -3392,7 +4246,6 @@
|
|
|
3392
4246
|
var toggle = document.getElementById(id);
|
|
3393
4247
|
if (toggle) {
|
|
3394
4248
|
toggle.classList.toggle("active", state.terminalInteractive);
|
|
3395
|
-
toggle.textContent = state.terminalInteractive ? "⌨ 交互开" : "⌨ 交互关";
|
|
3396
4249
|
}
|
|
3397
4250
|
});
|
|
3398
4251
|
// Inline keyboard visibility follows current view
|
|
@@ -3402,16 +4255,6 @@
|
|
|
3402
4255
|
if (inputHint) inputHint.classList.toggle("hidden", state.currentView === "terminal");
|
|
3403
4256
|
var container = document.getElementById("output");
|
|
3404
4257
|
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
4258
|
}
|
|
3416
4259
|
|
|
3417
4260
|
function captureTerminalInput(event) {
|
|
@@ -3442,8 +4285,7 @@
|
|
|
3442
4285
|
}
|
|
3443
4286
|
|
|
3444
4287
|
function handleInlineKeyboardClick(event) {
|
|
3445
|
-
|
|
3446
|
-
var btn = event.target.closest(".ik-key, .kp-key");
|
|
4288
|
+
var btn = event.target.closest(".shortcut-key");
|
|
3447
4289
|
if (!btn) return;
|
|
3448
4290
|
var key = btn.getAttribute("data-key");
|
|
3449
4291
|
if (!key) return;
|
|
@@ -3454,7 +4296,6 @@
|
|
|
3454
4296
|
return;
|
|
3455
4297
|
}
|
|
3456
4298
|
if (key === "ctrl_enter") {
|
|
3457
|
-
// Ctrl+Enter for confirm/approve in terminal
|
|
3458
4299
|
var sequence = buildPtySequence("enter", { ctrl: true, alt: false, shift: false });
|
|
3459
4300
|
if (sequence) sendTerminalSequence(sequence);
|
|
3460
4301
|
return;
|
|
@@ -3466,10 +4307,10 @@
|
|
|
3466
4307
|
}
|
|
3467
4308
|
|
|
3468
4309
|
function updateKeyboardPopupUI() {
|
|
3469
|
-
var
|
|
3470
|
-
if (!
|
|
4310
|
+
var container = document.querySelector(".inline-shortcuts-wrap");
|
|
4311
|
+
if (!container) return;
|
|
3471
4312
|
["ctrl", "alt"].forEach(function(name) {
|
|
3472
|
-
var btn =
|
|
4313
|
+
var btn = container.querySelector('[data-key="' + name + '"]');
|
|
3473
4314
|
if (btn) btn.classList.toggle("active", !!state.modifiers[name]);
|
|
3474
4315
|
});
|
|
3475
4316
|
}
|
|
@@ -3573,17 +4414,60 @@
|
|
|
3573
4414
|
function flushPendingMessages() {
|
|
3574
4415
|
if (state.pendingMessages.length === 0) return;
|
|
3575
4416
|
|
|
3576
|
-
// Send queued messages in order
|
|
4417
|
+
// Send queued messages in order, bypassing the session-running check
|
|
4418
|
+
// since our local state may be stale right after reconnect
|
|
3577
4419
|
var queue = state.pendingMessages.slice();
|
|
3578
4420
|
state.pendingMessages = [];
|
|
3579
4421
|
|
|
4422
|
+
var sendPromise = Promise.resolve();
|
|
3580
4423
|
queue.forEach(function(input) {
|
|
3581
|
-
|
|
3582
|
-
|
|
4424
|
+
sendPromise = sendPromise.then(function() {
|
|
4425
|
+
return sendInputDirect(input).catch(function() {
|
|
4426
|
+
// Ignore errors during flush
|
|
4427
|
+
});
|
|
3583
4428
|
});
|
|
3584
4429
|
});
|
|
3585
4430
|
}
|
|
3586
4431
|
|
|
4432
|
+
function sendInputDirect(input) {
|
|
4433
|
+
if (!input || !state.selectedId) return Promise.resolve();
|
|
4434
|
+
return fetch("/api/sessions/" + state.selectedId + "/input", {
|
|
4435
|
+
method: "POST",
|
|
4436
|
+
headers: { "Content-Type": "application/json" },
|
|
4437
|
+
credentials: "same-origin",
|
|
4438
|
+
body: JSON.stringify({ input: input, view: state.currentView })
|
|
4439
|
+
})
|
|
4440
|
+
.then(function(res) {
|
|
4441
|
+
if (!res.ok) {
|
|
4442
|
+
return res.json().catch(function() { return { error: "请求失败" }; }).then(function(payload) {
|
|
4443
|
+
var error = buildInputError(payload);
|
|
4444
|
+
error.httpStatus = res.status;
|
|
4445
|
+
// Don't re-queue on session-unavailable — the session will auto-resume
|
|
4446
|
+
// on the user's next message, and stale queue items would cause duplicates
|
|
4447
|
+
if (isSessionUnavailableError(error)) {
|
|
4448
|
+
console.log("[wand] sendInputDirect: session unavailable, dropping", {
|
|
4449
|
+
sessionId: state.selectedId,
|
|
4450
|
+
errorCode: error.errorCode
|
|
4451
|
+
});
|
|
4452
|
+
return null;
|
|
4453
|
+
}
|
|
4454
|
+
throw error;
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
return res.json();
|
|
4458
|
+
})
|
|
4459
|
+
.then(function(snapshot) {
|
|
4460
|
+
if (snapshot && snapshot.id) {
|
|
4461
|
+
updateSessionSnapshot(snapshot);
|
|
4462
|
+
if (snapshot.messages && snapshot.messages.length > 0) {
|
|
4463
|
+
state.currentMessages = snapshot.messages;
|
|
4464
|
+
}
|
|
4465
|
+
renderChat(true);
|
|
4466
|
+
}
|
|
4467
|
+
return snapshot;
|
|
4468
|
+
});
|
|
4469
|
+
}
|
|
4470
|
+
|
|
3587
4471
|
function stopSession() {
|
|
3588
4472
|
if (!state.selectedId) return;
|
|
3589
4473
|
fetch("/api/sessions/" + state.selectedId + "/stop", { method: "POST", credentials: "same-origin" })
|
|
@@ -3591,28 +4475,112 @@
|
|
|
3591
4475
|
}
|
|
3592
4476
|
|
|
3593
4477
|
function deleteSession(id) {
|
|
3594
|
-
|
|
3595
|
-
if (
|
|
4478
|
+
var item = document.querySelector('.session-item[data-session-id="' + id + '"]');
|
|
4479
|
+
if (item) {
|
|
4480
|
+
item.classList.add("deleting");
|
|
4481
|
+
}
|
|
4482
|
+
setTimeout(function() {
|
|
4483
|
+
fetch("/api/sessions/" + id, { method: "DELETE", credentials: "same-origin" })
|
|
4484
|
+
.then(function(res) { return res.json(); })
|
|
4485
|
+
.then(function(data) {
|
|
4486
|
+
if (data && data.error) {
|
|
4487
|
+
throw new Error(data.error);
|
|
4488
|
+
}
|
|
4489
|
+
if (state.selectedId === id) {
|
|
4490
|
+
state.selectedId = null;
|
|
4491
|
+
persistSelectedId();
|
|
4492
|
+
}
|
|
4493
|
+
return refreshAll();
|
|
4494
|
+
})
|
|
4495
|
+
.catch(function() {
|
|
4496
|
+
// Remove deleting state on error so item reappears
|
|
4497
|
+
if (item) item.classList.remove("deleting");
|
|
4498
|
+
var errorEl = document.getElementById("action-error");
|
|
4499
|
+
showError(errorEl, "无法删除会话。");
|
|
4500
|
+
});
|
|
4501
|
+
}, 250);
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
function executeDeleteHistory(claudeSessionId, item) {
|
|
4505
|
+
if (item) {
|
|
4506
|
+
item.classList.add("deleting");
|
|
4507
|
+
}
|
|
4508
|
+
setTimeout(function() {
|
|
4509
|
+
fetch("/api/claude-history/" + encodeURIComponent(claudeSessionId), { method: "DELETE", credentials: "same-origin" })
|
|
4510
|
+
.then(function(res) { return res.json(); })
|
|
4511
|
+
.then(function(data) {
|
|
4512
|
+
if (data && data.error) {
|
|
4513
|
+
throw new Error(data.error);
|
|
4514
|
+
}
|
|
4515
|
+
state.claudeHistory = state.claudeHistory.filter(function(s) {
|
|
4516
|
+
return s.claudeSessionId !== claudeSessionId;
|
|
4517
|
+
});
|
|
4518
|
+
delete state.selectedClaudeHistoryIds[claudeSessionId];
|
|
4519
|
+
updateSessionsList();
|
|
4520
|
+
})
|
|
4521
|
+
.catch(function() {
|
|
4522
|
+
if (item) item.classList.remove("deleting");
|
|
4523
|
+
var errorEl = document.getElementById("action-error");
|
|
4524
|
+
showError(errorEl, "无法删除历史会话。");
|
|
4525
|
+
});
|
|
4526
|
+
}, 250);
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
function deleteClaudeHistorySession(claudeSessionId, item) {
|
|
4530
|
+
executeDeleteHistory(claudeSessionId, item);
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
function deleteClaudeHistoryDirectory(cwd, btn, items) {
|
|
4534
|
+
if (!cwd) {
|
|
3596
4535
|
return;
|
|
3597
4536
|
}
|
|
3598
|
-
fetch("/api/
|
|
4537
|
+
fetch("/api/claude-history?cwd=" + encodeURIComponent(cwd), { method: "DELETE", credentials: "same-origin" })
|
|
3599
4538
|
.then(function(res) { return res.json(); })
|
|
3600
4539
|
.then(function(data) {
|
|
3601
4540
|
if (data && data.error) {
|
|
3602
4541
|
throw new Error(data.error);
|
|
3603
4542
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
return refreshAll();
|
|
4543
|
+
state.claudeHistory = state.claudeHistory.filter(function(s) {
|
|
4544
|
+
return s.cwd !== cwd;
|
|
4545
|
+
});
|
|
4546
|
+
updateSessionsList();
|
|
3609
4547
|
})
|
|
3610
4548
|
.catch(function() {
|
|
4549
|
+
setDeletingState(items, false);
|
|
3611
4550
|
var errorEl = document.getElementById("action-error");
|
|
3612
|
-
showError(errorEl, "
|
|
4551
|
+
showError(errorEl, "无法清理该目录的历史会话。");
|
|
3613
4552
|
});
|
|
3614
4553
|
}
|
|
3615
4554
|
|
|
4555
|
+
function setDeletingState(items, deleting) {
|
|
4556
|
+
items.forEach(function(item) {
|
|
4557
|
+
item.classList.toggle("deleting", deleting);
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
function getHistoryItemsByCwd(cwd) {
|
|
4562
|
+
return Array.prototype.slice.call(document.querySelectorAll('.claude-history-item[data-cwd="' + window.CSS.escape(String(cwd)) + '"]'));
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
// ── Swipe-to-delete gesture ──
|
|
4566
|
+
|
|
4567
|
+
var _swipeState = null;
|
|
4568
|
+
var _swipedItem = null;
|
|
4569
|
+
|
|
4570
|
+
function closeSwipedItem() {
|
|
4571
|
+
if (_swipedItem) {
|
|
4572
|
+
_swipedItem.classList.remove("swiped");
|
|
4573
|
+
var content = _swipedItem.querySelector(".session-item-content");
|
|
4574
|
+
if (content) content.style.transform = "";
|
|
4575
|
+
_swipedItem = null;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
function initSwipeToDelete() {
|
|
4580
|
+
_swipeState = null;
|
|
4581
|
+
_swipedItem = null;
|
|
4582
|
+
}
|
|
4583
|
+
|
|
3616
4584
|
function startCommand(command, cwd, errorEl) {
|
|
3617
4585
|
if (command === "claude") {
|
|
3618
4586
|
state.preferredCommand = command;
|
|
@@ -3641,6 +4609,224 @@
|
|
|
3641
4609
|
});
|
|
3642
4610
|
}
|
|
3643
4611
|
|
|
4612
|
+
function resumeSession(sessionId, errorEl) {
|
|
4613
|
+
if (!sessionId) return Promise.resolve(null);
|
|
4614
|
+
return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/resume", {
|
|
4615
|
+
method: "POST",
|
|
4616
|
+
headers: { "Content-Type": "application/json" },
|
|
4617
|
+
credentials: "same-origin",
|
|
4618
|
+
body: JSON.stringify({
|
|
4619
|
+
mode: state.chatMode || state.config.defaultMode || "default"
|
|
4620
|
+
})
|
|
4621
|
+
})
|
|
4622
|
+
.then(function(res) { return res.json(); })
|
|
4623
|
+
.then(function(data) {
|
|
4624
|
+
if (data.error) {
|
|
4625
|
+
if (errorEl) showError(errorEl, data.error);
|
|
4626
|
+
else showToast(data.error, "error");
|
|
4627
|
+
return null;
|
|
4628
|
+
}
|
|
4629
|
+
state.selectedId = data.id;
|
|
4630
|
+
persistSelectedId();
|
|
4631
|
+
state.drafts[data.id] = "";
|
|
4632
|
+
return data;
|
|
4633
|
+
})
|
|
4634
|
+
.catch(function(error) {
|
|
4635
|
+
var message = (error && error.message) || "无法恢复会话。";
|
|
4636
|
+
if (errorEl) showError(errorEl, message);
|
|
4637
|
+
else showToast(message, "error");
|
|
4638
|
+
return null;
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
function resumeClaudeSessionById(claudeSessionId, errorEl) {
|
|
4643
|
+
if (!claudeSessionId) return Promise.resolve(null);
|
|
4644
|
+
return fetch("/api/claude-sessions/" + encodeURIComponent(claudeSessionId) + "/resume", {
|
|
4645
|
+
method: "POST",
|
|
4646
|
+
headers: { "Content-Type": "application/json" },
|
|
4647
|
+
credentials: "same-origin",
|
|
4648
|
+
body: JSON.stringify({
|
|
4649
|
+
mode: state.chatMode || state.config.defaultMode || "default"
|
|
4650
|
+
})
|
|
4651
|
+
})
|
|
4652
|
+
.then(function(res) { return res.json(); })
|
|
4653
|
+
.then(function(data) {
|
|
4654
|
+
if (data.error) {
|
|
4655
|
+
if (errorEl) showError(errorEl, data.error);
|
|
4656
|
+
else showToast(data.error, "error");
|
|
4657
|
+
return null;
|
|
4658
|
+
}
|
|
4659
|
+
state.selectedId = data.id;
|
|
4660
|
+
persistSelectedId();
|
|
4661
|
+
state.drafts[data.id] = "";
|
|
4662
|
+
return data;
|
|
4663
|
+
})
|
|
4664
|
+
.catch(function(error) {
|
|
4665
|
+
var message = (error && error.message) || "无法按 Claude 会话 ID 恢复会话。";
|
|
4666
|
+
if (errorEl) showError(errorEl, message);
|
|
4667
|
+
else showToast(message, "error");
|
|
4668
|
+
return null;
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
function activateSession(data) {
|
|
4673
|
+
if (!data || !data.id) return Promise.resolve();
|
|
4674
|
+
state.lastRenderedHash = 0;
|
|
4675
|
+
state.lastRenderedMsgCount = 0;
|
|
4676
|
+
state.lastRenderedEmpty = null;
|
|
4677
|
+
switchToSessionView(data.id);
|
|
4678
|
+
updateSessionSnapshot(data);
|
|
4679
|
+
updateSessionsList();
|
|
4680
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
4681
|
+
state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
|
|
4682
|
+
}
|
|
4683
|
+
return loadOutput(data.id).then(function() {
|
|
4684
|
+
focusInputBox(true);
|
|
4685
|
+
});
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
function resumeSessionFromList(sessionId) {
|
|
4689
|
+
return resumeSession(sessionId).then(function(data) {
|
|
4690
|
+
if (!data) return null;
|
|
4691
|
+
return activateSession(data).then(function() {
|
|
4692
|
+
return data;
|
|
4693
|
+
});
|
|
4694
|
+
});
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
function startAndActivateCommand(command, cwd, errorEl) {
|
|
4698
|
+
return startCommand(command, cwd, errorEl).then(function(data) {
|
|
4699
|
+
if (!data) return null;
|
|
4700
|
+
return activateSession(data).then(function() {
|
|
4701
|
+
return data;
|
|
4702
|
+
});
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
function createSessionFromWelcomeInput(value) {
|
|
4707
|
+
var welcomeInput = document.getElementById("welcome-input");
|
|
4708
|
+
if (!welcomeInput) return;
|
|
4709
|
+
welcomeInput.placeholder = "Claude 正在思考,请稍候...";
|
|
4710
|
+
welcomeInput.disabled = true;
|
|
4711
|
+
var mode = state.chatMode || "full-access";
|
|
4712
|
+
var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
|
|
4713
|
+
var preferredTool = getPreferredTool();
|
|
4714
|
+
fetch("/api/commands", {
|
|
4715
|
+
method: "POST",
|
|
4716
|
+
headers: { "Content-Type": "application/json" },
|
|
4717
|
+
credentials: "same-origin",
|
|
4718
|
+
body: JSON.stringify({
|
|
4719
|
+
command: preferredTool,
|
|
4720
|
+
cwd: defaultCwd,
|
|
4721
|
+
mode: mode,
|
|
4722
|
+
initialInput: value
|
|
4723
|
+
})
|
|
4724
|
+
})
|
|
4725
|
+
.then(function(res) { return res.json(); })
|
|
4726
|
+
.then(function(data) {
|
|
4727
|
+
if (data.error) {
|
|
4728
|
+
showToast(data.error, "error");
|
|
4729
|
+
welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
|
|
4730
|
+
welcomeInput.disabled = false;
|
|
4731
|
+
return null;
|
|
4732
|
+
}
|
|
4733
|
+
return activateSession(data);
|
|
4734
|
+
})
|
|
4735
|
+
.catch(function(error) {
|
|
4736
|
+
showToast((error && error.message) || "无法启动会话。", "error");
|
|
4737
|
+
welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
|
|
4738
|
+
welcomeInput.disabled = false;
|
|
4739
|
+
})
|
|
4740
|
+
.finally(function() {
|
|
4741
|
+
welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
|
|
4742
|
+
welcomeInput.disabled = false;
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4746
|
+
function createSessionFromInput(value, inputBox, welcomeInput) {
|
|
4747
|
+
var mode = state.chatMode || "full-access";
|
|
4748
|
+
var defaultCwd = state.workingDir || (state.config && state.config.defaultCwd ? state.config.defaultCwd : "");
|
|
4749
|
+
var preferredTool = getPreferredTool();
|
|
4750
|
+
fetch("/api/commands", {
|
|
4751
|
+
method: "POST",
|
|
4752
|
+
headers: { "Content-Type": "application/json" },
|
|
4753
|
+
credentials: "same-origin",
|
|
4754
|
+
body: JSON.stringify({
|
|
4755
|
+
command: preferredTool,
|
|
4756
|
+
cwd: defaultCwd,
|
|
4757
|
+
mode: mode,
|
|
4758
|
+
initialInput: value || undefined
|
|
4759
|
+
})
|
|
4760
|
+
})
|
|
4761
|
+
.then(function(res) { return res.json(); })
|
|
4762
|
+
.then(function(data) {
|
|
4763
|
+
if (data.error) {
|
|
4764
|
+
showToast(data.error, "error");
|
|
4765
|
+
return null;
|
|
4766
|
+
}
|
|
4767
|
+
if (inputBox) inputBox.value = "";
|
|
4768
|
+
if (welcomeInput) welcomeInput.value = "";
|
|
4769
|
+
return activateSession(data);
|
|
4770
|
+
})
|
|
4771
|
+
.catch(function(error) {
|
|
4772
|
+
showToast((error && error.message) || "无法启动会话。", "error");
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
function handleResumeAction(actionButton) {
|
|
4777
|
+
actionButton.disabled = true;
|
|
4778
|
+
resumeSessionFromList(actionButton.dataset.sessionId)
|
|
4779
|
+
.finally(function() {
|
|
4780
|
+
actionButton.disabled = false;
|
|
4781
|
+
});
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
function handleResumeHistoryAction(actionButton) {
|
|
4785
|
+
var claudeSessionId = actionButton.dataset.claudeSessionId;
|
|
4786
|
+
var cwd = actionButton.dataset.cwd;
|
|
4787
|
+
if (!claudeSessionId) return;
|
|
4788
|
+
actionButton.disabled = true;
|
|
4789
|
+
resumeClaudeHistorySession(claudeSessionId, cwd)
|
|
4790
|
+
.then(function(data) {
|
|
4791
|
+
if (data && data.id) {
|
|
4792
|
+
state.selectedId = data.id;
|
|
4793
|
+
persistSelectedId();
|
|
4794
|
+
state.drafts[data.id] = "";
|
|
4795
|
+
loadSessions().then(function() {
|
|
4796
|
+
selectSession(data.id);
|
|
4797
|
+
closeSessionsDrawer();
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
4800
|
+
})
|
|
4801
|
+
.finally(function() {
|
|
4802
|
+
actionButton.disabled = false;
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
function resumeClaudeHistorySession(claudeSessionId, cwd) {
|
|
4807
|
+
return fetch("/api/claude-sessions/" + encodeURIComponent(claudeSessionId) + "/resume", {
|
|
4808
|
+
method: "POST",
|
|
4809
|
+
headers: { "Content-Type": "application/json" },
|
|
4810
|
+
credentials: "same-origin",
|
|
4811
|
+
body: JSON.stringify({
|
|
4812
|
+
mode: state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
4813
|
+
cwd: cwd
|
|
4814
|
+
})
|
|
4815
|
+
})
|
|
4816
|
+
.then(function(res) { return res.json(); })
|
|
4817
|
+
.then(function(data) {
|
|
4818
|
+
if (data.error) {
|
|
4819
|
+
showToast(data.error, "error");
|
|
4820
|
+
return null;
|
|
4821
|
+
}
|
|
4822
|
+
return data;
|
|
4823
|
+
})
|
|
4824
|
+
.catch(function(error) {
|
|
4825
|
+
showToast((error && error.message) || "无法恢复历史会话。", "error");
|
|
4826
|
+
return null;
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
|
|
3644
4830
|
function isTouchDevice() {
|
|
3645
4831
|
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
3646
4832
|
}
|
|
@@ -4483,23 +5669,26 @@
|
|
|
4483
5669
|
e.preventDefault();
|
|
4484
5670
|
});
|
|
4485
5671
|
|
|
4486
|
-
document
|
|
5672
|
+
// Store document-level listeners so they can be removed in teardownTerminal
|
|
5673
|
+
state.resizeMouseMove = function(e) {
|
|
4487
5674
|
if (!isResizing) return;
|
|
4488
5675
|
var deltaY = e.clientY - startY;
|
|
4489
5676
|
var newHeight = Math.max(200, Math.min(startHeight + deltaY, window.innerHeight - 200));
|
|
4490
5677
|
container.style.height = newHeight + "px";
|
|
4491
5678
|
container.style.flex = "none";
|
|
4492
5679
|
scheduleTerminalResize();
|
|
4493
|
-
}
|
|
5680
|
+
};
|
|
5681
|
+
document.addEventListener("mousemove", state.resizeMouseMove);
|
|
4494
5682
|
|
|
4495
|
-
|
|
5683
|
+
state.resizeMouseUp = function() {
|
|
4496
5684
|
if (isResizing) {
|
|
4497
5685
|
isResizing = false;
|
|
4498
5686
|
document.body.style.cursor = "";
|
|
4499
5687
|
document.body.style.userSelect = "";
|
|
4500
5688
|
scheduleTerminalResize();
|
|
4501
5689
|
}
|
|
4502
|
-
}
|
|
5690
|
+
};
|
|
5691
|
+
document.addEventListener("mouseup", state.resizeMouseUp);
|
|
4503
5692
|
|
|
4504
5693
|
// 触摸设备支持
|
|
4505
5694
|
resizeHandle.addEventListener("touchstart", function(e) {
|
|
@@ -4509,7 +5698,7 @@
|
|
|
4509
5698
|
e.preventDefault();
|
|
4510
5699
|
}, { passive: false });
|
|
4511
5700
|
|
|
4512
|
-
|
|
5701
|
+
state.resizeTouchMove = function(e) {
|
|
4513
5702
|
if (!isResizing) return;
|
|
4514
5703
|
var deltaY = e.touches[0].clientY - startY;
|
|
4515
5704
|
var newHeight = Math.max(200, Math.min(startHeight + deltaY, window.innerHeight - 200));
|
|
@@ -4517,14 +5706,16 @@
|
|
|
4517
5706
|
container.style.flex = "none";
|
|
4518
5707
|
scheduleTerminalResize();
|
|
4519
5708
|
e.preventDefault();
|
|
4520
|
-
}
|
|
5709
|
+
};
|
|
5710
|
+
document.addEventListener("touchmove", state.resizeTouchMove, { passive: false });
|
|
4521
5711
|
|
|
4522
|
-
|
|
5712
|
+
state.resizeTouchEnd = function() {
|
|
4523
5713
|
if (isResizing) {
|
|
4524
5714
|
isResizing = false;
|
|
4525
5715
|
scheduleTerminalResize();
|
|
4526
5716
|
}
|
|
4527
|
-
}
|
|
5717
|
+
};
|
|
5718
|
+
document.addEventListener("touchend", state.resizeTouchEnd);
|
|
4528
5719
|
}
|
|
4529
5720
|
|
|
4530
5721
|
function observeTerminalResize() {
|
|
@@ -4532,15 +5723,19 @@
|
|
|
4532
5723
|
if (!output) return;
|
|
4533
5724
|
|
|
4534
5725
|
if (typeof ResizeObserver === "function") {
|
|
4535
|
-
state.resizeObserver = new ResizeObserver(function() { scheduleTerminalResize(); });
|
|
5726
|
+
state.resizeObserver = new ResizeObserver(function() { scheduleTerminalResize(true); });
|
|
4536
5727
|
state.resizeObserver.observe(output);
|
|
4537
5728
|
}
|
|
4538
|
-
state.resizeHandler = scheduleTerminalResize;
|
|
5729
|
+
state.resizeHandler = function() { scheduleTerminalResize(true); };
|
|
4539
5730
|
window.addEventListener("resize", state.resizeHandler);
|
|
4540
|
-
requestAnimationFrame(scheduleTerminalResize);
|
|
5731
|
+
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
4541
5732
|
}
|
|
4542
5733
|
|
|
4543
5734
|
function teardownTerminal() {
|
|
5735
|
+
if (state.resizeTimer) {
|
|
5736
|
+
clearTimeout(state.resizeTimer);
|
|
5737
|
+
state.resizeTimer = null;
|
|
5738
|
+
}
|
|
4544
5739
|
if (state.resizeObserver) {
|
|
4545
5740
|
state.resizeObserver.disconnect();
|
|
4546
5741
|
state.resizeObserver = null;
|
|
@@ -4549,24 +5744,61 @@
|
|
|
4549
5744
|
window.removeEventListener("resize", state.resizeHandler);
|
|
4550
5745
|
state.resizeHandler = null;
|
|
4551
5746
|
}
|
|
5747
|
+
[["mousemove", "resizeMouseMove"], ["mouseup", "resizeMouseUp"],
|
|
5748
|
+
["touchmove", "resizeTouchMove"], ["touchend", "resizeTouchEnd"]
|
|
5749
|
+
].forEach(function(pair) {
|
|
5750
|
+
if (state[pair[1]]) {
|
|
5751
|
+
document.removeEventListener(pair[0], state[pair[1]]);
|
|
5752
|
+
state[pair[1]] = null;
|
|
5753
|
+
}
|
|
5754
|
+
});
|
|
5755
|
+
clearTerminalScrollIdleTimer();
|
|
5756
|
+
if (state.terminalViewportEl) {
|
|
5757
|
+
if (state.terminalViewportScrollHandler) {
|
|
5758
|
+
state.terminalViewportEl.removeEventListener("scroll", state.terminalViewportScrollHandler);
|
|
5759
|
+
}
|
|
5760
|
+
if (state.terminalViewportTouchHandler) {
|
|
5761
|
+
state.terminalViewportEl.removeEventListener("touchmove", state.terminalViewportTouchHandler);
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
state.terminalViewportEl = null;
|
|
5765
|
+
state.terminalViewportScrollHandler = null;
|
|
5766
|
+
state.terminalViewportTouchHandler = null;
|
|
4552
5767
|
if (state.terminal) {
|
|
4553
5768
|
state.terminal.dispose();
|
|
4554
5769
|
state.terminal = null;
|
|
4555
5770
|
}
|
|
4556
5771
|
state.fitAddon = null;
|
|
4557
5772
|
state.terminalSessionId = null;
|
|
5773
|
+
state.terminalOutput = "";
|
|
5774
|
+
state.terminalViewportSize = { width: 0, height: 0 };
|
|
5775
|
+
state.terminalAutoFollow = true;
|
|
5776
|
+
state.showTerminalJumpToBottom = false;
|
|
5777
|
+
updateTerminalJumpToBottomButton();
|
|
4558
5778
|
}
|
|
4559
5779
|
|
|
4560
|
-
function scheduleTerminalResize() {
|
|
4561
|
-
if (state.resizeTimer)
|
|
4562
|
-
|
|
5780
|
+
function scheduleTerminalResize(immediate) {
|
|
5781
|
+
if (state.resizeTimer) {
|
|
5782
|
+
clearTimeout(state.resizeTimer);
|
|
5783
|
+
state.resizeTimer = null;
|
|
5784
|
+
}
|
|
5785
|
+
var delay = immediate ? 0 : 100;
|
|
5786
|
+
state.resizeTimer = setTimeout(function() {
|
|
5787
|
+
state.resizeTimer = null;
|
|
5788
|
+
requestAnimationFrame(syncTerminalSize);
|
|
5789
|
+
}, delay);
|
|
4563
5790
|
}
|
|
4564
5791
|
|
|
4565
5792
|
function syncTerminalSize() {
|
|
4566
5793
|
var output = document.getElementById("output");
|
|
4567
5794
|
if (!state.terminal || !state.fitAddon || !output) return;
|
|
5795
|
+
if (!shouldResizeTerminalViewport()) return;
|
|
4568
5796
|
|
|
5797
|
+
var shouldFollow = state.terminalAutoFollow || isTerminalNearBottom();
|
|
4569
5798
|
state.fitAddon.fit();
|
|
5799
|
+
if (shouldFollow) {
|
|
5800
|
+
maybeScrollTerminalToBottom("resize");
|
|
5801
|
+
}
|
|
4570
5802
|
|
|
4571
5803
|
var nextSize = {
|
|
4572
5804
|
cols: state.terminal.cols,
|
|
@@ -4602,6 +5834,12 @@
|
|
|
4602
5834
|
function initWebSocket() {
|
|
4603
5835
|
if (!window.WebSocket) return false;
|
|
4604
5836
|
|
|
5837
|
+
// Prevent duplicate connections
|
|
5838
|
+
if (state.ws) {
|
|
5839
|
+
try { state.ws.close(); } catch (e) { /* ignore */ }
|
|
5840
|
+
state.ws = null;
|
|
5841
|
+
}
|
|
5842
|
+
|
|
4605
5843
|
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
4606
5844
|
var wsUrl = protocol + '//' + window.location.host + '/ws';
|
|
4607
5845
|
|
|
@@ -4669,16 +5907,21 @@
|
|
|
4669
5907
|
|
|
4670
5908
|
}
|
|
4671
5909
|
// Real-time terminal output
|
|
4672
|
-
if (msg.sessionId === state.selectedId && state.terminal && msg.data
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
state.
|
|
4678
|
-
|
|
5910
|
+
if (msg.sessionId === state.selectedId && state.terminal && msg.data) {
|
|
5911
|
+
if (msg.data.chunk && (!state.terminalSessionId || state.terminalSessionId === msg.sessionId)) {
|
|
5912
|
+
// Fast path: write chunk directly to avoid full-output comparison
|
|
5913
|
+
// which can trigger terminal.reset() and cause screen flicker.
|
|
5914
|
+
state.terminal.write(msg.data.chunk);
|
|
5915
|
+
state.terminalSessionId = msg.sessionId;
|
|
5916
|
+
if (msg.data.output) {
|
|
5917
|
+
state.terminalOutput = normalizeTerminalOutput(msg.data.output);
|
|
5918
|
+
}
|
|
5919
|
+
maybeScrollTerminalToBottom("output");
|
|
5920
|
+
updateTerminalJumpToBottomButton();
|
|
5921
|
+
} else if (Object.prototype.hasOwnProperty.call(msg.data, "output")) {
|
|
5922
|
+
// Fallback: no chunk available, use full-output comparison
|
|
5923
|
+
syncTerminalBuffer(msg.sessionId, msg.data.output || "", { mode: "append" });
|
|
4679
5924
|
}
|
|
4680
|
-
state.terminalOutput = newOutput;
|
|
4681
|
-
state.terminal.scrollToBottom();
|
|
4682
5925
|
}
|
|
4683
5926
|
break;
|
|
4684
5927
|
case 'started':
|
|
@@ -4727,8 +5970,7 @@
|
|
|
4727
5970
|
// Initial state for subscribed session (after reconnect or subscription)
|
|
4728
5971
|
if (msg.sessionId === state.selectedId && msg.data) {
|
|
4729
5972
|
if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
|
|
4730
|
-
updateTerminalOutput(msg.data.output || "");
|
|
4731
|
-
scheduleTerminalResize();
|
|
5973
|
+
updateTerminalOutput(msg.data.output || "", msg.sessionId, "replace");
|
|
4732
5974
|
}
|
|
4733
5975
|
break;
|
|
4734
5976
|
case 'usage':
|
|
@@ -4741,31 +5983,59 @@
|
|
|
4741
5983
|
updateTaskDisplay();
|
|
4742
5984
|
}
|
|
4743
5985
|
break;
|
|
5986
|
+
case 'status':
|
|
5987
|
+
if (msg.sessionId && msg.data) {
|
|
5988
|
+
var statusUpdate = { id: msg.sessionId };
|
|
5989
|
+
if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
|
|
5990
|
+
statusUpdate.permissionBlocked = !!msg.data.permissionBlocked;
|
|
5991
|
+
}
|
|
5992
|
+
if (msg.data.permissionRequest) {
|
|
5993
|
+
statusUpdate.pendingEscalation = {
|
|
5994
|
+
scope: msg.data.permissionRequest.scope,
|
|
5995
|
+
target: msg.data.permissionRequest.target,
|
|
5996
|
+
reason: msg.data.permissionRequest.prompt
|
|
5997
|
+
};
|
|
5998
|
+
}
|
|
5999
|
+
if (msg.data.permissionBlocked === false) {
|
|
6000
|
+
statusUpdate.pendingEscalation = null;
|
|
6001
|
+
}
|
|
6002
|
+
updateSessionSnapshot(statusUpdate);
|
|
6003
|
+
if (msg.sessionId === state.selectedId) {
|
|
6004
|
+
updateTaskDisplay();
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
break;
|
|
4744
6008
|
}
|
|
4745
6009
|
}
|
|
4746
6010
|
|
|
4747
6011
|
function updateTaskDisplay() {
|
|
4748
6012
|
var taskEl = document.getElementById("current-task");
|
|
4749
6013
|
var permissionActionsEl = document.getElementById("permission-actions");
|
|
6014
|
+
var permissionLabel = document.getElementById("permission-actions-label");
|
|
4750
6015
|
if (!taskEl) return;
|
|
4751
6016
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
4752
6017
|
var pendingEscalation = selectedSession && selectedSession.pendingEscalation ? selectedSession.pendingEscalation : null;
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
6018
|
+
var isBlocked = pendingEscalation || (selectedSession && selectedSession.permissionBlocked);
|
|
6019
|
+
|
|
6020
|
+
if (isBlocked) {
|
|
6021
|
+
// Show permission label in input composer area
|
|
6022
|
+
if (permissionLabel) {
|
|
6023
|
+
if (pendingEscalation) {
|
|
6024
|
+
var reason = pendingEscalation.reason || "等待授权";
|
|
6025
|
+
var target = pendingEscalation.target ? " · " + pendingEscalation.target : "";
|
|
6026
|
+
permissionLabel.textContent = reason + target;
|
|
6027
|
+
} else {
|
|
6028
|
+
permissionLabel.textContent = "等待授权";
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
4759
6031
|
if (permissionActionsEl) permissionActionsEl.classList.remove("hidden");
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
if (selectedSession && selectedSession.permissionBlocked) {
|
|
4763
|
-
taskEl.textContent = "等待 Claude 权限授权";
|
|
6032
|
+
// Also show in task bar
|
|
6033
|
+
taskEl.textContent = pendingEscalation ? (pendingEscalation.reason || "等待 Claude 权限授权") : "等待 Claude 权限授权";
|
|
4764
6034
|
taskEl.classList.remove("hidden");
|
|
4765
6035
|
taskEl.classList.add("permission-blocked");
|
|
4766
|
-
if (permissionActionsEl) permissionActionsEl.classList.remove("hidden");
|
|
4767
6036
|
return;
|
|
4768
6037
|
}
|
|
6038
|
+
|
|
4769
6039
|
taskEl.classList.remove("permission-blocked");
|
|
4770
6040
|
if (permissionActionsEl) permissionActionsEl.classList.add("hidden");
|
|
4771
6041
|
var task = state.currentTask;
|
|
@@ -4780,6 +6050,10 @@
|
|
|
4780
6050
|
|
|
4781
6051
|
function approvePermission() {
|
|
4782
6052
|
if (!state.selectedId) return;
|
|
6053
|
+
var approveBtn = document.getElementById("approve-permission-btn");
|
|
6054
|
+
var denyBtn = document.getElementById("deny-permission-btn");
|
|
6055
|
+
if (approveBtn) approveBtn.disabled = true;
|
|
6056
|
+
if (denyBtn) denyBtn.disabled = true;
|
|
4783
6057
|
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/approve-permission", {
|
|
4784
6058
|
method: "POST",
|
|
4785
6059
|
credentials: "same-origin"
|
|
@@ -4795,11 +6069,19 @@
|
|
|
4795
6069
|
})
|
|
4796
6070
|
.catch(function(error) {
|
|
4797
6071
|
showToast((error && error.message) || "无法批准授权。", "error");
|
|
6072
|
+
})
|
|
6073
|
+
.finally(function() {
|
|
6074
|
+
if (approveBtn) approveBtn.disabled = false;
|
|
6075
|
+
if (denyBtn) denyBtn.disabled = false;
|
|
4798
6076
|
});
|
|
4799
6077
|
}
|
|
4800
6078
|
|
|
4801
6079
|
function denyPermission() {
|
|
4802
6080
|
if (!state.selectedId) return;
|
|
6081
|
+
var approveBtn = document.getElementById("approve-permission-btn");
|
|
6082
|
+
var denyBtn = document.getElementById("deny-permission-btn");
|
|
6083
|
+
if (approveBtn) approveBtn.disabled = true;
|
|
6084
|
+
if (denyBtn) denyBtn.disabled = true;
|
|
4803
6085
|
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/deny-permission", {
|
|
4804
6086
|
method: "POST",
|
|
4805
6087
|
credentials: "same-origin"
|
|
@@ -4815,20 +6097,16 @@
|
|
|
4815
6097
|
})
|
|
4816
6098
|
.catch(function(error) {
|
|
4817
6099
|
showToast((error && error.message) || "无法拒绝授权。", "error");
|
|
6100
|
+
})
|
|
6101
|
+
.finally(function() {
|
|
6102
|
+
if (approveBtn) approveBtn.disabled = false;
|
|
6103
|
+
if (denyBtn) denyBtn.disabled = false;
|
|
4818
6104
|
});
|
|
4819
6105
|
}
|
|
4820
6106
|
|
|
4821
|
-
function updateTerminalOutput(output) {
|
|
4822
|
-
if (!state.terminal) return;
|
|
4823
|
-
|
|
4824
|
-
if (normalized.startsWith(state.terminalOutput)) {
|
|
4825
|
-
state.terminal.write(normalized.slice(state.terminalOutput.length));
|
|
4826
|
-
} else {
|
|
4827
|
-
state.terminal.reset();
|
|
4828
|
-
state.terminal.write(normalized);
|
|
4829
|
-
}
|
|
4830
|
-
state.terminalOutput = normalized;
|
|
4831
|
-
state.terminal.scrollToBottom();
|
|
6107
|
+
function updateTerminalOutput(output, sessionId, mode) {
|
|
6108
|
+
if (!state.terminal) return false;
|
|
6109
|
+
return syncTerminalBuffer(sessionId || state.selectedId, output, { mode: mode || "append" });
|
|
4832
6110
|
}
|
|
4833
6111
|
|
|
4834
6112
|
function stopPolling() {
|
|
@@ -4846,8 +6124,10 @@
|
|
|
4846
6124
|
}
|
|
4847
6125
|
applyCurrentView();
|
|
4848
6126
|
reconcileInteractiveState();
|
|
6127
|
+
updateTerminalJumpToBottomButton();
|
|
4849
6128
|
if (state.currentView === "terminal") {
|
|
4850
|
-
|
|
6129
|
+
state.terminalViewportSize = { width: 0, height: 0 };
|
|
6130
|
+
scheduleTerminalResize(true);
|
|
4851
6131
|
}
|
|
4852
6132
|
}
|
|
4853
6133
|
|
|
@@ -6486,20 +7766,9 @@
|
|
|
6486
7766
|
}
|
|
6487
7767
|
|
|
6488
7768
|
function normalizeTerminalOutput(value) {
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
var char = text.charAt(i);
|
|
6493
|
-
if (char === String.fromCharCode(10)) {
|
|
6494
|
-
if (i === 0 || text.charAt(i - 1) !== String.fromCharCode(13)) {
|
|
6495
|
-
normalized += String.fromCharCode(13);
|
|
6496
|
-
}
|
|
6497
|
-
normalized += char;
|
|
6498
|
-
continue;
|
|
6499
|
-
}
|
|
6500
|
-
normalized += char;
|
|
6501
|
-
}
|
|
6502
|
-
return normalized;
|
|
7769
|
+
return String(value || "")
|
|
7770
|
+
.replace(/\r\r\n/g, "\r\n")
|
|
7771
|
+
.replace(/\u0000/g, "");
|
|
6503
7772
|
}
|
|
6504
7773
|
|
|
6505
7774
|
function showError(el, msg) {
|