@co0ontty/wand 1.3.6 → 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 {
@@ -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>' : '') +
@@ -1256,13 +1338,21 @@
1256
1338
  explorer.innerHTML = '<div class="file-explorer empty">No working directory.</div>';
1257
1339
  return;
1258
1340
  }
1341
+ state.fileExplorerLoading = true;
1342
+ state.allFiles = [];
1259
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>';
1260
1344
  // Update the cwd display
1261
1345
  if (cwdEl) cwdEl.textContent = cwd;
1262
1346
  // Fetch with git status
1263
1347
  fetch("/api/directory?q=" + encodeURIComponent(cwd) + "&gitStatus=true", { credentials: "same-origin" })
1264
- .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
+ })
1265
1354
  .then(function(items) {
1355
+ state.fileExplorerLoading = false;
1266
1356
  if (!items || items.length === 0) {
1267
1357
  explorer.innerHTML = '<div class="file-explorer empty">Empty directory or inaccessible.</div>';
1268
1358
  return;
@@ -1271,6 +1361,7 @@
1271
1361
  filterFileTree();
1272
1362
  })
1273
1363
  .catch(function() {
1364
+ state.fileExplorerLoading = false;
1274
1365
  explorer.innerHTML = '<div class="file-explorer empty">Failed to load files.</div>';
1275
1366
  });
1276
1367
  }
@@ -1667,7 +1758,7 @@
1667
1758
  if (session.claudeSessionId) {
1668
1759
  var shortId = session.claudeSessionId.slice(0, 8);
1669
1760
  sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
1670
- if (session.status !== "running" && !state.sessionsManageMode) {
1761
+ if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
1671
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>';
1672
1763
  }
1673
1764
  }
@@ -1677,6 +1768,7 @@
1677
1768
  }
1678
1769
 
1679
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);
1680
1772
  var actionsHtml = '<span class="session-actions">' + resumeButton + deleteButton + '</span>';
1681
1773
 
1682
1774
  return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
@@ -1686,6 +1778,7 @@
1686
1778
  '<div class="session-main">' +
1687
1779
  '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>' +
1688
1780
  '<div class="session-meta">' +
1781
+ modeBadge +
1689
1782
  '<span>' + escapeHtml(modeName) + '</span>' +
1690
1783
  '<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
1691
1784
  sessionIdDisplay +
@@ -1697,6 +1790,15 @@
1697
1790
  '</div>' +
1698
1791
  '</div>';
1699
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
+
1700
1802
  function renderModeCards(selectedMode) {
1701
1803
  var modes = [
1702
1804
  { id: "managed", label: "托管", desc: "全自动完成任务" },
@@ -1714,19 +1816,48 @@
1714
1816
  }).join("");
1715
1817
  }
1716
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
+
1717
1840
  function renderSessionModal() {
1718
1841
  var modalTool = getPreferredTool();
1719
1842
  var modalMode = getSafeModeForTool(modalTool, state.modeValue || state.chatMode || "default");
1843
+ var sessionKind = state.sessionCreateKind || "pty";
1720
1844
  return '<section id="session-modal" class="modal-backdrop hidden">' +
1721
1845
  '<div class="modal session-modal">' +
1722
1846
  '<div class="modal-header">' +
1723
1847
  '<div>' +
1724
1848
  '<h2 class="modal-title">新对话</h2>' +
1725
- '<p class="modal-subtitle">启动 Claude 会话,选择模式和工作目录。</p>' +
1849
+ '<p class="modal-subtitle">启动 Claude 会话,选择会话类型、模式和工作目录。</p>' +
1726
1850
  '</div>' +
1727
1851
  '<button id="close-modal-button" class="btn btn-ghost btn-icon">&times;</button>' +
1728
1852
  '</div>' +
1729
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>' +
1730
1861
  '<div class="field">' +
1731
1862
  '<label class="field-label">模式</label>' +
1732
1863
  '<div id="mode-cards" class="mode-cards">' +
@@ -1899,6 +2030,16 @@
1899
2030
  quickStartSession();
1900
2031
  });
1901
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
+ }
1902
2043
  initBlankChatCwd();
1903
2044
 
1904
2045
  var sessionsList = document.getElementById("sessions-list");
@@ -1911,6 +2052,17 @@
1911
2052
  // Claude session ID badge click-to-copy (event delegation on document)
1912
2053
  document.addEventListener("click", handleClaudeIdCopy);
1913
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
+
1914
2066
  var modeCardsEl = document.getElementById("mode-cards");
