@co0ontty/wand 1.6.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +40 -0
- package/dist/git-worktree.d.ts +28 -0
- package/dist/git-worktree.js +287 -0
- package/dist/message-parser.d.ts +1 -1
- package/dist/message-parser.js +275 -1
- package/dist/process-manager.d.ts +6 -3
- package/dist/process-manager.js +135 -81
- package/dist/pty-text-utils.js +79 -29
- package/dist/server-session-routes.js +192 -7
- package/dist/server.js +38 -1
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +23 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +258 -58
- package/dist/structured-session-manager.d.ts +5 -0
- package/dist/structured-session-manager.js +107 -43
- package/dist/types.d.ts +65 -0
- package/dist/web-ui/content/scripts.js +2251 -500
- package/dist/web-ui/content/styles.css +1154 -119
- package/package.json +1 -1
|
@@ -58,6 +58,337 @@
|
|
|
58
58
|
|
|
59
59
|
(function() {
|
|
60
60
|
var configPath = "${escapeHtml(configPath)}";
|
|
61
|
+
var CHAT_EXPAND_STATE_STORAGE_KEY = "wand-chat-expand-state-v1";
|
|
62
|
+
var CHAT_AUTO_FOLLOW_STORAGE_KEY = "wand-chat-auto-follow";
|
|
63
|
+
var DEFAULT_PANEL_STATE = {
|
|
64
|
+
sessionsDrawerOpen: false,
|
|
65
|
+
filePanelOpen: false,
|
|
66
|
+
shortcutsExpanded: false,
|
|
67
|
+
claudeHistoryExpanded: true,
|
|
68
|
+
chatMessageExpanded: true,
|
|
69
|
+
structuredThinkingExpanded: true,
|
|
70
|
+
structuredToolGroupExpanded: false,
|
|
71
|
+
structuredInlineToolExpanded: false,
|
|
72
|
+
structuredTerminalExpanded: false,
|
|
73
|
+
structuredToolCardExpanded: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function getConfiguredPanelDefaults(configOverride) {
|
|
77
|
+
var currentConfig = configOverride;
|
|
78
|
+
if (!currentConfig || typeof currentConfig !== "object") {
|
|
79
|
+
return {
|
|
80
|
+
sessionsDrawerOpen: DEFAULT_PANEL_STATE.sessionsDrawerOpen,
|
|
81
|
+
filePanelOpen: DEFAULT_PANEL_STATE.filePanelOpen,
|
|
82
|
+
shortcutsExpanded: DEFAULT_PANEL_STATE.shortcutsExpanded,
|
|
83
|
+
claudeHistoryExpanded: DEFAULT_PANEL_STATE.claudeHistoryExpanded,
|
|
84
|
+
chatMessageExpanded: DEFAULT_PANEL_STATE.chatMessageExpanded,
|
|
85
|
+
structuredThinkingExpanded: DEFAULT_PANEL_STATE.structuredThinkingExpanded,
|
|
86
|
+
structuredToolGroupExpanded: DEFAULT_PANEL_STATE.structuredToolGroupExpanded,
|
|
87
|
+
structuredInlineToolExpanded: DEFAULT_PANEL_STATE.structuredInlineToolExpanded,
|
|
88
|
+
structuredTerminalExpanded: DEFAULT_PANEL_STATE.structuredTerminalExpanded,
|
|
89
|
+
structuredToolCardExpanded: DEFAULT_PANEL_STATE.structuredToolCardExpanded,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
var preferences = currentConfig.uiPreferences;
|
|
93
|
+
var configured = preferences && typeof preferences === "object" ? preferences.defaultPanelState : null;
|
|
94
|
+
return {
|
|
95
|
+
sessionsDrawerOpen: configured && typeof configured.sessionsDrawerOpen === "boolean" ? configured.sessionsDrawerOpen : DEFAULT_PANEL_STATE.sessionsDrawerOpen,
|
|
96
|
+
filePanelOpen: configured && typeof configured.filePanelOpen === "boolean" ? configured.filePanelOpen : DEFAULT_PANEL_STATE.filePanelOpen,
|
|
97
|
+
shortcutsExpanded: configured && typeof configured.shortcutsExpanded === "boolean" ? configured.shortcutsExpanded : DEFAULT_PANEL_STATE.shortcutsExpanded,
|
|
98
|
+
claudeHistoryExpanded: configured && typeof configured.claudeHistoryExpanded === "boolean" ? configured.claudeHistoryExpanded : DEFAULT_PANEL_STATE.claudeHistoryExpanded,
|
|
99
|
+
chatMessageExpanded: configured && typeof configured.chatMessageExpanded === "boolean" ? configured.chatMessageExpanded : DEFAULT_PANEL_STATE.chatMessageExpanded,
|
|
100
|
+
structuredThinkingExpanded: configured && typeof configured.structuredThinkingExpanded === "boolean" ? configured.structuredThinkingExpanded : DEFAULT_PANEL_STATE.structuredThinkingExpanded,
|
|
101
|
+
structuredToolGroupExpanded: configured && typeof configured.structuredToolGroupExpanded === "boolean" ? configured.structuredToolGroupExpanded : DEFAULT_PANEL_STATE.structuredToolGroupExpanded,
|
|
102
|
+
structuredInlineToolExpanded: configured && typeof configured.structuredInlineToolExpanded === "boolean" ? configured.structuredInlineToolExpanded : DEFAULT_PANEL_STATE.structuredInlineToolExpanded,
|
|
103
|
+
structuredTerminalExpanded: configured && typeof configured.structuredTerminalExpanded === "boolean" ? configured.structuredTerminalExpanded : DEFAULT_PANEL_STATE.structuredTerminalExpanded,
|
|
104
|
+
structuredToolCardExpanded: configured && typeof configured.structuredToolCardExpanded === "boolean" ? configured.structuredToolCardExpanded : DEFAULT_PANEL_STATE.structuredToolCardExpanded,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getStoredBoolean(key) {
|
|
109
|
+
try {
|
|
110
|
+
var saved = localStorage.getItem(key);
|
|
111
|
+
if (saved === "true") return true;
|
|
112
|
+
if (saved === "false") return false;
|
|
113
|
+
return null;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getInitialPanelBoolean(key, fieldName) {
|
|
120
|
+
var stored = getStoredBoolean(key);
|
|
121
|
+
if (stored !== null) return stored;
|
|
122
|
+
return DEFAULT_PANEL_STATE[fieldName];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function persistPanelBoolean(key, value) {
|
|
126
|
+
try {
|
|
127
|
+
localStorage.setItem(key, String(!!value));
|
|
128
|
+
} catch (e) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyConfiguredPanelDefaults() {
|
|
132
|
+
var defaults = getConfiguredPanelDefaults((typeof state !== "undefined" && state) ? state.config : null);
|
|
133
|
+
if (getStoredBoolean("wand-sessions-drawer-open") === null) {
|
|
134
|
+
state.sessionsDrawerOpen = defaults.sessionsDrawerOpen;
|
|
135
|
+
}
|
|
136
|
+
if (getStoredBoolean("wand-file-panel-open") === null) {
|
|
137
|
+
state.filePanelOpen = defaults.filePanelOpen;
|
|
138
|
+
}
|
|
139
|
+
if (getStoredBoolean("wand-shortcuts-expanded") === null) {
|
|
140
|
+
state.shortcutsExpanded = defaults.shortcutsExpanded;
|
|
141
|
+
}
|
|
142
|
+
if (getStoredBoolean("wand-claude-history-expanded") === null) {
|
|
143
|
+
state.claudeHistoryExpanded = defaults.claudeHistoryExpanded;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getPanelStateSettingsFormValues() {
|
|
148
|
+
return {
|
|
149
|
+
sessionsDrawerOpen: !!((document.getElementById("cfg-panel-sessions-drawer") || {}).checked),
|
|
150
|
+
filePanelOpen: !!((document.getElementById("cfg-panel-file") || {}).checked),
|
|
151
|
+
shortcutsExpanded: !!((document.getElementById("cfg-panel-shortcuts") || {}).checked),
|
|
152
|
+
claudeHistoryExpanded: !!((document.getElementById("cfg-panel-history") || {}).checked),
|
|
153
|
+
chatMessageExpanded: !!((document.getElementById("cfg-panel-chat-message") || {}).checked),
|
|
154
|
+
structuredThinkingExpanded: !!((document.getElementById("cfg-panel-structured-thinking") || {}).checked),
|
|
155
|
+
structuredToolGroupExpanded: !!((document.getElementById("cfg-panel-structured-tool-group") || {}).checked),
|
|
156
|
+
structuredInlineToolExpanded: !!((document.getElementById("cfg-panel-structured-inline-tool") || {}).checked),
|
|
157
|
+
structuredTerminalExpanded: !!((document.getElementById("cfg-panel-structured-terminal") || {}).checked),
|
|
158
|
+
structuredToolCardExpanded: !!((document.getElementById("cfg-panel-structured-tool-card") || {}).checked),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function syncPanelStateSettingsForm(panelDefaults) {
|
|
163
|
+
var defaults = panelDefaults || getConfiguredPanelDefaults();
|
|
164
|
+
var sessionsDrawerEl = document.getElementById("cfg-panel-sessions-drawer");
|
|
165
|
+
var filePanelEl = document.getElementById("cfg-panel-file");
|
|
166
|
+
var shortcutsEl = document.getElementById("cfg-panel-shortcuts");
|
|
167
|
+
var historyEl = document.getElementById("cfg-panel-history");
|
|
168
|
+
var chatMessageEl = document.getElementById("cfg-panel-chat-message");
|
|
169
|
+
var structuredThinkingEl = document.getElementById("cfg-panel-structured-thinking");
|
|
170
|
+
var structuredToolGroupEl = document.getElementById("cfg-panel-structured-tool-group");
|
|
171
|
+
var structuredInlineToolEl = document.getElementById("cfg-panel-structured-inline-tool");
|
|
172
|
+
var structuredTerminalEl = document.getElementById("cfg-panel-structured-terminal");
|
|
173
|
+
var structuredToolCardEl = document.getElementById("cfg-panel-structured-tool-card");
|
|
174
|
+
if (sessionsDrawerEl) sessionsDrawerEl.checked = !!defaults.sessionsDrawerOpen;
|
|
175
|
+
if (filePanelEl) filePanelEl.checked = !!defaults.filePanelOpen;
|
|
176
|
+
if (shortcutsEl) shortcutsEl.checked = !!defaults.shortcutsExpanded;
|
|
177
|
+
if (historyEl) historyEl.checked = !!defaults.claudeHistoryExpanded;
|
|
178
|
+
if (chatMessageEl) chatMessageEl.checked = !!defaults.chatMessageExpanded;
|
|
179
|
+
if (structuredThinkingEl) structuredThinkingEl.checked = !!defaults.structuredThinkingExpanded;
|
|
180
|
+
if (structuredToolGroupEl) structuredToolGroupEl.checked = !!defaults.structuredToolGroupExpanded;
|
|
181
|
+
if (structuredInlineToolEl) structuredInlineToolEl.checked = !!defaults.structuredInlineToolExpanded;
|
|
182
|
+
if (structuredTerminalEl) structuredTerminalEl.checked = !!defaults.structuredTerminalExpanded;
|
|
183
|
+
if (structuredToolCardEl) structuredToolCardEl.checked = !!defaults.structuredToolCardExpanded;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function applySettingsConfig(config) {
|
|
187
|
+
state.config = config || null;
|
|
188
|
+
applyConfiguredPanelDefaults();
|
|
189
|
+
updatePanelDefaultControls();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function renderSettingsNav() {
|
|
193
|
+
return '<div class="settings-nav">' +
|
|
194
|
+
'<button class="settings-tab active" data-tab="about">关于</button>' +
|
|
195
|
+
'<button class="settings-tab" data-tab="general">基本配置</button>' +
|
|
196
|
+
'<button class="settings-tab" data-tab="security">安全</button>' +
|
|
197
|
+
'<button class="settings-tab" data-tab="presets">命令预设</button>' +
|
|
198
|
+
'</div>';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderAppearanceSettingsCard() {
|
|
202
|
+
return '<div class="settings-card settings-card-accent">' +
|
|
203
|
+
'<div class="settings-card-header">' +
|
|
204
|
+
'<div>' +
|
|
205
|
+
'<h3 class="settings-section-title">界面偏好</h3>' +
|
|
206
|
+
'<p class="settings-hint">这些选项决定新页面或未保存本地偏好的默认展开状态。</p>' +
|
|
207
|
+
'</div>' +
|
|
208
|
+
'</div>' +
|
|
209
|
+
'<div class="settings-toggle-list">' +
|
|
210
|
+
'<label class="settings-toggle-item" for="cfg-panel-sessions-drawer">' +
|
|
211
|
+
'<div><span class="settings-toggle-title">默认展开会话侧栏</span><span class="settings-toggle-desc">进入页面时左侧会话列表默认展开。</span></div>' +
|
|
212
|
+
'<input id="cfg-panel-sessions-drawer" type="checkbox" class="field-checkbox" />' +
|
|
213
|
+
'</label>' +
|
|
214
|
+
'<label class="settings-toggle-item" for="cfg-panel-file">' +
|
|
215
|
+
'<div><span class="settings-toggle-title">默认展开文件面板</span><span class="settings-toggle-desc">右侧文件浏览器在初始状态下打开。</span></div>' +
|
|
216
|
+
'<input id="cfg-panel-file" type="checkbox" class="field-checkbox" />' +
|
|
217
|
+
'</label>' +
|
|
218
|
+
'<label class="settings-toggle-item" for="cfg-panel-shortcuts">' +
|
|
219
|
+
'<div><span class="settings-toggle-title">默认展开快捷键栏</span><span class="settings-toggle-desc">移动端快捷键面板首次显示时展开完整行。</span></div>' +
|
|
220
|
+
'<input id="cfg-panel-shortcuts" type="checkbox" class="field-checkbox" />' +
|
|
221
|
+
'</label>' +
|
|
222
|
+
'<label class="settings-toggle-item" for="cfg-panel-history">' +
|
|
223
|
+
'<div><span class="settings-toggle-title">默认展开 Claude 历史</span><span class="settings-toggle-desc">侧栏里的 Claude 历史分组默认展开。</span></div>' +
|
|
224
|
+
'<input id="cfg-panel-history" type="checkbox" class="field-checkbox" />' +
|
|
225
|
+
'</label>' +
|
|
226
|
+
'<label class="settings-toggle-item" for="cfg-panel-chat-message">' +
|
|
227
|
+
'<div><span class="settings-toggle-title">默认展开聊天详情</span><span class="settings-toggle-desc">当某条消息没有本地展开记录时,采用这里的默认值。</span></div>' +
|
|
228
|
+
'<input id="cfg-panel-chat-message" type="checkbox" class="field-checkbox" />' +
|
|
229
|
+
'</label>' +
|
|
230
|
+
'<label class="settings-toggle-item" for="cfg-panel-structured-thinking">' +
|
|
231
|
+
'<div><span class="settings-toggle-title">默认展开思考卡片</span><span class="settings-toggle-desc">结构化模式中的 thinking 块默认展开。</span></div>' +
|
|
232
|
+
'<input id="cfg-panel-structured-thinking" type="checkbox" class="field-checkbox" />' +
|
|
233
|
+
'</label>' +
|
|
234
|
+
'<label class="settings-toggle-item" for="cfg-panel-structured-tool-group">' +
|
|
235
|
+
'<div><span class="settings-toggle-title">默认展开工具组</span><span class="settings-toggle-desc">连续工具调用合并后的工具组默认展开。</span></div>' +
|
|
236
|
+
'<input id="cfg-panel-structured-tool-group" type="checkbox" class="field-checkbox" />' +
|
|
237
|
+
'</label>' +
|
|
238
|
+
'<label class="settings-toggle-item" for="cfg-panel-structured-inline-tool">' +
|
|
239
|
+
'<div><span class="settings-toggle-title">默认展开内联工具</span><span class="settings-toggle-desc">Read、Grep、Glob 等内联工具结果默认展开。</span></div>' +
|
|
240
|
+
'<input id="cfg-panel-structured-inline-tool" type="checkbox" class="field-checkbox" />' +
|
|
241
|
+
'</label>' +
|
|
242
|
+
'<label class="settings-toggle-item" for="cfg-panel-structured-terminal">' +
|
|
243
|
+
'<div><span class="settings-toggle-title">默认展开终端卡片</span><span class="settings-toggle-desc">Bash 等终端输出卡片默认展开。</span></div>' +
|
|
244
|
+
'<input id="cfg-panel-structured-terminal" type="checkbox" class="field-checkbox" />' +
|
|
245
|
+
'</label>' +
|
|
246
|
+
'<label class="settings-toggle-item" for="cfg-panel-structured-tool-card">' +
|
|
247
|
+
'<div><span class="settings-toggle-title">默认展开通用工具卡</span><span class="settings-toggle-desc">工具调用、计划类卡片等通用卡片默认展开。</span></div>' +
|
|
248
|
+
'<input id="cfg-panel-structured-tool-card" type="checkbox" class="field-checkbox" />' +
|
|
249
|
+
'</label>' +
|
|
250
|
+
'</div>' +
|
|
251
|
+
'</div>';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildSettingsGeneralPanel() {
|
|
255
|
+
return '<div class="settings-panel" id="settings-tab-general">' +
|
|
256
|
+
'<div class="settings-card settings-card-accent">' +
|
|
257
|
+
'<div class="settings-card-header">' +
|
|
258
|
+
'<div>' +
|
|
259
|
+
'<h3 class="settings-section-title">基础运行配置</h3>' +
|
|
260
|
+
'<p class="settings-hint">影响服务器监听、默认模式和 CLI 行为。</p>' +
|
|
261
|
+
'</div>' +
|
|
262
|
+
'</div>' +
|
|
263
|
+
'<div class="field-row">' +
|
|
264
|
+
'<div class="field">' +
|
|
265
|
+
'<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
|
|
266
|
+
'<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
|
|
267
|
+
'</div>' +
|
|
268
|
+
'<div class="field">' +
|
|
269
|
+
'<label class="field-label" for="cfg-port">端口 (port)</label>' +
|
|
270
|
+
'<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
|
|
271
|
+
'</div>' +
|
|
272
|
+
'</div>' +
|
|
273
|
+
'<div class="field field-inline">' +
|
|
274
|
+
'<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
|
|
275
|
+
'<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
|
|
276
|
+
'</div>' +
|
|
277
|
+
'<div class="field-row">' +
|
|
278
|
+
'<div class="field">' +
|
|
279
|
+
'<label class="field-label" for="cfg-mode">默认执行模式</label>' +
|
|
280
|
+
'<select id="cfg-mode" class="field-input">' +
|
|
281
|
+
'<option value="default">default</option>' +
|
|
282
|
+
'<option value="assist">assist</option>' +
|
|
283
|
+
'<option value="agent">agent</option>' +
|
|
284
|
+
'<option value="agent-max">agent-max</option>' +
|
|
285
|
+
'<option value="auto-edit">auto-edit</option>' +
|
|
286
|
+
'<option value="full-access">full-access</option>' +
|
|
287
|
+
'<option value="native">native</option>' +
|
|
288
|
+
'<option value="managed">managed</option>' +
|
|
289
|
+
'</select>' +
|
|
290
|
+
'</div>' +
|
|
291
|
+
'<div class="field">' +
|
|
292
|
+
'<label class="field-label" for="cfg-language">回复语言</label>' +
|
|
293
|
+
'<select id="cfg-language" class="field-input">' +
|
|
294
|
+
'<option value="">自动(不指定)</option>' +
|
|
295
|
+
'<option value="中文">中文</option>' +
|
|
296
|
+
'<option value="English">English</option>' +
|
|
297
|
+
'<option value="日本語">日本語</option>' +
|
|
298
|
+
'<option value="한국어">한국어</option>' +
|
|
299
|
+
'<option value="Español">Español</option>' +
|
|
300
|
+
'<option value="Français">Français</option>' +
|
|
301
|
+
'<option value="Deutsch">Deutsch</option>' +
|
|
302
|
+
'<option value="Русский">Русский</option>' +
|
|
303
|
+
'</select>' +
|
|
304
|
+
'</div>' +
|
|
305
|
+
'</div>' +
|
|
306
|
+
'<p class="field-hint settings-inline-hint">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
307
|
+
'<div class="field">' +
|
|
308
|
+
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
309
|
+
'<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
|
|
310
|
+
'</div>' +
|
|
311
|
+
'<div class="field">' +
|
|
312
|
+
'<label class="field-label" for="cfg-shell">Shell</label>' +
|
|
313
|
+
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
314
|
+
'</div>' +
|
|
315
|
+
'</div>' +
|
|
316
|
+
renderAppearanceSettingsCard() +
|
|
317
|
+
'<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
|
|
318
|
+
'<p id="config-message" class="hint hidden"></p>' +
|
|
319
|
+
'</div>';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function persistSettingsBackedUiState() {
|
|
323
|
+
persistPanelBoolean("wand-sessions-drawer-open", state.sessionsDrawerOpen);
|
|
324
|
+
persistPanelBoolean("wand-file-panel-open", state.filePanelOpen);
|
|
325
|
+
persistPanelBoolean("wand-shortcuts-expanded", state.shortcutsExpanded);
|
|
326
|
+
persistPanelBoolean("wand-claude-history-expanded", state.claudeHistoryExpanded);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function resetSettingsBackedUiStateToDefaults() {
|
|
330
|
+
var defaults = getConfiguredPanelDefaults();
|
|
331
|
+
state.sessionsDrawerOpen = defaults.sessionsDrawerOpen;
|
|
332
|
+
state.filePanelOpen = defaults.filePanelOpen;
|
|
333
|
+
state.shortcutsExpanded = defaults.shortcutsExpanded;
|
|
334
|
+
state.claudeHistoryExpanded = defaults.claudeHistoryExpanded;
|
|
335
|
+
persistSettingsBackedUiState();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function handleSettingsConfigSaved(nextConfig) {
|
|
339
|
+
applySettingsConfig(nextConfig || state.config);
|
|
340
|
+
syncPanelStateSettingsForm();
|
|
341
|
+
resetSettingsBackedUiStateToDefaults();
|
|
342
|
+
updateLayoutState();
|
|
343
|
+
updateSessionsList();
|
|
344
|
+
updateCurrentSession();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function updateSettingsActiveNav() {
|
|
348
|
+
var activeTab = document.querySelector(".settings-tab.active");
|
|
349
|
+
var nav = document.querySelector(".settings-nav");
|
|
350
|
+
if (!nav) return;
|
|
351
|
+
if (activeTab) {
|
|
352
|
+
nav.setAttribute("data-active-tab", activeTab.getAttribute("data-tab") || "");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function updateCollapsedShortcutsUi() {
|
|
357
|
+
var wrap = document.querySelector(".inline-shortcuts-wrap");
|
|
358
|
+
var row = document.querySelector(".inline-shortcuts-expanded-row");
|
|
359
|
+
var toggle = document.querySelector(".shortcuts-toggle");
|
|
360
|
+
if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
|
|
361
|
+
if (row) row.classList.toggle("visible", state.shortcutsExpanded);
|
|
362
|
+
if (toggle) {
|
|
363
|
+
toggle.classList.toggle("active", state.shortcutsExpanded);
|
|
364
|
+
toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function updatePanelDefaultControls() {
|
|
369
|
+
syncPanelStateSettingsForm();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function persistDrawerState() {
|
|
373
|
+
persistPanelBoolean("wand-sessions-drawer-open", state.sessionsDrawerOpen);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function persistHistoryPanelState() {
|
|
377
|
+
persistPanelBoolean("wand-claude-history-expanded", state.claudeHistoryExpanded);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function persistShortcutsExpandedState() {
|
|
381
|
+
persistPanelBoolean("wand-shortcuts-expanded", state.shortcutsExpanded);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function persistFilePanelState() {
|
|
385
|
+
persistPanelBoolean("wand-file-panel-open", state.filePanelOpen);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function refreshUiAfterPanelStateChange() {
|
|
389
|
+
updateLayoutState();
|
|
390
|
+
updateSessionsList();
|
|
391
|
+
}
|
|
61
392
|
|
|
62
393
|
var state = {
|
|
63
394
|
selectedId: (function() {
|
|
@@ -75,6 +406,7 @@
|
|
|
75
406
|
_lastDomHtml: "",
|
|
76
407
|
terminalSessionId: null,
|
|
77
408
|
terminalOutput: "",
|
|
409
|
+
terminalLiveStreamSessions: {},
|
|
78
410
|
terminalViewportSize: { width: 0, height: 0 },
|
|
79
411
|
terminalAutoFollow: true,
|
|
80
412
|
terminalScrollIdleTimer: null,
|
|
@@ -90,20 +422,35 @@
|
|
|
90
422
|
inputQueue: Promise.resolve(),
|
|
91
423
|
pendingMessages: [], // WebSocket 断线期间的消息队列
|
|
92
424
|
messageQueue: [], // 用户消息排队等待发送
|
|
93
|
-
crossSessionQueue:
|
|
425
|
+
crossSessionQueue: (function() {
|
|
426
|
+
try {
|
|
427
|
+
var saved = localStorage.getItem("wand-cross-session-queue");
|
|
428
|
+
var parsed = saved ? JSON.parse(saved) : [];
|
|
429
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
430
|
+
} catch (e) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
})(), // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
|
|
94
434
|
structuredInputQueue: [], // 结构化会话同会话排队消息
|
|
95
435
|
drafts: {},
|
|
96
436
|
isSyncingInputBox: false,
|
|
97
437
|
loginPending: false,
|
|
98
438
|
loginChecked: false,
|
|
99
|
-
|
|
439
|
+
bootstrapping: true,
|
|
440
|
+
sessionsDrawerOpen: getInitialPanelBoolean("wand-sessions-drawer-open", "sessionsDrawerOpen"),
|
|
100
441
|
modalOpen: false,
|
|
101
442
|
presetValue: "",
|
|
102
443
|
cwdValue: "",
|
|
103
444
|
modeValue: "managed",
|
|
104
445
|
chatMode: "managed",
|
|
105
446
|
sessionCreateKind: "structured",
|
|
447
|
+
sessionCreateWorktree: false,
|
|
106
448
|
sessionTool: "claude",
|
|
449
|
+
activeWorktreeMergeSessionId: null,
|
|
450
|
+
worktreeMergeCheckResult: null,
|
|
451
|
+
worktreeMergeLoading: false,
|
|
452
|
+
worktreeMergeSubmitting: false,
|
|
453
|
+
worktreeMergeError: "",
|
|
107
454
|
preferredCommand: "claude",
|
|
108
455
|
structuredRunner: "claude-cli-print",
|
|
109
456
|
lastResize: { cols: 0, rows: 0 },
|
|
@@ -123,13 +470,22 @@
|
|
|
123
470
|
})(),
|
|
124
471
|
terminalBaseFontSize: 13,
|
|
125
472
|
keyboardPopupOpen: false,
|
|
126
|
-
filePanelOpen: (
|
|
473
|
+
filePanelOpen: getInitialPanelBoolean("wand-file-panel-open", "filePanelOpen"),
|
|
474
|
+
chatAutoFollow: (function() {
|
|
127
475
|
try {
|
|
128
|
-
|
|
476
|
+
var saved = localStorage.getItem(CHAT_AUTO_FOLLOW_STORAGE_KEY);
|
|
477
|
+
return saved === null ? true : saved === "true";
|
|
129
478
|
} catch (e) {
|
|
130
|
-
return
|
|
479
|
+
return true;
|
|
131
480
|
}
|
|
132
481
|
})(),
|
|
482
|
+
showChatJumpToBottom: false,
|
|
483
|
+
chatScrollThreshold: 200,
|
|
484
|
+
chatIsProgrammaticScroll: false,
|
|
485
|
+
chatScrollElement: null,
|
|
486
|
+
chatScrollHandler: null,
|
|
487
|
+
lastForegroundSyncAt: 0,
|
|
488
|
+
foregroundSyncTimer: null,
|
|
133
489
|
currentMessages: [],
|
|
134
490
|
lastRenderedHash: 0,
|
|
135
491
|
lastRenderedMsgCount: 0,
|
|
@@ -138,14 +494,14 @@
|
|
|
138
494
|
currentTask: null, // Current task title from Claude
|
|
139
495
|
terminalInteractive: false,
|
|
140
496
|
miniKeyboardVisible: false,
|
|
141
|
-
shortcutsExpanded:
|
|
497
|
+
shortcutsExpanded: getInitialPanelBoolean("wand-shortcuts-expanded", "shortcutsExpanded"),
|
|
142
498
|
modifiers: { ctrl: false, alt: false, shift: false },
|
|
143
499
|
fileSearchQuery: "",
|
|
144
500
|
fileExplorerLoading: false,
|
|
145
501
|
allFiles: [],
|
|
146
502
|
claudeHistory: [],
|
|
147
503
|
claudeHistoryLoaded: false,
|
|
148
|
-
claudeHistoryExpanded:
|
|
504
|
+
claudeHistoryExpanded: getInitialPanelBoolean("wand-claude-history-expanded", "claudeHistoryExpanded"),
|
|
149
505
|
claudeHistoryExpandedDirs: {},
|
|
150
506
|
sessionsManageMode: false,
|
|
151
507
|
selectedSessionIds: {},
|
|
@@ -244,27 +600,472 @@
|
|
|
244
600
|
}
|
|
245
601
|
}
|
|
246
602
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
603
|
+
function persistChatAutoFollow() {
|
|
604
|
+
try {
|
|
605
|
+
localStorage.setItem(CHAT_AUTO_FOLLOW_STORAGE_KEY, state.chatAutoFollow ? "true" : "false");
|
|
606
|
+
} catch (e) {
|
|
607
|
+
// Ignore localStorage errors
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function getChatScrollElement() {
|
|
612
|
+
var chatOutput = document.getElementById("chat-output");
|
|
613
|
+
if (!chatOutput) {
|
|
614
|
+
state.chatScrollElement = null;
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
var chatMessages = chatOutput.querySelector(".chat-messages");
|
|
618
|
+
if (chatMessages) {
|
|
619
|
+
state.chatScrollElement = chatMessages;
|
|
620
|
+
return chatMessages;
|
|
621
|
+
}
|
|
622
|
+
state.chatScrollElement = null;
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isChatNearBottom(chatMsgs) {
|
|
627
|
+
var el = chatMsgs || getChatScrollElement();
|
|
628
|
+
if (!el) return true;
|
|
629
|
+
return el.scrollTop < state.chatScrollThreshold;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function updateChatFollowToggleButton() {
|
|
633
|
+
var button = document.getElementById("chat-follow-toggle");
|
|
634
|
+
if (!button) return;
|
|
635
|
+
var enabled = !!state.chatAutoFollow;
|
|
636
|
+
button.classList.toggle("active", enabled);
|
|
637
|
+
button.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
638
|
+
button.setAttribute("title", enabled ? "追踪底部:开启" : "追踪底部:已暂停");
|
|
639
|
+
button.textContent = enabled ? "追底" : "暂停";
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function updateChatJumpToBottomButton() {
|
|
643
|
+
var button = document.getElementById("chat-jump-bottom");
|
|
644
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
645
|
+
var shouldShow = !!selectedSession
|
|
646
|
+
&& state.currentView === "chat"
|
|
647
|
+
&& !state.chatAutoFollow
|
|
648
|
+
&& !isChatNearBottom();
|
|
649
|
+
state.showChatJumpToBottom = shouldShow;
|
|
650
|
+
if (button) {
|
|
651
|
+
button.classList.toggle("visible", shouldShow);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function scrollChatToBottom(smooth) {
|
|
656
|
+
var chatMsgs = getChatScrollElement();
|
|
657
|
+
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
658
|
+
state.chatIsProgrammaticScroll = true;
|
|
659
|
+
if (smooth && typeof chatMsgs.scrollTo === "function") {
|
|
660
|
+
chatMsgs.scrollTo({ top: 0, behavior: "smooth" });
|
|
661
|
+
setTimeout(function() {
|
|
662
|
+
state.chatIsProgrammaticScroll = false;
|
|
663
|
+
updateChatJumpToBottomButton();
|
|
664
|
+
}, 220);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
chatMsgs.scrollTop = 0;
|
|
668
|
+
requestAnimationFrame(function() {
|
|
669
|
+
state.chatIsProgrammaticScroll = false;
|
|
670
|
+
updateChatJumpToBottomButton();
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function setChatAutoFollow(enabled, options) {
|
|
675
|
+
options = options || {};
|
|
676
|
+
state.chatAutoFollow = !!enabled;
|
|
677
|
+
persistChatAutoFollow();
|
|
678
|
+
updateChatFollowToggleButton();
|
|
679
|
+
if (state.chatAutoFollow && options.scrollNow !== false) {
|
|
680
|
+
scrollChatToBottom(!!options.smooth);
|
|
681
|
+
} else {
|
|
682
|
+
updateChatJumpToBottomButton();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function bindChatScrollListener() {
|
|
687
|
+
var chatMsgs = getChatScrollElement();
|
|
688
|
+
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
689
|
+
if (state.chatScrollElement === chatMsgs && state.chatScrollHandler) {
|
|
690
|
+
updateChatJumpToBottomButton();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
694
|
+
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
695
|
+
}
|
|
696
|
+
state.chatScrollElement = chatMsgs;
|
|
697
|
+
state.chatScrollHandler = function() {
|
|
698
|
+
if (!chatMsgs.isConnected) return;
|
|
699
|
+
if (state.chatIsProgrammaticScroll) {
|
|
700
|
+
updateChatJumpToBottomButton();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (!isChatNearBottom(chatMsgs)) {
|
|
704
|
+
if (state.chatAutoFollow) {
|
|
705
|
+
setChatAutoFollow(false, { scrollNow: false });
|
|
706
|
+
} else {
|
|
707
|
+
updateChatJumpToBottomButton();
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
updateChatJumpToBottomButton();
|
|
712
|
+
};
|
|
713
|
+
chatMsgs.addEventListener("scroll", state.chatScrollHandler, { passive: true });
|
|
714
|
+
updateChatJumpToBottomButton();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Helper function to persist selected session ID to localStorage
|
|
718
|
+
function persistSelectedId() {
|
|
719
|
+
try {
|
|
720
|
+
if (state.selectedId) {
|
|
721
|
+
localStorage.setItem("wand-selected-session", state.selectedId);
|
|
722
|
+
} else {
|
|
723
|
+
localStorage.removeItem("wand-selected-session");
|
|
724
|
+
}
|
|
725
|
+
} catch (e) {
|
|
726
|
+
// Ignore localStorage errors
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function getStructuredQueuedInputs(session) {
|
|
731
|
+
if (session && Array.isArray(session.queuedMessages)) {
|
|
732
|
+
return session.queuedMessages;
|
|
733
|
+
}
|
|
734
|
+
return state.structuredInputQueue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function getSelectedStructuredQueuedInputs() {
|
|
738
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
739
|
+
return getStructuredQueuedInputs(session);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function syncStructuredQueueFromSession(session) {
|
|
743
|
+
var queued = getStructuredQueuedInputs(session);
|
|
744
|
+
state.structuredInputQueue = Array.isArray(queued) ? queued.slice() : [];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function hasRenderOnlyStructuredBlock(message, marker) {
|
|
748
|
+
return !!(message && Array.isArray(message.content) && message.content.some(function(block) {
|
|
749
|
+
return block && typeof block === "object" && block[marker];
|
|
750
|
+
}));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function isQueuedStructuredMessage(message) {
|
|
754
|
+
return !!(message && message.role === "user" && hasRenderOnlyStructuredBlock(message, "__queued"));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function isProcessingStructuredMessage(message) {
|
|
758
|
+
return !!(message && message.role === "assistant" && hasRenderOnlyStructuredBlock(message, "__processing"));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function stripRenderOnlyStructuredMessages(messages) {
|
|
762
|
+
if (!Array.isArray(messages)) return [];
|
|
763
|
+
var removed = false;
|
|
764
|
+
var filtered = [];
|
|
765
|
+
for (var i = 0; i < messages.length; i++) {
|
|
766
|
+
var message = messages[i];
|
|
767
|
+
if (isQueuedStructuredMessage(message) || isProcessingStructuredMessage(message)) {
|
|
768
|
+
removed = true;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
filtered.push(message);
|
|
772
|
+
}
|
|
773
|
+
return removed ? filtered : messages;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function normalizeStructuredSnapshot(snapshot, existingSession) {
|
|
777
|
+
if (!snapshot || !Array.isArray(snapshot.messages)) {
|
|
778
|
+
return snapshot;
|
|
779
|
+
}
|
|
780
|
+
var sessionKind = snapshot.sessionKind || (existingSession && existingSession.sessionKind);
|
|
781
|
+
if (sessionKind !== "structured") {
|
|
782
|
+
return snapshot;
|
|
783
|
+
}
|
|
784
|
+
var sanitizedMessages = stripRenderOnlyStructuredMessages(snapshot.messages);
|
|
785
|
+
if (sanitizedMessages === snapshot.messages) {
|
|
786
|
+
return snapshot;
|
|
787
|
+
}
|
|
788
|
+
return Object.assign({}, snapshot, { messages: sanitizedMessages });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function saveStructuredQueue() {
|
|
792
|
+
try {
|
|
793
|
+
var queued = getSelectedStructuredQueuedInputs();
|
|
794
|
+
if (!state.selectedId || queued.length === 0) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
localStorage.setItem("wand-structured-queue", JSON.stringify({
|
|
798
|
+
sessionId: state.selectedId,
|
|
799
|
+
items: queued
|
|
800
|
+
}));
|
|
801
|
+
} catch (e) {
|
|
802
|
+
// Ignore localStorage errors
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function clearStructuredQueuePersistence(sessionId) {
|
|
807
|
+
try {
|
|
808
|
+
var saved = localStorage.getItem("wand-structured-queue");
|
|
809
|
+
if (!saved) return;
|
|
810
|
+
var parsed = JSON.parse(saved);
|
|
811
|
+
if (!sessionId || !parsed || parsed.sessionId === sessionId) {
|
|
812
|
+
localStorage.removeItem("wand-structured-queue");
|
|
813
|
+
}
|
|
814
|
+
} catch (e) {
|
|
815
|
+
localStorage.removeItem("wand-structured-queue");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function restoreStructuredQueue() {
|
|
820
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
821
|
+
if (selectedSession && Array.isArray(selectedSession.queuedMessages)) {
|
|
822
|
+
syncStructuredQueueFromSession(selectedSession);
|
|
823
|
+
saveStructuredQueue();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
var saved = localStorage.getItem("wand-structured-queue");
|
|
828
|
+
if (!saved) return;
|
|
829
|
+
var parsed = JSON.parse(saved);
|
|
830
|
+
if (!parsed || parsed.sessionId !== state.selectedId || !Array.isArray(parsed.items)) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
state.structuredInputQueue = parsed.items.slice(0, 10);
|
|
834
|
+
} catch (e) {
|
|
835
|
+
state.structuredInputQueue = [];
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function persistCrossSessionQueue() {
|
|
840
|
+
try {
|
|
841
|
+
if (state.crossSessionQueue.length === 0) {
|
|
842
|
+
localStorage.removeItem("wand-cross-session-queue");
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
localStorage.setItem("wand-cross-session-queue", JSON.stringify(state.crossSessionQueue));
|
|
846
|
+
} catch (e) {
|
|
847
|
+
// Ignore localStorage errors
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function getConfigCwd() {
|
|
852
|
+
return (state.config && state.config.defaultCwd) || "/tmp";
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function loadChatExpandStateMap() {
|
|
856
|
+
try {
|
|
857
|
+
var saved = localStorage.getItem(CHAT_EXPAND_STATE_STORAGE_KEY);
|
|
858
|
+
if (!saved) return {};
|
|
859
|
+
var parsed = JSON.parse(saved);
|
|
860
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
861
|
+
} catch (e) {
|
|
862
|
+
return {};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function getDefaultChatMessageExpanded() {
|
|
867
|
+
return getConfiguredPanelDefaults().chatMessageExpanded;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function getDefaultStructuredCardExpanded(cardType, fallbackValue) {
|
|
871
|
+
var defaults = getConfiguredPanelDefaults();
|
|
872
|
+
if (cardType === "thinking") {
|
|
873
|
+
return defaults.structuredThinkingExpanded;
|
|
874
|
+
}
|
|
875
|
+
if (cardType === "tool-group") {
|
|
876
|
+
return defaults.structuredToolGroupExpanded;
|
|
877
|
+
}
|
|
878
|
+
if (cardType === "inline-tool") {
|
|
879
|
+
return defaults.structuredInlineToolExpanded;
|
|
880
|
+
}
|
|
881
|
+
if (cardType === "terminal") {
|
|
882
|
+
return defaults.structuredTerminalExpanded;
|
|
883
|
+
}
|
|
884
|
+
if (cardType === "tool-card") {
|
|
885
|
+
return defaults.structuredToolCardExpanded;
|
|
886
|
+
}
|
|
887
|
+
if (typeof fallbackValue === "boolean") {
|
|
888
|
+
return fallbackValue;
|
|
889
|
+
}
|
|
890
|
+
return defaults.chatMessageExpanded;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function hasPersistedExpandState(itemKey) {
|
|
894
|
+
if (!itemKey || !state.selectedId) return false;
|
|
895
|
+
var sessionState = getCurrentChatExpandState();
|
|
896
|
+
return typeof sessionState[itemKey] === "boolean";
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function getExpandState(itemKey, cardType, fallbackValue) {
|
|
900
|
+
if (!itemKey || !state.selectedId) {
|
|
901
|
+
if (typeof fallbackValue === "boolean") return fallbackValue;
|
|
902
|
+
return getDefaultStructuredCardExpanded(cardType, fallbackValue);
|
|
903
|
+
}
|
|
904
|
+
var sessionState = getCurrentChatExpandState();
|
|
905
|
+
if (typeof sessionState[itemKey] === "boolean") return sessionState[itemKey];
|
|
906
|
+
return getDefaultStructuredCardExpanded(cardType, fallbackValue);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function getPersistedExpandState(itemKey) {
|
|
910
|
+
if (!itemKey || !state.selectedId) return null;
|
|
911
|
+
var sessionState = getCurrentChatExpandState();
|
|
912
|
+
if (typeof sessionState[itemKey] === "boolean") return sessionState[itemKey];
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function saveChatExpandStateMap(map) {
|
|
917
|
+
try {
|
|
918
|
+
if (!map || Object.keys(map).length === 0) {
|
|
919
|
+
localStorage.removeItem(CHAT_EXPAND_STATE_STORAGE_KEY);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
localStorage.setItem(CHAT_EXPAND_STATE_STORAGE_KEY, JSON.stringify(map));
|
|
923
|
+
} catch (e) {
|
|
924
|
+
// Ignore localStorage errors
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function getCurrentChatExpandState() {
|
|
929
|
+
var sessionId = state.selectedId;
|
|
930
|
+
if (!sessionId) return {};
|
|
931
|
+
var map = loadChatExpandStateMap();
|
|
932
|
+
var sessionState = map[sessionId];
|
|
933
|
+
return sessionState && typeof sessionState === "object" ? sessionState : {};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function setPersistedExpandState(itemKey, expanded) {
|
|
937
|
+
if (!itemKey || !state.selectedId) return;
|
|
938
|
+
var map = loadChatExpandStateMap();
|
|
939
|
+
var sessionId = state.selectedId;
|
|
940
|
+
var sessionState = map[sessionId];
|
|
941
|
+
if (!sessionState || typeof sessionState !== "object") {
|
|
942
|
+
sessionState = {};
|
|
943
|
+
}
|
|
944
|
+
sessionState[itemKey] = !!expanded;
|
|
945
|
+
map[sessionId] = sessionState;
|
|
946
|
+
saveChatExpandStateMap(map);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function getMessageKey(msg, fallbackIndex) {
|
|
950
|
+
if (!msg) {
|
|
951
|
+
return "msg:unknown-" + (typeof fallbackIndex === "number" ? fallbackIndex : 0);
|
|
952
|
+
}
|
|
953
|
+
if (msg.uuid) return "msg:" + msg.uuid;
|
|
954
|
+
if (msg.id) return "msg:" + msg.id;
|
|
955
|
+
if (msg.messageId) return "msg:" + msg.messageId;
|
|
956
|
+
if (msg.turnId) return "msg:" + msg.turnId;
|
|
957
|
+
return "msg:" + (typeof fallbackIndex === "number" ? fallbackIndex : 0);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function buildExpandKey(kind, parts) {
|
|
961
|
+
var filtered = [];
|
|
962
|
+
for (var i = 0; i < parts.length; i++) {
|
|
963
|
+
var part = parts[i];
|
|
964
|
+
if (part === undefined || part === null || part === "") continue;
|
|
965
|
+
filtered.push(String(part));
|
|
966
|
+
}
|
|
967
|
+
return kind + ":" + filtered.join(":");
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function getElementExpandKey(el) {
|
|
971
|
+
if (!el || !el.dataset) return "";
|
|
972
|
+
return el.dataset.expandKey || "";
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function isElementExpanded(el, kind) {
|
|
976
|
+
if (!el) return false;
|
|
977
|
+
switch (kind) {
|
|
978
|
+
case "tool-card":
|
|
979
|
+
return !el.classList.contains("collapsed");
|
|
980
|
+
case "thinking":
|
|
981
|
+
return el.classList.contains("expanded") && !el.classList.contains("collapsed");
|
|
982
|
+
case "inline-tool":
|
|
983
|
+
return el.classList.contains("inline-tool-open");
|
|
984
|
+
case "terminal": {
|
|
985
|
+
var body = el.querySelector(".term-body");
|
|
986
|
+
if (body) return body.style.display !== "none";
|
|
987
|
+
return el.dataset.expanded === "true";
|
|
988
|
+
}
|
|
989
|
+
case "tool-group":
|
|
990
|
+
return el.getAttribute("data-expanded") === "true";
|
|
991
|
+
default:
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function applyExpandedState(el, kind, expanded) {
|
|
997
|
+
if (!el) return;
|
|
998
|
+
switch (kind) {
|
|
999
|
+
case "tool-card": {
|
|
1000
|
+
el.classList.toggle("collapsed", !expanded);
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
case "thinking": {
|
|
1004
|
+
el.classList.toggle("collapsed", !expanded);
|
|
1005
|
+
el.classList.toggle("expanded", !!expanded);
|
|
1006
|
+
var previewEl = el.querySelector(".thinking-inline-preview");
|
|
1007
|
+
if (previewEl) {
|
|
1008
|
+
var fullText = el.dataset.thinking || "";
|
|
1009
|
+
var preview = fullText.slice(0, 57) + (fullText.length > 60 ? "…" : "");
|
|
1010
|
+
previewEl.textContent = expanded ? fullText : preview;
|
|
1011
|
+
}
|
|
1012
|
+
var actionEl = el.querySelector(".thinking-inline-action");
|
|
1013
|
+
if (actionEl) actionEl.textContent = expanded ? "收起" : "展开";
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
case "inline-tool": {
|
|
1017
|
+
el.classList.toggle("inline-tool-open", !!expanded);
|
|
1018
|
+
var inlineBody = el.querySelector(".inline-tool-expanded");
|
|
1019
|
+
if (inlineBody) inlineBody.style.display = expanded ? "block" : "none";
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
case "terminal": {
|
|
1023
|
+
var body = el.querySelector(".term-body");
|
|
1024
|
+
if (body) body.style.display = expanded ? "block" : "none";
|
|
1025
|
+
el.dataset.expanded = expanded ? "true" : "false";
|
|
1026
|
+
var toggleIcon = el.querySelector(".term-toggle-icon");
|
|
1027
|
+
if (toggleIcon) toggleIcon.textContent = expanded ? "▼" : "▶";
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
case "tool-group": {
|
|
1031
|
+
el.setAttribute("data-expanded", expanded ? "true" : "false");
|
|
1032
|
+
var groupBody = el.querySelector(".tool-group-body");
|
|
1033
|
+
if (groupBody) groupBody.style.display = expanded ? "block" : "none";
|
|
1034
|
+
var chevron = el.querySelector(".tool-group-chevron");
|
|
1035
|
+
if (chevron) chevron.style.transform = expanded ? "rotate(180deg)" : "";
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function persistElementExpandState(el, kind) {
|
|
1042
|
+
var itemKey = getElementExpandKey(el);
|
|
1043
|
+
if (!itemKey) return;
|
|
1044
|
+
setPersistedExpandState(itemKey, isElementExpanded(el, kind));
|
|
258
1045
|
}
|
|
259
1046
|
|
|
260
|
-
function
|
|
261
|
-
|
|
1047
|
+
function applyPersistedExpandState(container) {
|
|
1048
|
+
if (!container || !state.selectedId) return;
|
|
1049
|
+
container.querySelectorAll("[data-expand-key]").forEach(function(el) {
|
|
1050
|
+
var itemKey = getElementExpandKey(el);
|
|
1051
|
+
var kind = el.dataset.expandKind || "";
|
|
1052
|
+
if (!kind || !hasPersistedExpandState(itemKey)) return;
|
|
1053
|
+
applyExpandedState(el, kind, getPersistedExpandState(itemKey));
|
|
1054
|
+
});
|
|
262
1055
|
}
|
|
263
1056
|
|
|
264
1057
|
function resetChatRenderCache() {
|
|
265
1058
|
state.lastRenderedHash = 0;
|
|
266
1059
|
state.lastRenderedMsgCount = 0;
|
|
267
1060
|
state.lastRenderedEmpty = null;
|
|
1061
|
+
state.renderPending = false;
|
|
1062
|
+
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
1063
|
+
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
1064
|
+
}
|
|
1065
|
+
state.chatScrollElement = null;
|
|
1066
|
+
state.chatScrollHandler = null;
|
|
1067
|
+
state.showChatJumpToBottom = false;
|
|
1068
|
+
state.chatIsProgrammaticScroll = false;
|
|
268
1069
|
}
|
|
269
1070
|
|
|
270
1071
|
function getEffectiveCwd() {
|
|
@@ -314,9 +1115,6 @@
|
|
|
314
1115
|
}
|
|
315
1116
|
}
|
|
316
1117
|
|
|
317
|
-
renderBootLoading();
|
|
318
|
-
restoreLoginSession();
|
|
319
|
-
|
|
320
1118
|
function renderBootLoading() {
|
|
321
1119
|
var app = document.getElementById("app");
|
|
322
1120
|
if (!app) return;
|
|
@@ -324,11 +1122,65 @@
|
|
|
324
1122
|
'<div class="boot-loading">' +
|
|
325
1123
|
'<div class="boot-loading-card">' +
|
|
326
1124
|
'<div class="boot-loading-spinner"></div>' +
|
|
327
|
-
'<div class="boot-loading-text"
|
|
1125
|
+
'<div class="boot-loading-text">正在连接 Wand…</div>' +
|
|
328
1126
|
'</div>' +
|
|
329
1127
|
'</div>';
|
|
330
1128
|
}
|
|
331
1129
|
|
|
1130
|
+
function scheduleForegroundSync(reason) {
|
|
1131
|
+
if (!state.config) return;
|
|
1132
|
+
if (document.hidden) return;
|
|
1133
|
+
var now = Date.now();
|
|
1134
|
+
if (now - state.lastForegroundSyncAt < 1500) return;
|
|
1135
|
+
state.lastForegroundSyncAt = now;
|
|
1136
|
+
if (state.foregroundSyncTimer) {
|
|
1137
|
+
clearTimeout(state.foregroundSyncTimer);
|
|
1138
|
+
}
|
|
1139
|
+
state.foregroundSyncTimer = setTimeout(function() {
|
|
1140
|
+
state.foregroundSyncTimer = null;
|
|
1141
|
+
syncOnForeground(reason);
|
|
1142
|
+
}, 80);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function syncOnForeground(reason) {
|
|
1146
|
+
if (!state.config) return Promise.resolve();
|
|
1147
|
+
if (document.hidden) return Promise.resolve();
|
|
1148
|
+
if (!state.ws || (state.ws.readyState !== WebSocket.OPEN && state.ws.readyState !== WebSocket.CONNECTING)) {
|
|
1149
|
+
initWebSocket();
|
|
1150
|
+
}
|
|
1151
|
+
return loadSessions({ skipSelectedOutputReload: true }).then(function() {
|
|
1152
|
+
if (state.selectedId) {
|
|
1153
|
+
return loadOutput(state.selectedId);
|
|
1154
|
+
}
|
|
1155
|
+
scheduleChatRender(true);
|
|
1156
|
+
}).catch(function(e) {
|
|
1157
|
+
console.error("[wand] foreground sync failed:", reason, e);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function bindForegroundSyncListeners() {
|
|
1162
|
+
if (window.__wandForegroundSyncBound) return;
|
|
1163
|
+
window.__wandForegroundSyncBound = true;
|
|
1164
|
+
|
|
1165
|
+
document.addEventListener("visibilitychange", function() {
|
|
1166
|
+
if (!document.hidden) {
|
|
1167
|
+
scheduleForegroundSync("visibility");
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
window.addEventListener("focus", function() {
|
|
1172
|
+
scheduleForegroundSync("focus");
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
window.addEventListener("pageshow", function() {
|
|
1176
|
+
scheduleForegroundSync("pageshow");
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
window.addEventListener("resume", function() {
|
|
1180
|
+
scheduleForegroundSync("resume");
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
332
1184
|
function restoreLoginSession() {
|
|
333
1185
|
fetch("/api/config", { credentials: "same-origin" })
|
|
334
1186
|
.then(function(res) {
|
|
@@ -341,23 +1193,19 @@
|
|
|
341
1193
|
})
|
|
342
1194
|
.then(function(config) {
|
|
343
1195
|
if (!config) return;
|
|
344
|
-
|
|
1196
|
+
applySettingsConfig(config);
|
|
345
1197
|
state.loginChecked = true;
|
|
346
1198
|
requestAnimationFrame(function() {
|
|
347
|
-
// Render the app shell first, THEN load session data into it.
|
|
348
|
-
// Skip updateShellChrome() here — sessions aren't loaded yet.
|
|
349
|
-
// refreshAll() will call updateShellChrome() after sessions arrive.
|
|
350
1199
|
try {
|
|
351
1200
|
render({ skipShellChrome: true });
|
|
352
1201
|
} catch (_e) {
|
|
353
1202
|
// render() may fail if external scripts (xterm.js) failed to load;
|
|
354
1203
|
// continue with polling and session loading so the app remains functional
|
|
355
1204
|
}
|
|
1205
|
+
bindForegroundSyncListeners();
|
|
356
1206
|
startPolling();
|
|
357
1207
|
refreshAll();
|
|
358
|
-
// Request browser notification permission after login
|
|
359
1208
|
requestNotificationPermission();
|
|
360
|
-
// Show update bubble if server reports an available update
|
|
361
1209
|
if (config.updateAvailable && config.latestVersion) {
|
|
362
1210
|
showNotificationBubble({
|
|
363
1211
|
title: "\u53d1\u73b0\u65b0\u7248\u672c",
|
|
@@ -373,7 +1221,6 @@
|
|
|
373
1221
|
});
|
|
374
1222
|
sendBrowserNotification("Wand \u53d1\u73b0\u65b0\u7248\u672c", "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion, { tag: "wand-update" });
|
|
375
1223
|
}
|
|
376
|
-
// Auto-load claude history since section defaults to expanded
|
|
377
1224
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
378
1225
|
loadClaudeHistory();
|
|
379
1226
|
}
|
|
@@ -381,7 +1228,6 @@
|
|
|
381
1228
|
})
|
|
382
1229
|
.catch(function() {
|
|
383
1230
|
state.loginChecked = true;
|
|
384
|
-
// If offline (no network), show a friendly offline message instead of login
|
|
385
1231
|
if (!navigator.onLine) {
|
|
386
1232
|
var app = document.getElementById("app");
|
|
387
1233
|
if (app) {
|
|
@@ -394,7 +1240,6 @@
|
|
|
394
1240
|
'</div>' +
|
|
395
1241
|
'</div>';
|
|
396
1242
|
}
|
|
397
|
-
// Retry when network comes back
|
|
398
1243
|
window.addEventListener('online', function() { location.reload(); }, { once: true });
|
|
399
1244
|
return;
|
|
400
1245
|
}
|
|
@@ -402,6 +1247,9 @@
|
|
|
402
1247
|
});
|
|
403
1248
|
}
|
|
404
1249
|
|
|
1250
|
+
renderBootLoading();
|
|
1251
|
+
restoreLoginSession();
|
|
1252
|
+
|
|
405
1253
|
function render(options) {
|
|
406
1254
|
var skipShellChrome = options && options.skipShellChrome;
|
|
407
1255
|
var app = document.getElementById("app");
|
|
@@ -565,12 +1413,9 @@
|
|
|
565
1413
|
var preferredTool = getComposerTool();
|
|
566
1414
|
var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
|
|
567
1415
|
|
|
1416
|
+
var showTerminalHeaderControls = !!selectedSession && state.currentView === "terminal";
|
|
1417
|
+
var showChatHeaderControls = !!selectedSession && state.currentView !== "terminal";
|
|
568
1418
|
return '<div class="app-container">' +
|
|
569
|
-
'<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="Toggle sidebar">' +
|
|
570
|
-
'<span class="hamburger-icon">' +
|
|
571
|
-
'<span></span><span></span><span></span>' +
|
|
572
|
-
'</span>' +
|
|
573
|
-
'</button>' +
|
|
574
1419
|
'<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
|
|
575
1420
|
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + '">' +
|
|
576
1421
|
'<aside id="sessions-drawer" class="sidebar' + drawerClass + '">' +
|
|
@@ -612,8 +1457,37 @@
|
|
|
612
1457
|
'</div>' +
|
|
613
1458
|
'</aside>' +
|
|
614
1459
|
'<main class="main-content">' +
|
|
615
|
-
'<
|
|
616
|
-
|
|
1460
|
+
'<div class="main-content-header">' +
|
|
1461
|
+
'<div class="main-content-header-left">' +
|
|
1462
|
+
'<button id="sessions-toggle-button" class="main-header-btn menu-toggle-btn' + (state.sessionsDrawerOpen ? ' active' : '') + '" type="button" aria-label="打开菜单" title="菜单">' +
|
|
1463
|
+
'<span class="hamburger-icon">' +
|
|
1464
|
+
'<span></span><span></span><span></span>' +
|
|
1465
|
+
'</span>' +
|
|
1466
|
+
'</button>' +
|
|
1467
|
+
'<span class="current-task hidden" id="current-task"></span>' +
|
|
1468
|
+
'</div>' +
|
|
1469
|
+
'<div class="main-content-header-center">' +
|
|
1470
|
+
'<div class="view-toggle-bar' + (state.selectedId ? '' : ' hidden') + '" id="view-toggle-bar">' +
|
|
1471
|
+
'<button id="view-terminal-btn" class="topbar-btn' + (state.currentView === "terminal" ? ' active' : '') + '" type="button" title="查看原始终端输出">终端</button>' +
|
|
1472
|
+
'<button id="view-chat-btn" class="topbar-btn' + (state.currentView !== "terminal" ? ' active' : '') + '" type="button" title="查看聊天解析视图">聊天</button>' +
|
|
1473
|
+
'</div>' +
|
|
1474
|
+
'</div>' +
|
|
1475
|
+
'<div class="main-content-header-right">' +
|
|
1476
|
+
'<div class="main-header-controls' + (showTerminalHeaderControls ? '' : ' hidden') + '" id="terminal-header-controls">' +
|
|
1477
|
+
'<button id="terminal-scale-down-top" class="main-header-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
|
|
1478
|
+
'<span class="main-header-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
|
|
1479
|
+
'<button id="terminal-scale-up-top" class="main-header-btn terminal-scale-btn" type="button" title="放大">+</button>' +
|
|
1480
|
+
'<button id="page-refresh-btn" class="main-header-btn" type="button" title="刷新页面">↻</button>' +
|
|
1481
|
+
'<button id="terminal-jump-bottom" class="main-header-btn jump-latest-btn' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
|
|
1482
|
+
'</div>' +
|
|
1483
|
+
'<div class="main-header-controls' + (showChatHeaderControls ? '' : ' hidden') + '" id="chat-header-controls">' +
|
|
1484
|
+
'<button id="chat-follow-toggle" class="chat-follow-toggle topbar-btn' + (state.chatAutoFollow ? ' active' : '') + '" type="button" aria-pressed="' + (state.chatAutoFollow ? 'true' : 'false') + '" title="' + (state.chatAutoFollow ? '追踪底部:开启' : '追踪底部:已暂停') + '">' + (state.chatAutoFollow ? '追底' : '暂停') + '</button>' +
|
|
1485
|
+
'<button id="chat-jump-bottom" class="main-header-btn jump-latest-btn' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底">↓ 最新</button>' +
|
|
1486
|
+
'</div>' +
|
|
1487
|
+
'<button id="topbar-new-session-button" class="main-header-btn main-header-new-session" type="button" title="新对话">+ 新对话</button>' +
|
|
1488
|
+
'</div>' +
|
|
1489
|
+
'</div>' +
|
|
1490
|
+
'<div class="main-content-body">' +
|
|
617
1491
|
// File panel backdrop (mobile)
|
|
618
1492
|
'<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
|
|
619
1493
|
// File side panel
|
|
@@ -634,16 +1508,7 @@
|
|
|
634
1508
|
'<div class="file-explorer" id="file-explorer">' + renderFileExplorer(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '</div>' +
|
|
635
1509
|
'</div>' +
|
|
636
1510
|
'</div>' +
|
|
637
|
-
'<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active">' +
|
|
638
|
-
'<div class="terminal-scale-overlay" aria-label="终端缩放控件">' +
|
|
639
|
-
'<button id="terminal-scale-down-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
|
|
640
|
-
'<span class="terminal-scale-overlay-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
|
|
641
|
-
'<button id="terminal-scale-up-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="放大">+</button>' +
|
|
642
|
-
'<span class="terminal-scale-overlay-divider"></span>' +
|
|
643
|
-
'<button id="page-refresh-btn" class="terminal-scale-overlay-btn" type="button" title="刷新页面">↻</button>' +
|
|
644
|
-
'</div>' +
|
|
645
|
-
'<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
|
|
646
|
-
'</div>' +
|
|
1511
|
+
'<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active"></div>' +
|
|
647
1512
|
'<div id="chat-output" class="chat-container hidden"></div>' +
|
|
648
1513
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
649
1514
|
'<div class="blank-chat-inner">' +
|
|
@@ -654,6 +1519,9 @@
|
|
|
654
1519
|
'<button class="blank-chat-tool-btn" id="welcome-tool-claude" type="button">' +
|
|
655
1520
|
'<span class="tool-icon">🤖</span>新建终端会话' +
|
|
656
1521
|
'</button>' +
|
|
1522
|
+
'<button class="blank-chat-tool-btn" id="welcome-tool-codex" type="button">' +
|
|
1523
|
+
'<span class="tool-icon">⌘</span>新建 Codex 会话' +
|
|
1524
|
+
'</button>' +
|
|
657
1525
|
'<button class="blank-chat-tool-btn" id="welcome-tool-structured" type="button">' +
|
|
658
1526
|
'<span class="tool-icon">💬</span>新建结构化会话' +
|
|
659
1527
|
'</button>' +
|
|
@@ -683,7 +1551,7 @@
|
|
|
683
1551
|
'</div>' +
|
|
684
1552
|
'</div>' +
|
|
685
1553
|
'<div class="input-composer">' +
|
|
686
|
-
'<textarea id="input-box" class="input-textarea" placeholder="' + (state.terminalInteractive
|
|
1554
|
+
'<textarea id="input-box" class="input-textarea" placeholder="' + getComposerPlaceholder(selectedSession, state.terminalInteractive) + '" rows="1">' + escapeHtml(currentDraft) + '</textarea>' +
|
|
687
1555
|
'<div class="input-composer-bar">' +
|
|
688
1556
|
'<div class="input-composer-left">' +
|
|
689
1557
|
'<select id="chat-mode-select" class="chat-mode-select" title="仅对新建会话生效">' +
|
|
@@ -721,7 +1589,7 @@
|
|
|
721
1589
|
'<span id="session-kind-display" class="session-kind-display">' + (selectedSession ? getSessionKindLabel(selectedSession) : '终端') + '</span>' +
|
|
722
1590
|
'<span class="session-info-separator">|</span>' +
|
|
723
1591
|
'<span id="session-status-display" class="session-status-display">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '-') + '</span>' +
|
|
724
|
-
(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>' : '') +
|
|
1592
|
+
(selectedSession && selectedSession.provider === "claude" && 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>' : '') +
|
|
725
1593
|
(selectedSession && !isStructuredSession(selectedSession) ? '<span class="session-info-separator">|</span><span id="session-exit-display" class="session-exit-display">退出码=' + (selectedSession.exitCode !== undefined ? selectedSession.exitCode : 'n/a') + '</span>' : '') +
|
|
726
1594
|
'</div>' +
|
|
727
1595
|
'</div>' +
|
|
@@ -751,7 +1619,29 @@
|
|
|
751
1619
|
'</section>' +
|
|
752
1620
|
'</main>' +
|
|
753
1621
|
'</div>' +
|
|
754
|
-
'</div>' + renderSessionModal() + renderSettingsModal();
|
|
1622
|
+
'</div>' + renderSessionModal() + renderWorktreeMergeModal() + renderSettingsModal();
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function renderWorktreeMergeModal() {
|
|
1626
|
+
return '<section id="worktree-merge-modal" class="modal-backdrop hidden">' +
|
|
1627
|
+
'<div class="modal worktree-merge-modal">' +
|
|
1628
|
+
'<div class="modal-header">' +
|
|
1629
|
+
'<div>' +
|
|
1630
|
+
'<h2 class="modal-title">合并 Worktree</h2>' +
|
|
1631
|
+
'<p class="modal-subtitle">检查当前任务分支并快捷合并到主分支。</p>' +
|
|
1632
|
+
'</div>' +
|
|
1633
|
+
'<button id="close-worktree-merge-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
1634
|
+
'</div>' +
|
|
1635
|
+
'<div class="modal-body">' +
|
|
1636
|
+
'<div id="worktree-merge-content" class="worktree-merge-content"></div>' +
|
|
1637
|
+
'<p id="worktree-merge-error" class="error-message hidden"></p>' +
|
|
1638
|
+
'<div class="worktree-merge-actions">' +
|
|
1639
|
+
'<button id="worktree-merge-cancel-button" class="btn btn-secondary">取消</button>' +
|
|
1640
|
+
'<button id="worktree-merge-confirm-button" class="btn btn-primary">确认合并并清理</button>' +
|
|
1641
|
+
'</div>' +
|
|
1642
|
+
'</div>' +
|
|
1643
|
+
'</div>' +
|
|
1644
|
+
'</section>';
|
|
755
1645
|
}
|
|
756
1646
|
|
|
757
1647
|
function renderSettingsModal() {
|
|
@@ -761,16 +1651,9 @@
|
|
|
761
1651
|
'<h2 class="modal-title">设置</h2>' +
|
|
762
1652
|
'<button id="close-settings-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
763
1653
|
'</div>' +
|
|
764
|
-
'<div class="modal-body">' +
|
|
765
|
-
|
|
766
|
-
'<div class="settings-
|
|
767
|
-
'<button class="settings-tab active" data-tab="about">关于</button>' +
|
|
768
|
-
'<button class="settings-tab" data-tab="general">基本配置</button>' +
|
|
769
|
-
'<button class="settings-tab" data-tab="security">安全</button>' +
|
|
770
|
-
'<button class="settings-tab" data-tab="presets">命令预设</button>' +
|
|
771
|
-
'</div>' +
|
|
772
|
-
|
|
773
|
-
// About tab
|
|
1654
|
+
'<div class="modal-body settings-layout">' +
|
|
1655
|
+
renderSettingsNav() +
|
|
1656
|
+
'<div class="settings-content">' +
|
|
774
1657
|
'<div class="settings-panel active" id="settings-tab-about">' +
|
|
775
1658
|
'<div class="settings-about-info">' +
|
|
776
1659
|
'<div class="settings-about-row"><span class="settings-label">包名</span><span class="settings-value" id="settings-pkg-name">-</span></div>' +
|
|
@@ -803,63 +1686,7 @@
|
|
|
803
1686
|
'</div>' +
|
|
804
1687
|
'</div>' +
|
|
805
1688
|
|
|
806
|
-
|
|
807
|
-
'<div class="settings-panel" id="settings-tab-general">' +
|
|
808
|
-
'<div class="field-row">' +
|
|
809
|
-
'<div class="field">' +
|
|
810
|
-
'<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
|
|
811
|
-
'<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
|
|
812
|
-
'</div>' +
|
|
813
|
-
'<div class="field">' +
|
|
814
|
-
'<label class="field-label" for="cfg-port">端口 (port)</label>' +
|
|
815
|
-
'<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
|
|
816
|
-
'</div>' +
|
|
817
|
-
'</div>' +
|
|
818
|
-
'<div class="field field-inline">' +
|
|
819
|
-
'<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
|
|
820
|
-
'<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
|
|
821
|
-
'</div>' +
|
|
822
|
-
'<div class="field-row">' +
|
|
823
|
-
'<div class="field">' +
|
|
824
|
-
'<label class="field-label" for="cfg-mode">默认执行模式</label>' +
|
|
825
|
-
'<select id="cfg-mode" class="field-input">' +
|
|
826
|
-
'<option value="default">default</option>' +
|
|
827
|
-
'<option value="assist">assist</option>' +
|
|
828
|
-
'<option value="agent">agent</option>' +
|
|
829
|
-
'<option value="agent-max">agent-max</option>' +
|
|
830
|
-
'<option value="auto-edit">auto-edit</option>' +
|
|
831
|
-
'<option value="full-access">full-access</option>' +
|
|
832
|
-
'<option value="native">native</option>' +
|
|
833
|
-
'<option value="managed">managed</option>' +
|
|
834
|
-
'</select>' +
|
|
835
|
-
'</div>' +
|
|
836
|
-
'<div class="field">' +
|
|
837
|
-
'<label class="field-label" for="cfg-language">回复语言</label>' +
|
|
838
|
-
'<select id="cfg-language" class="field-input">' +
|
|
839
|
-
'<option value="">自动(不指定)</option>' +
|
|
840
|
-
'<option value="中文">中文</option>' +
|
|
841
|
-
'<option value="English">English</option>' +
|
|
842
|
-
'<option value="日本語">日本語</option>' +
|
|
843
|
-
'<option value="한국어">한국어</option>' +
|
|
844
|
-
'<option value="Español">Español</option>' +
|
|
845
|
-
'<option value="Français">Français</option>' +
|
|
846
|
-
'<option value="Deutsch">Deutsch</option>' +
|
|
847
|
-
'<option value="Русский">Русский</option>' +
|
|
848
|
-
'</select>' +
|
|
849
|
-
'</div>' +
|
|
850
|
-
'</div>' +
|
|
851
|
-
'<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
852
|
-
'<div class="field">' +
|
|
853
|
-
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
854
|
-
'<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
|
|
855
|
-
'</div>' +
|
|
856
|
-
'<div class="field">' +
|
|
857
|
-
'<label class="field-label" for="cfg-shell">Shell</label>' +
|
|
858
|
-
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
859
|
-
'</div>' +
|
|
860
|
-
'<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
|
|
861
|
-
'<p id="config-message" class="hint hidden"></p>' +
|
|
862
|
-
'</div>' +
|
|
1689
|
+
buildSettingsGeneralPanel() +
|
|
863
1690
|
|
|
864
1691
|
// Security tab
|
|
865
1692
|
'<div class="settings-panel" id="settings-tab-security">' +
|
|
@@ -897,6 +1724,7 @@
|
|
|
897
1724
|
'<div class="settings-panel" id="settings-tab-presets">' +
|
|
898
1725
|
'<div id="presets-list" class="presets-list"></div>' +
|
|
899
1726
|
'</div>' +
|
|
1727
|
+
'</div>' +
|
|
900
1728
|
'</div>' +
|
|
901
1729
|
'</div>' +
|
|
902
1730
|
'</section>';
|
|
@@ -1259,9 +2087,7 @@
|
|
|
1259
2087
|
|
|
1260
2088
|
function setFilePanelOpen(nextOpen) {
|
|
1261
2089
|
state.filePanelOpen = nextOpen;
|
|
1262
|
-
|
|
1263
|
-
localStorage.setItem("wand-file-panel-open", String(state.filePanelOpen));
|
|
1264
|
-
} catch (e) {}
|
|
2090
|
+
persistFilePanelState();
|
|
1265
2091
|
if (state.filePanelOpen && isMobileLayout()) {
|
|
1266
2092
|
state.sessionsDrawerOpen = false;
|
|
1267
2093
|
}
|
|
@@ -1796,7 +2622,7 @@
|
|
|
1796
2622
|
var recoveryHint = "";
|
|
1797
2623
|
var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
|
|
1798
2624
|
|
|
1799
|
-
if (session.claudeSessionId) {
|
|
2625
|
+
if (session.provider === "claude" && session.claudeSessionId) {
|
|
1800
2626
|
var shortId = session.claudeSessionId.slice(0, 8);
|
|
1801
2627
|
sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
|
|
1802
2628
|
if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
|
|
@@ -1808,9 +2634,18 @@
|
|
|
1808
2634
|
recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
|
|
1809
2635
|
}
|
|
1810
2636
|
|
|
2637
|
+
var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
|
|
2638
|
+
var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
|
|
2639
|
+
var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
|
|
2640
|
+
var mergeTitle = needsCleanup ? "重试清理 worktree" : "合并到主分支";
|
|
2641
|
+
var mergeButton = canOpenMerge && session.worktreeMergeStatus !== "merged"
|
|
2642
|
+
? '<button class="session-action-btn merge-btn" data-action="worktree-merge" data-session-id="' + session.id + '" type="button" aria-label="' + escapeHtml(mergeTitle) + '" title="' + escapeHtml(mergeTitle) + '"' + (mergeDisabled ? ' disabled' : '') + '><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="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h10"/><path d="M5 7l-2 2 2 2"/><path d="M19 15l2 2-2 2"/></svg></button>'
|
|
2643
|
+
: needsCleanup
|
|
2644
|
+
? '<button class="session-action-btn merge-btn" data-action="worktree-cleanup" data-session-id="' + session.id + '" type="button" aria-label="重试清理 worktree" title="重试清理 worktree"><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>'
|
|
2645
|
+
: "";
|
|
1811
2646
|
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>';
|
|
1812
2647
|
var modeBadge = renderSessionKindBadge(session);
|
|
1813
|
-
var actionsHtml = '<span class="session-actions">' + resumeButton + deleteButton + '</span>';
|
|
2648
|
+
var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
|
|
1814
2649
|
|
|
1815
2650
|
return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
|
|
1816
2651
|
'<div class="session-item-content">' +
|
|
@@ -1834,12 +2669,43 @@
|
|
|
1834
2669
|
'</div>';
|
|
1835
2670
|
}
|
|
1836
2671
|
|
|
2672
|
+
function getWorktreeMergeStatusLabel(session) {
|
|
2673
|
+
if (!session || !session.worktreeMergeStatus) return "";
|
|
2674
|
+
var labels = {
|
|
2675
|
+
ready: "可合并",
|
|
2676
|
+
checking: "检查中",
|
|
2677
|
+
merging: "合并中",
|
|
2678
|
+
merged: session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false ? "已合并待清理" : "已合并",
|
|
2679
|
+
failed: "合并失败"
|
|
2680
|
+
};
|
|
2681
|
+
return labels[session.worktreeMergeStatus] || "";
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function renderWorktreeMergeBadge(session) {
|
|
2685
|
+
var label = getWorktreeMergeStatusLabel(session);
|
|
2686
|
+
if (!label) return "";
|
|
2687
|
+
return '<span class="session-kind-badge worktree-merge ' + escapeHtml(session.worktreeMergeStatus || "") + '">' + escapeHtml(label) + '</span>';
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
function renderWorktreeBadge(session) {
|
|
2691
|
+
if (!session || !session.worktreeEnabled) return "";
|
|
2692
|
+
var titleParts = [];
|
|
2693
|
+
if (session.worktree && session.worktree.branch) {
|
|
2694
|
+
titleParts.push('Worktree: ' + session.worktree.branch);
|
|
2695
|
+
}
|
|
2696
|
+
if (session.worktree && session.worktree.path) {
|
|
2697
|
+
titleParts.push('Path: ' + session.worktree.path);
|
|
2698
|
+
}
|
|
2699
|
+
var title = titleParts.length > 0 ? ' title="' + escapeHtml(titleParts.join('\n')) + '"' : '';
|
|
2700
|
+
return '<span class="session-kind-badge worktree"' + title + '>Worktree</span>' + renderWorktreeMergeBadge(session);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
1837
2703
|
function renderSessionKindBadge(session) {
|
|
1838
2704
|
if (!session) return "";
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
return
|
|
2705
|
+
var primary = isStructuredSession(session)
|
|
2706
|
+
? '<span class="session-kind-badge structured">Structured</span>'
|
|
2707
|
+
: '<span class="session-kind-badge pty">PTY</span>';
|
|
2708
|
+
return primary + renderWorktreeBadge(session);
|
|
1843
2709
|
}
|
|
1844
2710
|
|
|
1845
2711
|
function renderModeCards(selectedMode) {
|
|
@@ -1859,6 +2725,20 @@
|
|
|
1859
2725
|
}).join("");
|
|
1860
2726
|
}
|
|
1861
2727
|
|
|
2728
|
+
function renderProviderOptions(selectedTool) {
|
|
2729
|
+
var tools = [
|
|
2730
|
+
{ id: "claude", label: "Claude", desc: "完整 Claude 会话能力" },
|
|
2731
|
+
{ id: "codex", label: "Codex", desc: "PTY 透传,全权限启动" }
|
|
2732
|
+
];
|
|
2733
|
+
return tools.map(function(tool) {
|
|
2734
|
+
var active = tool.id === selectedTool ? " active" : "";
|
|
2735
|
+
return '<button type="button" class="mode-card provider-card' + active + '" data-provider="' + tool.id + '">' +
|
|
2736
|
+
'<span class="mode-card-label">' + tool.label + '</span>' +
|
|
2737
|
+
'<span class="mode-card-desc">' + tool.desc + '</span>' +
|
|
2738
|
+
'</button>';
|
|
2739
|
+
}).join("");
|
|
2740
|
+
}
|
|
2741
|
+
|
|
1862
2742
|
function renderSessionKindOptions(selectedKind) {
|
|
1863
2743
|
var kinds = [
|
|
1864
2744
|
{ id: "structured", label: "结构化", desc: "智能对话模式" },
|
|
@@ -1866,17 +2746,29 @@
|
|
|
1866
2746
|
];
|
|
1867
2747
|
return kinds.map(function(kind) {
|
|
1868
2748
|
var active = kind.id === selectedKind ? " active" : "";
|
|
1869
|
-
|
|
2749
|
+
var disabled = (state.sessionTool === "codex" && kind.id === "structured") ? " disabled" : "";
|
|
2750
|
+
return '<button type="button" class="mode-card session-kind-card' + active + disabled + '" data-session-kind="' + kind.id + '">' +
|
|
1870
2751
|
'<span class="mode-card-label">' + kind.label + '</span>' +
|
|
1871
2752
|
'<span class="mode-card-desc">' + kind.desc + '</span>' +
|
|
1872
2753
|
'</button>';
|
|
1873
2754
|
}).join("");
|
|
1874
2755
|
}
|
|
1875
2756
|
|
|
2757
|
+
function renderWorktreeToggle(enabled) {
|
|
2758
|
+
return '<label class="session-inline-toggle" for="session-worktree-toggle">' +
|
|
2759
|
+
'<input id="session-worktree-toggle" type="checkbox" class="field-checkbox"' + (enabled ? ' checked' : '') + ' />' +
|
|
2760
|
+
'<span class="session-inline-toggle-label">Worktree 模式</span>' +
|
|
2761
|
+
'</label>';
|
|
2762
|
+
}
|
|
2763
|
+
|
|
1876
2764
|
function getSessionKindHint(kind) {
|
|
2765
|
+
var tool = state.sessionTool || "claude";
|
|
1877
2766
|
if (kind === "structured") {
|
|
1878
2767
|
return "结构化聊天界面,支持多轮对话、流式输出和工具调用展示。";
|
|
1879
2768
|
}
|
|
2769
|
+
if (tool === "codex") {
|
|
2770
|
+
return "Codex 仅支持 PTY;terminal 是原始输出,chat 是解析后的阅读视图。";
|
|
2771
|
+
}
|
|
1880
2772
|
return "原始 PTY 终端会话,支持持续交互、终端视图和权限流。";
|
|
1881
2773
|
}
|
|
1882
2774
|
|
|
@@ -1884,22 +2776,32 @@
|
|
|
1884
2776
|
var modalTool = getPreferredTool();
|
|
1885
2777
|
var modalMode = getSafeModeForTool(modalTool, state.modeValue || state.chatMode || "default");
|
|
1886
2778
|
var sessionKind = state.sessionCreateKind || "structured";
|
|
2779
|
+
var worktreeEnabled = state.sessionCreateWorktree === true;
|
|
1887
2780
|
return '<section id="session-modal" class="modal-backdrop hidden">' +
|
|
1888
2781
|
'<div class="modal session-modal">' +
|
|
1889
2782
|
'<div class="modal-header">' +
|
|
1890
2783
|
'<div>' +
|
|
1891
2784
|
'<h2 class="modal-title">新对话</h2>' +
|
|
1892
|
-
'<p class="modal-subtitle">启动 Claude
|
|
2785
|
+
'<p class="modal-subtitle">启动 Claude 或 Codex 会话,选择 provider、会话类型、模式和工作目录。</p>' +
|
|
1893
2786
|
'</div>' +
|
|
1894
2787
|
'<button id="close-modal-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
1895
2788
|
'</div>' +
|
|
1896
2789
|
'<div class="modal-body">' +
|
|
2790
|
+
'<div class="field">' +
|
|
2791
|
+
'<label class="field-label">Provider</label>' +
|
|
2792
|
+
'<div id="provider-cards" class="mode-cards">' +
|
|
2793
|
+
renderProviderOptions(modalTool) +
|
|
2794
|
+
'</div>' +
|
|
2795
|
+
'</div>' +
|
|
1897
2796
|
'<div class="field">' +
|
|
1898
2797
|
'<label class="field-label">会话类型</label>' +
|
|
1899
2798
|
'<div id="session-kind-cards" class="mode-cards">' +
|
|
1900
2799
|
renderSessionKindOptions(sessionKind) +
|
|
1901
2800
|
'</div>' +
|
|
1902
|
-
'<
|
|
2801
|
+
'<div class="field-hint session-kind-hint-row">' +
|
|
2802
|
+
'<span id="session-kind-description">' + escapeHtml(getSessionKindHint(sessionKind)) + '</span>' +
|
|
2803
|
+
renderWorktreeToggle(worktreeEnabled) +
|
|
2804
|
+
'</div>' +
|
|
1903
2805
|
'</div>' +
|
|
1904
2806
|
'<div class="field">' +
|
|
1905
2807
|
'<label class="field-label">模式</label>' +
|
|
@@ -1927,7 +2829,10 @@
|
|
|
1927
2829
|
// Global toggle function for tool card headers — called via onclick attribute
|
|
1928
2830
|
window.__tcToggle = function(e, headerEl) {
|
|
1929
2831
|
var card = headerEl.closest(".tool-use-card");
|
|
1930
|
-
if (card)
|
|
2832
|
+
if (card) {
|
|
2833
|
+
card.classList.toggle("collapsed");
|
|
2834
|
+
persistElementExpandState(card, "tool-card");
|
|
2835
|
+
}
|
|
1931
2836
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
1932
2837
|
};
|
|
1933
2838
|
// Toggle function for inline thinking blocks — called via onclick attribute
|
|
@@ -1947,6 +2852,7 @@
|
|
|
1947
2852
|
var action = el.querySelector(".thinking-inline-action");
|
|
1948
2853
|
if (action) action.textContent = "展开";
|
|
1949
2854
|
}
|
|
2855
|
+
persistElementExpandState(el, "thinking");
|
|
1950
2856
|
};
|
|
1951
2857
|
// Toggle function for inline tool rows (Read, Glob, Grep, etc.)
|
|
1952
2858
|
window.__inlineToolToggle = function(el) {
|
|
@@ -1964,6 +2870,7 @@
|
|
|
1964
2870
|
statusSpan.textContent = "✓";
|
|
1965
2871
|
}
|
|
1966
2872
|
}
|
|
2873
|
+
persistElementExpandState(el, "inline-tool");
|
|
1967
2874
|
};
|
|
1968
2875
|
// Toggle function for terminal tool blocks
|
|
1969
2876
|
window.__terminalExpand = function(el) {
|
|
@@ -1976,6 +2883,7 @@
|
|
|
1976
2883
|
container.dataset.expanded = isHidden ? "true" : "false";
|
|
1977
2884
|
var toggleIcon = el.querySelector(".term-toggle-icon");
|
|
1978
2885
|
if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "▶";
|
|
2886
|
+
persistElementExpandState(container, "terminal");
|
|
1979
2887
|
}
|
|
1980
2888
|
};
|
|
1981
2889
|
// Update streaming thinking content (called from WebSocket handler)
|
|
@@ -2075,6 +2983,15 @@
|
|
|
2075
2983
|
quickStartSession();
|
|
2076
2984
|
});
|
|
2077
2985
|
}
|
|
2986
|
+
var welcomeCodexBtn = document.getElementById("welcome-tool-codex");
|
|
2987
|
+
if (welcomeCodexBtn) {
|
|
2988
|
+
welcomeCodexBtn.addEventListener("click", function() {
|
|
2989
|
+
state.sessionTool = "codex";
|
|
2990
|
+
state.preferredCommand = "codex";
|
|
2991
|
+
state.modeValue = "full-access";
|
|
2992
|
+
quickStartSession();
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2078
2995
|
var welcomeStructuredBtn = document.getElementById("welcome-tool-structured");
|
|
2079
2996
|
if (welcomeStructuredBtn) {
|
|
2080
2997
|
welcomeStructuredBtn.addEventListener("click", function() {
|
|
@@ -2097,10 +3014,26 @@
|
|
|
2097
3014
|
// Claude session ID badge click-to-copy (event delegation on document)
|
|
2098
3015
|
document.addEventListener("click", handleClaudeIdCopy);
|
|
2099
3016
|
|
|
3017
|
+
var providerCardsEl = document.getElementById("provider-cards");
|
|
3018
|
+
if (providerCardsEl) providerCardsEl.addEventListener("click", function(e) {
|
|
3019
|
+
var card = e.target.closest(".provider-card");
|
|
3020
|
+
if (!card || card.classList.contains("disabled")) return;
|
|
3021
|
+
var provider = card.getAttribute("data-provider");
|
|
3022
|
+
if (provider) {
|
|
3023
|
+
state.sessionTool = provider;
|
|
3024
|
+
state.preferredCommand = provider;
|
|
3025
|
+
if (provider === "codex") {
|
|
3026
|
+
state.sessionCreateKind = "pty";
|
|
3027
|
+
state.modeValue = "full-access";
|
|
3028
|
+
}
|
|
3029
|
+
syncSessionModalUI();
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
|
|
2100
3033
|
var kindCardsEl = document.getElementById("session-kind-cards");
|
|
2101
3034
|
if (kindCardsEl) kindCardsEl.addEventListener("click", function(e) {
|
|
2102
3035
|
var card = e.target.closest(".session-kind-card");
|
|
2103
|
-
if (!card) return;
|
|
3036
|
+
if (!card || card.classList.contains("disabled")) return;
|
|
2104
3037
|
var kind = card.getAttribute("data-session-kind");
|
|
2105
3038
|
if (kind) {
|
|
2106
3039
|
state.sessionCreateKind = kind;
|
|
@@ -2118,6 +3051,10 @@
|
|
|
2118
3051
|
syncSessionModalUI();
|
|
2119
3052
|
}
|
|
2120
3053
|
});
|
|
3054
|
+
var worktreeToggleEl = document.getElementById("session-worktree-toggle");
|
|
3055
|
+
if (worktreeToggleEl) worktreeToggleEl.addEventListener("change", function() {
|
|
3056
|
+
state.sessionCreateWorktree = this.checked;
|
|
3057
|
+
});
|
|
2121
3058
|
var cwdEl = document.getElementById("cwd");
|
|
2122
3059
|
if (cwdEl) {
|
|
2123
3060
|
cwdEl.addEventListener("input", function() { state.cwdValue = this.value; });
|
|
@@ -2156,15 +3093,7 @@
|
|
|
2156
3093
|
});
|
|
2157
3094
|
var savePassBtn = document.getElementById("save-password-button");
|
|
2158
3095
|
if (savePassBtn) savePassBtn.addEventListener("click", savePassword);
|
|
2159
|
-
|
|
2160
|
-
var settingsTabs = document.querySelectorAll(".settings-tab");
|
|
2161
|
-
for (var ti = 0; ti < settingsTabs.length; ti++) {
|
|
2162
|
-
settingsTabs[ti].addEventListener("click", function(e) {
|
|
2163
|
-
switchSettingsTab(e.target.getAttribute("data-tab"));
|
|
2164
|
-
});
|
|
2165
|
-
}
|
|
2166
|
-
var saveConfigBtn = document.getElementById("save-config-button");
|
|
2167
|
-
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
3096
|
+
bindSettingsModalEvents();
|
|
2168
3097
|
var uploadCertBtn = document.getElementById("upload-cert-button");
|
|
2169
3098
|
if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
|
|
2170
3099
|
var checkUpdateBtn = document.getElementById("check-update-button");
|
|
@@ -2187,6 +3116,12 @@
|
|
|
2187
3116
|
if (drawerNewSessBtn) drawerNewSessBtn.addEventListener("click", openSessionModal);
|
|
2188
3117
|
var closeModalBtn = document.getElementById("close-modal-button");
|
|
2189
3118
|
if (closeModalBtn) closeModalBtn.addEventListener("click", closeSessionModal);
|
|
3119
|
+
var closeWorktreeMergeBtn = document.getElementById("close-worktree-merge-button");
|
|
3120
|
+
if (closeWorktreeMergeBtn) closeWorktreeMergeBtn.addEventListener("click", closeWorktreeMergeModal);
|
|
3121
|
+
var worktreeMergeCancelBtn = document.getElementById("worktree-merge-cancel-button");
|
|
3122
|
+
if (worktreeMergeCancelBtn) worktreeMergeCancelBtn.addEventListener("click", closeWorktreeMergeModal);
|
|
3123
|
+
var worktreeMergeConfirmBtn = document.getElementById("worktree-merge-confirm-button");
|
|
3124
|
+
if (worktreeMergeConfirmBtn) worktreeMergeConfirmBtn.addEventListener("click", confirmWorktreeMerge);
|
|
2190
3125
|
var runBtn = document.getElementById("run-button");
|
|
2191
3126
|
if (runBtn) runBtn.addEventListener("click", runCommand);
|
|
2192
3127
|
var approvePermissionBtn = document.getElementById("approve-permission-btn");
|
|
@@ -2211,6 +3146,7 @@
|
|
|
2211
3146
|
var sessionModal = document.getElementById("session-modal");
|
|
2212
3147
|
if (sessionModal) sessionModal.addEventListener("click", function(e) {
|
|
2213
3148
|
if (e.target.id === "session-modal") closeSessionModal();
|
|
3149
|
+
if (e.target.id === "worktree-merge-modal") closeWorktreeMergeModal();
|
|
2214
3150
|
});
|
|
2215
3151
|
|
|
2216
3152
|
var inputBox = document.getElementById("input-box");
|
|
@@ -2219,6 +3155,9 @@
|
|
|
2219
3155
|
inputBox.addEventListener("keydown", handleInputBoxKeydown);
|
|
2220
3156
|
inputBox.addEventListener("paste", handleInputPaste);
|
|
2221
3157
|
inputBox.addEventListener("input", function() {
|
|
3158
|
+
if (handleInteractiveTextInput(inputBox)) {
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
2222
3161
|
refreshInputBoxState(inputBox);
|
|
2223
3162
|
setDraftValue(inputBox.value, true);
|
|
2224
3163
|
});
|
|
@@ -2233,6 +3172,8 @@
|
|
|
2233
3172
|
// View toggle handlers
|
|
2234
3173
|
var viewTermBtn = document.getElementById("view-terminal-btn");
|
|
2235
3174
|
if (viewTermBtn) viewTermBtn.addEventListener("click", function() { setView("terminal"); });
|
|
3175
|
+
var viewChatBtn = document.getElementById("view-chat-btn");
|
|
3176
|
+
if (viewChatBtn) viewChatBtn.addEventListener("click", function() { setView("chat"); });
|
|
2236
3177
|
// Terminal interactive toggle (both topbar and terminal-header)
|
|
2237
3178
|
var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
|
|
2238
3179
|
terminalInteractiveToggles.forEach(function(id) {
|
|
@@ -2249,15 +3190,8 @@
|
|
|
2249
3190
|
if (shortcutsToggleBtn) shortcutsToggleBtn.addEventListener("click", function(e) {
|
|
2250
3191
|
e.stopPropagation();
|
|
2251
3192
|
state.shortcutsExpanded = !state.shortcutsExpanded;
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
var row = document.querySelector(".inline-shortcuts-expanded-row");
|
|
2255
|
-
if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
|
|
2256
|
-
if (row) row.classList.toggle("visible", state.shortcutsExpanded);
|
|
2257
|
-
if (toggle) {
|
|
2258
|
-
toggle.classList.toggle("active", state.shortcutsExpanded);
|
|
2259
|
-
toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
|
|
2260
|
-
}
|
|
3193
|
+
persistShortcutsExpandedState();
|
|
3194
|
+
updateCollapsedShortcutsUi();
|
|
2261
3195
|
});
|
|
2262
3196
|
// Close shortcuts strip on outside click
|
|
2263
3197
|
document.addEventListener("click", function(e) {
|
|
@@ -2267,13 +3201,8 @@
|
|
|
2267
3201
|
var clickedInsideRow = expandedRow && expandedRow.contains(e.target);
|
|
2268
3202
|
if (wrap && !wrap.contains(e.target) && !clickedInsideRow) {
|
|
2269
3203
|
state.shortcutsExpanded = false;
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
var toggle = document.querySelector(".shortcuts-toggle");
|
|
2273
|
-
if (toggle) {
|
|
2274
|
-
toggle.classList.remove("active");
|
|
2275
|
-
toggle.textContent = "\u2039";
|
|
2276
|
-
}
|
|
3204
|
+
persistShortcutsExpandedState();
|
|
3205
|
+
updateCollapsedShortcutsUi();
|
|
2277
3206
|
}
|
|
2278
3207
|
});
|
|
2279
3208
|
|
|
@@ -2312,8 +3241,18 @@
|
|
|
2312
3241
|
if (jumpBottomBtn) jumpBottomBtn.addEventListener("click", function() {
|
|
2313
3242
|
maybeScrollTerminalToBottom("force");
|
|
2314
3243
|
});
|
|
2315
|
-
|
|
2316
|
-
|
|
3244
|
+
var chatFollowToggle = document.getElementById("chat-follow-toggle");
|
|
3245
|
+
if (chatFollowToggle) chatFollowToggle.addEventListener("click", function() {
|
|
3246
|
+
if (state.chatAutoFollow) {
|
|
3247
|
+
setChatAutoFollow(false, { scrollNow: false });
|
|
3248
|
+
} else {
|
|
3249
|
+
setChatAutoFollow(true, { scrollNow: true, smooth: false });
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
var chatJumpBottomBtn = document.getElementById("chat-jump-bottom");
|
|
3253
|
+
if (chatJumpBottomBtn) chatJumpBottomBtn.addEventListener("click", function() {
|
|
3254
|
+
setChatAutoFollow(true, { scrollNow: true, smooth: true });
|
|
3255
|
+
});
|
|
2317
3256
|
var fileRefresh = document.getElementById("file-explorer-refresh");
|
|
2318
3257
|
if (fileRefresh) fileRefresh.addEventListener("click", refreshFileExplorer);
|
|
2319
3258
|
|
|
@@ -2742,6 +3681,7 @@
|
|
|
2742
3681
|
event.preventDefault();
|
|
2743
3682
|
event.stopPropagation();
|
|
2744
3683
|
state.claudeHistoryExpanded = !state.claudeHistoryExpanded;
|
|
3684
|
+
persistHistoryPanelState();
|
|
2745
3685
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
2746
3686
|
loadClaudeHistory();
|
|
2747
3687
|
}
|
|
@@ -2789,6 +3729,10 @@
|
|
|
2789
3729
|
handleResumeAction(actionButton);
|
|
2790
3730
|
} else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
|
|
2791
3731
|
handleResumeHistoryAction(actionButton);
|
|
3732
|
+
} else if (actionButton.dataset.action === "worktree-merge" && actionButton.dataset.sessionId) {
|
|
3733
|
+
openWorktreeMergeModal(actionButton.dataset.sessionId);
|
|
3734
|
+
} else if (actionButton.dataset.action === "worktree-cleanup" && actionButton.dataset.sessionId) {
|
|
3735
|
+
retryWorktreeCleanup(actionButton.dataset.sessionId);
|
|
2792
3736
|
}
|
|
2793
3737
|
return;
|
|
2794
3738
|
}
|
|
@@ -2959,8 +3903,8 @@
|
|
|
2959
3903
|
|
|
2960
3904
|
function setTerminalManualScrollActive() {
|
|
2961
3905
|
state.terminalAutoFollow = false;
|
|
3906
|
+
clearTerminalScrollIdleTimer();
|
|
2962
3907
|
updateTerminalJumpToBottomButton();
|
|
2963
|
-
scheduleTerminalResumeFollow();
|
|
2964
3908
|
}
|
|
2965
3909
|
|
|
2966
3910
|
function maybeScrollTerminalToBottom(reason) {
|
|
@@ -3171,6 +4115,7 @@
|
|
|
3171
4115
|
var shouldScroll = opts.scroll !== false;
|
|
3172
4116
|
var sessionChanged = state.terminalSessionId !== nextSessionId;
|
|
3173
4117
|
var currentOutput = state.terminalOutput || "";
|
|
4118
|
+
var liveChunkStream = !!(nextSessionId && state.terminalLiveStreamSessions[nextSessionId]);
|
|
3174
4119
|
var wrote = false;
|
|
3175
4120
|
|
|
3176
4121
|
if (normalizedOutput === currentOutput && !sessionChanged) {
|
|
@@ -3199,6 +4144,10 @@
|
|
|
3199
4144
|
} else if (normalizedOutput.length < currentOutput.length && !sessionChanged) {
|
|
3200
4145
|
// Ignore regressive snapshots for the active session; wait for an explicit replace.
|
|
3201
4146
|
return false;
|
|
4147
|
+
} else if (liveChunkStream && !sessionChanged && mode !== "replace" && currentOutput && !normalizedOutput.startsWith(currentOutput)) {
|
|
4148
|
+
// When a session is already streaming live chunks, do not let polled snapshots
|
|
4149
|
+
// rewrite the terminal unless they are strict appends of what we've rendered.
|
|
4150
|
+
return false;
|
|
3202
4151
|
} else if (normalizedOutput.startsWith(currentOutput)) {
|
|
3203
4152
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
3204
4153
|
if (delta) {
|
|
@@ -3352,7 +4301,7 @@
|
|
|
3352
4301
|
if (state.selectedId) {
|
|
3353
4302
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
3354
4303
|
if (session) {
|
|
3355
|
-
syncTerminalBuffer(session.id, session.output || "", { mode: "
|
|
4304
|
+
syncTerminalBuffer(session.id, session.output || "", { mode: "append", scroll: false });
|
|
3356
4305
|
}
|
|
3357
4306
|
} else {
|
|
3358
4307
|
state.terminal.writeln("点击上方「新对话」开始你的第一次对话。");
|
|
@@ -3404,7 +4353,7 @@
|
|
|
3404
4353
|
})
|
|
3405
4354
|
.then(function(res) { return res.json(); })
|
|
3406
4355
|
.then(function(config) {
|
|
3407
|
-
|
|
4356
|
+
applySettingsConfig(config);
|
|
3408
4357
|
var statusDot = document.getElementById("status-dot");
|
|
3409
4358
|
var statusText = document.getElementById("status-text");
|
|
3410
4359
|
if (statusDot) statusDot.classList.add("active");
|
|
@@ -3442,9 +4391,16 @@
|
|
|
3442
4391
|
state.sessions = [];
|
|
3443
4392
|
state.claudeHistory = [];
|
|
3444
4393
|
state.claudeHistoryLoaded = false;
|
|
3445
|
-
state.claudeHistoryExpanded =
|
|
4394
|
+
state.claudeHistoryExpanded = getConfiguredPanelDefaults().claudeHistoryExpanded;
|
|
4395
|
+
persistHistoryPanelState();
|
|
3446
4396
|
state.claudeHistoryExpandedDirs = {};
|
|
3447
|
-
state.sessionsDrawerOpen =
|
|
4397
|
+
state.sessionsDrawerOpen = getConfiguredPanelDefaults().sessionsDrawerOpen;
|
|
4398
|
+
persistDrawerState();
|
|
4399
|
+
state.filePanelOpen = getConfiguredPanelDefaults().filePanelOpen;
|
|
4400
|
+
persistFilePanelState();
|
|
4401
|
+
state.shortcutsExpanded = getConfiguredPanelDefaults().shortcutsExpanded;
|
|
4402
|
+
persistShortcutsExpandedState();
|
|
4403
|
+
updateCollapsedShortcutsUi();
|
|
3448
4404
|
render();
|
|
3449
4405
|
}
|
|
3450
4406
|
|
|
@@ -3467,14 +4423,38 @@
|
|
|
3467
4423
|
}
|
|
3468
4424
|
|
|
3469
4425
|
function getPreferredTool() {
|
|
3470
|
-
return "claude";
|
|
4426
|
+
return state.sessionTool || state.preferredCommand || "claude";
|
|
3471
4427
|
}
|
|
3472
4428
|
|
|
3473
4429
|
function getComposerTool() {
|
|
3474
|
-
return
|
|
4430
|
+
var selected = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
4431
|
+
return (selected && selected.provider) || state.preferredCommand || "claude";
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
function getComposerPlaceholder(session, terminalInteractive) {
|
|
4435
|
+
if (terminalInteractive) {
|
|
4436
|
+
return "终端交互模式开启中,请直接在终端中输入";
|
|
4437
|
+
}
|
|
4438
|
+
if (session && session.provider === "codex") {
|
|
4439
|
+
if (session.status !== "running") {
|
|
4440
|
+
return "Codex 会话已结束,无法继续发送";
|
|
4441
|
+
}
|
|
4442
|
+
return state.currentView === "terminal"
|
|
4443
|
+
? "向 Codex 发送输入;terminal 为原始 TUI 输出"
|
|
4444
|
+
: "向 Codex 发送输入;chat 为解析后的阅读视图";
|
|
4445
|
+
}
|
|
4446
|
+
if (session && !isStructuredSession(session) && session.status !== "running") {
|
|
4447
|
+
return "会话已结束,无法继续发送";
|
|
4448
|
+
}
|
|
4449
|
+
return session && isStructuredSession(session) && session.structuredState && session.structuredState.inFlight
|
|
4450
|
+
? "思考中,可继续发送,消息会自动排队"
|
|
4451
|
+
: "输入消息...";
|
|
3475
4452
|
}
|
|
3476
4453
|
|
|
3477
4454
|
function getToolModeHint(tool, mode) {
|
|
4455
|
+
if (tool === "codex") {
|
|
4456
|
+
return "Codex 当前仅支持 PTY 透传,并固定以 full-access 启动。";
|
|
4457
|
+
}
|
|
3478
4458
|
if (mode === "full-access") {
|
|
3479
4459
|
return "自动确认权限请求与高权限操作,适合你确认环境安全后的连续修改。";
|
|
3480
4460
|
}
|
|
@@ -3491,6 +4471,9 @@
|
|
|
3491
4471
|
}
|
|
3492
4472
|
|
|
3493
4473
|
function getSupportedModes(tool) {
|
|
4474
|
+
if (tool === "codex") {
|
|
4475
|
+
return ["full-access"];
|
|
4476
|
+
}
|
|
3494
4477
|
return ["default", "full-access", "auto-edit", "native", "managed"];
|
|
3495
4478
|
}
|
|
3496
4479
|
|
|
@@ -3523,13 +4506,21 @@
|
|
|
3523
4506
|
}
|
|
3524
4507
|
|
|
3525
4508
|
function getSessionKindLabel(session) {
|
|
3526
|
-
|
|
4509
|
+
var provider = session && session.provider ? session.provider : "claude";
|
|
4510
|
+
return (isStructuredSession(session) ? "结构化" : "终端") + " · " + provider;
|
|
3527
4511
|
}
|
|
3528
4512
|
|
|
3529
4513
|
function getSessionKindDescription(session) {
|
|
3530
4514
|
return isStructuredSession(session)
|
|
3531
4515
|
? "结构化 · 块级记录"
|
|
3532
|
-
:
|
|
4516
|
+
: (session && session.provider === "codex"
|
|
4517
|
+
? "终端 · Codex PTY(chat 为解析视图)"
|
|
4518
|
+
: "终端 · PTY 会话");
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
function shouldRequestChatFormat(session) {
|
|
4522
|
+
if (!session) return false;
|
|
4523
|
+
return isStructuredSession(session) || session.provider === "codex";
|
|
3533
4524
|
}
|
|
3534
4525
|
|
|
3535
4526
|
function isRecoverableToolError(toolResult, nextResult) {
|
|
@@ -3546,9 +4537,7 @@
|
|
|
3546
4537
|
}
|
|
3547
4538
|
|
|
3548
4539
|
function isStructuredSession(session) {
|
|
3549
|
-
|
|
3550
|
-
if (session) console.log("[WAND] isStructuredSession id:", session.id, "sessionKind:", session.sessionKind, "runner:", session.runner, "=>", result);
|
|
3551
|
-
return result;
|
|
4540
|
+
return !!session && (session.sessionKind === "structured" || session.runner === "claude-cli-print");
|
|
3552
4541
|
}
|
|
3553
4542
|
|
|
3554
4543
|
function syncComposerModeSelect() {
|
|
@@ -3561,12 +4550,13 @@
|
|
|
3561
4550
|
if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
|
|
3562
4551
|
}
|
|
3563
4552
|
|
|
3564
|
-
function createStructuredSession(prompt, cwdOverride, modeOverride) {
|
|
4553
|
+
function createStructuredSession(prompt, cwdOverride, modeOverride, worktreeEnabled) {
|
|
3565
4554
|
var payload = {
|
|
3566
4555
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
3567
4556
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
3568
4557
|
runner: state.structuredRunner || "claude-cli-print",
|
|
3569
|
-
prompt: prompt || undefined
|
|
4558
|
+
prompt: prompt || undefined,
|
|
4559
|
+
worktreeEnabled: worktreeEnabled === true
|
|
3570
4560
|
};
|
|
3571
4561
|
console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
|
|
3572
4562
|
return fetch("/api/structured-sessions", {
|
|
@@ -3599,24 +4589,31 @@
|
|
|
3599
4589
|
function applyCurrentView() {
|
|
3600
4590
|
var hasSession = !!state.selectedId;
|
|
3601
4591
|
var terminalBtn = document.getElementById("view-terminal-btn");
|
|
4592
|
+
var chatBtn = document.getElementById("view-chat-btn");
|
|
4593
|
+
var toggleBar = document.getElementById("view-toggle-bar");
|
|
3602
4594
|
var terminalContainer = document.getElementById("output");
|
|
3603
4595
|
var chatContainer = document.getElementById("chat-output");
|
|
3604
4596
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
3605
4597
|
var structured = isStructuredSession(selectedSession);
|
|
3606
4598
|
var showTerminal = hasSession && !structured && state.currentView === "terminal";
|
|
3607
4599
|
var showChat = hasSession && (structured || state.currentView !== "terminal");
|
|
3608
|
-
console.log("[WAND] applyCurrentView hasSession:", hasSession, "structured:", structured, "currentView:", state.currentView, "showTerminal:", showTerminal, "showChat:", showChat, "sessionKind:", selectedSession && selectedSession.sessionKind, "runner:", selectedSession && selectedSession.runner);
|
|
3609
|
-
|
|
3610
4600
|
if (structured) {
|
|
3611
4601
|
state.currentView = "chat";
|
|
3612
4602
|
} else if (!hasSession) {
|
|
3613
4603
|
state.currentView = "terminal";
|
|
3614
4604
|
}
|
|
3615
4605
|
|
|
4606
|
+
if (toggleBar) {
|
|
4607
|
+
toggleBar.classList.toggle("hidden", !hasSession);
|
|
4608
|
+
}
|
|
3616
4609
|
if (terminalBtn) {
|
|
3617
4610
|
terminalBtn.classList.toggle("hidden", structured || !hasSession);
|
|
3618
4611
|
terminalBtn.classList.toggle("active", showTerminal);
|
|
3619
4612
|
}
|
|
4613
|
+
if (chatBtn) {
|
|
4614
|
+
chatBtn.classList.toggle("hidden", !hasSession);
|
|
4615
|
+
chatBtn.classList.toggle("active", showChat);
|
|
4616
|
+
}
|
|
3620
4617
|
if (terminalContainer) {
|
|
3621
4618
|
terminalContainer.classList.toggle("active", showTerminal);
|
|
3622
4619
|
terminalContainer.classList.toggle("hidden", !showTerminal);
|
|
@@ -3625,22 +4622,45 @@
|
|
|
3625
4622
|
chatContainer.classList.toggle("active", showChat);
|
|
3626
4623
|
chatContainer.classList.toggle("hidden", !showChat);
|
|
3627
4624
|
}
|
|
4625
|
+
if (chatContainer && showChat) {
|
|
4626
|
+
ensureChatMessagesContainer(chatContainer);
|
|
4627
|
+
}
|
|
4628
|
+
bindChatScrollListener();
|
|
4629
|
+
updateChatFollowToggleButton();
|
|
4630
|
+
updateChatJumpToBottomButton();
|
|
3628
4631
|
updateInteractiveControls();
|
|
3629
4632
|
}
|
|
3630
4633
|
|
|
3631
4634
|
function syncSessionModalUI() {
|
|
3632
4635
|
var modeHint = document.getElementById("mode-description");
|
|
3633
4636
|
var kindHint = document.getElementById("session-kind-description");
|
|
3634
|
-
var tool = "claude";
|
|
4637
|
+
var tool = state.sessionTool || "claude";
|
|
3635
4638
|
var sessionKind = state.sessionCreateKind || "structured";
|
|
3636
4639
|
|
|
4640
|
+
if (tool === "codex" && sessionKind === "structured") {
|
|
4641
|
+
sessionKind = "pty";
|
|
4642
|
+
state.sessionCreateKind = "pty";
|
|
4643
|
+
}
|
|
4644
|
+
|
|
3637
4645
|
state.sessionTool = tool;
|
|
3638
4646
|
state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
|
|
3639
4647
|
|
|
4648
|
+
var providerCards = document.querySelectorAll("#provider-cards .provider-card");
|
|
4649
|
+
if (providerCards.length) {
|
|
4650
|
+
providerCards.forEach(function(card) {
|
|
4651
|
+
var provider = card.getAttribute("data-provider");
|
|
4652
|
+
card.classList.toggle("active", provider === tool);
|
|
4653
|
+
card.classList.remove("disabled");
|
|
4654
|
+
});
|
|
4655
|
+
}
|
|
4656
|
+
|
|
3640
4657
|
var kindCards = document.querySelectorAll("#session-kind-cards .session-kind-card");
|
|
3641
4658
|
if (kindCards.length) {
|
|
3642
4659
|
kindCards.forEach(function(card) {
|
|
3643
|
-
|
|
4660
|
+
var kind = card.getAttribute("data-session-kind");
|
|
4661
|
+
var disabled = tool === "codex" && kind === "structured";
|
|
4662
|
+
card.classList.toggle("active", kind === sessionKind);
|
|
4663
|
+
card.classList.toggle("disabled", disabled);
|
|
3644
4664
|
});
|
|
3645
4665
|
}
|
|
3646
4666
|
|
|
@@ -3657,33 +4677,31 @@
|
|
|
3657
4677
|
|
|
3658
4678
|
function updateSessionSnapshot(snapshot) {
|
|
3659
4679
|
if (!snapshot || !snapshot.id) return;
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
status: snapshot.status,
|
|
3663
|
-
exitCode: snapshot.exitCode,
|
|
3664
|
-
sessionKind: snapshot.sessionKind,
|
|
3665
|
-
runner: snapshot.runner,
|
|
3666
|
-
inFlight: snapshot.structuredState && snapshot.structuredState.inFlight,
|
|
3667
|
-
msgCount: snapshot.messages && snapshot.messages.length
|
|
3668
|
-
}));
|
|
3669
|
-
}
|
|
4680
|
+
var currentSession = state.sessions.find(function(session) { return session.id === snapshot.id; }) || null;
|
|
4681
|
+
var normalizedSnapshot = normalizeStructuredSnapshot(snapshot, currentSession);
|
|
3670
4682
|
var updated = false;
|
|
3671
4683
|
var prevSession = null;
|
|
3672
4684
|
state.sessions = state.sessions.map(function(session) {
|
|
3673
|
-
if (session.id !==
|
|
4685
|
+
if (session.id !== normalizedSnapshot.id) return session;
|
|
3674
4686
|
prevSession = session;
|
|
3675
4687
|
updated = true;
|
|
3676
|
-
return Object.assign({}, session,
|
|
4688
|
+
return Object.assign({}, session, normalizedSnapshot);
|
|
3677
4689
|
});
|
|
3678
4690
|
if (!updated) {
|
|
3679
|
-
state.sessions.unshift(
|
|
4691
|
+
state.sessions.unshift(normalizedSnapshot);
|
|
3680
4692
|
}
|
|
3681
|
-
|
|
4693
|
+
var updatedSession = state.sessions.find(function(session) { return session.id === normalizedSnapshot.id; }) || normalizedSnapshot;
|
|
4694
|
+
if (updatedSession && Array.isArray(updatedSession.queuedMessages) && normalizedSnapshot.id === state.selectedId) {
|
|
4695
|
+
syncStructuredQueueFromSession(updatedSession);
|
|
4696
|
+
saveStructuredQueue();
|
|
4697
|
+
updateStructuredQueueCounter();
|
|
4698
|
+
}
|
|
4699
|
+
if (normalizedSnapshot.id === state.selectedId) {
|
|
3682
4700
|
reconcileInteractiveState();
|
|
3683
4701
|
updateTaskDisplay();
|
|
3684
4702
|
}
|
|
3685
4703
|
// When a session transitions to a non-running state, try flushing cross-session queue
|
|
3686
|
-
if (
|
|
4704
|
+
if (normalizedSnapshot.status && normalizedSnapshot.status !== "running" && state.crossSessionQueue.length > 0) {
|
|
3687
4705
|
// Use setTimeout(0) to let the current event processing complete first
|
|
3688
4706
|
setTimeout(flushCrossSessionQueue, 0);
|
|
3689
4707
|
}
|
|
@@ -3703,6 +4721,7 @@
|
|
|
3703
4721
|
var keepLocalOutput = localOutput.length > serverOutput.length;
|
|
3704
4722
|
var localStructuredState = localSession.structuredState || null;
|
|
3705
4723
|
var serverStructuredState = serverSession.structuredState || null;
|
|
4724
|
+
var structuredSession = (localSession.sessionKind === "structured") || (serverSession.sessionKind === "structured");
|
|
3706
4725
|
var localHasPendingAssistant = !!(localSession.messages && localSession.messages.length && (function() {
|
|
3707
4726
|
var last = localSession.messages[localSession.messages.length - 1];
|
|
3708
4727
|
return last && last.role === "assistant" && Array.isArray(last.content) && last.content.some(function(block) {
|
|
@@ -3716,6 +4735,16 @@
|
|
|
3716
4735
|
&& localHasPendingAssistant
|
|
3717
4736
|
&& !!localStructuredState.activeRequestId
|
|
3718
4737
|
&& (!serverStructuredState || !serverStructuredState.activeRequestId || serverStructuredState.activeRequestId === localStructuredState.activeRequestId);
|
|
4738
|
+
var localMessages = Array.isArray(localSession.messages)
|
|
4739
|
+
? (structuredSession ? stripRenderOnlyStructuredMessages(localSession.messages) : localSession.messages)
|
|
4740
|
+
: [];
|
|
4741
|
+
var serverMessages = Array.isArray(serverSession.messages)
|
|
4742
|
+
? (structuredSession ? stripRenderOnlyStructuredMessages(serverSession.messages) : serverSession.messages)
|
|
4743
|
+
: [];
|
|
4744
|
+
var preserveLocalMessages = localMessages.length > serverMessages.length
|
|
4745
|
+
|| (localMessages.length > 0 && serverMessages.length > 0
|
|
4746
|
+
&& JSON.stringify(localMessages[localMessages.length - 1]) !== JSON.stringify(serverMessages[serverMessages.length - 1])
|
|
4747
|
+
&& JSON.stringify(localMessages).length > JSON.stringify(serverMessages).length);
|
|
3719
4748
|
|
|
3720
4749
|
if (keepLocalOutput) {
|
|
3721
4750
|
merged.output = localOutput;
|
|
@@ -3727,6 +4756,10 @@
|
|
|
3727
4756
|
merged.messages = localSession.messages;
|
|
3728
4757
|
}
|
|
3729
4758
|
|
|
4759
|
+
if (preserveLocalMessages) {
|
|
4760
|
+
merged.messages = localMessages;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
3730
4763
|
if (localSession.id === state.selectedId) {
|
|
3731
4764
|
if (localSession.permissionBlocked && serverSession.permissionBlocked === false) {
|
|
3732
4765
|
} else if (localSession.permissionBlocked && !serverSession.permissionBlocked) {
|
|
@@ -3749,6 +4782,9 @@
|
|
|
3749
4782
|
if (session && session.messages && session.messages.length > 0) {
|
|
3750
4783
|
return session.messages;
|
|
3751
4784
|
}
|
|
4785
|
+
if (session && session.sessionKind === "structured") {
|
|
4786
|
+
return [];
|
|
4787
|
+
}
|
|
3752
4788
|
if (!allowFallback) {
|
|
3753
4789
|
return [];
|
|
3754
4790
|
}
|
|
@@ -3774,7 +4810,8 @@
|
|
|
3774
4810
|
return recent ? recent.id : sessions[0].id;
|
|
3775
4811
|
}
|
|
3776
4812
|
|
|
3777
|
-
function loadSessions() {
|
|
4813
|
+
function loadSessions(options) {
|
|
4814
|
+
var opts = options || {};
|
|
3778
4815
|
return fetch("/api/sessions", { credentials: "same-origin" })
|
|
3779
4816
|
.then(function(res) {
|
|
3780
4817
|
if (res.status === 401) {
|
|
@@ -3800,6 +4837,9 @@
|
|
|
3800
4837
|
if (preferredSessionId !== undefined) {
|
|
3801
4838
|
state.selectedId = preferredSessionId;
|
|
3802
4839
|
}
|
|
4840
|
+
restoreStructuredQueue();
|
|
4841
|
+
updateStructuredQueueCounter();
|
|
4842
|
+
state.bootstrapping = false;
|
|
3803
4843
|
persistSelectedId();
|
|
3804
4844
|
if (state.modalOpen) {
|
|
3805
4845
|
updateSessionsList();
|
|
@@ -3817,23 +4857,34 @@
|
|
|
3817
4857
|
}
|
|
3818
4858
|
updateShellChrome();
|
|
3819
4859
|
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
if (state.selectedId) {
|
|
4860
|
+
var reloadPromise = Promise.resolve();
|
|
4861
|
+
if (!opts.skipSelectedOutputReload && state.selectedId) {
|
|
4862
|
+
reloadPromise = loadOutput(state.selectedId);
|
|
4863
|
+
} else if (state.selectedId) {
|
|
3824
4864
|
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
3825
4865
|
if (isStructuredSession(sel)) {
|
|
3826
|
-
|
|
4866
|
+
resetChatRenderCache();
|
|
4867
|
+
scheduleChatRender(true);
|
|
3827
4868
|
}
|
|
3828
4869
|
}
|
|
3829
4870
|
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
4871
|
+
return reloadPromise.then(function() {
|
|
4872
|
+
if (state.crossSessionQueue.length > 0) {
|
|
4873
|
+
flushCrossSessionQueue();
|
|
4874
|
+
}
|
|
4875
|
+
renderCrossSessionQueue();
|
|
4876
|
+
});
|
|
3834
4877
|
})
|
|
3835
4878
|
.catch(function(e) {
|
|
3836
|
-
|
|
4879
|
+
var message = (e && e.message) || "";
|
|
4880
|
+
var isTransientAbort =
|
|
4881
|
+
message === "Failed to fetch" ||
|
|
4882
|
+
message === "NetworkError when attempting to fetch resource." ||
|
|
4883
|
+
message === "Load failed" ||
|
|
4884
|
+
/aborted|aborterror|networkerror|failed to fetch/i.test(message);
|
|
4885
|
+
if (!isTransientAbort) {
|
|
4886
|
+
console.error("[wand] loadSessions failed:", e);
|
|
4887
|
+
}
|
|
3837
4888
|
});
|
|
3838
4889
|
}
|
|
3839
4890
|
|
|
@@ -3896,7 +4947,7 @@
|
|
|
3896
4947
|
}
|
|
3897
4948
|
|
|
3898
4949
|
if (selectedSession && state.terminal) {
|
|
3899
|
-
syncTerminalBuffer(selectedSession.id, selectedSession.output || "", { mode: "
|
|
4950
|
+
syncTerminalBuffer(selectedSession.id, selectedSession.output || "", { mode: "append", scroll: false });
|
|
3900
4951
|
} else if (!selectedSession) {
|
|
3901
4952
|
state.terminalSessionId = null;
|
|
3902
4953
|
state.terminalOutput = "";
|
|
@@ -3933,7 +4984,7 @@
|
|
|
3933
4984
|
}
|
|
3934
4985
|
var sess = state.sessions.find(function(s) { return s.id === id; });
|
|
3935
4986
|
var url = "/api/sessions/" + id;
|
|
3936
|
-
if (
|
|
4987
|
+
if (shouldRequestChatFormat(sess)) {
|
|
3937
4988
|
url += "?format=chat";
|
|
3938
4989
|
}
|
|
3939
4990
|
return fetch(url, { credentials: "same-origin" })
|
|
@@ -3952,14 +5003,7 @@
|
|
|
3952
5003
|
updateShellChrome();
|
|
3953
5004
|
|
|
3954
5005
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
3955
|
-
|
|
3956
|
-
if (selectedSession && selectedSession.sessionKind === "structured") {
|
|
3957
|
-
appendQueuedPlaceholders(state.currentMessages);
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
if (state.terminal) {
|
|
3961
|
-
syncTerminalBuffer(id, data.output || "", { mode: "replace" });
|
|
3962
|
-
}
|
|
5006
|
+
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, data.output, false));
|
|
3963
5007
|
|
|
3964
5008
|
renderChat(false);
|
|
3965
5009
|
});
|
|
@@ -3967,9 +5011,7 @@
|
|
|
3967
5011
|
|
|
3968
5012
|
function selectSession(id) {
|
|
3969
5013
|
var foundSession = state.sessions.find(function(item) { return item.id === id; });
|
|
3970
|
-
console.log("[WAND] selectSession id:", id, "found:", !!foundSession, "sessionKind:", foundSession && foundSession.sessionKind, "runner:", foundSession && foundSession.runner, "isStructured:", isStructuredSession(foundSession));
|
|
3971
5014
|
if (!foundSession) {
|
|
3972
|
-
console.warn("[WAND] selectSession: session not found, skipping", id);
|
|
3973
5015
|
return;
|
|
3974
5016
|
}
|
|
3975
5017
|
state.selectedId = id;
|
|
@@ -3977,7 +5019,8 @@
|
|
|
3977
5019
|
// Clear queued inputs from the previous session to prevent cross-session leaks
|
|
3978
5020
|
state.messageQueue = [];
|
|
3979
5021
|
state.pendingMessages = [];
|
|
3980
|
-
|
|
5022
|
+
syncStructuredQueueFromSession(foundSession);
|
|
5023
|
+
restoreStructuredQueue();
|
|
3981
5024
|
updateStructuredQueueCounter();
|
|
3982
5025
|
resetChatRenderCache();
|
|
3983
5026
|
state.currentMessages = [];
|
|
@@ -4023,11 +5066,10 @@
|
|
|
4023
5066
|
|
|
4024
5067
|
function toggleSessionsDrawer() {
|
|
4025
5068
|
state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
|
|
5069
|
+
persistDrawerState();
|
|
4026
5070
|
if (state.sessionsDrawerOpen && isMobileLayout()) {
|
|
4027
5071
|
state.filePanelOpen = false;
|
|
4028
|
-
|
|
4029
|
-
localStorage.setItem("wand-file-panel-open", "false");
|
|
4030
|
-
} catch (e) {}
|
|
5072
|
+
persistFilePanelState();
|
|
4031
5073
|
}
|
|
4032
5074
|
updateLayoutState();
|
|
4033
5075
|
}
|
|
@@ -4036,6 +5078,7 @@
|
|
|
4036
5078
|
if (!state.sessionsDrawerOpen) return;
|
|
4037
5079
|
closeSwipedItem();
|
|
4038
5080
|
state.sessionsDrawerOpen = false;
|
|
5081
|
+
persistDrawerState();
|
|
4039
5082
|
updateLayoutState();
|
|
4040
5083
|
}
|
|
4041
5084
|
|
|
@@ -4054,7 +5097,9 @@
|
|
|
4054
5097
|
modal.classList.remove("hidden");
|
|
4055
5098
|
lastFocusedElement = document.activeElement;
|
|
4056
5099
|
state.sessionTool = getPreferredTool();
|
|
4057
|
-
state.
|
|
5100
|
+
state.preferredCommand = state.sessionTool;
|
|
5101
|
+
state.sessionCreateKind = state.sessionTool === "codex" ? "pty" : "structured";
|
|
5102
|
+
state.sessionCreateWorktree = false;
|
|
4058
5103
|
state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
|
|
4059
5104
|
syncSessionModalUI();
|
|
4060
5105
|
loadRecentPathBubbles();
|
|
@@ -4107,21 +5152,178 @@
|
|
|
4107
5152
|
e.preventDefault();
|
|
4108
5153
|
lastEl.focus();
|
|
4109
5154
|
}
|
|
4110
|
-
} else {
|
|
4111
|
-
// Tab
|
|
4112
|
-
if (document.activeElement === lastEl) {
|
|
4113
|
-
e.preventDefault();
|
|
4114
|
-
firstEl.focus();
|
|
5155
|
+
} else {
|
|
5156
|
+
// Tab
|
|
5157
|
+
if (document.activeElement === lastEl) {
|
|
5158
|
+
e.preventDefault();
|
|
5159
|
+
firstEl.focus();
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
};
|
|
5163
|
+
|
|
5164
|
+
document.addEventListener("keydown", focusTrapHandler);
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5167
|
+
function getActiveWorktreeMergeSession() {
|
|
5168
|
+
if (!state.activeWorktreeMergeSessionId) return null;
|
|
5169
|
+
return state.sessions.find(function(session) { return session.id === state.activeWorktreeMergeSessionId; }) || null;
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
function renderWorktreeMergeContent() {
|
|
5173
|
+
var container = document.getElementById("worktree-merge-content");
|
|
5174
|
+
var confirmBtn = document.getElementById("worktree-merge-confirm-button");
|
|
5175
|
+
var errorEl = document.getElementById("worktree-merge-error");
|
|
5176
|
+
var session = getActiveWorktreeMergeSession();
|
|
5177
|
+
var result = state.worktreeMergeCheckResult;
|
|
5178
|
+
if (!container || !confirmBtn) return;
|
|
5179
|
+
if (!session || !session.worktree) {
|
|
5180
|
+
container.innerHTML = '<p class="field-hint">未找到可合并的 worktree 会话。</p>';
|
|
5181
|
+
confirmBtn.disabled = true;
|
|
5182
|
+
return;
|
|
5183
|
+
}
|
|
5184
|
+
if (errorEl) {
|
|
5185
|
+
if (state.worktreeMergeError) {
|
|
5186
|
+
showError(errorEl, state.worktreeMergeError);
|
|
5187
|
+
} else {
|
|
5188
|
+
hideError(errorEl);
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
var rows = [
|
|
5192
|
+
'<div class="worktree-merge-row"><span>来源分支</span><strong>' + escapeHtml(session.worktree.branch || "-") + '</strong></div>',
|
|
5193
|
+
'<div class="worktree-merge-row"><span>工作目录</span><strong>' + escapeHtml(session.worktree.path || "-") + '</strong></div>'
|
|
5194
|
+
];
|
|
5195
|
+
if (result) {
|
|
5196
|
+
rows.push('<div class="worktree-merge-row"><span>目标分支</span><strong>' + escapeHtml(result.targetBranch || "-") + '</strong></div>');
|
|
5197
|
+
rows.push('<div class="worktree-merge-row"><span>待合并提交</span><strong>' + escapeHtml(String(result.aheadCount || 0)) + '</strong></div>');
|
|
5198
|
+
rows.push('<div class="worktree-merge-row"><span>未提交改动</span><strong>' + escapeHtml(result.hasUncommittedChanges ? "有" : "无") + '</strong></div>');
|
|
5199
|
+
rows.push('<div class="worktree-merge-row"><span>冲突风险</span><strong>' + escapeHtml(result.hasConflicts ? "有" : "无") + '</strong></div>');
|
|
5200
|
+
if (result.reason) {
|
|
5201
|
+
rows.push('<p class="field-hint">' + escapeHtml(result.reason) + '</p>');
|
|
5202
|
+
}
|
|
5203
|
+
} else if (state.worktreeMergeLoading) {
|
|
5204
|
+
rows.push('<p class="field-hint">正在检查 worktree 合并状态…</p>');
|
|
5205
|
+
}
|
|
5206
|
+
container.innerHTML = rows.join("");
|
|
5207
|
+
confirmBtn.disabled = state.worktreeMergeLoading || state.worktreeMergeSubmitting || !result || result.ok !== true;
|
|
5208
|
+
confirmBtn.textContent = state.worktreeMergeSubmitting ? "合并中..." : "确认合并并清理";
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
function openWorktreeMergeModal(sessionId) {
|
|
5212
|
+
state.activeWorktreeMergeSessionId = sessionId;
|
|
5213
|
+
state.worktreeMergeCheckResult = null;
|
|
5214
|
+
state.worktreeMergeLoading = true;
|
|
5215
|
+
state.worktreeMergeSubmitting = false;
|
|
5216
|
+
state.worktreeMergeError = "";
|
|
5217
|
+
closeSessionModal();
|
|
5218
|
+
closeSettingsModal();
|
|
5219
|
+
var modal = document.getElementById("worktree-merge-modal");
|
|
5220
|
+
if (modal) {
|
|
5221
|
+
modal.classList.remove("hidden");
|
|
5222
|
+
lastFocusedElement = document.activeElement;
|
|
5223
|
+
setupFocusTrap(modal);
|
|
5224
|
+
}
|
|
5225
|
+
renderWorktreeMergeContent();
|
|
5226
|
+
fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/worktree/merge/check", {
|
|
5227
|
+
method: "POST",
|
|
5228
|
+
credentials: "same-origin"
|
|
5229
|
+
})
|
|
5230
|
+
.then(function(res) { return res.json(); })
|
|
5231
|
+
.then(function(data) {
|
|
5232
|
+
if (data && data.error) {
|
|
5233
|
+
throw new Error(data.error);
|
|
5234
|
+
}
|
|
5235
|
+
if (data && data.session) {
|
|
5236
|
+
updateSessionSnapshot(data.session);
|
|
5237
|
+
}
|
|
5238
|
+
state.worktreeMergeCheckResult = data.result || null;
|
|
5239
|
+
state.worktreeMergeError = "";
|
|
5240
|
+
})
|
|
5241
|
+
.catch(function(error) {
|
|
5242
|
+
state.worktreeMergeError = (error && error.message) || "无法检查 worktree 合并状态。";
|
|
5243
|
+
})
|
|
5244
|
+
.finally(function() {
|
|
5245
|
+
state.worktreeMergeLoading = false;
|
|
5246
|
+
renderWorktreeMergeContent();
|
|
5247
|
+
});
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
function closeWorktreeMergeModal() {
|
|
5251
|
+
var modal = document.getElementById("worktree-merge-modal");
|
|
5252
|
+
state.activeWorktreeMergeSessionId = null;
|
|
5253
|
+
state.worktreeMergeCheckResult = null;
|
|
5254
|
+
state.worktreeMergeLoading = false;
|
|
5255
|
+
state.worktreeMergeSubmitting = false;
|
|
5256
|
+
state.worktreeMergeError = "";
|
|
5257
|
+
if (modal) {
|
|
5258
|
+
modal.classList.add("hidden");
|
|
5259
|
+
}
|
|
5260
|
+
if (focusTrapHandler) {
|
|
5261
|
+
document.removeEventListener("keydown", focusTrapHandler);
|
|
5262
|
+
focusTrapHandler = null;
|
|
5263
|
+
}
|
|
5264
|
+
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
5265
|
+
lastFocusedElement.focus();
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
|
|
5269
|
+
function confirmWorktreeMerge() {
|
|
5270
|
+
if (!state.activeWorktreeMergeSessionId || state.worktreeMergeSubmitting) return;
|
|
5271
|
+
state.worktreeMergeSubmitting = true;
|
|
5272
|
+
state.worktreeMergeError = "";
|
|
5273
|
+
renderWorktreeMergeContent();
|
|
5274
|
+
fetch("/api/sessions/" + encodeURIComponent(state.activeWorktreeMergeSessionId) + "/worktree/merge", {
|
|
5275
|
+
method: "POST",
|
|
5276
|
+
credentials: "same-origin",
|
|
5277
|
+
headers: { "Content-Type": "application/json" },
|
|
5278
|
+
body: JSON.stringify({})
|
|
5279
|
+
})
|
|
5280
|
+
.then(function(res) { return res.json(); })
|
|
5281
|
+
.then(function(data) {
|
|
5282
|
+
if (data && data.error) {
|
|
5283
|
+
throw new Error(data.error);
|
|
5284
|
+
}
|
|
5285
|
+
if (data && data.session) {
|
|
5286
|
+
updateSessionSnapshot(data.session);
|
|
5287
|
+
}
|
|
5288
|
+
showToast("已合并到 " + escapeHtml((data.result && data.result.targetBranch) || "主分支") + ((data.result && data.result.cleanupDone === false) ? ",但工作树待清理。" : "。"), "info");
|
|
5289
|
+
closeWorktreeMergeModal();
|
|
5290
|
+
return refreshAll();
|
|
5291
|
+
})
|
|
5292
|
+
.catch(function(error) {
|
|
5293
|
+
state.worktreeMergeError = (error && error.message) || "无法合并 worktree。";
|
|
5294
|
+
renderWorktreeMergeContent();
|
|
5295
|
+
})
|
|
5296
|
+
.finally(function() {
|
|
5297
|
+
state.worktreeMergeSubmitting = false;
|
|
5298
|
+
renderWorktreeMergeContent();
|
|
5299
|
+
});
|
|
5300
|
+
}
|
|
5301
|
+
|
|
5302
|
+
function retryWorktreeCleanup(sessionId) {
|
|
5303
|
+
fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/worktree/cleanup", {
|
|
5304
|
+
method: "POST",
|
|
5305
|
+
credentials: "same-origin"
|
|
5306
|
+
})
|
|
5307
|
+
.then(function(res) { return res.json(); })
|
|
5308
|
+
.then(function(data) {
|
|
5309
|
+
if (data && data.error) {
|
|
5310
|
+
throw new Error(data.error);
|
|
5311
|
+
}
|
|
5312
|
+
if (data && data.session) {
|
|
5313
|
+
updateSessionSnapshot(data.session);
|
|
4115
5314
|
}
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
5315
|
+
showToast("已完成 worktree 清理。", "info");
|
|
5316
|
+
return refreshAll();
|
|
5317
|
+
})
|
|
5318
|
+
.catch(function(error) {
|
|
5319
|
+
showToast((error && error.message) || "无法清理 worktree。", "error");
|
|
5320
|
+
});
|
|
4120
5321
|
}
|
|
4121
5322
|
|
|
4122
5323
|
function openSettingsModal() {
|
|
4123
5324
|
// Close session modal first if open (mutual exclusion)
|
|
4124
5325
|
closeSessionModal();
|
|
5326
|
+
refreshSettingsModalUi();
|
|
4125
5327
|
var modal = document.getElementById("settings-modal");
|
|
4126
5328
|
if (modal) {
|
|
4127
5329
|
modal.classList.remove("hidden");
|
|
@@ -4132,9 +5334,8 @@
|
|
|
4132
5334
|
if (confirmEl) confirmEl.value = "";
|
|
4133
5335
|
hideSettingsMessages();
|
|
4134
5336
|
setupFocusTrap(modal);
|
|
4135
|
-
|
|
5337
|
+
bindSettingsModalEvents();
|
|
4136
5338
|
switchSettingsTab("about");
|
|
4137
|
-
// Load settings data
|
|
4138
5339
|
loadSettingsData();
|
|
4139
5340
|
}
|
|
4140
5341
|
}
|
|
@@ -4225,62 +5426,120 @@
|
|
|
4225
5426
|
panels[j].classList.remove("active");
|
|
4226
5427
|
}
|
|
4227
5428
|
}
|
|
5429
|
+
updateSettingsActiveNav();
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5432
|
+
function refreshSettingsGeneralPanel() {
|
|
5433
|
+
var existingPanel = document.getElementById("settings-tab-general");
|
|
5434
|
+
if (!existingPanel) return;
|
|
5435
|
+
var wrapper = document.createElement("div");
|
|
5436
|
+
wrapper.innerHTML = buildSettingsGeneralPanel();
|
|
5437
|
+
var nextPanel = wrapper.firstChild;
|
|
5438
|
+
if (!nextPanel) return;
|
|
5439
|
+
existingPanel.replaceWith(nextPanel);
|
|
5440
|
+
var saveConfigBtn = document.getElementById("save-config-button");
|
|
5441
|
+
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
5442
|
+
syncPanelStateSettingsForm();
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
function bindSettingsTabEvents() {
|
|
5446
|
+
var settingsTabs = document.querySelectorAll(".settings-tab");
|
|
5447
|
+
for (var ti = 0; ti < settingsTabs.length; ti++) {
|
|
5448
|
+
settingsTabs[ti].addEventListener("click", function(e) {
|
|
5449
|
+
switchSettingsTab(e.target.getAttribute("data-tab"));
|
|
5450
|
+
});
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
function bindSettingsConfigActions() {
|
|
5455
|
+
var saveConfigBtn = document.getElementById("save-config-button");
|
|
5456
|
+
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
function bindSettingsModalEvents() {
|
|
5460
|
+
bindSettingsTabEvents();
|
|
5461
|
+
bindSettingsConfigActions();
|
|
5462
|
+
}
|
|
5463
|
+
|
|
5464
|
+
function refreshSettingsModalUi() {
|
|
5465
|
+
refreshSettingsGeneralPanel();
|
|
5466
|
+
updateSettingsActiveNav();
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
function normalizePanelStateSettings(cfg) {
|
|
5470
|
+
var panelState = cfg && cfg.uiPreferences && cfg.uiPreferences.defaultPanelState;
|
|
5471
|
+
var defaults = getConfiguredPanelDefaults(cfg);
|
|
5472
|
+
return {
|
|
5473
|
+
sessionsDrawerOpen: panelState && typeof panelState.sessionsDrawerOpen === "boolean" ? panelState.sessionsDrawerOpen : defaults.sessionsDrawerOpen,
|
|
5474
|
+
filePanelOpen: panelState && typeof panelState.filePanelOpen === "boolean" ? panelState.filePanelOpen : defaults.filePanelOpen,
|
|
5475
|
+
shortcutsExpanded: panelState && typeof panelState.shortcutsExpanded === "boolean" ? panelState.shortcutsExpanded : defaults.shortcutsExpanded,
|
|
5476
|
+
claudeHistoryExpanded: panelState && typeof panelState.claudeHistoryExpanded === "boolean" ? panelState.claudeHistoryExpanded : defaults.claudeHistoryExpanded,
|
|
5477
|
+
chatMessageExpanded: panelState && typeof panelState.chatMessageExpanded === "boolean" ? panelState.chatMessageExpanded : defaults.chatMessageExpanded,
|
|
5478
|
+
structuredThinkingExpanded: panelState && typeof panelState.structuredThinkingExpanded === "boolean" ? panelState.structuredThinkingExpanded : defaults.structuredThinkingExpanded,
|
|
5479
|
+
structuredToolGroupExpanded: panelState && typeof panelState.structuredToolGroupExpanded === "boolean" ? panelState.structuredToolGroupExpanded : defaults.structuredToolGroupExpanded,
|
|
5480
|
+
structuredInlineToolExpanded: panelState && typeof panelState.structuredInlineToolExpanded === "boolean" ? panelState.structuredInlineToolExpanded : defaults.structuredInlineToolExpanded,
|
|
5481
|
+
structuredTerminalExpanded: panelState && typeof panelState.structuredTerminalExpanded === "boolean" ? panelState.structuredTerminalExpanded : defaults.structuredTerminalExpanded,
|
|
5482
|
+
structuredToolCardExpanded: panelState && typeof panelState.structuredToolCardExpanded === "boolean" ? panelState.structuredToolCardExpanded : defaults.structuredToolCardExpanded,
|
|
5483
|
+
};
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5486
|
+
function applyLoadedSettingsData(data) {
|
|
5487
|
+
var cfg = data.config || {};
|
|
5488
|
+
applySettingsConfig(cfg);
|
|
5489
|
+
|
|
5490
|
+
var nameEl = document.getElementById("settings-pkg-name");
|
|
5491
|
+
var verEl = document.getElementById("settings-version");
|
|
5492
|
+
var nodeEl = document.getElementById("settings-node-req");
|
|
5493
|
+
var repoEl = document.getElementById("settings-repo-url");
|
|
5494
|
+
if (nameEl) nameEl.textContent = data.packageName || "-";
|
|
5495
|
+
if (verEl) verEl.textContent = data.version || "-";
|
|
5496
|
+
if (nodeEl) nodeEl.textContent = data.nodeVersion || "-";
|
|
5497
|
+
if (repoEl && data.repoUrl) {
|
|
5498
|
+
repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
var hostEl = document.getElementById("cfg-host");
|
|
5502
|
+
var portEl = document.getElementById("cfg-port");
|
|
5503
|
+
var httpsEl = document.getElementById("cfg-https");
|
|
5504
|
+
var modeEl = document.getElementById("cfg-mode");
|
|
5505
|
+
var cwdEl = document.getElementById("cfg-cwd");
|
|
5506
|
+
var shellEl = document.getElementById("cfg-shell");
|
|
5507
|
+
if (hostEl) hostEl.value = cfg.host || "";
|
|
5508
|
+
if (portEl) portEl.value = cfg.port || "";
|
|
5509
|
+
if (httpsEl) httpsEl.checked = cfg.https === true;
|
|
5510
|
+
if (modeEl) modeEl.value = cfg.defaultMode || "default";
|
|
5511
|
+
if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
|
|
5512
|
+
if (shellEl) shellEl.value = cfg.shell || "";
|
|
5513
|
+
var langEl = document.getElementById("cfg-language");
|
|
5514
|
+
if (langEl) langEl.value = cfg.language || "";
|
|
5515
|
+
syncPanelStateSettingsForm(normalizePanelStateSettings(cfg));
|
|
5516
|
+
|
|
5517
|
+
var certStatus = document.getElementById("cert-status");
|
|
5518
|
+
if (certStatus) {
|
|
5519
|
+
certStatus.textContent = data.hasCert ? "已安装 SSL 证书" : "未安装证书(使用自签名或 HTTP)";
|
|
5520
|
+
certStatus.style.color = data.hasCert ? "var(--success)" : "var(--text-secondary)";
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
var presetsList = document.getElementById("presets-list");
|
|
5524
|
+
if (presetsList && cfg.commandPresets) {
|
|
5525
|
+
var html = "";
|
|
5526
|
+
for (var i = 0; i < cfg.commandPresets.length; i++) {
|
|
5527
|
+
var p = cfg.commandPresets[i];
|
|
5528
|
+
html += '<div class="preset-item">' +
|
|
5529
|
+
'<span class="preset-label">' + escapeHtml(p.label) + '</span>' +
|
|
5530
|
+
'<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
|
|
5531
|
+
'</div>';
|
|
5532
|
+
}
|
|
5533
|
+
if (!html) html = '<div class="empty-state-compact"><span class="empty-icon">\u2699</span><span>没有命令预设</span><span class="hint">在 config.json 的 commandPresets 中配置</span></div>';
|
|
5534
|
+
presetsList.innerHTML = html;
|
|
5535
|
+
}
|
|
4228
5536
|
}
|
|
4229
5537
|
|
|
4230
5538
|
function loadSettingsData() {
|
|
4231
5539
|
fetch("/api/settings", { credentials: "same-origin" })
|
|
4232
5540
|
.then(function(res) { return res.json(); })
|
|
4233
5541
|
.then(function(data) {
|
|
4234
|
-
|
|
4235
|
-
var nameEl = document.getElementById("settings-pkg-name");
|
|
4236
|
-
var verEl = document.getElementById("settings-version");
|
|
4237
|
-
var nodeEl = document.getElementById("settings-node-req");
|
|
4238
|
-
var repoEl = document.getElementById("settings-repo-url");
|
|
4239
|
-
if (nameEl) nameEl.textContent = data.packageName || "-";
|
|
4240
|
-
if (verEl) verEl.textContent = data.version || "-";
|
|
4241
|
-
if (nodeEl) nodeEl.textContent = data.nodeVersion || "-";
|
|
4242
|
-
if (repoEl && data.repoUrl) {
|
|
4243
|
-
repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
|
|
4244
|
-
}
|
|
4245
|
-
|
|
4246
|
-
// Config fields
|
|
4247
|
-
var cfg = data.config || {};
|
|
4248
|
-
var hostEl = document.getElementById("cfg-host");
|
|
4249
|
-
var portEl = document.getElementById("cfg-port");
|
|
4250
|
-
var httpsEl = document.getElementById("cfg-https");
|
|
4251
|
-
var modeEl = document.getElementById("cfg-mode");
|
|
4252
|
-
var cwdEl = document.getElementById("cfg-cwd");
|
|
4253
|
-
var shellEl = document.getElementById("cfg-shell");
|
|
4254
|
-
if (hostEl) hostEl.value = cfg.host || "";
|
|
4255
|
-
if (portEl) portEl.value = cfg.port || "";
|
|
4256
|
-
if (httpsEl) httpsEl.checked = cfg.https === true;
|
|
4257
|
-
if (modeEl) modeEl.value = cfg.defaultMode || "default";
|
|
4258
|
-
if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
|
|
4259
|
-
if (shellEl) shellEl.value = cfg.shell || "";
|
|
4260
|
-
var langEl = document.getElementById("cfg-language");
|
|
4261
|
-
if (langEl) langEl.value = cfg.language || "";
|
|
4262
|
-
|
|
4263
|
-
// Cert status
|
|
4264
|
-
var certStatus = document.getElementById("cert-status");
|
|
4265
|
-
if (certStatus) {
|
|
4266
|
-
certStatus.textContent = data.hasCert ? "已安装 SSL 证书" : "未安装证书(使用自签名或 HTTP)";
|
|
4267
|
-
certStatus.style.color = data.hasCert ? "var(--success)" : "var(--text-secondary)";
|
|
4268
|
-
}
|
|
4269
|
-
|
|
4270
|
-
// Presets
|
|
4271
|
-
var presetsList = document.getElementById("presets-list");
|
|
4272
|
-
if (presetsList && cfg.commandPresets) {
|
|
4273
|
-
var html = "";
|
|
4274
|
-
for (var i = 0; i < cfg.commandPresets.length; i++) {
|
|
4275
|
-
var p = cfg.commandPresets[i];
|
|
4276
|
-
html += '<div class="preset-item">' +
|
|
4277
|
-
'<span class="preset-label">' + escapeHtml(p.label) + '</span>' +
|
|
4278
|
-
'<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
|
|
4279
|
-
'</div>';
|
|
4280
|
-
}
|
|
4281
|
-
if (!html) html = '<div class="empty-state-compact"><span class="empty-icon">\u2699</span><span>\u6ca1\u6709\u547d\u4ee4\u9884\u8bbe</span><span class="hint">\u5728 config.json \u7684 commandPresets \u4e2d\u914d\u7f6e</span></div>';
|
|
4282
|
-
presetsList.innerHTML = html;
|
|
4283
|
-
}
|
|
5542
|
+
applyLoadedSettingsData(data);
|
|
4284
5543
|
})
|
|
4285
5544
|
.catch(function() {});
|
|
4286
5545
|
}
|
|
@@ -4297,6 +5556,9 @@
|
|
|
4297
5556
|
defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
|
|
4298
5557
|
shell: (document.getElementById("cfg-shell") || {}).value,
|
|
4299
5558
|
language: (document.getElementById("cfg-language") || {}).value || "",
|
|
5559
|
+
uiPreferences: {
|
|
5560
|
+
defaultPanelState: getPanelStateSettingsFormValues(),
|
|
5561
|
+
}
|
|
4300
5562
|
};
|
|
4301
5563
|
|
|
4302
5564
|
fetch("/api/settings/config", {
|
|
@@ -4314,6 +5576,7 @@
|
|
|
4314
5576
|
} else {
|
|
4315
5577
|
msgEl.textContent = "配置已保存,部分更改需要重启后生效。";
|
|
4316
5578
|
msgEl.style.color = "var(--success)";
|
|
5579
|
+
handleSettingsConfigSaved(data.config);
|
|
4317
5580
|
}
|
|
4318
5581
|
msgEl.classList.remove("hidden");
|
|
4319
5582
|
}
|
|
@@ -4327,6 +5590,7 @@
|
|
|
4327
5590
|
});
|
|
4328
5591
|
}
|
|
4329
5592
|
|
|
5593
|
+
|
|
4330
5594
|
function uploadCertificates() {
|
|
4331
5595
|
var keyFile = document.getElementById("cert-key-file");
|
|
4332
5596
|
var certFile = document.getElementById("cert-cert-file");
|
|
@@ -4553,14 +5817,14 @@
|
|
|
4553
5817
|
function quickStartSession() {
|
|
4554
5818
|
var command = getPreferredTool();
|
|
4555
5819
|
var defaultCwd = getEffectiveCwd();
|
|
4556
|
-
var defaultMode = (state.config && state.config.defaultMode) ? state.config.defaultMode : "default";
|
|
5820
|
+
var defaultMode = getSafeModeForTool(command, (state.config && state.config.defaultMode) ? state.config.defaultMode : "default");
|
|
4557
5821
|
state.preferredCommand = command;
|
|
4558
5822
|
state.chatMode = getSafeModeForTool(command, state.chatMode);
|
|
4559
5823
|
fetch("/api/commands", {
|
|
4560
5824
|
method: "POST",
|
|
4561
5825
|
headers: { "Content-Type": "application/json" },
|
|
4562
5826
|
credentials: "same-origin",
|
|
4563
|
-
body: JSON.stringify({ command: command, cwd: defaultCwd, mode: defaultMode })
|
|
5827
|
+
body: JSON.stringify({ command: command, provider: command, cwd: defaultCwd, mode: defaultMode })
|
|
4564
5828
|
})
|
|
4565
5829
|
.then(function(res) { return res.json(); })
|
|
4566
5830
|
.then(function(data) {
|
|
@@ -4588,6 +5852,7 @@
|
|
|
4588
5852
|
var errorEl = document.getElementById("modal-error");
|
|
4589
5853
|
var command = getPreferredTool();
|
|
4590
5854
|
var sessionKind = state.sessionCreateKind || "structured";
|
|
5855
|
+
var worktreeEnabled = state.sessionCreateWorktree === true;
|
|
4591
5856
|
|
|
4592
5857
|
hideError(errorEl);
|
|
4593
5858
|
|
|
@@ -4596,22 +5861,22 @@
|
|
|
4596
5861
|
var selectedMode = getSafeModeForTool(command, state.modeValue);
|
|
4597
5862
|
|
|
4598
5863
|
if (sessionKind === "structured") {
|
|
4599
|
-
startStructuredSessionFromModal(cwd, selectedMode, errorEl);
|
|
5864
|
+
startStructuredSessionFromModal(cwd, selectedMode, worktreeEnabled, errorEl);
|
|
4600
5865
|
return;
|
|
4601
5866
|
}
|
|
4602
5867
|
|
|
4603
|
-
runPtyCommandFromModal(command, cwd, selectedMode, errorEl);
|
|
5868
|
+
runPtyCommandFromModal(command, cwd, selectedMode, worktreeEnabled, errorEl);
|
|
4604
5869
|
}
|
|
4605
5870
|
|
|
4606
|
-
function startStructuredSessionFromModal(cwd, mode, errorEl) {
|
|
4607
|
-
console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode);
|
|
5871
|
+
function startStructuredSessionFromModal(cwd, mode, worktreeEnabled, errorEl) {
|
|
5872
|
+
console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode, "worktreeEnabled:", worktreeEnabled);
|
|
4608
5873
|
_sessionCreating = true;
|
|
4609
5874
|
state.modeValue = mode;
|
|
4610
5875
|
state.chatMode = mode;
|
|
4611
5876
|
state.sessionTool = "claude";
|
|
4612
5877
|
state.preferredCommand = "claude";
|
|
4613
5878
|
syncComposerModeSelect();
|
|
4614
|
-
return createStructuredSession(undefined, cwd, mode)
|
|
5879
|
+
return createStructuredSession(undefined, cwd, mode, worktreeEnabled)
|
|
4615
5880
|
.then(function(data) {
|
|
4616
5881
|
saveWorkingDir(cwd);
|
|
4617
5882
|
closeSessionModal();
|
|
@@ -4625,8 +5890,8 @@
|
|
|
4625
5890
|
.finally(function() { _sessionCreating = false; });
|
|
4626
5891
|
}
|
|
4627
5892
|
|
|
4628
|
-
function runPtyCommandFromModal(command, cwd, mode, errorEl) {
|
|
4629
|
-
console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode);
|
|
5893
|
+
function runPtyCommandFromModal(command, cwd, mode, worktreeEnabled, errorEl) {
|
|
5894
|
+
console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode, "worktreeEnabled:", worktreeEnabled);
|
|
4630
5895
|
_sessionCreating = true;
|
|
4631
5896
|
state.modeValue = mode;
|
|
4632
5897
|
state.chatMode = mode;
|
|
@@ -4640,8 +5905,10 @@
|
|
|
4640
5905
|
credentials: "same-origin",
|
|
4641
5906
|
body: JSON.stringify({
|
|
4642
5907
|
command: command,
|
|
5908
|
+
provider: command,
|
|
4643
5909
|
cwd: cwd,
|
|
4644
|
-
mode: mode
|
|
5910
|
+
mode: mode,
|
|
5911
|
+
worktreeEnabled: worktreeEnabled
|
|
4645
5912
|
})
|
|
4646
5913
|
})
|
|
4647
5914
|
.then(function(res) { return res.json(); })
|
|
@@ -4669,7 +5936,9 @@
|
|
|
4669
5936
|
}
|
|
4670
5937
|
})
|
|
4671
5938
|
.catch(function() {
|
|
4672
|
-
showError(errorEl,
|
|
5939
|
+
showError(errorEl, command === "codex"
|
|
5940
|
+
? "无法启动 Codex 会话,请确认 codex 已正确安装并可在终端中执行。"
|
|
5941
|
+
: "无法启动 Claude 会话,请确认 Claude 已正确安装。");
|
|
4673
5942
|
})
|
|
4674
5943
|
.finally(function() { _sessionCreating = false; });
|
|
4675
5944
|
}
|
|
@@ -4957,10 +6226,25 @@
|
|
|
4957
6226
|
}
|
|
4958
6227
|
}
|
|
4959
6228
|
|
|
6229
|
+
function handleInteractiveTextInput(inputBox) {
|
|
6230
|
+
if (!state.terminalInteractive || !inputBox) return false;
|
|
6231
|
+
var value = inputBox.value || "";
|
|
6232
|
+
if (!value) return false;
|
|
6233
|
+
queueDirectInput(value, "interactive_text").catch(function() {});
|
|
6234
|
+
inputBox.value = "";
|
|
6235
|
+
autoResizeInput(inputBox);
|
|
6236
|
+
setDraftValue("", true);
|
|
6237
|
+
return true;
|
|
6238
|
+
}
|
|
6239
|
+
|
|
4960
6240
|
function handleInputPaste(event) {
|
|
4961
6241
|
var pasted = event.clipboardData && event.clipboardData.getData("text");
|
|
4962
6242
|
if (!pasted) return;
|
|
4963
6243
|
event.preventDefault();
|
|
6244
|
+
if (state.terminalInteractive) {
|
|
6245
|
+
queueDirectInput(pasted, "paste").catch(function() {});
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
4964
6248
|
var inputBox = document.getElementById("input-box");
|
|
4965
6249
|
if (inputBox) {
|
|
4966
6250
|
var start = inputBox.selectionStart || 0;
|
|
@@ -5065,6 +6349,7 @@
|
|
|
5065
6349
|
tool: getPreferredTool(),
|
|
5066
6350
|
queuedAt: Date.now()
|
|
5067
6351
|
});
|
|
6352
|
+
persistCrossSessionQueue();
|
|
5068
6353
|
renderCrossSessionQueue();
|
|
5069
6354
|
}
|
|
5070
6355
|
|
|
@@ -5089,6 +6374,7 @@
|
|
|
5089
6374
|
showToast(data.error, "error");
|
|
5090
6375
|
// 失败回填队首,不丢消息
|
|
5091
6376
|
state.crossSessionQueue.unshift(item);
|
|
6377
|
+
persistCrossSessionQueue();
|
|
5092
6378
|
renderCrossSessionQueue();
|
|
5093
6379
|
return null;
|
|
5094
6380
|
}
|
|
@@ -5098,6 +6384,7 @@
|
|
|
5098
6384
|
_queueLaunching = false;
|
|
5099
6385
|
showToast((error && error.message) || "无法启动排队会话。", "error");
|
|
5100
6386
|
state.crossSessionQueue.unshift(item);
|
|
6387
|
+
persistCrossSessionQueue();
|
|
5101
6388
|
renderCrossSessionQueue();
|
|
5102
6389
|
});
|
|
5103
6390
|
}
|
|
@@ -5106,6 +6393,7 @@
|
|
|
5106
6393
|
var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
|
|
5107
6394
|
if (idx < 0) return;
|
|
5108
6395
|
var item = state.crossSessionQueue.splice(idx, 1)[0];
|
|
6396
|
+
persistCrossSessionQueue();
|
|
5109
6397
|
renderCrossSessionQueue();
|
|
5110
6398
|
// 立即发送不受 _queueLaunching 限制
|
|
5111
6399
|
fetch("/api/commands", {
|
|
@@ -5123,12 +6411,18 @@
|
|
|
5123
6411
|
.then(function(data) {
|
|
5124
6412
|
if (data.error) {
|
|
5125
6413
|
showToast(data.error, "error");
|
|
6414
|
+
state.crossSessionQueue.splice(idx, 0, item);
|
|
6415
|
+
persistCrossSessionQueue();
|
|
6416
|
+
renderCrossSessionQueue();
|
|
5126
6417
|
return null;
|
|
5127
6418
|
}
|
|
5128
6419
|
return activateSession(data);
|
|
5129
6420
|
})
|
|
5130
6421
|
.catch(function(error) {
|
|
5131
6422
|
showToast((error && error.message) || "无法启动排队会话。", "error");
|
|
6423
|
+
state.crossSessionQueue.splice(idx, 0, item);
|
|
6424
|
+
persistCrossSessionQueue();
|
|
6425
|
+
renderCrossSessionQueue();
|
|
5132
6426
|
});
|
|
5133
6427
|
}
|
|
5134
6428
|
|
|
@@ -5136,6 +6430,7 @@
|
|
|
5136
6430
|
var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
|
|
5137
6431
|
if (idx < 0) return;
|
|
5138
6432
|
state.crossSessionQueue.splice(idx, 1);
|
|
6433
|
+
persistCrossSessionQueue();
|
|
5139
6434
|
renderCrossSessionQueue();
|
|
5140
6435
|
if (state.crossSessionQueue.length === 0) {
|
|
5141
6436
|
showToast("排队已清空。", "info");
|
|
@@ -5168,6 +6463,7 @@
|
|
|
5168
6463
|
|
|
5169
6464
|
if (state.crossSessionQueue.length === 0) {
|
|
5170
6465
|
if (container) container.remove();
|
|
6466
|
+
persistCrossSessionQueue();
|
|
5171
6467
|
return;
|
|
5172
6468
|
}
|
|
5173
6469
|
|
|
@@ -5239,6 +6535,7 @@
|
|
|
5239
6535
|
if (e.target.closest("#queue-clear-all")) {
|
|
5240
6536
|
e.preventDefault();
|
|
5241
6537
|
state.crossSessionQueue = [];
|
|
6538
|
+
persistCrossSessionQueue();
|
|
5242
6539
|
renderCrossSessionQueue();
|
|
5243
6540
|
showToast("排队已清空。", "info");
|
|
5244
6541
|
return;
|
|
@@ -5286,6 +6583,7 @@
|
|
|
5286
6583
|
credentials: "same-origin",
|
|
5287
6584
|
body: JSON.stringify({
|
|
5288
6585
|
command: preferredTool,
|
|
6586
|
+
provider: preferredTool,
|
|
5289
6587
|
cwd: defaultCwd,
|
|
5290
6588
|
mode: mode,
|
|
5291
6589
|
initialInput: value
|
|
@@ -5314,7 +6612,9 @@
|
|
|
5314
6612
|
});
|
|
5315
6613
|
})
|
|
5316
6614
|
.catch(function(error) {
|
|
5317
|
-
showToast((error && error.message) ||
|
|
6615
|
+
showToast((error && error.message) || (preferredTool === "codex"
|
|
6616
|
+
? "无法启动 Codex 会话。"
|
|
6617
|
+
: "无法启动 Claude 会话。"), "error");
|
|
5318
6618
|
welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
|
|
5319
6619
|
welcomeInput.disabled = false;
|
|
5320
6620
|
});
|
|
@@ -5353,6 +6653,7 @@
|
|
|
5353
6653
|
credentials: "same-origin",
|
|
5354
6654
|
body: JSON.stringify({
|
|
5355
6655
|
command: preferredTool,
|
|
6656
|
+
provider: preferredTool,
|
|
5356
6657
|
cwd: defaultCwd,
|
|
5357
6658
|
mode: mode,
|
|
5358
6659
|
initialInput: value || undefined
|
|
@@ -5378,7 +6679,9 @@
|
|
|
5378
6679
|
return loadOutput(data.id);
|
|
5379
6680
|
})
|
|
5380
6681
|
.catch(function(error) {
|
|
5381
|
-
showToast((error && error.message) ||
|
|
6682
|
+
showToast((error && error.message) || (preferredTool === "codex"
|
|
6683
|
+
? "无法启动 Codex 会话。"
|
|
6684
|
+
: "无法启动 Claude 会话。"), "error");
|
|
5382
6685
|
});
|
|
5383
6686
|
}
|
|
5384
6687
|
|
|
@@ -5434,7 +6737,7 @@
|
|
|
5434
6737
|
|
|
5435
6738
|
var inputBox = document.getElementById("input-box");
|
|
5436
6739
|
var value = inputBox ? inputBox.value : "";
|
|
5437
|
-
var selectedSession =
|
|
6740
|
+
var selectedSession = getSelectedSession();
|
|
5438
6741
|
if (value) {
|
|
5439
6742
|
console.log("[WAND] sendInputFromBox", {
|
|
5440
6743
|
sessionId: state.selectedId,
|
|
@@ -5455,16 +6758,12 @@
|
|
|
5455
6758
|
return postStructuredInput(value, inputBox, selectedSession);
|
|
5456
6759
|
}
|
|
5457
6760
|
|
|
5458
|
-
|
|
5459
|
-
var combinedInput = value + getControlInput("enter");
|
|
6761
|
+
var submitChunks = getTerminalSubmitChunks(selectedSession, value);
|
|
5460
6762
|
var isOffline = !state.wsConnected;
|
|
5461
6763
|
|
|
5462
6764
|
if (isOffline) {
|
|
5463
6765
|
// Offline: queue for flush on reconnect, clear input immediately
|
|
5464
|
-
|
|
5465
|
-
state.pendingMessages.shift();
|
|
5466
|
-
}
|
|
5467
|
-
state.pendingMessages.push(combinedInput);
|
|
6766
|
+
queueOfflineTerminalChunks(submitChunks);
|
|
5468
6767
|
if (inputBox) {
|
|
5469
6768
|
inputBox.value = "";
|
|
5470
6769
|
autoResizeInput(inputBox);
|
|
@@ -5479,7 +6778,11 @@
|
|
|
5479
6778
|
showToast("会话未就绪,将稍后重试。", "info");
|
|
5480
6779
|
return null;
|
|
5481
6780
|
}
|
|
5482
|
-
|
|
6781
|
+
var submitView = state.currentView;
|
|
6782
|
+
if (readySession && readySession.provider === "codex" && state.selectedId !== readySession.id) {
|
|
6783
|
+
throw new Error("Codex session changed before input send.");
|
|
6784
|
+
}
|
|
6785
|
+
return sendTerminalChunks(submitChunks, "enter_text", 0, submitView).then(function() {
|
|
5483
6786
|
// Clear input only after the send succeeds
|
|
5484
6787
|
if (inputBox && inputBox.value === value) {
|
|
5485
6788
|
inputBox.value = "";
|
|
@@ -5502,58 +6805,32 @@
|
|
|
5502
6805
|
showToast("会话不存在,请重新选择或新建会话。", "error");
|
|
5503
6806
|
return Promise.resolve();
|
|
5504
6807
|
}
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
session.messages = curMsgs;
|
|
6808
|
+
|
|
6809
|
+
var isQueueing = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
6810
|
+
if (!isQueueing) {
|
|
6811
|
+
// Immediately render user message with thinking indicator
|
|
6812
|
+
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
6813
|
+
var userMsgs = stripRenderOnlyStructuredMessages(Array.isArray(session.messages) ? session.messages.slice() : []);
|
|
6814
|
+
userMsgs.push(userTurn);
|
|
6815
|
+
var optimisticStructuredState = Object.assign({}, session.structuredState || {}, { inFlight: true });
|
|
6816
|
+
updateSessionSnapshot({
|
|
6817
|
+
id: session.id,
|
|
6818
|
+
status: "running",
|
|
6819
|
+
structuredState: optimisticStructuredState,
|
|
6820
|
+
});
|
|
6821
|
+
state.currentMessages = buildMessagesForRender(Object.assign({}, session, {
|
|
6822
|
+
status: "running",
|
|
6823
|
+
structuredState: optimisticStructuredState,
|
|
6824
|
+
}), userMsgs);
|
|
6825
|
+
updateInputHint("思考中…");
|
|
5524
6826
|
renderChat(true);
|
|
5525
|
-
showToast("已排队(第 " + state.structuredInputQueue.length + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
5526
|
-
updateStructuredQueueCounter();
|
|
5527
|
-
return Promise.resolve();
|
|
5528
6827
|
}
|
|
5529
6828
|
|
|
5530
|
-
// Immediately render user message with thinking indicator
|
|
5531
|
-
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
5532
|
-
var thinkingTurn = { role: "assistant", content: [{ type: "text", text: "", __processing: true }] };
|
|
5533
|
-
var userMsgs = Array.isArray(session.messages) ? session.messages.slice() : [];
|
|
5534
|
-
// Filter out __queued placeholders — they'll be re-appended after the new turns
|
|
5535
|
-
userMsgs = userMsgs.filter(function(m) {
|
|
5536
|
-
return !(m.role === "user" && m.content && m.content.some(function(b) { return b.__queued; }));
|
|
5537
|
-
});
|
|
5538
|
-
userMsgs.push(userTurn);
|
|
5539
|
-
userMsgs.push(thinkingTurn);
|
|
5540
|
-
// Re-append remaining queued messages after the current send
|
|
5541
|
-
appendQueuedPlaceholders(userMsgs);
|
|
5542
|
-
session.messages = userMsgs;
|
|
5543
|
-
state.currentMessages = userMsgs;
|
|
5544
|
-
// Mark inFlight optimistically to prevent double-send via WS updates
|
|
5545
|
-
if (session.structuredState) {
|
|
5546
|
-
session.structuredState.inFlight = true;
|
|
5547
|
-
}
|
|
5548
|
-
session.status = "running";
|
|
5549
6829
|
if (inputBox) {
|
|
5550
6830
|
inputBox.value = "";
|
|
5551
6831
|
autoResizeInput(inputBox);
|
|
5552
6832
|
}
|
|
5553
|
-
// Keep send button enabled so user can queue more messages
|
|
5554
|
-
updateInputHint("思考中…");
|
|
5555
6833
|
setDraftValue("");
|
|
5556
|
-
renderChat(true);
|
|
5557
6834
|
|
|
5558
6835
|
return fetch("/api/structured-sessions/" + state.selectedId + "/messages", {
|
|
5559
6836
|
method: "POST",
|
|
@@ -5568,27 +6845,29 @@
|
|
|
5568
6845
|
}
|
|
5569
6846
|
if (snapshot && snapshot.id) {
|
|
5570
6847
|
updateSessionSnapshot(snapshot);
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
// Re-append queued user messages
|
|
5574
|
-
appendQueuedPlaceholders(state.currentMessages);
|
|
5575
|
-
}
|
|
6848
|
+
var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
6849
|
+
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
5576
6850
|
renderChat(true);
|
|
5577
|
-
|
|
6851
|
+
if (isQueueing) {
|
|
6852
|
+
var queuedCount = getStructuredQueuedInputs(refreshedSession).length;
|
|
6853
|
+
showToast("已排队(第 " + queuedCount + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
6854
|
+
} else {
|
|
6855
|
+
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
6856
|
+
}
|
|
5578
6857
|
}
|
|
5579
6858
|
})
|
|
5580
6859
|
.catch(function(error) {
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
session.structuredState
|
|
5584
|
-
}
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
6860
|
+
updateSessionSnapshot({
|
|
6861
|
+
id: session.id,
|
|
6862
|
+
structuredState: Object.assign({}, session.structuredState || {}, { inFlight: false }),
|
|
6863
|
+
});
|
|
6864
|
+
var message = (error && error.message) || "";
|
|
6865
|
+
var isTransientAbort =
|
|
6866
|
+
message === "Failed to fetch" ||
|
|
6867
|
+
message === "NetworkError when attempting to fetch resource." ||
|
|
6868
|
+
message === "Load failed" ||
|
|
6869
|
+
/aborted|aborterror|networkerror|failed to fetch/i.test(message);
|
|
6870
|
+
if (!isTransientAbort) {
|
|
5592
6871
|
showToast((error && error.message) || "无法发送结构化消息。", "error");
|
|
5593
6872
|
}
|
|
5594
6873
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
@@ -5602,7 +6881,7 @@
|
|
|
5602
6881
|
|
|
5603
6882
|
function updateStructuredQueueCounter() {
|
|
5604
6883
|
var counter = document.getElementById("queue-counter");
|
|
5605
|
-
var count =
|
|
6884
|
+
var count = getSelectedStructuredQueuedInputs().length;
|
|
5606
6885
|
if (counter) {
|
|
5607
6886
|
counter.textContent = "队列: " + count;
|
|
5608
6887
|
if (count > 0) {
|
|
@@ -5615,56 +6894,48 @@
|
|
|
5615
6894
|
|
|
5616
6895
|
// Append queued user message placeholders to currentMessages so they
|
|
5617
6896
|
// remain visible across WS updates and re-renders.
|
|
5618
|
-
function
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
6897
|
+
function buildMessagesForRender(session, messages) {
|
|
6898
|
+
var sanitized = Array.isArray(messages) ? stripRenderOnlyStructuredMessages(messages) : [];
|
|
6899
|
+
var base = Array.isArray(sanitized) ? sanitized.slice() : [];
|
|
6900
|
+
if (!session || session.sessionKind !== "structured") {
|
|
6901
|
+
return base;
|
|
5622
6902
|
}
|
|
5623
|
-
|
|
6903
|
+
var queued = getStructuredQueuedInputs(session);
|
|
6904
|
+
if (queued && queued.length > 0) {
|
|
6905
|
+
for (var qi = 0; qi < queued.length; qi++) {
|
|
6906
|
+
base.push({ role: "user", content: [{ type: "text", text: queued[qi], __queued: true }] });
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
if (session.structuredState && session.structuredState.inFlight) {
|
|
6910
|
+
var last = base[base.length - 1];
|
|
6911
|
+
if (!last || last.role !== "assistant") {
|
|
6912
|
+
base.push({ role: "assistant", content: [{ type: "text", text: "", __processing: true }] });
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
return base;
|
|
5624
6916
|
}
|
|
5625
6917
|
|
|
6918
|
+
|
|
5626
6919
|
function flushStructuredInputQueue() {
|
|
5627
|
-
if (state.structuredInputQueue.length === 0) return;
|
|
5628
6920
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
5629
|
-
|
|
5630
|
-
state.structuredInputQueue = [];
|
|
5631
|
-
updateStructuredQueueCounter();
|
|
5632
|
-
return;
|
|
5633
|
-
}
|
|
5634
|
-
// Only flush if not inFlight
|
|
5635
|
-
if (session.structuredState && session.structuredState.inFlight) return;
|
|
5636
|
-
var nextInput = state.structuredInputQueue.shift();
|
|
6921
|
+
syncStructuredQueueFromSession(session);
|
|
5637
6922
|
updateStructuredQueueCounter();
|
|
5638
|
-
if (nextInput) {
|
|
5639
|
-
// Remove __queued marker from the matching user turn already in chat.
|
|
5640
|
-
// postStructuredInput will find it's not inFlight now and do the
|
|
5641
|
-
// normal send path, which re-adds the user turn + thinking turn.
|
|
5642
|
-
// So we need to remove the queued placeholder first to avoid duplicates.
|
|
5643
|
-
var msgs = Array.isArray(state.currentMessages) ? state.currentMessages : [];
|
|
5644
|
-
for (var qi = msgs.length - 1; qi >= 0; qi--) {
|
|
5645
|
-
var qm = msgs[qi];
|
|
5646
|
-
if (qm.role === "user" && qm.content && qm.content.some(function(b) {
|
|
5647
|
-
return b.__queued && b.text === nextInput;
|
|
5648
|
-
})) {
|
|
5649
|
-
msgs.splice(qi, 1);
|
|
5650
|
-
break;
|
|
5651
|
-
}
|
|
5652
|
-
}
|
|
5653
|
-
state.currentMessages = msgs;
|
|
5654
|
-
if (session.messages) session.messages = msgs;
|
|
5655
|
-
// Pass null for inputBox to avoid clearing user's current typing
|
|
5656
|
-
postStructuredInput(nextInput, null, session);
|
|
5657
|
-
}
|
|
5658
6923
|
}
|
|
5659
6924
|
|
|
5660
6925
|
function getInputErrorMessage(error) {
|
|
6926
|
+
var selectedSession = getSelectedSession();
|
|
6927
|
+
var isCodex = selectedSession && selectedSession.provider === "codex";
|
|
5661
6928
|
if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
|
|
5662
|
-
return
|
|
6929
|
+
return isCodex
|
|
6930
|
+
? "Codex 会话已结束,请新建会话后继续。"
|
|
6931
|
+
: "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
|
|
5663
6932
|
}
|
|
5664
6933
|
if (error && error.errorCode === "SESSION_NOT_FOUND") {
|
|
5665
6934
|
return "会话不存在,请重新选择或新建会话。";
|
|
5666
6935
|
}
|
|
5667
|
-
return (error && error.message) ||
|
|
6936
|
+
return (error && error.message) || (isCodex
|
|
6937
|
+
? "Codex 会话暂不可用,请检查终端视图或新建会话。"
|
|
6938
|
+
: "会话暂不可用;若存在 Claude 历史会话,将自动尝试恢复。");
|
|
5668
6939
|
}
|
|
5669
6940
|
|
|
5670
6941
|
function buildInputError(payload) {
|
|
@@ -5704,7 +6975,7 @@
|
|
|
5704
6975
|
}
|
|
5705
6976
|
|
|
5706
6977
|
function canAutoResumeSession(session) {
|
|
5707
|
-
return !!(session && session.status === "exited" && session.claudeSessionId && hasRealConversationHistory(session));
|
|
6978
|
+
return !!(session && session.provider === "claude" && session.status === "exited" && session.claudeSessionId && hasRealConversationHistory(session));
|
|
5708
6979
|
}
|
|
5709
6980
|
|
|
5710
6981
|
function ensureSessionReadyForInput(session, errorEl) {
|
|
@@ -5735,11 +7006,48 @@
|
|
|
5735
7006
|
});
|
|
5736
7007
|
}
|
|
5737
7008
|
|
|
5738
|
-
function
|
|
7009
|
+
function getTerminalSubmitChunks(session, text) {
|
|
7010
|
+
if (session && session.provider === "codex") {
|
|
7011
|
+
return [text, String.fromCharCode(13)];
|
|
7012
|
+
}
|
|
7013
|
+
return [text + String.fromCharCode(13)];
|
|
7014
|
+
}
|
|
7015
|
+
|
|
7016
|
+
function sendTerminalChunks(chunks, shortcutKey, delayMs, viewOverride) {
|
|
7017
|
+
var sequence = Array.isArray(chunks) ? chunks.filter(function(chunk) { return !!chunk; }) : [];
|
|
7018
|
+
if (sequence.length === 0) {
|
|
7019
|
+
return Promise.resolve();
|
|
7020
|
+
}
|
|
7021
|
+
var delay = typeof delayMs === "number" ? delayMs : 0;
|
|
7022
|
+
return sequence.reduce(function(promise, chunk, index) {
|
|
7023
|
+
return promise.then(function() {
|
|
7024
|
+
if (index > 0 && delay > 0) {
|
|
7025
|
+
return new Promise(function(resolve) {
|
|
7026
|
+
setTimeout(resolve, delay);
|
|
7027
|
+
}).then(function() {
|
|
7028
|
+
return queueDirectInput(chunk, index === sequence.length - 1 ? shortcutKey : undefined, viewOverride);
|
|
7029
|
+
});
|
|
7030
|
+
}
|
|
7031
|
+
return queueDirectInput(chunk, index === sequence.length - 1 ? shortcutKey : undefined, viewOverride);
|
|
7032
|
+
});
|
|
7033
|
+
}, Promise.resolve());
|
|
7034
|
+
}
|
|
7035
|
+
|
|
7036
|
+
function queueOfflineTerminalChunks(chunks) {
|
|
7037
|
+
var sequence = Array.isArray(chunks) ? chunks.filter(function(chunk) { return !!chunk; }) : [];
|
|
7038
|
+
sequence.forEach(function(chunk) {
|
|
7039
|
+
if (state.pendingMessages.length >= 100) {
|
|
7040
|
+
state.pendingMessages.shift();
|
|
7041
|
+
}
|
|
7042
|
+
state.pendingMessages.push(chunk);
|
|
7043
|
+
});
|
|
7044
|
+
}
|
|
7045
|
+
|
|
7046
|
+
function queueDirectInput(input, shortcutKey, viewOverride) {
|
|
5739
7047
|
if (!input || !state.selectedId) return Promise.resolve();
|
|
5740
7048
|
state.messageQueue.push(input);
|
|
5741
7049
|
state.inputQueue = state.inputQueue.then(function() {
|
|
5742
|
-
return postInput(input, shortcutKey).finally(function() {
|
|
7050
|
+
return postInput(input, shortcutKey, viewOverride).finally(function() {
|
|
5743
7051
|
var idx = state.messageQueue.indexOf(input);
|
|
5744
7052
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
5745
7053
|
scheduleMobileDomUpdate();
|
|
@@ -5748,8 +7056,9 @@
|
|
|
5748
7056
|
return state.inputQueue;
|
|
5749
7057
|
}
|
|
5750
7058
|
|
|
5751
|
-
function postInput(input, shortcutKey) {
|
|
7059
|
+
function postInput(input, shortcutKey, viewOverride) {
|
|
5752
7060
|
if (!state.selectedId) return Promise.resolve();
|
|
7061
|
+
var effectiveView = viewOverride || state.currentView;
|
|
5753
7062
|
|
|
5754
7063
|
// Pre-check: don't send if session is not running
|
|
5755
7064
|
if (!isSelectedSessionRunning()) {
|
|
@@ -5788,7 +7097,7 @@
|
|
|
5788
7097
|
console.log("[wand] postInput: sending", {
|
|
5789
7098
|
sessionId: state.selectedId,
|
|
5790
7099
|
inputLength: input.length,
|
|
5791
|
-
view:
|
|
7100
|
+
view: effectiveView,
|
|
5792
7101
|
wsConnected: state.wsConnected
|
|
5793
7102
|
});
|
|
5794
7103
|
|
|
@@ -5796,7 +7105,7 @@
|
|
|
5796
7105
|
method: "POST",
|
|
5797
7106
|
headers: { "Content-Type": "application/json" },
|
|
5798
7107
|
credentials: "same-origin",
|
|
5799
|
-
body: JSON.stringify({ input: input, view:
|
|
7108
|
+
body: JSON.stringify({ input: input, view: effectiveView, shortcutKey: shortcutKey || undefined })
|
|
5800
7109
|
})
|
|
5801
7110
|
.then(function(res) {
|
|
5802
7111
|
if (!res.ok) {
|
|
@@ -5834,6 +7143,14 @@
|
|
|
5834
7143
|
return queueDirectInput(input);
|
|
5835
7144
|
}
|
|
5836
7145
|
|
|
7146
|
+
function getSelectedSession() {
|
|
7147
|
+
return state.sessions.find(function(session) { return session.id === state.selectedId; }) || null;
|
|
7148
|
+
}
|
|
7149
|
+
|
|
7150
|
+
function getTerminalSubmitSequence(session) {
|
|
7151
|
+
return session && session.provider === "codex" ? "\n" : String.fromCharCode(13);
|
|
7152
|
+
}
|
|
7153
|
+
|
|
5837
7154
|
function isTerminalInteractionAvailable() {
|
|
5838
7155
|
return !!state.selectedId && state.currentView === "terminal";
|
|
5839
7156
|
}
|
|
@@ -6004,6 +7321,9 @@
|
|
|
6004
7321
|
function updateInteractiveControls() {
|
|
6005
7322
|
var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
|
|
6006
7323
|
var structured = isStructuredSession(selectedSession);
|
|
7324
|
+
var isCodex = selectedSession && selectedSession.provider === "codex";
|
|
7325
|
+
var isRunning = !!selectedSession && selectedSession.status === "running";
|
|
7326
|
+
var composer = document.getElementById("input-box");
|
|
6007
7327
|
// Update both toggle buttons (topbar and terminal-header)
|
|
6008
7328
|
var toggles = ["terminal-interactive-toggle-top"];
|
|
6009
7329
|
toggles.forEach(function(id) {
|
|
@@ -6019,7 +7339,27 @@
|
|
|
6019
7339
|
var expandedRow = document.querySelector(".inline-shortcuts-expanded-row");
|
|
6020
7340
|
if (expandedRow) expandedRow.classList.toggle("hidden", structured || state.currentView !== "terminal");
|
|
6021
7341
|
var inputHint = document.querySelector(".input-hint");
|
|
6022
|
-
if (inputHint)
|
|
7342
|
+
if (inputHint) {
|
|
7343
|
+
inputHint.classList.toggle("hidden", structured ? true : state.currentView === "terminal");
|
|
7344
|
+
if (!structured && selectedSession) {
|
|
7345
|
+
inputHint.textContent = isCodex
|
|
7346
|
+
? "Enter 发送 · chat 为解析视图,terminal 为原始输出"
|
|
7347
|
+
: "Enter 发送 · Shift+Enter 换行";
|
|
7348
|
+
}
|
|
7349
|
+
}
|
|
7350
|
+
var disableStructuredInput = !!selectedSession && structured && isCodex && !isRunning;
|
|
7351
|
+
if (composer) {
|
|
7352
|
+
composer.placeholder = getComposerPlaceholder(selectedSession, state.terminalInteractive);
|
|
7353
|
+
composer.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning);
|
|
7354
|
+
composer.setAttribute("aria-disabled", composer.disabled ? "true" : "false");
|
|
7355
|
+
}
|
|
7356
|
+
var sendBtn = document.getElementById("send-input-button");
|
|
7357
|
+
if (sendBtn) {
|
|
7358
|
+
sendBtn.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning);
|
|
7359
|
+
sendBtn.setAttribute("title", isCodex
|
|
7360
|
+
? (isRunning ? "发送给 Codex" : "Codex 会话已结束")
|
|
7361
|
+
: (structured ? "发送" : (!selectedSession || isRunning ? "发送" : "会话已结束")));
|
|
7362
|
+
}
|
|
6023
7363
|
var container = document.getElementById("output");
|
|
6024
7364
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
6025
7365
|
}
|
|
@@ -7345,6 +8685,10 @@
|
|
|
7345
8685
|
}
|
|
7346
8686
|
|
|
7347
8687
|
function focusInputFromTap() {
|
|
8688
|
+
if (state.terminalInteractive) {
|
|
8689
|
+
focusTerminalContainer();
|
|
8690
|
+
return;
|
|
8691
|
+
}
|
|
7348
8692
|
var inputBox = document.getElementById('input-box');
|
|
7349
8693
|
if (!inputBox || !state.selectedId || document.activeElement === inputBox) return;
|
|
7350
8694
|
focusInputWithSelection(inputBox);
|
|
@@ -7355,6 +8699,10 @@
|
|
|
7355
8699
|
if (!output) return;
|
|
7356
8700
|
output.setAttribute("tabindex", "0");
|
|
7357
8701
|
output.focus();
|
|
8702
|
+
var terminalTextarea = output.querySelector(".xterm-helper-textarea");
|
|
8703
|
+
if (terminalTextarea && typeof terminalTextarea.focus === "function") {
|
|
8704
|
+
terminalTextarea.focus();
|
|
8705
|
+
}
|
|
7358
8706
|
}
|
|
7359
8707
|
|
|
7360
8708
|
// Mobile keyboard handling
|
|
@@ -7450,7 +8798,7 @@
|
|
|
7450
8798
|
|
|
7451
8799
|
function initTerminalResizeHandle() {
|
|
7452
8800
|
// 终端容器拖动调整大小功能
|
|
7453
|
-
var container = document.getElementById("
|
|
8801
|
+
var container = document.getElementById("output");
|
|
7454
8802
|
if (!container) return;
|
|
7455
8803
|
|
|
7456
8804
|
// 创建拖动手柄
|
|
@@ -7747,27 +9095,19 @@
|
|
|
7747
9095
|
if (msg.data.messages) {
|
|
7748
9096
|
snapshot.messages = msg.data.messages;
|
|
7749
9097
|
}
|
|
9098
|
+
if (msg.data.queuedMessages) {
|
|
9099
|
+
snapshot.queuedMessages = msg.data.queuedMessages;
|
|
9100
|
+
}
|
|
9101
|
+
if (msg.data.structuredState) {
|
|
9102
|
+
snapshot.structuredState = msg.data.structuredState;
|
|
9103
|
+
}
|
|
7750
9104
|
updateSessionSnapshot(snapshot);
|
|
7751
9105
|
if (msg.sessionId === state.selectedId) {
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
if (msg.data.sessionKind === 'structured') {
|
|
7755
|
-
appendQueuedPlaceholders(state.currentMessages);
|
|
7756
|
-
}
|
|
7757
|
-
// Structured session with inFlight: keep __processing placeholder
|
|
7758
|
-
// so the loading indicator stays visible until assistant content arrives
|
|
7759
|
-
if (msg.data.sessionKind === 'structured') {
|
|
7760
|
-
var outSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
7761
|
-
if (outSession && outSession.structuredState && outSession.structuredState.inFlight) {
|
|
7762
|
-
var lastCur = state.currentMessages[state.currentMessages.length - 1];
|
|
7763
|
-
if (!lastCur || lastCur.role !== 'assistant') {
|
|
7764
|
-
state.currentMessages.push({ role: "assistant", content: [{ type: "text", text: "", __processing: true }] });
|
|
7765
|
-
}
|
|
7766
|
-
}
|
|
7767
|
-
}
|
|
9106
|
+
var updatedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; }) || snapshot;
|
|
9107
|
+
state.currentMessages = buildMessagesForRender(updatedSession, getPreferredMessages(updatedSession, msg.data.output, false));
|
|
7768
9108
|
updateTaskDisplay();
|
|
7769
9109
|
// Structured sessions: render immediately for responsiveness
|
|
7770
|
-
if (msg.data.sessionKind === 'structured') {
|
|
9110
|
+
if (updatedSession.sessionKind === 'structured' || msg.data.sessionKind === 'structured') {
|
|
7771
9111
|
renderChat();
|
|
7772
9112
|
} else {
|
|
7773
9113
|
scheduleChatRender();
|
|
@@ -7780,10 +9120,13 @@
|
|
|
7780
9120
|
if (msg.data.chunk && (!state.terminalSessionId || state.terminalSessionId === msg.sessionId)) {
|
|
7781
9121
|
// Fast path: write chunk directly to avoid full-output comparison
|
|
7782
9122
|
// which can trigger terminal.reset() and cause screen flicker.
|
|
9123
|
+
state.terminalLiveStreamSessions[msg.sessionId] = true;
|
|
7783
9124
|
state.terminal.write(msg.data.chunk);
|
|
7784
9125
|
state.terminalSessionId = msg.sessionId;
|
|
7785
9126
|
if (msg.data.output) {
|
|
7786
9127
|
state.terminalOutput = normalizeTerminalOutput(msg.data.output);
|
|
9128
|
+
} else {
|
|
9129
|
+
state.terminalOutput = normalizeTerminalOutput((state.terminalOutput || "") + msg.data.chunk);
|
|
7787
9130
|
}
|
|
7788
9131
|
maybeScrollTerminalToBottom("output");
|
|
7789
9132
|
updateTerminalJumpToBottomButton();
|
|
@@ -7811,6 +9154,9 @@
|
|
|
7811
9154
|
if (msg.data && msg.data.structuredState) {
|
|
7812
9155
|
endedSnapshot.structuredState = msg.data.structuredState;
|
|
7813
9156
|
}
|
|
9157
|
+
if (msg.data && msg.data.queuedMessages) {
|
|
9158
|
+
endedSnapshot.queuedMessages = msg.data.queuedMessages;
|
|
9159
|
+
}
|
|
7814
9160
|
updateSessionSnapshot(endedSnapshot);
|
|
7815
9161
|
|
|
7816
9162
|
if (msg.sessionId === state.selectedId) {
|
|
@@ -7846,22 +9192,24 @@
|
|
|
7846
9192
|
}
|
|
7847
9193
|
|
|
7848
9194
|
// Clear stale queued inputs for PTY sessions.
|
|
7849
|
-
// For structured sessions,
|
|
7850
|
-
// the session terminated), so we must NOT clear the structured queue —
|
|
7851
|
-
// instead, flush the next queued message.
|
|
9195
|
+
// For structured sessions, the queue is now managed by the server snapshot.
|
|
7852
9196
|
state.messageQueue = [];
|
|
7853
9197
|
state.pendingMessages = [];
|
|
7854
9198
|
|
|
7855
9199
|
var endedSessionObj = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
7856
|
-
var
|
|
9200
|
+
var selectedSessionObj = msg.sessionId === state.selectedId
|
|
9201
|
+
? state.sessions.find(function(s) { return s.id === state.selectedId; })
|
|
9202
|
+
: null;
|
|
9203
|
+
var isStructuredEnded = !!(
|
|
9204
|
+
(endedSessionObj && endedSessionObj.sessionKind === "structured") ||
|
|
9205
|
+
(selectedSessionObj && selectedSessionObj.sessionKind === "structured")
|
|
9206
|
+
);
|
|
7857
9207
|
|
|
7858
|
-
if (isStructuredEnded && msg.sessionId === state.selectedId
|
|
7859
|
-
|
|
7860
|
-
// Structured session turn completed — flush next queued message
|
|
7861
|
-
setTimeout(flushStructuredInputQueue, 50);
|
|
9208
|
+
if (isStructuredEnded && msg.sessionId === state.selectedId) {
|
|
9209
|
+
flushStructuredInputQueue();
|
|
7862
9210
|
} else if (!isStructuredEnded) {
|
|
7863
|
-
// PTY session ended — clear structured queue too
|
|
7864
9211
|
state.structuredInputQueue = [];
|
|
9212
|
+
clearStructuredQueuePersistence(state.selectedId);
|
|
7865
9213
|
updateStructuredQueueCounter();
|
|
7866
9214
|
}
|
|
7867
9215
|
|
|
@@ -7896,7 +9244,13 @@
|
|
|
7896
9244
|
// Initial state for subscribed session (after reconnect or subscription)
|
|
7897
9245
|
if (msg.sessionId === state.selectedId && msg.data) {
|
|
7898
9246
|
if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
|
|
7899
|
-
|
|
9247
|
+
updateSessionSnapshot(msg.data);
|
|
9248
|
+
var initSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
9249
|
+
state.currentMessages = buildMessagesForRender(initSession || msg.data, getPreferredMessages(initSession || msg.data, msg.data.output, false));
|
|
9250
|
+
renderChat(true);
|
|
9251
|
+
updateTaskDisplay();
|
|
9252
|
+
updateApprovalStats();
|
|
9253
|
+
updateTerminalOutput(msg.data.output || "", msg.sessionId, "append");
|
|
7900
9254
|
// Ensure terminal is properly fitted after receiving initial data
|
|
7901
9255
|
scheduleTerminalResize(true);
|
|
7902
9256
|
}
|
|
@@ -7913,7 +9267,6 @@
|
|
|
7913
9267
|
break;
|
|
7914
9268
|
case 'status':
|
|
7915
9269
|
if (msg.sessionId && msg.data) {
|
|
7916
|
-
console.log('[WAND] ws status', msg.sessionId, JSON.stringify(msg.data));
|
|
7917
9270
|
var statusUpdate = { id: msg.sessionId };
|
|
7918
9271
|
if (Object.prototype.hasOwnProperty.call(msg.data, 'status')) {
|
|
7919
9272
|
statusUpdate.status = msg.data.status;
|
|
@@ -8026,8 +9379,14 @@
|
|
|
8026
9379
|
var permissionLabel = document.getElementById("permission-actions-label");
|
|
8027
9380
|
if (!taskEl) return;
|
|
8028
9381
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
9382
|
+
if (selectedSession && selectedSession.provider === "codex") {
|
|
9383
|
+
if (permissionActionsEl) permissionActionsEl.classList.add("hidden");
|
|
9384
|
+
taskEl.classList.remove("permission-blocked");
|
|
9385
|
+
}
|
|
8029
9386
|
var pendingEscalation = selectedSession && selectedSession.pendingEscalation ? selectedSession.pendingEscalation : null;
|
|
8030
|
-
var isBlocked =
|
|
9387
|
+
var isBlocked = selectedSession && selectedSession.provider !== "codex"
|
|
9388
|
+
? (pendingEscalation || selectedSession.permissionBlocked)
|
|
9389
|
+
: false;
|
|
8031
9390
|
|
|
8032
9391
|
if (isBlocked) {
|
|
8033
9392
|
var isAutoApprove = selectedSession && selectedSession.autoApprovePermissions;
|
|
@@ -8161,6 +9520,11 @@
|
|
|
8161
9520
|
|
|
8162
9521
|
function toggleAutoApprove() {
|
|
8163
9522
|
if (!state.selectedId) return;
|
|
9523
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
9524
|
+
if (selectedSession && selectedSession.provider === "codex") {
|
|
9525
|
+
showToast("Codex 会话固定以 full-access PTY 启动,不支持切换自动批准。", "info");
|
|
9526
|
+
return;
|
|
9527
|
+
}
|
|
8164
9528
|
var toggle = document.getElementById("auto-approve-toggle");
|
|
8165
9529
|
if (toggle) toggle.style.opacity = "0.5";
|
|
8166
9530
|
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/toggle-auto-approve", {
|
|
@@ -8190,6 +9554,12 @@
|
|
|
8190
9554
|
var toggle = document.getElementById("auto-approve-toggle");
|
|
8191
9555
|
if (!toggle) return;
|
|
8192
9556
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
9557
|
+
if (selectedSession && selectedSession.provider === "codex") {
|
|
9558
|
+
toggle.className = "auto-approve-indicator active";
|
|
9559
|
+
toggle.title = "Codex 固定以 full-access PTY 启动,不支持切换自动批准";
|
|
9560
|
+
toggle.textContent = "🛡 Codex 固定全权限";
|
|
9561
|
+
return;
|
|
9562
|
+
}
|
|
8193
9563
|
var enabled = selectedSession && selectedSession.autoApprovePermissions;
|
|
8194
9564
|
if (enabled) {
|
|
8195
9565
|
toggle.className = "auto-approve-indicator active";
|
|
@@ -8222,6 +9592,10 @@
|
|
|
8222
9592
|
}
|
|
8223
9593
|
applyCurrentView();
|
|
8224
9594
|
reconcileInteractiveState();
|
|
9595
|
+
var selectedSession = getSelectedSession();
|
|
9596
|
+
if (selectedSession) {
|
|
9597
|
+
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, selectedSession.output, true));
|
|
9598
|
+
}
|
|
8225
9599
|
updateTerminalJumpToBottomButton();
|
|
8226
9600
|
if (state.currentView === "terminal") {
|
|
8227
9601
|
state.terminalViewportSize = { width: 0, height: 0 };
|
|
@@ -8260,10 +9634,7 @@
|
|
|
8260
9634
|
// Re-parse messages from the latest session output (fallback for edge cases)
|
|
8261
9635
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8262
9636
|
if (selectedSession) {
|
|
8263
|
-
|
|
8264
|
-
if (selectedSession.sessionKind === "structured") {
|
|
8265
|
-
appendQueuedPlaceholders(state.currentMessages);
|
|
8266
|
-
}
|
|
9637
|
+
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, selectedSession.output, true));
|
|
8267
9638
|
}
|
|
8268
9639
|
renderChat();
|
|
8269
9640
|
}, 30);
|
|
@@ -8353,6 +9724,26 @@
|
|
|
8353
9724
|
return systemInfo;
|
|
8354
9725
|
}
|
|
8355
9726
|
|
|
9727
|
+
function ensureChatMessagesContainer(chatOutput) {
|
|
9728
|
+
if (!chatOutput) return null;
|
|
9729
|
+
var chatMessages = chatOutput.querySelector(".chat-messages");
|
|
9730
|
+
if (chatMessages) return chatMessages;
|
|
9731
|
+
chatMessages = document.createElement("div");
|
|
9732
|
+
chatMessages.className = "chat-messages";
|
|
9733
|
+
chatOutput.appendChild(chatMessages);
|
|
9734
|
+
return chatMessages;
|
|
9735
|
+
}
|
|
9736
|
+
|
|
9737
|
+
function renderChatEmptyState(chatOutput, html) {
|
|
9738
|
+
var chatMessages = ensureChatMessagesContainer(chatOutput);
|
|
9739
|
+
if (!chatMessages) return null;
|
|
9740
|
+
chatMessages.innerHTML = html;
|
|
9741
|
+
bindChatScrollListener();
|
|
9742
|
+
updateChatFollowToggleButton();
|
|
9743
|
+
updateChatJumpToBottomButton();
|
|
9744
|
+
return chatMessages;
|
|
9745
|
+
}
|
|
9746
|
+
|
|
8356
9747
|
function doRenderChat(forceFullRender) {
|
|
8357
9748
|
var chatOutput = document.getElementById("chat-output");
|
|
8358
9749
|
if (!chatOutput) return;
|
|
@@ -8360,7 +9751,7 @@
|
|
|
8360
9751
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8361
9752
|
if (!selectedSession) {
|
|
8362
9753
|
if (state.lastRenderedEmpty !== "none") {
|
|
8363
|
-
chatOutput
|
|
9754
|
+
renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>未选择会话</strong><br>点击上方「新对话」开始你的第一次对话。</div>');
|
|
8364
9755
|
state.lastRenderedEmpty = "none";
|
|
8365
9756
|
state.lastRenderedMsgCount = 0;
|
|
8366
9757
|
}
|
|
@@ -8371,7 +9762,7 @@
|
|
|
8371
9762
|
|
|
8372
9763
|
if (messages.length === 0) {
|
|
8373
9764
|
if (state.lastRenderedEmpty !== "empty") {
|
|
8374
|
-
chatOutput
|
|
9765
|
+
renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>对话已开始</strong><br>在下方输入框发送消息,Claude 会自动回复。</div>');
|
|
8375
9766
|
state.lastRenderedEmpty = "empty";
|
|
8376
9767
|
state.lastRenderedMsgCount = 0;
|
|
8377
9768
|
}
|
|
@@ -8424,12 +9815,8 @@
|
|
|
8424
9815
|
state.lastRenderedMsgCount = msgCount;
|
|
8425
9816
|
state.lastRenderedHash = outputHash;
|
|
8426
9817
|
|
|
8427
|
-
var chatMessages = chatOutput
|
|
8428
|
-
if (!chatMessages)
|
|
8429
|
-
// First render - create container
|
|
8430
|
-
chatOutput.innerHTML = '<div class="chat-messages"></div>';
|
|
8431
|
-
chatMessages = chatOutput.querySelector(".chat-messages");
|
|
8432
|
-
}
|
|
9818
|
+
var chatMessages = ensureChatMessagesContainer(chatOutput);
|
|
9819
|
+
if (!chatMessages) return;
|
|
8433
9820
|
|
|
8434
9821
|
var existingCount = chatMessages.querySelectorAll(".chat-message").length;
|
|
8435
9822
|
// Full render when: forced, no existing messages, or message count decreased/changed
|
|
@@ -8447,7 +9834,7 @@
|
|
|
8447
9834
|
for (var i = 0; i < reversedMessages.length; i++) {
|
|
8448
9835
|
var msg = reversedMessages[i];
|
|
8449
9836
|
var originalIndex = msgCount - 1 - i; // Original index in messages array
|
|
8450
|
-
|
|
9837
|
+
|
|
8451
9838
|
// Find system info for this message position
|
|
8452
9839
|
var sysInfo = null;
|
|
8453
9840
|
for (var j = 0; j < systemInfo.length; j++) {
|
|
@@ -8456,7 +9843,7 @@
|
|
|
8456
9843
|
break;
|
|
8457
9844
|
}
|
|
8458
9845
|
}
|
|
8459
|
-
|
|
9846
|
+
|
|
8460
9847
|
// Render system info card if exists
|
|
8461
9848
|
if (sysInfo) {
|
|
8462
9849
|
html += '<div class="chat-message system-info">' +
|
|
@@ -8466,21 +9853,30 @@
|
|
|
8466
9853
|
'</div>' +
|
|
8467
9854
|
'</div>';
|
|
8468
9855
|
}
|
|
8469
|
-
|
|
9856
|
+
|
|
8470
9857
|
// Render message
|
|
8471
|
-
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null);
|
|
9858
|
+
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
|
|
8472
9859
|
}
|
|
8473
|
-
|
|
9860
|
+
|
|
8474
9861
|
chatMessages.innerHTML = html;
|
|
8475
9862
|
attachAllCopyHandlers(chatMessages);
|
|
9863
|
+
bindChatScrollListener();
|
|
9864
|
+
applyPersistedExpandState(chatMessages);
|
|
8476
9865
|
// Only expand the single newest tool card (first chat-message = newest due to column-reverse)
|
|
8477
9866
|
var firstMsg = chatMessages.querySelector(".chat-message:not(.system-info)");
|
|
8478
9867
|
if (firstMsg) {
|
|
8479
9868
|
var cards = firstMsg.querySelectorAll(".tool-use-card");
|
|
8480
9869
|
if (cards.length > 0) {
|
|
8481
|
-
cards[0]
|
|
9870
|
+
var firstCard = cards[0];
|
|
9871
|
+
var firstCardKey = getElementExpandKey(firstCard);
|
|
9872
|
+
if (!hasPersistedExpandState(firstCardKey) && !getConfiguredPanelDefaults().structuredToolCardExpanded) {
|
|
9873
|
+
firstCard.classList.remove("collapsed");
|
|
9874
|
+
}
|
|
8482
9875
|
for (var ci = 1; ci < cards.length; ci++) {
|
|
8483
|
-
cards[ci]
|
|
9876
|
+
var cardKey = getElementExpandKey(cards[ci]);
|
|
9877
|
+
if (!hasPersistedExpandState(cardKey) && !getConfiguredPanelDefaults().structuredToolCardExpanded) {
|
|
9878
|
+
cards[ci].classList.add("collapsed");
|
|
9879
|
+
}
|
|
8484
9880
|
}
|
|
8485
9881
|
}
|
|
8486
9882
|
}
|
|
@@ -8495,6 +9891,8 @@
|
|
|
8495
9891
|
function collapseOldToolCards(container, newEls) {
|
|
8496
9892
|
var allCards = container.querySelectorAll(".tool-use-card");
|
|
8497
9893
|
allCards.forEach(function(c) {
|
|
9894
|
+
var cardKey = getElementExpandKey(c);
|
|
9895
|
+
if (hasPersistedExpandState(cardKey) || getConfiguredPanelDefaults().structuredToolCardExpanded) return;
|
|
8498
9896
|
// Keep expanded if this card is inside a newly added message
|
|
8499
9897
|
if (newEls) {
|
|
8500
9898
|
for (var i = 0; i < newEls.length; i++) {
|
|
@@ -8558,7 +9956,7 @@
|
|
|
8558
9956
|
for (var i = 0; i < newMessages.length; i++) {
|
|
8559
9957
|
var div = document.createElement("div");
|
|
8560
9958
|
var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
|
|
8561
|
-
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null);
|
|
9959
|
+
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
|
|
8562
9960
|
var el = div.firstElementChild;
|
|
8563
9961
|
if (el) {
|
|
8564
9962
|
el.classList.add("animate-in");
|
|
@@ -8567,7 +9965,9 @@
|
|
|
8567
9965
|
}
|
|
8568
9966
|
}
|
|
8569
9967
|
chatMessages.insertBefore(fragment, chatMessages.firstChild);
|
|
9968
|
+
bindChatScrollListener();
|
|
8570
9969
|
attachAllCopyHandlers(chatMessages);
|
|
9970
|
+
applyPersistedExpandState(chatMessages);
|
|
8571
9971
|
// Collapse all existing cards; new cards (with animate-in) stay expanded
|
|
8572
9972
|
collapseOldToolCards(chatMessages, insertedEls);
|
|
8573
9973
|
// Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
|
|
@@ -8588,7 +9988,7 @@
|
|
|
8588
9988
|
var currentEl = existingEls[mi];
|
|
8589
9989
|
var tmpWrap = document.createElement("div");
|
|
8590
9990
|
var srOrigIdx = reversedMessages.length - 1 - mi;
|
|
8591
|
-
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null);
|
|
9991
|
+
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
|
|
8592
9992
|
var replacementEl = tmpWrap.firstElementChild;
|
|
8593
9993
|
if (!replacementEl) continue;
|
|
8594
9994
|
if (currentEl.innerHTML !== replacementEl.innerHTML || currentEl.className !== replacementEl.className) {
|
|
@@ -8606,6 +10006,8 @@
|
|
|
8606
10006
|
fullRenderChat();
|
|
8607
10007
|
}
|
|
8608
10008
|
if (replacedAny) {
|
|
10009
|
+
bindChatScrollListener();
|
|
10010
|
+
applyPersistedExpandState(chatMessages);
|
|
8609
10011
|
requestAnimationFrame(function() {
|
|
8610
10012
|
smartScrollToBottom(chatMessages);
|
|
8611
10013
|
});
|
|
@@ -8613,6 +10015,8 @@
|
|
|
8613
10015
|
var allCards = chatMessages.querySelectorAll(".tool-use-card");
|
|
8614
10016
|
var newestCard = null;
|
|
8615
10017
|
allCards.forEach(function(c) {
|
|
10018
|
+
var cardKey = getElementExpandKey(c);
|
|
10019
|
+
if (hasPersistedExpandState(cardKey) || getConfiguredPanelDefaults().structuredToolCardExpanded) return;
|
|
8616
10020
|
if (newestMsgEl && newestMsgEl.contains(c)) {
|
|
8617
10021
|
if (!newestCard) newestCard = c;
|
|
8618
10022
|
else c.classList.add("collapsed");
|
|
@@ -8635,14 +10039,15 @@
|
|
|
8635
10039
|
// Smart scroll: only auto-scroll if user is near bottom
|
|
8636
10040
|
// column-reverse: scrollTop near 0 = visual bottom (newest messages)
|
|
8637
10041
|
function smartScrollToBottom(container) {
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
// column-reverse: scrollTop=0 is the visual bottom; positive = scrolled up
|
|
8642
|
-
var isNearBottom = chatMsgs.scrollTop < threshold;
|
|
8643
|
-
if (isNearBottom) {
|
|
8644
|
-
chatMsgs.scrollTop = 0;
|
|
10042
|
+
if (!state.chatAutoFollow) {
|
|
10043
|
+
updateChatJumpToBottomButton();
|
|
10044
|
+
return;
|
|
8645
10045
|
}
|
|
10046
|
+
var chatMsgs = container && container.classList && container.classList.contains("chat-messages")
|
|
10047
|
+
? container
|
|
10048
|
+
: getChatScrollElement();
|
|
10049
|
+
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
10050
|
+
scrollChatToBottom(false);
|
|
8646
10051
|
}
|
|
8647
10052
|
|
|
8648
10053
|
// --- Todo progress bar ---
|
|
@@ -9026,6 +10431,74 @@
|
|
|
9026
10431
|
}, 150);
|
|
9027
10432
|
}
|
|
9028
10433
|
|
|
10434
|
+
function isNoiseLine(line) {
|
|
10435
|
+
if (!line) return false;
|
|
10436
|
+
var trimmed = String(line).trim();
|
|
10437
|
+
if (!trimmed) return false;
|
|
10438
|
+
if (trimmed.indexOf("────") === 0) return true;
|
|
10439
|
+
if (trimmed === "❯" || trimmed === "›") return true;
|
|
10440
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]{2,}$/.test(trimmed)) return true;
|
|
10441
|
+
if (/^[▁▂▃▄▅▆▇█▔▕▏▐]+$/.test(trimmed)) return true;
|
|
10442
|
+
if (trimmed.indexOf("esc to interrupt") !== -1) return true;
|
|
10443
|
+
if (trimmed.indexOf("Claude Code v") !== -1) return true;
|
|
10444
|
+
if (/^Sonnet\b/.test(trimmed)) return true;
|
|
10445
|
+
if (trimmed.indexOf("Failed to install Anthropic") !== -1) return true;
|
|
10446
|
+
if (trimmed.indexOf("Claude Code has switched") !== -1) return true;
|
|
10447
|
+
if (trimmed.indexOf("? for shortcuts") !== -1) return true;
|
|
10448
|
+
if (trimmed.indexOf("Claude is waiting") !== -1) return true;
|
|
10449
|
+
if (trimmed.indexOf("[wand]") !== -1) return true;
|
|
10450
|
+
if (trimmed.indexOf("0;") === 0 || trimmed.indexOf("9;") === 0) return true;
|
|
10451
|
+
if (trimmed.indexOf("ctrl+g") !== -1) return true;
|
|
10452
|
+
if (trimmed.indexOf("/effort") !== -1) return true;
|
|
10453
|
+
if (/^Using .* for .* session/.test(trimmed)) return true;
|
|
10454
|
+
if (trimmed.indexOf("Press ") === 0 && trimmed.indexOf(" for") !== -1) return true;
|
|
10455
|
+
if (trimmed.indexOf("type ") === 0 && trimmed.indexOf(" to ") !== -1) return true;
|
|
10456
|
+
if (trimmed.indexOf("auto mode is unavailable") !== -1) return true;
|
|
10457
|
+
if (/MCP server.*failed/i.test(trimmed)) return true;
|
|
10458
|
+
if (trimmed.indexOf("Germinating") !== -1 || trimmed.indexOf("Doodling") !== -1 || trimmed.indexOf("Brewing") !== -1) return true;
|
|
10459
|
+
if (trimmed.indexOf("Permissions") !== -1 && trimmed.indexOf("mode") !== -1) return true;
|
|
10460
|
+
if (trimmed.indexOf("●") === 0 && trimmed.indexOf("·") !== -1) return true;
|
|
10461
|
+
if (trimmed.indexOf("[>") === 0 || trimmed.indexOf("[<") === 0) return true;
|
|
10462
|
+
if (trimmed.indexOf("Captured Claude session ID") !== -1) return true;
|
|
10463
|
+
if (/^>_\s*OpenAI Codex\b/.test(trimmed)) return true;
|
|
10464
|
+
if (/^OpenAI Codex\b/i.test(trimmed)) return true;
|
|
10465
|
+
if (/^(model|directory):\s+/i.test(trimmed)) return true;
|
|
10466
|
+
if (/^(tip|context):\s+/i.test(trimmed)) return true;
|
|
10467
|
+
if (/^work(tree|space):\s+/i.test(trimmed)) return true;
|
|
10468
|
+
if (/^(approvals?|sandbox|provider|session id):\s+/i.test(trimmed)) return true;
|
|
10469
|
+
if (/^(thinking|working)(\.\.\.|…)?$/i.test(trimmed)) return true;
|
|
10470
|
+
if (/^[•◦·]\s+Working\b/i.test(trimmed)) return true;
|
|
10471
|
+
if (/^[•◦·]\s+(Running|Planning|Applying|Reading|Searching)\b/i.test(trimmed)) return true;
|
|
10472
|
+
if (/^[•◦·]\s+(Inspecting|Reviewing|Summarizing|Editing|Updating|Writing)\b/i.test(trimmed)) return true;
|
|
10473
|
+
if (/^[•◦·]\s+Completed\b/i.test(trimmed)) return true;
|
|
10474
|
+
if (/^(ctrl|enter|tab|shift|esc|alt)\+/i.test(trimmed)) return true;
|
|
10475
|
+
if (/\b(open|close|toggle) (chat|terminal)\b/i.test(trimmed)) return true;
|
|
10476
|
+
if (/\b(approve|deny)\b.*\b(permission|approval)\b/i.test(trimmed)) return true;
|
|
10477
|
+
if (/^(use|press) .* (to|for) .*/i.test(trimmed)) return true;
|
|
10478
|
+
if (/^(?:token|context window|remaining context|conversation):\s+/i.test(trimmed)) return true;
|
|
10479
|
+
if (/^(?:cwd|path):\s+\//i.test(trimmed)) return true;
|
|
10480
|
+
if (/^[<>│┆╎].*[<>│┆╎]$/.test(trimmed) && trimmed.length < 8) return true;
|
|
10481
|
+
return false;
|
|
10482
|
+
}
|
|
10483
|
+
|
|
10484
|
+
function stripAnsi(text) {
|
|
10485
|
+
return String(text || "")
|
|
10486
|
+
.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
|
|
10487
|
+
.replace(/\x1b\[(\d+)C/g, function(_match, count) { return " ".repeat(Number(count) || 1); })
|
|
10488
|
+
.replace(/\x1b\[[0-9;?]*[AB]/g, "\n")
|
|
10489
|
+
.replace(/\x1b\[[0-9;?]*[su]/g, "")
|
|
10490
|
+
.replace(/\x1b\[[0-9;?]*[HfJKr]/g, "\n")
|
|
10491
|
+
.replace(/\x1bM/g, "\n")
|
|
10492
|
+
.replace(/\x1b\[[0-9;?]*[ST]/g, "\n")
|
|
10493
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
10494
|
+
.replace(/\x1b[><=ePX^_]/g, "")
|
|
10495
|
+
.replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
|
|
10496
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
10497
|
+
.replace(/\r\n?/g, "\n")
|
|
10498
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
10499
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
10500
|
+
}
|
|
10501
|
+
|
|
9029
10502
|
function parseMessages(output, command) {
|
|
9030
10503
|
var messages = [];
|
|
9031
10504
|
if (!output) return messages;
|
|
@@ -9035,6 +10508,269 @@
|
|
|
9035
10508
|
var carriageReturn = String.fromCharCode(13);
|
|
9036
10509
|
var esc = String.fromCharCode(27);
|
|
9037
10510
|
|
|
10511
|
+
if (/^codex\b/.test(String(command || "").trim())) {
|
|
10512
|
+
var codexFooterRe = /\bgpt-\d+(?:\.\d+)?(?:\s+[a-z0-9.-]+)?\s+·\s+\d+%\s+left\s+·\s+(?:\/|~\/).+/i;
|
|
10513
|
+
var codexActivityRe = /^(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i;
|
|
10514
|
+
|
|
10515
|
+
function stripCodexSegment(raw) {
|
|
10516
|
+
return String(raw || "")
|
|
10517
|
+
.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
|
|
10518
|
+
.replace(/\x1b\[(\d+)C/g, function(_match, count) { return " ".repeat(Number(count) || 1); })
|
|
10519
|
+
.replace(/\x1b\[[0-9;?]*[AB]/g, newline)
|
|
10520
|
+
.replace(/\x1b\[[0-9;?]*[su]/g, "")
|
|
10521
|
+
.replace(/\x1b\[[0-9;?]*[HfJKr]/g, newline)
|
|
10522
|
+
.replace(/\x1bM/g, newline)
|
|
10523
|
+
.replace(/\x1b\[[0-9;?]*[ST]/g, newline)
|
|
10524
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
10525
|
+
.replace(/\x1b[><=ePX^_]/g, "")
|
|
10526
|
+
.replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
|
|
10527
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
10528
|
+
.replace(/[ \t]+\n/g, newline);
|
|
10529
|
+
}
|
|
10530
|
+
|
|
10531
|
+
function normalizeCodexText(value) {
|
|
10532
|
+
return String(value || "")
|
|
10533
|
+
.replace(/\s+/g, " ")
|
|
10534
|
+
.replace(/[M]+$/g, "")
|
|
10535
|
+
.trim();
|
|
10536
|
+
}
|
|
10537
|
+
|
|
10538
|
+
function normalizeCodexPromptLine(line) {
|
|
10539
|
+
return String(line || "")
|
|
10540
|
+
.replace(/^›\s*/, "")
|
|
10541
|
+
.replace(/^>\s*/, "")
|
|
10542
|
+
.trim();
|
|
10543
|
+
}
|
|
10544
|
+
|
|
10545
|
+
function shouldIgnoreCodexLine(line) {
|
|
10546
|
+
var trimmed = String(line || "").trim();
|
|
10547
|
+
if (!trimmed) return true;
|
|
10548
|
+
if (isNoiseLine(trimmed)) return true;
|
|
10549
|
+
if (codexFooterRe.test(trimmed)) return true;
|
|
10550
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed)) return true;
|
|
10551
|
+
if (/^\[>[0-9;?]*u$/i.test(trimmed)) return true;
|
|
10552
|
+
if (/^M+$/i.test(trimmed)) return true;
|
|
10553
|
+
if (/^(?:OpenAI Codex|Codex)\b/i.test(trimmed)) return true;
|
|
10554
|
+
if (/^(?:tokens?|context window|remaining context|approvals?|sandbox|provider|session id):\s*/i.test(trimmed)) return true;
|
|
10555
|
+
if (/^(?:thinking|working)\s*(?:\.\.\.|…)?$/i.test(trimmed)) return true;
|
|
10556
|
+
if (/^[•◦·]\s+(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i.test(trimmed)) return true;
|
|
10557
|
+
if (/^(?:model|directory|tip|context|cwd|path):\s+/i.test(trimmed)) return true;
|
|
10558
|
+
return false;
|
|
10559
|
+
}
|
|
10560
|
+
|
|
10561
|
+
function extractCodexPromptCandidate(line) {
|
|
10562
|
+
var trimmed = String(line || "").trim();
|
|
10563
|
+
if (!/^›(?:\s|$)/.test(trimmed)) return null;
|
|
10564
|
+
if (codexFooterRe.test(trimmed)) return null;
|
|
10565
|
+
var prompt = normalizeCodexText(normalizeCodexPromptLine(trimmed));
|
|
10566
|
+
if (!prompt || shouldIgnoreCodexLine(prompt)) return null;
|
|
10567
|
+
return prompt;
|
|
10568
|
+
}
|
|
10569
|
+
|
|
10570
|
+
function extractCodexAssistantCandidate(line) {
|
|
10571
|
+
var trimmed = String(line || "").trim();
|
|
10572
|
+
if (!/^[•◦·⏺]/.test(trimmed)) return null;
|
|
10573
|
+
|
|
10574
|
+
var assistant = trimmed
|
|
10575
|
+
.replace(/^[•◦·]\s*/, "")
|
|
10576
|
+
.replace(/^⏺\s+/, "")
|
|
10577
|
+
.replace(/^│\s*/, "")
|
|
10578
|
+
.trim();
|
|
10579
|
+
if (!assistant || /^[•◦·⏺]$/.test(assistant)) return null;
|
|
10580
|
+
|
|
10581
|
+
assistant = assistant
|
|
10582
|
+
.replace(/\s*\(\d+[smh]?\s*•\s*esc to interrupt\)[\s\S]*$/i, "")
|
|
10583
|
+
.replace(/(?:[a-z]{1,6})?›[\s\S]*$/, "")
|
|
10584
|
+
.replace(/\s{2,}gpt-\d[\s\S]*$/i, "")
|
|
10585
|
+
.replace(/\b(?:OpenAI Codex|model:|directory:|Tip:)\b[\s\S]*$/i, "");
|
|
10586
|
+
assistant = normalizeCodexText(assistant);
|
|
10587
|
+
|
|
10588
|
+
if (!assistant || assistant.length < 2 || codexActivityRe.test(assistant) || shouldIgnoreCodexLine(assistant)) {
|
|
10589
|
+
return null;
|
|
10590
|
+
}
|
|
10591
|
+
return assistant;
|
|
10592
|
+
}
|
|
10593
|
+
|
|
10594
|
+
function extractCodexEchoCandidate(line) {
|
|
10595
|
+
var trimmed = normalizeCodexText(line);
|
|
10596
|
+
if (!trimmed || shouldIgnoreCodexLine(trimmed)) return null;
|
|
10597
|
+
if (/^[•◦·⏺›]/.test(trimmed)) return null;
|
|
10598
|
+
if (/^[\[\]<>0-9;?]+u?$/i.test(trimmed)) return null;
|
|
10599
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed)) return null;
|
|
10600
|
+
if (trimmed.length > 500) return null;
|
|
10601
|
+
return trimmed;
|
|
10602
|
+
}
|
|
10603
|
+
|
|
10604
|
+
function isLikelyAssistantTailArtifact(longer, shorter) {
|
|
10605
|
+
if (longer.indexOf(shorter) !== 0) return false;
|
|
10606
|
+
var suffix = longer.slice(shorter.length);
|
|
10607
|
+
return /^[a-z]{1,4}$/i.test(suffix);
|
|
10608
|
+
}
|
|
10609
|
+
|
|
10610
|
+
function coalesceAssistantLines(lines) {
|
|
10611
|
+
var collected = [];
|
|
10612
|
+
for (var i = 0; i < lines.length; i++) {
|
|
10613
|
+
var normalized = normalizeCodexText(lines[i]);
|
|
10614
|
+
if (!normalized || normalized.length < 2 || shouldIgnoreCodexLine(normalized)) continue;
|
|
10615
|
+
|
|
10616
|
+
var previous = collected[collected.length - 1];
|
|
10617
|
+
if (!previous) {
|
|
10618
|
+
collected.push(normalized);
|
|
10619
|
+
continue;
|
|
10620
|
+
}
|
|
10621
|
+
if (normalized === previous) continue;
|
|
10622
|
+
if (normalized.indexOf(previous) === 0) {
|
|
10623
|
+
collected[collected.length - 1] = normalized;
|
|
10624
|
+
continue;
|
|
10625
|
+
}
|
|
10626
|
+
if (previous.indexOf(normalized) === 0) {
|
|
10627
|
+
if (isLikelyAssistantTailArtifact(previous, normalized)) {
|
|
10628
|
+
collected[collected.length - 1] = normalized;
|
|
10629
|
+
}
|
|
10630
|
+
continue;
|
|
10631
|
+
}
|
|
10632
|
+
collected.push(normalized);
|
|
10633
|
+
}
|
|
10634
|
+
return collected.join(newline).trim();
|
|
10635
|
+
}
|
|
10636
|
+
|
|
10637
|
+
function extractVisiblePrompt(lines) {
|
|
10638
|
+
for (var i = 0; i < lines.length; i++) {
|
|
10639
|
+
var line = String(lines[i] || "").trim();
|
|
10640
|
+
if (!line) continue;
|
|
10641
|
+
|
|
10642
|
+
var inlinePrompt = extractCodexPromptCandidate(line);
|
|
10643
|
+
if (inlinePrompt) return inlinePrompt;
|
|
10644
|
+
|
|
10645
|
+
if (line === "›") {
|
|
10646
|
+
for (var j = i + 1; j < lines.length; j++) {
|
|
10647
|
+
var nextLine = normalizeCodexText(lines[j]);
|
|
10648
|
+
if (!nextLine || codexFooterRe.test(nextLine) || shouldIgnoreCodexLine(nextLine)) continue;
|
|
10649
|
+
return nextLine;
|
|
10650
|
+
}
|
|
10651
|
+
}
|
|
10652
|
+
}
|
|
10653
|
+
return null;
|
|
10654
|
+
}
|
|
10655
|
+
|
|
10656
|
+
function extractVisibleAssistantLines(lines) {
|
|
10657
|
+
var assistantLines = [];
|
|
10658
|
+
var collecting = false;
|
|
10659
|
+
|
|
10660
|
+
for (var i = 0; i < lines.length; i++) {
|
|
10661
|
+
var line = String(lines[i] || "").trim();
|
|
10662
|
+
if (!line) {
|
|
10663
|
+
if (collecting) break;
|
|
10664
|
+
continue;
|
|
10665
|
+
}
|
|
10666
|
+
|
|
10667
|
+
var assistant = extractCodexAssistantCandidate(line);
|
|
10668
|
+
if (assistant) {
|
|
10669
|
+
assistantLines.push(assistant);
|
|
10670
|
+
collecting = true;
|
|
10671
|
+
continue;
|
|
10672
|
+
}
|
|
10673
|
+
|
|
10674
|
+
if (collecting) {
|
|
10675
|
+
if (line === "›" || /^›(?:\s|$)/.test(line) || codexFooterRe.test(line) || shouldIgnoreCodexLine(line)) {
|
|
10676
|
+
break;
|
|
10677
|
+
}
|
|
10678
|
+
assistantLines.push(normalizeCodexText(line));
|
|
10679
|
+
}
|
|
10680
|
+
}
|
|
10681
|
+
|
|
10682
|
+
return assistantLines;
|
|
10683
|
+
}
|
|
10684
|
+
|
|
10685
|
+
var rawCandidates = [];
|
|
10686
|
+
var candidateOrder = 0;
|
|
10687
|
+
var rawSegments = text.replace(/\r\n?/g, newline).split(newline);
|
|
10688
|
+
for (var rs = 0; rs < rawSegments.length; rs++) {
|
|
10689
|
+
var cleanedSegment = stripCodexSegment(rawSegments[rs]);
|
|
10690
|
+
var pieces = cleanedSegment.split(newline);
|
|
10691
|
+
for (var pi = 0; pi < pieces.length; pi++) {
|
|
10692
|
+
var piece = String(pieces[pi] || "").trim();
|
|
10693
|
+
if (!piece) continue;
|
|
10694
|
+
|
|
10695
|
+
var promptCandidate = extractCodexPromptCandidate(piece);
|
|
10696
|
+
if (promptCandidate) {
|
|
10697
|
+
rawCandidates.push({ kind: "user", order: candidateOrder++, text: promptCandidate });
|
|
10698
|
+
continue;
|
|
10699
|
+
}
|
|
10700
|
+
|
|
10701
|
+
var assistantCandidate = extractCodexAssistantCandidate(piece);
|
|
10702
|
+
if (assistantCandidate) {
|
|
10703
|
+
rawCandidates.push({ kind: "assistant", order: candidateOrder++, text: assistantCandidate });
|
|
10704
|
+
continue;
|
|
10705
|
+
}
|
|
10706
|
+
|
|
10707
|
+
var echoCandidate = extractCodexEchoCandidate(piece);
|
|
10708
|
+
if (echoCandidate) {
|
|
10709
|
+
rawCandidates.push({ kind: "echo", order: candidateOrder++, text: echoCandidate });
|
|
10710
|
+
}
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
10713
|
+
|
|
10714
|
+
var candidates = rawCandidates.filter(function(candidate, index, list) {
|
|
10715
|
+
var previous = list[index - 1];
|
|
10716
|
+
return !previous || previous.kind !== candidate.kind || previous.text !== candidate.text;
|
|
10717
|
+
});
|
|
10718
|
+
|
|
10719
|
+
var explicitUsers = candidates.filter(function(candidate) { return candidate.kind === "user"; });
|
|
10720
|
+
var assistantCandidates = candidates.filter(function(candidate) { return candidate.kind === "assistant"; });
|
|
10721
|
+
var echoCandidates = candidates.filter(function(candidate) { return candidate.kind === "echo"; });
|
|
10722
|
+
var strippedOutput = stripAnsi(text);
|
|
10723
|
+
var strippedLines = strippedOutput.split(newline).map(function(line) { return String(line || "").trimEnd(); });
|
|
10724
|
+
var visiblePrompt = extractVisiblePrompt(strippedLines);
|
|
10725
|
+
var latestExplicitUser = explicitUsers.length ? explicitUsers[explicitUsers.length - 1] : null;
|
|
10726
|
+
var echoedUserCandidates = echoCandidates
|
|
10727
|
+
.map(function(candidate) { return candidate.text; })
|
|
10728
|
+
.filter(function(value) { return value.length >= 3; });
|
|
10729
|
+
var latestEchoUser = null;
|
|
10730
|
+
for (var eu = echoedUserCandidates.length - 1; eu >= 0; eu--) {
|
|
10731
|
+
if (echoedUserCandidates[eu] !== visiblePrompt) {
|
|
10732
|
+
latestEchoUser = echoedUserCandidates[eu];
|
|
10733
|
+
break;
|
|
10734
|
+
}
|
|
10735
|
+
}
|
|
10736
|
+
if (!latestEchoUser && echoedUserCandidates.length) {
|
|
10737
|
+
latestEchoUser = echoedUserCandidates[echoedUserCandidates.length - 1];
|
|
10738
|
+
}
|
|
10739
|
+
|
|
10740
|
+
var currentUser = latestExplicitUser ? latestExplicitUser.text : latestEchoUser;
|
|
10741
|
+
var rawAssistantLines = assistantCandidates
|
|
10742
|
+
.filter(function(candidate) { return !latestExplicitUser || candidate.order > latestExplicitUser.order; })
|
|
10743
|
+
.map(function(candidate) { return candidate.text; });
|
|
10744
|
+
var visibleAssistantFallback = [];
|
|
10745
|
+
var bulletMatches = strippedOutput.match(/^[ \t]*[•◦·⏺][ \t]*(.+)$/gm) || [];
|
|
10746
|
+
for (var bm = 0; bm < bulletMatches.length; bm++) {
|
|
10747
|
+
var bulletContent = normalizeCodexText(bulletMatches[bm].replace(/^[ \t]*[•◦·⏺][ \t]*/, ""));
|
|
10748
|
+
if (!bulletContent) continue;
|
|
10749
|
+
if (codexActivityRe.test(bulletContent)) continue;
|
|
10750
|
+
if (codexFooterRe.test(bulletContent)) continue;
|
|
10751
|
+
if (/\b(?:OpenAI Codex|model:|directory:|Tip:|esc to interrupt)\b/i.test(bulletContent)) continue;
|
|
10752
|
+
visibleAssistantFallback.push(bulletContent);
|
|
10753
|
+
}
|
|
10754
|
+
|
|
10755
|
+
var assistantText = coalesceAssistantLines(rawAssistantLines)
|
|
10756
|
+
|| coalesceAssistantLines(extractVisibleAssistantLines(strippedLines))
|
|
10757
|
+
|| (visibleAssistantFallback.length ? visibleAssistantFallback[visibleAssistantFallback.length - 1] : null);
|
|
10758
|
+
|
|
10759
|
+
if (currentUser) {
|
|
10760
|
+
messages.push({ role: "user", content: currentUser });
|
|
10761
|
+
}
|
|
10762
|
+
if (assistantText) {
|
|
10763
|
+
messages.push({ role: "assistant", content: assistantText });
|
|
10764
|
+
}
|
|
10765
|
+
if (!messages.length && latestExplicitUser) {
|
|
10766
|
+
messages.push({ role: "user", content: latestExplicitUser.text });
|
|
10767
|
+
} else if (!messages.length && latestEchoUser) {
|
|
10768
|
+
messages.push({ role: "user", content: latestEchoUser });
|
|
10769
|
+
}
|
|
10770
|
+
|
|
10771
|
+
return messages;
|
|
10772
|
+
}
|
|
10773
|
+
|
|
9038
10774
|
// Optimized ANSI escape sequence stripping
|
|
9039
10775
|
// Handles: CSI sequences, OSC sequences, single-character escapes, control chars
|
|
9040
10776
|
var nul = String.fromCharCode(0);
|
|
@@ -9366,14 +11102,16 @@
|
|
|
9366
11102
|
'</div>';
|
|
9367
11103
|
}
|
|
9368
11104
|
|
|
9369
|
-
function renderChatMessage(msg, roundUsage) {
|
|
11105
|
+
function renderChatMessage(msg, roundUsage, messageIndex) {
|
|
9370
11106
|
// Thinking card (deep thought) — from PTY parsing
|
|
9371
11107
|
if (msg.role === "thinking") {
|
|
11108
|
+
var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
|
|
11109
|
+
var thinkingExpanded = getExpandState(thinkingKey, "thinking");
|
|
9372
11110
|
return '<div class="chat-message thinking">' +
|
|
9373
|
-
'<div class="thinking-inline thinking-pty collapsed" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
11111
|
+
'<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
9374
11112
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
9375
11113
|
'<span class="thinking-inline-preview">' + escapeHtml(msg.content) + '</span>' +
|
|
9376
|
-
'<span class="thinking-inline-action"
|
|
11114
|
+
'<span class="thinking-inline-action">' + (thinkingExpanded ? '收起' : '展开') + '</span>' +
|
|
9377
11115
|
'</div>' +
|
|
9378
11116
|
'</div>';
|
|
9379
11117
|
}
|
|
@@ -9390,7 +11128,7 @@
|
|
|
9390
11128
|
|
|
9391
11129
|
// Structured content blocks (from JSON chat mode)
|
|
9392
11130
|
if (Array.isArray(msg.content)) {
|
|
9393
|
-
return renderStructuredMessage(msg, roundUsage);
|
|
11131
|
+
return renderStructuredMessage(msg, roundUsage, messageIndex);
|
|
9394
11132
|
}
|
|
9395
11133
|
|
|
9396
11134
|
// Legacy string content (from PTY parsing)
|
|
@@ -9480,7 +11218,7 @@
|
|
|
9480
11218
|
|
|
9481
11219
|
var TOOL_GROUP_LABELS = { Read: "读取", Glob: "搜索", Grep: "搜索", WebFetch: "抓取", WebSearch: "搜索", TodoRead: "待办" };
|
|
9482
11220
|
|
|
9483
|
-
function renderToolGroup(items, role, toolResults) {
|
|
11221
|
+
function renderToolGroup(items, role, toolResults, messageKey) {
|
|
9484
11222
|
// Count by tool name
|
|
9485
11223
|
var counts = {};
|
|
9486
11224
|
for (var k = 0; k < items.length; k++) {
|
|
@@ -9504,28 +11242,31 @@
|
|
|
9504
11242
|
parts.push(counts[name] + " " + (TOOL_GROUP_LABELS[name] || name));
|
|
9505
11243
|
}
|
|
9506
11244
|
var summaryText = parts.join(" · ");
|
|
11245
|
+
var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
|
|
11246
|
+
var shouldExpand = getExpandState(groupKey, "tool-group");
|
|
9507
11247
|
|
|
9508
11248
|
// Render each item's inline-tool card
|
|
9509
11249
|
var innerHtml = "";
|
|
9510
11250
|
for (var k = 0; k < items.length; k++) {
|
|
9511
11251
|
try {
|
|
9512
|
-
innerHtml += renderContentBlock(items[k].block, role, toolResults, items[k].index);
|
|
11252
|
+
innerHtml += renderContentBlock(items[k].block, role, toolResults, items[k].index, messageKey);
|
|
9513
11253
|
} catch (e) {
|
|
9514
11254
|
innerHtml += '<div class="render-error">工具渲染失败</div>';
|
|
9515
11255
|
}
|
|
9516
11256
|
}
|
|
9517
11257
|
|
|
9518
|
-
return '<div class="tool-group" data-expanded="false" data-status="' + statusClass + '">' +
|
|
11258
|
+
return '<div class="tool-group" data-expand-kind="tool-group" data-expand-key="' + escapeHtml(groupKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '" data-status="' + statusClass + '">' +
|
|
9519
11259
|
'<div class="tool-group-summary" onclick="__toolGroupToggle(this.parentNode)">' +
|
|
9520
11260
|
'<span class="tool-group-status">' + statusIcon + '</span>' +
|
|
9521
11261
|
'<span class="tool-group-text">' + escapeHtml(summaryText) + '</span>' +
|
|
9522
11262
|
'<span class="tool-group-count">' + items.length + ' 个调用</span>' +
|
|
9523
|
-
'<svg class="tool-group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>' +
|
|
11263
|
+
'<svg class="tool-group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:' + (shouldExpand ? 'rotate(180deg)' : '') + '"><polyline points="6 9 12 15 18 9"/></svg>' +
|
|
9524
11264
|
'</div>' +
|
|
9525
|
-
'<div class="tool-group-body">' + innerHtml + '</div>' +
|
|
11265
|
+
'<div class="tool-group-body" style="display:' + (shouldExpand ? 'block' : 'none') + ';">' + innerHtml + '</div>' +
|
|
9526
11266
|
'</div>';
|
|
9527
11267
|
}
|
|
9528
11268
|
|
|
11269
|
+
|
|
9529
11270
|
// global toggle
|
|
9530
11271
|
window.__toolGroupToggle = function(el) {
|
|
9531
11272
|
if (!el) return;
|
|
@@ -9535,11 +11276,13 @@
|
|
|
9535
11276
|
if (body) body.style.display = expanded ? "none" : "block";
|
|
9536
11277
|
var chevron = el.querySelector(".tool-group-chevron");
|
|
9537
11278
|
if (chevron) chevron.style.transform = expanded ? "" : "rotate(180deg)";
|
|
11279
|
+
persistElementExpandState(el, "tool-group");
|
|
9538
11280
|
};
|
|
9539
11281
|
|
|
9540
|
-
function renderStructuredMessage(msg, roundUsage) {
|
|
11282
|
+
function renderStructuredMessage(msg, roundUsage, messageIndex) {
|
|
9541
11283
|
var role = msg.role;
|
|
9542
11284
|
var avatar = chatAvatar(role);
|
|
11285
|
+
var messageKey = getMessageKey(msg, messageIndex);
|
|
9543
11286
|
|
|
9544
11287
|
// Check if this is a queued user message
|
|
9545
11288
|
var isQueued = role === "user" && msg.content && msg.content.some(function(b) { return b.__queued; });
|
|
@@ -9563,9 +11306,9 @@
|
|
|
9563
11306
|
var grp = groups[g];
|
|
9564
11307
|
try {
|
|
9565
11308
|
if (grp.type === "group") {
|
|
9566
|
-
blocksHtml += renderToolGroup(grp.items, role, toolResults);
|
|
11309
|
+
blocksHtml += renderToolGroup(grp.items, role, toolResults, messageKey);
|
|
9567
11310
|
} else {
|
|
9568
|
-
blocksHtml += renderContentBlock(grp.block, role, toolResults, grp.index);
|
|
11311
|
+
blocksHtml += renderContentBlock(grp.block, role, toolResults, grp.index, messageKey);
|
|
9569
11312
|
}
|
|
9570
11313
|
} catch (e) {
|
|
9571
11314
|
blocksHtml += '<div class="render-error">消息块渲染失败</div>';
|
|
@@ -9582,13 +11325,13 @@
|
|
|
9582
11325
|
var queuedClass = isQueued ? " queued" : "";
|
|
9583
11326
|
var queuedBadge = isQueued ? '<span class="queued-badge">排队中</span>' : "";
|
|
9584
11327
|
|
|
9585
|
-
return '<div class="chat-message ' + role + queuedClass + '">' +
|
|
11328
|
+
return '<div class="chat-message ' + role + queuedClass + '" data-message-key="' + escapeHtml(messageKey) + '">' +
|
|
9586
11329
|
avatar +
|
|
9587
11330
|
'<div class="chat-message-content">' + blocksHtml + queuedBadge + '</div>' +
|
|
9588
11331
|
usageHtml +
|
|
9589
11332
|
'</div>';
|
|
9590
11333
|
}
|
|
9591
|
-
function renderContentBlock(block, role, toolResults, index) {
|
|
11334
|
+
function renderContentBlock(block, role, toolResults, index, messageKey) {
|
|
9592
11335
|
if (!block || !block.type) return "";
|
|
9593
11336
|
|
|
9594
11337
|
switch (block.type) {
|
|
@@ -9610,15 +11353,17 @@
|
|
|
9610
11353
|
'</div>' +
|
|
9611
11354
|
'</div>';
|
|
9612
11355
|
}
|
|
9613
|
-
|
|
11356
|
+
var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
|
|
11357
|
+
var thinkingExpanded = getExpandState(thinkingKey, "thinking");
|
|
11358
|
+
return '<div class="thinking-inline ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="' + escapeHtml(thinkingText) + '" onclick="__thinkingToggle(this)">' +
|
|
9614
11359
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
9615
|
-
'<span class="thinking-inline-preview">' + escapeHtml(preview) + '</span>' +
|
|
9616
|
-
'<span class="thinking-inline-action"
|
|
11360
|
+
'<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
|
|
11361
|
+
'<span class="thinking-inline-action">' + (thinkingExpanded ? '收起' : '展开') + '</span>' +
|
|
9617
11362
|
'</div>';
|
|
9618
11363
|
|
|
9619
11364
|
case "tool_use":
|
|
9620
11365
|
var toolResult = pickToolResultForDisplay(toolResults, block.id);
|
|
9621
|
-
var rendered = renderToolUseCard(block, toolResult, index);
|
|
11366
|
+
var rendered = renderToolUseCard(block, toolResult, index, messageKey);
|
|
9622
11367
|
if (hasRecoveredToolNoise(toolResults, block.id)) {
|
|
9623
11368
|
rendered = renderRecoveredToolHint(block.name || "工具") + rendered;
|
|
9624
11369
|
}
|
|
@@ -9632,8 +11377,9 @@
|
|
|
9632
11377
|
}
|
|
9633
11378
|
}
|
|
9634
11379
|
|
|
9635
|
-
function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo) {
|
|
11380
|
+
function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo, messageKey, index) {
|
|
9636
11381
|
var toolId = block.id || "tool-" + toolName;
|
|
11382
|
+
var expandKey = buildExpandKey("inline-tool", [messageKey, toolId || index, index]);
|
|
9637
11383
|
var inputData = block.input || {};
|
|
9638
11384
|
var resultContent = extractToolResultText(toolResult && toolResult.content);
|
|
9639
11385
|
|
|
@@ -9704,16 +11450,16 @@
|
|
|
9704
11450
|
var fullResult = resultContent;
|
|
9705
11451
|
|
|
9706
11452
|
var expandedHtml = "";
|
|
9707
|
-
var shouldExpand =
|
|
11453
|
+
var shouldExpand = getExpandState(expandKey, "inline-tool");
|
|
9708
11454
|
if (hasResult) {
|
|
9709
11455
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
9710
11456
|
'<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
|
|
9711
11457
|
'</div>';
|
|
9712
11458
|
} else if (isError) {
|
|
9713
|
-
expandedHtml = '<div class="inline-tool-expanded" style="display: none;"><div class="inline-tool-result inline-tool-error">' +
|
|
11459
|
+
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-result inline-tool-error">' +
|
|
9714
11460
|
escapeHtml(resultContent || "操作失败") + '</div></div>';
|
|
9715
11461
|
} else if (!toolResult) {
|
|
9716
|
-
expandedHtml = '<div class="inline-tool-expanded" style="display: none;"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
11462
|
+
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
9717
11463
|
}
|
|
9718
11464
|
|
|
9719
11465
|
var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
|
|
@@ -9721,6 +11467,8 @@
|
|
|
9721
11467
|
if (shouldExpand) extraClass += ' inline-tool-open';
|
|
9722
11468
|
|
|
9723
11469
|
return '<div class="inline-tool ' + extraClass + '" ' +
|
|
11470
|
+
'data-expand-kind="inline-tool" ' +
|
|
11471
|
+
'data-expand-key="' + escapeHtml(expandKey) + '" ' +
|
|
9724
11472
|
'data-result="' + escapeHtml(fullResult) + '" ' +
|
|
9725
11473
|
'data-preview="' + previewDataAttr + '" ' +
|
|
9726
11474
|
'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
|
|
@@ -9736,10 +11484,12 @@
|
|
|
9736
11484
|
}
|
|
9737
11485
|
|
|
9738
11486
|
// Terminal-style display for Bash commands
|
|
9739
|
-
function renderTerminalTool(block, toolResult, toolName) {
|
|
11487
|
+
function renderTerminalTool(block, toolResult, toolName, messageKey, index) {
|
|
9740
11488
|
var inputData = block.input || {};
|
|
9741
11489
|
var command = inputData.command || inputData.cmd || "";
|
|
9742
11490
|
var resultContent = extractToolResultText(toolResult && toolResult.content);
|
|
11491
|
+
var toolId = block.id || "tool-" + toolName;
|
|
11492
|
+
var expandKey = buildExpandKey("terminal", [messageKey, toolId || index, index]);
|
|
9743
11493
|
|
|
9744
11494
|
var isError = toolResult && toolResult.is_error;
|
|
9745
11495
|
var exitCode = inputData.exitCode;
|
|
@@ -9777,22 +11527,21 @@
|
|
|
9777
11527
|
|
|
9778
11528
|
// Show command preview in header (truncate long commands)
|
|
9779
11529
|
var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
|
|
11530
|
+
var shouldExpand = getExpandState(expandKey, "terminal");
|
|
9780
11531
|
|
|
9781
|
-
return '<div class="inline-terminal" data-expanded="false">' +
|
|
11532
|
+
return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '">' +
|
|
9782
11533
|
'<div class="term-header" onclick="__terminalExpand(this)">' +
|
|
9783
11534
|
statusDot +
|
|
9784
11535
|
'<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
|
|
9785
|
-
'<span class="term-toggle-icon"
|
|
11536
|
+
'<span class="term-toggle-icon">' + (shouldExpand ? '▼' : '▶') + '</span>' +
|
|
9786
11537
|
'</div>' +
|
|
9787
|
-
'<div class="term-body" style="display:none;">' +
|
|
11538
|
+
'<div class="term-body" style="display:' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
9788
11539
|
'<div class="term-command"><span class="term-prompt">$</span> ' + cmdDisplay + '</div>' +
|
|
9789
11540
|
(outputHtml ? '<div class="term-output">' + outputHtml + '</div>' : '') +
|
|
9790
11541
|
exitCodeHtml +
|
|
9791
11542
|
'</div>' +
|
|
9792
11543
|
'</div>';
|
|
9793
11544
|
}
|
|
9794
|
-
|
|
9795
|
-
// GitHub-style diff display for Edit/Write/MultiEdit
|
|
9796
11545
|
function extractToolResultText(content) {
|
|
9797
11546
|
if (!content) return "";
|
|
9798
11547
|
if (typeof content === "string") return content;
|
|
@@ -9883,7 +11632,7 @@
|
|
|
9883
11632
|
return '<pre class="inline-tool-result-text" style="max-height: 300px; overflow-y: auto;">' + escapeHtml(content) + '</pre>';
|
|
9884
11633
|
}
|
|
9885
11634
|
|
|
9886
|
-
function renderToolUseCard(block, toolResult, index) {
|
|
11635
|
+
function renderToolUseCard(block, toolResult, index, messageKey) {
|
|
9887
11636
|
var toolName = block.name || "unknown";
|
|
9888
11637
|
var toolId = block.id || "tool-" + toolName + "-" + (typeof index === "number" ? index : 0);
|
|
9889
11638
|
var fileInfo = extractFileInfo(toolName, block.input);
|
|
@@ -9891,12 +11640,12 @@
|
|
|
9891
11640
|
// ── Lightweight inline tools: Read, Glob, Grep, WebFetch, WebSearch, TodoRead
|
|
9892
11641
|
if (toolName === "Read" || toolName === "Glob" || toolName === "Grep" ||
|
|
9893
11642
|
toolName === "WebFetch" || toolName === "WebSearch" || toolName === "TodoRead") {
|
|
9894
|
-
return renderInlineTool(block, toolResult, toolName, fileInfo, "");
|
|
11643
|
+
return renderInlineTool(block, toolResult, toolName, fileInfo, "", messageKey, index);
|
|
9895
11644
|
}
|
|
9896
11645
|
|
|
9897
11646
|
// ── Terminal-style: Bash
|
|
9898
11647
|
if (toolName === "Bash") {
|
|
9899
|
-
return renderTerminalTool(block, toolResult, toolName);
|
|
11648
|
+
return renderTerminalTool(block, toolResult, toolName, messageKey, index);
|
|
9900
11649
|
}
|
|
9901
11650
|
|
|
9902
11651
|
// ── Diff-style: Edit, Write, MultiEdit
|
|
@@ -9975,9 +11724,11 @@
|
|
|
9975
11724
|
headerIcon = getToolIcon(toolName);
|
|
9976
11725
|
}
|
|
9977
11726
|
|
|
9978
|
-
var
|
|
11727
|
+
var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
|
|
11728
|
+
var shouldExpand = getExpandState(expandKey, "tool-card", statusClass === "loading");
|
|
11729
|
+
var collapsedClass = shouldExpand ? "" : " collapsed";
|
|
9979
11730
|
var toggleHtml = '<span class="tool-use-toggle">▼</span>';
|
|
9980
|
-
return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
11731
|
+
return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
9981
11732
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
9982
11733
|
'<span class="tool-use-icon">' + headerIcon + '</span>' +
|
|
9983
11734
|
'<span class="tool-use-name">' + escapeHtml(titleText) + '</span>' +
|