@co0ontty/wand 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -100,8 +100,10 @@
100
100
  cwdValue: "",
101
101
  modeValue: "managed",
102
102
  chatMode: "managed",
103
+ sessionCreateKind: "pty",
103
104
  sessionTool: "claude",
104
105
  preferredCommand: "claude",
106
+ structuredRunner: "claude-cli-print",
105
107
  lastResize: { cols: 0, rows: 0 },
106
108
  isOnline: navigator.onLine,
107
109
  deferredPrompt: null,
@@ -137,6 +139,7 @@
137
139
  shortcutsExpanded: false,
138
140
  modifiers: { ctrl: false, alt: false, shift: false },
139
141
  fileSearchQuery: "",
142
+ fileExplorerLoading: false,
140
143
  allFiles: [],
141
144
  claudeHistory: [],
142
145
  claudeHistoryLoaded: false,
@@ -156,6 +159,76 @@
156
159
  })()
157
160
  };
158
161
 
162
+ // ── Structured session status bar (in-flight timer) ──
163
+ var _statusBarTimerId = null;
164
+ var _statusBarStartTime = 0;
165
+
166
+ function renderStructuredStatusBar(chatMessages, session) {
167
+ // Remove stale bar if session changed or not structured
168
+ var existing = chatMessages.querySelector(".structured-status-bar");
169
+ if (!session || !isStructuredSession(session)) {
170
+ if (existing) existing.remove();
171
+ clearInterval(_statusBarTimerId);
172
+ _statusBarTimerId = null;
173
+ return;
174
+ }
175
+
176
+ var isInFlight = session.structuredState && session.structuredState.inFlight;
177
+
178
+ if (isInFlight) {
179
+ // Start timer if not already running
180
+ if (!_statusBarTimerId) {
181
+ _statusBarStartTime = Date.now();
182
+ }
183
+
184
+ if (!existing) {
185
+ var bar = document.createElement("div");
186
+ bar.className = "structured-status-bar";
187
+ bar.innerHTML =
188
+ '<span class="status-bar-label">回复中</span>' +
189
+ '<div class="status-bar-track"><div class="status-bar-fill"></div></div>' +
190
+ '<span class="status-bar-timer">0.0s</span>';
191
+ // column-reverse: first child = visual bottom
192
+ chatMessages.insertBefore(bar, chatMessages.firstChild);
193
+ existing = bar;
194
+ } else if (existing.classList.contains("completed")) {
195
+ // Was completed, now in-flight again — reset
196
+ existing.classList.remove("completed");
197
+ existing.style.animation = "none";
198
+ existing.querySelector(".status-bar-label").textContent = "回复中";
199
+ _statusBarStartTime = Date.now();
200
+ }
201
+
202
+ // Start interval to update timer
203
+ if (!_statusBarTimerId) {
204
+ _statusBarTimerId = setInterval(function() {
205
+ var bar = document.querySelector(".structured-status-bar:not(.completed)");
206
+ if (!bar) { clearInterval(_statusBarTimerId); _statusBarTimerId = null; return; }
207
+ var elapsed = ((Date.now() - _statusBarStartTime) / 1000).toFixed(1);
208
+ var timerEl = bar.querySelector(".status-bar-timer");
209
+ if (timerEl) timerEl.textContent = elapsed + "s";
210
+ }, 100);
211
+ }
212
+ } else {
213
+ // Not in-flight: show completion or remove
214
+ clearInterval(_statusBarTimerId);
215
+ _statusBarTimerId = null;
216
+
217
+ if (existing && !existing.classList.contains("completed")) {
218
+ // Just finished — transition to completed state
219
+ var elapsed = _statusBarStartTime ? ((Date.now() - _statusBarStartTime) / 1000).toFixed(1) : "0.0";
220
+ existing.classList.add("completed");
221
+ existing.querySelector(".status-bar-label").textContent = "完成";
222
+ existing.querySelector(".status-bar-timer").textContent = elapsed + "s";
223
+ _statusBarStartTime = 0;
224
+ // Remove after animation ends
225
+ setTimeout(function() {
226
+ if (existing.parentNode) existing.remove();
227
+ }, 3000);
228
+ }
229
+ }
230
+ }
231
+
159
232
  // Helper function to persist selected session ID to localStorage
