@co0ontty/wand 4.30.0 → 4.31.0

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": "e05a081ccabf4c588e9ece25430be61f0cf0cae9",
3
- "builtAt": "2026-08-06T02:00:45.524Z",
4
- "version": "4.30.0",
2
+ "commit": "963337ac2ab326332080b2ca10b39f1ccce64665",
3
+ "builtAt": "2026-08-06T12:29:30.333Z",
4
+ "version": "4.31.0",
5
5
  "channel": "stable"
6
6
  }
@@ -5,6 +5,7 @@ import { WandStorage } from "./storage.js";
5
5
  import { ExecutionMode, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
6
6
  import { toSessionListItemDTO } from "./session-transport.js";
7
7
  import { SessionRegistry } from "./session-registry.js";
8
+ import { type SessionDirectoryNode } from "./session-directory-tree.js";
8
9
  export declare function parseExecutionMode(value: unknown, fallback: ExecutionMode): ExecutionMode;
9
10
  export declare function parseSessionCreationOrigin(body: {
10
11
  sessionSource?: unknown;
@@ -50,7 +51,15 @@ export interface SessionListPage {
50
51
  total: number;
51
52
  revision: string;
52
53
  }
54
+ export interface SessionDirectoryTreeResponse {
55
+ roots: SessionDirectoryNode<SessionListPageEntry>[];
56
+ totalSessions: number;
57
+ directoryCount: number;
58
+ revision: string;
59
+ }
60
+ export declare function buildSessionListEntries(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionListPageEntry[];
53
61
  export declare function buildSessionListPage(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, offset: number, limit: number, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionListPage;
62
+ export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionDirectoryTreeResponse;
54
63
  /**
55
64
  * Provider history is scanned by ProcessManager, but structured sessions live
56
65
  * in StructuredSessionManager. Annotate against the combined session list so a
@@ -12,6 +12,7 @@ import { buildProviderResumeCommand, isProviderSessionId, isSafeProviderSessionI
12
12
  import { parseBoundedInteger } from "./request-limits.js";
13
13
  import { asyncRoute } from "./express-async.js";
14
14
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
15
+ import { buildDirectoryTree, } from "./session-directory-tree.js";
15
16
  export function parseExecutionMode(value, fallback) {
16
17
  if (value === undefined)
17
18
  return fallback;
@@ -196,7 +197,7 @@ function sessionSortTimestamp(snapshot) {
196
197
  const timestamp = Date.parse(snapshot.startedAt);
197
198
  return Number.isFinite(timestamp) ? timestamp : 0;
198
199
  }
199
- export function buildSessionListPage(sessions, claudeHistory, codexHistory, hiddenHistoryIds, offset, limit, openCodeHistory = [], qoderHistory = []) {
200
+ export function buildSessionListEntries(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory = [], qoderHistory = []) {
200
201
  const managed = sessions.map((session) => ({
201
202
  type: "managed",
202
203
  key: `session-${session.id}`,
@@ -222,14 +223,20 @@ export function buildSessionListPage(sessions, claudeHistory, codexHistory, hidd
222
223
  history: { ...history, provider },
223
224
  }];
224
225
  });
225
- const entries = [...managed, ...recoverable].sort((left, right) => {
226
+ return [...managed, ...recoverable].sort((left, right) => {
226
227
  const timestampOrder = right.sortTimestamp - left.sortTimestamp;
227
228
  return timestampOrder || left.key.localeCompare(right.key);
228
229
  });
229
- const boundedOffset = Math.min(Math.max(offset, 0), entries.length);
230
- const revision = createHash("sha256")
230
+ }
231
+ function sessionListRevision(entries) {
232
+ return createHash("sha256")
231
233
  .update(JSON.stringify(entries.map((entry) => [entry.key, entry.sortTimestamp])))
232
234
  .digest("base64url");
235
+ }
236
+ export function buildSessionListPage(sessions, claudeHistory, codexHistory, hiddenHistoryIds, offset, limit, openCodeHistory = [], qoderHistory = []) {
237
+ const entries = buildSessionListEntries(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory, qoderHistory);
238
+ const boundedOffset = Math.min(Math.max(offset, 0), entries.length);
239
+ const revision = sessionListRevision(entries);
233
240
  return {
234
241
  entries: entries.slice(boundedOffset, boundedOffset + limit),
235
242
  offset: boundedOffset,
@@ -237,6 +244,15 @@ export function buildSessionListPage(sessions, claudeHistory, codexHistory, hidd
237
244
  revision,
238
245
  };
239
246
  }
247
+ export function buildSessionDirectoryTree(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory = [], qoderHistory = []) {
248
+ const entries = buildSessionListEntries(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory, qoderHistory);
249
+ const tree = buildDirectoryTree(entries.map((entry) => ({
250
+ entry,
251
+ cwd: entry.type === "managed" ? entry.session.cwd : entry.history.cwd,
252
+ sortTimestamp: entry.sortTimestamp,
253
+ })));
254
+ return { ...tree, revision: sessionListRevision(entries) };
255
+ }
240
256
  /**
241
257
  * Provider history is scanned by ProcessManager, but structured sessions live
242
258
  * in StructuredSessionManager. Annotate against the combined session list so a
@@ -414,6 +430,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
414
430
  res.status(500).json({ error: getErrorMessage(error, "无法加载会话列表。") });
415
431
  }
416
432
  });
433
+ app.get("/api/session-directories", (_req, res) => {
434
+ try {
435
+ res.json(buildSessionDirectoryTree(sessions.listSlim(), processes.listClaudeHistorySessions(), processes.listCodexHistorySessions(), getHiddenClaudeSessionIds(storage), processes.listOpenCodeHistorySessions(), processes.listQoderHistorySessions()));
436
+ }
437
+ catch (error) {
438
+ res.status(500).json({ error: getErrorMessage(error, "无法加载会话目录。") });
439
+ }
440
+ });
417
441
  app.get("/api/sessions", (_req, res) => {
418
442
  res.json(sessions.listSlim().map(toSessionListItemDTO));
419
443
  });
@@ -0,0 +1,27 @@
1
+ export interface SessionDirectorySource<T> {
2
+ entry: T;
3
+ cwd: string;
4
+ sortTimestamp: number;
5
+ }
6
+ export interface SessionDirectoryNode<T> {
7
+ /** Exact path used when starting a new session. Empty only for the synthetic unknown group. */
8
+ path: string;
9
+ /** Compact path segment label. Single-child ancestors are folded into this label. */
10
+ name: string;
11
+ synthetic: boolean;
12
+ /** Sessions whose cwd is exactly this node. */
13
+ directCount: number;
14
+ /** Sessions at this node or any descendant node. */
15
+ totalCount: number;
16
+ latestTimestamp: number;
17
+ entries: T[];
18
+ children: SessionDirectoryNode<T>[];
19
+ }
20
+ export interface SessionDirectoryTree<T> {
21
+ roots: SessionDirectoryNode<T>[];
22
+ totalSessions: number;
23
+ /** Number of distinct real working directories, excluding ancestor-only and unknown nodes. */
24
+ directoryCount: number;
25
+ }
26
+ export declare function normalizeSessionDirectory(value: string): string;
27
+ export declare function buildDirectoryTree<T>(sources: readonly SessionDirectorySource<T>[], unknownLabel?: string): SessionDirectoryTree<T>;
@@ -0,0 +1,130 @@
1
+ import path from "node:path";
2
+ function pathApiFor(value) {
3
+ return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\")
4
+ ? path.win32
5
+ : path.posix;
6
+ }
7
+ export function normalizeSessionDirectory(value) {
8
+ const trimmed = value.trim();
9
+ if (!trimmed)
10
+ return "";
11
+ const api = pathApiFor(trimmed);
12
+ const normalized = api.normalize(trimmed);
13
+ const root = api.parse(normalized).root;
14
+ let end = normalized.length;
15
+ while (end > root.length && normalized[end - 1] === api.sep)
16
+ end -= 1;
17
+ return normalized.slice(0, end);
18
+ }
19
+ function ensureChild(parent, nodePath, name) {
20
+ const existing = parent.get(nodePath);
21
+ if (existing)
22
+ return existing;
23
+ const created = {
24
+ path: nodePath,
25
+ name,
26
+ synthetic: false,
27
+ entries: [],
28
+ children: new Map(),
29
+ };
30
+ parent.set(nodePath, created);
31
+ return created;
32
+ }
33
+ function insertPath(roots, cwd) {
34
+ const api = pathApiFor(cwd);
35
+ const parsed = api.parse(cwd);
36
+ const parts = cwd
37
+ .slice(parsed.root.length)
38
+ .split(api.sep)
39
+ .filter(Boolean);
40
+ let container = roots;
41
+ let currentPath = parsed.root;
42
+ let current = null;
43
+ if (parsed.root) {
44
+ current = ensureChild(container, parsed.root, parsed.root);
45
+ container = current.children;
46
+ }
47
+ for (const part of parts) {
48
+ currentPath = currentPath ? api.join(currentPath, part) : part;
49
+ current = ensureChild(container, currentPath, part);
50
+ container = current.children;
51
+ }
52
+ // path.normalize(".") has no segments or root. Keep it as a usable node.
53
+ return current ?? ensureChild(roots, cwd, cwd);
54
+ }
55
+ function compactLabel(parent, child, childPath) {
56
+ const separator = pathApiFor(childPath).sep;
57
+ if (parent === separator)
58
+ return `${separator}${child}`;
59
+ if (parent.endsWith(separator))
60
+ return `${parent}${child}`;
61
+ return `${parent}${separator}${child}`;
62
+ }
63
+ function finalizeNode(node) {
64
+ let children = [...node.children.values()].map(finalizeNode);
65
+ const sortedEntries = node.entries
66
+ .slice()
67
+ .sort((left, right) => right.sortTimestamp - left.sortTimestamp);
68
+ let result = {
69
+ path: node.path,
70
+ name: node.name,
71
+ synthetic: node.synthetic,
72
+ directCount: sortedEntries.length,
73
+ totalCount: sortedEntries.length + children.reduce((sum, child) => sum + child.totalCount, 0),
74
+ latestTimestamp: Math.max(0, ...sortedEntries.map((item) => item.sortTimestamp), ...children.map((child) => child.latestTimestamp)),
75
+ entries: sortedEntries.map((item) => item.entry),
76
+ children,
77
+ };
78
+ // A filesystem root followed by a single unambiguous chain is visual noise in
79
+ // a 280-300px sidebar. Fold it while retaining the exact descendant path.
80
+ while (!result.synthetic && result.directCount === 0 && result.children.length === 1) {
81
+ const child = result.children[0];
82
+ result = {
83
+ ...child,
84
+ name: compactLabel(result.name, child.name, child.path),
85
+ };
86
+ }
87
+ children = result.children.slice().sort((left, right) => {
88
+ const latestOrder = right.latestTimestamp - left.latestTimestamp;
89
+ return latestOrder || left.name.localeCompare(right.name);
90
+ });
91
+ return { ...result, children };
92
+ }
93
+ export function buildDirectoryTree(sources, unknownLabel = "未知目录") {
94
+ const roots = new Map();
95
+ let unknown = null;
96
+ const realDirectories = new Set();
97
+ for (const source of sources) {
98
+ const cwd = normalizeSessionDirectory(source.cwd);
99
+ let node;
100
+ if (!cwd) {
101
+ unknown ??= {
102
+ path: "",
103
+ name: unknownLabel,
104
+ synthetic: true,
105
+ entries: [],
106
+ children: new Map(),
107
+ };
108
+ node = unknown;
109
+ }
110
+ else {
111
+ realDirectories.add(cwd);
112
+ node = insertPath(roots, cwd);
113
+ }
114
+ node.entries.push({ entry: source.entry, sortTimestamp: source.sortTimestamp });
115
+ }
116
+ const finalized = [...roots.values()].map(finalizeNode);
117
+ if (unknown)
118
+ finalized.push(finalizeNode(unknown));
119
+ finalized.sort((left, right) => {
120
+ if (left.synthetic !== right.synthetic)
121
+ return left.synthetic ? 1 : -1;
122
+ const latestOrder = right.latestTimestamp - left.latestTimestamp;
123
+ return latestOrder || left.name.localeCompare(right.name);
124
+ });
125
+ return {
126
+ roots: finalized,
127
+ totalSessions: sources.length,
128
+ directoryCount: realDirectories.size,
129
+ };
130
+ }