@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.
- package/dist/build-info.json +3 -3
- package/dist/server-session-routes.d.ts +9 -0
- package/dist/server-session-routes.js +28 -4
- package/dist/session-directory-tree.d.ts +27 -0
- package/dist/session-directory-tree.js +130 -0
- package/dist/web-ui/content/scripts.js +61 -61
- 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-08-
|
|
4
|
-
"version": "4.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
+
}
|