160
233
  function persistSelectedId() {
161
234
  try {
@@ -173,6 +246,12 @@
173
246
  return (state.config && state.config.defaultCwd) || "/tmp";
174
247
  }
175
248
 
249
+ function resetChatRenderCache() {
250
+ state.lastRenderedHash = 0;
251
+ state.lastRenderedMsgCount = 0;
252
+ state.lastRenderedEmpty = null;
253
+ }
254
+
176
255
  function getEffectiveCwd() {
177
256
  return state.workingDir || getConfigCwd();
178
257
  }
@@ -270,7 +349,7 @@
270
349
  body: "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion,
271
350
  type: "info",
272
351
  icon: "\u2191",
273
- duration: 0,
352
+ duration: 10000,
274
353
  actionLabel: "\u53bb\u66f4\u65b0",
275
354
  action: function() {
276
355
  var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
@@ -313,7 +392,7 @@
313
392
  var app = document.getElementById("app");
314
393
  var isLoggedIn = state.config !== null;
315
394
  var wasModalOpen = state.modalOpen;
316
- var shouldResetShell = !isLoggedIn || !document.getElementById("output");
395
+ var shouldResetShell = !isLoggedIn || !!document.getElementById("output");
317
396
 
318
397
  if (shouldResetShell) {
319
398
  teardownTerminal();
@@ -324,9 +403,7 @@
324
403
 
325
404
  app.innerHTML = isLoggedIn ? renderAppShell() : renderLogin();
326
405
  // Reset chat render tracking since DOM was fully replaced
327
- state.lastRenderedHash = 0;
328
- state.lastRenderedMsgCount = 0;
329
- state.lastRenderedEmpty = null;
406
+ resetChatRenderCache();
330
407
  attachEventListeners();
331
408
  updateDrawerState();
332
409
  syncComposerModeSelect();
@@ -334,6 +411,9 @@
334
411
  if (!skipShellChrome) {
335
412
  updateShellChrome();
336
413
  }
414
+ if (isLoggedIn && state.filePanelOpen) {
415
+ refreshFileExplorer();
416
+ }
337
417
 
338
418
  // Force reflow then re-enable transitions after layout settles
339
419
  void document.body.offsetHeight;
@@ -367,6 +447,26 @@
367
447
  '<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
368
448
  }
369
449
 
450
+ function renderApprovalStatsBadge() {
451
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
452
+ var stats = selectedSession && selectedSession.approvalStats;
453
+ if (!stats || stats.total === 0) return '<span class="approval-stats hidden" id="approval-stats"></span>';
454
+ return '<span class="approval-stats" id="approval-stats">' +
455
+ '<span class="approval-stats-divider"></span>' +
456
+ '<span class="approval-stats-badge" id="approval-stats-badge" title="本次会话自动批准统计">' +
457
+ '<svg class="approval-stats-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>' +
458
+ '<span class="approval-stats-total">' + stats.total + '</span>' +
459
+ '</span>' +
460
+ '<span class="approval-stats-popup" id="approval-stats-popup">' +
461
+ '<span class="approval-stats-popup-title">自动批准统计</span>' +
462
+ (stats.command > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">⚡</span><span class="approval-stats-row-label">命令执行</span><span class="approval-stats-row-count">' + stats.command + '</span></span>' : '') +
463
+ (stats.file > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">📝</span><span class="approval-stats-row-label">文件写入</span><span class="approval-stats-row-count">' + stats.file + '</span></span>' : '') +
464
+ (stats.tool > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">🔧</span><span class="approval-stats-row-label">其他工具</span><span class="approval-stats-row-count">' + stats.tool + '</span></span>' : '') +
465
+ '<span class="approval-stats-row approval-stats-row-total"><span class="approval-stats-row-icon">∑</span><span class="approval-stats-row-label">合计</span><span class="approval-stats-row-count">' + stats.total + '</span></span>' +
466
+ '</span>' +
467
+ '</span>';
468
+ }
469
+
370
470
  function renderInlineKeyboard() {
371
471
  if (!state.selectedId) return "";
372
472
  var isTerminal = state.currentView === "terminal";
@@ -462,6 +562,12 @@
462
562
  '<span class="session-count" id="session-count">' + String(state.sessions.length) + '</span>' +
463
563
  '</div>' +
464
564
  '<div class="sidebar-header-actions">' +
565
+ '<button id="sidebar-home-btn" class="btn btn-ghost btn-sm" type="button" title="回到首页">' +
566
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>' +
567
+ '</button>' +
568
+ '<button id="sidebar-refresh-btn" class="btn btn-ghost btn-sm" type="button" title="刷新页面">' +
569
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>' +
570
+ '</button>' +
465
571
  '<button id="close-drawer-button" class="btn btn-ghost btn-sm sidebar-close" type="button" aria-label="关闭菜单">×</button>' +
466
572
  '</div>' +
467
573
  '</div>' +
@@ -524,11 +630,14 @@
524
630
  '<div class="blank-chat-inner">' +
525
631
  '<div class="blank-chat-logo">W</div>' +
526
632
  '<h2 class="blank-chat-title">Wand</h2>' +
527
- '<p class="blank-chat-subtitle">当前仅保留原生终端模式,优先修复 PTY 交互与显示。</p>' +
633
+ '<p class="blank-chat-subtitle">支持终端 PTY 会话与结构化 chat 会话,两种模式可并存。</p>' +
528
634
  '<div class="blank-chat-tools">' +
529
635
  '<button class="blank-chat-tool-btn" id="welcome-tool-claude" type="button">' +
530
636
  '<span class="tool-icon">🤖</span>新建终端会话' +
531
637
  '</button>' +
638
+ '<button class="blank-chat-tool-btn" id="welcome-tool-structured" type="button">' +
639
+ '<span class="tool-icon">💬</span>新建结构化会话' +
640
+ '</button>' +
532
641
  '</div>' +
533
642
  '<div class="blank-chat-cwd-wrap">' +
534
643
  '<div class="blank-chat-cwd" id="blank-chat-cwd" role="button" tabindex="0" title="点击切换工作目录">' +
@@ -569,6 +678,7 @@
569
678
  '<button id="approve-permission-btn" class="btn btn-permission btn-permission-approve" type="button">批准</button>' +
570
679
  '<button id="deny-permission-btn" class="btn btn-permission btn-permission-deny" type="button">拒绝</button>' +
571
680
  '</span>' +
681
+ renderApprovalStatsBadge() +
572
682
  '</div>' +
573
683
  '<div class="input-composer-right">' +
574
684
  '<span id="queue-counter" class="queue-counter hidden">队列: 0</span>' +
@@ -588,6 +698,9 @@
588
698
  '<span id="session-cwd-display" class="session-cwd-display">' + (selectedSession && selectedSession.cwd ? escapeHtml(selectedSession.cwd) : '未设置目录') + '</span>' +
589
699
  '<span class="session-info-separator">|</span>' +
590
700
  '<span id="session-mode-display" class="session-mode-display">' + (selectedSession ? getModeLabel(selectedSession.mode) : '默认') + '</span>' +
701
+ (selectedSession && selectedSession.autoApprovePermissions ? '<span class="session-info-separator">|</span><span id="auto-approve-toggle" class="auto-approve-indicator active" title="自动批准已启用 — 点击关闭">🛡 自动批准</span>' : '<span class="session-info-separator">|</span><span id="auto-approve-toggle" class="auto-approve-indicator" title="自动批准已关闭 — 点击开启">🛡 手动</span>') +
702
+ '<span class="session-info-separator">|</span>' +
703
+ '<span id="session-kind-display" class="session-kind-display">' + (selectedSession ? (isStructuredSession(selectedSession) ? 'Structured' : 'PTY') : 'PTY') + '</span>' +
591
704
  '<span class="session-info-separator">|</span>' +
592
705
  '<span id="session-status-display" class="session-status-display">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '-') + '</span>' +
593
706
  (selectedSession && selectedSession.claudeSessionId ? '<span class="session-info-separator">|</span><span id="claude-session-id-badge" class="claude-session-id-badge" data-claude-id="' + escapeHtml(selectedSession.claudeSessionId) + '" title="点击复制 Claude 会话 ID">☁ ' + escapeHtml(selectedSession.claudeSessionId.slice(0, 8)) + '</span>' : '') +
@@ -1225,13 +1338,21 @@
1225
1338
  explorer.innerHTML = '<div class="file-explorer empty">No working directory.</div>';
1226
1339
  return;
1227
1340
  }
1341
+ state.fileExplorerLoading = true;
1342
+ state.allFiles = [];
1228
1343
  explorer.innerHTML = '<div class="file-explorer"><div class="tree-loading" style="padding:12px;color:var(--text-muted);font-size:0.8125rem;">Loading...</div></div>';
1229
1344
  // Update the cwd display
1230
1345
  if (cwdEl) cwdEl.textContent = cwd;
1231
1346
  // Fetch with git status
1232
1347
  fetch("/api/directory?q=" + encodeURIComponent(cwd) + "&gitStatus=true", { credentials: "same-origin" })
1233
- .then(function(res) { return res.json(); })
1348
+ .then(function(res) {
1349
+ if (!res.ok) {
1350
+ throw new Error("Failed to load directory.");
1351
+ }
1352
+ return res.json();
1353
+ })
1234
1354
  .then(function(items) {
1355
+ state.fileExplorerLoading = false;
1235
1356
  if (!items || items.length === 0) {
1236
1357
  explorer.innerHTML = '<div class="file-explorer empty">Empty directory or inaccessible.</div>';
1237
1358
  return;
@@ -1240,6 +1361,7 @@
1240
1361
  filterFileTree();
1241
1362
  })
1242
1363
  .catch(function() {
1364
+ state.fileExplorerLoading = false;
1243
1365
  explorer.innerHTML = '<div class="file-explorer empty">Failed to load files.</div>';
1244
1366
  });
1245
1367
  }
@@ -1636,7 +1758,7 @@
1636
1758
  if (session.claudeSessionId) {
1637
1759
  var shortId = session.claudeSessionId.slice(0, 8);
1638
1760
  sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
1639
- if (session.status !== "running" && !state.sessionsManageMode) {
1761
+ if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
1640
1762
  resumeButton = '<button class="session-action-btn" data-action="resume" data-session-id="' + session.id + '" type="button" aria-label="恢复会话" title="恢复 Claude 会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
1641
1763
  }
1642
1764
  }
@@ -1646,6 +1768,7 @@
1646
1768
  }
1647
1769
 
1648
1770
  var deleteButton = state.sessionsManageMode ? '' : '<button class="session-action-btn delete-btn" data-action="delete-session" data-session-id="' + session.id + '" type="button" aria-label="删除会话" title="删除此会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
1771
+ var modeBadge = renderSessionKindBadge(session);
1649
1772
  var actionsHtml = '<span class="session-actions">' + resumeButton + deleteButton + '</span>';
1650
1773
 
1651
1774
  return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
@@ -1655,6 +1778,7 @@
1655
1778
  '<div class="session-main">' +
1656
1779
  '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>' +
1657
1780
  '<div class="session-meta">' +
1781
+ modeBadge +
1658
1782
  '<span>' + escapeHtml(modeName) + '</span>' +
1659
1783
  '<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
1660
1784
  sessionIdDisplay +
@@ -1666,6 +1790,15 @@
1666
1790
  '</div>' +
1667
1791
  '</div>';
1668
1792
  }
1793
+
1794
+ function renderSessionKindBadge(session) {
1795
+ if (!session) return "";
1796
+ if (isStructuredSession(session)) {
1797
+ return '<span class="session-kind-badge structured">Structured</span>';
1798
+ }
1799
+ return '<span class="session-kind-badge pty">PTY</span>';
1800
+ }
1801
+
1669
1802
  function renderModeCards(selectedMode) {
1670
1803
  var modes = [
1671
1804
  { id: "managed", label: "托管", desc: "全自动完成任务" },
@@ -1683,19 +1816,48 @@
1683
1816
  }).join("");
1684
1817
  }
1685
1818
 
1819
+ function renderSessionKindOptions(selectedKind) {
1820
+ var kinds = [
1821
+ { id: "pty", label: "PTY", desc: "交互式终端会话" },
1822
+ { id: "structured", label: "Structured", desc: "单轮结构化输出" }
1823
+ ];
1824
+ return kinds.map(function(kind) {
1825
+ var active = kind.id === selectedKind ? " active" : "";
1826
+ return '<button type="button" class="mode-card session-kind-card' + active + '" data-session-kind="' + kind.id + '">' +
1827
+ '<span class="mode-card-label">' + kind.label + '</span>' +
1828
+ '<span class="mode-card-desc">' + kind.desc + '</span>' +
1829
+ '</button>';
1830
+ }).join("");
1831
+ }
1832
+
1833
+ function getSessionKindHint(kind) {
1834
+ if (kind === "structured") {
1835
+ return "直接使用 claude -p 获取结构化单轮结果。";
1836
+ }
1837
+ return "默认 PTY 会话,支持持续交互、终端视图和权限流。";
1838
+ }
1839
+
1686
1840
  function renderSessionModal() {
1687
1841
  var modalTool = getPreferredTool();
1688
1842
  var modalMode = getSafeModeForTool(modalTool, state.modeValue || state.chatMode || "default");
1843
+ var sessionKind = state.sessionCreateKind || "pty";
1689
1844
  return '<section id="session-modal" class="modal-backdrop hidden">' +
1690
1845
  '<div class="modal session-modal">' +
1691
1846
  '<div class="modal-header">' +
1692
1847
  '<div>' +
1693
1848
  '<h2 class="modal-title">新对话</h2>' +
1694
- '<p class="modal-subtitle">启动 Claude 会话,选择模式和工作目录。</p>' +
1849
+ '<p class="modal-subtitle">启动 Claude 会话,选择会话类型、模式和工作目录。</p>' +
1695
1850
  '</div>' +
1696
1851
  '<button id="close-modal-button" class="btn btn-ghost btn-icon">&times;</button>' +
1697
1852
  '</div>' +
1698
1853
  '<div class="modal-body">' +
1854
+ '<div class="field">' +
1855
+ '<label class="field-label">会话类型</label>' +
1856
+ '<div id="session-kind-cards" class="mode-cards">' +
1857
+ renderSessionKindOptions(sessionKind) +
1858
+ '</div>' +
1859
+ '<p id="session-kind-description" class="field-hint">' + escapeHtml(getSessionKindHint(sessionKind)) + '</p>' +
1860
+ '</div>' +
1699
1861
  '<div class="field">' +
1700
1862
  '<label class="field-label">模式</label>' +
1701
1863
  '<div id="mode-cards" class="mode-cards">' +
@@ -1868,6 +2030,16 @@
1868
2030
  quickStartSession();
1869
2031
  });
1870
2032
  }
2033
+ var welcomeStructuredBtn = document.getElementById("welcome-tool-structured");
2034
+ if (welcomeStructuredBtn) {
2035
+ welcomeStructuredBtn.addEventListener("click", function() {
2036
+ createStructuredSession().then(function() {
2037
+ focusInputBox(true);
2038
+ }).catch(function(error) {
2039
+ showToast((error && error.message) || "无法启动结构化会话。", "error");
2040
+ });
2041
+ });
2042
+ }
1871
2043
  initBlankChatCwd();
1872
2044
 
1873
2045
  var sessionsList = document.getElementById("sessions-list");
@@ -1880,6 +2052,17 @@
1880
2052
  // Claude session ID badge click-to-copy (event delegation on document)
