@co0ontty/wand 1.6.0 → 1.6.2
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/config.js +23 -0
- package/dist/server.js +137 -14
- package/dist/structured-session-manager.js +8 -6
- package/dist/types.d.ts +9 -0
- package/dist/web-ui/content/scripts.js +468 -67
- package/dist/web-ui/content/styles.css +150 -0
- 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/config.js
CHANGED
|
@@ -79,6 +79,28 @@ export async function saveConfig(configPath, config) {
|
|
|
79
79
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
80
80
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
81
81
|
}
|
|
82
|
+
function normalizeStructuredChatPersona(input) {
|
|
83
|
+
if (!input || typeof input !== "object")
|
|
84
|
+
return undefined;
|
|
85
|
+
const normalizeRole = (roleInput) => {
|
|
86
|
+
if (!roleInput || typeof roleInput !== "object")
|
|
87
|
+
return undefined;
|
|
88
|
+
const role = roleInput;
|
|
89
|
+
const normalized = {
|
|
90
|
+
name: typeof role.name === "string" ? role.name.trim() : undefined,
|
|
91
|
+
avatar: typeof role.avatar === "string" ? role.avatar.trim() : undefined,
|
|
92
|
+
};
|
|
93
|
+
if (!normalized.name && !normalized.avatar)
|
|
94
|
+
return undefined;
|
|
95
|
+
return normalized;
|
|
96
|
+
};
|
|
97
|
+
const personaInput = input;
|
|
98
|
+
const user = normalizeRole(personaInput.user);
|
|
99
|
+
const assistant = normalizeRole(personaInput.assistant);
|
|
100
|
+
if (!user && !assistant)
|
|
101
|
+
return undefined;
|
|
102
|
+
return { user, assistant };
|
|
103
|
+
}
|
|
82
104
|
function mergeWithDefaults(input) {
|
|
83
105
|
const defaults = defaultConfig();
|
|
84
106
|
return {
|
|
@@ -108,6 +130,7 @@ function mergeWithDefaults(input) {
|
|
|
108
130
|
mode: isExecutionMode(preset.mode) ? preset.mode : undefined
|
|
109
131
|
}))
|
|
110
132
|
: defaults.commandPresets,
|
|
133
|
+
structuredChatPersona: normalizeStructuredChatPersona(input.structuredChatPersona),
|
|
111
134
|
language: typeof input.language === "string" ? input.language.trim() : defaults.language,
|
|
112
135
|
};
|
|
113
136
|
}
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,19 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
11
|
+
import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
12
|
+
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
13
|
+
import { ensureCertificates } from "./cert.js";
|
|
14
|
+
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
15
|
+
import { ProcessManager } from "./process-manager.js";
|
|
16
|
+
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
17
|
+
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
18
|
+
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
19
|
+
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
20
|
+
import { renderApp } from "./web-ui/index.js";
|
|
21
|
+
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
22
|
+
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
23
|
+
import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
11
24
|
const execAsync = promisify(exec);
|
|
12
25
|
const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
13
26
|
const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
|
|
@@ -52,19 +65,83 @@ function compareSemver(a, b) {
|
|
|
52
65
|
}
|
|
53
66
|
return 0;
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
function isExternalAvatarSource(value) {
|
|
69
|
+
return /^(https?:|data:)/i.test(value);
|
|
70
|
+
}
|
|
71
|
+
function normalizePersonaName(value) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
return undefined;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
return trimmed || undefined;
|
|
76
|
+
}
|
|
77
|
+
function normalizePersonaAvatar(value) {
|
|
78
|
+
if (typeof value !== "string")
|
|
79
|
+
return undefined;
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return trimmed || undefined;
|
|
82
|
+
}
|
|
83
|
+
function resolveStructuredChatPersona(config) {
|
|
84
|
+
const persona = config.structuredChatPersona;
|
|
85
|
+
if (!persona)
|
|
86
|
+
return undefined;
|
|
87
|
+
const userName = normalizePersonaName(persona.user?.name);
|
|
88
|
+
const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
|
|
89
|
+
const assistantName = normalizePersonaName(persona.assistant?.name);
|
|
90
|
+
const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
|
|
91
|
+
if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
user: userName || userAvatar ? { name: userName, avatar: userAvatar } : undefined,
|
|
96
|
+
assistant: assistantName || assistantAvatar ? { name: assistantName, avatar: assistantAvatar } : undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resolveStructuredChatAvatarPath(configPath, config, role) {
|
|
100
|
+
const avatar = role === "user"
|
|
101
|
+
? config.structuredChatPersona?.user?.avatar
|
|
102
|
+
: config.structuredChatPersona?.assistant?.avatar;
|
|
103
|
+
if (!avatar || isExternalAvatarSource(avatar)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const configDir = resolveConfigDir(configPath);
|
|
107
|
+
return path.isAbsolute(avatar) ? avatar : path.resolve(configDir, avatar);
|
|
108
|
+
}
|
|
109
|
+
async function buildStructuredChatPersonaPayload(configPath, config) {
|
|
110
|
+
const persona = resolveStructuredChatPersona(config);
|
|
111
|
+
if (!persona)
|
|
112
|
+
return undefined;
|
|
113
|
+
const buildRole = async (role) => {
|
|
114
|
+
const roleConfig = role === "user" ? persona.user : persona.assistant;
|
|
115
|
+
if (!roleConfig)
|
|
116
|
+
return undefined;
|
|
117
|
+
let avatar = roleConfig.avatar;
|
|
118
|
+
if (avatar && !isExternalAvatarSource(avatar)) {
|
|
119
|
+
const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
|
|
120
|
+
if (!resolvedPath) {
|
|
121
|
+
avatar = undefined;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
try {
|
|
125
|
+
const fileStat = await stat(resolvedPath);
|
|
126
|
+
avatar = fileStat.isFile() ? `/api/structured-chat-avatar/${role}` : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
avatar = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!roleConfig.name && !avatar)
|
|
134
|
+
return undefined;
|
|
135
|
+
return {
|
|
136
|
+
name: roleConfig.name,
|
|
137
|
+
avatar,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const [user, assistant] = await Promise.all([buildRole("user"), buildRole("assistant")]);
|
|
141
|
+
if (!user && !assistant)
|
|
142
|
+
return undefined;
|
|
143
|
+
return { user, assistant };
|
|
144
|
+
}
|
|
68
145
|
// ── Git helpers ──
|
|
69
146
|
async function isGitRepo(dirPath) {
|
|
70
147
|
try {
|
|
@@ -266,6 +343,50 @@ export async function startServer(config, configPath) {
|
|
|
266
343
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
267
344
|
res.type("html").send(renderApp(configPath));
|
|
268
345
|
});
|
|
346
|
+
app.get("/api/structured-chat-avatar/:role", async (req, res) => {
|
|
347
|
+
const role = req.params.role === "user" || req.params.role === "assistant"
|
|
348
|
+
? req.params.role
|
|
349
|
+
: null;
|
|
350
|
+
if (!role) {
|
|
351
|
+
res.status(404).end();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
|
|
355
|
+
if (!resolvedPath) {
|
|
356
|
+
res.status(404).end();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const fileStat = await stat(resolvedPath);
|
|
361
|
+
if (!fileStat.isFile()) {
|
|
362
|
+
res.status(404).end();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
366
|
+
const contentType = ext === ".svg"
|
|
367
|
+
? "image/svg+xml"
|
|
368
|
+
: ext === ".png"
|
|
369
|
+
? "image/png"
|
|
370
|
+
: ext === ".jpg" || ext === ".jpeg"
|
|
371
|
+
? "image/jpeg"
|
|
372
|
+
: ext === ".webp"
|
|
373
|
+
? "image/webp"
|
|
374
|
+
: ext === ".gif"
|
|
375
|
+
? "image/gif"
|
|
376
|
+
: ext === ".avif"
|
|
377
|
+
? "image/avif"
|
|
378
|
+
: null;
|
|
379
|
+
if (!contentType) {
|
|
380
|
+
res.status(415).json({ error: "不支持的头像格式。" });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
384
|
+
res.type(contentType).sendFile(resolvedPath);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
res.status(404).end();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
269
390
|
app.get("/manifest.json", (_req, res) => {
|
|
270
391
|
res.setHeader("Content-Type", "application/manifest+json");
|
|
271
392
|
res.send(generatePwaManifest());
|
|
@@ -328,7 +449,8 @@ export async function startServer(config, configPath) {
|
|
|
328
449
|
});
|
|
329
450
|
app.use("/api", requireAuth);
|
|
330
451
|
// ── Config & Session info ──
|
|
331
|
-
app.get("/api/config", (_req, res) => {
|
|
452
|
+
app.get("/api/config", async (_req, res) => {
|
|
453
|
+
const structuredChatPersona = await buildStructuredChatPersonaPayload(configPath, config);
|
|
332
454
|
res.json({
|
|
333
455
|
host: config.host,
|
|
334
456
|
port: config.port,
|
|
@@ -336,6 +458,7 @@ export async function startServer(config, configPath) {
|
|
|
336
458
|
defaultCwd: config.defaultCwd,
|
|
337
459
|
commandPresets: config.commandPresets,
|
|
338
460
|
structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
|
|
461
|
+
structuredChatPersona,
|
|
339
462
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
340
463
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
341
464
|
currentVersion: PKG_VERSION,
|
|
@@ -345,19 +345,21 @@ export class StructuredSessionManager {
|
|
|
345
345
|
// ---------------------------------------------------------------------------
|
|
346
346
|
// CLI argument construction
|
|
347
347
|
// ---------------------------------------------------------------------------
|
|
348
|
-
buildPermissionArgs(mode) {
|
|
348
|
+
buildPermissionArgs(mode, autoApprove) {
|
|
349
|
+
const shouldBypass = autoApprove || mode === "full-access" || mode === "managed";
|
|
350
|
+
const shouldAcceptEdits = mode === "auto-edit";
|
|
349
351
|
if (!isRunningAsRoot()) {
|
|
350
|
-
if (
|
|
352
|
+
if (shouldBypass) {
|
|
351
353
|
return ["--permission-mode", "bypassPermissions"];
|
|
352
354
|
}
|
|
353
|
-
if (
|
|
355
|
+
if (shouldAcceptEdits) {
|
|
354
356
|
return ["--permission-mode", "acceptEdits"];
|
|
355
357
|
}
|
|
356
358
|
return [];
|
|
357
359
|
}
|
|
358
360
|
// Root: Claude CLI refuses bypassPermissions.
|
|
359
361
|
// acceptEdits auto-approves within CWD; --allowedTools extends to all paths.
|
|
360
|
-
if (
|
|
362
|
+
if (shouldBypass || shouldAcceptEdits) {
|
|
361
363
|
return [
|
|
362
364
|
"--permission-mode", "acceptEdits",
|
|
363
365
|
"--allowedTools", "Bash", "Edit", "Write", "Read", "Glob", "Grep",
|
|
@@ -384,8 +386,8 @@ export class StructuredSessionManager {
|
|
|
384
386
|
return new Promise((resolve, reject) => {
|
|
385
387
|
const args = ["-p", "--verbose", "--output-format", "stream-json"];
|
|
386
388
|
console.log("[WAND] runClaudeStreaming sessionId:", sessionId, "mode:", session.mode, "claudeSessionId:", session.claudeSessionId);
|
|
387
|
-
// Add permission args based on mode
|
|
388
|
-
const permArgs = this.buildPermissionArgs(session.mode);
|
|
389
|
+
// Add permission args based on mode + autoApprovePermissions toggle
|
|
390
|
+
const permArgs = this.buildPermissionArgs(session.mode, session.autoApprovePermissions ?? false);
|
|
389
391
|
args.push(...permArgs);
|
|
390
392
|
// In managed mode, append autonomous system prompt
|
|
391
393
|
if (session.mode === "managed") {
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,14 @@ export interface CommandPreset {
|
|
|
38
38
|
command: string;
|
|
39
39
|
mode?: ExecutionMode;
|
|
40
40
|
}
|
|
41
|
+
export interface StructuredChatPersonaRoleConfig {
|
|
42
|
+
name?: string;
|
|
43
|
+
avatar?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface StructuredChatPersonaConfig {
|
|
46
|
+
user?: StructuredChatPersonaRoleConfig;
|
|
47
|
+
assistant?: StructuredChatPersonaRoleConfig;
|
|
48
|
+
}
|
|
41
49
|
export interface WandConfig {
|
|
42
50
|
host: string;
|
|
43
51
|
port: number;
|
|
@@ -50,6 +58,7 @@ export interface WandConfig {
|
|
|
50
58
|
startupCommands: string[];
|
|
51
59
|
allowedCommandPrefixes: string[];
|
|
52
60
|
commandPresets: CommandPreset[];
|
|
61
|
+
structuredChatPersona?: StructuredChatPersonaConfig;
|
|
53
62
|
/** Max total size (bytes) for shortcut interaction logs per session (default: 10 MB). Set 0 to disable logging. */
|
|
54
63
|
shortcutLogMaxBytes?: number;
|
|
55
64
|
/** Preferred response language for Claude (e.g. "中文", "English"). Empty string means no override. */
|
|
@@ -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,
|
|
@@ -1996,23 +1998,23 @@
|
|
|
1996
1998
|
var optionLabel = btnEl.dataset.optionLabel;
|
|
1997
1999
|
if (optionLabel && state.selectedId) {
|
|
1998
2000
|
btnEl.classList.add("selected");
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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
|
+
});
|
|
2006
2008
|
var sentDiv = document.createElement("div");
|
|
2007
2009
|
sentDiv.className = "ask-user-answer-sent";
|
|
2008
|
-
sentDiv.innerHTML = "
|
|
2009
|
-
|
|
2010
|
+
sentDiv.innerHTML = "\u2713 \u5df2\u53d1\u9001: " + escapeHtml(optionLabel);
|
|
2011
|
+
questionGroup.appendChild(sentDiv);
|
|
2010
2012
|
}
|
|
2011
2013
|
fetch("/api/sessions/" + state.selectedId + "/input", {
|
|
2012
2014
|
method: "POST",
|
|
2013
2015
|
headers: { "Content-Type": "application/json" },
|
|
2014
2016
|
credentials: "same-origin",
|
|
2015
|
-
body: JSON.stringify({ input: optionLabel + "
|
|
2017
|
+
body: JSON.stringify({ input: optionLabel + "\n", view: state.currentView })
|
|
2016
2018
|
}).catch(function(err) {
|
|
2017
2019
|
console.error("[wand] Error sending answer:", err);
|
|
2018
2020
|
});
|
|
@@ -3680,6 +3682,11 @@
|
|
|
3680
3682
|
reconcileInteractiveState();
|
|
3681
3683
|
updateTaskDisplay();
|
|
3682
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
|
+
}
|
|
3683
3690
|
}
|
|
3684
3691
|
|
|
3685
3692
|
function subscribeToSession(sessionId) {
|
|
@@ -3819,6 +3826,11 @@
|
|
|
3819
3826
|
loadOutput(state.selectedId);
|
|
3820
3827
|
}
|
|
3821
3828
|
}
|
|
3829
|
+
|
|
3830
|
+
// Try to flush cross-session queue on every session list refresh
|
|
3831
|
+
if (state.crossSessionQueue.length > 0) {
|
|
3832
|
+
flushCrossSessionQueue();
|
|
3833
|
+
}
|
|
3822
3834
|
})
|
|
3823
3835
|
.catch(function(e) {
|
|
3824
3836
|
console.error("[wand] loadSessions failed:", e);
|
|
@@ -3831,6 +3843,8 @@
|
|
|
3831
3843
|
if (listEl) listEl.innerHTML = renderSessions();
|
|
3832
3844
|
if (countEl) countEl.textContent = String(state.sessions.length);
|
|
3833
3845
|
updateShellChrome();
|
|
3846
|
+
// Re-render cross-session queue (container may have been destroyed by DOM rebuild)
|
|
3847
|
+
if (state.crossSessionQueue.length > 0) renderCrossSessionQueue();
|
|
3834
3848
|
}
|
|
3835
3849
|
|
|
3836
3850
|
function updateShellChrome() {
|
|
@@ -3939,6 +3953,9 @@
|
|
|
3939
3953
|
|
|
3940
3954
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
3941
3955
|
state.currentMessages = getPreferredMessages(selectedSession, data.output, false);
|
|
3956
|
+
if (selectedSession && selectedSession.sessionKind === "structured") {
|
|
3957
|
+
appendQueuedPlaceholders(state.currentMessages);
|
|
3958
|
+
}
|
|
3942
3959
|
|
|
3943
3960
|
if (state.terminal) {
|
|
3944
3961
|
syncTerminalBuffer(id, data.output || "", { mode: "replace" });
|
|
@@ -3960,7 +3977,8 @@
|
|
|
3960
3977
|
// Clear queued inputs from the previous session to prevent cross-session leaks
|
|
3961
3978
|
state.messageQueue = [];
|
|
3962
3979
|
state.pendingMessages = [];
|
|
3963
|
-
|
|
3980
|
+
state.structuredInputQueue = [];
|
|
3981
|
+
updateStructuredQueueCounter();
|
|
3964
3982
|
resetChatRenderCache();
|
|
3965
3983
|
state.currentMessages = [];
|
|
3966
3984
|
if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
|
|
@@ -5023,11 +5041,236 @@
|
|
|
5023
5041
|
return !!selectedSession && selectedSession.status === "running";
|
|
5024
5042
|
}
|
|
5025
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
|
+
|
|
5026
5260
|
// Send message from the welcome screen input
|
|
5027
5261
|
function welcomeInputSend() {
|
|
5028
5262
|
var welcomeInput = document.getElementById("welcome-input");
|
|
5029
5263
|
var value = welcomeInput ? welcomeInput.value.trim() : "";
|
|
5030
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
|
+
|
|
5031
5274
|
// Clear todo progress bar at the start of a new session
|
|
5032
5275
|
var todoEl = document.getElementById("todo-progress");
|
|
5033
5276
|
if (todoEl) todoEl.classList.add("hidden");
|
|
@@ -5093,7 +5336,14 @@
|
|
|
5093
5336
|
return;
|
|
5094
5337
|
}
|
|
5095
5338
|
|
|
5096
|
-
// 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
|
+
}
|
|
5097
5347
|
var mode = state.chatMode || "managed";
|
|
5098
5348
|
var defaultCwd = getEffectiveCwd();
|
|
5099
5349
|
var preferredTool = getPreferredTool();
|
|
@@ -5253,10 +5503,27 @@
|
|
|
5253
5503
|
return Promise.resolve();
|
|
5254
5504
|
}
|
|
5255
5505
|
if (session.structuredState && session.structuredState.inFlight && session.status === "running") {
|
|
5256
|
-
//
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
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();
|
|
5260
5527
|
return Promise.resolve();
|
|
5261
5528
|
}
|
|
5262
5529
|
|
|
@@ -5264,8 +5531,14 @@
|
|
|
5264
5531
|
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
5265
5532
|
var thinkingTurn = { role: "assistant", content: [{ type: "text", text: "", __processing: true }] };
|
|
5266
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
|
+
});
|
|
5267
5538
|
userMsgs.push(userTurn);
|
|
5268
5539
|
userMsgs.push(thinkingTurn);
|
|
5540
|
+
// Re-append remaining queued messages after the current send
|
|
5541
|
+
appendQueuedPlaceholders(userMsgs);
|
|
5269
5542
|
session.messages = userMsgs;
|
|
5270
5543
|
state.currentMessages = userMsgs;
|
|
5271
5544
|
// Mark inFlight optimistically to prevent double-send via WS updates
|
|
@@ -5277,9 +5550,7 @@
|
|
|
5277
5550
|
inputBox.value = "";
|
|
5278
5551
|
autoResizeInput(inputBox);
|
|
5279
5552
|
}
|
|
5280
|
-
//
|
|
5281
|
-
var sendBtnEl = document.getElementById("send-input-button");
|
|
5282
|
-
if (sendBtnEl) sendBtnEl.disabled = true;
|
|
5553
|
+
// Keep send button enabled so user can queue more messages
|
|
5283
5554
|
updateInputHint("思考中…");
|
|
5284
5555
|
setDraftValue("");
|
|
5285
5556
|
renderChat(true);
|
|
@@ -5299,17 +5570,28 @@
|
|
|
5299
5570
|
updateSessionSnapshot(snapshot);
|
|
5300
5571
|
if (snapshot.messages && snapshot.messages.length > 0) {
|
|
5301
5572
|
state.currentMessages = snapshot.messages;
|
|
5573
|
+
// Re-append queued user messages
|
|
5574
|
+
appendQueuedPlaceholders(state.currentMessages);
|
|
5302
5575
|
}
|
|
5303
5576
|
renderChat(true);
|
|
5304
5577
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
5305
5578
|
}
|
|
5306
5579
|
})
|
|
5307
5580
|
.catch(function(error) {
|
|
5308
|
-
|
|
5309
|
-
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
|
+
}
|
|
5310
5594
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
5311
|
-
showToast((error && error.message) || "无法发送结构化消息。", "error");
|
|
5312
|
-
throw error;
|
|
5313
5595
|
});
|
|
5314
5596
|
}
|
|
5315
5597
|
|
|
@@ -5318,6 +5600,63 @@
|
|
|
5318
5600
|
if (hint) hint.textContent = text;
|
|
5319
5601
|
}
|
|
5320
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
|
+
|
|
5321
5660
|
function getInputErrorMessage(error) {
|
|
5322
5661
|
if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
|
|
5323
5662
|
return "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
|
|
@@ -5399,12 +5738,10 @@
|
|
|
5399
5738
|
function queueDirectInput(input, shortcutKey) {
|
|
5400
5739
|
if (!input || !state.selectedId) return Promise.resolve();
|
|
5401
5740
|
state.messageQueue.push(input);
|
|
5402
|
-
updateQueueCounter();
|
|
5403
5741
|
state.inputQueue = state.inputQueue.then(function() {
|
|
5404
5742
|
return postInput(input, shortcutKey).finally(function() {
|
|
5405
5743
|
var idx = state.messageQueue.indexOf(input);
|
|
5406
5744
|
if (idx > -1) state.messageQueue.splice(idx, 1);
|
|
5407
|
-
updateQueueCounter();
|
|
5408
5745
|
scheduleMobileDomUpdate();
|
|
5409
5746
|
});
|
|
5410
5747
|
});
|
|
@@ -7413,6 +7750,10 @@
|
|
|
7413
7750
|
updateSessionSnapshot(snapshot);
|
|
7414
7751
|
if (msg.sessionId === state.selectedId) {
|
|
7415
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
|
+
}
|
|
7416
7757
|
// Structured session with inFlight: keep __processing placeholder
|
|
7417
7758
|
// so the loading indicator stays visible until assistant content arrives
|
|
7418
7759
|
if (msg.data.sessionKind === 'structured') {
|
|
@@ -7472,10 +7813,7 @@
|
|
|
7472
7813
|
}
|
|
7473
7814
|
updateSessionSnapshot(endedSnapshot);
|
|
7474
7815
|
|
|
7475
|
-
// Re-enable send button when structured session finishes
|
|
7476
7816
|
if (msg.sessionId === state.selectedId) {
|
|
7477
|
-
var endedSendBtn = document.getElementById("send-input-button");
|
|
7478
|
-
if (endedSendBtn) endedSendBtn.disabled = false;
|
|
7479
7817
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
7480
7818
|
// Trigger status bar completion animation
|
|
7481
7819
|
scheduleChatRender(true);
|
|
@@ -7507,16 +7845,32 @@
|
|
|
7507
7845
|
});
|
|
7508
7846
|
}
|
|
7509
7847
|
|
|
7510
|
-
// Clear stale queued inputs
|
|
7511
|
-
//
|
|
7512
|
-
//
|
|
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.
|
|
7513
7852
|
state.messageQueue = [];
|
|
7514
7853
|
state.pendingMessages = [];
|
|
7515
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
|
+
|
|
7516
7868
|
// Disable terminal interactive mode immediately so the terminal stops
|
|
7517
7869
|
// capturing keystrokes before loadSessions() completes.
|
|
7518
7870
|
if (msg.sessionId === state.selectedId) {
|
|
7519
|
-
|
|
7871
|
+
if (!isStructuredEnded) {
|
|
7872
|
+
setTerminalInteractive(false);
|
|
7873
|
+
}
|
|
7520
7874
|
state.currentTask = null;
|
|
7521
7875
|
updateTaskDisplay();
|
|
7522
7876
|
}
|
|
@@ -7527,9 +7881,14 @@
|
|
|
7527
7881
|
updateShellChrome();
|
|
7528
7882
|
}
|
|
7529
7883
|
|
|
7530
|
-
loadSessions()
|
|
7884
|
+
loadSessions().then(function() {
|
|
7885
|
+
// After sessions list is refreshed, try to flush cross-session queue
|
|
7886
|
+
flushCrossSessionQueue();
|
|
7887
|
+
});
|
|
7531
7888
|
if (msg.sessionId === state.selectedId) {
|
|
7532
|
-
|
|
7889
|
+
if (!isStructuredEnded) {
|
|
7890
|
+
loadOutput(msg.sessionId);
|
|
7891
|
+
}
|
|
7533
7892
|
}
|
|
7534
7893
|
break;
|
|
7535
7894
|
}
|
|
@@ -7626,6 +7985,11 @@
|
|
|
7626
7985
|
// Re-render chat when structured session inFlight state changes
|
|
7627
7986
|
if (statusUpdate.structuredState) {
|
|
7628
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
|
+
}
|
|
7629
7993
|
}
|
|
7630
7994
|
}
|
|
7631
7995
|
}
|
|
@@ -7897,6 +8261,9 @@
|
|
|
7897
8261
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
7898
8262
|
if (selectedSession) {
|
|
7899
8263
|
state.currentMessages = getPreferredMessages(selectedSession, selectedSession.output, true);
|
|
8264
|
+
if (selectedSession.sessionKind === "structured") {
|
|
8265
|
+
appendQueuedPlaceholders(state.currentMessages);
|
|
8266
|
+
}
|
|
7900
8267
|
}
|
|
7901
8268
|
renderChat();
|
|
7902
8269
|
}, 30);
|
|
@@ -8378,17 +8745,6 @@
|
|
|
8378
8745
|
|
|
8379
8746
|
}
|
|
8380
8747
|
|
|
8381
|
-
function updateQueueCounter() {
|
|
8382
|
-
var counter = document.getElementById("queue-counter");
|
|
8383
|
-
if (!counter) return;
|
|
8384
|
-
var count = state.messageQueue.length;
|
|
8385
|
-
if (count > 0) {
|
|
8386
|
-
counter.textContent = "队列: " + count;
|
|
8387
|
-
counter.classList.remove("hidden");
|
|
8388
|
-
} else {
|
|
8389
|
-
counter.classList.add("hidden");
|
|
8390
|
-
}
|
|
8391
|
-
}
|
|
8392
8748
|
|
|
8393
8749
|
function attachCopyHandler(el) {
|
|
8394
8750
|
el.querySelectorAll(".code-copy").forEach(function(btn) {
|
|
@@ -8962,13 +9318,51 @@
|
|
|
8962
9318
|
};
|
|
8963
9319
|
})();
|
|
8964
9320
|
|
|
9321
|
+
var DEFAULT_CHAT_PERSONA = {
|
|
9322
|
+
user: {
|
|
9323
|
+
name: "赛博虎妞",
|
|
9324
|
+
avatarSvg: PIXEL_AVATAR.user
|
|
9325
|
+
},
|
|
9326
|
+
assistant: {
|
|
9327
|
+
name: "勤劳初二",
|
|
9328
|
+
avatarSvg: PIXEL_AVATAR.assistant
|
|
9329
|
+
}
|
|
9330
|
+
};
|
|
9331
|
+
|
|
9332
|
+
function getStructuredChatPersona(role) {
|
|
9333
|
+
var configPersona = state.config && state.config.structuredChatPersona;
|
|
9334
|
+
var roleConfig = configPersona && configPersona[role] ? configPersona[role] : null;
|
|
9335
|
+
var defaults = DEFAULT_CHAT_PERSONA[role] || DEFAULT_CHAT_PERSONA.assistant;
|
|
9336
|
+
return {
|
|
9337
|
+
name: roleConfig && typeof roleConfig.name === "string" && roleConfig.name.trim()
|
|
9338
|
+
? roleConfig.name.trim()
|
|
9339
|
+
: defaults.name,
|
|
9340
|
+
avatar: roleConfig && typeof roleConfig.avatar === "string" && roleConfig.avatar.trim()
|
|
9341
|
+
? roleConfig.avatar.trim()
|
|
9342
|
+
: null,
|
|
9343
|
+
avatarSvg: defaults.avatarSvg
|
|
9344
|
+
};
|
|
9345
|
+
}
|
|
9346
|
+
|
|
9347
|
+
function renderAvatarFallback(svg) {
|
|
9348
|
+
return '<div class="pixel-avatar">' + svg + '</div>';
|
|
9349
|
+
}
|
|
9350
|
+
|
|
9351
|
+
function handleChatAvatarImageError(img, role) {
|
|
9352
|
+
if (!img || !img.parentNode) return;
|
|
9353
|
+
var persona = getStructuredChatPersona(role === "user" ? "user" : "assistant");
|
|
9354
|
+
img.outerHTML = renderAvatarFallback(persona.avatarSvg);
|
|
9355
|
+
}
|
|
9356
|
+
|
|
8965
9357
|
function chatAvatar(role) {
|
|
8966
|
-
var
|
|
8967
|
-
var
|
|
8968
|
-
var
|
|
9358
|
+
var personaRole = role === "user" ? "user" : "assistant";
|
|
9359
|
+
var persona = getStructuredChatPersona(personaRole);
|
|
9360
|
+
var avatarInner = persona.avatar
|
|
9361
|
+
? '<img class="pixel-avatar-image" src="' + escapeHtml(persona.avatar) + '" alt="' + escapeHtml(persona.name) + '" onerror="handleChatAvatarImageError(this, ' + JSON.stringify(personaRole) + ')" />'
|
|
9362
|
+
: renderAvatarFallback(persona.avatarSvg);
|
|
8969
9363
|
return '<div class="chat-message-avatar ' + role + '">' +
|
|
8970
|
-
|
|
8971
|
-
'<span class="avatar-name">' + name + '</span>' +
|
|
9364
|
+
avatarInner +
|
|
9365
|
+
'<span class="avatar-name">' + escapeHtml(persona.name) + '</span>' +
|
|
8972
9366
|
'</div>';
|
|
8973
9367
|
}
|
|
8974
9368
|
|
|
@@ -9147,6 +9541,9 @@
|
|
|
9147
9541
|
var role = msg.role;
|
|
9148
9542
|
var avatar = chatAvatar(role);
|
|
9149
9543
|
|
|
9544
|
+
// Check if this is a queued user message
|
|
9545
|
+
var isQueued = role === "user" && msg.content && msg.content.some(function(b) { return b.__queued; });
|
|
9546
|
+
|
|
9150
9547
|
if (!msg.content || msg.content.length === 0) {
|
|
9151
9548
|
if (role === "assistant") {
|
|
9152
9549
|
return '<div class="chat-message ' + role + '">' +
|
|
@@ -9182,10 +9579,12 @@
|
|
|
9182
9579
|
}
|
|
9183
9580
|
|
|
9184
9581
|
var usageHtml = "";
|
|
9582
|
+
var queuedClass = isQueued ? " queued" : "";
|
|
9583
|
+
var queuedBadge = isQueued ? '<span class="queued-badge">排队中</span>' : "";
|
|
9185
9584
|
|
|
9186
|
-
return '<div class="chat-message ' + role + '">' +
|
|
9585
|
+
return '<div class="chat-message ' + role + queuedClass + '">' +
|
|
9187
9586
|
avatar +
|
|
9188
|
-
'<div class="chat-message-content">' + blocksHtml + '</div>' +
|
|
9587
|
+
'<div class="chat-message-content">' + blocksHtml + queuedBadge + '</div>' +
|
|
9189
9588
|
usageHtml +
|
|
9190
9589
|
'</div>';
|
|
9191
9590
|
}
|
|
@@ -9509,27 +9908,29 @@
|
|
|
9509
9908
|
if (toolName === "AskUserQuestion" && block.input && block.input.questions) {
|
|
9510
9909
|
var questions = block.input.questions;
|
|
9511
9910
|
if (questions && questions.length > 0) {
|
|
9512
|
-
var
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
'<
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9911
|
+
var questionsHtml = "";
|
|
9912
|
+
questions.forEach(function(question, qIdx) {
|
|
9913
|
+
var questionText = question.question ? '<div class="ask-user-title">' + escapeHtml(question.question) + '</div>' : "";
|
|
9914
|
+
var optionsHtml = "";
|
|
9915
|
+
if (question.options && question.options.length > 0) {
|
|
9916
|
+
optionsHtml = '<div class="ask-user-options">';
|
|
9917
|
+
question.options.forEach(function(opt, idx) {
|
|
9918
|
+
var label = opt.label ? escapeHtml(opt.label) : "选项 " + (idx + 1);
|
|
9919
|
+
optionsHtml += '<button class="ask-user-option" data-option-index="' + idx + '" data-question-index="' + qIdx + '" data-option-label="' + escapeHtml(label) + '" onclick="__askOption(this)">' +
|
|
9920
|
+
'<div class="ask-user-option-label">' + label + '</div>' +
|
|
9921
|
+
'</button>';
|
|
9922
|
+
});
|
|
9923
|
+
optionsHtml += '</div>';
|
|
9924
|
+
}
|
|
9925
|
+
questionsHtml += '<div class="ask-user-question-group">' + questionText + optionsHtml + '</div>';
|
|
9926
|
+
});
|
|
9525
9927
|
return '<div class="tool-use-card ask-user" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
9526
9928
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
9527
9929
|
'<span class="tool-use-icon">?</span>' +
|
|
9528
9930
|
'<span class="tool-use-name">提问</span>' +
|
|
9529
9931
|
'</div>' +
|
|
9530
9932
|
'<div class="tool-use-body ask-user-body">' +
|
|
9531
|
-
|
|
9532
|
-
optionsHtml +
|
|
9933
|
+
questionsHtml +
|
|
9533
9934
|
'</div>' +
|
|
9534
9935
|
'</div>';
|
|
9535
9936
|
}
|
|
@@ -2328,6 +2328,13 @@
|
|
|
2328
2328
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
2329
2329
|
}
|
|
2330
2330
|
|
|
2331
|
+
.pixel-avatar-image {
|
|
2332
|
+
display: block;
|
|
2333
|
+
width: 100%;
|
|
2334
|
+
height: 100%;
|
|
2335
|
+
object-fit: cover;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2331
2338
|
.pixel-avatar-svg {
|
|
2332
2339
|
display: block;
|
|
2333
2340
|
width: 100%;
|
|
@@ -2766,6 +2773,12 @@
|
|
|
2766
2773
|
.ask-user-body {
|
|
2767
2774
|
padding: 10px 12px;
|
|
2768
2775
|
}
|
|
2776
|
+
.ask-user-question-group {
|
|
2777
|
+
margin-bottom: 10px;
|
|
2778
|
+
}
|
|
2779
|
+
.ask-user-question-group:last-child {
|
|
2780
|
+
margin-bottom: 0;
|
|
2781
|
+
}
|
|
2769
2782
|
.ask-user-title {
|
|
2770
2783
|
font-size: 0.875rem;
|
|
2771
2784
|
font-weight: 500;
|
|
@@ -3770,6 +3783,22 @@
|
|
|
3770
3783
|
50% { opacity: 0.7; }
|
|
3771
3784
|
}
|
|
3772
3785
|
|
|
3786
|
+
/* Queued message in chat view */
|
|
3787
|
+
.chat-message.user.queued {
|
|
3788
|
+
opacity: 0.6;
|
|
3789
|
+
}
|
|
3790
|
+
.queued-badge {
|
|
3791
|
+
display: inline-block;
|
|
3792
|
+
font-size: 0.625rem;
|
|
3793
|
+
color: var(--accent);
|
|
3794
|
+
background: var(--accent-muted);
|
|
3795
|
+
padding: 1px 6px;
|
|
3796
|
+
border-radius: 8px;
|
|
3797
|
+
font-weight: 600;
|
|
3798
|
+
margin-top: 4px;
|
|
3799
|
+
animation: queuePulse 1.5s ease-in-out infinite;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3773
3802
|
.input-hint {
|
|
3774
3803
|
font-size: 0.5625rem;
|
|
3775
3804
|
color: var(--text-muted);
|
|
@@ -7750,4 +7779,125 @@
|
|
|
7750
7779
|
100% { opacity: 0; max-height: 0; margin: 0; }
|
|
7751
7780
|
}
|
|
7752
7781
|
|
|
7782
|
+
/* ── 跨会话排队消息 ── */
|
|
7783
|
+
.cross-session-queue {
|
|
7784
|
+
display: flex;
|
|
7785
|
+
flex-direction: column;
|
|
7786
|
+
gap: 2px;
|
|
7787
|
+
margin: 0 8px 4px 8px;
|
|
7788
|
+
}
|
|
7789
|
+
|
|
7790
|
+
.queue-header {
|
|
7791
|
+
display: flex;
|
|
7792
|
+
align-items: center;
|
|
7793
|
+
gap: 5px;
|
|
7794
|
+
padding: 0 4px;
|
|
7795
|
+
font-size: 0.6rem;
|
|
7796
|
+
color: var(--text-muted);
|
|
7797
|
+
}
|
|
7798
|
+
|
|
7799
|
+
.queue-header-label {
|
|
7800
|
+
flex: 1;
|
|
7801
|
+
opacity: 0.6;
|
|
7802
|
+
}
|
|
7803
|
+
|
|
7804
|
+
.queue-header-clear {
|
|
7805
|
+
border: none;
|
|
7806
|
+
background: transparent;
|
|
7807
|
+
color: var(--text-muted);
|
|
7808
|
+
cursor: pointer;
|
|
7809
|
+
font-size: 0.5625rem;
|
|
7810
|
+
padding: 1px 4px;
|
|
7811
|
+
border-radius: 3px;
|
|
7812
|
+
opacity: 0.5;
|
|
7813
|
+
transition: color 0.15s, background 0.15s, opacity 0.15s;
|
|
7814
|
+
}
|
|
7815
|
+
|
|
7816
|
+
.queue-header-clear:hover {
|
|
7817
|
+
color: var(--error, #e55);
|
|
7818
|
+
background: rgba(229, 85, 85, 0.12);
|
|
7819
|
+
opacity: 1;
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
.queue-item {
|
|
7823
|
+
display: flex;
|
|
7824
|
+
align-items: center;
|
|
7825
|
+
gap: 6px;
|
|
7826
|
+
padding: 4px 8px;
|
|
7827
|
+
font-size: 0.6875rem;
|
|
7828
|
+
color: var(--text-muted);
|
|
7829
|
+
opacity: 0.7;
|
|
7830
|
+
transition: opacity 0.15s ease;
|
|
7831
|
+
}
|
|
7832
|
+
|
|
7833
|
+
.queue-item:hover {
|
|
7834
|
+
opacity: 1;
|
|
7835
|
+
}
|
|
7836
|
+
|
|
7837
|
+
.queue-item-dot {
|
|
7838
|
+
flex-shrink: 0;
|
|
7839
|
+
width: 4px;
|
|
7840
|
+
height: 4px;
|
|
7841
|
+
border-radius: 50%;
|
|
7842
|
+
background: rgba(var(--accent-rgb, 197, 101, 61), 0.6);
|
|
7843
|
+
animation: statusDotPulse 1.2s ease-in-out infinite;
|
|
7844
|
+
}
|
|
7845
|
+
|
|
7846
|
+
.queue-item-text {
|
|
7847
|
+
flex: 1;
|
|
7848
|
+
min-width: 0;
|
|
7849
|
+
overflow: hidden;
|
|
7850
|
+
text-overflow: ellipsis;
|
|
7851
|
+
white-space: nowrap;
|
|
7852
|
+
font-size: 0.6875rem;
|
|
7853
|
+
color: var(--text-muted);
|
|
7854
|
+
}
|
|
7855
|
+
|
|
7856
|
+
.queue-item-age {
|
|
7857
|
+
flex-shrink: 0;
|
|
7858
|
+
font-size: 0.5625rem;
|
|
7859
|
+
color: var(--text-muted);
|
|
7860
|
+
font-variant-numeric: tabular-nums;
|
|
7861
|
+
opacity: 0.5;
|
|
7862
|
+
}
|
|
7863
|
+
|
|
7864
|
+
.queue-item-send-now {
|
|
7865
|
+
flex-shrink: 0;
|
|
7866
|
+
border: 1px solid rgba(var(--accent-rgb, 197, 101, 61), 0.3);
|
|
7867
|
+
background: transparent;
|
|
7868
|
+
color: var(--accent, #c5653d);
|
|
7869
|
+
cursor: pointer;
|
|
7870
|
+
padding: 1px 8px;
|
|
7871
|
+
border-radius: 4px;
|
|
7872
|
+
font-size: 0.6rem;
|
|
7873
|
+
line-height: 1.4;
|
|
7874
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
7875
|
+
}
|
|
7876
|
+
|
|
7877
|
+
.queue-item-send-now:hover {
|
|
7878
|
+
color: var(--accent, #c5653d);
|
|
7879
|
+
background: rgba(var(--accent-rgb, 197, 101, 61), 0.12);
|
|
7880
|
+
border-color: rgba(var(--accent-rgb, 197, 101, 61), 0.5);
|
|
7881
|
+
}
|
|
7882
|
+
|
|
7883
|
+
.queue-item-cancel {
|
|
7884
|
+
flex-shrink: 0;
|
|
7885
|
+
border: none;
|
|
7886
|
+
background: transparent;
|
|
7887
|
+
color: var(--text-muted);
|
|
7888
|
+
cursor: pointer;
|
|
7889
|
+
padding: 1px 4px;
|
|
7890
|
+
border-radius: 3px;
|
|
7891
|
+
font-size: 0.6875rem;
|
|
7892
|
+
line-height: 1;
|
|
7893
|
+
opacity: 0.4;
|
|
7894
|
+
transition: color 0.15s, background 0.15s, opacity 0.15s;
|
|
7895
|
+
}
|
|
7896
|
+
|
|
7897
|
+
.queue-item-cancel:hover {
|
|
7898
|
+
color: var(--error, #e55);
|
|
7899
|
+
background: rgba(229, 85, 85, 0.12);
|
|
7900
|
+
opacity: 1;
|
|
7901
|
+
}
|
|
7902
|
+
|
|
7753
7903
|
/* 结束标记 */
|