@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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "c470535a559107d4b5f27a9cc7c7a4e7b057352e",
3
- "builtAt": "2026-07-10T13:26:51.037Z",
4
- "version": "2.4.4",
5
- "channel": "stable"
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 ${active} active · ${archived} archived · ${all.length} total${orphan}`,
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 tool_use per file, named Edit/Write/Bash by `kind`
208
- * (codex does NOT carry old_string/new_string in the
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
- if (typeof content === "string") {
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 blocks = this.extractCodexItemBlock(item, completed);
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 tool_use per file, named Edit/Write/Bash by `kind`
2947
- * (codex does NOT carry old_string/new_string in the
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
- // 注意:codex exec stream 没有 old_string/new_string——只给 path + kind。
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);
@@ -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.active} active · ${h.sessionCounts.archived} archived · ${h.sessionCounts.total} total`, "info");
90
+ appendActivity(`会话计数: ${h.sessionCounts.total}`, "info");
91
91
  }
92
92
  lastTotal = h.sessionCounts.total;
93
93
  }
@@ -174,9 +174,7 @@ export function buildLayout() {
174
174
  }
175
175
  function refreshHeader(info) {
176
176
  const counts = info.sessionCounts;
177
- const sess = `{green-fg}${counts.active}{/} active · ` +
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" | "archived" | "exited" | "failed" | "stopped";
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
- /** 把行集按"活跃优先 → idle → 已结束 → archived"排序,再按开始时间倒序。 */
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
- /** 把行集按"活跃优先 → idle → 已结束 → archived"排序,再按开始时间倒序。 */
71
+ /** 单一会话列表按开始时间倒序展示,不再按归档状态分组。 */
83
72
  export function sortRows(snaps) {
84
- const rank = {
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
  }