1915
2067
  if (modeCardsEl) modeCardsEl.addEventListener("click", function(e) {
1916
2068
  var card = e.target.closest(".mode-card");
@@ -1941,7 +2093,7 @@
1941
2093
  persistSelectedId();
1942
2094
  resetChatRenderCache();
1943
2095
  closeSessionsDrawer();
1944
- renderApp();
2096
+ render();
1945
2097
  });
1946
2098
  var refreshBtn = document.getElementById("sidebar-refresh-btn");
1947
2099
  if (refreshBtn) refreshBtn.addEventListener("click", function() {
@@ -1996,6 +2148,8 @@
1996
2148
  if (approvePermissionBtn) approvePermissionBtn.addEventListener("click", approvePermission);
1997
2149
  var denyPermissionBtn = document.getElementById("deny-permission-btn");
1998
2150
  if (denyPermissionBtn) denyPermissionBtn.addEventListener("click", denyPermission);
2151
+ var autoApproveToggle = document.getElementById("auto-approve-toggle");
2152
+ if (autoApproveToggle) autoApproveToggle.addEventListener("click", toggleAutoApprove);
1999
2153
  var sendBtn = document.getElementById("send-input-button");
2000
2154
  if (sendBtn) sendBtn.addEventListener("click", function() {
2001
2155
  closeSessionsDrawer();
@@ -2256,7 +2410,7 @@
2256
2410
  state.selectedId = null;
2257
2411
  persistSelectedId();
2258
2412
  state.drafts = {};
2259
- renderApp();
2413
+ render();
2260
2414
  // 聚焦到目录输入框
2261
2415
  setTimeout(function() {
2262
2416
  var folderInput = document.getElementById("folder-picker-input");
@@ -2539,7 +2693,7 @@
2539
2693
 
2540
2694
  function activateSessionItem(sessionId) {
2541
2695
  var session = state.sessions.find(function(s) { return s.id === sessionId; });
2542
- if (session && session.status !== "running") {
2696
+ if (session && session.status !== "running" && !isStructuredSession(session)) {
2543
2697
  resumeSessionFromList(sessionId);
2544
2698
  } else {
2545
2699
  selectSession(sessionId);
@@ -3336,6 +3490,35 @@
3336
3490
  return hints[mode] || '';
3337
3491
  }
3338
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
+
3339
3522
  function syncComposerModeSelect() {
3340
3523
  var select = document.getElementById("chat-mode-select");
3341
3524
  if (!select) return;
@@ -3346,29 +3529,89 @@
3346
3529
  if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
3347
3530
  }
3348
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
+
3349
3567
  function applyCurrentView() {
3350
3568
  var hasSession = !!state.selectedId;
3351
3569
  var terminalBtn = document.getElementById("view-terminal-btn");
3352
3570
  var terminalContainer = document.getElementById("output");
3353
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
+ }
3354
3583
 
3355
- if (terminalBtn) terminalBtn.classList.add("active");
3356
- 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
+ }
3357
3592
  if (chatContainer) {
3358
- chatContainer.classList.remove("active");
3359
- chatContainer.classList.add("hidden");
3593
+ chatContainer.classList.toggle("active", showChat);
3594
+ chatContainer.classList.toggle("hidden", !showChat);
3360
3595
  }
3361
3596
  updateInteractiveControls();
3362
3597
  }
3363
3598
 
3364
3599
  function syncSessionModalUI() {
3365
3600
  var modeHint = document.getElementById("mode-description");
3601
+ var kindHint = document.getElementById("session-kind-description");
3366
3602
  var tool = "claude";
3603
+ var sessionKind = state.sessionCreateKind || "pty";
3367
3604
 
3368
3605
  state.sessionTool = tool;
3369
3606
  state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
3370
3607
 
3371
- // 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
+
3372
3615
  var modeCards = document.querySelectorAll("#mode-cards .mode-card");
3373
3616
  if (modeCards.length) {
3374
3617
  modeCards.forEach(function(card) {
@@ -3376,11 +3619,22 @@
3376
3619
  });
3377
3620
  }
3378
3621
 
3622
+ if (kindHint) kindHint.textContent = getSessionKindHint(sessionKind);
3379
3623
  if (modeHint) modeHint.textContent = getToolModeHint(tool, state.modeValue);
3380
3624
  }
3381
3625
 
3382
3626
  function updateSessionSnapshot(snapshot) {
3383
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
+ }
3384
3638
  var updated = false;
3385
3639
  var prevSession = null;
3386
3640
  state.sessions = state.sessions.map(function(session) {
@@ -3410,14 +3664,34 @@
3410
3664
  var localOutput = localSession.output || "";
3411
3665
  var serverOutput = serverSession.output || "";
3412
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);
3413
3682
 
3414
3683
  if (keepLocalOutput) {
3415
3684
  merged.output = localOutput;
3416
3685
  }
3417
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
+
3418
3693
  if (localSession.id === state.selectedId) {
3419
3694
  if (localSession.permissionBlocked && serverSession.permissionBlocked === false) {
3420
- // server explicitly resolved it; keep resolved state
3421
3695
  } else if (localSession.permissionBlocked && !serverSession.permissionBlocked) {
3422
3696
  merged.permissionBlocked = true;
3423
3697
  }
@@ -3452,15 +3726,13 @@
3452
3726
 
3453
3727
  function getPreferredSessionId(sessions) {
3454
3728
  if (!sessions || !sessions.length) return null;
3455
- // Keep currently selected session as long as it still exists
3456
3729
  if (state.selectedId) {
3457
3730
  var stillExists = sessions.find(function(session) { return session.id === state.selectedId; });
3458
3731
  if (stillExists) return stillExists.id;
3732
+ return null;
3459
3733
  }
3460
- // No selection — pick a running session, or fall back to most recent
3461
3734
  var runningSession = sessions.find(function(session) { return session.status === "running"; });
3462
3735
  if (runningSession) return runningSession.id;
3463
- // Fall back to most recent non-archived session (sessions are sorted newest first)
3464
3736
  var recent = sessions.find(function(session) { return !session.archived; });
3465
3737
  return recent ? recent.id : sessions[0].id;
3466
3738
  }
@@ -3487,7 +3759,10 @@
3487
3759
  return mergeServerSession(localSession, serverSession);
3488
3760
  });
3489
3761
 
3490
- state.selectedId = getPreferredSessionId(state.sessions);
3762
+ var preferredSessionId = getPreferredSessionId(state.sessions);
3763
+ if (preferredSessionId !== undefined) {
3764
+ state.selectedId = preferredSessionId;
3765
+ }
3491
3766
  persistSelectedId();
3492
3767
  if (state.modalOpen) {
3493
3768
  updateSessionsList();
@@ -3504,13 +3779,22 @@
3504
3779
  }
3505
3780
  }
3506
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
+ }
3507
3792
  })
3508
3793
  .catch(function(e) {
3509
3794
  console.error("[wand] loadSessions failed:", e);
3510
3795
  });
3511
3796
  }
3512
3797
 
3513
-
3514
3798
  function updateSessionsList() {
3515
3799
  var listEl = document.getElementById("sessions-list");
3516
3800
  var countEl = document.getElementById("session-count");
@@ -3538,22 +3822,23 @@
3538
3822
 
3539
3823
  if (summaryEl && summaryEl.textContent !== terminalTitle) summaryEl.textContent = terminalTitle;
3540
3824
  if (titleEl && titleEl.textContent !== terminalTitle) titleEl.textContent = terminalTitle;
3541
- if (infoEl && infoEl.textContent !== terminalInfo) {
3542
- infoEl.textContent = terminalInfo;
3543
- }
3825
+ if (infoEl) infoEl.textContent = selectedSession ? (terminalInfo + " · " + getSessionKindDescription(selectedSession)) : terminalInfo;
3544
3826
 
3545
- // Update session info bar at bottom
3546
3827
  var cwdEl = document.getElementById("session-cwd-display");
3547
3828
  var modeEl = document.getElementById("session-mode-display");
3829
+ var kindEl = document.getElementById("session-kind-display");
3548
3830
  var statusEl = document.getElementById("session-status-display");
3549
3831
  var exitEl = document.getElementById("session-exit-display");
3550
3832
  var cwdText = selectedSession && selectedSession.cwd ? selectedSession.cwd : "未设置目录";
3551
3833
  var modeText = selectedSession ? getModeLabel(selectedSession.mode) : "默认";
3834
+ var kindText = selectedSession ? getSessionKindLabel(selectedSession) : "PTY";
3552
3835
  var exitText = "exit=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
3553
3836
  if (cwdEl && cwdEl.textContent !== cwdText) cwdEl.textContent = cwdText;
3554
3837
  if (modeEl && modeEl.textContent !== modeText) modeEl.textContent = modeText;
3838
+ if (kindEl && kindEl.textContent !== kindText) kindEl.textContent = kindText;
3555
3839
  if (statusEl && statusEl.textContent !== terminalInfo) statusEl.textContent = terminalInfo;
3556
3840
  if (exitEl && exitEl.textContent !== exitText) exitEl.textContent = exitText;
3841
+ updateAutoApproveIndicator();
3557
3842
 
3558
3843
  if (!state.terminal && terminalContainer && selectedSession) {
3559
3844
  initTerminal();
@@ -3601,7 +3886,12 @@
3601
3886
  clearTimeout(chatRenderTimer);
3602
3887
  chatRenderTimer = null;
3603
3888
  }
3604
- 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" })
3605
3895
  .then(function(res) { return res.json(); })
3606
3896
  .then(function(data) {
3607
3897
  if (data.error) {
@@ -3628,6 +3918,8 @@
3628
3918
  }
3629
3919
 
3630
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));
3631
3923
  state.selectedId = id;
3632
3924
  persistSelectedId();
3633
3925
  resetChatRenderCache();
@@ -3703,6 +3995,7 @@
3703
3995
  modal.classList.remove("hidden");
3704
3996
  lastFocusedElement = document.activeElement;
3705
3997
  state.sessionTool = getPreferredTool();
3998
+ state.sessionCreateKind = "pty";
3706
3999
  state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
3707
4000
  syncSessionModalUI();
3708
4001
  loadRecentPathBubbles();
@@ -4230,13 +4523,45 @@
4230
4523
  var cwdEl = document.getElementById("cwd");
4231
4524
  var errorEl = document.getElementById("modal-error");
4232
4525
  var command = getPreferredTool();
4526
+ var sessionKind = state.sessionCreateKind || "pty";
4233
4527
 
4234
4528
  hideError(errorEl);
4235
4529
 
4236
4530
  var defaultCwd = getEffectiveCwd();
4531
+ var cwd = cwdEl.value.trim() || defaultCwd;
4237
4532
  var selectedMode = getSafeModeForTool(command, state.modeValue);
4238
- state.modeValue = selectedMode;
4239
- 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;
4240
4565
  state.sessionTool = command;
4241
4566
  state.preferredCommand = command;
4242
4567
  syncComposerModeSelect();
@@ -4247,8 +4572,8 @@
4247
4572
  credentials: "same-origin",
4248
4573
  body: JSON.stringify({
4249
4574
  command: command,
4250
- cwd: cwdEl.value.trim() || defaultCwd,
4251
- mode: selectedMode
4575
+ cwd: cwd,
4576
+ mode: mode
4252
4577
  })
4253
4578
  })
