@co0ontty/wand 1.3.6 → 1.5.1

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 {
@@ -276,7 +349,7 @@
276
349
  body: "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion,
277
350
  type: "info",
278
351
  icon: "\u2191",
279
- duration: 0,
352
+ duration: 10000,
280
353
  actionLabel: "\u53bb\u66f4\u65b0",
281
354
  action: function() {
282
355
  var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
@@ -319,7 +392,7 @@
319
392
  var app = document.getElementById("app");
320
393
  var isLoggedIn = state.config !== null;
321
394
  var wasModalOpen = state.modalOpen;
322
- var shouldResetShell = !isLoggedIn || !document.getElementById("output");
395
+ var shouldResetShell = !isLoggedIn || !!document.getElementById("output");
323
396
 
324
397
  if (shouldResetShell) {
325
398
  teardownTerminal();
@@ -338,6 +411,9 @@
338
411
  if (!skipShellChrome) {
339
412
  updateShellChrome();
340
413
  }
414
+ if (isLoggedIn && state.filePanelOpen) {
415
+ refreshFileExplorer();
416
+ }
341
417
 
342
418
  // Force reflow then re-enable transitions after layout settles
343
419
  void document.body.offsetHeight;
@@ -554,11 +630,14 @@
554
630
  '<div class="blank-chat-inner">' +
555
631
  '<div class="blank-chat-logo">W</div>' +
556
632
  '<h2 class="blank-chat-title">Wand</h2>' +
557
- '<p class="blank-chat-subtitle">当前仅保留原生终端模式,优先修复 PTY 交互与显示。</p>' +
633
+ '<p class="blank-chat-subtitle">支持终端 PTY 会话与结构化 chat 会话,两种模式可并存。</p>' +
558
634
  '<div class="blank-chat-tools">' +
559
635
  '<button class="blank-chat-tool-btn" id="welcome-tool-claude" type="button">' +
560
636
  '<span class="tool-icon">🤖</span>新建终端会话' +
561
637
  '</button>' +
638
+ '<button class="blank-chat-tool-btn" id="welcome-tool-structured" type="button">' +
639
+ '<span class="tool-icon">💬</span>新建结构化会话' +
640
+ '</button>' +
562
641
  '</div>' +
563
642
  '<div class="blank-chat-cwd-wrap">' +
564
643
  '<div class="blank-chat-cwd" id="blank-chat-cwd" role="button" tabindex="0" title="点击切换工作目录">' +
@@ -619,6 +698,9 @@
619
698
  '<span id="session-cwd-display" class="session-cwd-display">' + (selectedSession && selectedSession.cwd ? escapeHtml(selectedSession.cwd) : '未设置目录') + '</span>' +
620
699
  '<span class="session-info-separator">|</span>' +
621
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>' +
622
704
  '<span class="session-info-separator">|</span>' +
623
705
  '<span id="session-status-display" class="session-status-display">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '-') + '</span>' +
624
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>' : '') +
@@ -739,11 +821,21 @@
739
821
  '<label class="field-label" for="cfg-shell">Shell</label>' +
740
822
  '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
741
823
  '</div>' +
742
- '<div class="field field-inline">' +
743
- '<label class="field-label" for="cfg-dom-terminal">终端 DOM 渲染 <span style="font-size:0.7em;color:var(--warning);font-weight:600;">实验性</span></label>' +
744
- '<input id="cfg-dom-terminal" type="checkbox" class="field-checkbox" />' +
824
+ '<div class="field">' +
825
+ '<label class="field-label" for="cfg-language">回复语言</label>' +
826
+ '<select id="cfg-language" class="field-input">' +
827
+ '<option value="">自动(不指定)</option>' +
828
+ '<option value="中文">中文</option>' +
829
+ '<option value="English">English</option>' +
830
+ '<option value="日本語">日本語</option>' +
831
+ '<option value="한국어">한국어</option>' +
832
+ '<option value="Español">Español</option>' +
833
+ '<option value="Français">Français</option>' +
834
+ '<option value="Deutsch">Deutsch</option>' +
835
+ '<option value="Русский">Русский</option>' +
836
+ '</select>' +
837
+ '<p class="hint" style="margin-top:4px;margin-bottom:0;">设置后,Claude 将尽量使用指定语言回复。</p>' +
745
838
  '</div>' +
746
- '<p class="hint" style="margin-top:-8px;margin-bottom:8px;">移动端使用 DOM 渲染终端,支持原生文本选择与复制。保存后刷新页面生效。</p>' +
747
839
  '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
748
840
  '<p id="config-message" class="hint hidden"></p>' +
749
841
  '</div>' +
@@ -1256,13 +1348,21 @@
1256
1348
  explorer.innerHTML = '<div class="file-explorer empty">No working directory.</div>';
1257
1349
  return;
1258
1350
  }
1351
+ state.fileExplorerLoading = true;
1352
+ state.allFiles = [];
1259
1353
  explorer.innerHTML = '<div class="file-explorer"><div class="tree-loading" style="padding:12px;color:var(--text-muted);font-size:0.8125rem;">Loading...</div></div>';
1260
1354
  // Update the cwd display
1261
1355
  if (cwdEl) cwdEl.textContent = cwd;
1262
1356
  // Fetch with git status
1263
1357
  fetch("/api/directory?q=" + encodeURIComponent(cwd) + "&gitStatus=true", { credentials: "same-origin" })
1264
- .then(function(res) { return res.json(); })
1358
+ .then(function(res) {
1359
+ if (!res.ok) {
1360
+ throw new Error("Failed to load directory.");
1361
+ }
1362
+ return res.json();
1363
+ })
1265
1364
  .then(function(items) {
1365
+ state.fileExplorerLoading = false;
1266
1366
  if (!items || items.length === 0) {
1267
1367
  explorer.innerHTML = '<div class="file-explorer empty">Empty directory or inaccessible.</div>';
1268
1368
  return;
@@ -1271,6 +1371,7 @@
1271
1371
  filterFileTree();
1272
1372
  })
1273
1373
  .catch(function() {
1374
+ state.fileExplorerLoading = false;
1274
1375
  explorer.innerHTML = '<div class="file-explorer empty">Failed to load files.</div>';
1275
1376
  });
1276
1377
  }
@@ -1667,7 +1768,7 @@
1667
1768
  if (session.claudeSessionId) {
1668
1769
  var shortId = session.claudeSessionId.slice(0, 8);
1669
1770
  sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
1670
- if (session.status !== "running" && !state.sessionsManageMode) {
1771
+ if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
1671
1772
  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>';
1672
1773
  }
1673
1774
  }
@@ -1677,6 +1778,7 @@
1677
1778
  }
1678
1779
 
1679
1780
  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>';
1781
+ var modeBadge = renderSessionKindBadge(session);
1680
1782
  var actionsHtml = '<span class="session-actions">' + resumeButton + deleteButton + '</span>';
1681
1783
 
1682
1784
  return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
@@ -1686,6 +1788,7 @@
1686
1788
  '<div class="session-main">' +
1687
1789
  '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>' +
1688
1790
  '<div class="session-meta">' +
1791
+ modeBadge +
1689
1792
  '<span>' + escapeHtml(modeName) + '</span>' +
1690
1793
  '<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
1691
1794
  sessionIdDisplay +
@@ -1697,6 +1800,15 @@
1697
1800
  '</div>' +
1698
1801
  '</div>';
1699
1802
  }
1803
+
1804
+ function renderSessionKindBadge(session) {
1805
+ if (!session) return "";
1806
+ if (isStructuredSession(session)) {
1807
+ return '<span class="session-kind-badge structured">Structured</span>';
1808
+ }
1809
+ return '<span class="session-kind-badge pty">PTY</span>';
1810
+ }
1811
+
1700
1812
  function renderModeCards(selectedMode) {
1701
1813
  var modes = [
1702
1814
  { id: "managed", label: "托管", desc: "全自动完成任务" },
@@ -1714,19 +1826,48 @@
1714
1826
  }).join("");
1715
1827
  }
1716
1828
 
1829
+ function renderSessionKindOptions(selectedKind) {
1830
+ var kinds = [
1831
+ { id: "pty", label: "PTY", desc: "交互式终端会话" },
1832
+ { id: "structured", label: "Structured", desc: "单轮结构化输出" }
1833
+ ];
1834
+ return kinds.map(function(kind) {
1835
+ var active = kind.id === selectedKind ? " active" : "";
1836
+ return '<button type="button" class="mode-card session-kind-card' + active + '" data-session-kind="' + kind.id + '">' +
1837
+ '<span class="mode-card-label">' + kind.label + '</span>' +
1838
+ '<span class="mode-card-desc">' + kind.desc + '</span>' +
1839
+ '</button>';
1840
+ }).join("");
1841
+ }
1842
+
1843
+ function getSessionKindHint(kind) {
1844
+ if (kind === "structured") {
1845
+ return "直接使用 claude -p 获取结构化单轮结果。";
1846
+ }
1847
+ return "默认 PTY 会话,支持持续交互、终端视图和权限流。";
1848
+ }
1849
+
1717
1850
  function renderSessionModal() {
1718
1851
  var modalTool = getPreferredTool();
1719
1852
  var modalMode = getSafeModeForTool(modalTool, state.modeValue || state.chatMode || "default");
1853
+ var sessionKind = state.sessionCreateKind || "pty";
1720
1854
  return '<section id="session-modal" class="modal-backdrop hidden">' +
1721
1855
  '<div class="modal session-modal">' +
1722
1856
  '<div class="modal-header">' +
1723
1857
  '<div>' +
1724
1858
  '<h2 class="modal-title">新对话</h2>' +
1725
- '<p class="modal-subtitle">启动 Claude 会话,选择模式和工作目录。</p>' +
1859
+ '<p class="modal-subtitle">启动 Claude 会话,选择会话类型、模式和工作目录。</p>' +
1726
1860
  '</div>' +
1727
1861
  '<button id="close-modal-button" class="btn btn-ghost btn-icon">&times;</button>' +
1728
1862
  '</div>' +
1729
1863
  '<div class="modal-body">' +
1864
+ '<div class="field">' +
1865
+ '<label class="field-label">会话类型</label>' +
1866
+ '<div id="session-kind-cards" class="mode-cards">' +
1867
+ renderSessionKindOptions(sessionKind) +
1868
+ '</div>' +
1869
+ '<p id="session-kind-description" class="field-hint">' + escapeHtml(getSessionKindHint(sessionKind)) + '</p>' +
1870
+ '</div>' +
1730
1871
  '<div class="field">' +
1731
1872
  '<label class="field-label">模式</label>' +
1732
1873
  '<div id="mode-cards" class="mode-cards">' +
@@ -1785,21 +1926,23 @@
1785
1926
  var statusSpan = el.querySelector(".inline-tool-status");
1786
1927
  if (statusSpan) {
1787
1928
  if (el.dataset.status === "error") {
1788
- statusSpan.textContent = "⚠️";
1929
+ statusSpan.textContent = "";
1789
1930
  } else if (el.dataset.status === "done") {
1790
- statusSpan.textContent = expanded ? "" : "✅";
1931
+ statusSpan.textContent = "";
1791
1932
  }
1792
1933
  }
1793
1934
  };
