@co0ontty/wand 1.2.0 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-pty-bridge.js +18 -6
- package/dist/process-manager.js +46 -196
- package/dist/pty-text-utils.d.ts +2 -0
- package/dist/pty-text-utils.js +20 -0
- package/dist/resume-policy.d.ts +80 -0
- package/dist/resume-policy.js +178 -0
- package/dist/server-session-routes.d.ts +6 -0
- package/dist/server-session-routes.js +359 -0
- package/dist/server.js +5 -328
- package/dist/web-ui/content/scripts.js +91 -45
- package/dist/web-ui/content/styles.css +27 -18
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -56,11 +56,11 @@ import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
|
56
56
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
57
57
|
import { ensureCertificates } from "./cert.js";
|
|
58
58
|
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
59
|
-
import { ProcessManager
|
|
59
|
+
import { ProcessManager } from "./process-manager.js";
|
|
60
|
+
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
61
|
+
import { registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
60
62
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
61
63
|
import { renderApp } from "./web-ui/index.js";
|
|
62
|
-
import { parseMessages } from "./message-parser.js";
|
|
63
|
-
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
64
64
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
65
65
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
66
66
|
import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
@@ -68,35 +68,6 @@ import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./mi
|
|
|
68
68
|
function getErrorMessage(error, fallback) {
|
|
69
69
|
return error instanceof Error ? error.message : fallback;
|
|
70
70
|
}
|
|
71
|
-
function getInputErrorResponse(error, sessionId) {
|
|
72
|
-
if (error instanceof SessionInputError) {
|
|
73
|
-
const statusCode = error.code === "SESSION_NOT_FOUND" ? 404 : 409;
|
|
74
|
-
return {
|
|
75
|
-
statusCode,
|
|
76
|
-
payload: {
|
|
77
|
-
error: error.message,
|
|
78
|
-
errorCode: error.code,
|
|
79
|
-
sessionId,
|
|
80
|
-
sessionStatus: error.sessionStatus ?? null,
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
statusCode: 400,
|
|
86
|
-
payload: {
|
|
87
|
-
error: getErrorMessage(error, "会话已结束,请启动新会话。"),
|
|
88
|
-
errorCode: "INPUT_SEND_FAILED",
|
|
89
|
-
sessionId,
|
|
90
|
-
sessionStatus: null,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function getInputDebugMeta(error) {
|
|
95
|
-
if (error instanceof Error) {
|
|
96
|
-
return { name: error.name, message: error.message, stack: error.stack };
|
|
97
|
-
}
|
|
98
|
-
return { error };
|
|
99
|
-
}
|
|
100
71
|
// ── Git helpers ──
|
|
101
72
|
async function isGitRepo(dirPath) {
|
|
102
73
|
try {
|
|
@@ -509,152 +480,8 @@ export async function startServer(config, configPath) {
|
|
|
509
480
|
updateInFlight = false;
|
|
510
481
|
}
|
|
511
482
|
});
|
|
512
|
-
app
|
|
513
|
-
|
|
514
|
-
});
|
|
515
|
-
app.get("/api/claude-history", (_req, res) => {
|
|
516
|
-
try {
|
|
517
|
-
const sessions = processes.listClaudeHistorySessions();
|
|
518
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
519
|
-
const filtered = hidden.size > 0
|
|
520
|
-
? sessions.filter((s) => !s.claudeSessionId || !hidden.has(s.claudeSessionId))
|
|
521
|
-
: sessions;
|
|
522
|
-
res.json(filtered);
|
|
523
|
-
}
|
|
524
|
-
catch (error) {
|
|
525
|
-
res.status(500).json({ error: getErrorMessage(error, "无法扫描 Claude 历史会话。") });
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
app.delete("/api/claude-history/:claudeSessionId", (req, res) => {
|
|
529
|
-
const claudeSessionId = req.params.claudeSessionId?.trim();
|
|
530
|
-
if (!claudeSessionId) {
|
|
531
|
-
res.status(400).json({ error: "会话 ID 不能为空。" });
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
const session = processes.listClaudeHistorySessions()
|
|
535
|
-
.find((s) => s.claudeSessionId === claudeSessionId);
|
|
536
|
-
if (session) {
|
|
537
|
-
processes.deleteClaudeHistoryFiles([{ claudeSessionId, cwd: session.cwd }]);
|
|
538
|
-
removeFromHiddenClaudeSessionIds(storage, [claudeSessionId]);
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
542
|
-
if (!hidden.has(claudeSessionId)) {
|
|
543
|
-
hidden.add(claudeSessionId);
|
|
544
|
-
saveHiddenClaudeSessionIds(storage, hidden);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
res.json({ ok: true });
|
|
548
|
-
});
|
|
549
|
-
app.delete("/api/claude-history", (req, res) => {
|
|
550
|
-
const cwd = typeof req.query.cwd === "string" ? req.query.cwd.trim() : "";
|
|
551
|
-
if (!cwd) {
|
|
552
|
-
res.status(400).json({ error: "目录不能为空。" });
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
try {
|
|
556
|
-
const sessions = processes.listClaudeHistorySessions();
|
|
557
|
-
const toDelete = [];
|
|
558
|
-
for (const session of sessions) {
|
|
559
|
-
if (session.claudeSessionId && session.cwd === cwd) {
|
|
560
|
-
toDelete.push({ claudeSessionId: session.claudeSessionId, cwd: session.cwd });
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
const deleted = processes.deleteClaudeHistoryFiles(toDelete);
|
|
564
|
-
removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
|
|
565
|
-
res.json({ ok: true, deleted });
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
app.post("/api/claude-history/batch-delete", express.json(), (req, res) => {
|
|
572
|
-
const claudeSessionIds = Array.isArray(req.body?.claudeSessionIds)
|
|
573
|
-
? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
574
|
-
: [];
|
|
575
|
-
if (claudeSessionIds.length === 0) {
|
|
576
|
-
res.status(400).json({ error: "至少提供一个历史会话 ID。" });
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
try {
|
|
580
|
-
const allSessions = processes.listClaudeHistorySessions();
|
|
581
|
-
const sessionMap = new Map();
|
|
582
|
-
for (const s of allSessions) {
|
|
583
|
-
if (s.claudeSessionId)
|
|
584
|
-
sessionMap.set(s.claudeSessionId, s.cwd);
|
|
585
|
-
}
|
|
586
|
-
const toDelete = [];
|
|
587
|
-
const toHide = [];
|
|
588
|
-
for (const id of claudeSessionIds) {
|
|
589
|
-
const cwd = sessionMap.get(id);
|
|
590
|
-
if (cwd) {
|
|
591
|
-
toDelete.push({ claudeSessionId: id, cwd });
|
|
592
|
-
}
|
|
593
|
-
else {
|
|
594
|
-
toHide.push(id);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
const deleted = processes.deleteClaudeHistoryFiles(toDelete);
|
|
598
|
-
removeFromHiddenClaudeSessionIds(storage, toDelete.map((s) => s.claudeSessionId));
|
|
599
|
-
if (toHide.length > 0) {
|
|
600
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
601
|
-
let added = 0;
|
|
602
|
-
for (const id of toHide) {
|
|
603
|
-
if (!hidden.has(id)) {
|
|
604
|
-
hidden.add(id);
|
|
605
|
-
added++;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (added > 0)
|
|
609
|
-
saveHiddenClaudeSessionIds(storage, hidden);
|
|
610
|
-
}
|
|
611
|
-
res.json({ ok: true, deleted: deleted + toHide.length });
|
|
612
|
-
}
|
|
613
|
-
catch (error) {
|
|
614
|
-
res.status(500).json({ error: getErrorMessage(error, "无法批量删除历史会话。") });
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
app.post("/api/sessions/batch-delete", express.json(), (req, res) => {
|
|
618
|
-
const sessionIds = Array.isArray(req.body?.sessionIds)
|
|
619
|
-
? req.body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
620
|
-
: [];
|
|
621
|
-
if (sessionIds.length === 0) {
|
|
622
|
-
res.status(400).json({ error: "至少提供一个会话 ID。" });
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
let deleted = 0;
|
|
626
|
-
const failed = [];
|
|
627
|
-
for (const sessionId of sessionIds) {
|
|
628
|
-
try {
|
|
629
|
-
processes.delete(sessionId);
|
|
630
|
-
deleted += 1;
|
|
631
|
-
}
|
|
632
|
-
catch {
|
|
633
|
-
failed.push(sessionId);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
if (deleted === 0 && failed.length > 0) {
|
|
637
|
-
res.status(400).json({ error: "无法批量删除会话。", failed });
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
res.json({ ok: true, deleted, failed });
|
|
641
|
-
});
|
|
642
|
-
app.get("/api/sessions/:id", (req, res) => {
|
|
643
|
-
const snapshot = processes.get(req.params.id);
|
|
644
|
-
if (!snapshot) {
|
|
645
|
-
res.status(404).json({ error: "未找到该会话,可能已被删除。" });
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
if (req.query.format === "chat") {
|
|
649
|
-
const messages = snapshot.messages && snapshot.messages.length > 0
|
|
650
|
-
? snapshot.messages
|
|
651
|
-
: parseMessages(snapshot.output);
|
|
652
|
-
res.json({ ...snapshot, messages });
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
res.json(snapshot);
|
|
656
|
-
}
|
|
657
|
-
});
|
|
483
|
+
registerSessionRoutes(app, processes, storage, config.defaultMode);
|
|
484
|
+
registerClaudeHistoryRoutes(app, processes, storage);
|
|
658
485
|
// ── Path suggestion ──
|
|
659
486
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
660
487
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
@@ -982,156 +809,6 @@ export async function startServer(config, configPath) {
|
|
|
982
809
|
res.status(400).json({ error: getErrorMessage(error, "无法启动命令。请检查命令是否安装。") });
|
|
983
810
|
}
|
|
984
811
|
});
|
|
985
|
-
app.post("/api/sessions/:id/resume", (req, res) => {
|
|
986
|
-
const sessionId = req.params.id;
|
|
987
|
-
const body = req.body;
|
|
988
|
-
try {
|
|
989
|
-
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
990
|
-
if (!existingSession) {
|
|
991
|
-
res.status(404).json({ error: "会话不存在。" });
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
const claudeSessionId = existingSession.claudeSessionId;
|
|
995
|
-
if (!claudeSessionId) {
|
|
996
|
-
res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
const command = existingSession.command.trim();
|
|
1000
|
-
if (!/^claude\b/.test(command)) {
|
|
1001
|
-
res.status(400).json({ error: "只有 Claude 命令支持恢复功能。" });
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
const newMode = body.mode
|
|
1005
|
-
? normalizeMode(body.mode, config.defaultMode)
|
|
1006
|
-
: normalizeMode(existingSession.mode, config.defaultMode);
|
|
1007
|
-
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
1008
|
-
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: sessionId });
|
|
1009
|
-
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
|
|
1010
|
-
res.status(201).json({ resumedFromSessionId: sessionId, ...newSnapshot });
|
|
1011
|
-
}
|
|
1012
|
-
catch (error) {
|
|
1013
|
-
res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
app.post("/api/claude-sessions/:claudeSessionId/resume", (req, res) => {
|
|
1017
|
-
const claudeSessionId = String(req.params.claudeSessionId || "").trim();
|
|
1018
|
-
const body = req.body;
|
|
1019
|
-
try {
|
|
1020
|
-
if (!claudeSessionId) {
|
|
1021
|
-
res.status(400).json({ error: "Claude 会话 ID 不能为空。" });
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
const existingSession = storage.getLatestSessionByClaudeSessionId(claudeSessionId);
|
|
1025
|
-
if (existingSession) {
|
|
1026
|
-
const command = existingSession.command.trim();
|
|
1027
|
-
if (!/^claude\b/.test(command)) {
|
|
1028
|
-
res.status(400).json({ error: "只有 Claude 命令支持按 Claude Session ID 恢复。" });
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
if (!existingSession.cwd || !processes.hasClaudeSessionFile(existingSession.cwd, claudeSessionId)) {
|
|
1032
|
-
res.status(400).json({ error: "对应的 Claude 历史会话文件不存在,无法恢复。" });
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
const newMode = body.mode
|
|
1036
|
-
? normalizeMode(body.mode, config.defaultMode)
|
|
1037
|
-
: normalizeMode(existingSession.mode, config.defaultMode);
|
|
1038
|
-
const resumeCommand = `${command} --resume ${claudeSessionId}`;
|
|
1039
|
-
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { resumedFromSessionId: existingSession.id });
|
|
1040
|
-
storage.saveSession({ ...existingSession, resumedToSessionId: newSnapshot.id });
|
|
1041
|
-
res.status(201).json({ resumedFromSessionId: existingSession.id, resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
// No existing wand session — resume directly with cwd from request body
|
|
1045
|
-
const cwd = body.cwd?.trim();
|
|
1046
|
-
if (!cwd) {
|
|
1047
|
-
res.status(400).json({ error: "未找到对应的会话记录,请提供工作目录 (cwd)。" });
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
const newMode = normalizeMode(body.mode, config.defaultMode);
|
|
1051
|
-
const resumeCommand = `claude --resume ${claudeSessionId}`;
|
|
1052
|
-
const newSnapshot = processes.start(resumeCommand, cwd, newMode);
|
|
1053
|
-
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
catch (error) {
|
|
1057
|
-
res.status(400).json({ error: getErrorMessage(error, "无法按 Claude 会话 ID 恢复会话。") });
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
app.post("/api/sessions/:id/input", (req, res) => {
|
|
1061
|
-
const body = req.body;
|
|
1062
|
-
const sessionId = req.params.id;
|
|
1063
|
-
const input = body.input ?? "";
|
|
1064
|
-
const view = body.view;
|
|
1065
|
-
const shortcutKey = body.shortcutKey;
|
|
1066
|
-
console.error("[wand] Input request received", { sessionId, inputLength: input.length, view: view ?? "chat" });
|
|
1067
|
-
try {
|
|
1068
|
-
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
1069
|
-
console.error("[wand] Input request succeeded", { sessionId, status: snapshot.status, inputLength: input.length, view: view ?? "chat" });
|
|
1070
|
-
res.json(snapshot);
|
|
1071
|
-
}
|
|
1072
|
-
catch (error) {
|
|
1073
|
-
const response = getInputErrorResponse(error, sessionId);
|
|
1074
|
-
console.error("[wand] Input request failed", {
|
|
1075
|
-
sessionId, inputLength: input.length, view: view ?? "chat",
|
|
1076
|
-
responseStatus: response.statusCode, responsePayload: response.payload,
|
|
1077
|
-
error: getInputDebugMeta(error),
|
|
1078
|
-
});
|
|
1079
|
-
res.status(response.statusCode).json(response.payload);
|
|
1080
|
-
}
|
|
1081
|
-
});
|
|
1082
|
-
app.post("/api/sessions/:id/resize", (req, res) => {
|
|
1083
|
-
const body = req.body;
|
|
1084
|
-
try {
|
|
1085
|
-
const snapshot = processes.resize(req.params.id, body.cols ?? 0, body.rows ?? 0);
|
|
1086
|
-
res.json(snapshot);
|
|
1087
|
-
}
|
|
1088
|
-
catch (error) {
|
|
1089
|
-
res.status(400).json({ error: getErrorMessage(error, "无法调整终端大小。") });
|
|
1090
|
-
}
|
|
1091
|
-
});
|
|
1092
|
-
app.post("/api/sessions/:id/approve-permission", (req, res) => {
|
|
1093
|
-
try {
|
|
1094
|
-
res.json(processes.approvePermission(req.params.id));
|
|
1095
|
-
}
|
|
1096
|
-
catch (error) {
|
|
1097
|
-
res.status(400).json({ error: getErrorMessage(error, "无法批准该授权请求。") });
|
|
1098
|
-
}
|
|
1099
|
-
});
|
|
1100
|
-
app.post("/api/sessions/:id/deny-permission", (req, res) => {
|
|
1101
|
-
try {
|
|
1102
|
-
res.json(processes.denyPermission(req.params.id));
|
|
1103
|
-
}
|
|
1104
|
-
catch (error) {
|
|
1105
|
-
res.status(400).json({ error: getErrorMessage(error, "无法拒绝该授权请求。") });
|
|
1106
|
-
}
|
|
1107
|
-
});
|
|
1108
|
-
app.post("/api/sessions/:id/escalations/:requestId/resolve", (req, res) => {
|
|
1109
|
-
try {
|
|
1110
|
-
const { requestId } = req.params;
|
|
1111
|
-
const body = req.body;
|
|
1112
|
-
res.json(processes.resolveEscalation(req.params.id, requestId, body.resolution));
|
|
1113
|
-
}
|
|
1114
|
-
catch (error) {
|
|
1115
|
-
res.status(400).json({ error: getErrorMessage(error, "无法处理该授权请求。") });
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
app.post("/api/sessions/:id/stop", (req, res) => {
|
|
1119
|
-
try {
|
|
1120
|
-
res.json(processes.stop(req.params.id));
|
|
1121
|
-
}
|
|
1122
|
-
catch (error) {
|
|
1123
|
-
res.status(400).json({ error: getErrorMessage(error, "无法停止会话。") });
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
app.delete("/api/sessions/:id", (req, res) => {
|
|
1127
|
-
try {
|
|
1128
|
-
processes.delete(req.params.id);
|
|
1129
|
-
res.json({ ok: true });
|
|
1130
|
-
}
|
|
1131
|
-
catch (error) {
|
|
1132
|
-
res.status(400).json({ error: getErrorMessage(error, "无法删除会话。") });
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
812
|
// ── WebSocket broadcast layer ──
|
|
1136
813
|
const server = useHttps
|
|
1137
814
|
? (() => {
|
|
@@ -349,22 +349,25 @@
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
function renderShortcutKeys() {
|
|
353
|
+
return '<button class="shortcut-key' + (state.modifiers.ctrl ? ' active' : '') + '" data-key="ctrl" type="button">Ctrl</button>' +
|
|
354
|
+
'<button class="shortcut-key' + (state.modifiers.alt ? ' active' : '') + '" data-key="alt" type="button">Alt</button>' +
|
|
355
|
+
'<span class="shortcut-sep">·</span>' +
|
|
356
|
+
'<button class="shortcut-key shortcut-dir" data-key="up" type="button">↑</button>' +
|
|
357
|
+
'<button class="shortcut-key shortcut-dir" data-key="down" type="button">↓</button>' +
|
|
358
|
+
'<button class="shortcut-key shortcut-dir" data-key="left" type="button">←</button>' +
|
|
359
|
+
'<button class="shortcut-key shortcut-dir" data-key="right" type="button">→</button>' +
|
|
360
|
+
'<span class="shortcut-sep">·</span>' +
|
|
361
|
+
'<button class="shortcut-key" data-key="enter" type="button">↵</button>' +
|
|
362
|
+
'<button class="shortcut-key" data-key="ctrl_enter" type="button">C-↵</button>' +
|
|
363
|
+
'<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
|
|
364
|
+
}
|
|
365
|
+
|
|
352
366
|
function renderInlineKeyboard() {
|
|
353
367
|
if (!state.selectedId) return "";
|
|
354
368
|
var isTerminal = state.currentView === "terminal";
|
|
355
369
|
if (!isTerminal) return "";
|
|
356
|
-
var keys =
|
|
357
|
-
'<button class="shortcut-key' + (state.modifiers.ctrl ? ' active' : '') + '" data-key="ctrl" type="button">Ctrl</button>' +
|
|
358
|
-
'<button class="shortcut-key' + (state.modifiers.alt ? ' active' : '') + '" data-key="alt" type="button">Alt</button>' +
|
|
359
|
-
'<span class="shortcut-sep">·</span>' +
|
|
360
|
-
'<button class="shortcut-key shortcut-dir" data-key="up" type="button">↑</button>' +
|
|
361
|
-
'<button class="shortcut-key shortcut-dir" data-key="down" type="button">↓</button>' +
|
|
362
|
-
'<button class="shortcut-key shortcut-dir" data-key="left" type="button">←</button>' +
|
|
363
|
-
'<button class="shortcut-key shortcut-dir" data-key="right" type="button">→</button>' +
|
|
364
|
-
'<span class="shortcut-sep">·</span>' +
|
|
365
|
-
'<button class="shortcut-key" data-key="enter" type="button">↵</button>' +
|
|
366
|
-
'<button class="shortcut-key" data-key="ctrl_enter" type="button">C-↵</button>' +
|
|
367
|
-
'<button class="shortcut-key" data-key="escape" type="button">Esc</button>';
|
|
370
|
+
var keys = renderShortcutKeys();
|
|
368
371
|
var arrow = state.shortcutsExpanded ? '›' : '‹';
|
|
369
372
|
return '<div class="inline-shortcuts-wrap' + (state.shortcutsExpanded ? ' expanded' : '') + '">' +
|
|
370
373
|
'<button class="shortcuts-toggle' + (state.shortcutsExpanded ? ' active' : '') + '" type="button" title="快捷键">' + arrow + '</button>' +
|
|
@@ -373,9 +376,11 @@
|
|
|
373
376
|
'</div>';
|
|
374
377
|
}
|
|
375
378
|
|
|
376
|
-
function
|
|
377
|
-
|
|
378
|
-
|
|
379
|
+
function renderExpandedShortcutsRow() {
|
|
380
|
+
if (!state.selectedId) return "";
|
|
381
|
+
var isTerminal = state.currentView === "terminal";
|
|
382
|
+
if (!isTerminal) return "";
|
|
383
|
+
return '<div class="inline-shortcuts-expanded-row' + (state.shortcutsExpanded ? ' visible' : '') + '">' + renderShortcutKeys() + '</div>';
|
|
379
384
|
}
|
|
380
385
|
|
|
381
386
|
function renderLogin() {
|
|
@@ -573,6 +578,7 @@
|
|
|
573
578
|
'</button>' +
|
|
574
579
|
'</div>' +
|
|
575
580
|
'</div>' +
|
|
581
|
+
renderExpandedShortcutsRow() +
|
|
576
582
|
// Session info bar at bottom
|
|
577
583
|
'<div class="input-session-info-bar">' +
|
|
578
584
|
'<span id="session-cwd-display" class="session-cwd-display">' + (selectedSession && selectedSession.cwd ? escapeHtml(selectedSession.cwd) : '未设置目录') + '</span>' +
|
|
@@ -1985,6 +1991,8 @@
|
|
|
1985
1991
|
// Inline shortcuts click handler
|
|
1986
1992
|
var inlineShortcutsWrap = document.querySelector(".inline-shortcuts-wrap");
|
|
1987
1993
|
if (inlineShortcutsWrap) inlineShortcutsWrap.addEventListener("click", handleInlineKeyboardClick);
|
|
1994
|
+
var expandedShortcutsRow = document.querySelector(".inline-shortcuts-expanded-row");
|
|
1995
|
+
if (expandedShortcutsRow) expandedShortcutsRow.addEventListener("click", handleInlineKeyboardClick);
|
|
1988
1996
|
// Shortcuts toggle (mobile fold/unfold)
|
|
1989
1997
|
var shortcutsToggleBtn = document.querySelector(".shortcuts-toggle");
|
|
1990
1998
|
if (shortcutsToggleBtn) shortcutsToggleBtn.addEventListener("click", function(e) {
|
|
@@ -1992,7 +2000,9 @@
|
|
|
1992
2000
|
state.shortcutsExpanded = !state.shortcutsExpanded;
|
|
1993
2001
|
var wrap = document.querySelector(".inline-shortcuts-wrap");
|
|
1994
2002
|
var toggle = document.querySelector(".shortcuts-toggle");
|
|
2003
|
+
var row = document.querySelector(".inline-shortcuts-expanded-row");
|
|
1995
2004
|
if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
|
|
2005
|
+
if (row) row.classList.toggle("visible", state.shortcutsExpanded);
|
|
1996
2006
|
if (toggle) {
|
|
1997
2007
|
toggle.classList.toggle("active", state.shortcutsExpanded);
|
|
1998
2008
|
toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
|
|
@@ -2002,9 +2012,12 @@
|
|
|
2002
2012
|
document.addEventListener("click", function(e) {
|
|
2003
2013
|
if (!state.shortcutsExpanded) return;
|
|
2004
2014
|
var wrap = document.querySelector(".inline-shortcuts-wrap");
|
|
2005
|
-
|
|
2015
|
+
var expandedRow = document.querySelector(".inline-shortcuts-expanded-row");
|
|
2016
|
+
var clickedInsideRow = expandedRow && expandedRow.contains(e.target);
|
|
2017
|
+
if (wrap && !wrap.contains(e.target) && !clickedInsideRow) {
|
|
2006
2018
|
state.shortcutsExpanded = false;
|
|
2007
2019
|
wrap.classList.remove("expanded");
|
|
2020
|
+
if (expandedRow) expandedRow.classList.remove("visible");
|
|
2008
2021
|
var toggle = document.querySelector(".shortcuts-toggle");
|
|
2009
2022
|
if (toggle) {
|
|
2010
2023
|
toggle.classList.remove("active");
|
|
@@ -3310,6 +3323,58 @@
|
|
|
3310
3323
|
}
|
|
3311
3324
|
}
|
|
3312
3325
|
|
|
3326
|
+
function subscribeToSession(sessionId) {
|
|
3327
|
+
if (!sessionId || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
|
3328
|
+
state.ws.send(JSON.stringify({ type: "subscribe", sessionId: sessionId }));
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function mergeServerSession(localSession, serverSession) {
|
|
3332
|
+
if (!localSession) return serverSession;
|
|
3333
|
+
|
|
3334
|
+
var merged = Object.assign({}, localSession, serverSession);
|
|
3335
|
+
var localOutput = localSession.output || "";
|
|
3336
|
+
var serverOutput = serverSession.output || "";
|
|
3337
|
+
var keepLocalOutput = localOutput.length > serverOutput.length;
|
|
3338
|
+
|
|
3339
|
+
if (keepLocalOutput) {
|
|
3340
|
+
merged.output = localOutput;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
if (localSession.id === state.selectedId) {
|
|
3344
|
+
if (localSession.permissionBlocked && serverSession.permissionBlocked === false) {
|
|
3345
|
+
// server explicitly resolved it; keep resolved state
|
|
3346
|
+
} else if (localSession.permissionBlocked && !serverSession.permissionBlocked) {
|
|
3347
|
+
merged.permissionBlocked = true;
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
if (localSession.pendingEscalation && !serverSession.pendingEscalation && serverSession.permissionBlocked !== false) {
|
|
3351
|
+
merged.pendingEscalation = localSession.pendingEscalation;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
if (localSession.messages && localSession.messages.length > 0 && (!serverSession.messages || serverSession.messages.length === 0)) {
|
|
3355
|
+
merged.messages = localSession.messages;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
return merged;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
function getPreferredMessages(session, fallbackOutput, allowFallback) {
|
|
3363
|
+
if (session && session.messages && session.messages.length > 0) {
|
|
3364
|
+
return session.messages;
|
|
3365
|
+
}
|
|
3366
|
+
if (!allowFallback) {
|
|
3367
|
+
return [];
|
|
3368
|
+
}
|
|
3369
|
+
var output = typeof fallbackOutput === "string"
|
|
3370
|
+
? fallbackOutput
|
|
3371
|
+
: (session && session.output) || "";
|
|
3372
|
+
if (!output) {
|
|
3373
|
+
return [];
|
|
3374
|
+
}
|
|
3375
|
+
return parseMessages(output, session && session.command);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3313
3378
|
function getPreferredSessionId(sessions) {
|
|
3314
3379
|
if (!sessions || !sessions.length) return null;
|
|
3315
3380
|
// Keep currently selected session as long as it still exists
|
|
@@ -3344,10 +3409,7 @@
|
|
|
3344
3409
|
|
|
3345
3410
|
state.sessions = serverSessions.map(function(serverSession) {
|
|
3346
3411
|
var localSession = state.sessions.find(function(s) { return s.id === serverSession.id; });
|
|
3347
|
-
|
|
3348
|
-
return localSession;
|
|
3349
|
-
}
|
|
3350
|
-
return serverSession;
|
|
3412
|
+
return mergeServerSession(localSession, serverSession);
|
|
3351
3413
|
});
|
|
3352
3414
|
|
|
3353
3415
|
state.selectedId = getPreferredSessionId(state.sessions);
|
|
@@ -3471,7 +3533,7 @@
|
|
|
3471
3533
|
updateShellChrome();
|
|
3472
3534
|
|
|
3473
3535
|
var selectedSession = state.sessions.find(function(s) { return s.id === id; });
|
|
3474
|
-
state.currentMessages = data.
|
|
3536
|
+
state.currentMessages = getPreferredMessages(selectedSession, data.output, false);
|
|
3475
3537
|
|
|
3476
3538
|
if (state.terminal) {
|
|
3477
3539
|
syncTerminalBuffer(id, data.output || "", { mode: "replace" });
|
|
@@ -3506,6 +3568,7 @@
|
|
|
3506
3568
|
refreshFileExplorer();
|
|
3507
3569
|
}
|
|
3508
3570
|
loadOutput(id).then(function() { focusInputBox(true); });
|
|
3571
|
+
subscribeToSession(id);
|
|
3509
3572
|
}
|
|
3510
3573
|
|
|
3511
3574
|
function updateDrawerState() {
|
|
@@ -4553,9 +4616,7 @@
|
|
|
4553
4616
|
switchToSessionView(data.id);
|
|
4554
4617
|
updateSessionSnapshot(data);
|
|
4555
4618
|
updateSessionsList();
|
|
4556
|
-
|
|
4557
|
-
state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
|
|
4558
|
-
}
|
|
4619
|
+
subscribeToSession(data.id);
|
|
4559
4620
|
loadOutput(data.id).then(function() {
|
|
4560
4621
|
focusInputBox(true);
|
|
4561
4622
|
});
|
|
@@ -4616,9 +4677,7 @@
|
|
|
4616
4677
|
updateSessionSnapshot(data);
|
|
4617
4678
|
updateSessionsList();
|
|
4618
4679
|
// Subscribe to new session via WebSocket
|
|
4619
|
-
|
|
4620
|
-
state.ws.send(JSON.stringify({ type: 'subscribe', sessionId: data.id }));
|
|
4621
|
-
}
|
|
4680
|
+
subscribeToSession(data.id);
|
|
4622
4681
|
return loadOutput(data.id);
|
|
4623
4682
|
})
|
|
4624
4683
|
.catch(function(error) {
|
|
@@ -4791,9 +4850,7 @@
|
|
|
4791
4850
|
updateSessionSnapshot(data);
|
|
4792
4851
|
updateSessionsList();
|
|
4793
4852
|
switchToSessionView(data.id);
|
|
4794
|
-
|
|
4795
|
-
state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
|
|
4796
|
-
}
|
|
4853
|
+
subscribeToSession(data.id);
|
|
4797
4854
|
return loadOutput(data.id).then(function() {
|
|
4798
4855
|
focusInputBox(true);
|
|
4799
4856
|
return data;
|
|
@@ -5506,9 +5563,7 @@
|
|
|
5506
5563
|
switchToSessionView(data.id);
|
|
5507
5564
|
updateSessionSnapshot(data);
|
|
5508
5565
|
updateSessionsList();
|
|
5509
|
-
|
|
5510
|
-
state.ws.send(JSON.stringify({ type: "subscribe", sessionId: data.id }));
|
|
5511
|
-
}
|
|
5566
|
+
subscribeToSession(data.id);
|
|
5512
5567
|
return loadOutput(data.id).then(function() {
|
|
5513
5568
|
focusInputBox(true);
|
|
5514
5569
|
});
|
|
@@ -6722,9 +6777,7 @@
|
|
|
6722
6777
|
state.ws = ws;
|
|
6723
6778
|
state.wsConnected = true;
|
|
6724
6779
|
// Subscribe to current session if any
|
|
6725
|
-
|
|
6726
|
-
ws.send(JSON.stringify({ type: 'subscribe', sessionId: state.selectedId }));
|
|
6727
|
-
}
|
|
6780
|
+
subscribeToSession(state.selectedId);
|
|
6728
6781
|
// Flush pending messages after reconnection
|
|
6729
6782
|
flushPendingMessages();
|
|
6730
6783
|
};
|
|
@@ -6774,9 +6827,7 @@
|
|
|
6774
6827
|
}
|
|
6775
6828
|
updateSessionSnapshot(snapshot);
|
|
6776
6829
|
if (msg.sessionId === state.selectedId) {
|
|
6777
|
-
|
|
6778
|
-
state.currentMessages = msg.data.messages;
|
|
6779
|
-
}
|
|
6830
|
+
state.currentMessages = getPreferredMessages(snapshot, msg.data.output, false);
|
|
6780
6831
|
updateTaskDisplay();
|
|
6781
6832
|
scheduleChatRender();
|
|
6782
6833
|
}
|
|
@@ -7119,12 +7170,7 @@
|
|
|
7119
7170
|
// Re-parse messages from the latest session output (fallback for edge cases)
|
|
7120
7171
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
7121
7172
|
if (selectedSession) {
|
|
7122
|
-
|
|
7123
|
-
if (selectedSession.messages && selectedSession.messages.length > 0) {
|
|
7124
|
-
state.currentMessages = selectedSession.messages;
|
|
7125
|
-
} else if (selectedSession.output) {
|
|
7126
|
-
state.currentMessages = parseMessages(selectedSession.output, selectedSession.command);
|
|
7127
|
-
}
|
|
7173
|
+
state.currentMessages = getPreferredMessages(selectedSession, selectedSession.output, true);
|
|
7128
7174
|
}
|
|
7129
7175
|
renderChat();
|
|
7130
7176
|
}, 30);
|