@co0ontty/wand 1.6.1 → 1.7.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 +23 -0
- package/dist/git-worktree.d.ts +12 -0
- package/dist/git-worktree.js +43 -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 +39 -7
- package/dist/server.js +141 -15
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +23 -0
- package/dist/storage.js +80 -18
- package/dist/structured-session-manager.d.ts +5 -0
- package/dist/structured-session-manager.js +115 -49
- package/dist/types.d.ts +23 -0
- package/dist/web-ui/content/scripts.js +1511 -306
- package/dist/web-ui/content/styles.css +142 -0
- package/package.json +1 -1
|
@@ -58,6 +58,8 @@
|
|
|
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";
|
|
61
63
|
|
|
62
64
|
var state = {
|
|
63
65
|
selectedId: (function() {
|
|
@@ -75,6 +77,7 @@
|
|
|
75
77
|
_lastDomHtml: "",
|
|
76
78
|
terminalSessionId: null,
|
|
77
79
|
terminalOutput: "",
|
|
80
|
+
terminalLiveStreamSessions: {},
|
|
78
81
|
terminalViewportSize: { width: 0, height: 0 },
|
|
79
82
|
terminalAutoFollow: true,
|
|
80
83
|
terminalScrollIdleTimer: null,
|
|
@@ -90,12 +93,21 @@
|
|
|
90
93
|
inputQueue: Promise.resolve(),
|
|
91
94
|
pendingMessages: [], // WebSocket 断线期间的消息队列
|
|
92
95
|
messageQueue: [], // 用户消息排队等待发送
|
|
93
|
-
crossSessionQueue:
|
|
96
|
+
crossSessionQueue: (function() {
|
|
97
|
+
try {
|
|
98
|
+
var saved = localStorage.getItem("wand-cross-session-queue");
|
|
99
|
+
var parsed = saved ? JSON.parse(saved) : [];
|
|
100
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
})(), // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
|
|
94
105
|
structuredInputQueue: [], // 结构化会话同会话排队消息
|
|
95
106
|
drafts: {},
|
|
96
107
|
isSyncingInputBox: false,
|
|
97
108
|
loginPending: false,
|
|
98
109
|
loginChecked: false,
|
|
110
|
+
bootstrapping: true,
|
|
99
111
|
sessionsDrawerOpen: false,
|
|
100
112
|
modalOpen: false,
|
|
101
113
|
presetValue: "",
|
|
@@ -103,6 +115,7 @@
|
|
|
103
115
|
modeValue: "managed",
|
|
104
116
|
chatMode: "managed",
|
|
105
117
|
sessionCreateKind: "structured",
|
|
118
|
+
sessionCreateWorktree: false,
|
|
106
119
|
sessionTool: "claude",
|
|
107
120
|
preferredCommand: "claude",
|
|
108
121
|
structuredRunner: "claude-cli-print",
|
|
@@ -130,6 +143,21 @@
|
|
|
130
143
|
return false;
|
|
131
144
|
}
|
|
132
145
|
})(),
|
|
146
|
+
chatAutoFollow: (function() {
|
|
147
|
+
try {
|
|
148
|
+
var saved = localStorage.getItem(CHAT_AUTO_FOLLOW_STORAGE_KEY);
|
|
149
|
+
return saved === null ? true : saved === "true";
|
|
150
|
+
} catch (e) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
})(),
|
|
154
|
+
showChatJumpToBottom: false,
|
|
155
|
+
chatScrollThreshold: 200,
|
|
156
|
+
chatIsProgrammaticScroll: false,
|
|
157
|
+
chatScrollElement: null,
|
|
158
|
+
chatScrollHandler: null,
|
|
159
|
+
lastForegroundSyncAt: 0,
|
|
160
|
+
foregroundSyncTimer: null,
|
|
133
161
|
currentMessages: [],
|
|
134
162
|
lastRenderedHash: 0,
|
|
135
163
|
lastRenderedMsgCount: 0,
|
|
@@ -244,6 +272,120 @@
|
|
|
244
272
|
}
|
|
245
273
|
}
|
|
246
274
|
|
|
275
|
+
function persistChatAutoFollow() {
|
|
276
|
+
try {
|
|
277
|
+
localStorage.setItem(CHAT_AUTO_FOLLOW_STORAGE_KEY, state.chatAutoFollow ? "true" : "false");
|
|
278
|
+
} catch (e) {
|
|
279
|
+
// Ignore localStorage errors
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function getChatScrollElement() {
|
|
284
|
+
var chatOutput = document.getElementById("chat-output");
|
|
285
|
+
if (!chatOutput) {
|
|
286
|
+
state.chatScrollElement = null;
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
var chatMessages = chatOutput.querySelector(".chat-messages");
|
|
290
|
+
if (chatMessages) {
|
|
291
|
+
state.chatScrollElement = chatMessages;
|
|
292
|
+
return chatMessages;
|
|
293
|
+
}
|
|
294
|
+
state.chatScrollElement = null;
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isChatNearBottom(chatMsgs) {
|
|
299
|
+
var el = chatMsgs || getChatScrollElement();
|
|
300
|
+
if (!el) return true;
|
|
301
|
+
return el.scrollTop < state.chatScrollThreshold;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function updateChatFollowToggleButton() {
|
|
305
|
+
var button = document.getElementById("chat-follow-toggle");
|
|
306
|
+
if (!button) return;
|
|
307
|
+
var enabled = !!state.chatAutoFollow;
|
|
308
|
+
button.classList.toggle("active", enabled);
|
|
309
|
+
button.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
310
|
+
button.setAttribute("title", enabled ? "追踪底部:开启" : "追踪底部:已暂停");
|
|
311
|
+
button.textContent = enabled ? "追底" : "暂停";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function updateChatJumpToBottomButton() {
|
|
315
|
+
var button = document.getElementById("chat-jump-bottom");
|
|
316
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
317
|
+
var shouldShow = !!selectedSession
|
|
318
|
+
&& state.currentView === "chat"
|
|
319
|
+
&& !state.chatAutoFollow
|
|
320
|
+
&& !isChatNearBottom();
|
|
321
|
+
state.showChatJumpToBottom = shouldShow;
|
|
322
|
+
if (button) {
|
|
323
|
+
button.classList.toggle("visible", shouldShow);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function scrollChatToBottom(smooth) {
|
|
328
|
+
var chatMsgs = getChatScrollElement();
|
|
329
|
+
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
330
|
+
state.chatIsProgrammaticScroll = true;
|
|
331
|
+
if (smooth && typeof chatMsgs.scrollTo === "function") {
|
|
332
|
+
chatMsgs.scrollTo({ top: 0, behavior: "smooth" });
|
|
333
|
+
setTimeout(function() {
|
|
334
|
+
state.chatIsProgrammaticScroll = false;
|
|
335
|
+
updateChatJumpToBottomButton();
|
|
336
|
+
}, 220);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
chatMsgs.scrollTop = 0;
|
|
340
|
+
requestAnimationFrame(function() {
|
|
341
|
+
state.chatIsProgrammaticScroll = false;
|
|
342
|
+
updateChatJumpToBottomButton();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function setChatAutoFollow(enabled, options) {
|
|
347
|
+
options = options || {};
|
|
348
|
+
state.chatAutoFollow = !!enabled;
|
|
349
|
+
persistChatAutoFollow();
|
|
350
|
+
updateChatFollowToggleButton();
|
|
351
|
+
if (state.chatAutoFollow && options.scrollNow !== false) {
|
|
352
|
+
scrollChatToBottom(!!options.smooth);
|
|
353
|
+
} else {
|
|
354
|
+
updateChatJumpToBottomButton();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function bindChatScrollListener() {
|
|
359
|
+
var chatMsgs = getChatScrollElement();
|
|
360
|
+
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
361
|
+
if (state.chatScrollElement === chatMsgs && state.chatScrollHandler) {
|
|
362
|
+
updateChatJumpToBottomButton();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
366
|
+
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
367
|
+
}
|
|
368
|
+
state.chatScrollElement = chatMsgs;
|
|
369
|
+
state.chatScrollHandler = function() {
|
|
370
|
+
if (!chatMsgs.isConnected) return;
|
|
371
|
+
if (state.chatIsProgrammaticScroll) {
|
|
372
|
+
updateChatJumpToBottomButton();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!isChatNearBottom(chatMsgs)) {
|
|
376
|
+
if (state.chatAutoFollow) {
|
|
377
|
+
setChatAutoFollow(false, { scrollNow: false });
|
|
378
|
+
} else {
|
|
379
|
+
updateChatJumpToBottomButton();
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
updateChatJumpToBottomButton();
|
|
384
|
+
};
|
|
385
|
+
chatMsgs.addEventListener("scroll", state.chatScrollHandler, { passive: true });
|
|
386
|
+
updateChatJumpToBottomButton();
|
|
387
|
+
}
|
|
388
|
+
|
|
247
389
|
// Helper function to persist selected session ID to localStorage
|
|
248
390
|
function persistSelectedId() {
|
|
249
391
|
try {
|
|
@@ -257,14 +399,302 @@
|
|
|
257
399
|
}
|
|
258
400
|
}
|
|
259
401
|
|
|
402
|
+
function getStructuredQueuedInputs(session) {
|
|
403
|
+
if (session && Array.isArray(session.queuedMessages)) {
|
|
404
|
+
return session.queuedMessages;
|
|
405
|
+
}
|
|
406
|
+
return state.structuredInputQueue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function getSelectedStructuredQueuedInputs() {
|
|
410
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
411
|
+
return getStructuredQueuedInputs(session);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function syncStructuredQueueFromSession(session) {
|
|
415
|
+
var queued = getStructuredQueuedInputs(session);
|
|
416
|
+
state.structuredInputQueue = Array.isArray(queued) ? queued.slice() : [];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function hasRenderOnlyStructuredBlock(message, marker) {
|
|
420
|
+
return !!(message && Array.isArray(message.content) && message.content.some(function(block) {
|
|
421
|
+
return block && typeof block === "object" && block[marker];
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isQueuedStructuredMessage(message) {
|
|
426
|
+
return !!(message && message.role === "user" && hasRenderOnlyStructuredBlock(message, "__queued"));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function isProcessingStructuredMessage(message) {
|
|
430
|
+
return !!(message && message.role === "assistant" && hasRenderOnlyStructuredBlock(message, "__processing"));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function stripRenderOnlyStructuredMessages(messages) {
|
|
434
|
+
if (!Array.isArray(messages)) return [];
|
|
435
|
+
var removed = false;
|
|
436
|
+
var filtered = [];
|
|
437
|
+
for (var i = 0; i < messages.length; i++) {
|
|
438
|
+
var message = messages[i];
|
|
439
|
+
if (isQueuedStructuredMessage(message) || isProcessingStructuredMessage(message)) {
|
|
440
|
+
removed = true;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
filtered.push(message);
|
|
444
|
+
}
|
|
445
|
+
return removed ? filtered : messages;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function normalizeStructuredSnapshot(snapshot, existingSession) {
|
|
449
|
+
if (!snapshot || !Array.isArray(snapshot.messages)) {
|
|
450
|
+
return snapshot;
|
|
451
|
+
}
|
|
452
|
+
var sessionKind = snapshot.sessionKind || (existingSession && existingSession.sessionKind);
|
|
453
|
+
if (sessionKind !== "structured") {
|
|
454
|
+
return snapshot;
|
|
455
|
+
}
|
|
456
|
+
var sanitizedMessages = stripRenderOnlyStructuredMessages(snapshot.messages);
|
|
457
|
+
if (sanitizedMessages === snapshot.messages) {
|
|
458
|
+
return snapshot;
|
|
459
|
+
}
|
|
460
|
+
return Object.assign({}, snapshot, { messages: sanitizedMessages });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function saveStructuredQueue() {
|
|
464
|
+
try {
|
|
465
|
+
var queued = getSelectedStructuredQueuedInputs();
|
|
466
|
+
if (!state.selectedId || queued.length === 0) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
localStorage.setItem("wand-structured-queue", JSON.stringify({
|
|
470
|
+
sessionId: state.selectedId,
|
|
471
|
+
items: queued
|
|
472
|
+
}));
|
|
473
|
+
} catch (e) {
|
|
474
|
+
// Ignore localStorage errors
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function clearStructuredQueuePersistence(sessionId) {
|
|
479
|
+
try {
|
|
480
|
+
var saved = localStorage.getItem("wand-structured-queue");
|
|
481
|
+
if (!saved) return;
|
|
482
|
+
var parsed = JSON.parse(saved);
|
|
483
|
+
if (!sessionId || !parsed || parsed.sessionId === sessionId) {
|
|
484
|
+
localStorage.removeItem("wand-structured-queue");
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
localStorage.removeItem("wand-structured-queue");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function restoreStructuredQueue() {
|
|
492
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
493
|
+
if (selectedSession && Array.isArray(selectedSession.queuedMessages)) {
|
|
494
|
+
syncStructuredQueueFromSession(selectedSession);
|
|
495
|
+
saveStructuredQueue();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
var saved = localStorage.getItem("wand-structured-queue");
|
|
500
|
+
if (!saved) return;
|
|
501
|
+
var parsed = JSON.parse(saved);
|
|
502
|
+
if (!parsed || parsed.sessionId !== state.selectedId || !Array.isArray(parsed.items)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
state.structuredInputQueue = parsed.items.slice(0, 10);
|
|
506
|
+
} catch (e) {
|
|
507
|
+
state.structuredInputQueue = [];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function persistCrossSessionQueue() {
|
|
512
|
+
try {
|
|
513
|
+
if (state.crossSessionQueue.length === 0) {
|
|
514
|
+
localStorage.removeItem("wand-cross-session-queue");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
localStorage.setItem("wand-cross-session-queue", JSON.stringify(state.crossSessionQueue));
|
|
518
|
+
} catch (e) {
|
|
519
|
+
// Ignore localStorage errors
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
260
523
|
function getConfigCwd() {
|
|
261
524
|
return (state.config && state.config.defaultCwd) || "/tmp";
|
|
262
525
|
}
|
|
263
526
|
|
|
527
|
+
function loadChatExpandStateMap() {
|
|
528
|
+
try {
|
|
529
|
+
var saved = localStorage.getItem(CHAT_EXPAND_STATE_STORAGE_KEY);
|
|
530
|
+
if (!saved) return {};
|
|
531
|
+
var parsed = JSON.parse(saved);
|
|
532
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
533
|
+
} catch (e) {
|
|
534
|
+
return {};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function saveChatExpandStateMap(map) {
|
|
539
|
+
try {
|
|
540
|
+
if (!map || Object.keys(map).length === 0) {
|
|
541
|
+
localStorage.removeItem(CHAT_EXPAND_STATE_STORAGE_KEY);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
localStorage.setItem(CHAT_EXPAND_STATE_STORAGE_KEY, JSON.stringify(map));
|
|
545
|
+
} catch (e) {
|
|
546
|
+
// Ignore localStorage errors
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function getCurrentChatExpandState() {
|
|
551
|
+
var sessionId = state.selectedId;
|
|
552
|
+
if (!sessionId) return {};
|
|
553
|
+
var map = loadChatExpandStateMap();
|
|
554
|
+
var sessionState = map[sessionId];
|
|
555
|
+
return sessionState && typeof sessionState === "object" ? sessionState : {};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getPersistedExpandState(itemKey) {
|
|
559
|
+
if (!itemKey || !state.selectedId) return null;
|
|
560
|
+
var sessionState = getCurrentChatExpandState();
|
|
561
|
+
return typeof sessionState[itemKey] === "boolean" ? sessionState[itemKey] : null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function setPersistedExpandState(itemKey, expanded) {
|
|
565
|
+
if (!itemKey || !state.selectedId) return;
|
|
566
|
+
var map = loadChatExpandStateMap();
|
|
567
|
+
var sessionId = state.selectedId;
|
|
568
|
+
var sessionState = map[sessionId];
|
|
569
|
+
if (!sessionState || typeof sessionState !== "object") {
|
|
570
|
+
sessionState = {};
|
|
571
|
+
}
|
|
572
|
+
sessionState[itemKey] = !!expanded;
|
|
573
|
+
map[sessionId] = sessionState;
|
|
574
|
+
saveChatExpandStateMap(map);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function getMessageKey(msg, fallbackIndex) {
|
|
578
|
+
if (!msg) {
|
|
579
|
+
return "msg:unknown-" + (typeof fallbackIndex === "number" ? fallbackIndex : 0);
|
|
580
|
+
}
|
|
581
|
+
if (msg.uuid) return "msg:" + msg.uuid;
|
|
582
|
+
if (msg.id) return "msg:" + msg.id;
|
|
583
|
+
if (msg.messageId) return "msg:" + msg.messageId;
|
|
584
|
+
if (msg.turnId) return "msg:" + msg.turnId;
|
|
585
|
+
return "msg:" + (typeof fallbackIndex === "number" ? fallbackIndex : 0);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function buildExpandKey(kind, parts) {
|
|
589
|
+
var filtered = [];
|
|
590
|
+
for (var i = 0; i < parts.length; i++) {
|
|
591
|
+
var part = parts[i];
|
|
592
|
+
if (part === undefined || part === null || part === "") continue;
|
|
593
|
+
filtered.push(String(part));
|
|
594
|
+
}
|
|
595
|
+
return kind + ":" + filtered.join(":");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getElementExpandKey(el) {
|
|
599
|
+
if (!el || !el.dataset) return "";
|
|
600
|
+
return el.dataset.expandKey || "";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function isElementExpanded(el, kind) {
|
|
604
|
+
if (!el) return false;
|
|
605
|
+
switch (kind) {
|
|
606
|
+
case "tool-card":
|
|
607
|
+
return !el.classList.contains("collapsed");
|
|
608
|
+
case "thinking":
|
|
609
|
+
return el.classList.contains("expanded") && !el.classList.contains("collapsed");
|
|
610
|
+
case "inline-tool":
|
|
611
|
+
return el.classList.contains("inline-tool-open");
|
|
612
|
+
case "terminal": {
|
|
613
|
+
var body = el.querySelector(".term-body");
|
|
614
|
+
if (body) return body.style.display !== "none";
|
|
615
|
+
return el.dataset.expanded === "true";
|
|
616
|
+
}
|
|
617
|
+
case "tool-group":
|
|
618
|
+
return el.getAttribute("data-expanded") === "true";
|
|
619
|
+
default:
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function applyExpandedState(el, kind, expanded) {
|
|
625
|
+
if (!el) return;
|
|
626
|
+
switch (kind) {
|
|
627
|
+
case "tool-card": {
|
|
628
|
+
el.classList.toggle("collapsed", !expanded);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case "thinking": {
|
|
632
|
+
el.classList.toggle("collapsed", !expanded);
|
|
633
|
+
el.classList.toggle("expanded", !!expanded);
|
|
634
|
+
var previewEl = el.querySelector(".thinking-inline-preview");
|
|
635
|
+
if (previewEl) {
|
|
636
|
+
var fullText = el.dataset.thinking || "";
|
|
637
|
+
var preview = fullText.slice(0, 57) + (fullText.length > 60 ? "…" : "");
|
|
638
|
+
previewEl.textContent = expanded ? fullText : preview;
|
|
639
|
+
}
|
|
640
|
+
var actionEl = el.querySelector(".thinking-inline-action");
|
|
641
|
+
if (actionEl) actionEl.textContent = expanded ? "收起" : "展开";
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
case "inline-tool": {
|
|
645
|
+
el.classList.toggle("inline-tool-open", !!expanded);
|
|
646
|
+
var inlineBody = el.querySelector(".inline-tool-expanded");
|
|
647
|
+
if (inlineBody) inlineBody.style.display = expanded ? "block" : "none";
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case "terminal": {
|
|
651
|
+
var body = el.querySelector(".term-body");
|
|
652
|
+
if (body) body.style.display = expanded ? "block" : "none";
|
|
653
|
+
el.dataset.expanded = expanded ? "true" : "false";
|
|
654
|
+
var toggleIcon = el.querySelector(".term-toggle-icon");
|
|
655
|
+
if (toggleIcon) toggleIcon.textContent = expanded ? "▼" : "▶";
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case "tool-group": {
|
|
659
|
+
el.setAttribute("data-expanded", expanded ? "true" : "false");
|
|
660
|
+
var groupBody = el.querySelector(".tool-group-body");
|
|
661
|
+
if (groupBody) groupBody.style.display = expanded ? "block" : "none";
|
|
662
|
+
var chevron = el.querySelector(".tool-group-chevron");
|
|
663
|
+
if (chevron) chevron.style.transform = expanded ? "rotate(180deg)" : "";
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function persistElementExpandState(el, kind) {
|
|
670
|
+
var itemKey = getElementExpandKey(el);
|
|
671
|
+
if (!itemKey) return;
|
|
672
|
+
setPersistedExpandState(itemKey, isElementExpanded(el, kind));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function applyPersistedExpandState(container) {
|
|
676
|
+
if (!container || !state.selectedId) return;
|
|
677
|
+
container.querySelectorAll("[data-expand-key]").forEach(function(el) {
|
|
678
|
+
var itemKey = getElementExpandKey(el);
|
|
679
|
+
var kind = el.dataset.expandKind || "";
|
|
680
|
+
var persisted = getPersistedExpandState(itemKey);
|
|
681
|
+
if (persisted === null || !kind) return;
|
|
682
|
+
applyExpandedState(el, kind, persisted);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
264
686
|
function resetChatRenderCache() {
|
|
265
687
|
state.lastRenderedHash = 0;
|
|
266
688
|
state.lastRenderedMsgCount = 0;
|
|
267
689
|
state.lastRenderedEmpty = null;
|
|
690
|
+
state.renderPending = false;
|
|
691
|
+
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
692
|
+
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
693
|
+
}
|
|
694
|
+
state.chatScrollElement = null;
|
|
695
|
+
state.chatScrollHandler = null;
|
|
696
|
+
state.showChatJumpToBottom = false;
|
|
697
|
+
state.chatIsProgrammaticScroll = false;
|
|
268
698
|
}
|
|
269
699
|
|
|
270
700
|
function getEffectiveCwd() {
|
|
@@ -314,9 +744,6 @@
|
|
|
314
744
|
}
|
|
315
745
|
}
|
|
316
746
|
|
|
317
|
-
renderBootLoading();
|
|
318
|
-
restoreLoginSession();
|
|
319
|
-
|
|
320
747
|
function renderBootLoading() {
|
|
321
748
|
var app = document.getElementById("app");
|
|
322
749
|
if (!app) return;
|
|
@@ -324,11 +751,65 @@
|
|
|
324
751
|
'<div class="boot-loading">' +
|
|
325
752
|
'<div class="boot-loading-card">' +
|
|
326
753
|
'<div class="boot-loading-spinner"></div>' +
|
|
327
|
-
'<div class="boot-loading-text"
|
|
754
|
+
'<div class="boot-loading-text">正在连接 Wand…</div>' +
|
|
328
755
|
'</div>' +
|
|
329
756
|
'</div>';
|
|
330
757
|
}
|
|
331
758
|
|
|
759
|
+
function scheduleForegroundSync(reason) {
|
|
760
|
+
if (!state.config) return;
|
|
761
|
+
if (document.hidden) return;
|
|
762
|
+
var now = Date.now();
|
|
763
|
+
if (now - state.lastForegroundSyncAt < 1500) return;
|
|
764
|
+
state.lastForegroundSyncAt = now;
|
|
765
|
+
if (state.foregroundSyncTimer) {
|
|
766
|
+
clearTimeout(state.foregroundSyncTimer);
|
|
767
|
+
}
|
|
768
|
+
state.foregroundSyncTimer = setTimeout(function() {
|
|
769
|
+
state.foregroundSyncTimer = null;
|
|
770
|
+
syncOnForeground(reason);
|
|
771
|
+
}, 80);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function syncOnForeground(reason) {
|
|
775
|
+
if (!state.config) return Promise.resolve();
|
|
776
|
+
if (document.hidden) return Promise.resolve();
|
|
777
|
+
if (!state.ws || (state.ws.readyState !== WebSocket.OPEN && state.ws.readyState !== WebSocket.CONNECTING)) {
|
|
778
|
+
initWebSocket();
|
|
779
|
+
}
|
|
780
|
+
return loadSessions({ skipSelectedOutputReload: true }).then(function() {
|
|
781
|
+
if (state.selectedId) {
|
|
782
|
+
return loadOutput(state.selectedId);
|
|
783
|
+
}
|
|
784
|
+
scheduleChatRender(true);
|
|
785
|
+
}).catch(function(e) {
|
|
786
|
+
console.error("[wand] foreground sync failed:", reason, e);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function bindForegroundSyncListeners() {
|
|
791
|
+
if (window.__wandForegroundSyncBound) return;
|
|
792
|
+
window.__wandForegroundSyncBound = true;
|
|
793
|
+
|
|
794
|
+
document.addEventListener("visibilitychange", function() {
|
|
795
|
+
if (!document.hidden) {
|
|
796
|
+
scheduleForegroundSync("visibility");
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
window.addEventListener("focus", function() {
|
|
801
|
+
scheduleForegroundSync("focus");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
window.addEventListener("pageshow", function() {
|
|
805
|
+
scheduleForegroundSync("pageshow");
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
window.addEventListener("resume", function() {
|
|
809
|
+
scheduleForegroundSync("resume");
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
332
813
|
function restoreLoginSession() {
|
|
333
814
|
fetch("/api/config", { credentials: "same-origin" })
|
|
334
815
|
.then(function(res) {
|
|
@@ -344,20 +825,16 @@
|
|
|
344
825
|
state.config = config;
|
|
345
826
|
state.loginChecked = true;
|
|
346
827
|
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
828
|
try {
|
|
351
829
|
render({ skipShellChrome: true });
|
|
352
830
|
} catch (_e) {
|
|
353
831
|
// render() may fail if external scripts (xterm.js) failed to load;
|
|
354
832
|
// continue with polling and session loading so the app remains functional
|
|
355
833
|
}
|
|
834
|
+
bindForegroundSyncListeners();
|
|
356
835
|
startPolling();
|
|
357
836
|
refreshAll();
|
|
358
|
-
// Request browser notification permission after login
|
|
359
837
|
requestNotificationPermission();
|
|
360
|
-
// Show update bubble if server reports an available update
|
|
361
838
|
if (config.updateAvailable && config.latestVersion) {
|
|
362
839
|
showNotificationBubble({
|
|
363
840
|
title: "\u53d1\u73b0\u65b0\u7248\u672c",
|
|
@@ -373,7 +850,6 @@
|
|
|
373
850
|
});
|
|
374
851
|
sendBrowserNotification("Wand \u53d1\u73b0\u65b0\u7248\u672c", "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion, { tag: "wand-update" });
|
|
375
852
|
}
|
|
376
|
-
// Auto-load claude history since section defaults to expanded
|
|
377
853
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
378
854
|
loadClaudeHistory();
|
|
379
855
|
}
|
|
@@ -381,7 +857,6 @@
|
|
|
381
857
|
})
|
|
382
858
|
.catch(function() {
|
|
383
859
|
state.loginChecked = true;
|
|
384
|
-
// If offline (no network), show a friendly offline message instead of login
|
|
385
860
|
if (!navigator.onLine) {
|
|
386
861
|
var app = document.getElementById("app");
|
|
387
862
|
if (app) {
|
|
@@ -394,7 +869,6 @@
|
|
|
394
869
|
'</div>' +
|
|
395
870
|
'</div>';
|
|
396
871
|
}
|
|
397
|
-
// Retry when network comes back
|
|
398
872
|
window.addEventListener('online', function() { location.reload(); }, { once: true });
|
|
399
873
|
return;
|
|
400
874
|
}
|
|
@@ -402,6 +876,9 @@
|
|
|
402
876
|
});
|
|
403
877
|
}
|
|
404
878
|
|
|
879
|
+
renderBootLoading();
|
|
880
|
+
restoreLoginSession();
|
|
881
|
+
|
|
405
882
|
function render(options) {
|
|
406
883
|
var skipShellChrome = options && options.skipShellChrome;
|
|
407
884
|
var app = document.getElementById("app");
|
|
@@ -613,7 +1090,10 @@
|
|
|
613
1090
|
'</aside>' +
|
|
614
1091
|
'<main class="main-content">' +
|
|
615
1092
|
'<span class="current-task hidden" id="current-task"></span>' +
|
|
616
|
-
'' +
|
|
1093
|
+
'<div class="view-toggle-bar' + (state.selectedId ? '' : ' hidden') + '" id="view-toggle-bar">' +
|
|
1094
|
+
'<button id="view-terminal-btn" class="topbar-btn' + (state.currentView === "terminal" ? ' active' : '') + '" type="button" title="查看原始终端输出">终端</button>' +
|
|
1095
|
+
'<button id="view-chat-btn" class="topbar-btn' + (state.currentView !== "terminal" ? ' active' : '') + '" type="button" title="查看聊天解析视图">聊天</button>' +
|
|
1096
|
+
'</div>' +
|
|
617
1097
|
// File panel backdrop (mobile)
|
|
618
1098
|
'<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
|
|
619
1099
|
// File side panel
|
|
@@ -644,7 +1124,12 @@
|
|
|
644
1124
|
'</div>' +
|
|
645
1125
|
'<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
|
|
646
1126
|
'</div>' +
|
|
647
|
-
'<div id="chat-output" class="chat-container hidden"
|
|
1127
|
+
'<div id="chat-output" class="chat-container hidden">' +
|
|
1128
|
+
'<div class="chat-overlay-controls">' +
|
|
1129
|
+
'<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>' +
|
|
1130
|
+
'</div>' +
|
|
1131
|
+
'<button id="chat-jump-bottom" class="chat-jump-bottom' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底">↓ 最新</button>' +
|
|
1132
|
+
'</div>' +
|
|
648
1133
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
649
1134
|
'<div class="blank-chat-inner">' +
|
|
650
1135
|
'<div class="blank-chat-logo">W</div>' +
|
|
@@ -654,6 +1139,9 @@
|
|
|
654
1139
|
'<button class="blank-chat-tool-btn" id="welcome-tool-claude" type="button">' +
|
|
655
1140
|
'<span class="tool-icon">🤖</span>新建终端会话' +
|
|
656
1141
|
'</button>' +
|
|
1142
|
+
'<button class="blank-chat-tool-btn" id="welcome-tool-codex" type="button">' +
|
|
1143
|
+
'<span class="tool-icon">⌘</span>新建 Codex 会话' +
|
|
1144
|
+
'</button>' +
|
|
657
1145
|
'<button class="blank-chat-tool-btn" id="welcome-tool-structured" type="button">' +
|
|
658
1146
|
'<span class="tool-icon">💬</span>新建结构化会话' +
|
|
659
1147
|
'</button>' +
|
|
@@ -683,7 +1171,7 @@
|
|
|
683
1171
|
'</div>' +
|
|
684
1172
|
'</div>' +
|
|
685
1173
|
'<div class="input-composer">' +
|
|
686
|
-
'<textarea id="input-box" class="input-textarea" placeholder="' + (state.terminalInteractive
|
|
1174
|
+
'<textarea id="input-box" class="input-textarea" placeholder="' + getComposerPlaceholder(selectedSession, state.terminalInteractive) + '" rows="1">' + escapeHtml(currentDraft) + '</textarea>' +
|
|
687
1175
|
'<div class="input-composer-bar">' +
|
|
688
1176
|
'<div class="input-composer-left">' +
|
|
689
1177
|
'<select id="chat-mode-select" class="chat-mode-select" title="仅对新建会话生效">' +
|
|
@@ -721,7 +1209,7 @@
|
|
|
721
1209
|
'<span id="session-kind-display" class="session-kind-display">' + (selectedSession ? getSessionKindLabel(selectedSession) : '终端') + '</span>' +
|
|
722
1210
|
'<span class="session-info-separator">|</span>' +
|
|
723
1211
|
'<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>' : '') +
|
|
1212
|
+
(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
1213
|
(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
1214
|
'</div>' +
|
|
727
1215
|
'</div>' +
|
|
@@ -1796,7 +2284,7 @@
|
|
|
1796
2284
|
var recoveryHint = "";
|
|
1797
2285
|
var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
|
|
1798
2286
|
|
|
1799
|
-
if (session.claudeSessionId) {
|
|
2287
|
+
if (session.provider === "claude" && session.claudeSessionId) {
|
|
1800
2288
|
var shortId = session.claudeSessionId.slice(0, 8);
|
|
1801
2289
|
sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
|
|
1802
2290
|
if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
|
|
@@ -1834,12 +2322,20 @@
|
|
|
1834
2322
|
'</div>';
|
|
1835
2323
|
}
|
|
1836
2324
|
|
|
2325
|
+
function renderWorktreeBadge(session) {
|
|
2326
|
+
if (!session || !session.worktreeEnabled) return "";
|
|
2327
|
+
var title = session.worktree && session.worktree.branch
|
|
2328
|
+
? ' title="' + escapeHtml('Worktree: ' + session.worktree.branch) + '"'
|
|
2329
|
+
: '';
|
|
2330
|
+
return '<span class="session-kind-badge worktree"' + title + '>Worktree</span>';
|
|
2331
|
+
}
|
|
2332
|
+
|
|
1837
2333
|
function renderSessionKindBadge(session) {
|
|
1838
2334
|
if (!session) return "";
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
return
|
|
2335
|
+
var primary = isStructuredSession(session)
|
|
2336
|
+
? '<span class="session-kind-badge structured">Structured</span>'
|
|
2337
|
+
: '<span class="session-kind-badge pty">PTY</span>';
|
|
2338
|
+
return primary + renderWorktreeBadge(session);
|
|
1843
2339
|
}
|
|
1844
2340
|
|
|
1845
2341
|
function renderModeCards(selectedMode) {
|
|
@@ -1859,6 +2355,20 @@
|
|
|
1859
2355
|
}).join("");
|
|
1860
2356
|
}
|
|
1861
2357
|
|
|
2358
|
+
function renderProviderOptions(selectedTool) {
|
|
2359
|
+
var tools = [
|
|
2360
|
+
{ id: "claude", label: "Claude", desc: "完整 Claude 会话能力" },
|
|
2361
|
+
{ id: "codex", label: "Codex", desc: "PTY 透传,全权限启动" }
|
|
2362
|
+
];
|
|
2363
|
+
return tools.map(function(tool) {
|
|
2364
|
+
var active = tool.id === selectedTool ? " active" : "";
|
|
2365
|
+
return '<button type="button" class="mode-card provider-card' + active + '" data-provider="' + tool.id + '">' +
|
|
2366
|
+
'<span class="mode-card-label">' + tool.label + '</span>' +
|
|
2367
|
+
'<span class="mode-card-desc">' + tool.desc + '</span>' +
|
|
2368
|
+
'</button>';
|
|
2369
|
+
}).join("");
|
|
2370
|
+
}
|
|
2371
|
+
|
|
1862
2372
|
function renderSessionKindOptions(selectedKind) {
|
|
1863
2373
|
var kinds = [
|
|
1864
2374
|
{ id: "structured", label: "结构化", desc: "智能对话模式" },
|
|
@@ -1866,17 +2376,29 @@
|
|
|
1866
2376
|
];
|
|
1867
2377
|
return kinds.map(function(kind) {
|
|
1868
2378
|
var active = kind.id === selectedKind ? " active" : "";
|
|
1869
|
-
|
|
2379
|
+
var disabled = (state.sessionTool === "codex" && kind.id === "structured") ? " disabled" : "";
|
|
2380
|
+
return '<button type="button" class="mode-card session-kind-card' + active + disabled + '" data-session-kind="' + kind.id + '">' +
|
|
1870
2381
|
'<span class="mode-card-label">' + kind.label + '</span>' +
|
|
1871
2382
|
'<span class="mode-card-desc">' + kind.desc + '</span>' +
|
|
1872
2383
|
'</button>';
|
|
1873
2384
|
}).join("");
|
|
1874
2385
|
}
|
|
1875
2386
|
|
|
2387
|
+
function renderWorktreeToggle(enabled) {
|
|
2388
|
+
return '<label class="session-inline-toggle" for="session-worktree-toggle">' +
|
|
2389
|
+
'<input id="session-worktree-toggle" type="checkbox" class="field-checkbox"' + (enabled ? ' checked' : '') + ' />' +
|
|
2390
|
+
'<span class="session-inline-toggle-label">Worktree 模式</span>' +
|
|
2391
|
+
'</label>';
|
|
2392
|
+
}
|
|
2393
|
+
|
|
1876
2394
|
function getSessionKindHint(kind) {
|
|
2395
|
+
var tool = state.sessionTool || "claude";
|
|
1877
2396
|
if (kind === "structured") {
|
|
1878
2397
|
return "结构化聊天界面,支持多轮对话、流式输出和工具调用展示。";
|
|
1879
2398
|
}
|
|
2399
|
+
if (tool === "codex") {
|
|
2400
|
+
return "Codex 仅支持 PTY;terminal 是原始输出,chat 是解析后的阅读视图。";
|
|
2401
|
+
}
|
|
1880
2402
|
return "原始 PTY 终端会话,支持持续交互、终端视图和权限流。";
|
|
1881
2403
|
}
|
|
1882
2404
|
|
|
@@ -1884,22 +2406,32 @@
|
|
|
1884
2406
|
var modalTool = getPreferredTool();
|
|
1885
2407
|
var modalMode = getSafeModeForTool(modalTool, state.modeValue || state.chatMode || "default");
|
|
1886
2408
|
var sessionKind = state.sessionCreateKind || "structured";
|
|
2409
|
+
var worktreeEnabled = state.sessionCreateWorktree === true;
|
|
1887
2410
|
return '<section id="session-modal" class="modal-backdrop hidden">' +
|
|
1888
2411
|
'<div class="modal session-modal">' +
|
|
1889
2412
|
'<div class="modal-header">' +
|
|
1890
2413
|
'<div>' +
|
|
1891
2414
|
'<h2 class="modal-title">新对话</h2>' +
|
|
1892
|
-
'<p class="modal-subtitle">启动 Claude
|
|
2415
|
+
'<p class="modal-subtitle">启动 Claude 或 Codex 会话,选择 provider、会话类型、模式和工作目录。</p>' +
|
|
1893
2416
|
'</div>' +
|
|
1894
2417
|
'<button id="close-modal-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
1895
2418
|
'</div>' +
|
|
1896
2419
|
'<div class="modal-body">' +
|
|
2420
|
+
'<div class="field">' +
|
|
2421
|
+
'<label class="field-label">Provider</label>' +
|
|
2422
|
+
'<div id="provider-cards" class="mode-cards">' +
|
|
2423
|
+
renderProviderOptions(modalTool) +
|
|
2424
|
+
'</div>' +
|
|
2425
|
+
'</div>' +
|
|
1897
2426
|
'<div class="field">' +
|
|
1898
2427
|
'<label class="field-label">会话类型</label>' +
|
|
1899
2428
|
'<div id="session-kind-cards" class="mode-cards">' +
|
|
1900
2429
|
renderSessionKindOptions(sessionKind) +
|
|
1901
2430
|
'</div>' +
|
|
1902
|
-
'<
|
|
2431
|
+
'<div class="field-hint session-kind-hint-row">' +
|
|
2432
|
+
'<span id="session-kind-description">' + escapeHtml(getSessionKindHint(sessionKind)) + '</span>' +
|
|
2433
|
+
renderWorktreeToggle(worktreeEnabled) +
|
|
2434
|
+
'</div>' +
|
|
1903
2435
|
'</div>' +
|
|
1904
2436
|
'<div class="field">' +
|
|
1905
2437
|
'<label class="field-label">模式</label>' +
|
|
@@ -1927,7 +2459,10 @@
|
|
|
1927
2459
|
// Global toggle function for tool card headers — called via onclick attribute
|
|
1928
2460
|
window.__tcToggle = function(e, headerEl) {
|
|
1929
2461
|
var card = headerEl.closest(".tool-use-card");
|
|
1930
|
-
if (card)
|
|
2462
|
+
if (card) {
|
|
2463
|
+
card.classList.toggle("collapsed");
|
|
2464
|
+
persistElementExpandState(card, "tool-card");
|
|
2465
|
+
}
|
|
1931
2466
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
1932
2467
|
};
|
|
1933
2468
|
// Toggle function for inline thinking blocks — called via onclick attribute
|
|
@@ -1947,6 +2482,7 @@
|
|
|
1947
2482
|
var action = el.querySelector(".thinking-inline-action");
|
|
1948
2483
|
if (action) action.textContent = "展开";
|
|
1949
2484
|
}
|
|
2485
|
+
persistElementExpandState(el, "thinking");
|
|
1950
2486
|
};
|
|
1951
2487
|
// Toggle function for inline tool rows (Read, Glob, Grep, etc.)
|
|
1952
2488
|
window.__inlineToolToggle = function(el) {
|
|
@@ -1964,6 +2500,7 @@
|
|
|
1964
2500
|
statusSpan.textContent = "✓";
|
|
1965
2501
|
}
|
|
1966
2502
|
}
|
|
2503
|
+
persistElementExpandState(el, "inline-tool");
|
|
1967
2504
|
};
|
|
1968
2505
|
// Toggle function for terminal tool blocks
|
|
1969
2506
|
window.__terminalExpand = function(el) {
|
|
@@ -1976,6 +2513,7 @@
|
|
|
1976
2513
|
container.dataset.expanded = isHidden ? "true" : "false";
|
|
1977
2514
|
var toggleIcon = el.querySelector(".term-toggle-icon");
|
|
1978
2515
|
if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "▶";
|
|
2516
|
+
persistElementExpandState(container, "terminal");
|
|
1979
2517
|
}
|
|
1980
2518
|
};
|
|
1981
2519
|
// Update streaming thinking content (called from WebSocket handler)
|
|
@@ -2075,6 +2613,15 @@
|
|
|
2075
2613
|
quickStartSession();
|
|
2076
2614
|
});
|
|
2077
2615
|
}
|
|
2616
|
+
var welcomeCodexBtn = document.getElementById("welcome-tool-codex");
|
|
2617
|
+
if (welcomeCodexBtn) {
|
|
2618
|
+
welcomeCodexBtn.addEventListener("click", function() {
|
|
2619
|
+
state.sessionTool = "codex";
|
|
2620
|
+
state.preferredCommand = "codex";
|
|
2621
|
+
state.modeValue = "full-access";
|
|
2622
|
+
quickStartSession();
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2078
2625
|
var welcomeStructuredBtn = document.getElementById("welcome-tool-structured");
|
|
2079
2626
|
if (welcomeStructuredBtn) {
|
|
2080
2627
|
welcomeStructuredBtn.addEventListener("click", function() {
|
|
@@ -2097,10 +2644,26 @@
|
|
|
2097
2644
|
// Claude session ID badge click-to-copy (event delegation on document)
|
|
2098
2645
|
document.addEventListener("click", handleClaudeIdCopy);
|
|
2099
2646
|
|
|
2647
|
+
var providerCardsEl = document.getElementById("provider-cards");
|
|
2648
|
+
if (providerCardsEl) providerCardsEl.addEventListener("click", function(e) {
|
|
2649
|
+
var card = e.target.closest(".provider-card");
|
|
2650
|
+
if (!card || card.classList.contains("disabled")) return;
|
|
2651
|
+
var provider = card.getAttribute("data-provider");
|
|
2652
|
+
if (provider) {
|
|
2653
|
+
state.sessionTool = provider;
|
|
2654
|
+
state.preferredCommand = provider;
|
|
2655
|
+
if (provider === "codex") {
|
|
2656
|
+
state.sessionCreateKind = "pty";
|
|
2657
|
+
state.modeValue = "full-access";
|
|
2658
|
+
}
|
|
2659
|
+
syncSessionModalUI();
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
|
|
2100
2663
|
var kindCardsEl = document.getElementById("session-kind-cards");
|
|
2101
2664
|
if (kindCardsEl) kindCardsEl.addEventListener("click", function(e) {
|
|
2102
2665
|
var card = e.target.closest(".session-kind-card");
|
|
2103
|
-
if (!card) return;
|
|
2666
|
+
if (!card || card.classList.contains("disabled")) return;
|
|
2104
2667
|
var kind = card.getAttribute("data-session-kind");
|
|
2105
2668
|
if (kind) {
|
|
2106
2669
|
state.sessionCreateKind = kind;
|
|
@@ -2118,6 +2681,10 @@
|
|
|
2118
2681
|
syncSessionModalUI();
|
|
2119
2682
|
}
|
|
2120
2683
|
});
|
|
2684
|
+
var worktreeToggleEl = document.getElementById("session-worktree-toggle");
|
|
2685
|
+
if (worktreeToggleEl) worktreeToggleEl.addEventListener("change", function() {
|
|
2686
|
+
state.sessionCreateWorktree = this.checked;
|
|
2687
|
+
});
|
|
2121
2688
|
var cwdEl = document.getElementById("cwd");
|
|
2122
2689
|
if (cwdEl) {
|
|
2123
2690
|
cwdEl.addEventListener("input", function() { state.cwdValue = this.value; });
|
|
@@ -2219,6 +2786,9 @@
|
|
|
2219
2786
|
inputBox.addEventListener("keydown", handleInputBoxKeydown);
|
|
2220
2787
|
inputBox.addEventListener("paste", handleInputPaste);
|
|
2221
2788
|
inputBox.addEventListener("input", function() {
|
|
2789
|
+
if (handleInteractiveTextInput(inputBox)) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2222
2792
|
refreshInputBoxState(inputBox);
|
|
2223
2793
|
setDraftValue(inputBox.value, true);
|
|
2224
2794
|
});
|
|
@@ -2233,6 +2803,8 @@
|
|
|
2233
2803
|
// View toggle handlers
|
|
2234
2804
|
var viewTermBtn = document.getElementById("view-terminal-btn");
|
|
2235
2805
|
if (viewTermBtn) viewTermBtn.addEventListener("click", function() { setView("terminal"); });
|
|
2806
|
+
var viewChatBtn = document.getElementById("view-chat-btn");
|
|
2807
|
+
if (viewChatBtn) viewChatBtn.addEventListener("click", function() { setView("chat"); });
|
|
2236
2808
|
// Terminal interactive toggle (both topbar and terminal-header)
|
|
2237
2809
|
var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
|
|
2238
2810
|
terminalInteractiveToggles.forEach(function(id) {
|
|
@@ -2312,8 +2884,18 @@
|
|
|
2312
2884
|
if (jumpBottomBtn) jumpBottomBtn.addEventListener("click", function() {
|
|
2313
2885
|
maybeScrollTerminalToBottom("force");
|
|
2314
2886
|
});
|
|
2315
|
-
|
|
2316
|
-
|
|
2887
|
+
var chatFollowToggle = document.getElementById("chat-follow-toggle");
|
|
2888
|
+
if (chatFollowToggle) chatFollowToggle.addEventListener("click", function() {
|
|
2889
|
+
if (state.chatAutoFollow) {
|
|
2890
|
+
setChatAutoFollow(false, { scrollNow: false });
|
|
2891
|
+
} else {
|
|
2892
|
+
setChatAutoFollow(true, { scrollNow: true, smooth: false });
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
var chatJumpBottomBtn = document.getElementById("chat-jump-bottom");
|
|
2896
|
+
if (chatJumpBottomBtn) chatJumpBottomBtn.addEventListener("click", function() {
|
|
2897
|
+
setChatAutoFollow(true, { scrollNow: true, smooth: true });
|
|
2898
|
+
});
|
|
2317
2899
|
var fileRefresh = document.getElementById("file-explorer-refresh");
|
|
2318
2900
|
if (fileRefresh) fileRefresh.addEventListener("click", refreshFileExplorer);
|
|
2319
2901
|
|
|
@@ -2959,8 +3541,8 @@
|
|
|
2959
3541
|
|
|
2960
3542
|
function setTerminalManualScrollActive() {
|
|
2961
3543
|
state.terminalAutoFollow = false;
|
|
3544
|
+
clearTerminalScrollIdleTimer();
|
|
2962
3545
|
updateTerminalJumpToBottomButton();
|
|
2963
|
-
scheduleTerminalResumeFollow();
|
|
2964
3546
|
}
|
|
2965
3547
|
|
|
2966
3548
|
function maybeScrollTerminalToBottom(reason) {
|
|
@@ -3171,6 +3753,7 @@
|
|
|
3171
3753
|
var shouldScroll = opts.scroll !== false;
|
|
3172
3754
|
var sessionChanged = state.terminalSessionId !== nextSessionId;
|
|
3173
3755
|
var currentOutput = state.terminalOutput || "";
|
|
3756
|
+
var liveChunkStream = !!(nextSessionId && state.terminalLiveStreamSessions[nextSessionId]);
|
|
3174
3757
|
var wrote = false;
|
|
3175
3758
|
|
|
3176
3759
|
if (normalizedOutput === currentOutput && !sessionChanged) {
|
|
@@ -3199,6 +3782,10 @@
|
|
|
3199
3782
|
} else if (normalizedOutput.length < currentOutput.length && !sessionChanged) {
|
|
3200
3783
|
// Ignore regressive snapshots for the active session; wait for an explicit replace.
|
|
3201
3784
|
return false;
|
|
3785
|
+
} else if (liveChunkStream && !sessionChanged && mode !== "replace" && currentOutput && !normalizedOutput.startsWith(currentOutput)) {
|
|
3786
|
+
// When a session is already streaming live chunks, do not let polled snapshots
|
|
3787
|
+
// rewrite the terminal unless they are strict appends of what we've rendered.
|
|
3788
|
+
return false;
|
|
3202
3789
|
} else if (normalizedOutput.startsWith(currentOutput)) {
|
|
3203
3790
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
3204
3791
|
if (delta) {
|
|
@@ -3352,7 +3939,7 @@
|
|
|
3352
3939
|
if (state.selectedId) {
|
|
3353
3940
|
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
3354
3941
|
if (session) {
|
|
3355
|
-
syncTerminalBuffer(session.id, session.output || "", { mode: "
|
|
3942
|
+
syncTerminalBuffer(session.id, session.output || "", { mode: "append", scroll: false });
|
|
3356
3943
|
}
|
|
3357
3944
|
} else {
|
|
3358
3945
|
state.terminal.writeln("点击上方「新对话」开始你的第一次对话。");
|
|
@@ -3467,14 +4054,38 @@
|
|
|
3467
4054
|
}
|
|
3468
4055
|
|
|
3469
4056
|
function getPreferredTool() {
|
|
3470
|
-
return "claude";
|
|
4057
|
+
return state.sessionTool || state.preferredCommand || "claude";
|
|
3471
4058
|
}
|
|
3472
4059
|
|
|
3473
4060
|
function getComposerTool() {
|
|
3474
|
-
return
|
|
4061
|
+
var selected = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
4062
|
+
return (selected && selected.provider) || state.preferredCommand || "claude";
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
function getComposerPlaceholder(session, terminalInteractive) {
|
|
4066
|
+
if (terminalInteractive) {
|
|
4067
|
+
return "终端交互模式开启中,请直接在终端中输入";
|
|
4068
|
+
}
|
|
4069
|
+
if (session && session.provider === "codex") {
|
|
4070
|
+
if (session.status !== "running") {
|
|
4071
|
+
return "Codex 会话已结束,无法继续发送";
|
|
4072
|
+
}
|
|
4073
|
+
return state.currentView === "terminal"
|
|
4074
|
+
? "向 Codex 发送输入;terminal 为原始 TUI 输出"
|
|
4075
|
+
: "向 Codex 发送输入;chat 为解析后的阅读视图";
|
|
4076
|
+
}
|
|
4077
|
+
if (session && !isStructuredSession(session) && session.status !== "running") {
|
|
4078
|
+
return "会话已结束,无法继续发送";
|
|
4079
|
+
}
|
|
4080
|
+
return session && isStructuredSession(session) && session.structuredState && session.structuredState.inFlight
|
|
4081
|
+
? "思考中,可继续发送,消息会自动排队"
|
|
4082
|
+
: "输入消息...";
|
|
3475
4083
|
}
|
|
3476
4084
|
|
|
3477
4085
|
function getToolModeHint(tool, mode) {
|
|
4086
|
+
if (tool === "codex") {
|
|
4087
|
+
return "Codex 当前仅支持 PTY 透传,并固定以 full-access 启动。";
|
|
4088
|
+
}
|
|
3478
4089
|
if (mode === "full-access") {
|
|
3479
4090
|
return "自动确认权限请求与高权限操作,适合你确认环境安全后的连续修改。";
|
|
3480
4091
|
}
|
|
@@ -3491,6 +4102,9 @@
|
|
|
3491
4102
|
}
|
|
3492
4103
|
|
|
3493
4104
|
function getSupportedModes(tool) {
|
|
4105
|
+
if (tool === "codex") {
|
|
4106
|
+
return ["full-access"];
|
|
4107
|
+
}
|
|
3494
4108
|
return ["default", "full-access", "auto-edit", "native", "managed"];
|
|
3495
4109
|
}
|
|
3496
4110
|
|
|
@@ -3523,13 +4137,21 @@
|
|
|
3523
4137
|
}
|
|
3524
4138
|
|
|
3525
4139
|
function getSessionKindLabel(session) {
|
|
3526
|
-
|
|
4140
|
+
var provider = session && session.provider ? session.provider : "claude";
|
|
4141
|
+
return (isStructuredSession(session) ? "结构化" : "终端") + " · " + provider;
|
|
3527
4142
|
}
|
|
3528
4143
|
|
|
3529
4144
|
function getSessionKindDescription(session) {
|
|
3530
4145
|
return isStructuredSession(session)
|
|
3531
4146
|
? "结构化 · 块级记录"
|
|
3532
|
-
:
|
|
4147
|
+
: (session && session.provider === "codex"
|
|
4148
|
+
? "终端 · Codex PTY(chat 为解析视图)"
|
|
4149
|
+
: "终端 · PTY 会话");
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
function shouldRequestChatFormat(session) {
|
|
4153
|
+
if (!session) return false;
|
|
4154
|
+
return isStructuredSession(session) || session.provider === "codex";
|
|
3533
4155
|
}
|
|
3534
4156
|
|
|
3535
4157
|
function isRecoverableToolError(toolResult, nextResult) {
|
|
@@ -3546,9 +4168,7 @@
|
|
|
3546
4168
|
}
|
|
3547
4169
|
|
|
3548
4170
|
function isStructuredSession(session) {
|
|
3549
|
-
|
|
3550
|
-
if (session) console.log("[WAND] isStructuredSession id:", session.id, "sessionKind:", session.sessionKind, "runner:", session.runner, "=>", result);
|
|
3551
|
-
return result;
|
|
4171
|
+
return !!session && (session.sessionKind === "structured" || session.runner === "claude-cli-print");
|
|
3552
4172
|
}
|
|
3553
4173
|
|
|
3554
4174
|
function syncComposerModeSelect() {
|
|
@@ -3561,12 +4181,13 @@
|
|
|
3561
4181
|
if (modeHint) modeHint.textContent = getModeHint(state.chatMode);
|
|
3562
4182
|
}
|
|
3563
4183
|
|
|
3564
|
-
function createStructuredSession(prompt, cwdOverride, modeOverride) {
|
|
4184
|
+
function createStructuredSession(prompt, cwdOverride, modeOverride, worktreeEnabled) {
|
|
3565
4185
|
var payload = {
|
|
3566
4186
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
3567
4187
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
3568
4188
|
runner: state.structuredRunner || "claude-cli-print",
|
|
3569
|
-
prompt: prompt || undefined
|
|
4189
|
+
prompt: prompt || undefined,
|
|
4190
|
+
worktreeEnabled: worktreeEnabled === true
|
|
3570
4191
|
};
|
|
3571
4192
|
console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
|
|
3572
4193
|
return fetch("/api/structured-sessions", {
|
|
@@ -3599,24 +4220,31 @@
|
|
|
3599
4220
|
function applyCurrentView() {
|
|
3600
4221
|
var hasSession = !!state.selectedId;
|
|
3601
4222
|
var terminalBtn = document.getElementById("view-terminal-btn");
|
|
4223
|
+
var chatBtn = document.getElementById("view-chat-btn");
|
|
4224
|
+
var toggleBar = document.getElementById("view-toggle-bar");
|
|
3602
4225
|
var terminalContainer = document.getElementById("output");
|
|
3603
4226
|
var chatContainer = document.getElementById("chat-output");
|
|
3604
4227
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
3605
4228
|
var structured = isStructuredSession(selectedSession);
|
|
3606
4229
|
var showTerminal = hasSession && !structured && state.currentView === "terminal";
|
|
3607
4230
|
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
4231
|
if (structured) {
|
|
3611
4232
|
state.currentView = "chat";
|
|
3612
4233
|
} else if (!hasSession) {
|
|
3613
4234
|
state.currentView = "terminal";
|
|
3614
4235
|
}
|
|
3615
4236
|
|
|
4237
|
+
if (toggleBar) {
|
|
4238
|
+
toggleBar.classList.toggle("hidden", !hasSession);
|
|
4239
|
+
}
|
|
3616
4240
|
if (terminalBtn) {
|
|
3617
4241
|
terminalBtn.classList.toggle("hidden", structured || !hasSession);
|
|
3618
4242
|
terminalBtn.classList.toggle("active", showTerminal);
|
|
3619
4243
|
}
|
|
4244
|
+
if (chatBtn) {
|
|
4245
|
+
chatBtn.classList.toggle("hidden", !hasSession);
|
|
4246
|
+
chatBtn.classList.toggle("active", showChat);
|
|
4247
|
+
}
|
|
3620
4248
|
if (terminalContainer) {
|
|
3621
4249
|
terminalContainer.classList.toggle("active", showTerminal);
|
|
3622
4250
|
terminalContainer.classList.toggle("hidden", !showTerminal);
|
|
@@ -3625,22 +4253,45 @@
|
|
|
3625
4253
|
chatContainer.classList.toggle("active", showChat);
|
|
3626
4254
|
chatContainer.classList.toggle("hidden", !showChat);
|
|
3627
4255
|
}
|
|
4256
|
+
if (chatContainer && showChat) {
|
|
4257
|
+
ensureChatMessagesContainer(chatContainer);
|
|
4258
|
+
}
|
|
4259
|
+
bindChatScrollListener();
|
|
4260
|
+
updateChatFollowToggleButton();
|
|
4261
|
+
updateChatJumpToBottomButton();
|
|
3628
4262
|
updateInteractiveControls();
|
|
3629
4263
|
}
|
|
3630
4264
|
|
|
3631
4265
|
function syncSessionModalUI() {
|
|
3632
4266
|
var modeHint = document.getElementById("mode-description");
|
|
3633
4267
|
var kindHint = document.getElementById("session-kind-description");
|
|
3634
|
-
var tool = "claude";
|
|
4268
|
+
var tool = state.sessionTool || "claude";
|
|
3635
4269
|
var sessionKind = state.sessionCreateKind || "structured";
|
|
3636
4270
|
|
|
4271
|
+
if (tool === "codex" && sessionKind === "structured") {
|
|
4272
|
+
sessionKind = "pty";
|
|
4273
|
+
state.sessionCreateKind = "pty";
|
|
4274
|
+
}
|
|
4275
|
+
|
|
3637
4276
|
state.sessionTool = tool;
|
|
3638
4277
|
state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
|
|
3639
4278
|
|
|
4279
|
+
var providerCards = document.querySelectorAll("#provider-cards .provider-card");
|
|
4280
|
+
if (providerCards.length) {
|
|
4281
|
+
providerCards.forEach(function(card) {
|
|
4282
|
+
var provider = card.getAttribute("data-provider");
|
|
4283
|
+
card.classList.toggle("active", provider === tool);
|
|
4284
|
+
card.classList.remove("disabled");
|
|
4285
|
+
});
|
|
4286
|
+
}
|
|
4287
|
+
|
|
3640
4288
|
var kindCards = document.querySelectorAll("#session-kind-cards .session-kind-card");
|
|
3641
4289
|
if (kindCards.length) {
|
|
3642
4290
|
kindCards.forEach(function(card) {
|
|
3643
|
-
|
|
4291
|
+
var kind = card.getAttribute("data-session-kind");
|
|
4292
|
+
var disabled = tool === "codex" && kind === "structured";
|
|
4293
|
+
card.classList.toggle("active", kind === sessionKind);
|
|
4294
|
+
card.classList.toggle("disabled", disabled);
|
|
3644
4295
|
});
|
|
3645
4296
|
}
|
|
3646
4297
|
|
|
@@ -3657,33 +4308,31 @@
|
|
|
3657
4308
|
|
|
3658
4309
|
function updateSessionSnapshot(snapshot) {
|
|
3659
4310
|
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
|
-
}
|
|
4311
|
+
var currentSession = state.sessions.find(function(session) { return session.id === snapshot.id; }) || null;
|
|
4312
|
+
var normalizedSnapshot = normalizeStructuredSnapshot(snapshot, currentSession);
|
|
3670
4313
|
var updated = false;
|
|
3671
4314
|
var prevSession = null;
|
|
3672
4315
|
state.sessions = state.sessions.map(function(session) {
|
|
3673
|
-
if (session.id !==
|
|
4316
|
+
if (session.id !== normalizedSnapshot.id) return session;
|
|
3674
4317
|
prevSession = session;
|
|
3675
4318
|
updated = true;
|
|
3676
|
-
return Object.assign({}, session,
|
|
4319
|
+
return Object.assign({}, session, normalizedSnapshot);
|
|
3677
4320
|
});
|
|
3678
4321
|
if (!updated) {
|
|
3679
|
-
state.sessions.unshift(
|
|
4322
|
+
state.sessions.unshift(normalizedSnapshot);
|
|
4323
|
+
}
|
|
4324
|
+
var updatedSession = state.sessions.find(function(session) { return session.id === normalizedSnapshot.id; }) || normalizedSnapshot;
|
|
4325
|
+
if (updatedSession && Array.isArray(updatedSession.queuedMessages) && normalizedSnapshot.id === state.selectedId) {
|
|
4326
|
+
syncStructuredQueueFromSession(updatedSession);
|
|
4327
|
+
saveStructuredQueue();
|
|
4328
|
+
updateStructuredQueueCounter();
|
|
3680
4329
|
}
|
|
3681
|
-
if (
|
|
4330
|
+
if (normalizedSnapshot.id === state.selectedId) {
|
|
3682
4331
|
reconcileInteractiveState();
|
|
3683
4332
|
updateTaskDisplay();
|
|
3684
4333
|
}
|
|
3685
4334
|
// When a session transitions to a non-running state, try flushing cross-session queue
|
|
3686
|
-
if (
|
|
4335
|
+
if (normalizedSnapshot.status && normalizedSnapshot.status !== "running" && state.crossSessionQueue.length > 0) {
|
|
3687
4336
|
// Use setTimeout(0) to let the current event processing complete first
|
|
3688
4337
|
setTimeout(flushCrossSessionQueue, 0);
|
|
3689
4338
|
}
|
|
@@ -3703,6 +4352,7 @@
|
|
|
3703
4352
|
var keepLocalOutput = localOutput.length > serverOutput.length;
|
|
3704
4353
|
var localStructuredState = localSession.structuredState || null;
|
|
3705
4354
|
var serverStructuredState = serverSession.structuredState || null;
|
|
4355
|
+
var structuredSession = (localSession.sessionKind === "structured") || (serverSession.sessionKind === "structured");
|
|
3706
4356
|
var localHasPendingAssistant = !!(localSession.messages && localSession.messages.length && (function() {
|
|
3707
4357
|
var last = localSession.messages[localSession.messages.length - 1];
|
|
3708
4358
|
return last && last.role === "assistant" && Array.isArray(last.content) && last.content.some(function(block) {
|
|
@@ -3716,6 +4366,16 @@
|
|
|
3716
4366
|
&& localHasPendingAssistant
|
|
3717
4367
|
&& !!localStructuredState.activeRequestId
|
|
3718
4368
|
&& (!serverStructuredState || !serverStructuredState.activeRequestId || serverStructuredState.activeRequestId === localStructuredState.activeRequestId);
|
|
4369
|
+
var localMessages = Array.isArray(localSession.messages)
|
|
4370
|
+
? (structuredSession ? stripRenderOnlyStructuredMessages(localSession.messages) : localSession.messages)
|
|
4371
|
+
: [];
|
|
4372
|
+
var serverMessages = Array.isArray(serverSession.messages)
|
|
4373
|
+
? (structuredSession ? stripRenderOnlyStructuredMessages(serverSession.messages) : serverSession.messages)
|
|
4374
|
+
: [];
|
|
4375
|
+
var preserveLocalMessages = localMessages.length > serverMessages.length
|
|
4376
|
+
|| (localMessages.length > 0 && serverMessages.length > 0
|
|
4377
|
+
&& JSON.stringify(localMessages[localMessages.length - 1]) !== JSON.stringify(serverMessages[serverMessages.length - 1])
|
|
4378
|
+
&& JSON.stringify(localMessages).length > JSON.stringify(serverMessages).length);
|
|
3719
4379
|
|
|
3720
4380
|
if (keepLocalOutput) {
|
|
3721
4381
|
merged.output = localOutput;
|
|
@@ -3727,6 +4387,10 @@
|
|
|
3727
4387
|
merged.messages = localSession.messages;
|
|
3728
4388
|
}
|
|
3729
4389
|
|
|
4390
|
+
if (preserveLocalMessages) {
|
|
4391
|
+
merged.messages = localMessages;
|
|
4392
|
+
}
|
|
4393
|
+
|
|
3730
4394
|
if (localSession.id === state.selectedId) {
|
|
3731
4395
|
if (localSession.permissionBlocked && serverSession.permissionBlocked === false) {
|
|
3732
4396
|
} else if (localSession.permissionBlocked && !serverSession.permissionBlocked) {
|
|
@@ -3749,6 +4413,9 @@
|
|
|
3749
4413
|
if (session && session.messages && session.messages.length > 0) {
|
|
3750
4414
|
return session.messages;
|
|
3751
4415
|
}
|
|
4416
|
+
if (session && session.sessionKind === "structured") {
|
|
4417
|
+
return [];
|
|
4418
|
+
}
|
|
3752
4419
|
if (!allowFallback) {
|
|
3753
4420
|
return [];
|
|
3754
4421
|
}
|
|
@@ -3774,7 +4441,8 @@
|
|
|
3774
4441
|
return recent ? recent.id : sessions[0].id;
|
|
3775
4442
|
}
|
|
3776
4443
|
|
|
3777
|
-
function loadSessions() {
|
|
4444
|
+
function loadSessions(options) {
|
|
4445
|
+
var opts = options || {};
|
|
3778
4446
|
return fetch("/api/sessions", { credentials: "same-origin" })
|
|
3779
4447
|
.then(function(res) {
|
|
3780
4448
|
if (res.status === 401) {
|
|
@@ -3800,6 +4468,9 @@
|
|
|
3800
4468
|
if (preferredSessionId !== undefined) {
|
|
3801
4469
|
state.selectedId = preferredSessionId;
|
|
3802
4470
|
}
|
|
4471
|
+
restoreStructuredQueue();
|
|
4472
|
+
updateStructuredQueueCounter();
|
|
4473
|
+
state.bootstrapping = false;
|
|
3803
4474
|
persistSelectedId();
|
|
3804
4475
|
if (state.modalOpen) {
|
|
3805
4476
|
updateSessionsList();
|
|
@@ -3817,23 +4488,34 @@
|
|
|
3817
4488
|
}
|
|
3818
4489
|
updateShellChrome();
|
|
3819
4490
|
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
if (state.selectedId) {
|
|
4491
|
+
var reloadPromise = Promise.resolve();
|
|
4492
|
+
if (!opts.skipSelectedOutputReload && state.selectedId) {
|
|
4493
|
+
reloadPromise = loadOutput(state.selectedId);
|
|
4494
|
+
} else if (state.selectedId) {
|
|
3824
4495
|
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
3825
4496
|
if (isStructuredSession(sel)) {
|
|
3826
|
-
|
|
4497
|
+
resetChatRenderCache();
|
|
4498
|
+
scheduleChatRender(true);
|
|
3827
4499
|
}
|
|
3828
4500
|
}
|
|
3829
4501
|
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
4502
|
+
return reloadPromise.then(function() {
|
|
4503
|
+
if (state.crossSessionQueue.length > 0) {
|
|
4504
|
+
flushCrossSessionQueue();
|
|
4505
|
+
}
|
|
4506
|
+
renderCrossSessionQueue();
|
|
4507
|
+
});
|
|
3834
4508
|
})
|
|
3835
4509
|
.catch(function(e) {
|
|
3836
|
-
|
|
4510
|
+
var message = (e && e.message) || "";
|
|
4511
|
+
var isTransientAbort =
|
|
4512
|
+
message === "Failed to fetch" ||
|
|
4513
|
+
message === "NetworkError when attempting to fetch resource." ||
|
|
4514
|
+
message === "Load failed" ||
|
|
4515
|
+
/aborted|aborterror|networkerror|failed to fetch/i.test(message);
|
|
4516
|
+
if (!isTransientAbort) {
|
|
4517
|
+
console.error("[wand] loadSessions failed:", e);
|
|
4518
|
+
}
|
|
3837
4519
|
});
|
|
3838
4520
|
}
|
|
3839
4521
|
|
|
@@ -3896,7 +4578,7 @@
|
|
|
3896
4578
|
}
|
|
3897
4579
|
|
|
3898
4580
|
if (selectedSession && state.terminal) {
|
|
3899
|
-
syncTerminalBuffer(selectedSession.id, selectedSession.output || "", { mode: "
|
|
4581
|
+
syncTerminalBuffer(selectedSession.id, selectedSession.output || "", { mode: "append", scroll: false });
|
|
3900
4582
|
} else if (!selectedSession) {
|
|
3901
4583
|
state.terminalSessionId = null;
|
|
3902
4584
|
state.terminalOutput = "";
|
|
@@ -3933,7 +4615,7 @@
|
|
|
3933
4615
|
}
|
|
3934
4616
|
var sess = state.sessions.find(function(s) { return s.id === id; });
|
|
3935
4617
|
var url = "/api/sessions/" + id;
|
|
3936
|
-
if (
|
|
4618
|
+
if (shouldRequestChatFormat(sess)) {
|
|
3937
4619
|
url += "?format=chat";
|
|
3938
4620
|
}
|
|
3939
4621
|
return fetch(url, { credentials: "same-origin" })
|
|
@@ -3952,14 +4634,7 @@
|
|
|
3952
4634
|
updateShellChrome();
|
|
3953
4635
|
|
|
3954
4636
|
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
|
-
}
|
|
4637
|
+
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, data.output, false));
|
|
3963
4638
|
|
|
3964
4639
|
renderChat(false);
|
|
3965
4640
|
});
|
|
@@ -3967,9 +4642,7 @@
|
|
|
3967
4642
|
|
|
3968
4643
|
function selectSession(id) {
|
|
3969
4644
|
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
4645
|
if (!foundSession) {
|
|
3972
|
-
console.warn("[WAND] selectSession: session not found, skipping", id);
|
|
3973
4646
|
return;
|
|
3974
4647
|
}
|
|
3975
4648
|
state.selectedId = id;
|
|
@@ -3977,7 +4650,8 @@
|
|
|
3977
4650
|
// Clear queued inputs from the previous session to prevent cross-session leaks
|
|
3978
4651
|
state.messageQueue = [];
|
|
3979
4652
|
state.pendingMessages = [];
|
|
3980
|
-
|
|
4653
|
+
syncStructuredQueueFromSession(foundSession);
|
|
4654
|
+
restoreStructuredQueue();
|
|
3981
4655
|
updateStructuredQueueCounter();
|
|
3982
4656
|
resetChatRenderCache();
|
|
3983
4657
|
state.currentMessages = [];
|
|
@@ -4054,7 +4728,9 @@
|
|
|
4054
4728
|
modal.classList.remove("hidden");
|
|
4055
4729
|
lastFocusedElement = document.activeElement;
|
|
4056
4730
|
state.sessionTool = getPreferredTool();
|
|
4057
|
-
state.
|
|
4731
|
+
state.preferredCommand = state.sessionTool;
|
|
4732
|
+
state.sessionCreateKind = state.sessionTool === "codex" ? "pty" : "structured";
|
|
4733
|
+
state.sessionCreateWorktree = false;
|
|
4058
4734
|
state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
|
|
4059
4735
|
syncSessionModalUI();
|
|
4060
4736
|
loadRecentPathBubbles();
|
|
@@ -4553,14 +5229,14 @@
|
|
|
4553
5229
|
function quickStartSession() {
|
|
4554
5230
|
var command = getPreferredTool();
|
|
4555
5231
|
var defaultCwd = getEffectiveCwd();
|
|
4556
|
-
var defaultMode = (state.config && state.config.defaultMode) ? state.config.defaultMode : "default";
|
|
5232
|
+
var defaultMode = getSafeModeForTool(command, (state.config && state.config.defaultMode) ? state.config.defaultMode : "default");
|
|
4557
5233
|
state.preferredCommand = command;
|
|
4558
5234
|
state.chatMode = getSafeModeForTool(command, state.chatMode);
|
|
4559
5235
|
fetch("/api/commands", {
|
|
4560
5236
|
method: "POST",
|
|
4561
5237
|
headers: { "Content-Type": "application/json" },
|
|
4562
5238
|
credentials: "same-origin",
|
|
4563
|
-
body: JSON.stringify({ command: command, cwd: defaultCwd, mode: defaultMode })
|
|
5239
|
+
body: JSON.stringify({ command: command, provider: command, cwd: defaultCwd, mode: defaultMode })
|
|
4564
5240
|
})
|
|
4565
5241
|
.then(function(res) { return res.json(); })
|
|
4566
5242
|
.then(function(data) {
|
|
@@ -4588,6 +5264,7 @@
|
|
|
4588
5264
|
var errorEl = document.getElementById("modal-error");
|
|
4589
5265
|
var command = getPreferredTool();
|
|
4590
5266
|
var sessionKind = state.sessionCreateKind || "structured";
|
|
5267
|
+
var worktreeEnabled = state.sessionCreateWorktree === true;
|
|
4591
5268
|
|
|
4592
5269
|
hideError(errorEl);
|
|
4593
5270
|
|
|
@@ -4596,22 +5273,22 @@
|
|
|
4596
5273
|
var selectedMode = getSafeModeForTool(command, state.modeValue);
|
|
4597
5274
|
|
|
4598
5275
|
if (sessionKind === "structured") {
|
|
4599
|
-
startStructuredSessionFromModal(cwd, selectedMode, errorEl);
|
|
5276
|
+
startStructuredSessionFromModal(cwd, selectedMode, worktreeEnabled, errorEl);
|
|
4600
5277
|
return;
|
|
4601
5278
|
}
|
|
4602
5279
|
|
|
4603
|
-
runPtyCommandFromModal(command, cwd, selectedMode, errorEl);
|
|
5280
|
+
runPtyCommandFromModal(command, cwd, selectedMode, worktreeEnabled, errorEl);
|
|
4604
5281
|
}
|
|
4605
5282
|
|
|
4606
|
-
function startStructuredSessionFromModal(cwd, mode, errorEl) {
|
|
4607
|
-
console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode);
|
|
5283
|
+
function startStructuredSessionFromModal(cwd, mode, worktreeEnabled, errorEl) {
|
|
5284
|
+
console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode, "worktreeEnabled:", worktreeEnabled);
|
|
4608
5285
|
_sessionCreating = true;
|
|
4609
5286
|
state.modeValue = mode;
|
|
4610
5287
|
state.chatMode = mode;
|
|
4611
5288
|
state.sessionTool = "claude";
|
|
4612
5289
|
state.preferredCommand = "claude";
|
|
4613
5290
|
syncComposerModeSelect();
|
|
4614
|
-
return createStructuredSession(undefined, cwd, mode)
|
|
5291
|
+
return createStructuredSession(undefined, cwd, mode, worktreeEnabled)
|
|
4615
5292
|
.then(function(data) {
|
|
4616
5293
|
saveWorkingDir(cwd);
|
|
4617
5294
|
closeSessionModal();
|
|
@@ -4625,8 +5302,8 @@
|
|
|
4625
5302
|
.finally(function() { _sessionCreating = false; });
|
|
4626
5303
|
}
|
|
4627
5304
|
|
|
4628
|
-
function runPtyCommandFromModal(command, cwd, mode, errorEl) {
|
|
4629
|
-
console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode);
|
|
5305
|
+
function runPtyCommandFromModal(command, cwd, mode, worktreeEnabled, errorEl) {
|
|
5306
|
+
console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode, "worktreeEnabled:", worktreeEnabled);
|
|
4630
5307
|
_sessionCreating = true;
|
|
4631
5308
|
state.modeValue = mode;
|
|
4632
5309
|
state.chatMode = mode;
|
|
@@ -4640,8 +5317,10 @@
|
|
|
4640
5317
|
credentials: "same-origin",
|
|
4641
5318
|
body: JSON.stringify({
|
|
4642
5319
|
command: command,
|
|
5320
|
+
provider: command,
|
|
4643
5321
|
cwd: cwd,
|
|
4644
|
-
mode: mode
|
|
5322
|
+
mode: mode,
|
|
5323
|
+
worktreeEnabled: worktreeEnabled
|
|
4645
5324
|
})
|
|
4646
5325
|
})
|
|
4647
5326
|
.then(function(res) { return res.json(); })
|
|
@@ -4669,7 +5348,9 @@
|
|
|
4669
5348
|
}
|
|
4670
5349
|
})
|
|
4671
5350
|
.catch(function() {
|
|
4672
|
-
showError(errorEl,
|
|
5351
|
+
showError(errorEl, command === "codex"
|
|
5352
|
+
? "无法启动 Codex 会话,请确认 codex 已正确安装并可在终端中执行。"
|
|
5353
|
+
: "无法启动 Claude 会话,请确认 Claude 已正确安装。");
|
|
4673
5354
|
})
|
|
4674
5355
|
.finally(function() { _sessionCreating = false; });
|
|
4675
5356
|
}
|
|
@@ -4957,10 +5638,25 @@
|
|
|
4957
5638
|
}
|
|
4958
5639
|
}
|
|
4959
5640
|
|
|
5641
|
+
function handleInteractiveTextInput(inputBox) {
|
|
5642
|
+
if (!state.terminalInteractive || !inputBox) return false;
|
|
5643
|
+
var value = inputBox.value || "";
|
|
5644
|
+
if (!value) return false;
|
|
5645
|
+
queueDirectInput(value, "interactive_text").catch(function() {});
|
|
5646
|
+
inputBox.value = "";
|
|
5647
|
+
autoResizeInput(inputBox);
|
|
5648
|
+
setDraftValue("", true);
|
|
5649
|
+
return true;
|
|
5650
|
+
}
|
|
5651
|
+
|
|
4960
5652
|
function handleInputPaste(event) {
|
|
4961
5653
|
var pasted = event.clipboardData && event.clipboardData.getData("text");
|
|
4962
5654
|
if (!pasted) return;
|
|
4963
5655
|
event.preventDefault();
|
|
5656
|
+
if (state.terminalInteractive) {
|
|
5657
|
+
queueDirectInput(pasted, "paste").catch(function() {});
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
4964
5660
|
var inputBox = document.getElementById("input-box");
|
|
4965
5661
|
if (inputBox) {
|
|
4966
5662
|
var start = inputBox.selectionStart || 0;
|
|
@@ -5065,6 +5761,7 @@
|
|
|
5065
5761
|
tool: getPreferredTool(),
|
|
5066
5762
|
queuedAt: Date.now()
|
|
5067
5763
|
});
|
|
5764
|
+
persistCrossSessionQueue();
|
|
5068
5765
|
renderCrossSessionQueue();
|
|
5069
5766
|
}
|
|
5070
5767
|
|
|
@@ -5089,6 +5786,7 @@
|
|
|
5089
5786
|
showToast(data.error, "error");
|
|
5090
5787
|
// 失败回填队首,不丢消息
|
|
5091
5788
|
state.crossSessionQueue.unshift(item);
|
|
5789
|
+
persistCrossSessionQueue();
|
|
5092
5790
|
renderCrossSessionQueue();
|
|
5093
5791
|
return null;
|
|
5094
5792
|
}
|
|
@@ -5098,6 +5796,7 @@
|
|
|
5098
5796
|
_queueLaunching = false;
|
|
5099
5797
|
showToast((error && error.message) || "无法启动排队会话。", "error");
|
|
5100
5798
|
state.crossSessionQueue.unshift(item);
|
|
5799
|
+
persistCrossSessionQueue();
|
|
5101
5800
|
renderCrossSessionQueue();
|
|
5102
5801
|
});
|
|
5103
5802
|
}
|
|
@@ -5106,6 +5805,7 @@
|
|
|
5106
5805
|
var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
|
|
5107
5806
|
if (idx < 0) return;
|
|
5108
5807
|
var item = state.crossSessionQueue.splice(idx, 1)[0];
|
|
5808
|
+
persistCrossSessionQueue();
|
|
5109
5809
|
renderCrossSessionQueue();
|
|
5110
5810
|
// 立即发送不受 _queueLaunching 限制
|
|
5111
5811
|
fetch("/api/commands", {
|
|
@@ -5123,12 +5823,18 @@
|
|
|
5123
5823
|
.then(function(data) {
|
|
5124
5824
|
if (data.error) {
|
|
5125
5825
|
showToast(data.error, "error");
|
|
5826
|
+
state.crossSessionQueue.splice(idx, 0, item);
|
|
5827
|
+
persistCrossSessionQueue();
|
|
5828
|
+
renderCrossSessionQueue();
|
|
5126
5829
|
return null;
|
|
5127
5830
|
}
|
|
5128
5831
|
return activateSession(data);
|
|
5129
5832
|
})
|
|
5130
5833
|
.catch(function(error) {
|
|
5131
5834
|
showToast((error && error.message) || "无法启动排队会话。", "error");
|
|
5835
|
+
state.crossSessionQueue.splice(idx, 0, item);
|
|
5836
|
+
persistCrossSessionQueue();
|
|
5837
|
+
renderCrossSessionQueue();
|
|
5132
5838
|
});
|
|
5133
5839
|
}
|
|
5134
5840
|
|
|
@@ -5136,6 +5842,7 @@
|
|
|
5136
5842
|
var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
|
|
5137
5843
|
if (idx < 0) return;
|
|
5138
5844
|
state.crossSessionQueue.splice(idx, 1);
|
|
5845
|
+
persistCrossSessionQueue();
|
|
5139
5846
|
renderCrossSessionQueue();
|
|
5140
5847
|
if (state.crossSessionQueue.length === 0) {
|
|
5141
5848
|
showToast("排队已清空。", "info");
|
|
@@ -5168,6 +5875,7 @@
|
|
|
5168
5875
|
|
|
5169
5876
|
if (state.crossSessionQueue.length === 0) {
|
|
5170
5877
|
if (container) container.remove();
|
|
5878
|
+
persistCrossSessionQueue();
|
|
5171
5879
|
return;
|
|
5172
5880
|
}
|
|
5173
5881
|
|
|
@@ -5239,6 +5947,7 @@
|
|
|
5239
5947
|
if (e.target.closest("#queue-clear-all")) {
|
|
5240
5948
|
e.preventDefault();
|
|
5241
5949
|
state.crossSessionQueue = [];
|
|
5950
|
+
persistCrossSessionQueue();
|
|
5242
5951
|
renderCrossSessionQueue();
|
|
5243
5952
|
showToast("排队已清空。", "info");
|
|
5244
5953
|
return;
|
|
@@ -5286,6 +5995,7 @@
|
|
|
5286
5995
|
credentials: "same-origin",
|
|
5287
5996
|
body: JSON.stringify({
|
|
5288
5997
|
command: preferredTool,
|
|
5998
|
+
provider: preferredTool,
|
|
5289
5999
|
cwd: defaultCwd,
|
|
5290
6000
|
mode: mode,
|
|
5291
6001
|
initialInput: value
|
|
@@ -5314,7 +6024,9 @@
|
|
|
5314
6024
|
});
|
|
5315
6025
|
})
|
|
5316
6026
|
.catch(function(error) {
|
|
5317
|
-
showToast((error && error.message) ||
|
|
6027
|
+
showToast((error && error.message) || (preferredTool === "codex"
|
|
6028
|
+
? "无法启动 Codex 会话。"
|
|
6029
|
+
: "无法启动 Claude 会话。"), "error");
|
|
5318
6030
|
welcomeInput.placeholder = "输入你的问题,按 Enter 发送...";
|
|
5319
6031
|
welcomeInput.disabled = false;
|
|
5320
6032
|
});
|
|
@@ -5353,6 +6065,7 @@
|
|
|
5353
6065
|
credentials: "same-origin",
|
|
5354
6066
|
body: JSON.stringify({
|
|
5355
6067
|
command: preferredTool,
|
|
6068
|
+
provider: preferredTool,
|
|
5356
6069
|
cwd: defaultCwd,
|
|
5357
6070
|
mode: mode,
|
|
5358
6071
|
initialInput: value || undefined
|
|
@@ -5378,7 +6091,9 @@
|
|
|
5378
6091
|
return loadOutput(data.id);
|
|
5379
6092
|
})
|
|
5380
6093
|
.catch(function(error) {
|
|
5381
|
-
showToast((error && error.message) ||
|
|
6094
|
+
showToast((error && error.message) || (preferredTool === "codex"
|
|
6095
|
+
? "无法启动 Codex 会话。"
|
|
6096
|
+
: "无法启动 Claude 会话。"), "error");
|
|
5382
6097
|
});
|
|
5383
6098
|
}
|
|
5384
6099
|
|
|
@@ -5434,7 +6149,7 @@
|
|
|
5434
6149
|
|
|
5435
6150
|
var inputBox = document.getElementById("input-box");
|
|
5436
6151
|
var value = inputBox ? inputBox.value : "";
|
|
5437
|
-
var selectedSession =
|
|
6152
|
+
var selectedSession = getSelectedSession();
|
|
5438
6153
|
if (value) {
|
|
5439
6154
|
console.log("[WAND] sendInputFromBox", {
|
|
5440
6155
|
sessionId: state.selectedId,
|
|
@@ -5455,16 +6170,12 @@
|
|
|
5455
6170
|
return postStructuredInput(value, inputBox, selectedSession);
|
|
5456
6171
|
}
|
|
5457
6172
|
|
|
5458
|
-
|
|
5459
|
-
var combinedInput = value + getControlInput("enter");
|
|
6173
|
+
var submitChunks = getTerminalSubmitChunks(selectedSession, value);
|
|
5460
6174
|
var isOffline = !state.wsConnected;
|
|
5461
6175
|
|
|
5462
6176
|
if (isOffline) {
|
|
5463
6177
|
// Offline: queue for flush on reconnect, clear input immediately
|
|
5464
|
-
|
|
5465
|
-
state.pendingMessages.shift();
|
|
5466
|
-
}
|
|
5467
|
-
state.pendingMessages.push(combinedInput);
|
|
6178
|
+
queueOfflineTerminalChunks(submitChunks);
|
|
5468
6179
|
if (inputBox) {
|
|
5469
6180
|
inputBox.value = "";
|
|
5470
6181
|
autoResizeInput(inputBox);
|
|
@@ -5479,7 +6190,11 @@
|
|
|
5479
6190
|
showToast("会话未就绪,将稍后重试。", "info");
|
|
5480
6191
|
return null;
|
|
5481
6192
|
}
|
|
5482
|
-
|
|
6193
|
+
var submitView = state.currentView;
|
|
6194
|
+
if (readySession && readySession.provider === "codex" && state.selectedId !== readySession.id) {
|
|
6195
|
+
throw new Error("Codex session changed before input send.");
|
|
6196
|
+
}
|
|
6197
|
+
return sendTerminalChunks(submitChunks, "enter_text", 0, submitView).then(function() {
|
|
5483
6198
|
// Clear input only after the send succeeds
|
|
5484
6199
|
if (inputBox && inputBox.value === value) {
|
|
5485
6200
|
inputBox.value = "";
|
|
@@ -5502,58 +6217,32 @@
|
|
|
5502
6217
|
showToast("会话不存在,请重新选择或新建会话。", "error");
|
|
5503
6218
|
return Promise.resolve();
|
|
5504
6219
|
}
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
session.messages = curMsgs;
|
|
6220
|
+
|
|
6221
|
+
var isQueueing = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
6222
|
+
if (!isQueueing) {
|
|
6223
|
+
// Immediately render user message with thinking indicator
|
|
6224
|
+
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
6225
|
+
var userMsgs = stripRenderOnlyStructuredMessages(Array.isArray(session.messages) ? session.messages.slice() : []);
|
|
6226
|
+
userMsgs.push(userTurn);
|
|
6227
|
+
var optimisticStructuredState = Object.assign({}, session.structuredState || {}, { inFlight: true });
|
|
6228
|
+
updateSessionSnapshot({
|
|
6229
|
+
id: session.id,
|
|
6230
|
+
status: "running",
|
|
6231
|
+
structuredState: optimisticStructuredState,
|
|
6232
|
+
});
|
|
6233
|
+
state.currentMessages = buildMessagesForRender(Object.assign({}, session, {
|
|
6234
|
+
status: "running",
|
|
6235
|
+
structuredState: optimisticStructuredState,
|
|
6236
|
+
}), userMsgs);
|
|
6237
|
+
updateInputHint("思考中…");
|
|
5524
6238
|
renderChat(true);
|
|
5525
|
-
showToast("已排队(第 " + state.structuredInputQueue.length + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
5526
|
-
updateStructuredQueueCounter();
|
|
5527
|
-
return Promise.resolve();
|
|
5528
6239
|
}
|
|
5529
6240
|
|
|
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
6241
|
if (inputBox) {
|
|
5550
6242
|
inputBox.value = "";
|
|
5551
6243
|
autoResizeInput(inputBox);
|
|
5552
6244
|
}
|
|
5553
|
-
// Keep send button enabled so user can queue more messages
|
|
5554
|
-
updateInputHint("思考中…");
|
|
5555
6245
|
setDraftValue("");
|
|
5556
|
-
renderChat(true);
|
|
5557
6246
|
|
|
5558
6247
|
return fetch("/api/structured-sessions/" + state.selectedId + "/messages", {
|
|
5559
6248
|
method: "POST",
|
|
@@ -5568,27 +6257,29 @@
|
|
|
5568
6257
|
}
|
|
5569
6258
|
if (snapshot && snapshot.id) {
|
|
5570
6259
|
updateSessionSnapshot(snapshot);
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
// Re-append queued user messages
|
|
5574
|
-
appendQueuedPlaceholders(state.currentMessages);
|
|
5575
|
-
}
|
|
6260
|
+
var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
6261
|
+
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
5576
6262
|
renderChat(true);
|
|
5577
|
-
|
|
6263
|
+
if (isQueueing) {
|
|
6264
|
+
var queuedCount = getStructuredQueuedInputs(refreshedSession).length;
|
|
6265
|
+
showToast("已排队(第 " + queuedCount + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
6266
|
+
} else {
|
|
6267
|
+
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
6268
|
+
}
|
|
5578
6269
|
}
|
|
5579
6270
|
})
|
|
5580
6271
|
.catch(function(error) {
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
session.structuredState
|
|
5584
|
-
}
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
6272
|
+
updateSessionSnapshot({
|
|
6273
|
+
id: session.id,
|
|
6274
|
+
structuredState: Object.assign({}, session.structuredState || {}, { inFlight: false }),
|
|
6275
|
+
});
|
|
6276
|
+
var message = (error && error.message) || "";
|
|
6277
|
+
var isTransientAbort =
|
|
6278
|
+
message === "Failed to fetch" ||
|
|
6279
|
+
message === "NetworkError when attempting to fetch resource." ||
|
|
6280
|
+
message === "Load failed" ||
|
|
6281
|
+
/aborted|aborterror|networkerror|failed to fetch/i.test(message);
|
|
6282
|
+
if (!isTransientAbort) {
|
|
5592
6283
|
showToast((error && error.message) || "无法发送结构化消息。", "error");
|
|
5593
6284
|
}
|
|
5594
6285
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
@@ -5602,7 +6293,7 @@
|
|
|
5602
6293
|
|
|
5603
6294
|
function updateStructuredQueueCounter() {
|
|
5604
6295
|
var counter = document.getElementById("queue-counter");
|
|
5605
|
-
var count =
|
|
6296
|
+
var count = getSelectedStructuredQueuedInputs().length;
|
|
5606
6297
|
if (counter) {
|
|
5607
6298
|
counter.textContent = "队列: " + count;
|
|
5608
6299
|
if (count > 0) {
|
|
@@ -5615,56 +6306,48 @@
|
|
|
5615
6306
|
|
|
5616
6307
|
// Append queued user message placeholders to currentMessages so they
|
|
5617
6308
|
// remain visible across WS updates and re-renders.
|
|
5618
|
-
function
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
6309
|
+
function buildMessagesForRender(session, messages) {
|
|
6310
|
+
var sanitized = Array.isArray(messages) ? stripRenderOnlyStructuredMessages(messages) : [];
|
|
6311
|
+
var base = Array.isArray(sanitized) ? sanitized.slice() : [];
|
|
6312
|
+
if (!session || session.sessionKind !== "structured") {
|
|
6313
|
+
return base;
|
|
5622
6314
|
}
|
|
5623
|
-
|
|
6315
|
+
var queued = getStructuredQueuedInputs(session);
|
|
6316
|
+
if (queued && queued.length > 0) {
|
|
6317
|
+
for (var qi = 0; qi < queued.length; qi++) {
|
|
6318
|
+
base.push({ role: "user", content: [{ type: "text", text: queued[qi], __queued: true }] });
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
if (session.structuredState && session.structuredState.inFlight) {
|
|
6322
|
+
var last = base[base.length - 1];
|
|
6323
|
+
if (!last || last.role !== "assistant") {
|
|
6324
|
+
base.push({ role: "assistant", content: [{ type: "text", text: "", __processing: true }] });
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
return base;
|
|
5624
6328
|
}
|
|
5625
6329
|
|
|
6330
|
+
|
|
5626
6331
|
function flushStructuredInputQueue() {
|
|
5627
|
-
if (state.structuredInputQueue.length === 0) return;
|
|
5628
6332
|
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();
|
|
6333
|
+
syncStructuredQueueFromSession(session);
|
|
5637
6334
|
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
6335
|
}
|
|
5659
6336
|
|
|
5660
6337
|
function getInputErrorMessage(error) {
|
|
6338
|
+
var selectedSession = getSelectedSession();
|
|
6339
|
+
var isCodex = selectedSession && selectedSession.provider === "codex";
|
|
5661
6340
|
if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
|
|
5662
|
-
return
|
|
6341
|
+
return isCodex
|
|
6342
|
+
? "Codex 会话已结束,请新建会话后继续。"
|
|
6343
|
+
: "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
|
|
5663
6344
|
}
|
|
5664
6345
|
if (error && error.errorCode === "SESSION_NOT_FOUND") {
|
|
5665
6346
|
return "会话不存在,请重新选择或新建会话。";
|
|
5666
6347
|
}
|
|
5667
|
-
return (error && error.message) ||
|
|
6348
|
+
return (error && error.message) || (isCodex
|
|
6349
|
+
? "Codex 会话暂不可用,请检查终端视图或新建会话。"
|
|
6350
|
+
: "会话暂不可用;若存在 Claude 历史会话,将自动尝试恢复。");
|
|
5668
6351
|
}
|
|
5669
6352
|
|
|
5670
6353
|
function buildInputError(payload) {
|
|
@@ -5704,7 +6387,7 @@
|
|
|
5704
6387
|
}
|
|
5705
6388
|
|
|
5706
6389
|
function canAutoResumeSession(session) {
|
|
5707
|
-
return !!(session && session.status === "exited" && session.claudeSessionId && hasRealConversationHistory(session));
|
|
6390
|
+
return !!(session && session.provider === "claude" && session.status === "exited" && session.claudeSessionId && hasRealConversationHistory(session));
|
|
5708
6391
|
}
|
|
5709
6392
|
|
|
5710
6393
|
function ensureSessionReadyForInput(session, errorEl) {
|
|
@@ -5735,11 +6418,48 @@
|
|
|
5735
6418
|
});
|
|
5736
6419
|
}
|
|
5737
6420
|
|
|
5738
|
-
function
|
|
6421
|
+
function getTerminalSubmitChunks(session, text) {
|
|
6422
|
+
if (session && session.provider === "codex") {
|
|
6423
|
+
return [text, String.fromCharCode(13)];
|
|
6424
|
+
}
|
|
6425
|
+
return [text + String.fromCharCode(13)];
|
|
6426
|
+
}
|
|
6427
|
+
|
|
6428
|
+
function sendTerminalChunks(chunks, shortcutKey, delayMs, viewOverride) {
|
|
6429
|
+
var sequence = Array.isArray(chunks) ? chunks.filter(function(chunk) { return !!chunk; }) : [];
|
|
6430
|
+
if (sequence.length === 0) {
|
|
6431
|
+
return Promise.resolve();
|
|
6432
|
+
}
|
|
6433
|
+
var delay = typeof delayMs === "number" ? delayMs : 0;
|
|
6434
|
+
return sequence.reduce(function(promise, chunk, index) {
|
|
6435
|
+
return promise.then(function() {
|
|
6436
|
+
if (index > 0 && delay > 0) {
|
|
6437
|
+
return new Promise(function(resolve) {
|
|
6438
|
+
setTimeout(resolve, delay);
|
|
6439
|
+
}).then(function() {
|
|
6440
|
+
return queueDirectInput(chunk, index === sequence.length - 1 ? shortcutKey : undefined, viewOverride);
|
|
6441
|
+
});
|
|
6442
|
+
}
|
|
6443
|
+
return queueDirectInput(chunk, index === sequence.length - 1 ? shortcutKey : undefined, viewOverride);
|
|
6444
|
+
});
|
|
6445
|
+
}, Promise.resolve());
|
|
6446
|
+
}
|
|
6447
|
+
|
|
6448
|
+
function queueOfflineTerminalChunks(chunks) {
|
|
6449
|
+
var sequence = Array.isArray(chunks) ? chunks.filter(function(chunk) { return !!chunk; }) : [];
|
|
6450
|
+
sequence.forEach(function(chunk) {
|
|
6451
|
+
if (state.pendingMessages.length >= 100) {
|
|
6452
|
+
state.pendingMessages.shift();
|
|
6453
|
+
}
|
|
6454
|
+
state.pendingMessages.push(chunk);
|
|
6455
|
+
});
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6458
|
+
function queueDirectInput(input, shortcutKey, viewOverride) {
|
|
5739
6459
|
if (!input || !state.selectedId) return Promise.resolve();
|
|
5740
6460
|
state.messageQueue.push(input);
|
|
5741
6461
|
state.inputQueue = state.inputQueue.then(function() {
|
|
5742
|
-
return postInput(input, shortcutKey).finally(function() {
|
|
6462
|
+
return postInput(input, shortcutKey, viewOverride).finally(function() {
|
|
5743
6463
|
var idx = state.messageQueue.indexOf(input);
|
|
5744
6464
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
5745
6465
|
scheduleMobileDomUpdate();
|
|
@@ -5748,8 +6468,9 @@
|
|
|
5748
6468
|
return state.inputQueue;
|
|
5749
6469
|
}
|
|
5750
6470
|
|
|
5751
|
-
function postInput(input, shortcutKey) {
|
|
6471
|
+
function postInput(input, shortcutKey, viewOverride) {
|
|
5752
6472
|
if (!state.selectedId) return Promise.resolve();
|
|
6473
|
+
var effectiveView = viewOverride || state.currentView;
|
|
5753
6474
|
|
|
5754
6475
|
// Pre-check: don't send if session is not running
|
|
5755
6476
|
if (!isSelectedSessionRunning()) {
|
|
@@ -5788,7 +6509,7 @@
|
|
|
5788
6509
|
console.log("[wand] postInput: sending", {
|
|
5789
6510
|
sessionId: state.selectedId,
|
|
5790
6511
|
inputLength: input.length,
|
|
5791
|
-
view:
|
|
6512
|
+
view: effectiveView,
|
|
5792
6513
|
wsConnected: state.wsConnected
|
|
5793
6514
|
});
|
|
5794
6515
|
|
|
@@ -5796,7 +6517,7 @@
|
|
|
5796
6517
|
method: "POST",
|
|
5797
6518
|
headers: { "Content-Type": "application/json" },
|
|
5798
6519
|
credentials: "same-origin",
|
|
5799
|
-
body: JSON.stringify({ input: input, view:
|
|
6520
|
+
body: JSON.stringify({ input: input, view: effectiveView, shortcutKey: shortcutKey || undefined })
|
|
5800
6521
|
})
|
|
5801
6522
|
.then(function(res) {
|
|
5802
6523
|
if (!res.ok) {
|
|
@@ -5834,6 +6555,14 @@
|
|
|
5834
6555
|
return queueDirectInput(input);
|
|
5835
6556
|
}
|
|
5836
6557
|
|
|
6558
|
+
function getSelectedSession() {
|
|
6559
|
+
return state.sessions.find(function(session) { return session.id === state.selectedId; }) || null;
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
function getTerminalSubmitSequence(session) {
|
|
6563
|
+
return session && session.provider === "codex" ? "\n" : String.fromCharCode(13);
|
|
6564
|
+
}
|
|
6565
|
+
|
|
5837
6566
|
function isTerminalInteractionAvailable() {
|
|
5838
6567
|
return !!state.selectedId && state.currentView === "terminal";
|
|
5839
6568
|
}
|
|
@@ -6004,6 +6733,9 @@
|
|
|
6004
6733
|
function updateInteractiveControls() {
|
|
6005
6734
|
var selectedSession = state.sessions.find(function(session) { return session.id === state.selectedId; });
|
|
6006
6735
|
var structured = isStructuredSession(selectedSession);
|
|
6736
|
+
var isCodex = selectedSession && selectedSession.provider === "codex";
|
|
6737
|
+
var isRunning = !!selectedSession && selectedSession.status === "running";
|
|
6738
|
+
var composer = document.getElementById("input-box");
|
|
6007
6739
|
// Update both toggle buttons (topbar and terminal-header)
|
|
6008
6740
|
var toggles = ["terminal-interactive-toggle-top"];
|
|
6009
6741
|
toggles.forEach(function(id) {
|
|
@@ -6019,7 +6751,27 @@
|
|
|
6019
6751
|
var expandedRow = document.querySelector(".inline-shortcuts-expanded-row");
|
|
6020
6752
|
if (expandedRow) expandedRow.classList.toggle("hidden", structured || state.currentView !== "terminal");
|
|
6021
6753
|
var inputHint = document.querySelector(".input-hint");
|
|
6022
|
-
if (inputHint)
|
|
6754
|
+
if (inputHint) {
|
|
6755
|
+
inputHint.classList.toggle("hidden", structured ? true : state.currentView === "terminal");
|
|
6756
|
+
if (!structured && selectedSession) {
|
|
6757
|
+
inputHint.textContent = isCodex
|
|
6758
|
+
? "Enter 发送 · chat 为解析视图,terminal 为原始输出"
|
|
6759
|
+
: "Enter 发送 · Shift+Enter 换行";
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6762
|
+
var disableStructuredInput = !!selectedSession && structured && isCodex && !isRunning;
|
|
6763
|
+
if (composer) {
|
|
6764
|
+
composer.placeholder = getComposerPlaceholder(selectedSession, state.terminalInteractive);
|
|
6765
|
+
composer.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning);
|
|
6766
|
+
composer.setAttribute("aria-disabled", composer.disabled ? "true" : "false");
|
|
6767
|
+
}
|
|
6768
|
+
var sendBtn = document.getElementById("send-input-button");
|
|
6769
|
+
if (sendBtn) {
|
|
6770
|
+
sendBtn.disabled = structured ? disableStructuredInput : (!!selectedSession && !isRunning);
|
|
6771
|
+
sendBtn.setAttribute("title", isCodex
|
|
6772
|
+
? (isRunning ? "发送给 Codex" : "Codex 会话已结束")
|
|
6773
|
+
: (structured ? "发送" : (!selectedSession || isRunning ? "发送" : "会话已结束")));
|
|
6774
|
+
}
|
|
6023
6775
|
var container = document.getElementById("output");
|
|
6024
6776
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
6025
6777
|
}
|
|
@@ -7345,6 +8097,10 @@
|
|
|
7345
8097
|
}
|
|
7346
8098
|
|
|
7347
8099
|
function focusInputFromTap() {
|
|
8100
|
+
if (state.terminalInteractive) {
|
|
8101
|
+
focusTerminalContainer();
|
|
8102
|
+
return;
|
|
8103
|
+
}
|
|
7348
8104
|
var inputBox = document.getElementById('input-box');
|
|
7349
8105
|
if (!inputBox || !state.selectedId || document.activeElement === inputBox) return;
|
|
7350
8106
|
focusInputWithSelection(inputBox);
|
|
@@ -7355,6 +8111,10 @@
|
|
|
7355
8111
|
if (!output) return;
|
|
7356
8112
|
output.setAttribute("tabindex", "0");
|
|
7357
8113
|
output.focus();
|
|
8114
|
+
var terminalTextarea = output.querySelector(".xterm-helper-textarea");
|
|
8115
|
+
if (terminalTextarea && typeof terminalTextarea.focus === "function") {
|
|
8116
|
+
terminalTextarea.focus();
|
|
8117
|
+
}
|
|
7358
8118
|
}
|
|
7359
8119
|
|
|
7360
8120
|
// Mobile keyboard handling
|
|
@@ -7450,7 +8210,7 @@
|
|
|
7450
8210
|
|
|
7451
8211
|
function initTerminalResizeHandle() {
|
|
7452
8212
|
// 终端容器拖动调整大小功能
|
|
7453
|
-
var container = document.getElementById("
|
|
8213
|
+
var container = document.getElementById("output");
|
|
7454
8214
|
if (!container) return;
|
|
7455
8215
|
|
|
7456
8216
|
// 创建拖动手柄
|
|
@@ -7747,27 +8507,19 @@
|
|
|
7747
8507
|
if (msg.data.messages) {
|
|
7748
8508
|
snapshot.messages = msg.data.messages;
|
|
7749
8509
|
}
|
|
8510
|
+
if (msg.data.queuedMessages) {
|
|
8511
|
+
snapshot.queuedMessages = msg.data.queuedMessages;
|
|
8512
|
+
}
|
|
8513
|
+
if (msg.data.structuredState) {
|
|
8514
|
+
snapshot.structuredState = msg.data.structuredState;
|
|
8515
|
+
}
|
|
7750
8516
|
updateSessionSnapshot(snapshot);
|
|
7751
8517
|
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
|
-
}
|
|
8518
|
+
var updatedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; }) || snapshot;
|
|
8519
|
+
state.currentMessages = buildMessagesForRender(updatedSession, getPreferredMessages(updatedSession, msg.data.output, false));
|
|
7768
8520
|
updateTaskDisplay();
|
|
7769
8521
|
// Structured sessions: render immediately for responsiveness
|
|
7770
|
-
if (msg.data.sessionKind === 'structured') {
|
|
8522
|
+
if (updatedSession.sessionKind === 'structured' || msg.data.sessionKind === 'structured') {
|
|
7771
8523
|
renderChat();
|
|
7772
8524
|
} else {
|
|
7773
8525
|
scheduleChatRender();
|
|
@@ -7780,10 +8532,13 @@
|
|
|
7780
8532
|
if (msg.data.chunk && (!state.terminalSessionId || state.terminalSessionId === msg.sessionId)) {
|
|
7781
8533
|
// Fast path: write chunk directly to avoid full-output comparison
|
|
7782
8534
|
// which can trigger terminal.reset() and cause screen flicker.
|
|
8535
|
+
state.terminalLiveStreamSessions[msg.sessionId] = true;
|
|
7783
8536
|
state.terminal.write(msg.data.chunk);
|
|
7784
8537
|
state.terminalSessionId = msg.sessionId;
|
|
7785
8538
|
if (msg.data.output) {
|
|
7786
8539
|
state.terminalOutput = normalizeTerminalOutput(msg.data.output);
|
|
8540
|
+
} else {
|
|
8541
|
+
state.terminalOutput = normalizeTerminalOutput((state.terminalOutput || "") + msg.data.chunk);
|
|
7787
8542
|
}
|
|
7788
8543
|
maybeScrollTerminalToBottom("output");
|
|
7789
8544
|
updateTerminalJumpToBottomButton();
|
|
@@ -7811,6 +8566,9 @@
|
|
|
7811
8566
|
if (msg.data && msg.data.structuredState) {
|
|
7812
8567
|
endedSnapshot.structuredState = msg.data.structuredState;
|
|
7813
8568
|
}
|
|
8569
|
+
if (msg.data && msg.data.queuedMessages) {
|
|
8570
|
+
endedSnapshot.queuedMessages = msg.data.queuedMessages;
|
|
8571
|
+
}
|
|
7814
8572
|
updateSessionSnapshot(endedSnapshot);
|
|
7815
8573
|
|
|
7816
8574
|
if (msg.sessionId === state.selectedId) {
|
|
@@ -7846,22 +8604,24 @@
|
|
|
7846
8604
|
}
|
|
7847
8605
|
|
|
7848
8606
|
// 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.
|
|
8607
|
+
// For structured sessions, the queue is now managed by the server snapshot.
|
|
7852
8608
|
state.messageQueue = [];
|
|
7853
8609
|
state.pendingMessages = [];
|
|
7854
8610
|
|
|
7855
8611
|
var endedSessionObj = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
7856
|
-
var
|
|
8612
|
+
var selectedSessionObj = msg.sessionId === state.selectedId
|
|
8613
|
+
? state.sessions.find(function(s) { return s.id === state.selectedId; })
|
|
8614
|
+
: null;
|
|
8615
|
+
var isStructuredEnded = !!(
|
|
8616
|
+
(endedSessionObj && endedSessionObj.sessionKind === "structured") ||
|
|
8617
|
+
(selectedSessionObj && selectedSessionObj.sessionKind === "structured")
|
|
8618
|
+
);
|
|
7857
8619
|
|
|
7858
|
-
if (isStructuredEnded && msg.sessionId === state.selectedId
|
|
7859
|
-
|
|
7860
|
-
// Structured session turn completed — flush next queued message
|
|
7861
|
-
setTimeout(flushStructuredInputQueue, 50);
|
|
8620
|
+
if (isStructuredEnded && msg.sessionId === state.selectedId) {
|
|
8621
|
+
flushStructuredInputQueue();
|
|
7862
8622
|
} else if (!isStructuredEnded) {
|
|
7863
|
-
// PTY session ended — clear structured queue too
|
|
7864
8623
|
state.structuredInputQueue = [];
|
|
8624
|
+
clearStructuredQueuePersistence(state.selectedId);
|
|
7865
8625
|
updateStructuredQueueCounter();
|
|
7866
8626
|
}
|
|
7867
8627
|
|
|
@@ -7896,7 +8656,13 @@
|
|
|
7896
8656
|
// Initial state for subscribed session (after reconnect or subscription)
|
|
7897
8657
|
if (msg.sessionId === state.selectedId && msg.data) {
|
|
7898
8658
|
if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
|
|
7899
|
-
|
|
8659
|
+
updateSessionSnapshot(msg.data);
|
|
8660
|
+
var initSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
8661
|
+
state.currentMessages = buildMessagesForRender(initSession || msg.data, getPreferredMessages(initSession || msg.data, msg.data.output, false));
|
|
8662
|
+
renderChat(true);
|
|
8663
|
+
updateTaskDisplay();
|
|
8664
|
+
updateApprovalStats();
|
|
8665
|
+
updateTerminalOutput(msg.data.output || "", msg.sessionId, "append");
|
|
7900
8666
|
// Ensure terminal is properly fitted after receiving initial data
|
|
7901
8667
|
scheduleTerminalResize(true);
|
|
7902
8668
|
}
|
|
@@ -7913,7 +8679,6 @@
|
|
|
7913
8679
|
break;
|
|
7914
8680
|
case 'status':
|
|
7915
8681
|
if (msg.sessionId && msg.data) {
|
|
7916
|
-
console.log('[WAND] ws status', msg.sessionId, JSON.stringify(msg.data));
|
|
7917
8682
|
var statusUpdate = { id: msg.sessionId };
|
|
7918
8683
|
if (Object.prototype.hasOwnProperty.call(msg.data, 'status')) {
|
|
7919
8684
|
statusUpdate.status = msg.data.status;
|
|
@@ -8026,8 +8791,14 @@
|
|
|
8026
8791
|
var permissionLabel = document.getElementById("permission-actions-label");
|
|
8027
8792
|
if (!taskEl) return;
|
|
8028
8793
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8794
|
+
if (selectedSession && selectedSession.provider === "codex") {
|
|
8795
|
+
if (permissionActionsEl) permissionActionsEl.classList.add("hidden");
|
|
8796
|
+
taskEl.classList.remove("permission-blocked");
|
|
8797
|
+
}
|
|
8029
8798
|
var pendingEscalation = selectedSession && selectedSession.pendingEscalation ? selectedSession.pendingEscalation : null;
|
|
8030
|
-
var isBlocked =
|
|
8799
|
+
var isBlocked = selectedSession && selectedSession.provider !== "codex"
|
|
8800
|
+
? (pendingEscalation || selectedSession.permissionBlocked)
|
|
8801
|
+
: false;
|
|
8031
8802
|
|
|
8032
8803
|
if (isBlocked) {
|
|
8033
8804
|
var isAutoApprove = selectedSession && selectedSession.autoApprovePermissions;
|
|
@@ -8161,6 +8932,11 @@
|
|
|
8161
8932
|
|
|
8162
8933
|
function toggleAutoApprove() {
|
|
8163
8934
|
if (!state.selectedId) return;
|
|
8935
|
+
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8936
|
+
if (selectedSession && selectedSession.provider === "codex") {
|
|
8937
|
+
showToast("Codex 会话固定以 full-access PTY 启动,不支持切换自动批准。", "info");
|
|
8938
|
+
return;
|
|
8939
|
+
}
|
|
8164
8940
|
var toggle = document.getElementById("auto-approve-toggle");
|
|
8165
8941
|
if (toggle) toggle.style.opacity = "0.5";
|
|
8166
8942
|
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/toggle-auto-approve", {
|
|
@@ -8190,6 +8966,12 @@
|
|
|
8190
8966
|
var toggle = document.getElementById("auto-approve-toggle");
|
|
8191
8967
|
if (!toggle) return;
|
|
8192
8968
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8969
|
+
if (selectedSession && selectedSession.provider === "codex") {
|
|
8970
|
+
toggle.className = "auto-approve-indicator active";
|
|
8971
|
+
toggle.title = "Codex 固定以 full-access PTY 启动,不支持切换自动批准";
|
|
8972
|
+
toggle.textContent = "🛡 Codex 固定全权限";
|
|
8973
|
+
return;
|
|
8974
|
+
}
|
|
8193
8975
|
var enabled = selectedSession && selectedSession.autoApprovePermissions;
|
|
8194
8976
|
if (enabled) {
|
|
8195
8977
|
toggle.className = "auto-approve-indicator active";
|
|
@@ -8222,6 +9004,10 @@
|
|
|
8222
9004
|
}
|
|
8223
9005
|
applyCurrentView();
|
|
8224
9006
|
reconcileInteractiveState();
|
|
9007
|
+
var selectedSession = getSelectedSession();
|
|
9008
|
+
if (selectedSession) {
|
|
9009
|
+
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, selectedSession.output, true));
|
|
9010
|
+
}
|
|
8225
9011
|
updateTerminalJumpToBottomButton();
|
|
8226
9012
|
if (state.currentView === "terminal") {
|
|
8227
9013
|
state.terminalViewportSize = { width: 0, height: 0 };
|
|
@@ -8260,10 +9046,7 @@
|
|
|
8260
9046
|
// Re-parse messages from the latest session output (fallback for edge cases)
|
|
8261
9047
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8262
9048
|
if (selectedSession) {
|
|
8263
|
-
|
|
8264
|
-
if (selectedSession.sessionKind === "structured") {
|
|
8265
|
-
appendQueuedPlaceholders(state.currentMessages);
|
|
8266
|
-
}
|
|
9049
|
+
state.currentMessages = buildMessagesForRender(selectedSession, getPreferredMessages(selectedSession, selectedSession.output, true));
|
|
8267
9050
|
}
|
|
8268
9051
|
renderChat();
|
|
8269
9052
|
}, 30);
|
|
@@ -8353,6 +9136,26 @@
|
|
|
8353
9136
|
return systemInfo;
|
|
8354
9137
|
}
|
|
8355
9138
|
|
|
9139
|
+
function ensureChatMessagesContainer(chatOutput) {
|
|
9140
|
+
if (!chatOutput) return null;
|
|
9141
|
+
var chatMessages = chatOutput.querySelector(".chat-messages");
|
|
9142
|
+
if (chatMessages) return chatMessages;
|
|
9143
|
+
chatMessages = document.createElement("div");
|
|
9144
|
+
chatMessages.className = "chat-messages";
|
|
9145
|
+
chatOutput.appendChild(chatMessages);
|
|
9146
|
+
return chatMessages;
|
|
9147
|
+
}
|
|
9148
|
+
|
|
9149
|
+
function renderChatEmptyState(chatOutput, html) {
|
|
9150
|
+
var chatMessages = ensureChatMessagesContainer(chatOutput);
|
|
9151
|
+
if (!chatMessages) return null;
|
|
9152
|
+
chatMessages.innerHTML = html;
|
|
9153
|
+
bindChatScrollListener();
|
|
9154
|
+
updateChatFollowToggleButton();
|
|
9155
|
+
updateChatJumpToBottomButton();
|
|
9156
|
+
return chatMessages;
|
|
9157
|
+
}
|
|
9158
|
+
|
|
8356
9159
|
function doRenderChat(forceFullRender) {
|
|
8357
9160
|
var chatOutput = document.getElementById("chat-output");
|
|
8358
9161
|
if (!chatOutput) return;
|
|
@@ -8360,7 +9163,7 @@
|
|
|
8360
9163
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
8361
9164
|
if (!selectedSession) {
|
|
8362
9165
|
if (state.lastRenderedEmpty !== "none") {
|
|
8363
|
-
chatOutput
|
|
9166
|
+
renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>未选择会话</strong><br>点击上方「新对话」开始你的第一次对话。</div>');
|
|
8364
9167
|
state.lastRenderedEmpty = "none";
|
|
8365
9168
|
state.lastRenderedMsgCount = 0;
|
|
8366
9169
|
}
|
|
@@ -8371,7 +9174,7 @@
|
|
|
8371
9174
|
|
|
8372
9175
|
if (messages.length === 0) {
|
|
8373
9176
|
if (state.lastRenderedEmpty !== "empty") {
|
|
8374
|
-
chatOutput
|
|
9177
|
+
renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>对话已开始</strong><br>在下方输入框发送消息,Claude 会自动回复。</div>');
|
|
8375
9178
|
state.lastRenderedEmpty = "empty";
|
|
8376
9179
|
state.lastRenderedMsgCount = 0;
|
|
8377
9180
|
}
|
|
@@ -8424,12 +9227,8 @@
|
|
|
8424
9227
|
state.lastRenderedMsgCount = msgCount;
|
|
8425
9228
|
state.lastRenderedHash = outputHash;
|
|
8426
9229
|
|
|
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
|
-
}
|
|
9230
|
+
var chatMessages = ensureChatMessagesContainer(chatOutput);
|
|
9231
|
+
if (!chatMessages) return;
|
|
8433
9232
|
|
|
8434
9233
|
var existingCount = chatMessages.querySelectorAll(".chat-message").length;
|
|
8435
9234
|
// Full render when: forced, no existing messages, or message count decreased/changed
|
|
@@ -8447,7 +9246,7 @@
|
|
|
8447
9246
|
for (var i = 0; i < reversedMessages.length; i++) {
|
|
8448
9247
|
var msg = reversedMessages[i];
|
|
8449
9248
|
var originalIndex = msgCount - 1 - i; // Original index in messages array
|
|
8450
|
-
|
|
9249
|
+
|
|
8451
9250
|
// Find system info for this message position
|
|
8452
9251
|
var sysInfo = null;
|
|
8453
9252
|
for (var j = 0; j < systemInfo.length; j++) {
|
|
@@ -8456,7 +9255,7 @@
|
|
|
8456
9255
|
break;
|
|
8457
9256
|
}
|
|
8458
9257
|
}
|
|
8459
|
-
|
|
9258
|
+
|
|
8460
9259
|
// Render system info card if exists
|
|
8461
9260
|
if (sysInfo) {
|
|
8462
9261
|
html += '<div class="chat-message system-info">' +
|
|
@@ -8466,21 +9265,30 @@
|
|
|
8466
9265
|
'</div>' +
|
|
8467
9266
|
'</div>';
|
|
8468
9267
|
}
|
|
8469
|
-
|
|
9268
|
+
|
|
8470
9269
|
// Render message
|
|
8471
|
-
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null);
|
|
9270
|
+
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
|
|
8472
9271
|
}
|
|
8473
|
-
|
|
9272
|
+
|
|
8474
9273
|
chatMessages.innerHTML = html;
|
|
8475
9274
|
attachAllCopyHandlers(chatMessages);
|
|
9275
|
+
bindChatScrollListener();
|
|
9276
|
+
applyPersistedExpandState(chatMessages);
|
|
8476
9277
|
// Only expand the single newest tool card (first chat-message = newest due to column-reverse)
|
|
8477
9278
|
var firstMsg = chatMessages.querySelector(".chat-message:not(.system-info)");
|
|
8478
9279
|
if (firstMsg) {
|
|
8479
9280
|
var cards = firstMsg.querySelectorAll(".tool-use-card");
|
|
8480
9281
|
if (cards.length > 0) {
|
|
8481
|
-
cards[0]
|
|
9282
|
+
var firstCard = cards[0];
|
|
9283
|
+
var firstCardKey = getElementExpandKey(firstCard);
|
|
9284
|
+
if (getPersistedExpandState(firstCardKey) === null) {
|
|
9285
|
+
firstCard.classList.remove("collapsed");
|
|
9286
|
+
}
|
|
8482
9287
|
for (var ci = 1; ci < cards.length; ci++) {
|
|
8483
|
-
cards[ci]
|
|
9288
|
+
var cardKey = getElementExpandKey(cards[ci]);
|
|
9289
|
+
if (getPersistedExpandState(cardKey) === null) {
|
|
9290
|
+
cards[ci].classList.add("collapsed");
|
|
9291
|
+
}
|
|
8484
9292
|
}
|
|
8485
9293
|
}
|
|
8486
9294
|
}
|
|
@@ -8495,6 +9303,8 @@
|
|
|
8495
9303
|
function collapseOldToolCards(container, newEls) {
|
|
8496
9304
|
var allCards = container.querySelectorAll(".tool-use-card");
|
|
8497
9305
|
allCards.forEach(function(c) {
|
|
9306
|
+
var cardKey = getElementExpandKey(c);
|
|
9307
|
+
if (getPersistedExpandState(cardKey) !== null) return;
|
|
8498
9308
|
// Keep expanded if this card is inside a newly added message
|
|
8499
9309
|
if (newEls) {
|
|
8500
9310
|
for (var i = 0; i < newEls.length; i++) {
|
|
@@ -8558,7 +9368,7 @@
|
|
|
8558
9368
|
for (var i = 0; i < newMessages.length; i++) {
|
|
8559
9369
|
var div = document.createElement("div");
|
|
8560
9370
|
var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
|
|
8561
|
-
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null);
|
|
9371
|
+
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
|
|
8562
9372
|
var el = div.firstElementChild;
|
|
8563
9373
|
if (el) {
|
|
8564
9374
|
el.classList.add("animate-in");
|
|
@@ -8567,7 +9377,9 @@
|
|
|
8567
9377
|
}
|
|
8568
9378
|
}
|
|
8569
9379
|
chatMessages.insertBefore(fragment, chatMessages.firstChild);
|
|
9380
|
+
bindChatScrollListener();
|
|
8570
9381
|
attachAllCopyHandlers(chatMessages);
|
|
9382
|
+
applyPersistedExpandState(chatMessages);
|
|
8571
9383
|
// Collapse all existing cards; new cards (with animate-in) stay expanded
|
|
8572
9384
|
collapseOldToolCards(chatMessages, insertedEls);
|
|
8573
9385
|
// Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
|
|
@@ -8588,7 +9400,7 @@
|
|
|
8588
9400
|
var currentEl = existingEls[mi];
|
|
8589
9401
|
var tmpWrap = document.createElement("div");
|
|
8590
9402
|
var srOrigIdx = reversedMessages.length - 1 - mi;
|
|
8591
|
-
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null);
|
|
9403
|
+
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
|
|
8592
9404
|
var replacementEl = tmpWrap.firstElementChild;
|
|
8593
9405
|
if (!replacementEl) continue;
|
|
8594
9406
|
if (currentEl.innerHTML !== replacementEl.innerHTML || currentEl.className !== replacementEl.className) {
|
|
@@ -8606,6 +9418,8 @@
|
|
|
8606
9418
|
fullRenderChat();
|
|
8607
9419
|
}
|
|
8608
9420
|
if (replacedAny) {
|
|
9421
|
+
bindChatScrollListener();
|
|
9422
|
+
applyPersistedExpandState(chatMessages);
|
|
8609
9423
|
requestAnimationFrame(function() {
|
|
8610
9424
|
smartScrollToBottom(chatMessages);
|
|
8611
9425
|
});
|
|
@@ -8613,6 +9427,8 @@
|
|
|
8613
9427
|
var allCards = chatMessages.querySelectorAll(".tool-use-card");
|
|
8614
9428
|
var newestCard = null;
|
|
8615
9429
|
allCards.forEach(function(c) {
|
|
9430
|
+
var cardKey = getElementExpandKey(c);
|
|
9431
|
+
if (getPersistedExpandState(cardKey) !== null) return;
|
|
8616
9432
|
if (newestMsgEl && newestMsgEl.contains(c)) {
|
|
8617
9433
|
if (!newestCard) newestCard = c;
|
|
8618
9434
|
else c.classList.add("collapsed");
|
|
@@ -8635,14 +9451,15 @@
|
|
|
8635
9451
|
// Smart scroll: only auto-scroll if user is near bottom
|
|
8636
9452
|
// column-reverse: scrollTop near 0 = visual bottom (newest messages)
|
|
8637
9453
|
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;
|
|
9454
|
+
if (!state.chatAutoFollow) {
|
|
9455
|
+
updateChatJumpToBottomButton();
|
|
9456
|
+
return;
|
|
8645
9457
|
}
|
|
9458
|
+
var chatMsgs = container && container.classList && container.classList.contains("chat-messages")
|
|
9459
|
+
? container
|
|
9460
|
+
: getChatScrollElement();
|
|
9461
|
+
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
9462
|
+
scrollChatToBottom(false);
|
|
8646
9463
|
}
|
|
8647
9464
|
|
|
8648
9465
|
// --- Todo progress bar ---
|
|
@@ -9026,6 +9843,74 @@
|
|
|
9026
9843
|
}, 150);
|
|
9027
9844
|
}
|
|
9028
9845
|
|
|
9846
|
+
function isNoiseLine(line) {
|
|
9847
|
+
if (!line) return false;
|
|
9848
|
+
var trimmed = String(line).trim();
|
|
9849
|
+
if (!trimmed) return false;
|
|
9850
|
+
if (trimmed.indexOf("────") === 0) return true;
|
|
9851
|
+
if (trimmed === "❯" || trimmed === "›") return true;
|
|
9852
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]{2,}$/.test(trimmed)) return true;
|
|
9853
|
+
if (/^[▁▂▃▄▅▆▇█▔▕▏▐]+$/.test(trimmed)) return true;
|
|
9854
|
+
if (trimmed.indexOf("esc to interrupt") !== -1) return true;
|
|
9855
|
+
if (trimmed.indexOf("Claude Code v") !== -1) return true;
|
|
9856
|
+
if (/^Sonnet\b/.test(trimmed)) return true;
|
|
9857
|
+
if (trimmed.indexOf("Failed to install Anthropic") !== -1) return true;
|
|
9858
|
+
if (trimmed.indexOf("Claude Code has switched") !== -1) return true;
|
|
9859
|
+
if (trimmed.indexOf("? for shortcuts") !== -1) return true;
|
|
9860
|
+
if (trimmed.indexOf("Claude is waiting") !== -1) return true;
|
|
9861
|
+
if (trimmed.indexOf("[wand]") !== -1) return true;
|
|
9862
|
+
if (trimmed.indexOf("0;") === 0 || trimmed.indexOf("9;") === 0) return true;
|
|
9863
|
+
if (trimmed.indexOf("ctrl+g") !== -1) return true;
|
|
9864
|
+
if (trimmed.indexOf("/effort") !== -1) return true;
|
|
9865
|
+
if (/^Using .* for .* session/.test(trimmed)) return true;
|
|
9866
|
+
if (trimmed.indexOf("Press ") === 0 && trimmed.indexOf(" for") !== -1) return true;
|
|
9867
|
+
if (trimmed.indexOf("type ") === 0 && trimmed.indexOf(" to ") !== -1) return true;
|
|
9868
|
+
if (trimmed.indexOf("auto mode is unavailable") !== -1) return true;
|
|
9869
|
+
if (/MCP server.*failed/i.test(trimmed)) return true;
|
|
9870
|
+
if (trimmed.indexOf("Germinating") !== -1 || trimmed.indexOf("Doodling") !== -1 || trimmed.indexOf("Brewing") !== -1) return true;
|
|
9871
|
+
if (trimmed.indexOf("Permissions") !== -1 && trimmed.indexOf("mode") !== -1) return true;
|
|
9872
|
+
if (trimmed.indexOf("●") === 0 && trimmed.indexOf("·") !== -1) return true;
|
|
9873
|
+
if (trimmed.indexOf("[>") === 0 || trimmed.indexOf("[<") === 0) return true;
|
|
9874
|
+
if (trimmed.indexOf("Captured Claude session ID") !== -1) return true;
|
|
9875
|
+
if (/^>_\s*OpenAI Codex\b/.test(trimmed)) return true;
|
|
9876
|
+
if (/^OpenAI Codex\b/i.test(trimmed)) return true;
|
|
9877
|
+
if (/^(model|directory):\s+/i.test(trimmed)) return true;
|
|
9878
|
+
if (/^(tip|context):\s+/i.test(trimmed)) return true;
|
|
9879
|
+
if (/^work(tree|space):\s+/i.test(trimmed)) return true;
|
|
9880
|
+
if (/^(approvals?|sandbox|provider|session id):\s+/i.test(trimmed)) return true;
|
|
9881
|
+
if (/^(thinking|working)(\.\.\.|…)?$/i.test(trimmed)) return true;
|
|
9882
|
+
if (/^[•◦·]\s+Working\b/i.test(trimmed)) return true;
|
|
9883
|
+
if (/^[•◦·]\s+(Running|Planning|Applying|Reading|Searching)\b/i.test(trimmed)) return true;
|
|
9884
|
+
if (/^[•◦·]\s+(Inspecting|Reviewing|Summarizing|Editing|Updating|Writing)\b/i.test(trimmed)) return true;
|
|
9885
|
+
if (/^[•◦·]\s+Completed\b/i.test(trimmed)) return true;
|
|
9886
|
+
if (/^(ctrl|enter|tab|shift|esc|alt)\+/i.test(trimmed)) return true;
|
|
9887
|
+
if (/\b(open|close|toggle) (chat|terminal)\b/i.test(trimmed)) return true;
|
|
9888
|
+
if (/\b(approve|deny)\b.*\b(permission|approval)\b/i.test(trimmed)) return true;
|
|
9889
|
+
if (/^(use|press) .* (to|for) .*/i.test(trimmed)) return true;
|
|
9890
|
+
if (/^(?:token|context window|remaining context|conversation):\s+/i.test(trimmed)) return true;
|
|
9891
|
+
if (/^(?:cwd|path):\s+\//i.test(trimmed)) return true;
|
|
9892
|
+
if (/^[<>│┆╎].*[<>│┆╎]$/.test(trimmed) && trimmed.length < 8) return true;
|
|
9893
|
+
return false;
|
|
9894
|
+
}
|
|
9895
|
+
|
|
9896
|
+
function stripAnsi(text) {
|
|
9897
|
+
return String(text || "")
|
|
9898
|
+
.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
|
|
9899
|
+
.replace(/\x1b\[(\d+)C/g, function(_match, count) { return " ".repeat(Number(count) || 1); })
|
|
9900
|
+
.replace(/\x1b\[[0-9;?]*[AB]/g, "\n")
|
|
9901
|
+
.replace(/\x1b\[[0-9;?]*[su]/g, "")
|
|
9902
|
+
.replace(/\x1b\[[0-9;?]*[HfJKr]/g, "\n")
|
|
9903
|
+
.replace(/\x1bM/g, "\n")
|
|
9904
|
+
.replace(/\x1b\[[0-9;?]*[ST]/g, "\n")
|
|
9905
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
9906
|
+
.replace(/\x1b[><=ePX^_]/g, "")
|
|
9907
|
+
.replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
|
|
9908
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
9909
|
+
.replace(/\r\n?/g, "\n")
|
|
9910
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
9911
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
9912
|
+
}
|
|
9913
|
+
|
|
9029
9914
|
function parseMessages(output, command) {
|
|
9030
9915
|
var messages = [];
|
|
9031
9916
|
if (!output) return messages;
|
|
@@ -9035,6 +9920,269 @@
|
|
|
9035
9920
|
var carriageReturn = String.fromCharCode(13);
|
|
9036
9921
|
var esc = String.fromCharCode(27);
|
|
9037
9922
|
|
|
9923
|
+
if (/^codex\b/.test(String(command || "").trim())) {
|
|
9924
|
+
var codexFooterRe = /\bgpt-\d+(?:\.\d+)?(?:\s+[a-z0-9.-]+)?\s+·\s+\d+%\s+left\s+·\s+(?:\/|~\/).+/i;
|
|
9925
|
+
var codexActivityRe = /^(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i;
|
|
9926
|
+
|
|
9927
|
+
function stripCodexSegment(raw) {
|
|
9928
|
+
return String(raw || "")
|
|
9929
|
+
.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
|
|
9930
|
+
.replace(/\x1b\[(\d+)C/g, function(_match, count) { return " ".repeat(Number(count) || 1); })
|
|
9931
|
+
.replace(/\x1b\[[0-9;?]*[AB]/g, newline)
|
|
9932
|
+
.replace(/\x1b\[[0-9;?]*[su]/g, "")
|
|
9933
|
+
.replace(/\x1b\[[0-9;?]*[HfJKr]/g, newline)
|
|
9934
|
+
.replace(/\x1bM/g, newline)
|
|
9935
|
+
.replace(/\x1b\[[0-9;?]*[ST]/g, newline)
|
|
9936
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
9937
|
+
.replace(/\x1b[><=ePX^_]/g, "")
|
|
9938
|
+
.replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
|
|
9939
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
9940
|
+
.replace(/[ \t]+\n/g, newline);
|
|
9941
|
+
}
|
|
9942
|
+
|
|
9943
|
+
function normalizeCodexText(value) {
|
|
9944
|
+
return String(value || "")
|
|
9945
|
+
.replace(/\s+/g, " ")
|
|
9946
|
+
.replace(/[M]+$/g, "")
|
|
9947
|
+
.trim();
|
|
9948
|
+
}
|
|
9949
|
+
|
|
9950
|
+
function normalizeCodexPromptLine(line) {
|
|
9951
|
+
return String(line || "")
|
|
9952
|
+
.replace(/^›\s*/, "")
|
|
9953
|
+
.replace(/^>\s*/, "")
|
|
9954
|
+
.trim();
|
|
9955
|
+
}
|
|
9956
|
+
|
|
9957
|
+
function shouldIgnoreCodexLine(line) {
|
|
9958
|
+
var trimmed = String(line || "").trim();
|
|
9959
|
+
if (!trimmed) return true;
|
|
9960
|
+
if (isNoiseLine(trimmed)) return true;
|
|
9961
|
+
if (codexFooterRe.test(trimmed)) return true;
|
|
9962
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed)) return true;
|
|
9963
|
+
if (/^\[>[0-9;?]*u$/i.test(trimmed)) return true;
|
|
9964
|
+
if (/^M+$/i.test(trimmed)) return true;
|
|
9965
|
+
if (/^(?:OpenAI Codex|Codex)\b/i.test(trimmed)) return true;
|
|
9966
|
+
if (/^(?:tokens?|context window|remaining context|approvals?|sandbox|provider|session id):\s*/i.test(trimmed)) return true;
|
|
9967
|
+
if (/^(?:thinking|working)\s*(?:\.\.\.|…)?$/i.test(trimmed)) return true;
|
|
9968
|
+
if (/^[•◦·]\s+(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i.test(trimmed)) return true;
|
|
9969
|
+
if (/^(?:model|directory|tip|context|cwd|path):\s+/i.test(trimmed)) return true;
|
|
9970
|
+
return false;
|
|
9971
|
+
}
|
|
9972
|
+
|
|
9973
|
+
function extractCodexPromptCandidate(line) {
|
|
9974
|
+
var trimmed = String(line || "").trim();
|
|
9975
|
+
if (!/^›(?:\s|$)/.test(trimmed)) return null;
|
|
9976
|
+
if (codexFooterRe.test(trimmed)) return null;
|
|
9977
|
+
var prompt = normalizeCodexText(normalizeCodexPromptLine(trimmed));
|
|
9978
|
+
if (!prompt || shouldIgnoreCodexLine(prompt)) return null;
|
|
9979
|
+
return prompt;
|
|
9980
|
+
}
|
|
9981
|
+
|
|
9982
|
+
function extractCodexAssistantCandidate(line) {
|
|
9983
|
+
var trimmed = String(line || "").trim();
|
|
9984
|
+
if (!/^[•◦·⏺]/.test(trimmed)) return null;
|
|
9985
|
+
|
|
9986
|
+
var assistant = trimmed
|
|
9987
|
+
.replace(/^[•◦·]\s*/, "")
|
|
9988
|
+
.replace(/^⏺\s+/, "")
|
|
9989
|
+
.replace(/^│\s*/, "")
|
|
9990
|
+
.trim();
|
|
9991
|
+
if (!assistant || /^[•◦·⏺]$/.test(assistant)) return null;
|
|
9992
|
+
|
|
9993
|
+
assistant = assistant
|
|
9994
|
+
.replace(/\s*\(\d+[smh]?\s*•\s*esc to interrupt\)[\s\S]*$/i, "")
|
|
9995
|
+
.replace(/(?:[a-z]{1,6})?›[\s\S]*$/, "")
|
|
9996
|
+
.replace(/\s{2,}gpt-\d[\s\S]*$/i, "")
|
|
9997
|
+
.replace(/\b(?:OpenAI Codex|model:|directory:|Tip:)\b[\s\S]*$/i, "");
|
|
9998
|
+
assistant = normalizeCodexText(assistant);
|
|
9999
|
+
|
|
10000
|
+
if (!assistant || assistant.length < 2 || codexActivityRe.test(assistant) || shouldIgnoreCodexLine(assistant)) {
|
|
10001
|
+
return null;
|
|
10002
|
+
}
|
|
10003
|
+
return assistant;
|
|
10004
|
+
}
|
|
10005
|
+
|
|
10006
|
+
function extractCodexEchoCandidate(line) {
|
|
10007
|
+
var trimmed = normalizeCodexText(line);
|
|
10008
|
+
if (!trimmed || shouldIgnoreCodexLine(trimmed)) return null;
|
|
10009
|
+
if (/^[•◦·⏺›]/.test(trimmed)) return null;
|
|
10010
|
+
if (/^[\[\]<>0-9;?]+u?$/i.test(trimmed)) return null;
|
|
10011
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed)) return null;
|
|
10012
|
+
if (trimmed.length > 500) return null;
|
|
10013
|
+
return trimmed;
|
|
10014
|
+
}
|
|
10015
|
+
|
|
10016
|
+
function isLikelyAssistantTailArtifact(longer, shorter) {
|
|
10017
|
+
if (longer.indexOf(shorter) !== 0) return false;
|
|
10018
|
+
var suffix = longer.slice(shorter.length);
|
|
10019
|
+
return /^[a-z]{1,4}$/i.test(suffix);
|
|
10020
|
+
}
|
|
10021
|
+
|
|
10022
|
+
function coalesceAssistantLines(lines) {
|
|
10023
|
+
var collected = [];
|
|
10024
|
+
for (var i = 0; i < lines.length; i++) {
|
|
10025
|
+
var normalized = normalizeCodexText(lines[i]);
|
|
10026
|
+
if (!normalized || normalized.length < 2 || shouldIgnoreCodexLine(normalized)) continue;
|
|
10027
|
+
|
|
10028
|
+
var previous = collected[collected.length - 1];
|
|
10029
|
+
if (!previous) {
|
|
10030
|
+
collected.push(normalized);
|
|
10031
|
+
continue;
|
|
10032
|
+
}
|
|
10033
|
+
if (normalized === previous) continue;
|
|
10034
|
+
if (normalized.indexOf(previous) === 0) {
|
|
10035
|
+
collected[collected.length - 1] = normalized;
|
|
10036
|
+
continue;
|
|
10037
|
+
}
|
|
10038
|
+
if (previous.indexOf(normalized) === 0) {
|
|
10039
|
+
if (isLikelyAssistantTailArtifact(previous, normalized)) {
|
|
10040
|
+
collected[collected.length - 1] = normalized;
|
|
10041
|
+
}
|
|
10042
|
+
continue;
|
|
10043
|
+
}
|
|
10044
|
+
collected.push(normalized);
|
|
10045
|
+
}
|
|
10046
|
+
return collected.join(newline).trim();
|
|
10047
|
+
}
|
|
10048
|
+
|
|
10049
|
+
function extractVisiblePrompt(lines) {
|
|
10050
|
+
for (var i = 0; i < lines.length; i++) {
|
|
10051
|
+
var line = String(lines[i] || "").trim();
|
|
10052
|
+
if (!line) continue;
|
|
10053
|
+
|
|
10054
|
+
var inlinePrompt = extractCodexPromptCandidate(line);
|
|
10055
|
+
if (inlinePrompt) return inlinePrompt;
|
|
10056
|
+
|
|
10057
|
+
if (line === "›") {
|
|
10058
|
+
for (var j = i + 1; j < lines.length; j++) {
|
|
10059
|
+
var nextLine = normalizeCodexText(lines[j]);
|
|
10060
|
+
if (!nextLine || codexFooterRe.test(nextLine) || shouldIgnoreCodexLine(nextLine)) continue;
|
|
10061
|
+
return nextLine;
|
|
10062
|
+
}
|
|
10063
|
+
}
|
|
10064
|
+
}
|
|
10065
|
+
return null;
|
|
10066
|
+
}
|
|
10067
|
+
|
|
10068
|
+
function extractVisibleAssistantLines(lines) {
|
|
10069
|
+
var assistantLines = [];
|
|
10070
|
+
var collecting = false;
|
|
10071
|
+
|
|
10072
|
+
for (var i = 0; i < lines.length; i++) {
|
|
10073
|
+
var line = String(lines[i] || "").trim();
|
|
10074
|
+
if (!line) {
|
|
10075
|
+
if (collecting) break;
|
|
10076
|
+
continue;
|
|
10077
|
+
}
|
|
10078
|
+
|
|
10079
|
+
var assistant = extractCodexAssistantCandidate(line);
|
|
10080
|
+
if (assistant) {
|
|
10081
|
+
assistantLines.push(assistant);
|
|
10082
|
+
collecting = true;
|
|
10083
|
+
continue;
|
|
10084
|
+
}
|
|
10085
|
+
|
|
10086
|
+
if (collecting) {
|
|
10087
|
+
if (line === "›" || /^›(?:\s|$)/.test(line) || codexFooterRe.test(line) || shouldIgnoreCodexLine(line)) {
|
|
10088
|
+
break;
|
|
10089
|
+
}
|
|
10090
|
+
assistantLines.push(normalizeCodexText(line));
|
|
10091
|
+
}
|
|
10092
|
+
}
|
|
10093
|
+
|
|
10094
|
+
return assistantLines;
|
|
10095
|
+
}
|
|
10096
|
+
|
|
10097
|
+
var rawCandidates = [];
|
|
10098
|
+
var candidateOrder = 0;
|
|
10099
|
+
var rawSegments = text.replace(/\r\n?/g, newline).split(newline);
|
|
10100
|
+
for (var rs = 0; rs < rawSegments.length; rs++) {
|
|
10101
|
+
var cleanedSegment = stripCodexSegment(rawSegments[rs]);
|
|
10102
|
+
var pieces = cleanedSegment.split(newline);
|
|
10103
|
+
for (var pi = 0; pi < pieces.length; pi++) {
|
|
10104
|
+
var piece = String(pieces[pi] || "").trim();
|
|
10105
|
+
if (!piece) continue;
|
|
10106
|
+
|
|
10107
|
+
var promptCandidate = extractCodexPromptCandidate(piece);
|
|
10108
|
+
if (promptCandidate) {
|
|
10109
|
+
rawCandidates.push({ kind: "user", order: candidateOrder++, text: promptCandidate });
|
|
10110
|
+
continue;
|
|
10111
|
+
}
|
|
10112
|
+
|
|
10113
|
+
var assistantCandidate = extractCodexAssistantCandidate(piece);
|
|
10114
|
+
if (assistantCandidate) {
|
|
10115
|
+
rawCandidates.push({ kind: "assistant", order: candidateOrder++, text: assistantCandidate });
|
|
10116
|
+
continue;
|
|
10117
|
+
}
|
|
10118
|
+
|
|
10119
|
+
var echoCandidate = extractCodexEchoCandidate(piece);
|
|
10120
|
+
if (echoCandidate) {
|
|
10121
|
+
rawCandidates.push({ kind: "echo", order: candidateOrder++, text: echoCandidate });
|
|
10122
|
+
}
|
|
10123
|
+
}
|
|
10124
|
+
}
|
|
10125
|
+
|
|
10126
|
+
var candidates = rawCandidates.filter(function(candidate, index, list) {
|
|
10127
|
+
var previous = list[index - 1];
|
|
10128
|
+
return !previous || previous.kind !== candidate.kind || previous.text !== candidate.text;
|
|
10129
|
+
});
|
|
10130
|
+
|
|
10131
|
+
var explicitUsers = candidates.filter(function(candidate) { return candidate.kind === "user"; });
|
|
10132
|
+
var assistantCandidates = candidates.filter(function(candidate) { return candidate.kind === "assistant"; });
|
|
10133
|
+
var echoCandidates = candidates.filter(function(candidate) { return candidate.kind === "echo"; });
|
|
10134
|
+
var strippedOutput = stripAnsi(text);
|
|
10135
|
+
var strippedLines = strippedOutput.split(newline).map(function(line) { return String(line || "").trimEnd(); });
|
|
10136
|
+
var visiblePrompt = extractVisiblePrompt(strippedLines);
|
|
10137
|
+
var latestExplicitUser = explicitUsers.length ? explicitUsers[explicitUsers.length - 1] : null;
|
|
10138
|
+
var echoedUserCandidates = echoCandidates
|
|
10139
|
+
.map(function(candidate) { return candidate.text; })
|
|
10140
|
+
.filter(function(value) { return value.length >= 3; });
|
|
10141
|
+
var latestEchoUser = null;
|
|
10142
|
+
for (var eu = echoedUserCandidates.length - 1; eu >= 0; eu--) {
|
|
10143
|
+
if (echoedUserCandidates[eu] !== visiblePrompt) {
|
|
10144
|
+
latestEchoUser = echoedUserCandidates[eu];
|
|
10145
|
+
break;
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
if (!latestEchoUser && echoedUserCandidates.length) {
|
|
10149
|
+
latestEchoUser = echoedUserCandidates[echoedUserCandidates.length - 1];
|
|
10150
|
+
}
|
|
10151
|
+
|
|
10152
|
+
var currentUser = latestExplicitUser ? latestExplicitUser.text : latestEchoUser;
|
|
10153
|
+
var rawAssistantLines = assistantCandidates
|
|
10154
|
+
.filter(function(candidate) { return !latestExplicitUser || candidate.order > latestExplicitUser.order; })
|
|
10155
|
+
.map(function(candidate) { return candidate.text; });
|
|
10156
|
+
var visibleAssistantFallback = [];
|
|
10157
|
+
var bulletMatches = strippedOutput.match(/^[ \t]*[•◦·⏺][ \t]*(.+)$/gm) || [];
|
|
10158
|
+
for (var bm = 0; bm < bulletMatches.length; bm++) {
|
|
10159
|
+
var bulletContent = normalizeCodexText(bulletMatches[bm].replace(/^[ \t]*[•◦·⏺][ \t]*/, ""));
|
|
10160
|
+
if (!bulletContent) continue;
|
|
10161
|
+
if (codexActivityRe.test(bulletContent)) continue;
|
|
10162
|
+
if (codexFooterRe.test(bulletContent)) continue;
|
|
10163
|
+
if (/\b(?:OpenAI Codex|model:|directory:|Tip:|esc to interrupt)\b/i.test(bulletContent)) continue;
|
|
10164
|
+
visibleAssistantFallback.push(bulletContent);
|
|
10165
|
+
}
|
|
10166
|
+
|
|
10167
|
+
var assistantText = coalesceAssistantLines(rawAssistantLines)
|
|
10168
|
+
|| coalesceAssistantLines(extractVisibleAssistantLines(strippedLines))
|
|
10169
|
+
|| (visibleAssistantFallback.length ? visibleAssistantFallback[visibleAssistantFallback.length - 1] : null);
|
|
10170
|
+
|
|
10171
|
+
if (currentUser) {
|
|
10172
|
+
messages.push({ role: "user", content: currentUser });
|
|
10173
|
+
}
|
|
10174
|
+
if (assistantText) {
|
|
10175
|
+
messages.push({ role: "assistant", content: assistantText });
|
|
10176
|
+
}
|
|
10177
|
+
if (!messages.length && latestExplicitUser) {
|
|
10178
|
+
messages.push({ role: "user", content: latestExplicitUser.text });
|
|
10179
|
+
} else if (!messages.length && latestEchoUser) {
|
|
10180
|
+
messages.push({ role: "user", content: latestEchoUser });
|
|
10181
|
+
}
|
|
10182
|
+
|
|
10183
|
+
return messages;
|
|
10184
|
+
}
|
|
10185
|
+
|
|
9038
10186
|
// Optimized ANSI escape sequence stripping
|
|
9039
10187
|
// Handles: CSI sequences, OSC sequences, single-character escapes, control chars
|
|
9040
10188
|
var nul = String.fromCharCode(0);
|
|
@@ -9318,24 +10466,64 @@
|
|
|
9318
10466
|
};
|
|
9319
10467
|
})();
|
|
9320
10468
|
|
|
10469
|
+
var DEFAULT_CHAT_PERSONA = {
|
|
10470
|
+
user: {
|
|
10471
|
+
name: "赛博虎妞",
|
|
10472
|
+
avatarSvg: PIXEL_AVATAR.user
|
|
10473
|
+
},
|
|
10474
|
+
assistant: {
|
|
10475
|
+
name: "勤劳初二",
|
|
10476
|
+
avatarSvg: PIXEL_AVATAR.assistant
|
|
10477
|
+
}
|
|
10478
|
+
};
|
|
10479
|
+
|
|
10480
|
+
function getStructuredChatPersona(role) {
|
|
10481
|
+
var configPersona = state.config && state.config.structuredChatPersona;
|
|
10482
|
+
var roleConfig = configPersona && configPersona[role] ? configPersona[role] : null;
|
|
10483
|
+
var defaults = DEFAULT_CHAT_PERSONA[role] || DEFAULT_CHAT_PERSONA.assistant;
|
|
10484
|
+
return {
|
|
10485
|
+
name: roleConfig && typeof roleConfig.name === "string" && roleConfig.name.trim()
|
|
10486
|
+
? roleConfig.name.trim()
|
|
10487
|
+
: defaults.name,
|
|
10488
|
+
avatar: roleConfig && typeof roleConfig.avatar === "string" && roleConfig.avatar.trim()
|
|
10489
|
+
? roleConfig.avatar.trim()
|
|
10490
|
+
: null,
|
|
10491
|
+
avatarSvg: defaults.avatarSvg
|
|
10492
|
+
};
|
|
10493
|
+
}
|
|
10494
|
+
|
|
10495
|
+
function renderAvatarFallback(svg) {
|
|
10496
|
+
return '<div class="pixel-avatar">' + svg + '</div>';
|
|
10497
|
+
}
|
|
10498
|
+
|
|
10499
|
+
function handleChatAvatarImageError(img, role) {
|
|
10500
|
+
if (!img || !img.parentNode) return;
|
|
10501
|
+
var persona = getStructuredChatPersona(role === "user" ? "user" : "assistant");
|
|
10502
|
+
img.outerHTML = renderAvatarFallback(persona.avatarSvg);
|
|
10503
|
+
}
|
|
10504
|
+
|
|
9321
10505
|
function chatAvatar(role) {
|
|
9322
|
-
var
|
|
9323
|
-
var
|
|
9324
|
-
var
|
|
10506
|
+
var personaRole = role === "user" ? "user" : "assistant";
|
|
10507
|
+
var persona = getStructuredChatPersona(personaRole);
|
|
10508
|
+
var avatarInner = persona.avatar
|
|
10509
|
+
? '<img class="pixel-avatar-image" src="' + escapeHtml(persona.avatar) + '" alt="' + escapeHtml(persona.name) + '" onerror="handleChatAvatarImageError(this, ' + JSON.stringify(personaRole) + ')" />'
|
|
10510
|
+
: renderAvatarFallback(persona.avatarSvg);
|
|
9325
10511
|
return '<div class="chat-message-avatar ' + role + '">' +
|
|
9326
|
-
|
|
9327
|
-
'<span class="avatar-name">' + name + '</span>' +
|
|
10512
|
+
avatarInner +
|
|
10513
|
+
'<span class="avatar-name">' + escapeHtml(persona.name) + '</span>' +
|
|
9328
10514
|
'</div>';
|
|
9329
10515
|
}
|
|
9330
10516
|
|
|
9331
|
-
function renderChatMessage(msg, roundUsage) {
|
|
10517
|
+
function renderChatMessage(msg, roundUsage, messageIndex) {
|
|
9332
10518
|
// Thinking card (deep thought) — from PTY parsing
|
|
9333
10519
|
if (msg.role === "thinking") {
|
|
10520
|
+
var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
|
|
10521
|
+
var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
|
|
9334
10522
|
return '<div class="chat-message thinking">' +
|
|
9335
|
-
'<div class="thinking-inline thinking-pty collapsed" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
10523
|
+
'<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
9336
10524
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
9337
10525
|
'<span class="thinking-inline-preview">' + escapeHtml(msg.content) + '</span>' +
|
|
9338
|
-
'<span class="thinking-inline-action"
|
|
10526
|
+
'<span class="thinking-inline-action">' + (thinkingExpanded ? '收起' : '展开') + '</span>' +
|
|
9339
10527
|
'</div>' +
|
|
9340
10528
|
'</div>';
|
|
9341
10529
|
}
|
|
@@ -9352,7 +10540,7 @@
|
|
|
9352
10540
|
|
|
9353
10541
|
// Structured content blocks (from JSON chat mode)
|
|
9354
10542
|
if (Array.isArray(msg.content)) {
|
|
9355
|
-
return renderStructuredMessage(msg, roundUsage);
|
|
10543
|
+
return renderStructuredMessage(msg, roundUsage, messageIndex);
|
|
9356
10544
|
}
|
|
9357
10545
|
|
|
9358
10546
|
// Legacy string content (from PTY parsing)
|
|
@@ -9442,7 +10630,7 @@
|
|
|
9442
10630
|
|
|
9443
10631
|
var TOOL_GROUP_LABELS = { Read: "读取", Glob: "搜索", Grep: "搜索", WebFetch: "抓取", WebSearch: "搜索", TodoRead: "待办" };
|
|
9444
10632
|
|
|
9445
|
-
function renderToolGroup(items, role, toolResults) {
|
|
10633
|
+
function renderToolGroup(items, role, toolResults, messageKey) {
|
|
9446
10634
|
// Count by tool name
|
|
9447
10635
|
var counts = {};
|
|
9448
10636
|
for (var k = 0; k < items.length; k++) {
|
|
@@ -9466,28 +10654,32 @@
|
|
|
9466
10654
|
parts.push(counts[name] + " " + (TOOL_GROUP_LABELS[name] || name));
|
|
9467
10655
|
}
|
|
9468
10656
|
var summaryText = parts.join(" · ");
|
|
10657
|
+
var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
|
|
10658
|
+
var persistedExpanded = getPersistedExpandState(groupKey);
|
|
10659
|
+
var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
|
|
9469
10660
|
|
|
9470
10661
|
// Render each item's inline-tool card
|
|
9471
10662
|
var innerHtml = "";
|
|
9472
10663
|
for (var k = 0; k < items.length; k++) {
|
|
9473
10664
|
try {
|
|
9474
|
-
innerHtml += renderContentBlock(items[k].block, role, toolResults, items[k].index);
|
|
10665
|
+
innerHtml += renderContentBlock(items[k].block, role, toolResults, items[k].index, messageKey);
|
|
9475
10666
|
} catch (e) {
|
|
9476
10667
|
innerHtml += '<div class="render-error">工具渲染失败</div>';
|
|
9477
10668
|
}
|
|
9478
10669
|
}
|
|
9479
10670
|
|
|
9480
|
-
return '<div class="tool-group" data-expanded="false" data-status="' + statusClass + '">' +
|
|
10671
|
+
return '<div class="tool-group" data-expand-kind="tool-group" data-expand-key="' + escapeHtml(groupKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '" data-status="' + statusClass + '">' +
|
|
9481
10672
|
'<div class="tool-group-summary" onclick="__toolGroupToggle(this.parentNode)">' +
|
|
9482
10673
|
'<span class="tool-group-status">' + statusIcon + '</span>' +
|
|
9483
10674
|
'<span class="tool-group-text">' + escapeHtml(summaryText) + '</span>' +
|
|
9484
10675
|
'<span class="tool-group-count">' + items.length + ' 个调用</span>' +
|
|
9485
|
-
'<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>' +
|
|
10676
|
+
'<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>' +
|
|
9486
10677
|
'</div>' +
|
|
9487
|
-
'<div class="tool-group-body">' + innerHtml + '</div>' +
|
|
10678
|
+
'<div class="tool-group-body" style="display:' + (shouldExpand ? 'block' : 'none') + ';">' + innerHtml + '</div>' +
|
|
9488
10679
|
'</div>';
|
|
9489
10680
|
}
|
|
9490
10681
|
|
|
10682
|
+
|
|
9491
10683
|
// global toggle
|
|
9492
10684
|
window.__toolGroupToggle = function(el) {
|
|
9493
10685
|
if (!el) return;
|
|
@@ -9497,11 +10689,13 @@
|
|
|
9497
10689
|
if (body) body.style.display = expanded ? "none" : "block";
|
|
9498
10690
|
var chevron = el.querySelector(".tool-group-chevron");
|
|
9499
10691
|
if (chevron) chevron.style.transform = expanded ? "" : "rotate(180deg)";
|
|
10692
|
+
persistElementExpandState(el, "tool-group");
|
|
9500
10693
|
};
|
|
9501
10694
|
|
|
9502
|
-
function renderStructuredMessage(msg, roundUsage) {
|
|
10695
|
+
function renderStructuredMessage(msg, roundUsage, messageIndex) {
|
|
9503
10696
|
var role = msg.role;
|
|
9504
10697
|
var avatar = chatAvatar(role);
|
|
10698
|
+
var messageKey = getMessageKey(msg, messageIndex);
|
|
9505
10699
|
|
|
9506
10700
|
// Check if this is a queued user message
|
|
9507
10701
|
var isQueued = role === "user" && msg.content && msg.content.some(function(b) { return b.__queued; });
|
|
@@ -9525,9 +10719,9 @@
|
|
|
9525
10719
|
var grp = groups[g];
|
|
9526
10720
|
try {
|
|
9527
10721
|
if (grp.type === "group") {
|
|
9528
|
-
blocksHtml += renderToolGroup(grp.items, role, toolResults);
|
|
10722
|
+
blocksHtml += renderToolGroup(grp.items, role, toolResults, messageKey);
|
|
9529
10723
|
} else {
|
|
9530
|
-
blocksHtml += renderContentBlock(grp.block, role, toolResults, grp.index);
|
|
10724
|
+
blocksHtml += renderContentBlock(grp.block, role, toolResults, grp.index, messageKey);
|
|
9531
10725
|
}
|
|
9532
10726
|
} catch (e) {
|
|
9533
10727
|
blocksHtml += '<div class="render-error">消息块渲染失败</div>';
|
|
@@ -9544,13 +10738,13 @@
|
|
|
9544
10738
|
var queuedClass = isQueued ? " queued" : "";
|
|
9545
10739
|
var queuedBadge = isQueued ? '<span class="queued-badge">排队中</span>' : "";
|
|
9546
10740
|
|
|
9547
|
-
return '<div class="chat-message ' + role + queuedClass + '">' +
|
|
10741
|
+
return '<div class="chat-message ' + role + queuedClass + '" data-message-key="' + escapeHtml(messageKey) + '">' +
|
|
9548
10742
|
avatar +
|
|
9549
10743
|
'<div class="chat-message-content">' + blocksHtml + queuedBadge + '</div>' +
|
|
9550
10744
|
usageHtml +
|
|
9551
10745
|
'</div>';
|
|
9552
10746
|
}
|
|
9553
|
-
function renderContentBlock(block, role, toolResults, index) {
|
|
10747
|
+
function renderContentBlock(block, role, toolResults, index, messageKey) {
|
|
9554
10748
|
if (!block || !block.type) return "";
|
|
9555
10749
|
|
|
9556
10750
|
switch (block.type) {
|
|
@@ -9572,15 +10766,17 @@
|
|
|
9572
10766
|
'</div>' +
|
|
9573
10767
|
'</div>';
|
|
9574
10768
|
}
|
|
9575
|
-
|
|
10769
|
+
var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
|
|
10770
|
+
var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
|
|
10771
|
+
return '<div class="thinking-inline ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="' + escapeHtml(thinkingText) + '" onclick="__thinkingToggle(this)">' +
|
|
9576
10772
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
9577
|
-
'<span class="thinking-inline-preview">' + escapeHtml(preview) + '</span>' +
|
|
9578
|
-
'<span class="thinking-inline-action"
|
|
10773
|
+
'<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
|
|
10774
|
+
'<span class="thinking-inline-action">' + (thinkingExpanded ? '收起' : '展开') + '</span>' +
|
|
9579
10775
|
'</div>';
|
|
9580
10776
|
|
|
9581
10777
|
case "tool_use":
|
|
9582
10778
|
var toolResult = pickToolResultForDisplay(toolResults, block.id);
|
|
9583
|
-
var rendered = renderToolUseCard(block, toolResult, index);
|
|
10779
|
+
var rendered = renderToolUseCard(block, toolResult, index, messageKey);
|
|
9584
10780
|
if (hasRecoveredToolNoise(toolResults, block.id)) {
|
|
9585
10781
|
rendered = renderRecoveredToolHint(block.name || "工具") + rendered;
|
|
9586
10782
|
}
|
|
@@ -9594,8 +10790,10 @@
|
|
|
9594
10790
|
}
|
|
9595
10791
|
}
|
|
9596
10792
|
|
|
9597
|
-
function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo) {
|
|
10793
|
+
function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo, messageKey, index) {
|
|
9598
10794
|
var toolId = block.id || "tool-" + toolName;
|
|
10795
|
+
var expandKey = buildExpandKey("inline-tool", [messageKey, toolId || index, index]);
|
|
10796
|
+
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
9599
10797
|
var inputData = block.input || {};
|
|
9600
10798
|
var resultContent = extractToolResultText(toolResult && toolResult.content);
|
|
9601
10799
|
|
|
@@ -9666,16 +10864,16 @@
|
|
|
9666
10864
|
var fullResult = resultContent;
|
|
9667
10865
|
|
|
9668
10866
|
var expandedHtml = "";
|
|
9669
|
-
var shouldExpand =
|
|
10867
|
+
var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
|
|
9670
10868
|
if (hasResult) {
|
|
9671
10869
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
9672
10870
|
'<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
|
|
9673
10871
|
'</div>';
|
|
9674
10872
|
} else if (isError) {
|
|
9675
|
-
expandedHtml = '<div class="inline-tool-expanded" style="display: none;"><div class="inline-tool-result inline-tool-error">' +
|
|
10873
|
+
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-result inline-tool-error">' +
|
|
9676
10874
|
escapeHtml(resultContent || "操作失败") + '</div></div>';
|
|
9677
10875
|
} else if (!toolResult) {
|
|
9678
|
-
expandedHtml = '<div class="inline-tool-expanded" style="display: none;"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
10876
|
+
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
9679
10877
|
}
|
|
9680
10878
|
|
|
9681
10879
|
var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
|
|
@@ -9683,6 +10881,8 @@
|
|
|
9683
10881
|
if (shouldExpand) extraClass += ' inline-tool-open';
|
|
9684
10882
|
|
|
9685
10883
|
return '<div class="inline-tool ' + extraClass + '" ' +
|
|
10884
|
+
'data-expand-kind="inline-tool" ' +
|
|
10885
|
+
'data-expand-key="' + escapeHtml(expandKey) + '" ' +
|
|
9686
10886
|
'data-result="' + escapeHtml(fullResult) + '" ' +
|
|
9687
10887
|
'data-preview="' + previewDataAttr + '" ' +
|
|
9688
10888
|
'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
|
|
@@ -9698,10 +10898,13 @@
|
|
|
9698
10898
|
}
|
|
9699
10899
|
|
|
9700
10900
|
// Terminal-style display for Bash commands
|
|
9701
|
-
function renderTerminalTool(block, toolResult, toolName) {
|
|
10901
|
+
function renderTerminalTool(block, toolResult, toolName, messageKey, index) {
|
|
9702
10902
|
var inputData = block.input || {};
|
|
9703
10903
|
var command = inputData.command || inputData.cmd || "";
|
|
9704
10904
|
var resultContent = extractToolResultText(toolResult && toolResult.content);
|
|
10905
|
+
var toolId = block.id || "tool-" + toolName;
|
|
10906
|
+
var expandKey = buildExpandKey("terminal", [messageKey, toolId || index, index]);
|
|
10907
|
+
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
9705
10908
|
|
|
9706
10909
|
var isError = toolResult && toolResult.is_error;
|
|
9707
10910
|
var exitCode = inputData.exitCode;
|
|
@@ -9739,22 +10942,21 @@
|
|
|
9739
10942
|
|
|
9740
10943
|
// Show command preview in header (truncate long commands)
|
|
9741
10944
|
var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
|
|
10945
|
+
var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
|
|
9742
10946
|
|
|
9743
|
-
return '<div class="inline-terminal" data-expanded="false">' +
|
|
10947
|
+
return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '">' +
|
|
9744
10948
|
'<div class="term-header" onclick="__terminalExpand(this)">' +
|
|
9745
10949
|
statusDot +
|
|
9746
10950
|
'<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
|
|
9747
|
-
'<span class="term-toggle-icon"
|
|
10951
|
+
'<span class="term-toggle-icon">' + (shouldExpand ? '▼' : '▶') + '</span>' +
|
|
9748
10952
|
'</div>' +
|
|
9749
|
-
'<div class="term-body" style="display:none;">' +
|
|
10953
|
+
'<div class="term-body" style="display:' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
9750
10954
|
'<div class="term-command"><span class="term-prompt">$</span> ' + cmdDisplay + '</div>' +
|
|
9751
10955
|
(outputHtml ? '<div class="term-output">' + outputHtml + '</div>' : '') +
|
|
9752
10956
|
exitCodeHtml +
|
|
9753
10957
|
'</div>' +
|
|
9754
10958
|
'</div>';
|
|
9755
10959
|
}
|
|
9756
|
-
|
|
9757
|
-
// GitHub-style diff display for Edit/Write/MultiEdit
|
|
9758
10960
|
function extractToolResultText(content) {
|
|
9759
10961
|
if (!content) return "";
|
|
9760
10962
|
if (typeof content === "string") return content;
|
|
@@ -9845,7 +11047,7 @@
|
|
|
9845
11047
|
return '<pre class="inline-tool-result-text" style="max-height: 300px; overflow-y: auto;">' + escapeHtml(content) + '</pre>';
|
|
9846
11048
|
}
|
|
9847
11049
|
|
|
9848
|
-
function renderToolUseCard(block, toolResult, index) {
|
|
11050
|
+
function renderToolUseCard(block, toolResult, index, messageKey) {
|
|
9849
11051
|
var toolName = block.name || "unknown";
|
|
9850
11052
|
var toolId = block.id || "tool-" + toolName + "-" + (typeof index === "number" ? index : 0);
|
|
9851
11053
|
var fileInfo = extractFileInfo(toolName, block.input);
|
|
@@ -9853,12 +11055,12 @@
|
|
|
9853
11055
|
// ── Lightweight inline tools: Read, Glob, Grep, WebFetch, WebSearch, TodoRead
|
|
9854
11056
|
if (toolName === "Read" || toolName === "Glob" || toolName === "Grep" ||
|
|
9855
11057
|
toolName === "WebFetch" || toolName === "WebSearch" || toolName === "TodoRead") {
|
|
9856
|
-
return renderInlineTool(block, toolResult, toolName, fileInfo, "");
|
|
11058
|
+
return renderInlineTool(block, toolResult, toolName, fileInfo, "", messageKey, index);
|
|
9857
11059
|
}
|
|
9858
11060
|
|
|
9859
11061
|
// ── Terminal-style: Bash
|
|
9860
11062
|
if (toolName === "Bash") {
|
|
9861
|
-
return renderTerminalTool(block, toolResult, toolName);
|
|
11063
|
+
return renderTerminalTool(block, toolResult, toolName, messageKey, index);
|
|
9862
11064
|
}
|
|
9863
11065
|
|
|
9864
11066
|
// ── Diff-style: Edit, Write, MultiEdit
|
|
@@ -9937,9 +11139,12 @@
|
|
|
9937
11139
|
headerIcon = getToolIcon(toolName);
|
|
9938
11140
|
}
|
|
9939
11141
|
|
|
9940
|
-
var
|
|
11142
|
+
var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
|
|
11143
|
+
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
11144
|
+
var shouldExpand = persistedExpanded === null ? statusClass === "loading" : persistedExpanded;
|
|
11145
|
+
var collapsedClass = shouldExpand ? "" : " collapsed";
|
|
9941
11146
|
var toggleHtml = '<span class="tool-use-toggle">▼</span>';
|
|
9942
|
-
return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
11147
|
+
return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
9943
11148
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
9944
11149
|
'<span class="tool-use-icon">' + headerIcon + '</span>' +
|
|
9945
11150
|
'<span class="tool-use-name">' + escapeHtml(titleText) + '</span>' +
|