1881
2053
  document.addEventListener("click", handleClaudeIdCopy);
1882
2054
 
2055
+ var kindCardsEl = document.getElementById("session-kind-cards");
2056
+ if (kindCardsEl) kindCardsEl.addEventListener("click", function(e) {
2057
+ var card = e.target.closest(".session-kind-card");
2058
+ if (!card) return;
2059
+ var kind = card.getAttribute("data-session-kind");
2060
+ if (kind) {
2061
+ state.sessionCreateKind = kind;
2062
+ syncSessionModalUI();
2063
+ }
2064
+ });
2065
+
1883
2066
  var modeCardsEl = document.getElementById("mode-cards");
1884
2067
  if (modeCardsEl) modeCardsEl.addEventListener("click", function(e) {
1885
2068
  var card = e.target.closest(".mode-card");
@@ -1904,6 +2087,18 @@
1904
2087
  if (drawerBackdrop) drawerBackdrop.addEventListener("click", closeSessionsDrawer);
1905
2088
  var closeDrawerBtn = document.getElementById("close-drawer-button");
1906
2089
  if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
2090
+ var homeBtn = document.getElementById("sidebar-home-btn");
2091
+ if (homeBtn) homeBtn.addEventListener("click", function() {
2092
+ state.selectedId = null;
2093
+ persistSelectedId();
2094
+ resetChatRenderCache();
2095
+ closeSessionsDrawer();
2096
+ render();
2097
+ });
2098
+ var refreshBtn = document.getElementById("sidebar-refresh-btn");
2099
+ if (refreshBtn) refreshBtn.addEventListener("click", function() {
2100
+ window.location.reload();
2101
+ });
1907
2102
  var logoutBtn = document.getElementById("logout-button");
1908
2103
  if (logoutBtn) logoutBtn.addEventListener("click", logout);
1909
2104
  var settingsBtn = document.getElementById("settings-button");
@@ -1953,6 +2148,8 @@
1953
2148
  if (approvePermissionBtn) approvePermissionBtn.addEventListener("click", approvePermission);
1954
2149
  var denyPermissionBtn = document.getElementById("deny-permission-btn");
1955
2150
  if (denyPermissionBtn) denyPermissionBtn.addEventListener("click", denyPermission);
2151
+ var autoApproveToggle = document.getElementById("auto-approve-toggle");
2152
+ if (autoApproveToggle) autoApproveToggle.addEventListener("click", toggleAutoApprove);
1956
2153
  var sendBtn = document.getElementById("send-input-button");
1957
2154
  if (sendBtn) sendBtn.addEventListener("click", function() {
1958
2155
  closeSessionsDrawer();
@@ -2213,7 +2410,7 @@
2213
2410
  state.selectedId = null;
2214
2411
  persistSelectedId();
2215
2412
  state.drafts = {};
2216
- renderApp();
2413
+ render();
2217
2414
  // 聚焦到目录输入框
2218
2415
  setTimeout(function() {
2219
2416
  var folderInput = document.getElementById("folder-picker-input");
@@ -2496,7 +2693,7 @@
2496
2693
 
2497
2694
  function activateSessionItem(sessionId) {
2498
2695
  var session = state.sessions.find(function(s) { return s.id === sessionId; });
2499
- if (session && session.status !== "running") {
2696
+ if (session && session.status !== "running" && !isStructuredSession(session)) {
2500
2697
  resumeSessionFromList(sessionId);
2501
2698
  } else {
2502
2699
  selectSession(sessionId);
@@ -3293,6 +3490,35 @@
3293
3490
  return hints[mode] || '';
3294
3491
  }
3295
3492
 
3493
+ function getSessionKindLabel(session) {
3494
+ return isStructuredSession(session) ? "Structured" : "PTY";
3495
+ }
3496
+
3497
+ function getSessionKindDescription(session) {
3498
+ return isStructuredSession(session)
3499
+ ? "Structured · block transcript"
3500
+ : "PTY · terminal session";
3501
+ }
3502
+
3503
+ function isRecoverableToolError(toolResult, nextResult) {
3504
+ if (!toolResult || !toolResult.is_error || !nextResult || nextResult.is_error) {
3505
+ return false;
3506
+ }
3507
+ var currentText = extractToolResultText(toolResult.content).toLowerCase();
3508
+ var nextText = extractToolResultText(nextResult.content).toLowerCase();
3509
+ if (!currentText) return false;
3510
+ if (currentText.indexOf("invalid pages parameter") !== -1 && nextText.length > 0) {
3511
+ return true;
3512
+ }
3513
+ return false;
3514
+ }
3515
+
3516
+ function isStructuredSession(session) {
3517
+ var result = !!session && (session.sessionKind === "structured" || session.runner === "claude-cli-print");
3518
+ if (session) console.log("[WAND] isStructuredSession id:", session.id, "sessionKind:", session.sessionKind, "runner:", session.runner, "=>", result);
3519
+ return result;
3520
+ }
3521
+
3296
3522
  function syncComposerModeSelect() {
3297
3523
  var select = document.getElementById("chat-mode-select");
3298
3524
  if (!select) return;
@@ -3303,29 +3529,89 @@
3303
3529
  if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
3304
3530
  }
3305
3531
 
3532
+ function createStructuredSession(prompt, cwdOverride, modeOverride) {
3533
+ var payload = {
3534
+ cwd: cwdOverride || getEffectiveCwd(),
3535
+ mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
3536
+ runner: state.structuredRunner || "claude-cli-print",
3537
+ prompt: prompt || undefined
3538
+ };
3539
+ console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
3540
+ return fetch("/api/structured-sessions", {
3541
+ method: "POST",
3542
+ headers: { "Content-Type": "application/json" },
3543
+ credentials: "same-origin",
3544
+ body: JSON.stringify(payload)
3545
+ })
3546
+ .then(function(res) {
3547
+ console.log("[WAND] createStructuredSession response status:", res.status);
3548
+ return res.json();
3549
+ })
3550
+ .then(function(data) {
3551
+ console.log("[WAND] createStructuredSession data:", JSON.stringify({ id: data.id, error: data.error, sessionKind: data.sessionKind, runner: data.runner, status: data.status }));
3552
+ if (data.error) {
3553
+ throw new Error(data.error);
3554
+ }
3555
+ state.selectedId = data.id;
3556
+ persistSelectedId();
3557
+ state.drafts[data.id] = "";
3558
+ resetChatRenderCache();
3559
+ updateSessionSnapshot(data);
3560
+ updateSessionsList();
3561
+ switchToSessionView(data.id);
3562
+ subscribeToSession(data.id);
3563
+ return loadOutput(data.id).then(function() { return data; });
3564
+ });
3565
+ }
3566
+
3306
3567
  function applyCurrentView() {
3307
3568
  var hasSession = !!state.selectedId;
3308
3569
  var terminalBtn = document.getElementById("view-terminal-btn");
3309
3570
  var terminalContainer = document.getElementById("output");
3310
3571
  var chatContainer = document.getElementById("chat-output");
3572
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
3573
+ var structured = isStructuredSession(selectedSession);
3574
+ var showTerminal = hasSession && !structured && state.currentView === "terminal";
3575
+ var showChat = hasSession && (structured || state.currentView !== "terminal");
3576
+ console.log("[WAND] applyCurrentView hasSession:", hasSession, "structured:", structured, "currentView:", state.currentView, "showTerminal:", showTerminal, "showChat:", showChat, "sessionKind:", selectedSession && selectedSession.sessionKind, "runner:", selectedSession && selectedSession.runner);
3577
+
3578
+ if (structured) {
3579
+ state.currentView = "chat";
3580
+ } else if (!hasSession) {
3581
+ state.currentView = "terminal";
3582
+ }
3311
3583
 
3312
- if (terminalBtn) terminalBtn.classList.add("active");
3313
- if (terminalContainer) terminalContainer.classList.toggle("active", hasSession);
3584
+ if (terminalBtn) {
3585
+ terminalBtn.classList.toggle("hidden", structured || !hasSession);
3586
+ terminalBtn.classList.toggle("active", showTerminal);
3587
+ }
3588
+ if (terminalContainer) {
3589
+ terminalContainer.classList.toggle("active", showTerminal);
3590
+ terminalContainer.classList.toggle("hidden", !showTerminal);
3591
+ }
3314
3592
  if (chatContainer) {
3315
- chatContainer.classList.remove("active");
3316
- chatContainer.classList.add("hidden");
3593
+ chatContainer.classList.toggle("active", showChat);
3594
+ chatContainer.classList.toggle("hidden", !showChat);
3317
3595
  }
3318
3596
  updateInteractiveControls();
3319
3597
  }
3320
3598
 
3321
3599
  function syncSessionModalUI() {
3322
3600
  var modeHint = document.getElementById("mode-description");
3601
+ var kindHint = document.getElementById("session-kind-description");
3323
3602
  var tool = "claude";
3603
+ var sessionKind = state.sessionCreateKind || "pty";
3324
3604
 
3325
3605
  state.sessionTool = tool;
3326
3606
  state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
3327
3607
 
3328
- // Update mode cards active state
3608
+ var kindCards = document.querySelectorAll("#session-kind-cards .session-kind-card");
3609
+ if (kindCards.length) {
3610
+ kindCards.forEach(function(card) {
3611
+ card.classList.toggle("active", card.getAttribute("data-session-kind") === sessionKind);
3612
+ });
3613
+ }
3614
+
3329
3615
  var modeCards = document.querySelectorAll("#mode-cards .mode-card");
3330
3616
  if (modeCards.length) {
3331
3617
  modeCards.forEach(function(card) {
@@ -3333,11 +3619,22 @@
3333
3619
  });
3334
3620
  }
3335
3621
 
3622
+ if (kindHint) kindHint.textContent = getSessionKindHint(sessionKind);
3336
3623
  if (modeHint) modeHint.textContent = getToolModeHint(tool, state.modeValue);
3337
3624
  }
3338
3625
 
3339
3626
  function updateSessionSnapshot(snapshot) {
3340
3627
  if (!snapshot || !snapshot.id) return;
3628
+ if (snapshot.id === state.selectedId || (snapshot.sessionKind === "structured") || snapshot.structuredState) {
3629
+ console.log("[WAND] updateSessionSnapshot", snapshot.id, JSON.stringify({
3630
+ status: snapshot.status,
3631
+ exitCode: snapshot.exitCode,
3632
+ sessionKind: snapshot.sessionKind,
3633
+ runner: snapshot.runner,
3634
+ inFlight: snapshot.structuredState && snapshot.structuredState.inFlight,
3635
+ msgCount: snapshot.messages && snapshot.messages.length
3636
+ }));
3637
+ }
3341
3638
  var updated = false;
3342
3639
  var prevSession = null;
3343
3640
  state.sessions = state.sessions.map(function(session) {
@@ -3367,14 +3664,34 @@
3367
3664
  var localOutput = localSession.output || "";
3368
3665
  var serverOutput = serverSession.output || "";
3369
3666
  var keepLocalOutput = localOutput.length > serverOutput.length;
3667
+ var localStructuredState = localSession.structuredState || null;
3668
+ var serverStructuredState = serverSession.structuredState || null;
3669
+ var localHasPendingAssistant = !!(localSession.messages && localSession.messages.length && (function() {
3670
+ var last = localSession.messages[localSession.messages.length - 1];
3671
+ return last && last.role === "assistant" && Array.isArray(last.content) && last.content.some(function(block) {
3672
+ return block && block.__processing;
3673
+ });
3674
+ })());
3675
+ var preserveLocalStructuredProgress = (localSession.sessionKind === "structured")
3676
+ && !!localStructuredState
3677
+ && localStructuredState.inFlight === true
3678
+ && (!serverStructuredState || serverStructuredState.inFlight !== true)
3679
+ && localHasPendingAssistant
3680
+ && !!localStructuredState.activeRequestId
3681
+ && (!serverStructuredState || !serverStructuredState.activeRequestId || serverStructuredState.activeRequestId === localStructuredState.activeRequestId);
3370
3682
 
3371
3683
  if (keepLocalOutput) {
3372
3684
  merged.output = localOutput;
3373
3685
  }
3374
3686
 
3687
+ if (preserveLocalStructuredProgress) {
3688
+ merged.status = localSession.status || merged.status;
3689
+ merged.structuredState = Object.assign({}, serverStructuredState || {}, localStructuredState, { inFlight: true });
3690
+ merged.messages = localSession.messages;
3691
+ }
3692
+
3375
3693
  if (localSession.id === state.selectedId) {
3376
3694
  if (localSession.permissionBlocked && serverSession.permissionBlocked === false) {
3377
- // server explicitly resolved it; keep resolved state
3378
3695
  } else if (localSession.permissionBlocked && !serverSession.permissionBlocked) {
3379
3696
  merged.permissionBlocked = true;
3380
3697
  }
@@ -3409,15 +3726,13 @@
3409
3726
 
3410
3727
  function getPreferredSessionId(sessions) {
3411
3728
  if (!sessions || !sessions.length) return null;
3412
- // Keep currently selected session as long as it still exists
3413
3729
  if (state.selectedId) {
3414
3730
  var stillExists = sessions.find(function(session) { return session.id === state.selectedId; });
3415
3731
  if (stillExists) return stillExists.id;
3732
+ return null;
3416
3733
  }
3417
- // No selection — pick a running session, or fall back to most recent
3418
3734
  var runningSession = sessions.find(function(session) { return session.status === "running"; });
3419
3735
  if (runningSession) return runningSession.id;
3420
- // Fall back to most recent non-archived session (sessions are sorted newest first)
3421
3736
  var recent = sessions.find(function(session) { return !session.archived; });
3422
3737
  return recent ? recent.id : sessions[0].id;
3423
3738
  }
@@ -3444,7 +3759,10 @@
3444
3759
  return mergeServerSession(localSession, serverSession);
3445
3760
  });
3446
3761
 
3447
- state.selectedId = getPreferredSessionId(state.sessions);
3762
+ var preferredSessionId = getPreferredSessionId(state.sessions);
3763
+ if (preferredSessionId !== undefined) {
3764
+ state.selectedId = preferredSessionId;
3765
+ }
3448
3766
  persistSelectedId();
3449
3767
  if (state.modalOpen) {
3450
3768
  updateSessionsList();
@@ -3461,13 +3779,22 @@
3461
3779
  }
3462
3780
  }
3463
3781
  updateShellChrome();
3782
+
3783
+ // For structured sessions, loadOutput is needed to fetch messages
3784
+ // (the sessions list endpoint doesn't include them).
3785
+ // On page refresh this is the only place that can trigger it.
3786
+ if (state.selectedId) {
3787
+ var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
3788
+ if (isStructuredSession(sel)) {
3789
+ loadOutput(state.selectedId);
3790
+ }
3791
+ }
3464
3792
  })
3465
3793
  .catch(function(e) {
3466
3794
  console.error("[wand] loadSessions failed:", e);
3467
3795
  });
3468
3796
  }
3469
3797
 
3470
-
3471
3798
  function updateSessionsList() {
3472
3799
  var listEl = document.getElementById("sessions-list");
3473
3800
  var countEl = document.getElementById("session-count");
@@ -3495,22 +3822,23 @@
3495
3822
 
3496
3823
  if (summaryEl && summaryEl.textContent !== terminalTitle) summaryEl.textContent = terminalTitle;
3497
3824
  if (titleEl && titleEl.textContent !== terminalTitle) titleEl.textContent = terminalTitle;
3498
- if (infoEl && infoEl.textContent !== terminalInfo) {
3499
- infoEl.textContent = terminalInfo;
3500
- }
3825
+ if (infoEl) infoEl.textContent = selectedSession ? (terminalInfo + " · " + getSessionKindDescription(selectedSession)) : terminalInfo;
3501
3826
 
3502
- // Update session info bar at bottom
3503
3827
  var cwdEl = document.getElementById("session-cwd-display");
3504
3828
  var modeEl = document.getElementById("session-mode-display");
3829
+ var kindEl = document.getElementById("session-kind-display");
3505
3830
  var statusEl = document.getElementById("session-status-display");
3506
3831
  var exitEl = document.getElementById("session-exit-display");
3507
3832
  var cwdText = selectedSession && selectedSession.cwd ? selectedSession.cwd : "未设置目录";
3508
3833
  var modeText = selectedSession ? getModeLabel(selectedSession.mode) : "默认";
3834
+ var kindText = selectedSession ? getSessionKindLabel(selectedSession) : "PTY";
3509
3835
  var exitText = "exit=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
3510
3836
  if (cwdEl && cwdEl.textContent !== cwdText) cwdEl.textContent = cwdText;
3511
3837
  if (modeEl && modeEl.textContent !== modeText) modeEl.textContent = modeText;
3838
+ if (kindEl && kindEl.textContent !== kindText) kindEl.textContent = kindText;
3512
3839
  if (statusEl && statusEl.textContent !== terminalInfo) statusEl.textContent = terminalInfo;
3513
3840
  if (exitEl && exitEl.textContent !== exitText) exitEl.textContent = exitText;
3841
+ updateAutoApproveIndicator();
3514
3842
 
3515
3843
  if (!state.terminal && terminalContainer && selectedSession) {
3516
3844
  initTerminal();
@@ -3558,7 +3886,12 @@
3558
3886
  clearTimeout(chatRenderTimer);
3559
3887
  chatRenderTimer = null;
3560
3888
  }
3561
- return fetch("/api/sessions/" + id, { credentials: "same-origin" })
3889
+ var sess = state.sessions.find(function(s) { return s.id === id; });
3890
+ var url = "/api/sessions/" + id;
3891
+ if (isStructuredSession(sess)) {
3892
+ url += "?format=chat";
3893
+ }
3894
+ return fetch(url, { credentials: "same-origin" })
3562
3895
  .then(function(res) { return res.json(); })
3563
3896
  .then(function(data) {
3564
3897
  if (data.error) {
@@ -3585,11 +3918,11 @@
3585
3918
  }
3586
3919
 
3587
3920
  function selectSession(id) {
3921
+ var foundSession = state.sessions.find(function(item) { return item.id === id; });
3922
+ console.log("[WAND] selectSession id:", id, "found:", !!foundSession, "sessionKind:", foundSession && foundSession.sessionKind, "runner:", foundSession && foundSession.runner, "isStructured:", isStructuredSession(foundSession));
3588
3923
  state.selectedId = id;
3589
3924
  persistSelectedId();
3590
- state.lastRenderedHash = 0;
3591
- state.lastRenderedMsgCount = 0;
3592
- state.lastRenderedEmpty = null;
3925
+ resetChatRenderCache();
3593
3926
  state.currentMessages = [];
3594
3927
  if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
3595
3928
  // Reset todo progress bar
@@ -3662,6 +3995,7 @@
3662
3995
  modal.classList.remove("hidden");
3663
3996
  lastFocusedElement = document.activeElement;
3664
3997
  state.sessionTool = getPreferredTool();
3998
+ state.sessionCreateKind = "pty";
3665
3999
  state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
3666
4000
  syncSessionModalUI();
3667
4001
  loadRecentPathBubbles();
@@ -4176,9 +4510,7 @@
4176
4510
  state.selectedId = data.id;
4177
4511
  persistSelectedId();
4178
4512
  state.drafts[data.id] = "";
4179
- state.lastRenderedHash = 0;
4180
- state.lastRenderedMsgCount = 0;
4181
- state.lastRenderedEmpty = null;
4513
+ resetChatRenderCache();
4182
4514
  return refreshAll();
4183
4515
  })
4184
4516
  .then(function() { focusInputBox(true); })
@@ -4191,13 +4523,45 @@
4191
4523
  var cwdEl = document.getElementById("cwd");
4192
4524
  var errorEl = document.getElementById("modal-error");
4193
4525
  var command = getPreferredTool();
4526
+ var sessionKind = state.sessionCreateKind || "pty";
4194
4527
 
4195
4528
  hideError(errorEl);
4196
4529
 
4197
4530
  var defaultCwd = getEffectiveCwd();
4531
+ var cwd = cwdEl.value.trim() || defaultCwd;
4198
4532
  var selectedMode = getSafeModeForTool(command, state.modeValue);
4199
- state.modeValue = selectedMode;
4200
- state.chatMode = selectedMode;
4533
+
4534
+ if (sessionKind === "structured") {
4535
+ startStructuredSessionFromModal(cwd, selectedMode, errorEl);
4536
+ return;
4537
+ }
4538
+
4539
+ runPtyCommandFromModal(command, cwd, selectedMode, errorEl);
4540
+ }
4541
+
4542
+ function startStructuredSessionFromModal(cwd, mode, errorEl) {
4543
+ console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode);
4544
+ state.modeValue = mode;
4545
+ state.chatMode = mode;
4546
+ state.sessionTool = "claude";
4547
+ state.preferredCommand = "claude";
4548
+ syncComposerModeSelect();
4549
+ return createStructuredSession(undefined, cwd, mode)
4550
+ .then(function(data) {
4551
+ closeSessionModal();
4552
+ closeSessionsDrawer();
4553
+ return data;
4554
+ })
4555
+ .then(function() { focusInputBox(true); })
4556
+ .catch(function(error) {
4557
+ showError(errorEl, (error && error.message) || "无法启动结构化会话,请确认 Claude 已正确安装。");
4558
+ });
4559
+ }
4560
+
4561
+ function runPtyCommandFromModal(command, cwd, mode, errorEl) {
4562
+ console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode);
4563
+ state.modeValue = mode;
4564
+ state.chatMode = mode;
4201
4565
  state.sessionTool = command;
4202
4566
  state.preferredCommand = command;
4203
4567
  syncComposerModeSelect();
@@ -4208,8 +4572,8 @@
4208
4572
  credentials: "same-origin",
4209
4573
  body: JSON.stringify({
4210
4574
  command: command,
4211
- cwd: cwdEl.value.trim() || defaultCwd,
4212
- mode: selectedMode
4575
+ cwd: cwd,
4576
+ mode: mode
4213
4577
  })
4214
4578
  })
4215
4579
  .then(function(res) { return res.json(); })
@@ -4219,22 +4583,27 @@
4219
4583
  return;
4220
4584
  }
4221
4585
  state.selectedId = data.id;
4586
+ console.log("[WAND] runPtyCommandFromModal created session:", data.id, "sessionKind:", data.sessionKind, "runner:", data.runner);
4222
4587
  persistSelectedId();
4223
4588
  state.drafts[data.id] = "";
4224
- state.lastRenderedHash = 0;
4225
- state.lastRenderedMsgCount = 0;
4226
- state.lastRenderedEmpty = null;
4589
+ resetChatRenderCache();
4227
4590
  closeSessionModal();
4228
4591
  closeSessionsDrawer();
4229
4592
  return refreshAll();
4230
4593
  })
4231
- .then(function() { focusInputBox(true); })
4594
+ .then(function() {
4595
+ if (state.selectedId) {
4596
+ console.log("[WAND] runPtyCommandFromModal calling selectSession:", state.selectedId);
4597
+ selectSession(state.selectedId);
4598
+ } else {
4599
+ focusInputBox(true);
4600
+ }
4601
+ })
4232
4602
  .catch(function() {
4233
4603
  showError(errorEl, "无法启动会话,请确认 Claude 已正确安装。");
4234
4604
  });
4235
4605
  }
