@co0ontty/wand 0.2.1 → 0.3.0

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