4254
4579
  .then(function(res) { return res.json(); })
@@ -4258,6 +4583,7 @@
4258
4583
  return;
4259
4584
  }
4260
4585
  state.selectedId = data.id;
4586
+ console.log("[WAND] runPtyCommandFromModal created session:", data.id, "sessionKind:", data.sessionKind, "runner:", data.runner);
4261
4587
  persistSelectedId();
4262
4588
  state.drafts[data.id] = "";
4263
4589
  resetChatRenderCache();
@@ -4265,13 +4591,19 @@
4265
4591
  closeSessionsDrawer();
4266
4592
  return refreshAll();
4267
4593
  })
4268
- .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
+ })
4269
4602
  .catch(function() {
4270
4603
  showError(errorEl, "无法启动会话,请确认 Claude 已正确安装。");
4271
4604
  });
4272
4605
  }
4273
4606
 
4274
- // Blank-chat CWD inline display + dropdown
4275
4607
  function initBlankChatCwd() {
4276
4608
  var cwdEl = document.getElementById("blank-chat-cwd");
4277
4609
  if (!cwdEl) return;
@@ -4691,11 +5023,13 @@
4691
5023
  persistSelectedId();
4692
5024
  state.drafts[data.id] = "";
4693
5025
  resetChatRenderCache();
4694
- switchToSessionView(data.id);
4695
5026
  updateSessionSnapshot(data);
4696
5027
  updateSessionsList();
5028
+ switchToSessionView(data.id);
4697
5029
  subscribeToSession(data.id);
4698
5030
  loadOutput(data.id).then(function() {
5031
+ welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
5032
+ welcomeInput.disabled = false;
4699
5033
  focusInputBox(true);
4700
5034
  });
4701
5035
  })