4236
4606
 
4237
- // Blank-chat CWD inline display + dropdown
4238
4607
  function initBlankChatCwd() {
4239
4608
  var cwdEl = document.getElementById("blank-chat-cwd");
4240
4609
  if (!cwdEl) return;
@@ -4653,14 +5022,14 @@
4653
5022
  state.selectedId = data.id;
4654
5023
  persistSelectedId();
4655
5024
  state.drafts[data.id] = "";
4656
- state.lastRenderedHash = 0;
4657
- state.lastRenderedMsgCount = 0;
4658
- state.lastRenderedEmpty = null;
4659
- switchToSessionView(data.id);
5025
+ resetChatRenderCache();
4660
5026
  updateSessionSnapshot(data);
4661
5027
  updateSessionsList();
5028
+ switchToSessionView(data.id);
4662
5029
  subscribeToSession(data.id);
4663
5030
  loadOutput(data.id).then(function() {
5031
+ welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
5032
+ welcomeInput.disabled = false;
4664
5033
  focusInputBox(true);
4665
5034
  });
4666
5035
  })
@@ -4711,14 +5080,12 @@
4711
5080
  state.selectedId = data.id;
4712
5081
  persistSelectedId();
4713
5082
  state.drafts[data.id] = "";
4714
- state.lastRenderedHash = 0;
4715
- state.lastRenderedMsgCount = 0;
4716
- state.lastRenderedEmpty = null;
5083
+ resetChatRenderCache();
4717
5084
  if (inputBox) inputBox.value = "";