1794
1935
  // Toggle function for terminal tool blocks
1795
1936
  window.__terminalExpand = function(el) {
1796
- var body = el.querySelector(".term-body");
1937
+ var container = el.closest(".inline-terminal");
1938
+ if (!container) return;
1939
+ var body = container.querySelector(".term-body");
1797
1940
  if (body) {
1798
1941
  var isHidden = body.style.display === "none";
1799
1942
  body.style.display = isHidden ? "block" : "none";
1800
- el.dataset.expanded = isHidden ? "true" : "false";
1943
+ container.dataset.expanded = isHidden ? "true" : "false";
1801
1944
  var toggleIcon = el.querySelector(".term-toggle-icon");
1802
- if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "";
1945
+ if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "";
1803
1946
  }
1804
1947
  };
1805
1948
  // Update streaming thinking content (called from WebSocket handler)
@@ -1899,6 +2042,16 @@
1899
2042
  quickStartSession();
1900
2043
  });
1901
2044
  }
2045
+ var welcomeStructuredBtn = document.getElementById("welcome-tool-structured");
2046
+ if (welcomeStructuredBtn) {
2047
+ welcomeStructuredBtn.addEventListener("click", function() {
2048
+ createStructuredSession().then(function() {
2049
+ focusInputBox(true);
2050
+ }).catch(function(error) {
2051
+ showToast((error && error.message) || "无法启动结构化会话。", "error");
2052
+ });
2053
+ });
2054
+ }
1902
2055
  initBlankChatCwd();
1903
2056
 
1904
2057
  var sessionsList = document.getElementById("sessions-list");
@@ -1911,6 +2064,17 @@
1911
2064
  // Claude session ID badge click-to-copy (event delegation on document)
1912
2065
  document.addEventListener("click", handleClaudeIdCopy);
1913
2066
 
2067
+ var kindCardsEl = document.getElementById("session-kind-cards");
2068
+ if (kindCardsEl) kindCardsEl.addEventListener("click", function(e) {
2069
+ var card = e.target.closest(".session-kind-card");
2070
+ if (!card) return;
2071
+ var kind = card.getAttribute("data-session-kind");
2072
+ if (kind) {
2073
+ state.sessionCreateKind = kind;
2074
+ syncSessionModalUI();
2075
+ }
2076
+ });
2077
+
1914
2078
  var modeCardsEl = document.getElementById("mode-cards");
1915
2079
  if (modeCardsEl) modeCardsEl.addEventListener("click", function(e) {
1916
2080
  var card = e.target.closest(".mode-card");
@@ -1941,7 +2105,7 @@
1941
2105
  persistSelectedId();
1942
2106
  resetChatRenderCache();
1943
2107
  closeSessionsDrawer();
1944
- renderApp();
2108
+ render();
1945
2109
  });
1946
2110
  var refreshBtn = document.getElementById("sidebar-refresh-btn");
