@co0ontty/wand 2.4.4 → 2.5.0-beta.g697977e
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/cli.js +1 -8
- package/dist/structured-session-manager.d.ts +18 -4
- package/dist/structured-session-manager.js +277 -56
- package/dist/tui/attach.js +1 -1
- package/dist/tui/layout.js +1 -4
- package/dist/tui/session-formatter.d.ts +2 -2
- package/dist/tui/session-formatter.js +2 -39
- 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-07-
|
|
4
|
-
"version": "2.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "697977e70f1b4cd85e483f372bac733119e3fc48",
|
|
3
|
+
"builtAt": "2026-07-10T14:37:30.531Z",
|
|
4
|
+
"version": "2.5.0-beta.g697977e",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -269,13 +269,6 @@ function shouldUseTui() {
|
|
|
269
269
|
}
|
|
270
270
|
function printStartupBanner(handle) {
|
|
271
271
|
const all = [...handle.processManager.listSlim(), ...handle.structuredSessions.listSlim()];
|
|
272
|
-
let active = 0, archived = 0;
|
|
273
|
-
for (const s of all) {
|
|
274
|
-
if (s.archived)
|
|
275
|
-
archived += 1;
|
|
276
|
-
else if (s.status === "running")
|
|
277
|
-
active += 1;
|
|
278
|
-
}
|
|
279
272
|
const scheme = handle.httpsEnabled ? "HTTPS" : "HTTP";
|
|
280
273
|
const primary = handle.urls[0]?.url ?? `${handle.httpsEnabled ? "https" : "http"}://${handle.bindAddr}`;
|
|
281
274
|
const orphan = handle.orphanRecoveredCount > 0
|
|
@@ -286,7 +279,7 @@ function printStartupBanner(handle) {
|
|
|
286
279
|
` Bind ${handle.bindAddr}`,
|
|
287
280
|
` Config ${handle.configPath}`,
|
|
288
281
|
` Database ${handle.dbPath}`,
|
|
289
|
-
` Sessions ${
|
|
282
|
+
` Sessions ${all.length} total${orphan}`,
|
|
290
283
|
];
|
|
291
284
|
for (const extra of handle.urls.slice(1)) {
|
|
292
285
|
lines.splice(1, 0, ` URL ${extra.url} (${extra.scheme})`);
|
|
@@ -33,7 +33,23 @@ export declare function thinkingEffortToClaudeCliEffort(effort: SessionSnapshot[
|
|
|
33
33
|
export declare function thinkingEffortToClaudeSlashEffort(effort: SessionSnapshot["thinkingEffort"]): string;
|
|
34
34
|
/** Codex CLI 用:把 thinkingEffort 映射到 model_reasoning_effort 配置。off → 不覆盖 Codex 默认。 */
|
|
35
35
|
export declare function thinkingEffortToCodexReasoningEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Preserve both Responses content-part arrays and arbitrary structured tool output.
|
|
38
|
+
* Arrays without a `type` discriminator (for example Codex tool_search results)
|
|
39
|
+
* are serialized instead of being filtered to an empty result.
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeStructuredToolResultContent(content: unknown): string | Array<{
|
|
42
|
+
type: string;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}>;
|
|
36
45
|
export declare function buildCodexPatchApplyBlocks(item: Record<string, unknown>): ContentBlock[];
|
|
46
|
+
export interface CodexFileSnapshot {
|
|
47
|
+
exists: boolean;
|
|
48
|
+
text: string | null;
|
|
49
|
+
unavailableReason?: string;
|
|
50
|
+
}
|
|
51
|
+
type CodexFileSnapshotMap = Map<string, CodexFileSnapshot>;
|
|
52
|
+
export declare function buildCodexFileChangeBlocks(item: Record<string, unknown>, completed: boolean, beforeSnapshots?: CodexFileSnapshotMap, afterSnapshots?: CodexFileSnapshotMap): ContentBlock[];
|
|
37
53
|
export declare class StructuredSessionManager {
|
|
38
54
|
private readonly storage;
|
|
39
55
|
private readonly config;
|
|
@@ -204,10 +220,8 @@ export declare class StructuredSessionManager {
|
|
|
204
220
|
* agent_message → text
|
|
205
221
|
* reasoning → thinking
|
|
206
222
|
* command_execution → tool_use "Bash" + tool_result
|
|
207
|
-
* file_change → one
|
|
208
|
-
*
|
|
209
|
-
* exec stream, only the path list; diff card body is
|
|
210
|
-
* empty but the file row + status still render)
|
|
223
|
+
* file_change → one Edit/Write per file; snapshots taken between
|
|
224
|
+
* item.started/completed restore the omitted diff body
|
|
211
225
|
* mcp_tool_call → tool_use named "<server>__<tool>" + tool_result
|
|
212
226
|
* web_search → tool_use "WebSearch" + tool_result (results not in stream)
|
|
213
227
|
* todo_list → tool_use "TodoWrite" (replaced in place on each update)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -91,6 +91,27 @@ function parseJsonRecord(value) {
|
|
|
91
91
|
return {};
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Preserve both Responses content-part arrays and arbitrary structured tool output.
|
|
96
|
+
* Arrays without a `type` discriminator (for example Codex tool_search results)
|
|
97
|
+
* are serialized instead of being filtered to an empty result.
|
|
98
|
+
*/
|
|
99
|
+
export function normalizeStructuredToolResultContent(content) {
|
|
100
|
+
if (typeof content === "string")
|
|
101
|
+
return content;
|
|
102
|
+
if (Array.isArray(content)) {
|
|
103
|
+
const parts = content.filter((item) => !!item && typeof item === "object" && typeof item.type === "string");
|
|
104
|
+
if (parts.length === content.length)
|
|
105
|
+
return parts;
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(content, null, 2);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return String(content);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return typeof content === "undefined" || content === null ? "" : String(content);
|
|
114
|
+
}
|
|
94
115
|
function codexPatchToolName(kind) {
|
|
95
116
|
if (kind === "add")
|
|
96
117
|
return "Write";
|
|
@@ -144,6 +165,216 @@ export function buildCodexPatchApplyBlocks(item) {
|
|
|
144
165
|
});
|
|
145
166
|
return blocks;
|
|
146
167
|
}
|
|
168
|
+
const CODEX_FILE_SNAPSHOT_MAX_BYTES = 512 * 1024;
|
|
169
|
+
const CODEX_DIFF_MAX_EDIT_DISTANCE = 512;
|
|
170
|
+
const CODEX_DIFF_MAX_CHARS = 32 * 1024;
|
|
171
|
+
const CODEX_DIFF_CONTEXT_LINES = 3;
|
|
172
|
+
function readCodexFileSnapshot(filePath) {
|
|
173
|
+
if (!filePath || !existsSync(filePath))
|
|
174
|
+
return { exists: false, text: "" };
|
|
175
|
+
try {
|
|
176
|
+
const stat = statSync(filePath);
|
|
177
|
+
if (!stat.isFile()) {
|
|
178
|
+
return { exists: true, text: null, unavailableReason: "目标不是普通文件" };
|
|
179
|
+
}
|
|
180
|
+
if (stat.size > CODEX_FILE_SNAPSHOT_MAX_BYTES) {
|
|
181
|
+
return { exists: true, text: null, unavailableReason: "文件过大,未生成差异正文" };
|
|
182
|
+
}
|
|
183
|
+
const content = readFileSync(filePath);
|
|
184
|
+
if (content.includes(0)) {
|
|
185
|
+
return { exists: true, text: null, unavailableReason: "二进制文件不支持文本差异" };
|
|
186
|
+
}
|
|
187
|
+
return { exists: true, text: content.toString("utf8") };
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
exists: true,
|
|
192
|
+
text: null,
|
|
193
|
+
unavailableReason: `读取文件失败:${getErrorMessage(error)}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Myers line diff. File snapshots are bounded above; the edit-distance guard
|
|
199
|
+
* keeps completely rewritten generated files from consuming quadratic memory.
|
|
200
|
+
*/
|
|
201
|
+
function diffCodexLines(before, after) {
|
|
202
|
+
const max = before.length + after.length;
|
|
203
|
+
let frontier = new Map([[1, 0]]);
|
|
204
|
+
const trace = [];
|
|
205
|
+
let completedDistance = -1;
|
|
206
|
+
for (let distance = 0; distance <= max && distance <= CODEX_DIFF_MAX_EDIT_DISTANCE; distance++) {
|
|
207
|
+
trace.push(new Map(frontier));
|
|
208
|
+
for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
|
|
209
|
+
const down = frontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY;
|
|
210
|
+
const right = frontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY;
|
|
211
|
+
let oldIndex = diagonal === -distance || (diagonal !== distance && right < down)
|
|
212
|
+
? Math.max(0, down)
|
|
213
|
+
: Math.max(0, right + 1);
|
|
214
|
+
let newIndex = oldIndex - diagonal;
|
|
215
|
+
while (oldIndex < before.length
|
|
216
|
+
&& newIndex < after.length
|
|
217
|
+
&& before[oldIndex] === after[newIndex]) {
|
|
218
|
+
oldIndex++;
|
|
219
|
+
newIndex++;
|
|
220
|
+
}
|
|
221
|
+
frontier.set(diagonal, oldIndex);
|
|
222
|
+
if (oldIndex >= before.length && newIndex >= after.length) {
|
|
223
|
+
completedDistance = distance;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (completedDistance >= 0)
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
// A very large rewrite is still useful to inspect. This fallback is not
|
|
231
|
+
// minimal, but remains truthful and is later clipped by transport/UI limits.
|
|
232
|
+
if (completedDistance < 0) {
|
|
233
|
+
return [
|
|
234
|
+
...before.map((text) => ({ kind: "delete", text })),
|
|
235
|
+
...after.map((text) => ({ kind: "add", text })),
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
const reversed = [];
|
|
239
|
+
let oldIndex = before.length;
|
|
240
|
+
let newIndex = after.length;
|
|
241
|
+
for (let distance = completedDistance; distance >= 0; distance--) {
|
|
242
|
+
const previous = trace[distance];
|
|
243
|
+
const diagonal = oldIndex - newIndex;
|
|
244
|
+
const down = previous.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY;
|
|
245
|
+
const right = previous.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY;
|
|
246
|
+
const previousDiagonal = diagonal === -distance || (diagonal !== distance && right < down)
|
|
247
|
+
? diagonal + 1
|
|
248
|
+
: diagonal - 1;
|
|
249
|
+
const previousOldIndex = Math.max(0, previous.get(previousDiagonal) ?? 0);
|
|
250
|
+
const previousNewIndex = previousOldIndex - previousDiagonal;
|
|
251
|
+
while (oldIndex > previousOldIndex && newIndex > previousNewIndex) {
|
|
252
|
+
reversed.push({ kind: "equal", text: before[oldIndex - 1] });
|
|
253
|
+
oldIndex--;
|
|
254
|
+
newIndex--;
|
|
255
|
+
}
|
|
256
|
+
if (distance === 0)
|
|
257
|
+
break;
|
|
258
|
+
if (oldIndex === previousOldIndex) {
|
|
259
|
+
reversed.push({ kind: "add", text: after[newIndex - 1] });
|
|
260
|
+
newIndex--;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
reversed.push({ kind: "delete", text: before[oldIndex - 1] });
|
|
264
|
+
oldIndex--;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return reversed.reverse();
|
|
268
|
+
}
|
|
269
|
+
function codexDiffPath(filePath) {
|
|
270
|
+
return filePath.replace(/[\r\n]/g, " ").replace(/^\/+/, "");
|
|
271
|
+
}
|
|
272
|
+
function codexDiffLines(text) {
|
|
273
|
+
if (!text)
|
|
274
|
+
return [];
|
|
275
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
276
|
+
const lines = normalized.split("\n");
|
|
277
|
+
if (normalized.endsWith("\n"))
|
|
278
|
+
lines.pop();
|
|
279
|
+
return lines;
|
|
280
|
+
}
|
|
281
|
+
function buildCodexUnifiedDiff(filePath, before, after) {
|
|
282
|
+
if (before.text === null || after.text === null || before.text === after.text)
|
|
283
|
+
return "";
|
|
284
|
+
const oldLines = codexDiffLines(before.text);
|
|
285
|
+
const newLines = codexDiffLines(after.text);
|
|
286
|
+
const lines = diffCodexLines(oldLines, newLines);
|
|
287
|
+
const changedIndexes = lines
|
|
288
|
+
.map((line, index) => line.kind === "equal" ? -1 : index)
|
|
289
|
+
.filter((index) => index >= 0);
|
|
290
|
+
if (changedIndexes.length === 0)
|
|
291
|
+
return "";
|
|
292
|
+
const oldBefore = [];
|
|
293
|
+
const newBefore = [];
|
|
294
|
+
let oldCount = 0;
|
|
295
|
+
let newCount = 0;
|
|
296
|
+
lines.forEach((line, index) => {
|
|
297
|
+
oldBefore[index] = oldCount;
|
|
298
|
+
newBefore[index] = newCount;
|
|
299
|
+
if (line.kind !== "add")
|
|
300
|
+
oldCount++;
|
|
301
|
+
if (line.kind !== "delete")
|
|
302
|
+
newCount++;
|
|
303
|
+
});
|
|
304
|
+
const hunks = [];
|
|
305
|
+
for (const changedIndex of changedIndexes) {
|
|
306
|
+
const start = Math.max(0, changedIndex - CODEX_DIFF_CONTEXT_LINES);
|
|
307
|
+
const end = Math.min(lines.length, changedIndex + CODEX_DIFF_CONTEXT_LINES + 1);
|
|
308
|
+
const previous = hunks[hunks.length - 1];
|
|
309
|
+
if (previous && start <= previous.end)
|
|
310
|
+
previous.end = Math.max(previous.end, end);
|
|
311
|
+
else
|
|
312
|
+
hunks.push({ start, end });
|
|
313
|
+
}
|
|
314
|
+
const displayPath = codexDiffPath(filePath);
|
|
315
|
+
const output = [
|
|
316
|
+
before.exists ? `--- a/${displayPath}` : "--- /dev/null",
|
|
317
|
+
after.exists ? `+++ b/${displayPath}` : "+++ /dev/null",
|
|
318
|
+
];
|
|
319
|
+
for (const hunk of hunks) {
|
|
320
|
+
const hunkLines = lines.slice(hunk.start, hunk.end);
|
|
321
|
+
const hunkOldCount = hunkLines.filter((line) => line.kind !== "add").length;
|
|
322
|
+
const hunkNewCount = hunkLines.filter((line) => line.kind !== "delete").length;
|
|
323
|
+
const hunkOldStart = hunkOldCount === 0 ? oldBefore[hunk.start] : oldBefore[hunk.start] + 1;
|
|
324
|
+
const hunkNewStart = hunkNewCount === 0 ? newBefore[hunk.start] : newBefore[hunk.start] + 1;
|
|
325
|
+
output.push(`@@ -${hunkOldStart},${hunkOldCount} +${hunkNewStart},${hunkNewCount} @@`);
|
|
326
|
+
for (const line of hunkLines) {
|
|
327
|
+
output.push(`${line.kind === "add" ? "+" : line.kind === "delete" ? "-" : " "}${line.text}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const diff = output.join("\n");
|
|
331
|
+
if (diff.length <= CODEX_DIFF_MAX_CHARS)
|
|
332
|
+
return diff;
|
|
333
|
+
const cutAt = diff.lastIndexOf("\n", CODEX_DIFF_MAX_CHARS);
|
|
334
|
+
return `${diff.slice(0, cutAt > 0 ? cutAt : CODEX_DIFF_MAX_CHARS)}\n…(差异正文已截断)`;
|
|
335
|
+
}
|
|
336
|
+
export function buildCodexFileChangeBlocks(item, completed, beforeSnapshots = new Map(), afterSnapshots = new Map()) {
|
|
337
|
+
const id = getString(item.id) || "file-change";
|
|
338
|
+
const rawChanges = Array.isArray(item.changes) ? item.changes : [];
|
|
339
|
+
const status = getString(item.status) || (completed ? "completed" : "in_progress");
|
|
340
|
+
const isError = status === "failed";
|
|
341
|
+
const blocks = [];
|
|
342
|
+
rawChanges.forEach((entry, index) => {
|
|
343
|
+
const change = asRecord(entry);
|
|
344
|
+
if (!change)
|
|
345
|
+
return;
|
|
346
|
+
const filePath = getString(change.path);
|
|
347
|
+
const kind = getString(change.kind) || "update";
|
|
348
|
+
const toolUseId = `${id}#${index}`;
|
|
349
|
+
const input = { file_path: filePath, kind, status };
|
|
350
|
+
const before = beforeSnapshots.get(toolUseId);
|
|
351
|
+
const after = afterSnapshots.get(toolUseId);
|
|
352
|
+
if (completed && before && after) {
|
|
353
|
+
const unifiedDiff = buildCodexUnifiedDiff(filePath, before, after);
|
|
354
|
+
if (unifiedDiff)
|
|
355
|
+
input.unified_diff = unifiedDiff;
|
|
356
|
+
const unavailableReason = before.unavailableReason || after.unavailableReason;
|
|
357
|
+
if (!unifiedDiff && unavailableReason)
|
|
358
|
+
input.diff_unavailable_reason = unavailableReason;
|
|
359
|
+
}
|
|
360
|
+
blocks.push({
|
|
361
|
+
type: "tool_use",
|
|
362
|
+
id: toolUseId,
|
|
363
|
+
name: codexPatchToolName(kind),
|
|
364
|
+
description: kind,
|
|
365
|
+
input,
|
|
366
|
+
});
|
|
367
|
+
if (completed) {
|
|
368
|
+
blocks.push({
|
|
369
|
+
type: "tool_result",
|
|
370
|
+
tool_use_id: toolUseId,
|
|
371
|
+
content: isError ? `file change failed: ${filePath}` : "",
|
|
372
|
+
is_error: isError,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return blocks;
|
|
377
|
+
}
|
|
147
378
|
function captureTaskMeta(blocks, registry) {
|
|
148
379
|
for (const b of blocks) {
|
|
149
380
|
if (b.type !== "tool_use")
|
|
@@ -1250,6 +1481,8 @@ export class StructuredSessionManager {
|
|
|
1250
1481
|
model: session.selectedModel ?? session.structuredState?.model,
|
|
1251
1482
|
usage: undefined,
|
|
1252
1483
|
codexBlockIndex: new Map(),
|
|
1484
|
+
codexFileSnapshots: new Map(),
|
|
1485
|
+
cwd: session.cwd,
|
|
1253
1486
|
};
|
|
1254
1487
|
let lineBuf = "";
|
|
1255
1488
|
let stderr = "";
|
|
@@ -2741,13 +2974,7 @@ export class StructuredSessionManager {
|
|
|
2741
2974
|
return record;
|
|
2742
2975
|
}
|
|
2743
2976
|
normalizeToolResultContent(content) {
|
|
2744
|
-
|
|
2745
|
-
return content;
|
|
2746
|
-
}
|
|
2747
|
-
if (Array.isArray(content)) {
|
|
2748
|
-
return content.filter((item) => !!item && typeof item === "object" && typeof item.type === "string");
|
|
2749
|
-
}
|
|
2750
|
-
return typeof content === "undefined" || content === null ? "" : String(content);
|
|
2977
|
+
return normalizeStructuredToolResultContent(content);
|
|
2751
2978
|
}
|
|
2752
2979
|
unwrapCodexStreamEvent(parsed) {
|
|
2753
2980
|
const event = asRecord(parsed);
|
|
@@ -2771,12 +2998,18 @@ export class StructuredSessionManager {
|
|
|
2771
2998
|
"function_call_output",
|
|
2772
2999
|
"custom_tool_call",
|
|
2773
3000
|
"custom_tool_call_output",
|
|
3001
|
+
"command_execution",
|
|
2774
3002
|
"patch_apply_end",
|
|
3003
|
+
"file_change",
|
|
3004
|
+
"mcp_tool_call",
|
|
2775
3005
|
"mcp_tool_call_end",
|
|
2776
3006
|
"web_search_call",
|
|
2777
3007
|
"web_search_end",
|
|
3008
|
+
"web_search",
|
|
2778
3009
|
"tool_search_call",
|
|
2779
3010
|
"tool_search_output",
|
|
3011
|
+
"collab_tool_call",
|
|
3012
|
+
"todo_list",
|
|
2780
3013
|
]);
|
|
2781
3014
|
if (!supported.has(type))
|
|
2782
3015
|
return false;
|
|
@@ -2903,7 +3136,32 @@ export class StructuredSessionManager {
|
|
|
2903
3136
|
applyCodexItem(turnState, item, phase) {
|
|
2904
3137
|
const completed = phase === "completed";
|
|
2905
3138
|
const itemId = typeof item.id === "string" ? item.id : "";
|
|
2906
|
-
const
|
|
3139
|
+
const itemType = getString(item.type);
|
|
3140
|
+
let afterSnapshots;
|
|
3141
|
+
if (itemType === "file_change" && itemId) {
|
|
3142
|
+
const snapshots = turnState.codexFileSnapshots ??= new Map();
|
|
3143
|
+
const rawChanges = Array.isArray(item.changes) ? item.changes : [];
|
|
3144
|
+
if (phase === "started") {
|
|
3145
|
+
rawChanges.forEach((entry, index) => {
|
|
3146
|
+
const filePath = getString(asRecord(entry)?.path);
|
|
3147
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
3148
|
+
? filePath
|
|
3149
|
+
: path.resolve(turnState.cwd || process.cwd(), filePath);
|
|
3150
|
+
snapshots.set(`${itemId}#${index}`, readCodexFileSnapshot(absolutePath));
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
else if (completed) {
|
|
3154
|
+
afterSnapshots = new Map();
|
|
3155
|
+
rawChanges.forEach((entry, index) => {
|
|
3156
|
+
const filePath = getString(asRecord(entry)?.path);
|
|
3157
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
3158
|
+
? filePath
|
|
3159
|
+
: path.resolve(turnState.cwd || process.cwd(), filePath);
|
|
3160
|
+
afterSnapshots?.set(`${itemId}#${index}`, readCodexFileSnapshot(absolutePath));
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
const blocks = this.extractCodexItemBlock(item, completed, turnState.codexFileSnapshots, afterSnapshots);
|
|
2907
3165
|
if (blocks.length === 0)
|
|
2908
3166
|
return;
|
|
2909
3167
|
const index = turnState.codexBlockIndex ??= new Map();
|
|
@@ -2931,6 +3189,12 @@ export class StructuredSessionManager {
|
|
|
2931
3189
|
// 仍然走原有 upsert:tool_result 按 tool_use_id 配对,其余直接 push。
|
|
2932
3190
|
this.upsertCodexBlock(turnState.blocks, block);
|
|
2933
3191
|
}
|
|
3192
|
+
if (completed && itemType === "file_change") {
|
|
3193
|
+
for (const key of [...(turnState.codexFileSnapshots?.keys() ?? [])]) {
|
|
3194
|
+
if (key.startsWith(`${itemId}#`))
|
|
3195
|
+
turnState.codexFileSnapshots?.delete(key);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
2934
3198
|
}
|
|
2935
3199
|
/**
|
|
2936
3200
|
* Map a codex `item.{started,updated,completed}` payload into wand's
|
|
@@ -2943,10 +3207,8 @@ export class StructuredSessionManager {
|
|
|
2943
3207
|
* agent_message → text
|
|
2944
3208
|
* reasoning → thinking
|
|
2945
3209
|
* command_execution → tool_use "Bash" + tool_result
|
|
2946
|
-
* file_change → one
|
|
2947
|
-
*
|
|
2948
|
-
* exec stream, only the path list; diff card body is
|
|
2949
|
-
* empty but the file row + status still render)
|
|
3210
|
+
* file_change → one Edit/Write per file; snapshots taken between
|
|
3211
|
+
* item.started/completed restore the omitted diff body
|
|
2950
3212
|
* mcp_tool_call → tool_use named "<server>__<tool>" + tool_result
|
|
2951
3213
|
* web_search → tool_use "WebSearch" + tool_result (results not in stream)
|
|
2952
3214
|
* todo_list → tool_use "TodoWrite" (replaced in place on each update)
|
|
@@ -2959,7 +3221,7 @@ export class StructuredSessionManager {
|
|
|
2959
3221
|
* `turnState.codexBlockIndex`; tool_use ↔ tool_result pairing still goes
|
|
2960
3222
|
* through `upsertCodexBlock` by matching ids.
|
|
2961
3223
|
*/
|
|
2962
|
-
extractCodexItemBlock(item, completed) {
|
|
3224
|
+
extractCodexItemBlock(item, completed, beforeSnapshots, afterSnapshots) {
|
|
2963
3225
|
const id = typeof item.id === "string" ? item.id : randomUUID();
|
|
2964
3226
|
const type = typeof item.type === "string" ? item.type : "unknown";
|
|
2965
3227
|
if (type === "message") {
|
|
@@ -3055,48 +3317,7 @@ export class StructuredSessionManager {
|
|
|
3055
3317
|
return buildCodexPatchApplyBlocks(item);
|
|
3056
3318
|
}
|
|
3057
3319
|
if (type === "file_change") {
|
|
3058
|
-
|
|
3059
|
-
// 这里每个 file 一个 sub-id(`${item.id}#${i}`),这样如果 codex 一次给多
|
|
3060
|
-
// 个文件,每个文件能独立成卡片 + 独立 tool_result 状态。
|
|
3061
|
-
const rawChanges = Array.isArray(item.changes) ? item.changes : [];
|
|
3062
|
-
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
3063
|
-
const isError = status === "failed";
|
|
3064
|
-
const blocks = [];
|
|
3065
|
-
rawChanges.forEach((entry, idx) => {
|
|
3066
|
-
if (!entry || typeof entry !== "object")
|
|
3067
|
-
return;
|
|
3068
|
-
const change = entry;
|
|
3069
|
-
const path = typeof change.path === "string" ? change.path : "";
|
|
3070
|
-
const kind = typeof change.kind === "string" ? change.kind : "update";
|
|
3071
|
-
const subId = `${id}#${idx}`;
|
|
3072
|
-
let toolName;
|
|
3073
|
-
let input;
|
|
3074
|
-
if (kind === "add") {
|
|
3075
|
-
toolName = "Write";
|
|
3076
|
-
input = { file_path: path, content: "", kind, status };
|
|
3077
|
-
}
|
|
3078
|
-
else if (kind === "delete") {
|
|
3079
|
-
toolName = "Edit";
|
|
3080
|
-
input = { file_path: path, kind, status };
|
|
3081
|
-
}
|
|
3082
|
-
else {
|
|
3083
|
-
toolName = "Edit";
|
|
3084
|
-
input = { file_path: path, old_string: "", new_string: "", kind, status };
|
|
3085
|
-
}
|
|
3086
|
-
if (!completed) {
|
|
3087
|
-
blocks.push({ type: "tool_use", id: subId, name: toolName, input });
|
|
3088
|
-
}
|
|
3089
|
-
else {
|
|
3090
|
-
blocks.push({ type: "tool_use", id: subId, name: toolName, input });
|
|
3091
|
-
blocks.push({
|
|
3092
|
-
type: "tool_result",
|
|
3093
|
-
tool_use_id: subId,
|
|
3094
|
-
content: isError ? `file change failed: ${path}` : "",
|
|
3095
|
-
is_error: isError,
|
|
3096
|
-
});
|
|
3097
|
-
}
|
|
3098
|
-
});
|
|
3099
|
-
return blocks;
|
|
3320
|
+
return buildCodexFileChangeBlocks(item, completed, beforeSnapshots, afterSnapshots);
|
|
3100
3321
|
}
|
|
3101
3322
|
if (type === "mcp_tool_call_end") {
|
|
3102
3323
|
return this.codexMcpToolBlocks(item);
|
package/dist/tui/attach.js
CHANGED
|
@@ -87,7 +87,7 @@ export function startAttachTui(deps) {
|
|
|
87
87
|
}
|
|
88
88
|
if (h.sessionCounts.total !== lastTotal) {
|
|
89
89
|
if (lastTotal !== -1) {
|
|
90
|
-
appendActivity(`会话计数: ${h.sessionCounts.
|
|
90
|
+
appendActivity(`会话计数: ${h.sessionCounts.total}`, "info");
|
|
91
91
|
}
|
|
92
92
|
lastTotal = h.sessionCounts.total;
|
|
93
93
|
}
|
package/dist/tui/layout.js
CHANGED
|
@@ -174,9 +174,7 @@ export function buildLayout() {
|
|
|
174
174
|
}
|
|
175
175
|
function refreshHeader(info) {
|
|
176
176
|
const counts = info.sessionCounts;
|
|
177
|
-
const sess = `{
|
|
178
|
-
`{gray-fg}${counts.archived}{/} archived · ` +
|
|
179
|
-
`{white-fg}${counts.total}{/} total`;
|
|
177
|
+
const sess = `{white-fg}${counts.total}{/} sessions`;
|
|
180
178
|
const orphan = info.orphanRecoveredCount > 0
|
|
181
179
|
? ` {gray-fg}(${info.orphanRecoveredCount} orphan PTYs cleaned){/}`
|
|
182
180
|
: "";
|
|
@@ -500,7 +498,6 @@ function toneColor(tone) {
|
|
|
500
498
|
case "failed": return "red";
|
|
501
499
|
case "stopped": return "yellow";
|
|
502
500
|
case "exited": return "gray";
|
|
503
|
-
case "archived": return "gray";
|
|
504
501
|
default: return "white";
|
|
505
502
|
}
|
|
506
503
|
}
|
|
@@ -7,11 +7,11 @@ export interface SessionRow {
|
|
|
7
7
|
state: string;
|
|
8
8
|
duration: string;
|
|
9
9
|
/** 用于上色的语义级别(blessed tag 由调用方决定)。 */
|
|
10
|
-
tone: "running" | "idle" | "
|
|
10
|
+
tone: "running" | "idle" | "exited" | "failed" | "stopped";
|
|
11
11
|
}
|
|
12
12
|
/** 把绝对路径压缩为友好显示:`~/foo` 或者长路径只保留尾部。 */
|
|
13
13
|
export declare function shortenCwd(cwd: string, max?: number): string;
|
|
14
14
|
/** 从 SessionSnapshot 推导出一行表格数据。状态规则见 plan。 */
|
|
15
15
|
export declare function formatSession(snap: SessionSnapshot, now?: number): SessionRow;
|
|
16
|
-
/**
|
|
16
|
+
/** 单一会话列表按开始时间倒序展示,不再按归档状态分组。 */
|
|
17
17
|
export declare function sortRows(snaps: SessionSnapshot[]): SessionSnapshot[];
|
|
@@ -22,17 +22,6 @@ export function shortenCwd(cwd, max = 28) {
|
|
|
22
22
|
export function formatSession(snap, now = Date.now()) {
|
|
23
23
|
const runner = (snap.runner || snap.provider || "pty").toString();
|
|
24
24
|
const cwd = shortenCwd(snap.cwd);
|
|
25
|
-
if (snap.archived) {
|
|
26
|
-
return {
|
|
27
|
-
id: snap.id,
|
|
28
|
-
glyph: "○",
|
|
29
|
-
runner,
|
|
30
|
-
cwd,
|
|
31
|
-
state: "archived",
|
|
32
|
-
duration: "",
|
|
33
|
-
tone: "archived",
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
25
|
switch (snap.status) {
|
|
37
26
|
case "running": {
|
|
38
27
|
const hasTask = typeof snap.currentTaskTitle === "string" && snap.currentTaskTitle.length > 0;
|
|
@@ -79,33 +68,7 @@ export function formatSession(snap, now = Date.now()) {
|
|
|
79
68
|
};
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
|
-
/**
|
|
71
|
+
/** 单一会话列表按开始时间倒序展示,不再按归档状态分组。 */
|
|
83
72
|
export function sortRows(snaps) {
|
|
84
|
-
|
|
85
|
-
running: 0,
|
|
86
|
-
idle: 1,
|
|
87
|
-
failed: 2,
|
|
88
|
-
stopped: 3,
|
|
89
|
-
exited: 4,
|
|
90
|
-
archived: 5,
|
|
91
|
-
};
|
|
92
|
-
return [...snaps].sort((a, b) => {
|
|
93
|
-
const ra = a.archived
|
|
94
|
-
? rank.archived
|
|
95
|
-
: a.status === "running"
|
|
96
|
-
? a.currentTaskTitle
|
|
97
|
-
? rank.running
|
|
98
|
-
: rank.idle
|
|
99
|
-
: rank[a.status] ?? 99;
|
|
100
|
-
const rb = b.archived
|
|
101
|
-
? rank.archived
|
|
102
|
-
: b.status === "running"
|
|
103
|
-
? b.currentTaskTitle
|
|
104
|
-
? rank.running
|
|
105
|
-
: rank.idle
|
|
106
|
-
: rank[b.status] ?? 99;
|
|
107
|
-
if (ra !== rb)
|
|
108
|
-
return ra - rb;
|
|
109
|
-
return b.startedAt.localeCompare(a.startedAt);
|
|
110
|
-
});
|
|
73
|
+
return [...snaps].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
111
74
|
}
|