4718
5085
  if (welcomeInput) welcomeInput.value = "";
4719
- switchToSessionView(data.id);
4720
5086
  updateSessionSnapshot(data);
4721
5087
  updateSessionsList();
5088
+ switchToSessionView(data.id);
4722
5089
  // Subscribe to new session via WebSocket
4723
5090
  subscribeToSession(data.id);
4724
5091
  return loadOutput(data.id);
@@ -4730,6 +5097,7 @@
4730
5097
 
4731
5098
  function switchToSessionView(sessionId) {
4732
5099
  var session = state.sessions.find(function(s) { return s.id === sessionId; });
5100
+ console.log("[WAND] switchToSessionView id:", sessionId, "found:", !!session, "sessionKind:", session && session.sessionKind, "runner:", session && session.runner, "isStructured:", isStructuredSession(session), "currentView:", state.currentView);
4733
5101
  var blankChat = document.getElementById("blank-chat");
4734
5102
  var terminalContainer = document.getElementById("output");
4735
5103
  var chatContainer = document.getElementById("chat-output");
@@ -4737,28 +5105,36 @@
4737
5105
  var terminalTitle = document.getElementById("terminal-title");
4738
5106
  var terminalInfo = document.getElementById("terminal-info");
4739
5107
  var sessionSummary = document.querySelector(".session-summary-value");
5108
+ var structured = isStructuredSession(session);
4740
5109
 
4741
5110
  if (blankChat) blankChat.classList.add("hidden");
4742
- if (terminalContainer) terminalContainer.classList.remove("hidden");
5111
+ if (terminalContainer) {
5112
+ terminalContainer.classList.toggle("hidden", structured);
5113
+ }
4743
5114
  if (chatContainer) {
4744
5115
  chatContainer.classList.remove("hidden");
4745
5116
  }
4746
5117
  if (stopBtn) stopBtn.classList.remove("hidden");
4747
5118
 
5119
+ if (structured) {
5120
+ state.currentView = "chat";
5121
+ } else {
5122
+ state.currentView = "terminal";
5123
+ }
5124
+
4748
5125
  var title = session ? shortCommand(session.command) : "Wand";
4749
5126
  var info = session ? getSessionStatusLabel(session) : "开始对话";
4750
5127
  if (terminalTitle) terminalTitle.textContent = title;
4751
5128
  if (terminalInfo) terminalInfo.textContent = info;
4752
5129
  if (sessionSummary) sessionSummary.textContent = title;
4753
5130
 
4754
- // Init terminal if not already done
4755
- if (!state.terminal) initTerminal();
4756
- applyCurrentView();
4757
- if (state.terminal && state.fitAddon) {
4758
- ensureTerminalFit();
5131
+ if (!structured) {
5132
+ if (!state.terminal) initTerminal();
5133
+ if (state.terminal && state.fitAddon) {
5134
+ ensureTerminalFit();
5135
+ }
4759
5136
  }
4760
- // Don't call renderChat() here — loadOutput() always calls renderChat() after it resolves.
4761
- // Calling renderChat() prematurely would render with stale/empty messages.
5137
+ applyCurrentView();
4762
5138
  focusInputBox();
4763
5139
  }
4764
5140
 
@@ -4773,9 +5149,12 @@
4773
5149
  var value = inputBox ? inputBox.value : "";
4774
5150
  var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; }) || null;
