@co0ontty/wand 4.31.0 → 4.32.0-beta.gefb23cd
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/server-session-routes.d.ts +4 -1
- package/dist/server-session-routes.js +69 -7
- package/dist/server.js +1 -0
- package/dist/session-directory-tree.d.ts +3 -1
- package/dist/session-directory-tree.js +10 -8
- package/dist/storage.d.ts +4 -0
- package/dist/storage.js +33 -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.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "efb23cdde52eafc12024c59c5c1259d73c2054fa",
|
|
3
|
+
"builtAt": "2026-08-06T23:29:50.325Z",
|
|
4
|
+
"version": "4.32.0-beta.gefb23cd",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
|
@@ -55,11 +55,14 @@ export interface SessionDirectoryTreeResponse {
|
|
|
55
55
|
roots: SessionDirectoryNode<SessionListPageEntry>[];
|
|
56
56
|
totalSessions: number;
|
|
57
57
|
directoryCount: number;
|
|
58
|
+
/** Session-entry revision, kept identical to /api/session-list for pagination compatibility. */
|
|
58
59
|
revision: string;
|
|
60
|
+
/** Full directory-tree revision, including custom workspace names. */
|
|
61
|
+
treeRevision: string;
|
|
59
62
|
}
|
|
60
63
|
export declare function buildSessionListEntries(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[]): SessionListPageEntry[];
|
|
61
64
|
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;
|
|
65
|
+
export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, openCodeHistory?: ProviderHistorySession[], qoderHistory?: ProviderHistorySession[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
|
|
63
66
|
/**
|
|
64
67
|
* Provider history is scanned by ProcessManager, but structured sessions live
|
|
65
68
|
* in StructuredSessionManager. Annotate against the combined session list so a
|
|
@@ -12,7 +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
|
+
import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
|
|
16
16
|
export function parseExecutionMode(value, fallback) {
|
|
17
17
|
if (value === undefined)
|
|
18
18
|
return fallback;
|
|
@@ -228,9 +228,16 @@ export function buildSessionListEntries(sessions, claudeHistory, codexHistory, h
|
|
|
228
228
|
return timestampOrder || left.key.localeCompare(right.key);
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
|
-
function sessionListRevision(entries) {
|
|
231
|
+
function sessionListRevision(entries, directoryNames = []) {
|
|
232
|
+
const entryState = entries.map((entry) => [entry.key, entry.sortTimestamp]);
|
|
233
|
+
const revisionState = directoryNames.length === 0
|
|
234
|
+
? entryState
|
|
235
|
+
: {
|
|
236
|
+
entries: entryState,
|
|
237
|
+
directoryNames: directoryNames.slice().sort(([left], [right]) => left.localeCompare(right)),
|
|
238
|
+
};
|
|
232
239
|
return createHash("sha256")
|
|
233
|
-
.update(JSON.stringify(
|
|
240
|
+
.update(JSON.stringify(revisionState))
|
|
234
241
|
.digest("base64url");
|
|
235
242
|
}
|
|
236
243
|
export function buildSessionListPage(sessions, claudeHistory, codexHistory, hiddenHistoryIds, offset, limit, openCodeHistory = [], qoderHistory = []) {
|
|
@@ -244,14 +251,31 @@ export function buildSessionListPage(sessions, claudeHistory, codexHistory, hidd
|
|
|
244
251
|
revision,
|
|
245
252
|
};
|
|
246
253
|
}
|
|
247
|
-
export function buildSessionDirectoryTree(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory = [], qoderHistory = []) {
|
|
254
|
+
export function buildSessionDirectoryTree(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory = [], qoderHistory = [], customNames = new Map()) {
|
|
248
255
|
const entries = buildSessionListEntries(sessions, claudeHistory, codexHistory, hiddenHistoryIds, openCodeHistory, qoderHistory);
|
|
249
256
|
const tree = buildDirectoryTree(entries.map((entry) => ({
|
|
250
257
|
entry,
|
|
251
258
|
cwd: entry.type === "managed" ? entry.session.cwd : entry.history.cwd,
|
|
252
259
|
sortTimestamp: entry.sortTimestamp,
|
|
253
|
-
})));
|
|
254
|
-
|
|
260
|
+
})), "未知目录", customNames);
|
|
261
|
+
const appliedCustomNames = [];
|
|
262
|
+
const collectCustomNames = (nodes) => {
|
|
263
|
+
for (const node of nodes) {
|
|
264
|
+
if (node.customName)
|
|
265
|
+
appliedCustomNames.push([node.path, node.customName]);
|
|
266
|
+
collectCustomNames(node.children);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
collectCustomNames(tree.roots);
|
|
270
|
+
return {
|
|
271
|
+
...tree,
|
|
272
|
+
revision: sessionListRevision(entries),
|
|
273
|
+
treeRevision: sessionListRevision(entries, appliedCustomNames),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function directoryTreeContainsPath(nodes, directoryPath) {
|
|
277
|
+
return nodes.some((node) => ((!node.synthetic && node.path === directoryPath)
|
|
278
|
+
|| directoryTreeContainsPath(node.children, directoryPath)));
|
|
255
279
|
}
|
|
256
280
|
/**
|
|
257
281
|
* Provider history is scanned by ProcessManager, but structured sessions live
|
|
@@ -413,6 +437,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
413
437
|
messageTotal: windowed.messageTotal,
|
|
414
438
|
});
|
|
415
439
|
};
|
|
440
|
+
const currentDirectoryTree = () => buildSessionDirectoryTree(sessions.listSlim(), processes.listClaudeHistorySessions(), processes.listCodexHistorySessions(), getHiddenClaudeSessionIds(storage), processes.listOpenCodeHistorySessions(), processes.listQoderHistorySessions(), storage.listSessionDirectoryNames());
|
|
416
441
|
app.get("/api/session-list", (req, res) => {
|
|
417
442
|
try {
|
|
418
443
|
const offset = parseBoundedInteger(req.query.offset, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
@@ -432,12 +457,49 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
432
457
|
});
|
|
433
458
|
app.get("/api/session-directories", (_req, res) => {
|
|
434
459
|
try {
|
|
435
|
-
res.json(
|
|
460
|
+
res.json(currentDirectoryTree());
|
|
436
461
|
}
|
|
437
462
|
catch (error) {
|
|
438
463
|
res.status(500).json({ error: getErrorMessage(error, "无法加载会话目录。") });
|
|
439
464
|
}
|
|
440
465
|
});
|
|
466
|
+
app.put("/api/session-directories/name", (req, res) => {
|
|
467
|
+
const body = req.body;
|
|
468
|
+
if (typeof body?.path !== "string") {
|
|
469
|
+
res.status(400).json({ error: "path 必须是当前工作区的目录路径。" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const directoryPath = normalizeSessionDirectory(body.path);
|
|
473
|
+
if (!directoryPath) {
|
|
474
|
+
res.status(400).json({ error: "不能重命名未知目录。" });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (body.name !== null && typeof body.name !== "string") {
|
|
478
|
+
res.status(400).json({ error: "name 必须是字符串或 null。" });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const customName = typeof body.name === "string" ? body.name.trim() : "";
|
|
482
|
+
if (Array.from(customName).length > 80) {
|
|
483
|
+
res.status(400).json({ error: "工作区名称不能超过 80 个字符。" });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (/[\u0000-\u001F\u007F-\u009F\u2028\u2029]/u.test(customName)) {
|
|
487
|
+
res.status(400).json({ error: "工作区名称不能包含换行或控制字符。" });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const tree = currentDirectoryTree();
|
|
492
|
+
if (!directoryTreeContainsPath(tree.roots, directoryPath)) {
|
|
493
|
+
res.status(404).json({ error: "未找到该会话目录。" });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
storage.setSessionDirectoryName(directoryPath, customName || null);
|
|
497
|
+
res.json({ ok: true, path: directoryPath, name: customName || null });
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
res.status(500).json({ error: getErrorMessage(error, "无法保存工作区名称。") });
|
|
501
|
+
}
|
|
502
|
+
});
|
|
441
503
|
app.get("/api/sessions", (_req, res) => {
|
|
442
504
|
res.json(sessions.listSlim().map(toSessionListItemDTO));
|
|
443
505
|
});
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,8 @@ export interface SessionDirectoryNode<T> {
|
|
|
8
8
|
path: string;
|
|
9
9
|
/** Compact path segment label. Single-child ancestors are folded into this label. */
|
|
10
10
|
name: string;
|
|
11
|
+
/** User-defined workspace label. Clients display this in preference to name. */
|
|
12
|
+
customName?: string;
|
|
11
13
|
synthetic: boolean;
|
|
12
14
|
/** Sessions whose cwd is exactly this node. */
|
|
13
15
|
directCount: number;
|
|
@@ -24,4 +26,4 @@ export interface SessionDirectoryTree<T> {
|
|
|
24
26
|
directoryCount: number;
|
|
25
27
|
}
|
|
26
28
|
export declare function normalizeSessionDirectory(value: string): string;
|
|
27
|
-
export declare function buildDirectoryTree<T>(sources: readonly SessionDirectorySource<T>[], unknownLabel?: string): SessionDirectoryTree<T>;
|
|
29
|
+
export declare function buildDirectoryTree<T>(sources: readonly SessionDirectorySource<T>[], unknownLabel?: string, customNames?: ReadonlyMap<string, string>): SessionDirectoryTree<T>;
|
|
@@ -60,14 +60,16 @@ function compactLabel(parent, child, childPath) {
|
|
|
60
60
|
return `${parent}${child}`;
|
|
61
61
|
return `${parent}${separator}${child}`;
|
|
62
62
|
}
|
|
63
|
-
function finalizeNode(node) {
|
|
64
|
-
let children = [...node.children.values()].map(finalizeNode);
|
|
63
|
+
function finalizeNode(node, customNames) {
|
|
64
|
+
let children = [...node.children.values()].map((child) => finalizeNode(child, customNames));
|
|
65
65
|
const sortedEntries = node.entries
|
|
66
66
|
.slice()
|
|
67
67
|
.sort((left, right) => right.sortTimestamp - left.sortTimestamp);
|
|
68
|
+
const customName = customNames.get(node.path);
|
|
68
69
|
let result = {
|
|
69
70
|
path: node.path,
|
|
70
71
|
name: node.name,
|
|
72
|
+
...(customName ? { customName } : {}),
|
|
71
73
|
synthetic: node.synthetic,
|
|
72
74
|
directCount: sortedEntries.length,
|
|
73
75
|
totalCount: sortedEntries.length + children.reduce((sum, child) => sum + child.totalCount, 0),
|
|
@@ -77,7 +79,7 @@ function finalizeNode(node) {
|
|
|
77
79
|
};
|
|
78
80
|
// A filesystem root followed by a single unambiguous chain is visual noise in
|
|
79
81
|
// a 280-300px sidebar. Fold it while retaining the exact descendant path.
|
|
80
|
-
while (!result.synthetic && result.directCount === 0 && result.children.length === 1) {
|
|
82
|
+
while (!result.synthetic && !result.customName && result.directCount === 0 && result.children.length === 1) {
|
|
81
83
|
const child = result.children[0];
|
|
82
84
|
result = {
|
|
83
85
|
...child,
|
|
@@ -86,11 +88,11 @@ function finalizeNode(node) {
|
|
|
86
88
|
}
|
|
87
89
|
children = result.children.slice().sort((left, right) => {
|
|
88
90
|
const latestOrder = right.latestTimestamp - left.latestTimestamp;
|
|
89
|
-
return latestOrder || left.name.localeCompare(right.name);
|
|
91
|
+
return latestOrder || (left.customName ?? left.name).localeCompare(right.customName ?? right.name);
|
|
90
92
|
});
|
|
91
93
|
return { ...result, children };
|
|
92
94
|
}
|
|
93
|
-
export function buildDirectoryTree(sources, unknownLabel = "未知目录") {
|
|
95
|
+
export function buildDirectoryTree(sources, unknownLabel = "未知目录", customNames = new Map()) {
|
|
94
96
|
const roots = new Map();
|
|
95
97
|
let unknown = null;
|
|
96
98
|
const realDirectories = new Set();
|
|
@@ -113,14 +115,14 @@ export function buildDirectoryTree(sources, unknownLabel = "未知目录") {
|
|
|
113
115
|
}
|
|
114
116
|
node.entries.push({ entry: source.entry, sortTimestamp: source.sortTimestamp });
|
|
115
117
|
}
|
|
116
|
-
const finalized = [...roots.values()].map(finalizeNode);
|
|
118
|
+
const finalized = [...roots.values()].map((node) => finalizeNode(node, customNames));
|
|
117
119
|
if (unknown)
|
|
118
|
-
finalized.push(finalizeNode(unknown));
|
|
120
|
+
finalized.push(finalizeNode(unknown, customNames));
|
|
119
121
|
finalized.sort((left, right) => {
|
|
120
122
|
if (left.synthetic !== right.synthetic)
|
|
121
123
|
return left.synthetic ? 1 : -1;
|
|
122
124
|
const latestOrder = right.latestTimestamp - left.latestTimestamp;
|
|
123
|
-
return latestOrder || left.name.localeCompare(right.name);
|
|
125
|
+
return latestOrder || (left.customName ?? left.name).localeCompare(right.customName ?? right.name);
|
|
124
126
|
});
|
|
125
127
|
return {
|
|
126
128
|
roots: finalized,
|
package/dist/storage.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export declare class WandStorage {
|
|
|
36
36
|
setPreference<T>(key: string, value: T | null | undefined): void;
|
|
37
37
|
/** 判断偏好是否在 DB 中存在(区别于值为 null/false/"")。 */
|
|
38
38
|
hasPreference(key: string): boolean;
|
|
39
|
+
/** Return user-defined workspace labels keyed by normalized session cwd. */
|
|
40
|
+
listSessionDirectoryNames(): Map<string, string>;
|
|
41
|
+
/** Set a workspace label, or remove it when name is null/blank. */
|
|
42
|
+
setSessionDirectoryName(directoryPath: string, name: string | null): void;
|
|
39
43
|
/** Get password from database */
|
|
40
44
|
getPassword(): string | null;
|
|
41
45
|
/** Set password in database */
|
package/dist/storage.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { normalizeSessionDirectory } from "./session-directory-tree.js";
|
|
5
6
|
import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
|
|
6
7
|
function safeJsonParse(raw) {
|
|
7
8
|
if (!raw)
|
|
@@ -450,6 +451,12 @@ const INIT_SQL = `
|
|
|
450
451
|
value TEXT NOT NULL
|
|
451
452
|
);
|
|
452
453
|
|
|
454
|
+
CREATE TABLE IF NOT EXISTS session_directory_names (
|
|
455
|
+
path TEXT PRIMARY KEY,
|
|
456
|
+
name TEXT NOT NULL,
|
|
457
|
+
updated_at TEXT NOT NULL
|
|
458
|
+
);
|
|
459
|
+
|
|
453
460
|
CREATE TABLE IF NOT EXISTS password_vaults (
|
|
454
461
|
id TEXT PRIMARY KEY,
|
|
455
462
|
name TEXT NOT NULL,
|
|
@@ -641,6 +648,32 @@ export class WandStorage {
|
|
|
641
648
|
hasPreference(key) {
|
|
642
649
|
return this.getConfigValue(key) !== null;
|
|
643
650
|
}
|
|
651
|
+
// ============ Session Directory Names ============
|
|
652
|
+
/** Return user-defined workspace labels keyed by normalized session cwd. */
|
|
653
|
+
listSessionDirectoryNames() {
|
|
654
|
+
const rows = this.db
|
|
655
|
+
.prepare("SELECT path, name FROM session_directory_names ORDER BY path ASC")
|
|
656
|
+
.all();
|
|
657
|
+
return new Map(rows.map((row) => [row.path, row.name]));
|
|
658
|
+
}
|
|
659
|
+
/** Set a workspace label, or remove it when name is null/blank. */
|
|
660
|
+
setSessionDirectoryName(directoryPath, name) {
|
|
661
|
+
const normalizedPath = normalizeSessionDirectory(directoryPath);
|
|
662
|
+
if (!normalizedPath)
|
|
663
|
+
throw new Error("会话目录路径不能为空。");
|
|
664
|
+
const normalizedName = name?.trim() ?? "";
|
|
665
|
+
if (!normalizedName) {
|
|
666
|
+
this.db.prepare("DELETE FROM session_directory_names WHERE path = ?").run(normalizedPath);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
this.db
|
|
670
|
+
.prepare(`INSERT INTO session_directory_names (path, name, updated_at)
|
|
671
|
+
VALUES (?, ?, ?)
|
|
672
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
673
|
+
name = excluded.name,
|
|
674
|
+
updated_at = excluded.updated_at`)
|
|
675
|
+
.run(normalizedPath, normalizedName, nowIso());
|
|
676
|
+
}
|
|
644
677
|
/** Get password from database */
|
|
645
678
|
getPassword() {
|
|
646
679
|
return this.getConfigValue("password");
|