@co0ontty/wand 1.5.7 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/process-manager.js +20 -1
- package/dist/structured-session-manager.js +22 -2
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +491 -100
- package/dist/web-ui/content/styles.css +195 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,15 +41,16 @@ wand config:set port 9443
|
|
|
41
41
|
| `port` | `8443` | 监听端口 |
|
|
42
42
|
| `https` | `false` | 启用 HTTPS(自签证书自动生成) |
|
|
43
43
|
| `password` | (随机生成) | 登录密码 |
|
|
44
|
-
| `
|
|
44
|
+
| `language` | `""` | Claude 回复语言偏好 |
|
|
45
45
|
|
|
46
46
|
## 功能
|
|
47
47
|
|
|
48
48
|
- **双视图模式** — 终端原始输出和结构化对话视图可随时切换
|
|
49
|
-
- **会话管理** — 创建、归档、恢复会话;支持从 Claude
|
|
50
|
-
- **权限控制** —
|
|
51
|
-
- **文件浏览器** —
|
|
49
|
+
- **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复;会话列表显示摘要
|
|
50
|
+
- **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略;工具调用自动分组
|
|
51
|
+
- **文件浏览器** — 内置路径浏览和搜索功能
|
|
52
52
|
- **多种运行模式** — full-access / default / auto-edit 等 Claude 运行模式
|
|
53
|
+
- **个性化** — 像素风猫咪头像、回复语言偏好设置
|
|
53
54
|
- **PWA 支持** — 可添加到主屏幕作为独立应用使用
|
|
54
55
|
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
55
56
|
|
package/dist/process-manager.js
CHANGED
|
@@ -397,6 +397,24 @@ function getLatestClaudeTaskId(excludeIds) {
|
|
|
397
397
|
return null;
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
|
+
/** Derive a short summary for a session from user messages or current task. */
|
|
401
|
+
function deriveSessionSummary(messages, currentTaskTitle) {
|
|
402
|
+
// Prefer first user message as summary
|
|
403
|
+
for (const msg of messages) {
|
|
404
|
+
if (msg.role !== "user")
|
|
405
|
+
continue;
|
|
406
|
+
for (const block of msg.content) {
|
|
407
|
+
if (block.type === "text" && block.text.trim()) {
|
|
408
|
+
return block.text.trim().slice(0, 120);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
break; // only check the first user turn
|
|
412
|
+
}
|
|
413
|
+
// Fallback to current task title
|
|
414
|
+
if (currentTaskTitle)
|
|
415
|
+
return currentTaskTitle.slice(0, 120);
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
400
418
|
export class ProcessManager extends EventEmitter {
|
|
401
419
|
config;
|
|
402
420
|
storage;
|
|
@@ -1102,7 +1120,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1102
1120
|
resumedToSessionId: record.resumedToSessionId ?? undefined,
|
|
1103
1121
|
autoRecovered: record.autoRecovered ?? false,
|
|
1104
1122
|
autoApprovePermissions: record.autoApprovePermissions || undefined,
|
|
1105
|
-
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined
|
|
1123
|
+
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined,
|
|
1124
|
+
summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
|
|
1106
1125
|
};
|
|
1107
1126
|
}
|
|
1108
1127
|
isPermissionBlocked(record) {
|
|
@@ -4,6 +4,23 @@ const STREAM_EMIT_DEBOUNCE_MS = 16;
|
|
|
4
4
|
function isRunningAsRoot() {
|
|
5
5
|
return process.getuid?.() === 0 || process.geteuid?.() === 0;
|
|
6
6
|
}
|
|
7
|
+
/** Enrich a snapshot with a derived summary from the first user message. */
|
|
8
|
+
function withSummary(snapshot) {
|
|
9
|
+
if (snapshot.summary)
|
|
10
|
+
return snapshot;
|
|
11
|
+
const messages = snapshot.messages ?? [];
|
|
12
|
+
for (const msg of messages) {
|
|
13
|
+
if (msg.role !== "user")
|
|
14
|
+
continue;
|
|
15
|
+
for (const block of msg.content) {
|
|
16
|
+
if (block.type === "text" && block.text.trim()) {
|
|
17
|
+
return { ...snapshot, summary: block.text.trim().slice(0, 120) };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
return snapshot;
|
|
23
|
+
}
|
|
7
24
|
/** Should we auto-approve permissions for this mode? */
|
|
8
25
|
function shouldAutoApproveForMode(mode) {
|
|
9
26
|
return mode === "full-access" || mode === "managed" || mode === "auto-edit";
|
|
@@ -46,10 +63,13 @@ export class StructuredSessionManager {
|
|
|
46
63
|
this.emitEvent = emitEvent;
|
|
47
64
|
}
|
|
48
65
|
list() {
|
|
49
|
-
return Array.from(this.sessions.values())
|
|
66
|
+
return Array.from(this.sessions.values())
|
|
67
|
+
.map(withSummary)
|
|
68
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
50
69
|
}
|
|
51
70
|
get(id) {
|
|
52
|
-
|
|
71
|
+
const s = this.sessions.get(id);
|
|
72
|
+
return s ? withSummary(s) : null;
|
|
53
73
|
}
|
|
54
74
|
createSession(options) {
|
|
55
75
|
const id = randomUUID();
|
package/dist/types.d.ts
CHANGED
|
@@ -189,6 +189,8 @@ export interface SessionSnapshot {
|
|
|
189
189
|
file: number;
|
|
190
190
|
total: number;
|
|
191
191
|
};
|
|
192
|
+
/** 会话摘要:从首条用户消息或当前任务提取 */
|
|
193
|
+
summary?: string;
|
|
192
194
|
}
|
|
193
195
|
export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
|
|
194
196
|
export interface SessionLifecycle {
|
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
inputQueue: Promise.resolve(),
|
|
91
91
|
pendingMessages: [], // WebSocket 断线期间的消息队列
|
|
92
92
|
messageQueue: [], // 用户消息排队等待发送
|
|
93
|
+
crossSessionQueue: [], // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
|
|
94
|
+
structuredInputQueue: [], // 结构化会话同会话排队消息
|
|
93
95
|
drafts: {},
|
|
94
96
|
isSyncingInputBox: false,
|
|
95
97
|
loginPending: false,
|
|
@@ -678,7 +680,6 @@
|
|
|
678
680
|
'</div>' +
|
|
679
681
|
'<div class="todo-progress-body hidden" id="todo-progress-body">' +
|
|
680
682
|
'<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
|
|
681
|
-
'<div id="recent-actions" class="recent-actions"></div>' +
|
|
682
683
|
'</div>' +
|
|
683
684
|
'</div>' +
|
|
684
685
|
'<div class="input-composer">' +
|
|
@@ -1816,7 +1817,9 @@
|
|
|
1816
1817
|
'<div class="session-item-row">' +
|
|
1817
1818
|
checkbox +
|
|
1818
1819
|
'<div class="session-main">' +
|
|
1819
|
-
|
|
1820
|
+
(session.summary
|
|
1821
|
+
? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
|
|
1822
|
+
: '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>') +
|
|
1820
1823
|
'<div class="session-meta">' +
|
|
1821
1824
|
modeBadge +
|
|
1822
1825
|
'<span>' + escapeHtml(modeName) + '</span>' +
|
|
@@ -1995,23 +1998,23 @@
|
|
|
1995
1998
|
var optionLabel = btnEl.dataset.optionLabel;
|
|
1996
1999
|
if (optionLabel && state.selectedId) {
|
|
1997
2000
|
btnEl.classList.add("selected");
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2001
|
+
// Only disable options within the same question group, not globally
|
|
2002
|
+
var questionGroup = btnEl.closest(".ask-user-question-group");
|
|
2003
|
+
if (questionGroup) {
|
|
2004
|
+
questionGroup.querySelectorAll(".ask-user-option").forEach(function(opt) {
|
|
2005
|
+
opt.classList.add("selected");
|
|
2006
|
+
opt.style.pointerEvents = "none";
|
|
2007
|
+
});
|
|
2005
2008
|
var sentDiv = document.createElement("div");
|
|
2006
2009
|
sentDiv.className = "ask-user-answer-sent";
|
|
2007
|
-
sentDiv.innerHTML = "
|
|
2008
|
-
|
|
2010
|
+
sentDiv.innerHTML = "\u2713 \u5df2\u53d1\u9001: " + escapeHtml(optionLabel);
|
|
2011
|
+
questionGroup.appendChild(sentDiv);
|
|
2009
2012
|
}
|
|
2010
2013
|
fetch("/api/sessions/" + state.selectedId + "/input", {
|
|
2011
2014
|
method: "POST",
|
|
2012
2015
|
headers: { "Content-Type": "application/json" },
|
|
2013
2016
|
credentials: "same-origin",
|
|
2014
|
-
body: JSON.stringify({ input: optionLabel + "
|
|
2017
|
+
body: JSON.stringify({ input: optionLabel + "\n", view: state.currentView })
|
|
2015
2018
|
}).catch(function(err) {
|
|
2016
2019
|
console.error("[wand] Error sending answer:", err);
|
|
2017
2020
|
});
|
|
@@ -3679,6 +3682,11 @@
|
|
|
3679
3682
|
reconcileInteractiveState();
|
|
3680
3683
|
updateTaskDisplay();
|
|
3681
3684
|
}
|
|
3685
|
+
// When a session transitions to a non-running state, try flushing cross-session queue
|
|
3686
|
+
if (snapshot.status && snapshot.status !== "running" && state.crossSessionQueue.length > 0) {
|
|
3687
|
+
// Use setTimeout(0) to let the current event processing complete first
|
|
3688
|
+
setTimeout(flushCrossSessionQueue, 0);
|
|
3689
|
+
}
|
|
3682
3690
|
}
|
|
3683
3691
|
|
|
3684
3692
|
function subscribeToSession(sessionId) {
|
|
@@ -3818,6 +3826,11 @@
|
|
|
3818
3826
|
loadOutput(state.selectedId);
|
|
3819
3827
|
}
|
|
3820
3828
|
}
|
|
3829
|
+
|
|
3830
|
+
// Try to flush cross-session queue on every session list refresh
|
|
3831
|
+
if (state.crossSessionQueue.length > 0) {
|
|
3832
|
+
flushCrossSessionQueue();
|
|
3833
|
+
}
|
|
3821
3834
|
})
|
|
3822
3835
|
.catch(function(e) {
|
|
3823
3836
|
console.error("[wand] loadSessions failed:", e);
|
|
@@ -3830,6 +3843,8 @@
|
|
|
3830
3843
|
if (listEl) listEl.innerHTML = renderSessions();
|
|
3831
3844
|
if (countEl) countEl.textContent = String(state.sessions.length);
|
|
3832
3845
|
updateShellChrome();
|
|
3846
|
+
// Re-render cross-session queue (container may have been destroyed by DOM rebuild)
|
|
3847
|
+
if (state.crossSessionQueue.length > 0) renderCrossSessionQueue();
|
|
3833
3848
|
}
|
|
3834
3849
|
|
|
3835
3850
|
function updateShellChrome() {
|
|
@@ -3938,6 +3953,9 @@
|
|
|
3938
3953
|
|
|
3939
3954
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
3940
3955
|
state.currentMessages = getPreferredMessages(selectedSession, data.output, false);
|
|
3956
|
+
if (selectedSession && selectedSession.sessionKind === "structured") {
|
|
3957
|
+
appendQueuedPlaceholders(state.currentMessages);
|
|
3958
|
+
}
|
|
3941
3959
|
|
|
3942
3960
|
if (state.terminal) {
|
|
3943
3961
|
syncTerminalBuffer(id, data.output || "", { mode: "replace" });
|
|
@@ -3959,7 +3977,8 @@
|
|
|
3959
3977
|
// Clear queued inputs from the previous session to prevent cross-session leaks
|
|
3960
3978
|
state.messageQueue = [];
|
|
3961
3979
|
state.pendingMessages = [];
|
|
3962
|
-
|
|
3980
|
+
state.structuredInputQueue = [];
|
|
3981
|
+
updateStructuredQueueCounter();
|
|
3963
3982
|
resetChatRenderCache();
|
|
3964
3983
|
state.currentMessages = [];
|
|
3965
3984
|
if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
|
|
@@ -5022,11 +5041,236 @@
|
|
|
5022
5041
|
return !!selectedSession && selectedSession.status === "running";
|
|
5023
5042
|
}
|
|
5024
5043
|
|
|
5044
|
+
// ── 跨会话排队 ──
|
|
5045
|
+
|
|
5046
|
+
var _queueLaunching = false; // 防止并发 launch
|
|
5047
|
+
|
|
5048
|
+
function hasAnyBusySession() {
|
|
5049
|
+
return state.sessions.some(function(s) {
|
|
5050
|
+
return s.status === "running" && !s.archived;
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
function enqueueCrossSessionMessage(text) {
|
|
5055
|
+
if (state.crossSessionQueue.length >= 10) {
|
|
5056
|
+
showToast("排队消息已满(最多 10 条),请等待当前会话完成。", "warning");
|
|
5057
|
+
return;
|
|
5058
|
+
}
|
|
5059
|
+
var id = "csq-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8);
|
|
5060
|
+
state.crossSessionQueue.push({
|
|
5061
|
+
id: id,
|
|
5062
|
+
text: text,
|
|
5063
|
+
cwd: getEffectiveCwd(),
|
|
5064
|
+
mode: state.chatMode || "managed",
|
|
5065
|
+
tool: getPreferredTool(),
|
|
5066
|
+
queuedAt: Date.now()
|
|
5067
|
+
});
|
|
5068
|
+
renderCrossSessionQueue();
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
function launchQueueItem(item) {
|
|
5072
|
+
if (_queueLaunching) return;
|
|
5073
|
+
_queueLaunching = true;
|
|
5074
|
+
fetch("/api/commands", {
|
|
5075
|
+
method: "POST",
|
|
5076
|
+
headers: { "Content-Type": "application/json" },
|
|
5077
|
+
credentials: "same-origin",
|
|
5078
|
+
body: JSON.stringify({
|
|
5079
|
+
command: item.tool,
|
|
5080
|
+
cwd: item.cwd,
|
|
5081
|
+
mode: item.mode,
|
|
5082
|
+
initialInput: item.text
|
|
5083
|
+
})
|
|
5084
|
+
})
|
|
5085
|
+
.then(function(res) { return res.json(); })
|
|
5086
|
+
.then(function(data) {
|
|
5087
|
+
_queueLaunching = false;
|
|
5088
|
+
if (data.error) {
|
|
5089
|
+
showToast(data.error, "error");
|
|
5090
|
+
// 失败回填队首,不丢消息
|
|
5091
|
+
state.crossSessionQueue.unshift(item);
|
|
5092
|
+
renderCrossSessionQueue();
|
|
5093
|
+
return null;
|
|
5094
|
+
}
|
|
5095
|
+
return activateSession(data);
|
|
5096
|
+
})
|
|
5097
|
+
.catch(function(error) {
|
|
5098
|
+
_queueLaunching = false;
|
|
5099
|
+
showToast((error && error.message) || "无法启动排队会话。", "error");
|
|
5100
|
+
state.crossSessionQueue.unshift(item);
|
|
5101
|
+
renderCrossSessionQueue();
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
|
|
5105
|
+
function sendQueueItemNow(queueId) {
|
|
5106
|
+
var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
|
|
5107
|
+
if (idx < 0) return;
|
|
5108
|
+
var item = state.crossSessionQueue.splice(idx, 1)[0];
|
|
5109
|
+
renderCrossSessionQueue();
|
|
5110
|
+
// 立即发送不受 _queueLaunching 限制
|
|
5111
|
+
fetch("/api/commands", {
|
|
5112
|
+
method: "POST",
|
|
5113
|
+
headers: { "Content-Type": "application/json" },
|
|
5114
|
+
credentials: "same-origin",
|
|
5115
|
+
body: JSON.stringify({
|
|
5116
|
+
command: item.tool,
|
|
5117
|
+
cwd: item.cwd,
|
|
5118
|
+
mode: item.mode,
|
|
5119
|
+
initialInput: item.text
|
|
5120
|
+
})
|
|
5121
|
+
})
|
|
5122
|
+
.then(function(res) { return res.json(); })
|
|
5123
|
+
.then(function(data) {
|
|
5124
|
+
if (data.error) {
|
|
5125
|
+
showToast(data.error, "error");
|
|
5126
|
+
return null;
|
|
5127
|
+
}
|
|
5128
|
+
return activateSession(data);
|
|
5129
|
+
})
|
|
5130
|
+
.catch(function(error) {
|
|
5131
|
+
showToast((error && error.message) || "无法启动排队会话。", "error");
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
function cancelQueueItem(queueId) {
|
|
5136
|
+
var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
|
|
5137
|
+
if (idx < 0) return;
|
|
5138
|
+
state.crossSessionQueue.splice(idx, 1);
|
|
5139
|
+
renderCrossSessionQueue();
|
|
5140
|
+
if (state.crossSessionQueue.length === 0) {
|
|
5141
|
+
showToast("排队已清空。", "info");
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
function flushCrossSessionQueue() {
|
|
5146
|
+
if (state.crossSessionQueue.length === 0) return;
|
|
5147
|
+
if (hasAnyBusySession()) return;
|
|
5148
|
+
if (_queueLaunching) return;
|
|
5149
|
+
var item = state.crossSessionQueue.shift();
|
|
5150
|
+
renderCrossSessionQueue();
|
|
5151
|
+
launchQueueItem(item);
|
|
5152
|
+
}
|
|
5153
|
+
|
|
5154
|
+
function formatQueueAge(queuedAt) {
|
|
5155
|
+
var sec = Math.floor((Date.now() - queuedAt) / 1000);
|
|
5156
|
+
if (sec < 60) return sec + "s";
|
|
5157
|
+
var min = Math.floor(sec / 60);
|
|
5158
|
+
if (min < 60) return min + "m";
|
|
5159
|
+
return Math.floor(min / 60) + "h";
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
function renderCrossSessionQueue() {
|
|
5163
|
+
var container = document.querySelector(".cross-session-queue");
|
|
5164
|
+
var inputPanel = document.querySelector(".input-panel");
|
|
5165
|
+
var statusBar = document.querySelector(".structured-status-bar");
|
|
5166
|
+
var composer = document.querySelector(".input-composer");
|
|
5167
|
+
var blankChat = document.getElementById("blank-chat");
|
|
5168
|
+
|
|
5169
|
+
if (state.crossSessionQueue.length === 0) {
|
|
5170
|
+
if (container) container.remove();
|
|
5171
|
+
return;
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5174
|
+
// Determine parent: input-panel (session view) or blank-chat (welcome view)
|
|
5175
|
+
var isInputPanelVisible = inputPanel && !inputPanel.classList.contains("hidden");
|
|
5176
|
+
var parent = isInputPanelVisible ? inputPanel : blankChat;
|
|
5177
|
+
// Insert above status bar if present, otherwise above composer
|
|
5178
|
+
var insertBefore = isInputPanelVisible ? (statusBar || composer) : null;
|
|
5179
|
+
|
|
5180
|
+
if (!parent) return;
|
|
5181
|
+
|
|
5182
|
+
// If container exists but is in the wrong parent, move it
|
|
5183
|
+
if (container && container.parentNode !== parent) {
|
|
5184
|
+
container.remove();
|
|
5185
|
+
container = null;
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
if (!container) {
|
|
5189
|
+
container = document.createElement("div");
|
|
5190
|
+
container.className = "cross-session-queue";
|
|
5191
|
+
if (insertBefore) {
|
|
5192
|
+
parent.insertBefore(container, insertBefore);
|
|
5193
|
+
} else {
|
|
5194
|
+
parent.appendChild(container);
|
|
5195
|
+
}
|
|
5196
|
+
} else if (isInputPanelVisible && insertBefore && container.nextSibling !== insertBefore) {
|
|
5197
|
+
// Ensure queue stays above status bar
|
|
5198
|
+
parent.insertBefore(container, insertBefore);
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
var total = state.crossSessionQueue.length;
|
|
5202
|
+
var items = state.crossSessionQueue.map(function(item, i) {
|
|
5203
|
+
var preview = item.text.length > 60 ? item.text.slice(0, 60) + "…" : item.text;
|
|
5204
|
+
var age = formatQueueAge(item.queuedAt);
|
|
5205
|
+
return '<div class="queue-item" data-queue-id="' + escapeHtml(item.id) + '">' +
|
|
5206
|
+
'<span class="queue-item-dot"></span>' +
|
|
5207
|
+
'<span class="queue-item-text" title="' + escapeHtml(item.text) + '">' + escapeHtml(preview) + '</span>' +
|
|
5208
|
+
'<span class="queue-item-age">' + age + '</span>' +
|
|
5209
|
+
'<button class="queue-item-send-now" data-queue-id="' + escapeHtml(item.id) + '" title="立即发送" type="button">发送</button>' +
|
|
5210
|
+
'<button class="queue-item-cancel" data-queue-id="' + escapeHtml(item.id) + '" title="取消" type="button">×</button>' +
|
|
5211
|
+
'</div>';
|
|
5212
|
+
}).join("");
|
|
5213
|
+
|
|
5214
|
+
var header = total > 1
|
|
5215
|
+
? '<div class="queue-header">' +
|
|
5216
|
+
'<span class="queue-header-label">排队 ' + total + ' 条</span>' +
|
|
5217
|
+
'<button class="queue-header-clear" id="queue-clear-all" type="button" title="清空排队">清空</button>' +
|
|
5218
|
+
'</div>'
|
|
5219
|
+
: '';
|
|
5220
|
+
|
|
5221
|
+
container.innerHTML = header + items;
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
// 定时刷新排队项的等待时间 + 尝试 flush
|
|
5225
|
+
setInterval(function() {
|
|
5226
|
+
if (state.crossSessionQueue.length > 0) {
|
|
5227
|
+
// 只更新 age 文本,不重建整个 DOM
|
|
5228
|
+
var ages = document.querySelectorAll(".queue-item-age");
|
|
5229
|
+
state.crossSessionQueue.forEach(function(item, i) {
|
|
5230
|
+
if (ages[i]) ages[i].textContent = formatQueueAge(item.queuedAt);
|
|
5231
|
+
});
|
|
5232
|
+
// 尝试 flush 作为保底(防止 ended 事件 flush 失败)
|
|
5233
|
+
flushCrossSessionQueue();
|
|
5234
|
+
}
|
|
5235
|
+
}, 5000);
|
|
5236
|
+
|
|
5237
|
+
// Delegate click events for cross-session queue items
|
|
5238
|
+
document.addEventListener("click", function(e) {
|
|
5239
|
+
if (e.target.closest("#queue-clear-all")) {
|
|
5240
|
+
e.preventDefault();
|
|
5241
|
+
state.crossSessionQueue = [];
|
|
5242
|
+
renderCrossSessionQueue();
|
|
5243
|
+
showToast("排队已清空。", "info");
|
|
5244
|
+
return;
|
|
5245
|
+
}
|
|
5246
|
+
var sendNow = e.target.closest(".queue-item-send-now");
|
|
5247
|
+
if (sendNow) {
|
|
5248
|
+
e.preventDefault();
|
|
5249
|
+
sendQueueItemNow(sendNow.dataset.queueId);
|
|
5250
|
+
return;
|
|
5251
|
+
}
|
|
5252
|
+
var cancel = e.target.closest(".queue-item-cancel");
|
|
5253
|
+
if (cancel) {
|
|
5254
|
+
e.preventDefault();
|
|
5255
|
+
cancelQueueItem(cancel.dataset.queueId);
|
|
5256
|
+
return;
|
|
5257
|
+
}
|
|
5258
|
+
});
|
|
5259
|
+
|
|
5025
5260
|
// Send message from the welcome screen input
|
|
5026
5261
|
function welcomeInputSend() {
|
|
5027
5262
|
var welcomeInput = document.getElementById("welcome-input");
|
|
5028
5263
|
var value = welcomeInput ? welcomeInput.value.trim() : "";
|
|
5029
5264
|
if (!value) return;
|
|
5265
|
+
|
|
5266
|
+
// Cross-session queue: if any session is busy, queue instead of creating
|
|
5267
|
+
if (hasAnyBusySession()) {
|
|
5268
|
+
welcomeInput.value = "";
|
|
5269
|
+
enqueueCrossSessionMessage(value);
|
|
5270
|
+
showToast("已排队,将在当前会话完成后自动发送。", "info");
|
|
5271
|
+
return;
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5030
5274
|
// Clear todo progress bar at the start of a new session
|
|
5031
5275
|
var todoEl = document.getElementById("todo-progress");
|
|
5032
5276
|
if (todoEl) todoEl.classList.add("hidden");
|
|
@@ -5092,7 +5336,14 @@
|
|
|
5092
5336
|
return;
|
|
5093
5337
|
}
|
|
5094
5338
|
|
|
5095
|
-
// No selected session, create a new one
|
|
5339
|
+
// No selected session, create a new one (or queue if busy)
|
|
5340
|
+
if (value && hasAnyBusySession()) {
|
|
5341
|
+
if (inputBox) inputBox.value = "";
|
|
5342
|
+
if (welcomeInput) welcomeInput.value = "";
|
|
5343
|
+
enqueueCrossSessionMessage(value);
|
|
5344
|
+
showToast("已排队,将在当前会话完成后自动发送。", "info");
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5096
5347
|
var mode = state.chatMode || "managed";
|
|
5097
5348
|
var defaultCwd = getEffectiveCwd();
|
|
5098
5349
|
var preferredTool = getPreferredTool();
|
|
@@ -5252,10 +5503,27 @@
|
|
|
5252
5503
|
return Promise.resolve();
|
|
5253
5504
|
}
|
|
5254
5505
|
if (session.structuredState && session.structuredState.inFlight && session.status === "running") {
|
|
5255
|
-
//
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5506
|
+
// Queue the message for sending after current processing completes
|
|
5507
|
+
if (state.structuredInputQueue.length >= 10) {
|
|
5508
|
+
showToast("排队消息已满(最多 10 条),请等待当前消息处理完成。", "warning");
|
|
5509
|
+
return Promise.resolve();
|
|
5510
|
+
}
|
|
5511
|
+
state.structuredInputQueue.push(input);
|
|
5512
|
+
if (inputBox) {
|
|
5513
|
+
inputBox.value = "";
|
|
5514
|
+
autoResizeInput(inputBox);
|
|
5515
|
+
}
|
|
5516
|
+
setDraftValue("");
|
|
5517
|
+
// Show the queued message in chat view with a "queued" marker
|
|
5518
|
+
var queuedTurn = { role: "user", content: [{ type: "text", text: input, __queued: true }] };
|
|
5519
|
+
var curMsgs = Array.isArray(state.currentMessages) ? state.currentMessages.slice() : [];
|
|
5520
|
+
curMsgs.push(queuedTurn);
|
|
5521
|
+
state.currentMessages = curMsgs;
|
|
5522
|
+
// Also update session.messages so the queued turn survives WS updates
|
|
5523
|
+
session.messages = curMsgs;
|
|
5524
|
+
renderChat(true);
|
|
5525
|
+
showToast("已排队(第 " + state.structuredInputQueue.length + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
5526
|
+
updateStructuredQueueCounter();
|
|
5259
5527
|
return Promise.resolve();
|
|
5260
5528
|
}
|
|
5261
5529
|
|
|
@@ -5263,8 +5531,14 @@
|
|
|
5263
5531
|
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
5264
5532
|
var thinkingTurn = { role: "assistant", content: [{ type: "text", text: "", __processing: true }] };
|
|
5265
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
|
+
});
|
|
5266
5538
|
userMsgs.push(userTurn);
|
|
5267
5539
|
userMsgs.push(thinkingTurn);
|
|
5540
|
+
// Re-append remaining queued messages after the current send
|
|
5541
|
+
appendQueuedPlaceholders(userMsgs);
|
|
5268
5542
|
session.messages = userMsgs;
|
|
5269
5543
|
state.currentMessages = userMsgs;
|
|
5270
5544
|
// Mark inFlight optimistically to prevent double-send via WS updates
|
|
@@ -5276,9 +5550,7 @@
|
|
|
5276
5550
|
inputBox.value = "";
|
|
5277
5551
|
autoResizeInput(inputBox);
|
|
5278
5552
|
}
|
|
5279
|
-
//
|
|
5280
|
-
var sendBtnEl = document.getElementById("send-input-button");
|
|
5281
|
-
if (sendBtnEl) sendBtnEl.disabled = true;
|
|
5553
|
+
// Keep send button enabled so user can queue more messages
|
|
5282
5554
|
updateInputHint("思考中…");
|
|
5283
5555
|
setDraftValue("");
|
|
5284
5556
|
renderChat(true);
|
|
@@ -5298,17 +5570,28 @@
|
|
|
5298
5570
|
updateSessionSnapshot(snapshot);
|
|
5299
5571
|
if (snapshot.messages && snapshot.messages.length > 0) {
|
|
5300
5572
|
state.currentMessages = snapshot.messages;
|
|
5573
|
+
// Re-append queued user messages
|
|
5574
|
+
appendQueuedPlaceholders(state.currentMessages);
|
|
5301
5575
|
}
|
|
5302
5576
|
renderChat(true);
|
|
5303
5577
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
5304
5578
|
}
|
|
5305
5579
|
})
|
|
5306
5580
|
.catch(function(error) {
|
|
5307
|
-
|
|
5308
|
-
if (
|
|
5581
|
+
// Reset inFlight so user can send again
|
|
5582
|
+
if (session.structuredState) {
|
|
5583
|
+
session.structuredState.inFlight = false;
|
|
5584
|
+
}
|
|
5585
|
+
// Clear remaining queued messages since the session is likely broken
|
|
5586
|
+
if (state.structuredInputQueue.length > 0) {
|
|
5587
|
+
var dropped = state.structuredInputQueue.length;
|
|
5588
|
+
state.structuredInputQueue = [];
|
|
5589
|
+
updateStructuredQueueCounter();
|
|
5590
|
+
showToast("发送失败,已清空 " + dropped + " 条排队消息。", "error");
|
|
5591
|
+
} else {
|
|
5592
|
+
showToast((error && error.message) || "无法发送结构化消息。", "error");
|
|
5593
|
+
}
|
|
5309
5594
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
5310
|
-
showToast((error && error.message) || "无法发送结构化消息。", "error");
|
|
5311
|
-
throw error;
|
|
5312
5595
|
});
|
|
5313
5596
|
}
|
|
5314
5597
|
|
|
@@ -5317,6 +5600,63 @@
|
|
|
5317
5600
|
if (hint) hint.textContent = text;
|
|
5318
5601
|
}
|
|
5319
5602
|
|
|
5603
|
+
function updateStructuredQueueCounter() {
|
|
5604
|
+
var counter = document.getElementById("queue-counter");
|
|
5605
|
+
var count = state.structuredInputQueue.length;
|
|
5606
|
+
if (counter) {
|
|
5607
|
+
counter.textContent = "队列: " + count;
|
|
5608
|
+
if (count > 0) {
|
|
5609
|
+
counter.classList.remove("hidden");
|
|
5610
|
+
} else {
|
|
5611
|
+
counter.classList.add("hidden");
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
// Append queued user message placeholders to currentMessages so they
|
|
5617
|
+
// remain visible across WS updates and re-renders.
|
|
5618
|
+
function appendQueuedPlaceholders(messages) {
|
|
5619
|
+
if (state.structuredInputQueue.length === 0) return messages;
|
|
5620
|
+
for (var qi = 0; qi < state.structuredInputQueue.length; qi++) {
|
|
5621
|
+
messages.push({ role: "user", content: [{ type: "text", text: state.structuredInputQueue[qi], __queued: true }] });
|
|
5622
|
+
}
|
|
5623
|
+
return messages;
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
function flushStructuredInputQueue() {
|
|
5627
|
+
if (state.structuredInputQueue.length === 0) return;
|
|
5628
|
+
var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
5629
|
+
if (!session || session.sessionKind !== "structured") {
|
|
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();
|
|
5637
|
+
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
|
+
}
|
|
5659
|
+
|
|
5320
5660
|
function getInputErrorMessage(error) {
|
|
5321
5661
|
if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
|
|
5322
5662
|
return "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
|
|
@@ -5398,12 +5738,10 @@
|
|
|
5398
5738
|
function queueDirectInput(input, shortcutKey) {
|
|
5399
5739
|
if (!input || !state.selectedId) return Promise.resolve();
|
|
5400
5740
|
state.messageQueue.push(input);
|
|
5401
|
-
updateQueueCounter();
|
|
5402
5741
|
state.inputQueue = state.inputQueue.then(function() {
|
|
5403
5742
|
return postInput(input, shortcutKey).finally(function() {
|
|
5404
5743
|
var idx = state.messageQueue.indexOf(input);
|
|
5405
5744
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
5406
|
-
updateQueueCounter();
|
|
5407
5745
|
scheduleMobileDomUpdate();
|
|
5408
5746
|
});
|
|
5409
5747
|
});
|
|
@@ -7412,6 +7750,10 @@
|
|
|
7412
7750
|
updateSessionSnapshot(snapshot);
|
|
7413
7751
|
if (msg.sessionId === state.selectedId) {
|
|
7414
7752
|
state.currentMessages = getPreferredMessages(snapshot, msg.data.output, false);
|
|
7753
|
+
// Re-append queued user messages that haven't been sent yet
|
|
7754
|
+
if (msg.data.sessionKind === 'structured') {
|
|
7755
|
+
appendQueuedPlaceholders(state.currentMessages);
|
|
7756
|
+
}
|
|
7415
7757
|
// Structured session with inFlight: keep __processing placeholder
|
|
7416
7758
|
// so the loading indicator stays visible until assistant content arrives
|
|
7417
7759
|
if (msg.data.sessionKind === 'structured') {
|
|
@@ -7471,10 +7813,7 @@
|
|
|
7471
7813
|
}
|
|
7472
7814
|
updateSessionSnapshot(endedSnapshot);
|
|
7473
7815
|
|
|
7474
|
-
// Re-enable send button when structured session finishes
|
|
7475
7816
|
if (msg.sessionId === state.selectedId) {
|
|
7476
|
-
var endedSendBtn = document.getElementById("send-input-button");
|
|
7477
|
-
if (endedSendBtn) endedSendBtn.disabled = false;
|
|
7478
7817
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
7479
7818
|
// Trigger status bar completion animation
|
|
7480
7819
|
scheduleChatRender(true);
|
|
@@ -7506,16 +7845,32 @@
|
|
|
7506
7845
|
});
|
|
7507
7846
|
}
|
|
7508
7847
|
|
|
7509
|
-
// Clear stale queued inputs
|
|
7510
|
-
//
|
|
7511
|
-
//
|
|
7848
|
+
// Clear stale queued inputs for PTY sessions.
|
|
7849
|
+
// For structured sessions, each "ended" means one turn completed (not
|
|
7850
|
+
// the session terminated), so we must NOT clear the structured queue —
|
|
7851
|
+
// instead, flush the next queued message.
|
|
7512
7852
|
state.messageQueue = [];
|
|
7513
7853
|
state.pendingMessages = [];
|
|
7514
7854
|
|
|
7855
|
+
var endedSessionObj = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
7856
|
+
var isStructuredEnded = endedSessionObj && endedSessionObj.sessionKind === "structured";
|
|
7857
|
+
|
|
7858
|
+
if (isStructuredEnded && msg.sessionId === state.selectedId &&
|
|
7859
|
+
state.structuredInputQueue.length > 0) {
|
|
7860
|
+
// Structured session turn completed — flush next queued message
|
|
7861
|
+
setTimeout(flushStructuredInputQueue, 50);
|
|
7862
|
+
} else if (!isStructuredEnded) {
|
|
7863
|
+
// PTY session ended — clear structured queue too
|
|
7864
|
+
state.structuredInputQueue = [];
|
|
7865
|
+
updateStructuredQueueCounter();
|
|
7866
|
+
}
|
|
7867
|
+
|
|
7515
7868
|
// Disable terminal interactive mode immediately so the terminal stops
|
|
7516
7869
|
// capturing keystrokes before loadSessions() completes.
|
|
7517
7870
|
if (msg.sessionId === state.selectedId) {
|
|
7518
|
-
|
|
7871
|
+
if (!isStructuredEnded) {
|
|
7872
|
+
setTerminalInteractive(false);
|
|
7873
|
+
}
|
|
7519
7874
|
state.currentTask = null;
|
|
7520
7875
|
updateTaskDisplay();
|
|
7521
7876
|
}
|
|
@@ -7526,9 +7881,14 @@
|
|
|
7526
7881
|
updateShellChrome();
|
|
7527
7882
|
}
|
|
7528
7883
|
|
|
7529
|
-
loadSessions()
|
|
7884
|
+
loadSessions().then(function() {
|
|
7885
|
+
// After sessions list is refreshed, try to flush cross-session queue
|
|
7886
|
+
flushCrossSessionQueue();
|
|
7887
|
+
});
|
|
7530
7888
|
if (msg.sessionId === state.selectedId) {
|
|
7531
|
-
|
|
7889
|
+
if (!isStructuredEnded) {
|
|
7890
|
+
loadOutput(msg.sessionId);
|
|
7891
|
+
}
|
|
7532
7892
|
}
|
|
7533
7893
|
break;
|
|
7534
7894
|
}
|
|
@@ -7625,6 +7985,11 @@
|
|
|
7625
7985
|
// Re-render chat when structured session inFlight state changes
|
|
7626
7986
|
if (statusUpdate.structuredState) {
|
|
7627
7987
|
scheduleChatRender();
|
|
7988
|
+
// Flush queued structured messages when inFlight clears
|
|
7989
|
+
if (!statusUpdate.structuredState.inFlight) {
|
|
7990
|
+
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
7991
|
+
setTimeout(flushStructuredInputQueue, 50);
|
|
7992
|
+
}
|
|
7628
7993
|
}
|
|
7629
7994
|
}
|
|
7630
7995
|
}
|
|
@@ -7896,6 +8261,9 @@
|
|
|
7896
8261
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
7897
8262
|
if (selectedSession) {
|
|
7898
8263
|
state.currentMessages = getPreferredMessages(selectedSession, selectedSession.output, true);
|
|
8264
|
+
if (selectedSession.sessionKind === "structured") {
|
|
8265
|
+
appendQueuedPlaceholders(state.currentMessages);
|
|
8266
|
+
}
|
|
7899
8267
|
}
|
|
7900
8268
|
renderChat();
|
|
7901
8269
|
}, 30);
|
|
@@ -8375,53 +8743,8 @@
|
|
|
8375
8743
|
list.innerHTML = html;
|
|
8376
8744
|
}
|
|
8377
8745
|
|
|
8378
|
-
// Extract recent important actions for key points summary
|
|
8379
|
-
var recentActions = [];
|
|
8380
|
-
var actionTools = ["Write", "Edit", "Bash", "WebFetch", "WebSearch"];
|
|
8381
|
-
var msgCount = messages.length;
|
|
8382
|
-
for (var ai = 0; ai < msgCount && recentActions.length < 5; ai++) {
|
|
8383
|
-
var m = messages[ai];
|
|
8384
|
-
if (!m.content || !Array.isArray(m.content)) continue;
|
|
8385
|
-
for (var bi = 0; bi < m.content.length && recentActions.length < 5; bi++) {
|
|
8386
|
-
var blk = m.content[bi];
|
|
8387
|
-
if (blk.type !== "tool_use") continue;
|
|
8388
|
-
var toolName = blk.name || "";
|
|
8389
|
-
if (actionTools.indexOf(toolName) === -1) continue;
|
|
8390
|
-
var desc = blk.description || generateInputSummary(toolName, blk.input) || toolName;
|
|
8391
|
-
if (desc && desc.length > 50) desc = desc.slice(0, 47) + "...";
|
|
8392
|
-
var icon = getToolIcon(toolName);
|
|
8393
|
-
recentActions.push({ icon: icon, text: desc });
|
|
8394
|
-
}
|
|
8395
|
-
}
|
|
8396
|
-
|
|
8397
|
-
var actionsEl = document.getElementById("recent-actions");
|
|
8398
|
-
if (actionsEl) {
|
|
8399
|
-
if (recentActions.length > 0) {
|
|
8400
|
-
var actionsHtml = '<div class="recent-actions-label">最近操作</div>';
|
|
8401
|
-
actionsHtml += '<div class="recent-actions-list">';
|
|
8402
|
-
for (var ri = 0; ri < recentActions.length; ri++) {
|
|
8403
|
-
var a = recentActions[ri];
|
|
8404
|
-
actionsHtml += '<span class="recent-action-pill">' + a.icon + ' ' + escapeHtml(a.text) + '</span>';
|
|
8405
|
-
}
|
|
8406
|
-
actionsHtml += '</div>';
|
|
8407
|
-
actionsEl.innerHTML = actionsHtml;
|
|
8408
|
-
} else {
|
|
8409
|
-
actionsEl.innerHTML = '';
|
|
8410
|
-
}
|
|
8411
|
-
}
|
|
8412
8746
|
}
|
|
8413
8747
|
|
|
8414
|
-
function updateQueueCounter() {
|
|
8415
|
-
var counter = document.getElementById("queue-counter");
|
|
8416
|
-
if (!counter) return;
|
|
8417
|
-
var count = state.messageQueue.length;
|
|
8418
|
-
if (count > 0) {
|
|
8419
|
-
counter.textContent = "队列: " + count;
|
|
8420
|
-
counter.classList.remove("hidden");
|
|
8421
|
-
} else {
|
|
8422
|
-
counter.classList.add("hidden");
|
|
8423
|
-
}
|
|
8424
|
-
}
|
|
8425
8748
|
|
|
8426
8749
|
function attachCopyHandler(el) {
|
|
8427
8750
|
el.querySelectorAll(".code-copy").forEach(function(btn) {
|
|
@@ -8944,6 +9267,67 @@
|
|
|
8944
9267
|
return messages;
|
|
8945
9268
|
}
|
|
8946
9269
|
|
|
9270
|
+
// ── 像素风猫咪头像 ──
|
|
9271
|
+
var PIXEL_AVATAR = (function() {
|
|
9272
|
+
var _ = "transparent";
|
|
9273
|
+
function buildSvg(grid, size) {
|
|
9274
|
+
var s = size || 3;
|
|
9275
|
+
var w = grid[0].length * s;
|
|
9276
|
+
var h = grid.length * s;
|
|
9277
|
+
var rects = "";
|
|
9278
|
+
for (var y = 0; y < grid.length; y++) {
|
|
9279
|
+
for (var x = 0; x < grid[y].length; x++) {
|
|
9280
|
+
if (grid[y][x] !== _) {
|
|
9281
|
+
rects += '<rect x="' + (x * s) + '" y="' + (y * s) + '" width="' + s + '" height="' + s + '" fill="' + grid[y][x] + '"/>';
|
|
9282
|
+
}
|
|
9283
|
+
}
|
|
9284
|
+
}
|
|
9285
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + w + ' ' + h + '" class="pixel-avatar-svg">' + rects + '</svg>';
|
|
9286
|
+
}
|
|
9287
|
+
// 加菲猫 (勤劳初二 / AI) — 橙色系
|
|
9288
|
+
var o = "#F0923A", d = "#C46A1A", w = "#FFFFFF", k = "#2D2D2D", p = "#F28B9A", n = "#E87D5A";
|
|
9289
|
+
var garfield = [
|
|
9290
|
+
[_,d,_,_,_,_,_,_,d,_],
|
|
9291
|
+
[d,o,d,_,_,_,_,d,o,d],
|
|
9292
|
+
[d,o,o,o,o,o,o,o,o,d],
|
|
9293
|
+
[o,o,w,k,o,o,w,k,o,o],
|
|
9294
|
+
[o,o,w,w,o,o,w,w,o,o],
|
|
9295
|
+
[o,o,o,o,p,p,o,o,o,o],
|
|
9296
|
+
[o,d,o,n,o,o,n,o,d,o],
|
|
9297
|
+
[_,o,o,o,o,o,o,o,o,_],
|
|
9298
|
+
[_,_,o,d,o,o,d,o,_,_],
|
|
9299
|
+
[_,_,_,o,_,_,o,_,_,_],
|
|
9300
|
+
];
|
|
9301
|
+
// 美短 (赛博虎妞 / 用户) — 灰色系
|
|
9302
|
+
var g = "#9EAAB8", dg = "#6B7B8D", lg = "#C5CED8", gn = "#7EC88B";
|
|
9303
|
+
var shorthair = [
|
|
9304
|
+
[_,dg,_,_,_,_,_,_,dg,_],
|
|
9305
|
+
[dg,g,dg,_,_,_,_,dg,g,dg],
|
|
9306
|
+
[dg,g,g,g,g,g,g,g,g,dg],
|
|
9307
|
+
[g,g,w,gn,g,g,w,gn,g,g],
|
|
9308
|
+
[g,g,w,w,g,g,w,w,g,g],
|
|
9309
|
+
[g,g,g,g,p,p,g,g,g,g],
|
|
9310
|
+
[g,dg,g,lg,g,g,lg,g,dg,g],
|
|
9311
|
+
[_,g,g,g,g,g,g,g,g,_],
|
|
9312
|
+
[_,_,g,dg,g,g,dg,g,_,_],
|
|
9313
|
+
[_,_,_,g,_,_,g,_,_,_],
|
|
9314
|
+
];
|
|
9315
|
+
return {
|
|
9316
|
+
assistant: buildSvg(garfield),
|
|
9317
|
+
user: buildSvg(shorthair)
|
|
9318
|
+
};
|
|
9319
|
+
})();
|
|
9320
|
+
|
|
9321
|
+
function chatAvatar(role) {
|
|
9322
|
+
var isUser = role === "user";
|
|
9323
|
+
var svg = isUser ? PIXEL_AVATAR.user : PIXEL_AVATAR.assistant;
|
|
9324
|
+
var name = isUser ? "赛博虎妞" : "勤劳初二";
|
|
9325
|
+
return '<div class="chat-message-avatar ' + role + '">' +
|
|
9326
|
+
'<div class="pixel-avatar">' + svg + '</div>' +
|
|
9327
|
+
'<span class="avatar-name">' + name + '</span>' +
|
|
9328
|
+
'</div>';
|
|
9329
|
+
}
|
|
9330
|
+
|
|
8947
9331
|
function renderChatMessage(msg, roundUsage) {
|
|
8948
9332
|
// Thinking card (deep thought) — from PTY parsing
|
|
8949
9333
|
if (msg.role === "thinking") {
|
|
@@ -8972,7 +9356,7 @@
|
|
|
8972
9356
|
}
|
|
8973
9357
|
|
|
8974
9358
|
// Legacy string content (from PTY parsing)
|
|
8975
|
-
var avatar = msg.role
|
|
9359
|
+
var avatar = chatAvatar(msg.role);
|
|
8976
9360
|
var bubbleContent = msg.role === "assistant" ? renderMarkdown(msg.content) : escapeHtml(msg.content);
|
|
8977
9361
|
return '<div class="chat-message ' + msg.role + '">' +
|
|
8978
9362
|
avatar +
|
|
@@ -9117,7 +9501,10 @@
|
|
|
9117
9501
|
|
|
9118
9502
|
function renderStructuredMessage(msg, roundUsage) {
|
|
9119
9503
|
var role = msg.role;
|
|
9120
|
-
var avatar = role
|
|
9504
|
+
var avatar = chatAvatar(role);
|
|
9505
|
+
|
|
9506
|
+
// Check if this is a queued user message
|
|
9507
|
+
var isQueued = role === "user" && msg.content && msg.content.some(function(b) { return b.__queued; });
|
|
9121
9508
|
|
|
9122
9509
|
if (!msg.content || msg.content.length === 0) {
|
|
9123
9510
|
if (role === "assistant") {
|
|
@@ -9154,10 +9541,12 @@
|
|
|
9154
9541
|
}
|
|
9155
9542
|
|
|
9156
9543
|
var usageHtml = "";
|
|
9544
|
+
var queuedClass = isQueued ? " queued" : "";
|
|
9545
|
+
var queuedBadge = isQueued ? '<span class="queued-badge">排队中</span>' : "";
|
|
9157
9546
|
|
|
9158
|
-
return '<div class="chat-message ' + role + '">' +
|
|
9547
|
+
return '<div class="chat-message ' + role + queuedClass + '">' +
|
|
9159
9548
|
avatar +
|
|
9160
|
-
'<div class="chat-message-content">' + blocksHtml + '</div>' +
|
|
9549
|
+
'<div class="chat-message-content">' + blocksHtml + queuedBadge + '</div>' +
|
|
9161
9550
|
usageHtml +
|
|
9162
9551
|
'</div>';
|
|
9163
9552
|
}
|
|
@@ -9481,27 +9870,29 @@
|
|
|
9481
9870
|
if (toolName === "AskUserQuestion" && block.input && block.input.questions) {
|
|
9482
9871
|
var questions = block.input.questions;
|
|
9483
9872
|
if (questions && questions.length > 0) {
|
|
9484
|
-
var
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
'<
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9873
|
+
var questionsHtml = "";
|
|
9874
|
+
questions.forEach(function(question, qIdx) {
|
|
9875
|
+
var questionText = question.question ? '<div class="ask-user-title">' + escapeHtml(question.question) + '</div>' : "";
|
|
9876
|
+
var optionsHtml = "";
|
|
9877
|
+
if (question.options && question.options.length > 0) {
|
|
9878
|
+
optionsHtml = '<div class="ask-user-options">';
|
|
9879
|
+
question.options.forEach(function(opt, idx) {
|
|
9880
|
+
var label = opt.label ? escapeHtml(opt.label) : "选项 " + (idx + 1);
|
|
9881
|
+
optionsHtml += '<button class="ask-user-option" data-option-index="' + idx + '" data-question-index="' + qIdx + '" data-option-label="' + escapeHtml(label) + '" onclick="__askOption(this)">' +
|
|
9882
|
+
'<div class="ask-user-option-label">' + label + '</div>' +
|
|
9883
|
+
'</button>';
|
|
9884
|
+
});
|
|
9885
|
+
optionsHtml += '</div>';
|
|
9886
|
+
}
|
|
9887
|
+
questionsHtml += '<div class="ask-user-question-group">' + questionText + optionsHtml + '</div>';
|
|
9888
|
+
});
|
|
9497
9889
|
return '<div class="tool-use-card ask-user" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
9498
9890
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
9499
9891
|
'<span class="tool-use-icon">?</span>' +
|
|
9500
9892
|
'<span class="tool-use-name">提问</span>' +
|
|
9501
9893
|
'</div>' +
|
|
9502
9894
|
'<div class="tool-use-body ask-user-body">' +
|
|
9503
|
-
|
|
9504
|
-
optionsHtml +
|
|
9895
|
+
questionsHtml +
|
|
9505
9896
|
'</div>' +
|
|
9506
9897
|
'</div>';
|
|
9507
9898
|
}
|
|
@@ -1162,6 +1162,17 @@
|
|
|
1162
1162
|
letter-spacing: -0.01em;
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
|
+
.session-title {
|
|
1166
|
+
font-weight: 600;
|
|
1167
|
+
font-size: 0.8125rem;
|
|
1168
|
+
white-space: nowrap;
|
|
1169
|
+
overflow: hidden;
|
|
1170
|
+
text-overflow: ellipsis;
|
|
1171
|
+
color: var(--text-primary);
|
|
1172
|
+
letter-spacing: -0.01em;
|
|
1173
|
+
line-height: 1.3;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1165
1176
|
/* ===== 会话元信息 ===== */
|
|
1166
1177
|
.session-meta {
|
|
1167
1178
|
display: flex;
|
|
@@ -2292,16 +2303,51 @@
|
|
|
2292
2303
|
}
|
|
2293
2304
|
|
|
2294
2305
|
/* ===== 消息头像 ===== */
|
|
2295
|
-
.chat-message
|
|
2296
|
-
display:
|
|
2306
|
+
.chat-message-avatar {
|
|
2307
|
+
display: flex;
|
|
2308
|
+
align-items: center;
|
|
2309
|
+
gap: 6px;
|
|
2310
|
+
padding: 0 2px 4px 2px;
|
|
2297
2311
|
}
|
|
2298
2312
|
|
|
2299
|
-
.chat-message.assistant
|
|
2300
|
-
|
|
2313
|
+
.chat-message-avatar.assistant {
|
|
2314
|
+
flex-direction: row;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
.chat-message-avatar.user {
|
|
2318
|
+
flex-direction: row-reverse;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
.pixel-avatar {
|
|
2322
|
+
width: 24px;
|
|
2323
|
+
height: 24px;
|
|
2324
|
+
flex-shrink: 0;
|
|
2325
|
+
border-radius: 6px;
|
|
2326
|
+
overflow: hidden;
|
|
2327
|
+
background: var(--bg-tertiary);
|
|
2328
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
.pixel-avatar-svg {
|
|
2332
|
+
display: block;
|
|
2333
|
+
width: 100%;
|
|
2334
|
+
height: 100%;
|
|
2335
|
+
image-rendering: pixelated;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
.avatar-name {
|
|
2339
|
+
font-size: 0.7rem;
|
|
2301
2340
|
font-weight: 600;
|
|
2302
|
-
color: var(--accent);
|
|
2303
|
-
padding: 0 2px 4px 2px;
|
|
2304
2341
|
letter-spacing: 0.03em;
|
|
2342
|
+
line-height: 1;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.chat-message.assistant .avatar-name {
|
|
2346
|
+
color: var(--accent);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
.chat-message.user .avatar-name {
|
|
2350
|
+
color: var(--text-tertiary);
|
|
2305
2351
|
}
|
|
2306
2352
|
|
|
2307
2353
|
/* ===== 消息气泡 ===== */
|
|
@@ -2720,6 +2766,12 @@
|
|
|
2720
2766
|
.ask-user-body {
|
|
2721
2767
|
padding: 10px 12px;
|
|
2722
2768
|
}
|
|
2769
|
+
.ask-user-question-group {
|
|
2770
|
+
margin-bottom: 10px;
|
|
2771
|
+
}
|
|
2772
|
+
.ask-user-question-group:last-child {
|
|
2773
|
+
margin-bottom: 0;
|
|
2774
|
+
}
|
|
2723
2775
|
.ask-user-title {
|
|
2724
2776
|
font-size: 0.875rem;
|
|
2725
2777
|
font-weight: 500;
|
|
@@ -3724,6 +3776,22 @@
|
|
|
3724
3776
|
50% { opacity: 0.7; }
|
|
3725
3777
|
}
|
|
3726
3778
|
|
|
3779
|
+
/* Queued message in chat view */
|
|
3780
|
+
.chat-message.user.queued {
|
|
3781
|
+
opacity: 0.6;
|
|
3782
|
+
}
|
|
3783
|
+
.queued-badge {
|
|
3784
|
+
display: inline-block;
|
|
3785
|
+
font-size: 0.625rem;
|
|
3786
|
+
color: var(--accent);
|
|
3787
|
+
background: var(--accent-muted);
|
|
3788
|
+
padding: 1px 6px;
|
|
3789
|
+
border-radius: 8px;
|
|
3790
|
+
font-weight: 600;
|
|
3791
|
+
margin-top: 4px;
|
|
3792
|
+
animation: queuePulse 1.5s ease-in-out infinite;
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3727
3795
|
.input-hint {
|
|
3728
3796
|
font-size: 0.5625rem;
|
|
3729
3797
|
color: var(--text-muted);
|
|
@@ -3904,39 +3972,6 @@
|
|
|
3904
3972
|
.todo-item-icon.active { color: var(--accent); }
|
|
3905
3973
|
.todo-item-icon.done { color: #4a7a4f; }
|
|
3906
3974
|
|
|
3907
|
-
/* Recent actions key points section */
|
|
3908
|
-
.recent-actions {
|
|
3909
|
-
margin-top: 8px;
|
|
3910
|
-
padding-top: 8px;
|
|
3911
|
-
border-top: 1px solid var(--border-subtle);
|
|
3912
|
-
}
|
|
3913
|
-
.recent-actions-label {
|
|
3914
|
-
font-size: 0.6875rem;
|
|
3915
|
-
font-weight: 600;
|
|
3916
|
-
color: var(--text-muted);
|
|
3917
|
-
text-transform: uppercase;
|
|
3918
|
-
letter-spacing: 0.03em;
|
|
3919
|
-
margin-bottom: 6px;
|
|
3920
|
-
}
|
|
3921
|
-
.recent-actions-list {
|
|
3922
|
-
display: flex;
|
|
3923
|
-
flex-wrap: wrap;
|
|
3924
|
-
gap: 4px;
|
|
3925
|
-
}
|
|
3926
|
-
.recent-action-pill {
|
|
3927
|
-
display: inline-flex;
|
|
3928
|
-
align-items: center;
|
|
3929
|
-
gap: 3px;
|
|
3930
|
-
padding: 3px 8px;
|
|
3931
|
-
font-size: 0.6875rem;
|
|
3932
|
-
color: var(--text-secondary);
|
|
3933
|
-
background: rgba(79, 122, 88, 0.08);
|
|
3934
|
-
border-radius: 10px;
|
|
3935
|
-
white-space: nowrap;
|
|
3936
|
-
max-width: 220px;
|
|
3937
|
-
overflow: hidden;
|
|
3938
|
-
text-overflow: ellipsis;
|
|
3939
|
-
}
|
|
3940
3975
|
|
|
3941
3976
|
@keyframes spin {
|
|
3942
3977
|
to { transform: rotate(360deg); }
|
|
@@ -7737,4 +7772,125 @@
|
|
|
7737
7772
|
100% { opacity: 0; max-height: 0; margin: 0; }
|
|
7738
7773
|
}
|
|
7739
7774
|
|
|
7775
|
+
/* ── 跨会话排队消息 ── */
|
|
7776
|
+
.cross-session-queue {
|
|
7777
|
+
display: flex;
|
|
7778
|
+
flex-direction: column;
|
|
7779
|
+
gap: 2px;
|
|
7780
|
+
margin: 0 8px 4px 8px;
|
|
7781
|
+
}
|
|
7782
|
+
|
|
7783
|
+
.queue-header {
|
|
7784
|
+
display: flex;
|
|
7785
|
+
align-items: center;
|
|
7786
|
+
gap: 5px;
|
|
7787
|
+
padding: 0 4px;
|
|
7788
|
+
font-size: 0.6rem;
|
|
7789
|
+
color: var(--text-muted);
|
|
7790
|
+
}
|
|
7791
|
+
|
|
7792
|
+
.queue-header-label {
|
|
7793
|
+
flex: 1;
|
|
7794
|
+
opacity: 0.6;
|
|
7795
|
+
}
|
|
7796
|
+
|
|
7797
|
+
.queue-header-clear {
|
|
7798
|
+
border: none;
|
|
7799
|
+
background: transparent;
|
|
7800
|
+
color: var(--text-muted);
|
|
7801
|
+
cursor: pointer;
|
|
7802
|
+
font-size: 0.5625rem;
|
|
7803
|
+
padding: 1px 4px;
|
|
7804
|
+
border-radius: 3px;
|
|
7805
|
+
opacity: 0.5;
|
|
7806
|
+
transition: color 0.15s, background 0.15s, opacity 0.15s;
|
|
7807
|
+
}
|
|
7808
|
+
|
|
7809
|
+
.queue-header-clear:hover {
|
|
7810
|
+
color: var(--error, #e55);
|
|
7811
|
+
background: rgba(229, 85, 85, 0.12);
|
|
7812
|
+
opacity: 1;
|
|
7813
|
+
}
|
|
7814
|
+
|
|
7815
|
+
.queue-item {
|
|
7816
|
+
display: flex;
|
|
7817
|
+
align-items: center;
|
|
7818
|
+
gap: 6px;
|
|
7819
|
+
padding: 4px 8px;
|
|
7820
|
+
font-size: 0.6875rem;
|
|
7821
|
+
color: var(--text-muted);
|
|
7822
|
+
opacity: 0.7;
|
|
7823
|
+
transition: opacity 0.15s ease;
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7826
|
+
.queue-item:hover {
|
|
7827
|
+
opacity: 1;
|
|
7828
|
+
}
|
|
7829
|
+
|
|
7830
|
+
.queue-item-dot {
|
|
7831
|
+
flex-shrink: 0;
|
|
7832
|
+
width: 4px;
|
|
7833
|
+
height: 4px;
|
|
7834
|
+
border-radius: 50%;
|
|
7835
|
+
background: rgba(var(--accent-rgb, 197, 101, 61), 0.6);
|
|
7836
|
+
animation: statusDotPulse 1.2s ease-in-out infinite;
|
|
7837
|
+
}
|
|
7838
|
+
|
|
7839
|
+
.queue-item-text {
|
|
7840
|
+
flex: 1;
|
|
7841
|
+
min-width: 0;
|
|
7842
|
+
overflow: hidden;
|
|
7843
|
+
text-overflow: ellipsis;
|
|
7844
|
+
white-space: nowrap;
|
|
7845
|
+
font-size: 0.6875rem;
|
|
7846
|
+
color: var(--text-muted);
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7849
|
+
.queue-item-age {
|
|
7850
|
+
flex-shrink: 0;
|
|
7851
|
+
font-size: 0.5625rem;
|
|
7852
|
+
color: var(--text-muted);
|
|
7853
|
+
font-variant-numeric: tabular-nums;
|
|
7854
|
+
opacity: 0.5;
|
|
7855
|
+
}
|
|
7856
|
+
|
|
7857
|
+
.queue-item-send-now {
|
|
7858
|
+
flex-shrink: 0;
|
|
7859
|
+
border: 1px solid rgba(var(--accent-rgb, 197, 101, 61), 0.3);
|
|
7860
|
+
background: transparent;
|
|
7861
|
+
color: var(--accent, #c5653d);
|
|
7862
|
+
cursor: pointer;
|
|
7863
|
+
padding: 1px 8px;
|
|
7864
|
+
border-radius: 4px;
|
|
7865
|
+
font-size: 0.6rem;
|
|
7866
|
+
line-height: 1.4;
|
|
7867
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
.queue-item-send-now:hover {
|
|
7871
|
+
color: var(--accent, #c5653d);
|
|
7872
|
+
background: rgba(var(--accent-rgb, 197, 101, 61), 0.12);
|
|
7873
|
+
border-color: rgba(var(--accent-rgb, 197, 101, 61), 0.5);
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
.queue-item-cancel {
|
|
7877
|
+
flex-shrink: 0;
|
|
7878
|
+
border: none;
|
|
7879
|
+
background: transparent;
|
|
7880
|
+
color: var(--text-muted);
|
|
7881
|
+
cursor: pointer;
|
|
7882
|
+
padding: 1px 4px;
|
|
7883
|
+
border-radius: 3px;
|
|
7884
|
+
font-size: 0.6875rem;
|
|
7885
|
+
line-height: 1;
|
|
7886
|
+
opacity: 0.4;
|
|
7887
|
+
transition: color 0.15s, background 0.15s, opacity 0.15s;
|
|
7888
|
+
}
|
|
7889
|
+
|
|
7890
|
+
.queue-item-cancel:hover {
|
|
7891
|
+
color: var(--error, #e55);
|
|
7892
|
+
background: rgba(229, 85, 85, 0.12);
|
|
7893
|
+
opacity: 1;
|
|
7894
|
+
}
|
|
7895
|
+
|
|
7740
7896
|
/* 结束标记 */
|