4775
5151
  if (value) {
4776
- console.log("[wand] sendInputFromBox", {
5152
+ console.log("[WAND] sendInputFromBox", {
4777
5153
  sessionId: state.selectedId,
4778
5154
  sessionStatus: selectedSession ? selectedSession.status : null,
5155
+ sessionKind: selectedSession ? selectedSession.sessionKind : null,
5156
+ runner: selectedSession ? selectedSession.runner : null,
5157
+ isStructured: isStructuredSession(selectedSession),
4779
5158
  view: state.currentView,
4780
5159
  wsConnected: state.wsConnected,
4781
5160
  terminalInteractive: state.terminalInteractive,
@@ -4784,6 +5163,11 @@
4784
5163
  // Clear todo progress bar at the start of a new user turn
4785
5164
  var todoEl = document.getElementById("todo-progress");
4786
5165
  if (todoEl) todoEl.classList.add("hidden");
5166
+
5167
+ if (isStructuredSession(selectedSession)) {
5168
+ return postStructuredInput(value, inputBox, selectedSession);
5169
+ }
5170
+
4787
5171
  // Send text + Enter as a single call to avoid race conditions
4788
5172
  var combinedInput = value + getControlInput("enter");
4789
5173
  var isOffline = !state.wsConnected;
@@ -4824,6 +5208,79 @@
4824
5208
  return Promise.resolve();
4825
5209
  }
4826
5210
 
5211
+ function postStructuredInput(input, inputBox, session) {
5212
+ console.log("[WAND] postStructuredInput selectedId:", state.selectedId, "input:", input && input.substring(0, 50), "session:", session && { id: session.id, sessionKind: session.sessionKind, runner: session.runner, status: session.status, inFlight: session.structuredState && session.structuredState.inFlight });
5213
+ if (!state.selectedId || !input) return Promise.resolve();
5214
+ if (!session) {
5215
+ showToast("会话不存在,请重新选择或新建会话。", "error");
5216
+ return Promise.resolve();
5217
+ }
5218
+ if (session.structuredState && session.structuredState.inFlight && session.status === "running") {
5219
+ // Disable send button while processing, show subtle indicator
5220
+ var sendBtn = document.getElementById("send-input-button");
5221
+ if (sendBtn) sendBtn.disabled = true;
5222
+ showToast("正在等待上一条消息处理完成…", "info");
5223
+ return Promise.resolve();
5224
+ }
5225
+
5226
+ // Immediately render user message with thinking indicator
5227
+ var userTurn = { role: "user", content: [{ type: "text", text: input }] };
5228
+ var thinkingTurn = { role: "assistant", content: [{ type: "text", text: "", __processing: true }] };
5229
+ var userMsgs = Array.isArray(session.messages) ? session.messages.slice() : [];
5230
+ userMsgs.push(userTurn);
5231
+ userMsgs.push(thinkingTurn);
5232
+ session.messages = userMsgs;
5233
+ state.currentMessages = userMsgs;
5234
+ // Mark inFlight optimistically to prevent double-send via WS updates
5235
+ if (session.structuredState) {
5236
+ session.structuredState.inFlight = true;
5237
+ }
5238
+ session.status = "running";
5239
+ if (inputBox) {
5240
+ inputBox.value = "";
5241
+ autoResizeInput(inputBox);
5242
+ }
5243
+ // Disable send button so user can't double-send
5244
+ var sendBtnEl = document.getElementById("send-input-button");
5245
+ if (sendBtnEl) sendBtnEl.disabled = true;
5246
+ updateInputHint("思考中…");
5247
+ setDraftValue("");
5248
+ renderChat(true);
5249
+
5250
+ return fetch("/api/structured-sessions/" + state.selectedId + "/messages", {
5251
+ method: "POST",
5252
+ headers: { "Content-Type": "application/json" },
5253
+ credentials: "same-origin",
5254
+ body: JSON.stringify({ input: input })
5255
+ })
5256
+ .then(function(res) { return res.json(); })
5257
+ .then(function(snapshot) {
5258
+ if (snapshot && snapshot.error) {
5259
+ throw new Error(snapshot.error);
5260
+ }
5261
+ if (snapshot && snapshot.id) {
5262
+ updateSessionSnapshot(snapshot);
5263
+ if (snapshot.messages && snapshot.messages.length > 0) {
5264
+ state.currentMessages = snapshot.messages;
5265
+ }
5266
+ renderChat(true);
5267
+ updateInputHint("Enter 发送 · Shift+Enter 换行");
5268
+ }
5269
+ })
5270
+ .catch(function(error) {
5271
+ var errSendBtn = document.getElementById("send-input-button");
5272
+ if (errSendBtn) errSendBtn.disabled = false;
5273
+ updateInputHint("Enter 发送 · Shift+Enter 换行");
5274
+ showToast((error && error.message) || "无法发送结构化消息。", "error");
5275
+ throw error;
5276
+ });
5277
+ }
5278
+
5279
+ function updateInputHint(text) {
5280
+ var hint = document.querySelector(".input-hint");
5281
+ if (hint) hint.textContent = text;
5282
+ }
5283
+
4827
5284
  function getInputErrorMessage(error) {
4828
5285
  if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
4829
5286
  return "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
@@ -4875,6 +5332,7 @@
4875
5332
  }
4876
5333
 
4877
5334
  function ensureSessionReadyForInput(session, errorEl) {
5335
+ console.log("[WAND] ensureSessionReadyForInput session:", session && { id: session.id, status: session.status, claudeSessionId: session.claudeSessionId, sessionKind: session.sessionKind, runner: session.runner });
4878
5336
  if (!session) {
4879
5337
  showToast("会话不存在,请重新选择或新建会话。", "error");
4880
5338
  return Promise.resolve(null);
@@ -5170,21 +5628,24 @@
5170
5628
  }
5171
5629
 
5172
5630
  function updateInteractiveControls() {
5631
+ var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
5632
+ var structured = isStructuredSession(selectedSession);
5173
5633
  // Update both toggle buttons (topbar and terminal-header)
5174
5634
  var toggles = ["terminal-interactive-toggle-top"];
5175
5635
  toggles.forEach(function(id) {
5176
5636
  var toggle = document.getElementById(id);
5177
5637
  if (toggle) {
5178
5638
  toggle.classList.toggle("active", state.terminalInteractive);
5639
+ toggle.classList.toggle("hidden", structured || state.currentView !== "terminal" || !selectedSession);
5179
5640
  }
5180
5641
  });
5181
5642
  // Inline keyboard visibility follows current view
5182
5643
  var inlineKeyboard = document.getElementById("inline-keyboard");
5183
- if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", state.currentView !== "terminal");
5644
+ if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", structured || state.currentView !== "terminal");
5184
5645
  var inputHint = document.querySelector(".input-hint");
5185
- if (inputHint) inputHint.classList.toggle("hidden", state.currentView === "terminal");
5646
+ if (inputHint) inputHint.classList.toggle("hidden", structured ? false : state.currentView === "terminal");
5186
5647
  var container = document.getElementById("output");
5187
- if (container) container.classList.toggle("interactive", state.terminalInteractive);
5648
+ if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
5188
5649
  }
5189
5650
 
5190
5651
  function captureTerminalInput(event) {
@@ -5540,6 +6001,7 @@
5540
6001
  }
5541
6002
 
5542
6003
  function resumeSession(sessionId, errorEl) {
6004
+ console.log("[WAND] resumeSession sessionId:", sessionId);
5543
6005
  if (!sessionId) return Promise.resolve(null);
5544
6006
  return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/resume", {
5545
6007
  method: "POST",
@@ -5601,9 +6063,7 @@
5601
6063
 
5602
6064
  function activateSession(data) {
5603
6065
  if (!data || !data.id) return Promise.resolve();
5604
- state.lastRenderedHash = 0;
5605
- state.lastRenderedMsgCount = 0;
5606
- state.lastRenderedEmpty = null;
6066
+ resetChatRenderCache();
5607
6067
  switchToSessionView(data.id);
5608
6068
  updateSessionSnapshot(data);
5609
6069
  updateSessionsList();
@@ -5614,6 +6074,7 @@
5614
6074
  }
5615
6075
 
5616
6076
  function resumeSessionFromList(sessionId) {
6077
+ console.log("[WAND] resumeSessionFromList sessionId:", sessionId);
5617
6078
  return resumeSession(sessionId).then(function(data) {
5618
6079
  if (!data) return null;
5619
6080
  return activateSession(data).then(function() {
@@ -5702,6 +6163,7 @@
5702
6163
  }
5703
6164
 
5704
6165
  function handleResumeAction(actionButton) {
6166
+ console.log("[WAND] handleResumeAction sessionId:", actionButton.dataset.sessionId);
5705
6167
  actionButton.disabled = true;
5706
6168
  resumeSessionFromList(actionButton.dataset.sessionId)
5707
6169
  .finally(function() {
@@ -5712,6 +6174,7 @@
5712
6174
  function handleResumeHistoryAction(actionButton) {
5713
6175
  var claudeSessionId = actionButton.dataset.claudeSessionId;
5714
6176
  var cwd = actionButton.dataset.cwd;
6177
+ console.log("[WAND] handleResumeHistoryAction claudeSessionId:", claudeSessionId, "cwd:", cwd);
5715
6178
  if (!claudeSessionId) return;
5716
6179
  actionButton.disabled = true;
5717
6180
  resumeClaudeHistorySession(claudeSessionId, cwd)
@@ -6757,15 +7220,18 @@
6757
7220
  state.fitAddon.fit();
6758
7221
  maybeScrollTerminalToBottom("resize");
6759
7222
  if (state.selectedId && state.terminal) {
6760
- var nextSize = { cols: state.terminal.cols, rows: state.terminal.rows };
6761
- if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
6762
- state.lastResize = nextSize;
6763
- fetch("/api/sessions/" + state.selectedId + "/resize", {
6764
- method: "POST",
6765
- headers: { "Content-Type": "application/json" },
6766
- credentials: "same-origin",
6767
- body: JSON.stringify(nextSize)
6768
- }).catch(function() {});
7223
+ var selectedSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
7224
+ if (!isStructuredSession(selectedSess)) {
7225
+ var nextSize = { cols: state.terminal.cols, rows: state.terminal.rows };
7226
+ if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
7227
+ state.lastResize = nextSize;
7228
+ fetch("/api/sessions/" + state.selectedId + "/resize", {
7229
+ method: "POST",
7230
+ headers: { "Content-Type": "application/json" },
7231
+ credentials: "same-origin",
7232
+ body: JSON.stringify(nextSize)
7233
+ }).catch(function() {});
7234
+ }
6769
7235
  }
6770
7236
  }
6771
7237
  } else if (attempt < maxAttempts) {
@@ -6805,6 +7271,10 @@
6805
7271
 
6806
7272
  if (!state.selectedId) return;
6807
7273
 
7274
+ // Skip resize for structured sessions (no PTY)
7275
+ var resizeSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
7276
+ if (isStructuredSession(resizeSess)) return;
7277
+
6808
7278
  // Only send resize API call if dimensions actually changed
6809
7279
  if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
6810
7280
  state.lastResize = nextSize;
@@ -6899,6 +7369,17 @@
6899
7369
  updateSessionSnapshot(snapshot);
6900
7370
  if (msg.sessionId === state.selectedId) {
6901
7371
  state.currentMessages = getPreferredMessages(snapshot, msg.data.output, false);
7372
+ // Structured session with inFlight: keep __processing placeholder
7373
+ // so the loading indicator stays visible until assistant content arrives
7374
+ if (msg.data.sessionKind === 'structured') {
7375
+ var outSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
7376
+ if (outSession && outSession.structuredState && outSession.structuredState.inFlight) {
7377
+ var lastCur = state.currentMessages[state.currentMessages.length - 1];
7378
+ if (!lastCur || lastCur.role !== 'assistant') {
7379
+ state.currentMessages.push({ role: "assistant", content: [{ type: "text", text: "", __processing: true }] });
7380
+ }
7381
+ }
7382
+ }
6902
7383
  updateTaskDisplay();
6903
7384
  scheduleChatRender();
6904
7385
  }
@@ -6937,8 +7418,19 @@
6937
7418
  if (msg.data && msg.data.messages) {
6938
7419
  endedSnapshot.messages = msg.data.messages;
6939
7420
  }
7421
+ if (msg.data && msg.data.structuredState) {
7422
+ endedSnapshot.structuredState = msg.data.structuredState;
7423
+ }
6940
7424
  updateSessionSnapshot(endedSnapshot);
6941
7425
 
7426
+ // Re-enable send button when structured session finishes
7427
+ if (msg.sessionId === state.selectedId) {
7428
+ var endedSendBtn = document.getElementById("send-input-button");
7429
+ if (endedSendBtn) endedSendBtn.disabled = false;
7430
+ updateInputHint("Enter 发送 · Shift+Enter 换行");
7431
+ // Trigger status bar completion animation
7432
+ scheduleChatRender(true);
7433
+ }
6942
7434
  // Notify user when a session completes (browser + in-app if backgrounded or not viewing)
6943
7435
  var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
6944
7436
  var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
@@ -7013,7 +7505,24 @@
7013
7505
  break;
7014
7506
  case 'status':
7015
7507
  if (msg.sessionId && msg.data) {
7508
+ console.log('[WAND] ws status', msg.sessionId, JSON.stringify(msg.data));
7016
7509
  var statusUpdate = { id: msg.sessionId };
7510
+ if (Object.prototype.hasOwnProperty.call(msg.data, 'status')) {
7511
+ statusUpdate.status = msg.data.status;
7512
+ }
7513
+ if (Object.prototype.hasOwnProperty.call(msg.data, 'exitCode')) {
7514
+ statusUpdate.exitCode = msg.data.exitCode;
7515
+ }
7516
+ if (msg.data.structuredState) {
7517
+ statusUpdate.structuredState = msg.data.structuredState;
7518
+ } else if (Object.prototype.hasOwnProperty.call(msg.data, 'status')) {
7519
+ var existingSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
7520
+ if (existingSession && existingSession.sessionKind === 'structured') {
7521
+ statusUpdate.structuredState = Object.assign({}, existingSession.structuredState || {}, {
7522
+ inFlight: msg.data.status === 'running'
7523
+ });
7524
+ }
7525
+ }
7017
7526
  if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
7018
7527
  statusUpdate.permissionBlocked = !!msg.data.permissionBlocked;
7019
7528
  }
@@ -7056,9 +7565,19 @@
7056
7565
  if (msg.data.permissionBlocked === false) {
7057
7566
  statusUpdate.pendingEscalation = null;
7058
7567
  }
7568
+ if (msg.data.approvalStats) {
7569
+ statusUpdate.approvalStats = msg.data.approvalStats;
7570
+ }
7059
7571
  updateSessionSnapshot(statusUpdate);
7060
7572
  if (msg.sessionId === state.selectedId) {
7061
7573
  updateTaskDisplay();
7574
+ if (msg.data.approvalStats) {
7575
+ updateApprovalStats();
7576
+ }
7577
+ // Re-render chat when structured session inFlight state changes
7578
+ if (statusUpdate.structuredState) {
7579
+ scheduleChatRender();
7580
+ }
7062
7581
  }
7063
7582
  }
7064
7583
  break;
@@ -7098,9 +7617,12 @@
7098
7617
  var isBlocked = pendingEscalation || (selectedSession && selectedSession.permissionBlocked);
7099
7618
 
7100
7619
  if (isBlocked) {
7620
+ var isAutoApprove = selectedSession && selectedSession.autoApprovePermissions;
7101
7621
  // Show permission label in input composer area
7102
7622
  if (permissionLabel) {
7103
- if (pendingEscalation) {
7623
+ if (isAutoApprove) {
7624
+ permissionLabel.textContent = "自动批准中...";
7625
+ } else if (pendingEscalation) {
7104
7626
  var reason = pendingEscalation.reason || "等待授权";
7105
7627
  var target = pendingEscalation.target ? " · " + pendingEscalation.target : "";
7106
7628
  permissionLabel.textContent = reason + target;
@@ -7108,7 +7630,14 @@
7108
7630
  permissionLabel.textContent = "等待授权";
7109
7631
  }
7110
7632
  }
7111
- if (permissionActionsEl) permissionActionsEl.classList.remove("hidden");
7633
+ if (permissionActionsEl) {
7634
+ permissionActionsEl.classList.remove("hidden");
7635
+ // Hide approve/deny buttons when auto-approve is active
7636
+ var approveBtn = document.getElementById("approve-permission-btn");
7637
+ var denyBtn = document.getElementById("deny-permission-btn");
7638
+ if (approveBtn) approveBtn.classList.toggle("hidden", !!isAutoApprove);
7639
+ if (denyBtn) denyBtn.classList.toggle("hidden", !!isAutoApprove);
7640
+ }
7112
7641
  // Hide top task bar — permission info is already shown in the composer
7113
7642
  taskEl.textContent = "";
7114
7643
  taskEl.classList.add("hidden");
@@ -7128,6 +7657,39 @@
7128
7657
  }
7129
7658
  }
7130
7659
 
7660
+ function updateApprovalStats() {
7661
+ var container = document.getElementById("approval-stats");
7662
+ if (!container) return;
7663
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
7664
+ var stats = selectedSession && selectedSession.approvalStats;
7665
+ if (!stats || stats.total === 0) {
7666
+ container.className = "approval-stats hidden";
7667
+ container.innerHTML = "";
7668
+ return;
7669
+ }
7670
+ container.className = "approval-stats";
7671
+ container.innerHTML =
7672
+ '<span class="approval-stats-divider"></span>' +
7673
+ '<span class="approval-stats-badge" id="approval-stats-badge" title="本次会话自动批准统计">' +
7674
+ '<svg class="approval-stats-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>' +
7675
+ '<span class="approval-stats-total">' + stats.total + '</span>' +
7676
+ '</span>' +
7677
+ '<span class="approval-stats-popup" id="approval-stats-popup">' +
7678
+ '<span class="approval-stats-popup-title">自动批准统计</span>' +
7679
+ (stats.command > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">⚡</span><span class="approval-stats-row-label">命令执行</span><span class="approval-stats-row-count">' + stats.command + '</span></span>' : '') +
7680
+ (stats.file > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">📝</span><span class="approval-stats-row-label">文件写入</span><span class="approval-stats-row-count">' + stats.file + '</span></span>' : '') +
7681
+ (stats.tool > 0 ? '<span class="approval-stats-row"><span class="approval-stats-row-icon">🔧</span><span class="approval-stats-row-label">其他工具</span><span class="approval-stats-row-count">' + stats.tool + '</span></span>' : '') +
7682
+ '<span class="approval-stats-row approval-stats-row-total"><span class="approval-stats-row-icon">∑</span><span class="approval-stats-row-label">合计</span><span class="approval-stats-row-count">' + stats.total + '</span></span>' +
7683
+ '</span>';
7684
+ // Pulse animation on the badge
7685
+ var badge = container.querySelector(".approval-stats-badge");
7686
+ if (badge) {
7687
+ badge.classList.remove("approval-stats-pulse");
7688
+ void badge.offsetWidth;
7689
+ badge.classList.add("approval-stats-pulse");
7690
+ }
7691
+ }
7692
+
7131
7693
  function approvePermission() {
7132
7694
  if (!state.selectedId) return;
7133
7695
  var approveBtn = document.getElementById("approve-permission-btn");
@@ -7184,6 +7746,49 @@
7184
7746
  });
7185
7747
  }
7186
7748
 
7749
+ function toggleAutoApprove() {
7750
+ if (!state.selectedId) return;
7751
+ var toggle = document.getElementById("auto-approve-toggle");
7752
+ if (toggle) toggle.style.opacity = "0.5";
7753
+ fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/toggle-auto-approve", {
7754
+ method: "POST",
7755
+ credentials: "same-origin"
7756
+ })
7757
+ .then(function(res) { return res.json(); })
7758
+ .then(function(data) {
7759
+ if (data && data.error) {
7760
+ showToast(data.error, "error");
7761
+ return;
7762
+ }
7763
+ updateSessionSnapshot(data);
7764
+ updateAutoApproveIndicator();
7765
+ var enabled = data.autoApprovePermissions;
7766
+ showToast(enabled ? "自动批准已开启" : "自动批准已关闭", "info");
7767
+ })
7768
+ .catch(function(error) {
7769
+ showToast((error && error.message) || "无法切换自动批准。", "error");
7770
+ })
7771
+ .finally(function() {
7772
+ if (toggle) toggle.style.opacity = "";
7773
+ });
7774
+ }
7775
+
7776
+ function updateAutoApproveIndicator() {
7777
+ var toggle = document.getElementById("auto-approve-toggle");
7778
+ if (!toggle) return;
7779
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
7780
+ var enabled = selectedSession && selectedSession.autoApprovePermissions;
7781
+ if (enabled) {
7782
+ toggle.className = "auto-approve-indicator active";
7783
+ toggle.title = "自动批准已启用 — 点击关闭";
7784
+ toggle.textContent = "🛡 自动批准";
7785
+ } else {
7786
+ toggle.className = "auto-approve-indicator";
7787
+ toggle.title = "自动批准已关闭 — 点击开启";
7788
+ toggle.textContent = "🛡 手动";
7789
+ }
7790
+ }
7791
+
7187
7792
  function updateTerminalOutput(output, sessionId, mode) {
7188
7793
  if (!state.terminal) return false;
7189
7794
  return syncTerminalBuffer(sessionId || state.selectedId, output, { mode: mode || "append" });
@@ -7392,6 +7997,10 @@
7392
7997
  // Force full render if message count changed or explicitly requested
7393
7998
  var forceRender = forceFullRender || msgCount !== state.lastRenderedMsgCount;
7394
7999
  if (!forceRender && msgCount === state.lastRenderedMsgCount && outputHash === state.lastRenderedHash) {
8000
+ // Even if message content hasn't changed, update the status bar
8001
+ // (inFlight state may have changed without new message content)
8002
+ var chatMessages = chatOutput.querySelector(".chat-messages");
8003
+ if (chatMessages) renderStructuredStatusBar(chatMessages, selectedSession);
7395
8004
  return;
7396
8005
  }
7397
8006
  var prevHash = state.lastRenderedHash;
@@ -7545,6 +8154,9 @@
7545
8154
  fullRenderChat();
7546
8155
  }
7547
8156
 
8157
+ // Update structured session status bar (in-flight / completed indicator)
8158
+ renderStructuredStatusBar(chatMessages, selectedSession);
8159
+
7548
8160
  // Update todo progress bar from latest messages
7549
8161
  updateTodoProgress(messages);
7550
8162
  }
@@ -8267,11 +8879,53 @@
8267
8879
  '</div>';
8268
8880
  }
8269
8881
 
8882
+ function buildToolResultMap(contentBlocks) {
8883
+ var toolResults = {};
8884
+ if (!Array.isArray(contentBlocks)) return toolResults;
8885
+ for (var i = 0; i < contentBlocks.length; i++) {
8886
+ var block = contentBlocks[i];
8887
+ if (block && block.type === "tool_result") {
8888
+ var toolUseId = block.tool_use_id;
8889
+ if (!toolUseId) continue;
8890
+ if (!toolResults[toolUseId]) {
8891
+ toolResults[toolUseId] = [];
8892
+ }
8893
+ toolResults[toolUseId].push(block);
8894
+ }
8895
+ }
8896
+ return toolResults;
8897
+ }
8898
+
8899
+ function pickToolResultForDisplay(toolResults, toolUseId) {
8900
+ var entries = toolResults && toolUseId ? toolResults[toolUseId] : null;
8901
+ if (!entries || !entries.length) return null;
8902
+ for (var i = 0; i < entries.length - 1; i++) {
8903
+ if (isRecoverableToolError(entries[i], entries[i + 1])) {
8904
+ return entries[i + 1];
8905
+ }
8906
+ }
8907
+ return entries[entries.length - 1];
8908
+ }
8909
+
8910
+ function hasRecoveredToolNoise(toolResults, toolUseId) {
8911
+ var entries = toolResults && toolUseId ? toolResults[toolUseId] : null;
8912
+ if (!entries || entries.length < 2) return false;
8913
+ for (var i = 0; i < entries.length - 1; i++) {
8914
+ if (isRecoverableToolError(entries[i], entries[i + 1])) {
8915
+ return true;
8916
+ }
8917
+ }
8918
+ return false;
8919
+ }
8920
+
8921
+ function renderRecoveredToolHint(toolName) {
8922
+ return '<div class="structured-tool-hint">已自动恢复一次 ' + escapeHtml(getToolDisplayName(toolName)) + ' 参数问题</div>';
8923
+ }
8924
+
8270
8925
  function renderStructuredMessage(msg) {
8271
8926
  var role = msg.role;
8272
8927
  var avatar = role === "assistant" ? '<div class="chat-message-avatar">AI</div>' : "";
8273
8928
 
8274
- // Empty content array — streaming placeholder, show typing indicator
8275
8929
  if (!msg.content || msg.content.length === 0) {
8276
8930
  if (role === "assistant") {
8277
8931
  return '<div class="chat-message ' + role + '">' +
@@ -8282,18 +8936,7 @@
8282
8936
  return "";
8283
8937
  }
8284
8938
 
8285
- // 先建立 tool_use_id -> tool_result 的映射
8286
- var toolResults = {};
8287
- for (var i = 0; i < msg.content.length; i++) {
8288
- var block = msg.content[i];
8289
- if (block && block.type === "tool_result") {
8290
- var toolUseId = block.tool_use_id;
8291
- if (toolUseId) {
8292
- toolResults[toolUseId] = block;
8293
- }
8294
- }
8295
- }
8296
-
8939
+ var toolResults = buildToolResultMap(msg.content);
8297
8940
  var blocksHtml = "";
8298
8941
 
8299
8942
  try {
@@ -8302,19 +8945,16 @@
8302
8945
  try {
8303
8946
  blocksHtml += renderContentBlock(block, role, toolResults, i);
8304
8947
  } catch (e) {
8305
- // Render error for individual block
8306
8948
  blocksHtml += '<div class="render-error">消息块渲染失败</div>';
8307
8949
  }
8308
8950
  }
8309
8951
  } catch (e) {
8310
- // Render error for entire message
8311
8952
  return '<div class="chat-message ' + role + '">' +
8312
8953
  avatar +
8313
8954
  '<div class="chat-message-bubble"><div class="render-error">消息渲染失败</div></div>' +
8314
8955
  '</div>';
8315
8956
  }
8316
8957
 
8317
- // Build usage indicator for assistant messages
8318
8958
  var usageHtml = "";
8319
8959
  if (role === "assistant" && msg.usage) {
8320
8960
  var u = msg.usage;
@@ -8334,21 +8974,21 @@
8334
8974
  usageHtml +
8335
8975
  '</div>';
8336
8976
  }
8337
-
8338
8977
  function renderContentBlock(block, role, toolResults, index) {
8339
8978
  if (!block || !block.type) return "";
8340
8979
 
8341
8980
  switch (block.type) {
8342
8981
  case "text":
8982
+ if (role === "assistant" && block.__processing) {
8983
+ return '<div class="typing-indicator"><span></span><span></span><span></span></div>';
8984
+ }
8343
8985
  return role === "assistant" ? renderMarkdown(block.text || "") : escapeHtml(block.text || "");
8344
8986
 
8345
8987
  case "thinking":
8346
8988
  var thinkingText = block.thinking || "";
8347
- // Compact display: brain icon + brief text, click to expand
8348
8989
  var preview = thinkingText.length > 60 ? thinkingText.slice(0, 57) + "…" : thinkingText;
8349
8990
  var isStreaming = block.thinking === undefined && block.type === "thinking";
8350
8991
  if (isStreaming) {
8351
- // During streaming: show 3-line scrollable area
8352
8992
  return '<div class="thinking-inline thinking-streaming" data-thinking="">' +
8353
8993
  '<div class="thinking-streaming-inner">' +
8354
8994
  '<span class="thinking-streaming-icon spinning">🧠</span>' +
@@ -8363,11 +9003,14 @@
8363
9003
  '</div>';
8364
9004
 
8365
9005
  case "tool_use":
8366
- var toolResult = toolResults[block.id];
8367
- return renderToolUseCard(block, toolResult, index);
9006
+ var toolResult = pickToolResultForDisplay(toolResults, block.id);
9007
+ var rendered = renderToolUseCard(block, toolResult, index);
9008
+ if (hasRecoveredToolNoise(toolResults, block.id)) {
9009
+ rendered = renderRecoveredToolHint(block.name || "工具") + rendered;
9010
+ }
9011
+ return rendered;
8368
9012
 
8369
9013
  case "tool_result":
8370
- // tool_result 已经在 tool_use 渲染时处理了,不再单独渲染
8371
9014
  return "";
8372
9015
 
8373
9016
  default:
@@ -8375,11 +9018,11 @@
8375
9018
  }
8376
9019
  }
8377
9020
 
8378
- // Lightweight inline display — used for Read, Glob, Grep, WebFetch, WebSearch, TodoRead
8379
9021
  function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo) {
8380
9022
  var toolId = block.id || "tool-" + toolName;
8381
9023
  var inputData = block.input || {};
8382
- var resultContent = (toolResult && toolResult.content) ? toolResult.content.trim() : "";
9024
+ var resultContent = extractToolResultText(toolResult && toolResult.content);
9025
+
8383
9026
  var isError = toolResult && toolResult.is_error;
8384
9027
  var hasResult = resultContent.length > 0;
8385
9028
  var statusIcon = isError ? "⚠️" : (hasResult ? "✅" : "⏳");
@@ -8482,7 +9125,8 @@
8482
9125
  function renderTerminalTool(block, toolResult, toolName) {
8483
9126
  var inputData = block.input || {};
8484
9127
  var command = inputData.command || inputData.cmd || "";
8485
- var resultContent = (toolResult && toolResult.content) ? toolResult.content.trim() : "";
9128
+ var resultContent = extractToolResultText(toolResult && toolResult.content);
9129
+
8486
9130
  var isError = toolResult && toolResult.is_error;
8487
9131
  var exitCode = inputData.exitCode;
8488
9132
  var hasResult = resultContent.length > 0;
@@ -8699,7 +9343,7 @@
8699
9343
 
8700
9344
  if (toolResult) {
8701
9345
  var isError = toolResult.is_error;
8702
- var content = toolResult.content || "";
9346
+ var content = extractToolResultText(toolResult.content);
8703
9347
  statusClass = isError ? "error" : "success";
8704
9348
  headerIcon = getToolIcon(toolName);
8705
9349
  var hasContent = content && content.trim().length > 0;