@@ -4749,9 +5083,9 @@
4749
5083
  resetChatRenderCache();
4750
5084
  if (inputBox) inputBox.value = "";
4751
5085
  if (welcomeInput) welcomeInput.value = "";
4752
- switchToSessionView(data.id);
4753
5086
  updateSessionSnapshot(data);
4754
5087
  updateSessionsList();
5088
+ switchToSessionView(data.id);
4755
5089
  // Subscribe to new session via WebSocket
4756
5090
  subscribeToSession(data.id);
4757
5091
  return loadOutput(data.id);
@@ -4763,6 +5097,7 @@
4763
5097
 
4764
5098
  function switchToSessionView(sessionId) {
4765
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);
4766
5101
  var blankChat = document.getElementById("blank-chat");
4767
5102
  var terminalContainer = document.getElementById("output");
4768
5103
  var chatContainer = document.getElementById("chat-output");
@@ -4770,28 +5105,36 @@
4770
5105
  var terminalTitle = document.getElementById("terminal-title");
4771
5106
  var terminalInfo = document.getElementById("terminal-info");
4772
5107
  var sessionSummary = document.querySelector(".session-summary-value");
5108
+ var structured = isStructuredSession(session);
4773
5109
 
4774
5110
  if (blankChat) blankChat.classList.add("hidden");
