@co0ontty/wand 1.60.5 → 1.61.0-beta.g5043a45
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/build-info.json +4 -4
- package/dist/process-manager.js +9 -2
- package/dist/server-session-routes.js +22 -1
- package/dist/structured-session-manager.d.ts +7 -2
- package/dist/structured-session-manager.js +57 -13
- package/dist/web-ui/content/scripts.js +30 -30
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-06-
|
|
4
|
-
"version": "1.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "5043a45750e7229c7f3567c971edfd10beccbce8",
|
|
3
|
+
"builtAt": "2026-06-13T23:53:44.046Z",
|
|
4
|
+
"version": "1.61.0-beta.g5043a45",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
package/dist/process-manager.js
CHANGED
|
@@ -165,7 +165,10 @@ function isClaudeSessionFileAvailable(cwd, claudeSessionId) {
|
|
|
165
165
|
* interpretations and validate with existsSync, falling back to naive replacement.
|
|
166
166
|
*/
|
|
167
167
|
function invertNormalizedProjectDir(dirName) {
|
|
168
|
-
// The normalization
|
|
168
|
+
// The normalization replaces every non-alphanumeric char with "-", so this
|
|
169
|
+
// inversion is best-effort: "-" most often maps back to "/", but may also be
|
|
170
|
+
// a literal "-", ".", or "_". We try "/" vs "-" per position and validate with
|
|
171
|
+
// existsSync; dots/underscores in the original path can't be recovered here.
|
|
169
172
|
const naive = dirName.replace(/-/g, "/");
|
|
170
173
|
if (existsSync(naive))
|
|
171
174
|
return naive;
|
|
@@ -494,7 +497,11 @@ function listClaudeTaskIds() {
|
|
|
494
497
|
}
|
|
495
498
|
}
|
|
496
499
|
function getClaudeProjectDir(cwd) {
|
|
497
|
-
|
|
500
|
+
// Claude Code encodes the project dir by replacing every non-alphanumeric
|
|
501
|
+
// character (slash, dot, underscore, etc.) with "-", not just "/". Mirroring
|
|
502
|
+
// only "/" misses paths like ".../vibe_coding/wand" → the scan looks in a
|
|
503
|
+
// directory Claude never wrote to, so the session ID is never discovered.
|
|
504
|
+
const normalized = path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
498
505
|
return path.join(os.homedir(), ".claude", "projects", normalized);
|
|
499
506
|
}
|
|
500
507
|
function getLatestClaudeTaskId(excludeIds) {
|
|
@@ -252,7 +252,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
252
252
|
}
|
|
253
253
|
});
|
|
254
254
|
// ── Structured queued-messages management ──
|
|
255
|
-
//
|
|
255
|
+
// 这些端点构成"排队消息条"的后端操作面:reorder、立即发送、单条删除、全部清空。
|
|
256
256
|
// 全部走乐观更新模型,失败时前端会回滚到上一次 WS 推送的 queuedMessages。
|
|
257
257
|
app.patch("/api/structured-sessions/:id/queued", express.json(), (req, res) => {
|
|
258
258
|
const rawOrder = req.body?.order;
|
|
@@ -282,6 +282,27 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
282
282
|
res.status(400).json({ error: getErrorMessage(error, "无法删除排队消息。") });
|
|
283
283
|
}
|
|
284
284
|
});
|
|
285
|
+
app.post("/api/structured-sessions/:id/queued/:index/promote", express.json(), async (req, res) => {
|
|
286
|
+
const index = Number(req.params.index);
|
|
287
|
+
if (!Number.isInteger(index)) {
|
|
288
|
+
res.status(400).json({ error: "下标无效。" });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const expectedText = typeof req.body?.expectedText === "string" ? req.body.expectedText : undefined;
|
|
292
|
+
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
293
|
+
try {
|
|
294
|
+
const snapshot = await structured.promoteQueuedMessage(req.params.id, index, expectedText, idempotencyKey);
|
|
295
|
+
res.json(snapshot);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
const errorCode = error?.code;
|
|
299
|
+
const status = errorCode === "duplicate_idempotency_key" ? 409 : 400;
|
|
300
|
+
res.status(status).json({
|
|
301
|
+
error: getErrorMessage(error, "无法立即发送排队消息。"),
|
|
302
|
+
errorCode,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
});
|
|
285
306
|
app.delete("/api/structured-sessions/:id/queued", (req, res) => {
|
|
286
307
|
try {
|
|
287
308
|
const snapshot = structured.clearQueuedMessages(req.params.id);
|
|
@@ -90,6 +90,7 @@ export declare class StructuredSessionManager {
|
|
|
90
90
|
interrupt?: boolean;
|
|
91
91
|
idempotencyKey?: string;
|
|
92
92
|
preserveQueue?: boolean;
|
|
93
|
+
queueAlreadyRemoved?: boolean;
|
|
93
94
|
}): Promise<SessionSnapshot>;
|
|
94
95
|
/**
|
|
95
96
|
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
@@ -102,6 +103,12 @@ export declare class StructuredSessionManager {
|
|
|
102
103
|
reorderQueuedMessages(sessionId: string, order: number[]): SessionSnapshot;
|
|
103
104
|
/** Remove a single queued message by index. */
|
|
104
105
|
deleteQueuedMessage(sessionId: string, index: number): SessionSnapshot;
|
|
106
|
+
/**
|
|
107
|
+
* Remove one queued message by index before sending it. Keeping this operation
|
|
108
|
+
* on the server prevents clients from re-sending the text while the original
|
|
109
|
+
* queue entry remains available for the automatic flush path.
|
|
110
|
+
*/
|
|
111
|
+
promoteQueuedMessage(sessionId: string, index: number, expectedText?: string, idempotencyKey?: string): Promise<SessionSnapshot>;
|
|
105
112
|
/** Clear all queued messages. No-op when queue is already empty. */
|
|
106
113
|
clearQueuedMessages(sessionId: string): SessionSnapshot;
|
|
107
114
|
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
@@ -119,8 +126,6 @@ export declare class StructuredSessionManager {
|
|
|
119
126
|
stop(id: string): SessionSnapshot;
|
|
120
127
|
delete(id: string): void;
|
|
121
128
|
private requireSession;
|
|
122
|
-
private buildQueuedPlaceholderTurns;
|
|
123
|
-
private buildRenderableMessages;
|
|
124
129
|
private emitStructuredSnapshot;
|
|
125
130
|
private flushNextQueuedMessage;
|
|
126
131
|
private emit;
|
|
@@ -629,6 +629,22 @@ export class StructuredSessionManager {
|
|
|
629
629
|
this.interruptedWith.set(id, prompt);
|
|
630
630
|
if (opts.preserveQueue) {
|
|
631
631
|
this.preserveQueueOnInterrupt.add(id);
|
|
632
|
+
// 「立即发送」排队条某一条:interrupt 把它作为新输入重发,但该条仍留在
|
|
633
|
+
// queuedMessages 里。必须在这里把它从队列摘掉一次,否则 preserveQueue 会
|
|
634
|
+
// 原样保留整条队列,待 interruptPrompt 跑完 flushNextQueuedMessage 会把它
|
|
635
|
+
// 当成普通排队再发一遍(重复发送)。旧客户端没有走 promote endpoint,
|
|
636
|
+
// 服务端只能按文本删第一处匹配;新客户端会带 queueAlreadyRemoved 跳过这里。
|
|
637
|
+
if (!opts.queueAlreadyRemoved) {
|
|
638
|
+
const queue = session.queuedMessages ?? [];
|
|
639
|
+
const removeAt = queue.indexOf(prompt);
|
|
640
|
+
if (removeAt !== -1) {
|
|
641
|
+
const trimmedQueue = queue.slice(0, removeAt).concat(queue.slice(removeAt + 1));
|
|
642
|
+
session = { ...session, queuedMessages: trimmedQueue };
|
|
643
|
+
this.sessions.set(id, session);
|
|
644
|
+
this.storage.saveSession(session);
|
|
645
|
+
this.emitStructuredSnapshot(session);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
632
648
|
}
|
|
633
649
|
else {
|
|
634
650
|
this.preserveQueueOnInterrupt.delete(id);
|
|
@@ -796,6 +812,44 @@ export class StructuredSessionManager {
|
|
|
796
812
|
this.emitStructuredSnapshot(updated);
|
|
797
813
|
return updated;
|
|
798
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Remove one queued message by index before sending it. Keeping this operation
|
|
817
|
+
* on the server prevents clients from re-sending the text while the original
|
|
818
|
+
* queue entry remains available for the automatic flush path.
|
|
819
|
+
*/
|
|
820
|
+
async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey) {
|
|
821
|
+
const session = this.requireSession(sessionId);
|
|
822
|
+
if (idempotencyKey && this.seenIdempotencyKeys.has(`${sessionId}:${idempotencyKey}`)) {
|
|
823
|
+
return session;
|
|
824
|
+
}
|
|
825
|
+
const queue = session.queuedMessages ?? [];
|
|
826
|
+
if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
|
|
827
|
+
throw new Error("队列中没有该条消息(可能已被处理)。");
|
|
828
|
+
}
|
|
829
|
+
if (expectedText !== undefined && queue[index] !== expectedText) {
|
|
830
|
+
throw new Error("排队消息已变化,请按最新顺序重试。");
|
|
831
|
+
}
|
|
832
|
+
const prompt = queue[index];
|
|
833
|
+
const remaining = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
834
|
+
const inFlight = session.status === "running" && session.structuredState?.inFlight === true;
|
|
835
|
+
const updated = { ...session, queuedMessages: remaining };
|
|
836
|
+
this.sessions.set(sessionId, updated);
|
|
837
|
+
this.storage.saveSession(updated);
|
|
838
|
+
this.emitStructuredSnapshot(updated);
|
|
839
|
+
try {
|
|
840
|
+
return await this.sendMessage(sessionId, prompt, {
|
|
841
|
+
interrupt: inFlight,
|
|
842
|
+
preserveQueue: inFlight,
|
|
843
|
+
queueAlreadyRemoved: true,
|
|
844
|
+
idempotencyKey,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
// Once the item has been promoted it must not return to the queue: the
|
|
849
|
+
// send path may have already persisted its user turn before a runner error.
|
|
850
|
+
return this.requireSession(sessionId);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
799
853
|
/** Clear all queued messages. No-op when queue is already empty. */
|
|
800
854
|
clearQueuedMessages(sessionId) {
|
|
801
855
|
const session = this.requireSession(sessionId);
|
|
@@ -957,23 +1011,13 @@ export class StructuredSessionManager {
|
|
|
957
1011
|
}
|
|
958
1012
|
return session;
|
|
959
1013
|
}
|
|
960
|
-
buildQueuedPlaceholderTurns(session) {
|
|
961
|
-
return (session.queuedMessages ?? []).map((text) => ({
|
|
962
|
-
role: "user",
|
|
963
|
-
content: [{ type: "text", text, __queued: true }],
|
|
964
|
-
}));
|
|
965
|
-
}
|
|
966
|
-
buildRenderableMessages(session) {
|
|
967
|
-
return [
|
|
968
|
-
...(session.messages ?? []),
|
|
969
|
-
...this.buildQueuedPlaceholderTurns(session),
|
|
970
|
-
];
|
|
971
|
-
}
|
|
972
1014
|
emitStructuredSnapshot(session, eventType = "output") {
|
|
1015
|
+
// 排队消息只通过 payload.queuedMessages 单独下发,由各端在消息卡片外的「排队条」
|
|
1016
|
+
// 里纵向渲染——绝不再把它们当成 __queued 占位 turn 混进 messages 消息流里,否则会
|
|
1017
|
+
// 和排队条重复显示(旧的「显示异常」根因)。
|
|
973
1018
|
const payload = buildStructuredOutputPayload(session);
|
|
974
1019
|
const data = {
|
|
975
1020
|
...payload,
|
|
976
|
-
messages: this.buildRenderableMessages(session),
|
|
977
1021
|
status: session.status,
|
|
978
1022
|
exitCode: session.exitCode,
|
|
979
1023
|
};
|