1947
2111
  if (refreshBtn) refreshBtn.addEventListener("click", function() {
@@ -1996,6 +2160,8 @@
1996
2160
  if (approvePermissionBtn) approvePermissionBtn.addEventListener("click", approvePermission);
1997
2161
  var denyPermissionBtn = document.getElementById("deny-permission-btn");
1998
2162
  if (denyPermissionBtn) denyPermissionBtn.addEventListener("click", denyPermission);
2163
+ var autoApproveToggle = document.getElementById("auto-approve-toggle");
2164
+ if (autoApproveToggle) autoApproveToggle.addEventListener("click", toggleAutoApprove);
1999
2165
  var sendBtn = document.getElementById("send-input-button");
2000
2166
  if (sendBtn) sendBtn.addEventListener("click", function() {
2001
2167
  closeSessionsDrawer();
@@ -2256,7 +2422,7 @@
2256
2422
  state.selectedId = null;
2257
2423
  persistSelectedId();
2258
2424
  state.drafts = {};
2259
- renderApp();
2425
+ render();
2260
2426
  // 聚焦到目录输入框
2261
2427
  setTimeout(function() {
2262
2428
  var folderInput = document.getElementById("folder-picker-input");
@@ -2539,7 +2705,7 @@
2539
2705
 
2540
2706
  function activateSessionItem(sessionId) {
2541
2707
  var session = state.sessions.find(function(s) { return s.id === sessionId; });
2542
- if (session && session.status !== "running") {
2708
+ if (session && session.status !== "running" && !isStructuredSession(session)) {
2543
2709
  resumeSessionFromList(sessionId);
2544
2710
  } else {
2545
2711
  selectSession(sessionId);
@@ -3336,6 +3502,35 @@
3336
3502
  return hints[mode] || '';
3337
3503
  }
3338
3504
 
3505
+ function getSessionKindLabel(session) {
3506
+ return isStructuredSession(session) ? "Structured" : "PTY";
3507
+ }
3508
+
3509
+ function getSessionKindDescription(session) {
3510
+ return isStructuredSession(session)
3511
+ ? "Structured · block transcript"
3512
+ : "PTY · terminal session";
3513
+ }
3514
+
3515
+ function isRecoverableToolError(toolResult, nextResult) {
3516
+ if (!toolResult || !toolResult.is_error || !nextResult || nextResult.is_error) {
3517
+ return false;
3518
+ }
3519
+ var currentText = extractToolResultText(toolResult.content).toLowerCase();
3520
+ var nextText = extractToolResultText(nextResult.content).toLowerCase();
3521
+ if (!currentText) return false;
3522
+ if (currentText.indexOf("invalid pages parameter") !== -1 && nextText.length > 0) {
3523
+ return true;
3524
+ }
3525
+ return false;
3526
+ }
3527
+
3528
+ function isStructuredSession(session) {
3529
+ var result = !!session && (session.sessionKind === "structured" || session.runner === "claude-cli-print");
3530
+ if (session) console.log("[WAND] isStructuredSession id:", session.id, "sessionKind:", session.sessionKind, "runner:", session.runner, "=>", result);
3531
+ return result;
3532
+ }
3533
+
3339
3534
  function syncComposerModeSelect() {
3340
3535
  var select = document.getElementById("chat-mode-select");
3341
3536
  if (!select) return;
@@ -3346,29 +3541,89 @@
3346
3541
  if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
3347
3542
  }
3348
3543
 
3544
+ function createStructuredSession(prompt, cwdOverride, modeOverride) {
3545
+ var payload = {
3546
+ cwd: cwdOverride || getEffectiveCwd(),
3547
+ mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
3548
+ runner: state.structuredRunner || "claude-cli-print",
3549
+ prompt: prompt || undefined
3550
+ };
3551
+ console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
3552
+ return fetch("/api/structured-sessions", {
3553
+ method: "POST",
3554
+ headers: { "Content-Type": "application/json" },
3555
+ credentials: "same-origin",
3556
+ body: JSON.stringify(payload)
3557
+ })
3558
+ .then(function(res) {
3559
+ console.log("[WAND] createStructuredSession response status:", res.status);
3560
+ return res.json();
3561
+ })
3562
+ .then(function(data) {
3563
+ console.log("[WAND] createStructuredSession data:", JSON.stringify({ id: data.id, error: data.error, sessionKind: data.sessionKind, runner: data.runner, status: data.status }));
3564
+ if (data.error) {
3565
+ throw new Error(data.error);
3566
+ }
3567
+ state.selectedId = data.id;
3568
+ persistSelectedId();
3569
+ state.drafts[data.id] = "";
3570
+ resetChatRenderCache();
3571
+ updateSessionSnapshot(data);
3572
+ updateSessionsList();
3573
+ switchToSessionView(data.id);
3574
+ subscribeToSession(data.id);
3575
+ return loadOutput(data.id).then(function() { return data; });
3576
+ });
3577
+ }
3578
+
3349
3579
  function applyCurrentView() {
3350
3580
  var hasSession = !!state.selectedId;
3351
3581
  var terminalBtn = document.getElementById("view-terminal-btn");
3352
3582
  var terminalContainer = document.getElementById("output");
3353
3583
  var chatContainer = document.getElementById("chat-output");
3584
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
3585
+ var structured = isStructuredSession(selectedSession);
3586
+ var showTerminal = hasSession && !structured && state.currentView === "terminal";
3587
+ var showChat = hasSession && (structured || state.currentView !== "terminal");
3588
+ console.log("[WAND] applyCurrentView hasSession:", hasSession, "structured:", structured, "currentView:", state.currentView, "showTerminal:", showTerminal, "showChat:", showChat, "sessionKind:", selectedSession && selectedSession.sessionKind, "runner:", selectedSession && selectedSession.runner);
3589
+
3590
+ if (structured) {
3591
+ state.currentView = "chat";
3592
+ } else if (!hasSession) {
3593
+ state.currentView = "terminal";
3594
+ }
3354
3595
 
3355
- if (terminalBtn) terminalBtn.classList.add("active");
3356
- if (terminalContainer) terminalContainer.classList.toggle("active", hasSession);
3596
+ if (terminalBtn) {
3597
+ terminalBtn.classList.toggle("hidden", structured || !hasSession);
3598
+ terminalBtn.classList.toggle("active", showTerminal);
3599
+ }
3600
+ if (terminalContainer) {
3601
+ terminalContainer.classList.toggle("active", showTerminal);
3602
+ terminalContainer.classList.toggle("hidden", !showTerminal);
3603
+ }
3357
3604
  if (chatContainer) {
3358
- chatContainer.classList.remove("active");
3359
- chatContainer.classList.add("hidden");
3605
+ chatContainer.classList.toggle("active", showChat);
3606
+ chatContainer.classList.toggle("hidden", !showChat);
3360
3607
  }
3361
3608
  updateInteractiveControls();
3362
3609
  }
3363
3610
 
3364
3611
  function syncSessionModalUI() {
3365
3612
  var modeHint = document.getElementById("mode-description");
3613
+ var kindHint = document.getElementById("session-kind-description");
3366
3614
  var tool = "claude";
3615
+ var sessionKind = state.sessionCreateKind || "pty";
3367
3616
 
3368
3617
  state.sessionTool = tool;
3369
3618
  state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
3370
3619
 
3371
- // Update mode cards active state
3620
+ var kindCards = document.querySelectorAll("#session-kind-cards .session-kind-card");
3621
+ if (kindCards.length) {
3622
+ kindCards.forEach(function(card) {
3623
+ card.classList.toggle("active", card.getAttribute("data-session-kind") === sessionKind);
3624
+ });
3625
+ }
3626
+
3372
3627
  var modeCards = document.querySelectorAll("#mode-cards .mode-card");
3373
3628
  if (modeCards.length) {
3374
3629
  modeCards.forEach(function(card) {
@@ -3376,11 +3631,22 @@
3376
3631
  });
3377
3632
  }
3378
3633
 
3634
+ if (kindHint) kindHint.textContent = getSessionKindHint(sessionKind);
3379
3635
  if (modeHint) modeHint.textContent = getToolModeHint(tool, state.modeValue);
3380
3636
  }
3381
3637
 
3382
3638
  function updateSessionSnapshot(snapshot) {
3383
3639
  if (!snapshot || !snapshot.id) return;
3640
+ if (snapshot.id === state.selectedId || (snapshot.sessionKind === "structured") || snapshot.structuredState) {
3641
+ console.log("[WAND] updateSessionSnapshot", snapshot.id, JSON.stringify({
3642
+ status: snapshot.status,
3643
+ exitCode: snapshot.exitCode,
3644
+ sessionKind: snapshot.sessionKind,
3645
+ runner: snapshot.runner,
3646
+ inFlight: snapshot.structuredState && snapshot.structuredState.inFlight,
3647
+ msgCount: snapshot.messages && snapshot.messages.length
3648
+ }));
3649
+ }
3384
3650
  var updated = false;
3385
3651
  var prevSession = null;
3386
3652
  state.sessions = state.sessions.map(function(session) {
@@ -3410,14 +3676,34 @@
3410
3676
  var localOutput = localSession.output || "";
3411
3677
  var serverOutput = serverSession.output || "";
3412
3678
  var keepLocalOutput = localOutput.length > serverOutput.length;
3679
+ var localStructuredState = localSession.structuredState || null;
3680
+ var serverStructuredState = serverSession.structuredState || null;
3681
+ var localHasPendingAssistant = !!(localSession.messages && localSession.messages.length && (function() {
3682
+ var last = localSession.messages[localSession.messages.length - 1];
3683
+ return last && last.role === "assistant" && Array.isArray(last.content) && last.content.some(function(block) {
3684
+ return block && block.__processing;
3685
+ });
3686
+ })());
3687
+ var preserveLocalStructuredProgress = (localSession.sessionKind === "structured")
3688
+ && !!localStructuredState
3689
+ && localStructuredState.inFlight === true
3690
+ && (!serverStructuredState || serverStructuredState.inFlight !== true)
3691
+ && localHasPendingAssistant
3692
+ && !!localStructuredState.activeRequestId
3693
+ && (!serverStructuredState || !serverStructuredState.activeRequestId || serverStructuredState.activeRequestId === localStructuredState.activeRequestId);
3413
3694
 
3414
3695
  if (keepLocalOutput) {
3415
3696
  merged.output = localOutput;
3416
3697
  }
3417
3698
 
3699
+ if (preserveLocalStructuredProgress) {
3700
+ merged.status = localSession.status || merged.status;
3701
+ merged.structuredState = Object.assign({}, serverStructuredState || {}, localStructuredState, { inFlight: true });
3702
+ merged.messages = localSession.messages;
3703
+ }
3704
+
3418
3705
  if (localSession.id === state.selectedId) {
3419
3706
  if (localSession.permissionBlocked && serverSession.permissionBlocked === false) {
3420
- // server explicitly resolved it; keep resolved state
3421
3707
  } else if (localSession.permissionBlocked && !serverSession.permissionBlocked) {
3422
3708
  merged.permissionBlocked = true;
3423
3709
  }
@@ -3452,15 +3738,13 @@
3452
3738
 
3453
3739
  function getPreferredSessionId(sessions) {
3454
3740
  if (!sessions || !sessions.length) return null;
3455
- // Keep currently selected session as long as it still exists
3456
3741
  if (state.selectedId) {
3457
3742
  var stillExists = sessions.find(function(session) { return session.id === state.selectedId; });
3458
3743
  if (stillExists) return stillExists.id;
3744
+ return null;
3459
3745
  }
3460
- // No selection — pick a running session, or fall back to most recent
3461
3746
  var runningSession = sessions.find(function(session) { return session.status === "running"; });
3462
3747
  if (runningSession) return runningSession.id;
3463
- // Fall back to most recent non-archived session (sessions are sorted newest first)
3464
3748
  var recent = sessions.find(function(session) { return !session.archived; });
3465
3749
  return recent ? recent.id : sessions[0].id;
3466
3750
  }
@@ -3487,7 +3771,10 @@
3487
3771
  return mergeServerSession(localSession, serverSession);
3488
3772
  });
3489
3773
 
3490
- state.selectedId = getPreferredSessionId(state.sessions);
3774
+ var preferredSessionId = getPreferredSessionId(state.sessions);
3775
+ if (preferredSessionId !== undefined) {
3776
+ state.selectedId = preferredSessionId;
3777
+ }
3491
3778
  persistSelectedId();
3492
3779
  if (state.modalOpen) {
3493
3780
  updateSessionsList();
@@ -3504,13 +3791,22 @@
3504
3791
  }
3505
3792
  }
3506
3793
  updateShellChrome();
3794
+
3795
+ // For structured sessions, loadOutput is needed to fetch messages
3796
+ // (the sessions list endpoint doesn't include them).
3797
+ // On page refresh this is the only place that can trigger it.
3798
+ if (state.selectedId) {
3799
+ var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
3800
+ if (isStructuredSession(sel)) {
3801
+ loadOutput(state.selectedId);
3802
+ }
3803
+ }
3507
3804
  })
3508
3805
  .catch(function(e) {
3509
3806
  console.error("[wand] loadSessions failed:", e);
3510
3807
  });
3511
3808
  }
3512
3809
 
3513
-
3514
3810
  function updateSessionsList() {
3515
3811
  var listEl = document.getElementById("sessions-list");
3516
3812
  var countEl = document.getElementById("session-count");
@@ -3538,22 +3834,23 @@
3538
3834
 
3539
3835
  if (summaryEl && summaryEl.textContent !== terminalTitle) summaryEl.textContent = terminalTitle;
3540
3836
  if (titleEl && titleEl.textContent !== terminalTitle) titleEl.textContent = terminalTitle;
3541
- if (infoEl && infoEl.textContent !== terminalInfo) {
3542
- infoEl.textContent = terminalInfo;
3543
- }
3837
+ if (infoEl) infoEl.textContent = selectedSession ? (terminalInfo + " · " + getSessionKindDescription(selectedSession)) : terminalInfo;
3544
3838
 
3545
- // Update session info bar at bottom
3546
3839
  var cwdEl = document.getElementById("session-cwd-display");
3547
3840
  var modeEl = document.getElementById("session-mode-display");
3841
+ var kindEl = document.getElementById("session-kind-display");
3548
3842
  var statusEl = document.getElementById("session-status-display");
3549
3843
  var exitEl = document.getElementById("session-exit-display");
3550
3844
  var cwdText = selectedSession && selectedSession.cwd ? selectedSession.cwd : "未设置目录";
3551
3845
  var modeText = selectedSession ? getModeLabel(selectedSession.mode) : "默认";
3846
+ var kindText = selectedSession ? getSessionKindLabel(selectedSession) : "PTY";
3552
3847
  var exitText = "exit=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
3553
3848
  if (cwdEl && cwdEl.textContent !== cwdText) cwdEl.textContent = cwdText;
3554
3849
  if (modeEl && modeEl.textContent !== modeText) modeEl.textContent = modeText;
3850
+ if (kindEl && kindEl.textContent !== kindText) kindEl.textContent = kindText;
3555
3851
  if (statusEl && statusEl.textContent !== terminalInfo) statusEl.textContent = terminalInfo;
3556
3852
  if (exitEl && exitEl.textContent !== exitText) exitEl.textContent = exitText;
3853
+ updateAutoApproveIndicator();
3557
3854
 
3558
3855
  if (!state.terminal && terminalContainer && selectedSession) {
3559
3856
  initTerminal();
@@ -3601,7 +3898,12 @@
3601
3898
  clearTimeout(chatRenderTimer);
3602
3899
  chatRenderTimer = null;
3603
3900
  }
3604
- return fetch("/api/sessions/" + id, { credentials: "same-origin" })
3901
+ var sess = state.sessions.find(function(s) { return s.id === id; });
3902
+ var url = "/api/sessions/" + id;
3903
+ if (isStructuredSession(sess)) {
3904
+ url += "?format=chat";
3905
+ }
3906
+ return fetch(url, { credentials: "same-origin" })
3605
3907
  .then(function(res) { return res.json(); })
3606
3908
  .then(function(data) {
3607
3909
  if (data.error) {
@@ -3628,6 +3930,8 @@
3628
3930
  }
3629
3931
 
3630
3932
  function selectSession(id) {
3933
+ var foundSession = state.sessions.find(function(item) { return item.id === id; });
3934
+ console.log("[WAND] selectSession id:", id, "found:", !!foundSession, "sessionKind:", foundSession && foundSession.sessionKind, "runner:", foundSession && foundSession.runner, "isStructured:", isStructuredSession(foundSession));
3631
3935
  state.selectedId = id;
3632
3936
  persistSelectedId();
3633
3937
  resetChatRenderCache();
@@ -3703,6 +4007,7 @@
3703
4007
  modal.classList.remove("hidden");
3704
4008
  lastFocusedElement = document.activeElement;
3705
4009
  state.sessionTool = getPreferredTool();
4010
+ state.sessionCreateKind = "pty";
3706
4011
  state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
3707
4012
  syncSessionModalUI();
3708
4013
  loadRecentPathBubbles();
@@ -3903,8 +4208,8 @@
3903
4208
  if (modeEl) modeEl.value = cfg.defaultMode || "default";
3904
4209
  if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
3905
4210
  if (shellEl) shellEl.value = cfg.shell || "";
3906
- var domTermEl = document.getElementById("cfg-dom-terminal");
3907
- if (domTermEl) domTermEl.checked = cfg.experimentalDomTerminal === true;
4211
+ var langEl = document.getElementById("cfg-language");
4212
+ if (langEl) langEl.value = cfg.language || "";
3908
4213
 
3909
4214
  // Cert status
3910
4215
  var certStatus = document.getElementById("cert-status");
@@ -3942,7 +4247,7 @@
3942
4247
  defaultMode: (document.getElementById("cfg-mode") || {}).value,
3943
4248
  defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
3944
4249
  shell: (document.getElementById("cfg-shell") || {}).value,
3945
- experimentalDomTerminal: (document.getElementById("cfg-dom-terminal") || {}).checked,
4250
+ language: (document.getElementById("cfg-language") || {}).value || "",
3946
4251
  };
3947
4252
 
3948
4253
  fetch("/api/settings/config", {
@@ -4230,13 +4535,45 @@
4230
4535
  var cwdEl = document.getElementById("cwd");
4231
4536
  var errorEl = document.getElementById("modal-error");
4232
4537
  var command = getPreferredTool();
4538
+ var sessionKind = state.sessionCreateKind || "pty";
4233
4539
 
4234
4540
  hideError(errorEl);
4235
4541
 
4236
4542
  var defaultCwd = getEffectiveCwd();
4543
+ var cwd = cwdEl.value.trim() || defaultCwd;
4237
4544
  var selectedMode = getSafeModeForTool(command, state.modeValue);
4238
- state.modeValue = selectedMode;
4239
- state.chatMode = selectedMode;
4545
+
4546
+ if (sessionKind === "structured") {
4547
+ startStructuredSessionFromModal(cwd, selectedMode, errorEl);
4548
+ return;
4549
+ }
4550
+
4551
+ runPtyCommandFromModal(command, cwd, selectedMode, errorEl);
4552
+ }
4553
+
4554
+ function startStructuredSessionFromModal(cwd, mode, errorEl) {
4555
+ console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode);
4556
+ state.modeValue = mode;
4557
+ state.chatMode = mode;
4558
+ state.sessionTool = "claude";
4559
+ state.preferredCommand = "claude";
4560
+ syncComposerModeSelect();
4561
+ return createStructuredSession(undefined, cwd, mode)
4562
+ .then(function(data) {
4563
+ closeSessionModal();
4564
+ closeSessionsDrawer();
4565
+ return data;
4566
+ })
4567
+ .then(function() { focusInputBox(true); })
4568
+ .catch(function(error) {
4569
+ showError(errorEl, (error && error.message) || "无法启动结构化会话,请确认 Claude 已正确安装。");
4570
+ });
4571
+ }
4572
+
4573
+ function runPtyCommandFromModal(command, cwd, mode, errorEl) {
4574
+ console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode);
4575
+ state.modeValue = mode;
4576
+ state.chatMode = mode;
4240
4577
  state.sessionTool = command;
4241
4578
  state.preferredCommand = command;
4242
4579
  syncComposerModeSelect();
@@ -4247,8 +4584,8 @@
4247
4584
  credentials: "same-origin",
4248
4585
  body: JSON.stringify({
4249
4586
  command: command,
4250
- cwd: cwdEl.value.trim() || defaultCwd,
4251
- mode: selectedMode
4587
+ cwd: cwd,
4588
+ mode: mode
4252
4589
  })
4253
4590
  })
4254
4591
  .then(function(res) { return res.json(); })
@@ -4258,6 +4595,7 @@
4258
4595
  return;
4259
4596
  }
4260
4597
  state.selectedId = data.id;
4598
+ console.log("[WAND] runPtyCommandFromModal created session:", data.id, "sessionKind:", data.sessionKind, "runner:", data.runner);
4261
4599
  persistSelectedId();
4262
4600
  state.drafts[data.id] = "";
4263
4601
  resetChatRenderCache();
@@ -4265,13 +4603,19 @@
4265
4603
  closeSessionsDrawer();
4266
4604
  return refreshAll();
4267
4605
  })
4268
- .then(function() { focusInputBox(true); })
4606
+ .then(function() {
4607
+ if (state.selectedId) {
4608
+ console.log("[WAND] runPtyCommandFromModal calling selectSession:", state.selectedId);
4609
+ selectSession(state.selectedId);
4610
+ } else {
4611
+ focusInputBox(true);
4612
+ }
4613
+ })
4269
4614
  .catch(function() {
4270
4615
  showError(errorEl, "无法启动会话,请确认 Claude 已正确安装。");
4271
4616
  });
4272
4617
  }
4273
4618
 
4274
- // Blank-chat CWD inline display + dropdown
4275
4619
  function initBlankChatCwd() {
4276
4620
  var cwdEl = document.getElementById("blank-chat-cwd");
4277
4621
  if (!cwdEl) return;
@@ -4691,11 +5035,13 @@
4691
5035
  persistSelectedId();
4692
5036
  state.drafts[data.id] = "";
4693
5037
  resetChatRenderCache();
4694
- switchToSessionView(data.id);
4695
5038
  updateSessionSnapshot(data);
4696
5039
  updateSessionsList();
5040
+ switchToSessionView(data.id);
4697
5041
  subscribeToSession(data.id);
4698
5042
  loadOutput(data.id).then(function() {
5043
+ welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
5044
+ welcomeInput.disabled = false;
4699
5045
  focusInputBox(true);
4700
5046
  });
4701
5047
  })
@@ -4749,9 +5095,9 @@
4749
5095
  resetChatRenderCache();
4750
5096
  if (inputBox) inputBox.value = "";
4751
5097
  if (welcomeInput) welcomeInput.value = "";
4752
- switchToSessionView(data.id);
4753
5098
  updateSessionSnapshot(data);
4754
5099
  updateSessionsList();
5100
+ switchToSessionView(data.id);
4755
5101
  // Subscribe to new session via WebSocket
4756
5102
  subscribeToSession(data.id);
4757
5103
  return loadOutput(data.id);
@@ -4763,6 +5109,7 @@
4763
5109
 
4764
5110
  function switchToSessionView(sessionId) {
4765
5111
  var session = state.sessions.find(function(s) { return s.id === sessionId; });
5112
+ console.log("[WAND] switchToSessionView id:", sessionId, "found:", !!session, "sessionKind:", session && session.sessionKind, "runner:", session && session.runner, "isStructured:", isStructuredSession(session), "currentView:", state.currentView);
4766
5113
  var blankChat = document.getElementById("blank-chat");
4767
5114
  var terminalContainer = document.getElementById("output");
4768
5115
  var chatContainer = document.getElementById("chat-output");
@@ -4770,28 +5117,36 @@
4770
5117
  var terminalTitle = document.getElementById("terminal-title");
4771
5118
  var terminalInfo = document.getElementById("terminal-info");
4772
5119
  var sessionSummary = document.querySelector(".session-summary-value");
5120
+ var structured = isStructuredSession(session);
4773
5121
 
4774
5122
  if (blankChat) blankChat.classList.add("hidden");
4775
- if (terminalContainer) terminalContainer.classList.remove("hidden");
5123
+ if (terminalContainer) {
5124
+ terminalContainer.classList.toggle("hidden", structured);
5125
+ }
4776
5126
  if (chatContainer) {
4777
5127
  chatContainer.classList.remove("hidden");
4778
5128
  }
4779
5129
  if (stopBtn) stopBtn.classList.remove("hidden");
4780
5130
 
5131
+ if (structured) {
5132
+ state.currentView = "chat";
5133
+ } else {
5134
+ state.currentView = "terminal";
5135
+ }
5136
+
4781
5137
  var title = session ? shortCommand(session.command) : "Wand";
4782
5138
  var info = session ? getSessionStatusLabel(session) : "开始对话";
4783
5139
  if (terminalTitle) terminalTitle.textContent = title;
4784
5140
  if (terminalInfo) terminalInfo.textContent = info;
4785
5141
  if (sessionSummary) sessionSummary.textContent = title;
4786
5142
 
4787
- // Init terminal if not already done
4788
- if (!state.terminal) initTerminal();
4789
- applyCurrentView();
4790
- if (state.terminal && state.fitAddon) {
4791
- ensureTerminalFit();
5143
+ if (!structured) {
5144
+ if (!state.terminal) initTerminal();
5145
+ if (state.terminal && state.fitAddon) {
5146
+ ensureTerminalFit();
5147
+ }
4792
5148
  }
4793
- // Don't call renderChat() here — loadOutput() always calls renderChat() after it resolves.
4794
- // Calling renderChat() prematurely would render with stale/empty messages.
5149
+ applyCurrentView();
4795
5150
  focusInputBox();
4796
5151
  }
4797
5152
 
@@ -4806,9 +5161,12 @@
4806
5161
  var value = inputBox ? inputBox.value : "";
4807
5162
  var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; }) || null;
4808
5163
  if (value) {
4809
- console.log("[wand] sendInputFromBox", {
5164
+ console.log("[WAND] sendInputFromBox", {
4810
5165
  sessionId: state.selectedId,
4811
5166
  sessionStatus: selectedSession ? selectedSession.status : null,
5167
+ sessionKind: selectedSession ? selectedSession.sessionKind : null,
5168
+ runner: selectedSession ? selectedSession.runner : null,
5169
+ isStructured: isStructuredSession(selectedSession),
4812
5170
  view: state.currentView,
4813
5171
  wsConnected: state.wsConnected,
4814
5172
  terminalInteractive: state.terminalInteractive,
@@ -4817,6 +5175,11 @@
4817
5175
  // Clear todo progress bar at the start of a new user turn
4818
5176
  var todoEl = document.getElementById("todo-progress");
4819
5177
  if (todoEl) todoEl.classList.add("hidden");
5178
+
5179
+ if (isStructuredSession(selectedSession)) {
5180
+ return postStructuredInput(value, inputBox, selectedSession);
5181
+ }
5182
+
4820
5183
  // Send text + Enter as a single call to avoid race conditions
4821
5184
  var combinedInput = value + getControlInput("enter");
4822
5185
  var isOffline = !state.wsConnected;
@@ -4857,6 +5220,79 @@
4857
5220
  return Promise.resolve();
4858
5221
  }
4859
5222
 
5223
+ function postStructuredInput(input, inputBox, session) {
5224
+ 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 });
5225
+ if (!state.selectedId || !input) return Promise.resolve();
5226
+ if (!session) {
5227
+ showToast("会话不存在,请重新选择或新建会话。", "error");
5228
+ return Promise.resolve();
5229
+ }
5230
+ if (session.structuredState && session.structuredState.inFlight && session.status === "running") {
5231
+ // Disable send button while processing, show subtle indicator
5232
+ var sendBtn = document.getElementById("send-input-button");
5233
+ if (sendBtn) sendBtn.disabled = true;
5234
+ showToast("正在等待上一条消息处理完成…", "info");
5235
+ return Promise.resolve();
5236
+ }
5237
+
5238
+ // Immediately render user message with thinking indicator
5239
+ var userTurn = { role: "user", content: [{ type: "text", text: input }] };
5240
+ var thinkingTurn = { role: "assistant", content: [{ type: "text", text: "", __processing: true }] };
5241
+ var userMsgs = Array.isArray(session.messages) ? session.messages.slice() : [];
5242
+ userMsgs.push(userTurn);
5243
+ userMsgs.push(thinkingTurn);
5244
+ session.messages = userMsgs;
5245
+ state.currentMessages = userMsgs;
5246
+ // Mark inFlight optimistically to prevent double-send via WS updates
5247
+ if (session.structuredState) {
5248
+ session.structuredState.inFlight = true;
5249
+ }
5250
+ session.status = "running";
5251
+ if (inputBox) {
5252
+ inputBox.value = "";
5253
+ autoResizeInput(inputBox);
5254
+ }
5255
+ // Disable send button so user can't double-send
5256
+ var sendBtnEl = document.getElementById("send-input-button");
5257
+ if (sendBtnEl) sendBtnEl.disabled = true;
5258
+ updateInputHint("思考中…");
5259
+ setDraftValue("");
5260
+ renderChat(true);
5261
+
5262
+ return fetch("/api/structured-sessions/" + state.selectedId + "/messages", {
5263
+ method: "POST",
5264
+ headers: { "Content-Type": "application/json" },
5265
+ credentials: "same-origin",
5266
+ body: JSON.stringify({ input: input })
5267
+ })
5268
+ .then(function(res) { return res.json(); })
5269
+ .then(function(snapshot) {
5270
+ if (snapshot && snapshot.error) {
5271
+ throw new Error(snapshot.error);
5272
+ }
5273
+ if (snapshot && snapshot.id) {
5274
+ updateSessionSnapshot(snapshot);
5275
+ if (snapshot.messages && snapshot.messages.length > 0) {
5276
+ state.currentMessages = snapshot.messages;
5277
+ }
5278
+ renderChat(true);
5279
+ updateInputHint("Enter 发送 · Shift+Enter 换行");
5280
+ }
5281
+ })
5282
+ .catch(function(error) {
5283
+ var errSendBtn = document.getElementById("send-input-button");
5284
+ if (errSendBtn) errSendBtn.disabled = false;
5285
+ updateInputHint("Enter 发送 · Shift+Enter 换行");
5286
+ showToast((error && error.message) || "无法发送结构化消息。", "error");
5287
+ throw error;
5288
+ });
5289
+ }
5290
+
5291
+ function updateInputHint(text) {
5292
+ var hint = document.querySelector(".input-hint");
5293
+ if (hint) hint.textContent = text;
5294
+ }
5295
+
4860
5296
  function getInputErrorMessage(error) {
4861
5297
  if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
4862
5298
  return "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
@@ -4908,6 +5344,7 @@
4908
5344
  }
4909
5345
 
4910
5346
  function ensureSessionReadyForInput(session, errorEl) {
5347
+ console.log("[WAND] ensureSessionReadyForInput session:", session && { id: session.id, status: session.status, claudeSessionId: session.claudeSessionId, sessionKind: session.sessionKind, runner: session.runner });
4911
5348
  if (!session) {
4912
5349
  showToast("会话不存在,请重新选择或新建会话。", "error");
4913
5350
  return Promise.resolve(null);
@@ -5203,21 +5640,24 @@
5203
5640
  }
5204
5641
 
5205
5642
  function updateInteractiveControls() {
5643
+ var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
5644
+ var structured = isStructuredSession(selectedSession);
5206
5645
  // Update both toggle buttons (topbar and terminal-header)
5207
5646
  var toggles = ["terminal-interactive-toggle-top"];
5208
5647
  toggles.forEach(function(id) {
5209
5648
  var toggle = document.getElementById(id);
5210
5649
  if (toggle) {
5211
5650
  toggle.classList.toggle("active", state.terminalInteractive);
5651
+ toggle.classList.toggle("hidden", structured || state.currentView !== "terminal" || !selectedSession);
5212
5652
  }
5213
5653
  });
5214
5654
  // Inline keyboard visibility follows current view
5215
5655
  var inlineKeyboard = document.getElementById("inline-keyboard");
5216
- if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", state.currentView !== "terminal");
5656
+ if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", structured || state.currentView !== "terminal");
5217
5657
  var inputHint = document.querySelector(".input-hint");
5218
- if (inputHint) inputHint.classList.toggle("hidden", state.currentView === "terminal");
5658
+ if (inputHint) inputHint.classList.toggle("hidden", structured ? false : state.currentView === "terminal");
5219
5659
  var container = document.getElementById("output");
5220
- if (container) container.classList.toggle("interactive", state.terminalInteractive);
5660
+ if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
5221
5661
  }
5222
5662
 
5223
5663
  function captureTerminalInput(event) {
@@ -5573,6 +6013,7 @@
5573
6013
  }
5574
6014
 
5575
6015
  function resumeSession(sessionId, errorEl) {
6016
+ console.log("[WAND] resumeSession sessionId:", sessionId);
5576
6017
  if (!sessionId) return Promise.resolve(null);
5577
6018
  return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/resume", {
5578
6019
  method: "POST",
@@ -5645,6 +6086,7 @@
5645
6086
  }
5646
6087
 
5647
6088
  function resumeSessionFromList(sessionId) {
6089
+ console.log("[WAND] resumeSessionFromList sessionId:", sessionId);
5648
6090
  return resumeSession(sessionId).then(function(data) {
5649
6091
  if (!data) return null;
5650
6092
  return activateSession(data).then(function() {
@@ -5733,6 +6175,7 @@
5733
6175
  }
5734
6176
 
5735
6177
  function handleResumeAction(actionButton) {
6178
+ console.log("[WAND] handleResumeAction sessionId:", actionButton.dataset.sessionId);
5736
6179
  actionButton.disabled = true;
5737
6180
  resumeSessionFromList(actionButton.dataset.sessionId)
5738
6181
  .finally(function() {
@@ -5743,6 +6186,7 @@
5743
6186
  function handleResumeHistoryAction(actionButton) {
5744
6187
  var claudeSessionId = actionButton.dataset.claudeSessionId;
5745
6188
  var cwd = actionButton.dataset.cwd;
6189
+ console.log("[WAND] handleResumeHistoryAction claudeSessionId:", claudeSessionId, "cwd:", cwd);
5746
6190
  if (!claudeSessionId) return;
5747
6191
  actionButton.disabled = true;
5748
6192
  resumeClaudeHistorySession(claudeSessionId, cwd)
@@ -6788,15 +7232,18 @@
6788
7232
  state.fitAddon.fit();
6789
7233
  maybeScrollTerminalToBottom("resize");
6790
7234
  if (state.selectedId && state.terminal) {
6791
- var nextSize = { cols: state.terminal.cols, rows: state.terminal.rows };
6792
- if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
6793
- state.lastResize = nextSize;
6794
- fetch("/api/sessions/" + state.selectedId + "/resize", {
6795
- method: "POST",
6796
- headers: { "Content-Type": "application/json" },
6797
- credentials: "same-origin",
6798
- body: JSON.stringify(nextSize)
6799
- }).catch(function() {});
7235
+ var selectedSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
7236
+ if (!isStructuredSession(selectedSess)) {
7237
+ var nextSize = { cols: state.terminal.cols, rows: state.terminal.rows };
7238
+ if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
7239
+ state.lastResize = nextSize;
7240
+ fetch("/api/sessions/" + state.selectedId + "/resize", {
7241
+ method: "POST",
7242
+ headers: { "Content-Type": "application/json" },
7243
+ credentials: "same-origin",
7244
+ body: JSON.stringify(nextSize)
7245
+ }).catch(function() {});
7246
+ }
6800
7247
  }
6801
7248
  }
6802
7249
  } else if (attempt < maxAttempts) {
@@ -6836,6 +7283,10 @@
6836
7283
 
6837
7284
  if (!state.selectedId) return;
6838
7285
 
7286
+ // Skip resize for structured sessions (no PTY)
7287
+ var resizeSess = state.sessions.find(function(s) { return s.id === state.selectedId; });
7288
+ if (isStructuredSession(resizeSess)) return;
7289
+
6839
7290
  // Only send resize API call if dimensions actually changed
6840
7291
  if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
6841
7292
  state.lastResize = nextSize;
@@ -6918,7 +7369,8 @@
6918
7369
  switch (msg.type) {
6919
7370
  case 'output':
6920
7371
  // Update session output (for terminal display and local message parsing)
6921
- if (msg.data && msg.data.output && msg.sessionId) {
7372
+ // NOTE: For structured sessions, output may be "" during streaming — check messages too
7373
+ if (msg.data && (msg.data.output || msg.data.messages) && msg.sessionId) {
6922
7374
  var snapshot = { id: msg.sessionId, output: msg.data.output };
6923
7375
  if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
6924
7376
  snapshot.permissionBlocked = !!msg.data.permissionBlocked;
@@ -6930,8 +7382,24 @@
6930
7382
  updateSessionSnapshot(snapshot);
6931
7383
  if (msg.sessionId === state.selectedId) {
6932
7384
  state.currentMessages = getPreferredMessages(snapshot, msg.data.output, false);
7385
+ // Structured session with inFlight: keep __processing placeholder
7386
+ // so the loading indicator stays visible until assistant content arrives
7387
+ if (msg.data.sessionKind === 'structured') {
7388
+ var outSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
7389
+ if (outSession && outSession.structuredState && outSession.structuredState.inFlight) {
7390
+ var lastCur = state.currentMessages[state.currentMessages.length - 1];
7391
+ if (!lastCur || lastCur.role !== 'assistant') {
7392
+ state.currentMessages.push({ role: "assistant", content: [{ type: "text", text: "", __processing: true }] });
7393
+ }
7394
+ }
7395
+ }
6933
7396
  updateTaskDisplay();
6934
- scheduleChatRender();
7397
+ // Structured sessions: render immediately for responsiveness
7398
+ if (msg.data.sessionKind === 'structured') {
7399
+ renderChat();
7400
+ } else {
7401
+ scheduleChatRender();
7402
+ }
6935
7403
  }
6936
7404
 
6937
7405
  }
@@ -6968,8 +7436,19 @@
6968
7436
  if (msg.data && msg.data.messages) {
6969
7437
  endedSnapshot.messages = msg.data.messages;
6970
7438
  }
7439
+ if (msg.data && msg.data.structuredState) {
7440
+ endedSnapshot.structuredState = msg.data.structuredState;
7441
+ }
6971
7442
  updateSessionSnapshot(endedSnapshot);
6972
7443
 
7444
+ // Re-enable send button when structured session finishes
7445
+ if (msg.sessionId === state.selectedId) {
7446
+ var endedSendBtn = document.getElementById("send-input-button");
7447
+ if (endedSendBtn) endedSendBtn.disabled = false;
7448
+ updateInputHint("Enter 发送 · Shift+Enter 换行");
7449
+ // Trigger status bar completion animation
7450
+ scheduleChatRender(true);
7451
+ }
6973
7452
  // Notify user when a session completes (browser + in-app if backgrounded or not viewing)
6974
7453
  var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
6975
7454
  var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
@@ -7044,7 +7523,24 @@
7044
7523
  break;
7045
7524
  case 'status':
7046
7525
  if (msg.sessionId && msg.data) {
7526
+ console.log('[WAND] ws status', msg.sessionId, JSON.stringify(msg.data));
7047
7527
  var statusUpdate = { id: msg.sessionId };
7528
+ if (Object.prototype.hasOwnProperty.call(msg.data, 'status')) {
7529
+ statusUpdate.status = msg.data.status;
7530
+ }
7531
+ if (Object.prototype.hasOwnProperty.call(msg.data, 'exitCode')) {
7532
+ statusUpdate.exitCode = msg.data.exitCode;
7533
+ }
7534
+ if (msg.data.structuredState) {
7535
+ statusUpdate.structuredState = msg.data.structuredState;
7536
+ } else if (Object.prototype.hasOwnProperty.call(msg.data, 'status')) {
7537
+ var existingSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
7538
+ if (existingSession && existingSession.sessionKind === 'structured') {
7539
+ statusUpdate.structuredState = Object.assign({}, existingSession.structuredState || {}, {
7540
+ inFlight: msg.data.status === 'running'
7541
+ });
7542
+ }
7543
+ }
7048
7544
  if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
7049
7545
  statusUpdate.permissionBlocked = !!msg.data.permissionBlocked;
7050
7546
  }
@@ -7096,6 +7592,10 @@
7096
7592
  if (msg.data.approvalStats) {
7097
7593
  updateApprovalStats();
7098
7594
  }
7595
+ // Re-render chat when structured session inFlight state changes
7596
+ if (statusUpdate.structuredState) {
7597
+ scheduleChatRender();
7598
+ }
7099
7599
  }
7100
7600
  }
7101
7601
  break;
@@ -7135,9 +7635,12 @@
7135
7635
  var isBlocked = pendingEscalation || (selectedSession && selectedSession.permissionBlocked);
7136
7636
 
7137
7637
  if (isBlocked) {
7638
+ var isAutoApprove = selectedSession && selectedSession.autoApprovePermissions;
7138
7639
  // Show permission label in input composer area
7139
7640
  if (permissionLabel) {
7140
- if (pendingEscalation) {
7641
+ if (isAutoApprove) {
7642
+ permissionLabel.textContent = "自动批准中...";
7643
+ } else if (pendingEscalation) {
7141
7644
  var reason = pendingEscalation.reason || "等待授权";
7142
7645
  var target = pendingEscalation.target ? " · " + pendingEscalation.target : "";
7143
7646
  permissionLabel.textContent = reason + target;
@@ -7145,7 +7648,14 @@
7145
7648
  permissionLabel.textContent = "等待授权";
7146
7649
  }
7147
7650
  }
7148
- if (permissionActionsEl) permissionActionsEl.classList.remove("hidden");
7651
+ if (permissionActionsEl) {
7652
+ permissionActionsEl.classList.remove("hidden");
7653
+ // Hide approve/deny buttons when auto-approve is active
7654
+ var approveBtn = document.getElementById("approve-permission-btn");
7655
+ var denyBtn = document.getElementById("deny-permission-btn");
7656
+ if (approveBtn) approveBtn.classList.toggle("hidden", !!isAutoApprove);
7657
+ if (denyBtn) denyBtn.classList.toggle("hidden", !!isAutoApprove);
7658
+ }
7149
7659
  // Hide top task bar — permission info is already shown in the composer
7150
7660
  taskEl.textContent = "";
7151
7661
  taskEl.classList.add("hidden");
@@ -7254,6 +7764,49 @@
7254
7764
  });
7255
7765
  }
7256
7766
 
7767
+ function toggleAutoApprove() {
7768
+ if (!state.selectedId) return;
7769
+ var toggle = document.getElementById("auto-approve-toggle");
7770
+ if (toggle) toggle.style.opacity = "0.5";
7771
+ fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/toggle-auto-approve", {
7772
+ method: "POST",
7773
+ credentials: "same-origin"
7774
+ })
7775
+ .then(function(res) { return res.json(); })
7776
+ .then(function(data) {
7777
+ if (data && data.error) {
7778
+ showToast(data.error, "error");
7779
+ return;
7780
+ }
7781
+ updateSessionSnapshot(data);
7782
+ updateAutoApproveIndicator();
7783
+ var enabled = data.autoApprovePermissions;
7784
+ showToast(enabled ? "自动批准已开启" : "自动批准已关闭", "info");
7785
+ })
7786
+ .catch(function(error) {
7787
+ showToast((error && error.message) || "无法切换自动批准。", "error");
7788
+ })
7789
+ .finally(function() {
7790
+ if (toggle) toggle.style.opacity = "";
7791
+ });
7792
+ }
7793
+
7794
+ function updateAutoApproveIndicator() {
7795
+ var toggle = document.getElementById("auto-approve-toggle");
7796
+ if (!toggle) return;
7797
+ var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
7798
+ var enabled = selectedSession && selectedSession.autoApprovePermissions;
7799
+ if (enabled) {
7800
+ toggle.className = "auto-approve-indicator active";
7801
+ toggle.title = "自动批准已启用 — 点击关闭";
7802
+ toggle.textContent = "🛡 自动批准";
7803
+ } else {
7804
+ toggle.className = "auto-approve-indicator";
7805
+ toggle.title = "自动批准已关闭 — 点击开启";
7806
+ toggle.textContent = "🛡 手动";
7807
+ }
7808
+ }
7809
+
7257
7810
  function updateTerminalOutput(output, sessionId, mode) {
7258
7811
  if (!state.terminal) return false;
7259
7812
  return syncTerminalBuffer(sessionId || state.selectedId, output, { mode: mode || "append" });
@@ -7462,6 +8015,10 @@
7462
8015
  // Force full render if message count changed or explicitly requested
7463
8016
  var forceRender = forceFullRender || msgCount !== state.lastRenderedMsgCount;
7464
8017
  if (!forceRender && msgCount === state.lastRenderedMsgCount && outputHash === state.lastRenderedHash) {
8018
+ // Even if message content hasn't changed, update the status bar
8019
+ // (inFlight state may have changed without new message content)
8020
+ var chatMessages = chatOutput.querySelector(".chat-messages");
8021
+ if (chatMessages) renderStructuredStatusBar(chatMessages, selectedSession);
7465
8022
  return;
7466
8023
  }
7467
8024
  var prevHash = state.lastRenderedHash;
@@ -7615,6 +8172,9 @@
7615
8172
  fullRenderChat();
7616
8173
  }
7617
8174
 
8175
+ // Update structured session status bar (in-flight / completed indicator)
8176
+ renderStructuredStatusBar(chatMessages, selectedSession);
8177
+
7618
8178
  // Update todo progress bar from latest messages
7619
8179
  updateTodoProgress(messages);
7620
8180
  }
@@ -7904,10 +8464,8 @@
7904
8464
  // ===== Terminal copy button for mobile =====
7905
8465
  // ===== Mobile DOM terminal view =====
7906
8466
  function initMobileDomTerminal(container) {
7907
- var isTouch = window.matchMedia("(pointer: coarse)").matches;
7908
- if (!isTouch) return;
7909
- // Gated by experimental config flag
7910
- if (!state.config || !state.config.experimentalDomTerminal) return;
8467
+ // DOM terminal feature removed — always return
8468
+ return;
7911
8469
 
7912
8470
  // Create DOM view container
7913
8471
  var domView = document.createElement("div");
@@ -8306,7 +8864,7 @@
8306
8864
  if (msg.role === "thinking") {
8307
8865
  return '<div class="chat-message thinking">' +
8308
8866
  '<div class="thinking-inline thinking-pty collapsed" data-thinking="" onclick="__thinkingToggle(this)">' +
8309
- '<span class="thinking-inline-icon">🧠</span>' +
8867
+ '<span class="thinking-inline-icon">⦿</span>' +
8310
8868
  '<span class="thinking-inline-preview">' + escapeHtml(msg.content) + '</span>' +
8311
8869
  '<span class="thinking-inline-action">展开</span>' +
8312
8870
  '</div>' +
@@ -8317,7 +8875,7 @@
8317
8875
  if (msg.role === "prompt") {
8318
8876
  return '<div class="chat-message prompt">' +
8319
8877
  '<div class="prompt-card">' +
8320
- '<div class="prompt-icon">💡</div>' +
8878
+ '<div class="prompt-icon">→</div>' +
8321
8879
  '<div class="prompt-content">试试:<span class="prompt-text">' + escapeHtml(msg.content) + '</span></div>' +
8322
8880
  '</div>' +
8323
8881
  '</div>';
@@ -8329,7 +8887,7 @@
8329
8887
  }
8330
8888
 
8331
8889
  // Legacy string content (from PTY parsing)
8332
- var avatar = msg.role === "assistant" ? '<div class="chat-message-avatar">AI</div>' : "";
8890
+ var avatar = msg.role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
8333
8891
  var bubbleContent = msg.role === "assistant" ? renderMarkdown(msg.content) : escapeHtml(msg.content);
8334
8892
  return '<div class="chat-message ' + msg.role + '">' +
8335
8893
  avatar +
@@ -8337,11 +8895,53 @@
8337
8895
  '</div>';
8338
8896
  }
8339
8897
 
8898
+ function buildToolResultMap(contentBlocks) {
8899
+ var toolResults = {};
8900
+ if (!Array.isArray(contentBlocks)) return toolResults;
8901
+ for (var i = 0; i < contentBlocks.length; i++) {
8902
+ var block = contentBlocks[i];
8903
+ if (block && block.type === "tool_result") {
8904
+ var toolUseId = block.tool_use_id;
8905
+ if (!toolUseId) continue;
8906
+ if (!toolResults[toolUseId]) {
8907
+ toolResults[toolUseId] = [];
8908
+ }
8909
+ toolResults[toolUseId].push(block);
8910
+ }
8911
+ }
8912
+ return toolResults;
8913
+ }
8914
+
8915
+ function pickToolResultForDisplay(toolResults, toolUseId) {
8916
+ var entries = toolResults && toolUseId ? toolResults[toolUseId] : null;
8917
+ if (!entries || !entries.length) return null;
8918
+ for (var i = 0; i < entries.length - 1; i++) {
8919
+ if (isRecoverableToolError(entries[i], entries[i + 1])) {
8920
+ return entries[i + 1];
8921
+ }
8922
+ }
8923
+ return entries[entries.length - 1];
8924
+ }
8925
+
8926
+ function hasRecoveredToolNoise(toolResults, toolUseId) {
8927
+ var entries = toolResults && toolUseId ? toolResults[toolUseId] : null;
8928
+ if (!entries || entries.length < 2) return false;
8929
+ for (var i = 0; i < entries.length - 1; i++) {
8930
+ if (isRecoverableToolError(entries[i], entries[i + 1])) {
8931
+ return true;
8932
+ }
8933
+ }
8934
+ return false;
8935
+ }
8936
+
8937
+ function renderRecoveredToolHint(toolName) {
8938
+ return '<div class="structured-tool-hint">已自动恢复一次 ' + escapeHtml(getToolDisplayName(toolName)) + ' 参数问题</div>';
8939
+ }
8940
+
8340
8941
  function renderStructuredMessage(msg) {
8341
8942
  var role = msg.role;
8342
- var avatar = role === "assistant" ? '<div class="chat-message-avatar">AI</div>' : "";
8943
+ var avatar = role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
8343
8944
 
8344
- // Empty content array — streaming placeholder, show typing indicator
8345
8945
  if (!msg.content || msg.content.length === 0) {
8346
8946
  if (role === "assistant") {
8347
8947
  return '<div class="chat-message ' + role + '">' +
@@ -8352,18 +8952,7 @@
8352
8952
  return "";
8353
8953
  }
8354
8954
 
8355
- // 先建立 tool_use_id -> tool_result 的映射
8356
- var toolResults = {};
8357
- for (var i = 0; i < msg.content.length; i++) {
8358
- var block = msg.content[i];
8359
- if (block && block.type === "tool_result") {
8360
- var toolUseId = block.tool_use_id;
8361
- if (toolUseId) {
8362
- toolResults[toolUseId] = block;
8363
- }
8364
- }
8365
- }
8366
-
8955
+ var toolResults = buildToolResultMap(msg.content);
8367
8956
  var blocksHtml = "";
8368
8957
 
8369
8958
  try {
@@ -8372,19 +8961,16 @@
8372
8961
  try {
8373
8962
  blocksHtml += renderContentBlock(block, role, toolResults, i);
8374
8963
  } catch (e) {
8375
- // Render error for individual block
8376
8964
  blocksHtml += '<div class="render-error">消息块渲染失败</div>';
8377
8965
  }
8378
8966
  }
8379
8967
  } catch (e) {
8380
- // Render error for entire message
8381
8968
  return '<div class="chat-message ' + role + '">' +
8382
8969
  avatar +
8383
- '<div class="chat-message-bubble"><div class="render-error">消息渲染失败</div></div>' +
8970
+ '<div class="chat-message-content"><div class="render-error">消息渲染失败</div></div>' +
8384
8971
  '</div>';
8385
8972
  }
8386
8973
 
8387
- // Build usage indicator for assistant messages
8388
8974
  var usageHtml = "";
8389
8975
  if (role === "assistant" && msg.usage) {
8390
8976
  var u = msg.usage;
@@ -8400,44 +8986,47 @@
8400
8986
 
8401
8987
  return '<div class="chat-message ' + role + '">' +
8402
8988
  avatar +
8403
- '<div class="chat-message-bubble">' + blocksHtml + '</div>' +
8989
+ '<div class="chat-message-content">' + blocksHtml + '</div>' +
8404
8990
  usageHtml +
8405
8991
  '</div>';
8406
8992
  }
8407
-
8408
8993
  function renderContentBlock(block, role, toolResults, index) {
8409
8994
  if (!block || !block.type) return "";
8410
8995
 
8411
8996
  switch (block.type) {
8412
8997
  case "text":
8998
+ if (role === "assistant" && block.__processing) {
8999
+ return '<div class="typing-indicator"><span></span><span></span><span></span></div>';
9000
+ }
8413
9001
  return role === "assistant" ? renderMarkdown(block.text || "") : escapeHtml(block.text || "");
8414
9002
 
8415
9003
  case "thinking":
8416
9004
  var thinkingText = block.thinking || "";
8417
- // Compact display: brain icon + brief text, click to expand
8418
9005
  var preview = thinkingText.length > 60 ? thinkingText.slice(0, 57) + "…" : thinkingText;
8419
9006
  var isStreaming = block.thinking === undefined && block.type === "thinking";
8420
9007
  if (isStreaming) {
8421
- // During streaming: show 3-line scrollable area
8422
9008
  return '<div class="thinking-inline thinking-streaming" data-thinking="">' +
8423
9009
  '<div class="thinking-streaming-inner">' +
8424
- '<span class="thinking-streaming-icon spinning">🧠</span>' +
9010
+ '<span class="thinking-streaming-icon spinning">⦿</span>' +
8425
9011
  '<div class="thinking-streaming-text"></div>' +
8426
9012
  '</div>' +
8427
9013
  '</div>';
8428
9014
  }
8429
9015
  return '<div class="thinking-inline collapsed" data-thinking="' + escapeHtml(thinkingText) + '" onclick="__thinkingToggle(this)">' +
8430
- '<span class="thinking-inline-icon">🧠</span>' +
9016
+ '<span class="thinking-inline-icon">⦿</span>' +
8431
9017
  '<span class="thinking-inline-preview">' + escapeHtml(preview) + '</span>' +
8432
9018
  '<span class="thinking-inline-action">展开</span>' +
8433
9019
  '</div>';
8434
9020
 
8435
9021
  case "tool_use":
8436
- var toolResult = toolResults[block.id];
8437
- return renderToolUseCard(block, toolResult, index);
9022
+ var toolResult = pickToolResultForDisplay(toolResults, block.id);
9023
+ var rendered = renderToolUseCard(block, toolResult, index);
9024
+ if (hasRecoveredToolNoise(toolResults, block.id)) {
9025
+ rendered = renderRecoveredToolHint(block.name || "工具") + rendered;
9026
+ }
9027
+ return rendered;
8438
9028
 
8439
9029
  case "tool_result":
8440
- // tool_result 已经在 tool_use 渲染时处理了,不再单独渲染
8441
9030
  return "";
8442
9031
 
8443
9032
  default:
@@ -8445,14 +9034,14 @@
8445
9034
  }
8446
9035
  }
8447
9036
 
8448
- // Lightweight inline display — used for Read, Glob, Grep, WebFetch, WebSearch, TodoRead
8449
9037
  function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo) {
8450
9038
  var toolId = block.id || "tool-" + toolName;
8451
9039
  var inputData = block.input || {};
8452
- var resultContent = (toolResult && toolResult.content) ? toolResult.content.trim() : "";
9040
+ var resultContent = extractToolResultText(toolResult && toolResult.content);
9041
+
8453
9042
  var isError = toolResult && toolResult.is_error;
8454
9043
  var hasResult = resultContent.length > 0;
8455
- var statusIcon = isError ? "⚠️" : (hasResult ? "" : "");
9044
+ var statusIcon = isError ? "" : (hasResult ? "" : "");
8456
9045
 
8457
9046
  // Build the inline preview line
8458
9047
  var icon = "";
@@ -8517,7 +9106,7 @@
8517
9106
  var fullResult = resultContent;
8518
9107
 
8519
9108
  var expandedHtml = "";
8520
- var shouldExpand = hasResult;
9109
+ var shouldExpand = false; // All inline tools collapsed by default
8521
9110
  if (hasResult) {
8522
9111
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
8523
9112
  '<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
@@ -8531,7 +9120,7 @@
8531
9120
 
8532
9121
  var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
8533
9122
  var extraClass = isError ? 'inline-tool-error-inline' : '';
8534
- if (hasResult) extraClass += ' inline-tool-open';
9123
+ if (shouldExpand) extraClass += ' inline-tool-open';
8535
9124
 
8536
9125
  return '<div class="inline-tool ' + extraClass + '" ' +
8537
9126
  'data-result="' + escapeHtml(fullResult) + '" ' +
@@ -8552,7 +9141,8 @@
8552
9141
  function renderTerminalTool(block, toolResult, toolName) {
8553
9142
  var inputData = block.input || {};
8554
9143
  var command = inputData.command || inputData.cmd || "";
8555
- var resultContent = (toolResult && toolResult.content) ? toolResult.content.trim() : "";
9144
+ var resultContent = extractToolResultText(toolResult && toolResult.content);
9145
+
8556
9146
  var isError = toolResult && toolResult.is_error;
8557
9147
  var exitCode = inputData.exitCode;
8558
9148
  var hasResult = resultContent.length > 0;
@@ -8587,13 +9177,16 @@
8587
9177
  exitCodeHtml = '<div class="term-exit ' + codeClass + '">exit ' + exitCode + '</div>';
8588
9178
  }
8589
9179
 
8590
- return '<div class="inline-terminal" data-expanded="true">' +
9180
+ // Show command preview in header (truncate long commands)
9181
+ var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
9182
+
9183
+ return '<div class="inline-terminal" data-expanded="false">' +
8591
9184
  '<div class="term-header" onclick="__terminalExpand(this)">' +
8592
9185
  statusDot +
8593
- '<span class="term-title">执行命令</span>' +
8594
- '<span class="term-toggle-icon">▼</span>' +
9186
+ '<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
9187
+ '<span class="term-toggle-icon">▶</span>' +
8595
9188
  '</div>' +
8596
- '<div class="term-body">' +
9189
+ '<div class="term-body" style="display:none;">' +
8597
9190
  '<div class="term-command"><span class="term-prompt">$</span> ' + cmdDisplay + '</div>' +
8598
9191
  (outputHtml ? '<div class="term-output">' + outputHtml + '</div>' : '') +
8599
9192
  exitCodeHtml +
@@ -8656,15 +9249,15 @@
8656
9249
  if (isError) {
8657
9250
  statusClass = "diff-error";
8658
9251
  statusText = toolResultText.indexOf("haven't granted") !== -1 || toolResultText.indexOf("permission") !== -1
8659
- ? "等待授权"
8660
- : "❌ 修改失败";
9252
+ ? "等待授权"
9253
+ : "失败";
8661
9254
  } else {
8662
9255
  statusClass = "diff-success";
8663
- statusText = "已修改";
9256
+ statusText = "已修改";
8664
9257
  }
8665
9258
  } else {
8666
9259
  statusClass = "diff-pending";
8667
- statusText = "⏳ 执行中…";
9260
+ statusText = "执行中";
8668
9261
  }
8669
9262
 
8670
9263
  // If only one column has content, show full width
@@ -8673,7 +9266,7 @@
8673
9266
 
8674
9267
  return '<div class="inline-diff" data-tool-name="' + escapeHtml(toolName) + '">' +
8675
9268
  '<div class="diff-header">' +
8676
- '<span class="diff-file-icon">📄</span>' +
9269
+ '<span class="diff-file-icon"></span>' +
8677
9270
  '<span class="diff-file-name">' + escapeHtml(fileName) + '</span>' +
8678
9271
  '<span class="diff-path">' + escapeHtml(path) + '</span>' +
8679
9272
  '<span class="diff-status ' + statusClass + '">' + statusText + '</span>' +
@@ -8732,7 +9325,7 @@
8732
9325
  }
8733
9326
  return '<div class="tool-use-card ask-user" data-tool-use-id="' + escapeHtml(toolId) + '">' +
8734
9327
  '<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
8735
- '<span class="tool-use-icon">❓</span>' +
9328
+ '<span class="tool-use-icon">?</span>' +
8736
9329
  '<span class="tool-use-name">提问</span>' +
8737
9330
  '</div>' +
8738
9331
  '<div class="tool-use-body ask-user-body">' +
@@ -8769,7 +9362,7 @@
8769
9362
 
8770
9363
  if (toolResult) {
8771
9364
  var isError = toolResult.is_error;
8772
- var content = toolResult.content || "";
9365
+ var content = extractToolResultText(toolResult.content);
8773
9366
  statusClass = isError ? "error" : "success";
8774
9367
  headerIcon = getToolIcon(toolName);
8775
9368
  var hasContent = content && content.trim().length > 0;
@@ -8823,23 +9416,23 @@
8823
9416
 
8824
9417
  function getToolIcon(toolName) {
8825
9418
  var icons = {
8826
- "Read": "📄",
8827
- "Write": "✏️",
8828
- "Edit": "📝",
8829
- "MultiEdit": "📝",
8830
- "Bash": "💻",
8831
- "Grep": "🔍",
8832
- "Glob": "📂",
8833
- "WebFetch": "🌐",
8834
- "WebSearch": "🔎",
8835
- "Task": "📋",
8836
- "TodoWrite": "📝",
8837
- "TodoRead": "📋",
8838
- "NotebookEdit": "📓",
8839
- "Agent": "🤖",
8840
- "Exit": "🚪"
9419
+ "Read": "R",
9420
+ "Write": "W",
9421
+ "Edit": "E",
9422
+ "MultiEdit": "E",
9423
+ "Bash": "$",
9424
+ "Grep": "G",
9425
+ "Glob": "F",
9426
+ "WebFetch": "",
9427
+ "WebSearch": "",
9428
+ "Task": "T",
9429
+ "TodoWrite": "",
9430
+ "TodoRead": "",
9431
+ "NotebookEdit": "N",
9432
+ "Agent": "A",
9433
+ "Exit": "×"
8841
9434
  };
8842
- return icons[toolName] || "🔧";
9435
+ return icons[toolName] || "·";
8843
9436
  }
8844
9437
 
8845
9438
  function generateInputSummary(toolName, input) {