4775
- if (terminalContainer) terminalContainer.classList.remove("hidden");
5111
+ if (terminalContainer) {
5112
+ terminalContainer.classList.toggle("hidden", structured);
5113
+ }
4776
5114
  if (chatContainer) {
4777
5115
  chatContainer.classList.remove("hidden");
4778
5116
  }
4779
5117
  if (stopBtn) stopBtn.classList.remove("hidden");
4780
5118
 
5119
+ if (structured) {
5120
+ state.currentView = "chat";
5121
+ } else {
5122
+ state.currentView = "terminal";
5123
+ }
5124
+
4781
5125
  var title = session ? shortCommand(session.command) : "Wand";
4782
5126
  var info = session ? getSessionStatusLabel(session) : "开始对话";
4783
5127
  if (terminalTitle) terminalTitle.textContent = title;
4784
5128
  if (terminalInfo) terminalInfo.textContent = info;
4785
5129
  if (sessionSummary) sessionSummary.textContent = title;
4786
5130
 
4787
- // Init terminal if not already done
4788
- if (!state.terminal) initTerminal();
4789
- applyCurrentView();
4790
- if (state.terminal && state.fitAddon) {
4791
- ensureTerminalFit();
5131
+ if (!structured) {
5132
+ if (!state.terminal) initTerminal();
5133
+ if (state.terminal && state.fitAddon) {
5134
+ ensureTerminalFit();
5135
+ }
4792
5136
  }
4793
- // Don't call renderChat() here — loadOutput() always calls renderChat() after it resolves.
4794
- // Calling renderChat() prematurely would render with stale/empty messages.
5137
+ applyCurrentView();
4795
5138
  focusInputBox();
4796
5139
  }
4797
5140
 
@@ -4806,9 +5149,12 @@
4806
5149
  var value = inputBox ? inputBox.value : "";
4807
5150
  var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; }) || null;
4808
5151
  if (value) {
4809
- console.log("[wand] sendInputFromBox", {
5152
+ console.log("[WAND] sendInputFromBox", {
4810
5153
  sessionId: state.selectedId,
4811
5154
  sessionStatus: selectedSession ? selectedSession.status : null,
5155
+ sessionKind: selectedSession ? selectedSession.sessionKind : null,
5156
+ runner: selectedSession ? selectedSession.runner : null,
5157
+ isStructured: isStructuredSession(selectedSession),
4812
5158
  view: state.currentView,
4813
5159
  wsConnected: state.wsConnected,
4814
5160
  terminalInteractive: state.terminalInteractive,
@@ -4817,6 +5163,11 @@
4817
5163
  // Clear todo progress bar at the start of a new user turn
4818
5164
  var todoEl = document.getElementById("todo-progress");
4819
5165
  if (todoEl) todoEl.classList.add("hidden");
5166
+
5167
+ if (isStructuredSession(selectedSession)) {
5168
+ return postStructuredInput(value, inputBox, selectedSession);
5169
+ }
5170
+
4820
5171
  // Send text + Enter as a single call to avoid race conditions
4821
5172
  var combinedInput = value + getControlInput("enter");
4822
5173
  var isOffline = !state.wsConnected;
@@ -4857,6 +5208,79 @@
4857
5208
  return Promise.resolve();
4858
5209
  }
4859
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
+
4860
5284
  function getInputErrorMessage(error) {
4861
5285
  if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
4862
5286
  return "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
@@ -4908,6 +5332,7 @@
4908
5332
  }
4909
5333
 
4910
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 });
4911
5336
  if (!session) {
4912
5337
  showToast("会话不存在,请重新选择或新建会话。", "error");
4913
5338
  return Promise.resolve(null);
@@ -5203,21 +5628,24 @@
5203
5628
  }
5204
5629
 
5205
5630
  function updateInteractiveControls() {
5631
+ var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
5632
+ var structured = isStructuredSession(selectedSession);
5206
5633
  // Update both toggle buttons (topbar and terminal-header)
5207
5634
  var toggles = ["terminal-interactive-toggle-top"];
5208
5635
  toggles.forEach(function(id) {
5209
5636
  var toggle = document.getElementById(id);
5210
5637
  if (toggle) {
5211
5638
  toggle.classList.toggle("active", state.terminalInteractive);
5639
+ toggle.classList.toggle("hidden", structured || state.currentView !== "terminal" || !selectedSession);
5212
5640
  }
5213
5641
  });
5214
5642
  // Inline keyboard visibility follows current view
5215
5643
  var inlineKeyboard = document.getElementById("inline-keyboard");
5216
- if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", state.currentView !== "terminal");
5644
+ if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", structured || state.currentView !== "terminal");
5217
5645
  var inputHint = document.querySelector(".input-hint");
5218
- if (inputHint) inputHint.classList.toggle("hidden", state.currentView === "terminal");
5646
+ if (inputHint) inputHint.classList.toggle("hidden", structured ? false : state.currentView === "terminal");
5219
5647
  var container = document.getElementById("output");
5220
- if (container) container.classList.toggle("interactive", state.terminalInteractive);
5648
+ if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
5221
5649
  }
5222
5650
 
5223
5651
  function captureTerminalInput(event) {
@@ -5573,6 +6001,7 @@
5573
6001
  }
5574
6002
 
5575
6003
  function resumeSession(sessionId, errorEl) {
6004
+ console.log("[WAND] resumeSession sessionId:", sessionId);
5576
6005
  if (!sessionId) return Promise.resolve(null);
5577
6006
  return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/resume", {
5578
6007
  method: "POST",
@@ -5645,6 +6074,7 @@
5645
6074
  }
5646
6075
 
5647
6076
  function resumeSessionFromList(sessionId) {
6077
+ console.log("[WAND] resumeSessionFromList sessionId:", sessionId);
5648
6078
  return resumeSession(sessionId).then(function(data) {
5649
6079
  if (!data) return null;
5650
6080
  return activateSession(data).then(function() {
@@ -5733,6 +6163,7 @@
5733
6163
  }
5734
6164
 
5735
6165
  function handleResumeAction(actionButton) {
6166
+ console.log("[WAND] handleResumeAction sessionId:", actionButton.dataset.sessionId);
5736
6167
  actionButton.disabled = true;
5737
6168
  resumeSessionFromList(actionButton.dataset.sessionId)
5738
6169
  .finally(function() {
@@ -5743,6 +6174,7 @@
5743
6174
  function handleResumeHistoryAction(actionButton) {
5744
6175
  var claudeSessionId = actionButton.dataset.claudeSessionId;
5745
6176
  var cwd = actionButton.dataset.cwd;
6177
+ console.log("[WAND] handleResumeHistoryAction claudeSessionId:", claudeSessionId, "cwd:", cwd);
5746
6178
  if (!claudeSessionId) return;
5747
6179
  actionButton.disabled = true;
5748
6180
  resumeClaudeHistorySession(claudeSessionId, cwd)
@@ -6788,15 +7220,18 @@
6788
7220
  state.fitAddon.fit();
6789
7221
  maybeScrollTerminalToBottom("resize");
6790
7222
  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() {});
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
+ }
6800
7235
  }
6801
7236
  }
6802
7237
  } else if (attempt < maxAttempts) {
@@ -6836,6 +7271,10 @@
6836
7271
 
6837
7272
  if (!state.selectedId) return;
6838
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
+
6839
7278
  // Only send resize API call if dimensions actually changed
6840
7279
  if (state.lastResize.cols !== nextSize.cols || state.lastResize.rows !== nextSize.rows) {
6841
7280
  state.lastResize = nextSize;
@@ -6930,6 +7369,17 @@
6930
7369
  updateSessionSnapshot(snapshot);
6931
7370
  if (msg.sessionId === state.selectedId) {
6932
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
+ }
6933
7383
  updateTaskDisplay();
6934
7384
  scheduleChatRender();
6935
7385
  }
@@ -6968,8 +7418,19 @@
6968
7418
  if (msg.data && msg.data.messages) {
6969
7419
  endedSnapshot.messages = msg.data.messages;
6970
7420
  }
7421
+ if (msg.data && msg.data.structuredState) {
7422
+ endedSnapshot.structuredState = msg.data.structuredState;
7423
+ }
6971
7424
  updateSessionSnapshot(endedSnapshot);
6972
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
+ }
6973
7434
  // Notify user when a session completes (browser + in-app if backgrounded or not viewing)
6974
7435
  var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
6975
7436
  var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
@@ -7044,7 +7505,24 @@
7044
7505
  break;
7045
7506
  case 'status':
7046
7507
  if (msg.sessionId && msg.data) {
7508
+ console.log('[WAND] ws status', msg.sessionId, JSON.stringify(msg.data));
7047
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
+ }
7048
7526
  if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
7049
7527
  statusUpdate.permissionBlocked = !!msg.data.permissionBlocked;
7050
7528
  }
@@ -7096,6 +7574,10 @@
7096
7574
  if (msg.data.approvalStats) {
7097
7575
  updateApprovalStats();
7098
7576
  }
7577
+ // Re-render chat when structured session inFlight state changes
7578
+ if (statusUpdate.structuredState) {
7579
+ scheduleChatRender();
7580
+ }
7099
7581
  }
7100
7582
  }
7101
7583
  break;
@@ -7135,9 +7617,12 @@
7135
7617
  var isBlocked = pendingEscalation || (selectedSession && selectedSession.permissionBlocked);
7136
7618
 
7137
7619
  if (isBlocked) {
7620
+ var isAutoApprove = selectedSession && selectedSession.autoApprovePermissions;
7138
7621
  // Show permission label in input composer area
7139
7622
  if (permissionLabel) {
7140
- if (pendingEscalation) {
7623
+ if (isAutoApprove) {
7624
+ permissionLabel.textContent = "自动批准中...";
7625
+ } else if (pendingEscalation) {
7141
7626
  var reason = pendingEscalation.reason || "等待授权";
7142
7627
  var target = pendingEscalation.target ? " · " + pendingEscalation.target : "";
7143
7628
  permissionLabel.textContent = reason + target;
@@ -7145,7 +7630,14 @@
7145
7630
  permissionLabel.textContent = "等待授权";
7146
7631
  }
7147
7632
  }
7148
- 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
+ }
7149
7641
  // Hide top task bar — permission info is already shown in the composer
7150
7642
  taskEl.textContent = "";
7151
7643
  taskEl.classList.add("hidden");
@@ -7254,6 +7746,49 @@
7254
7746
  });
7255
7747
  }
7256
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
+
7257
7792
  function updateTerminalOutput(output, sessionId, mode) {
7258
7793
  if (!state.terminal) return false;
7259
7794
  return syncTerminalBuffer(sessionId || state.selectedId, output, { mode: mode || "append" });
@@ -7462,6 +7997,10 @@
7462
7997
  // Force full render if message count changed or explicitly requested
7463
7998
  var forceRender = forceFullRender || msgCount !== state.lastRenderedMsgCount;
7464
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);
7465
8004
  return;
7466
8005
  }
7467
8006
  var prevHash = state.lastRenderedHash;
@@ -7615,6 +8154,9 @@
7615
8154
  fullRenderChat();
7616
8155
  }
7617
8156
 
8157
+ // Update structured session status bar (in-flight / completed indicator)
8158
+ renderStructuredStatusBar(chatMessages, selectedSession);
8159
+
7618
8160
  // Update todo progress bar from latest messages
7619
8161
  updateTodoProgress(messages);
7620
8162
  }
@@ -8337,11 +8879,53 @@
8337
8879
  '</div>';
8338
8880
  }
8339
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
+
8340
8925
  function renderStructuredMessage(msg) {
8341
8926
  var role = msg.role;
8342
8927
  var avatar = role === "assistant" ? '<div class="chat-message-avatar">AI</div>' : "";
8343
8928
 
8344
- // Empty content array — streaming placeholder, show typing indicator
8345
8929
  if (!msg.content || msg.content.length === 0) {
8346
8930
  if (role === "assistant") {
8347
8931
  return '<div class="chat-message ' + role + '">' +
@@ -8352,18 +8936,7 @@
8352
8936
  return "";
8353
8937
  }
8354
8938
 
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
-
8939
+ var toolResults = buildToolResultMap(msg.content);
8367
8940
  var blocksHtml = "";
8368
8941
 
8369
8942
  try {
@@ -8372,19 +8945,16 @@
8372
8945
  try {
8373
8946
  blocksHtml += renderContentBlock(block, role, toolResults, i);
8374
8947
  } catch (e) {
8375
- // Render error for individual block
8376
8948
  blocksHtml += '<div class="render-error">消息块渲染失败</div>';
8377
8949
  }
8378
8950
  }
8379
8951
  } catch (e) {
8380
- // Render error for entire message
8381
8952
  return '<div class="chat-message ' + role + '">' +
8382
8953
  avatar +
8383
8954
  '<div class="chat-message-bubble"><div class="render-error">消息渲染失败</div></div>' +
8384
8955
  '</div>';
8385
8956
  }
8386
8957
 
8387
- // Build usage indicator for assistant messages
8388
8958
  var usageHtml = "";
8389
8959
  if (role === "assistant" && msg.usage) {
8390
8960
  var u = msg.usage;
@@ -8404,21 +8974,21 @@
8404
8974
  usageHtml +
8405
8975
  '</div>';
8406
8976
  }
8407
-
8408
8977
  function renderContentBlock(block, role, toolResults, index) {
8409
8978
  if (!block || !block.type) return "";
8410
8979
 
8411
8980
  switch (block.type) {
8412
8981
  case "text":
8982
+ if (role === "assistant" && block.__processing) {
8983
+ return '<div class="typing-indicator"><span></span><span></span><span></span></div>';
8984
+ }
8413
8985
  return role === "assistant" ? renderMarkdown(block.text || "") : escapeHtml(block.text || "");
8414
8986
 
8415
8987
  case "thinking":
8416
8988
  var thinkingText = block.thinking || "";
8417
- // Compact display: brain icon + brief text, click to expand
8418
8989
  var preview = thinkingText.length > 60 ? thinkingText.slice(0, 57) + "…" : thinkingText;
8419
8990
  var isStreaming = block.thinking === undefined && block.type === "thinking";
8420
8991
  if (isStreaming) {
8421
- // During streaming: show 3-line scrollable area
8422
8992
  return '<div class="thinking-inline thinking-streaming" data-thinking="">' +
8423
8993
  '<div class="thinking-streaming-inner">' +
8424
8994
  '<span class="thinking-streaming-icon spinning">🧠</span>' +
@@ -8433,11 +9003,14 @@
8433
9003
  '</div>';
8434
9004
 
8435
9005
  case "tool_use":
8436
- var toolResult = toolResults[block.id];
8437
- 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;
8438
9012
 
8439
9013
  case "tool_result":
8440
- // tool_result 已经在 tool_use 渲染时处理了,不再单独渲染
8441
9014
  return "";
8442
9015
 
8443
9016
  default:
@@ -8445,11 +9018,11 @@
8445
9018
  }
8446
9019
  }
8447
9020
 
8448
- // Lightweight inline display — used for Read, Glob, Grep, WebFetch, WebSearch, TodoRead
8449
9021
  function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo) {
8450
9022
  var toolId = block.id || "tool-" + toolName;
8451
9023
  var inputData = block.input || {};
8452
- var resultContent = (toolResult && toolResult.content) ? toolResult.content.trim() : "";
9024
+ var resultContent = extractToolResultText(toolResult && toolResult.content);
9025
+
8453
9026
  var isError = toolResult && toolResult.is_error;
8454
9027
  var hasResult = resultContent.length > 0;
8455
9028
  var statusIcon = isError ? "⚠️" : (hasResult ? "✅" : "⏳");
@@ -8552,7 +9125,8 @@
8552
9125
  function renderTerminalTool(block, toolResult, toolName) {
8553
9126
  var inputData = block.input || {};
8554
9127
  var command = inputData.command || inputData.cmd || "";
8555
- var resultContent = (toolResult && toolResult.content) ? toolResult.content.trim() : "";
9128
+ var resultContent = extractToolResultText(toolResult && toolResult.content);
9129
+
8556
9130
  var isError = toolResult && toolResult.is_error;
8557
9131
  var exitCode = inputData.exitCode;
8558
9132
  var hasResult = resultContent.length > 0;
@@ -8769,7 +9343,7 @@
8769
9343
 
8770
9344
  if (toolResult) {
8771
9345
  var isError = toolResult.is_error;
8772
- var content = toolResult.content || "";
9346
+ var content = extractToolResultText(toolResult.content);
8773
9347
  statusClass = isError ? "error" : "success";
8774
9348
  headerIcon = getToolIcon(toolName);
8775
9349
  var hasContent = content && content.trim().length > 0;