@co0ontty/wand 4.15.0 → 4.16.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/browser-extension/src/options.css +39 -5
- package/browser-extension/src/popup.css +64 -5
- package/dist/build-info.json +3 -3
- package/dist/server-session-routes.d.ts +27 -0
- package/dist/server-session-routes.js +59 -0
- package/dist/session-transport.d.ts +6 -1
- package/dist/session-transport.js +15 -1
- package/dist/structured-claude-protocol.js +10 -0
- package/dist/structured-client-protocol.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/web-ui/content/scripts.js +268 -95
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
color-scheme: light;
|
|
3
|
-
font-family:
|
|
3
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
4
4
|
color: #172033;
|
|
5
5
|
background: #f5f7fb;
|
|
6
6
|
}
|
|
@@ -62,16 +62,35 @@ p {
|
|
|
62
62
|
min-height: 16px;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
input {
|
|
65
|
+
input:not([type="checkbox"]) {
|
|
66
|
+
appearance: none;
|
|
66
67
|
width: 100%;
|
|
67
|
-
min-height:
|
|
68
|
+
min-height: 44px;
|
|
68
69
|
border: 1px solid #cbd5e1;
|
|
69
|
-
border-radius:
|
|
70
|
-
padding: 0
|
|
70
|
+
border-radius: 12px;
|
|
71
|
+
padding: 0 13px;
|
|
72
|
+
outline: none;
|
|
73
|
+
background: rgba(255, 255, 255, .84);
|
|
71
74
|
font: inherit;
|
|
72
75
|
color: #172033;
|
|
76
|
+
caret-color: #2458a6;
|
|
77
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7);
|
|
78
|
+
transition: border-color 160ms cubic-bezier(.2,.8,.2,1), background-color 160ms cubic-bezier(.2,.8,.2,1), box-shadow 160ms cubic-bezier(.2,.8,.2,1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
input:not([type="checkbox"]):hover {
|
|
82
|
+
border-color: #9babc2;
|
|
83
|
+
background: #fff;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
input:not([type="checkbox"]):focus {
|
|
87
|
+
border-color: #2458a6;
|
|
88
|
+
background: #fff;
|
|
89
|
+
box-shadow: 0 0 0 3px rgba(36, 88, 166, .14), 0 10px 24px -20px rgba(36, 88, 166, .65);
|
|
73
90
|
}
|
|
74
91
|
|
|
92
|
+
input:not([type="checkbox"])::placeholder { color: #667189; opacity: .72; }
|
|
93
|
+
|
|
75
94
|
.actions {
|
|
76
95
|
display: flex;
|
|
77
96
|
gap: 8px;
|
|
@@ -89,6 +108,12 @@ button {
|
|
|
89
108
|
cursor: pointer;
|
|
90
109
|
}
|
|
91
110
|
|
|
111
|
+
button:focus-visible,
|
|
112
|
+
input:not([type="checkbox"]):focus-visible {
|
|
113
|
+
outline: none;
|
|
114
|
+
box-shadow: 0 0 0 3px rgba(36, 88, 166, .18);
|
|
115
|
+
}
|
|
116
|
+
|
|
92
117
|
button:hover {
|
|
93
118
|
background: #edf4ff;
|
|
94
119
|
}
|
|
@@ -116,3 +141,12 @@ button:hover {
|
|
|
116
141
|
.status.ok {
|
|
117
142
|
color: #1f6b45;
|
|
118
143
|
}
|
|
144
|
+
|
|
145
|
+
@media (prefers-reduced-motion: reduce) {
|
|
146
|
+
input:not([type="checkbox"]) { transition: none; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@media (prefers-contrast: more) {
|
|
150
|
+
input:not([type="checkbox"]) { border-color: #526079; background: #fff; box-shadow: none; }
|
|
151
|
+
input:not([type="checkbox"]):focus { border-color: #143f78; box-shadow: 0 0 0 2px #143f78; }
|
|
152
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
color-scheme: light;
|
|
3
|
-
font-family:
|
|
3
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
4
4
|
color: #182033;
|
|
5
5
|
background: #f6f8fb;
|
|
6
6
|
}
|
|
@@ -107,12 +107,35 @@ button:hover {
|
|
|
107
107
|
.field textarea,
|
|
108
108
|
.generated input {
|
|
109
109
|
width: 100%;
|
|
110
|
-
min-height:
|
|
110
|
+
min-height: 40px;
|
|
111
111
|
border: 1px solid #cbd5e1;
|
|
112
|
-
border-radius:
|
|
113
|
-
padding: 0
|
|
114
|
-
|
|
112
|
+
border-radius: 11px;
|
|
113
|
+
padding: 0 12px;
|
|
114
|
+
outline: none;
|
|
115
|
+
background: rgba(255, 255, 255, .88);
|
|
115
116
|
color: #182033;
|
|
117
|
+
caret-color: #2458a6;
|
|
118
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7);
|
|
119
|
+
transition: border-color 160ms cubic-bezier(.2,.8,.2,1), background-color 160ms cubic-bezier(.2,.8,.2,1), box-shadow 160ms cubic-bezier(.2,.8,.2,1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.search-row input:hover,
|
|
123
|
+
.field input:hover,
|
|
124
|
+
.field select:hover,
|
|
125
|
+
.field textarea:hover,
|
|
126
|
+
.generated input:hover {
|
|
127
|
+
border-color: #9babc2;
|
|
128
|
+
background: #fff;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.search-row input:focus,
|
|
132
|
+
.field input:focus,
|
|
133
|
+
.field select:focus,
|
|
134
|
+
.field textarea:focus,
|
|
135
|
+
.generated input:focus {
|
|
136
|
+
border-color: #2458a6;
|
|
137
|
+
background: #fff;
|
|
138
|
+
box-shadow: 0 0 0 3px rgba(36, 88, 166, .14), 0 10px 24px -20px rgba(36, 88, 166, .65);
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
.field textarea {
|
|
@@ -121,6 +144,20 @@ button:hover {
|
|
|
121
144
|
resize: vertical;
|
|
122
145
|
}
|
|
123
146
|
|
|
147
|
+
.search-row input::placeholder,
|
|
148
|
+
.field input::placeholder,
|
|
149
|
+
.field textarea::placeholder { color: #667189; opacity: .72; }
|
|
150
|
+
|
|
151
|
+
button:focus-visible,
|
|
152
|
+
.search-row input:focus-visible,
|
|
153
|
+
.field input:focus-visible,
|
|
154
|
+
.field select:focus-visible,
|
|
155
|
+
.field textarea:focus-visible,
|
|
156
|
+
.generated input:focus-visible {
|
|
157
|
+
outline: none;
|
|
158
|
+
box-shadow: 0 0 0 3px rgba(36, 88, 166, .18);
|
|
159
|
+
}
|
|
160
|
+
|
|
124
161
|
.tabs {
|
|
125
162
|
display: grid;
|
|
126
163
|
grid-template-columns: repeat(4, 1fr);
|
|
@@ -263,6 +300,28 @@ button:hover {
|
|
|
263
300
|
color: #667189;
|
|
264
301
|
}
|
|
265
302
|
|
|
303
|
+
@media (prefers-reduced-motion: reduce) {
|
|
304
|
+
.search-row input,
|
|
305
|
+
.field input,
|
|
306
|
+
.field select,
|
|
307
|
+
.field textarea,
|
|
308
|
+
.generated input { transition: none; }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@media (prefers-contrast: more) {
|
|
312
|
+
.search-row input,
|
|
313
|
+
.field input,
|
|
314
|
+
.field select,
|
|
315
|
+
.field textarea,
|
|
316
|
+
.generated input { border-color: #526079; background: #fff; box-shadow: none; }
|
|
317
|
+
|
|
318
|
+
.search-row input:focus,
|
|
319
|
+
.field input:focus,
|
|
320
|
+
.field select:focus,
|
|
321
|
+
.field textarea:focus,
|
|
322
|
+
.generated input:focus { border-color: #143f78; box-shadow: 0 0 0 2px #143f78; }
|
|
323
|
+
}
|
|
324
|
+
|
|
266
325
|
.error {
|
|
267
326
|
border-color: #f2b8b5;
|
|
268
327
|
color: #9f2f2a;
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-07-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "3eec1794b989808b804cd4f0b187af8a3f12938d",
|
|
3
|
+
"builtAt": "2026-07-18T09:37:04.414Z",
|
|
4
|
+
"version": "4.16.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -3,6 +3,7 @@ import { ProcessManager } from "./process-manager.js";
|
|
|
3
3
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
4
|
import { WandStorage } from "./storage.js";
|
|
5
5
|
import { ExecutionMode, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
6
|
+
import { toSessionListItemDTO } from "./session-transport.js";
|
|
6
7
|
import { SessionRegistry } from "./session-registry.js";
|
|
7
8
|
export declare function parseExecutionMode(value: unknown, fallback: ExecutionMode): ExecutionMode;
|
|
8
9
|
export declare function parseSessionCreationOrigin(body: {
|
|
@@ -22,8 +23,34 @@ type SessionDeletionStructured = Pick<StructuredSessionManager, "get" | "delete"
|
|
|
22
23
|
export declare function deleteSessionWithProviderHistory(processes: SessionDeletionProcesses, structured: SessionDeletionStructured, storage: Pick<WandStorage, "getConfigValue" | "setConfigValue">, id: string): void;
|
|
23
24
|
type ProviderHistorySession = {
|
|
24
25
|
claudeSessionId: string;
|
|
26
|
+
cwd: string;
|
|
27
|
+
firstUserMessage: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
mtimeMs: number;
|
|
30
|
+
hasConversation: boolean;
|
|
25
31
|
managedByWand: boolean;
|
|
32
|
+
provider?: "claude" | "codex";
|
|
26
33
|
};
|
|
34
|
+
export type SessionListPageEntry = {
|
|
35
|
+
type: "managed";
|
|
36
|
+
key: string;
|
|
37
|
+
sortTimestamp: number;
|
|
38
|
+
session: ReturnType<typeof toSessionListItemDTO>;
|
|
39
|
+
} | {
|
|
40
|
+
type: "recoverable";
|
|
41
|
+
key: string;
|
|
42
|
+
sortTimestamp: number;
|
|
43
|
+
history: ProviderHistorySession & {
|
|
44
|
+
provider: "claude" | "codex";
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export interface SessionListPage {
|
|
48
|
+
entries: SessionListPageEntry[];
|
|
49
|
+
offset: number;
|
|
50
|
+
total: number;
|
|
51
|
+
revision: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function buildSessionListPage(sessions: SessionSnapshot[], claudeHistory: ProviderHistorySession[], codexHistory: ProviderHistorySession[], hiddenHistoryIds: Set<string>, offset: number, limit: number): SessionListPage;
|
|
27
54
|
/**
|
|
28
55
|
* Provider history is scanned by ProcessManager, but structured sessions live
|
|
29
56
|
* in StructuredSessionManager. Annotate against the combined session list so a
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { SessionInputError } from "./process-manager.js";
|
|
2
3
|
import { getDefaultModelForProvider, isExecutionMode } from "./config.js";
|
|
3
4
|
import { blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
@@ -167,6 +168,47 @@ export function deleteSessionWithProviderHistory(processes, structured, storage,
|
|
|
167
168
|
// Keep a tombstone as a fallback for permission errors or process tail writes.
|
|
168
169
|
addToHiddenClaudeSessionIds(storage, [providerSessionId]);
|
|
169
170
|
}
|
|
171
|
+
function sessionSortTimestamp(snapshot) {
|
|
172
|
+
const timestamp = Date.parse(snapshot.startedAt);
|
|
173
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
174
|
+
}
|
|
175
|
+
export function buildSessionListPage(sessions, claudeHistory, codexHistory, hiddenHistoryIds, offset, limit) {
|
|
176
|
+
const managed = sessions.map((session) => ({
|
|
177
|
+
type: "managed",
|
|
178
|
+
key: `session-${session.id}`,
|
|
179
|
+
sortTimestamp: sessionSortTimestamp(session),
|
|
180
|
+
session: toSessionListItemDTO(session),
|
|
181
|
+
}));
|
|
182
|
+
const recoverable = [
|
|
183
|
+
...markManagedProviderHistory(claudeHistory, sessions, "claude"),
|
|
184
|
+
...markManagedProviderHistory(codexHistory, sessions, "codex"),
|
|
185
|
+
].flatMap((history) => {
|
|
186
|
+
const provider = history.provider === "codex" ? "codex" : "claude";
|
|
187
|
+
if (!history.hasConversation || history.managedByWand || hiddenHistoryIds.has(history.claudeSessionId)) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return [{
|
|
191
|
+
type: "recoverable",
|
|
192
|
+
key: `recoverable-${provider}-${history.claudeSessionId}`,
|
|
193
|
+
sortTimestamp: history.mtimeMs,
|
|
194
|
+
history: { ...history, provider },
|
|
195
|
+
}];
|
|
196
|
+
});
|
|
197
|
+
const entries = [...managed, ...recoverable].sort((left, right) => {
|
|
198
|
+
const timestampOrder = right.sortTimestamp - left.sortTimestamp;
|
|
199
|
+
return timestampOrder || left.key.localeCompare(right.key);
|
|
200
|
+
});
|
|
201
|
+
const boundedOffset = Math.min(Math.max(offset, 0), entries.length);
|
|
202
|
+
const revision = createHash("sha256")
|
|
203
|
+
.update(JSON.stringify(entries.map((entry) => [entry.key, entry.sortTimestamp])))
|
|
204
|
+
.digest("base64url");
|
|
205
|
+
return {
|
|
206
|
+
entries: entries.slice(boundedOffset, boundedOffset + limit),
|
|
207
|
+
offset: boundedOffset,
|
|
208
|
+
total: entries.length,
|
|
209
|
+
revision,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
170
212
|
/**
|
|
171
213
|
* Provider history is scanned by ProcessManager, but structured sessions live
|
|
172
214
|
* in StructuredSessionManager. Annotate against the combined session list so a
|
|
@@ -319,6 +361,23 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
319
361
|
messageTotal: windowed.messageTotal,
|
|
320
362
|
});
|
|
321
363
|
};
|
|
364
|
+
app.get("/api/session-list", (req, res) => {
|
|
365
|
+
try {
|
|
366
|
+
const offset = parseBoundedInteger(req.query.offset, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
367
|
+
const limit = parseBoundedInteger(req.query.limit, 40, 1, 200);
|
|
368
|
+
const requestedRevision = typeof req.query.revision === "string" ? req.query.revision : "";
|
|
369
|
+
const currentSessions = sessions.listSlim();
|
|
370
|
+
const page = buildSessionListPage(currentSessions, processes.listClaudeHistorySessions(), processes.listCodexHistorySessions(), getHiddenClaudeSessionIds(storage), offset, limit);
|
|
371
|
+
if (offset > 0 && requestedRevision !== page.revision) {
|
|
372
|
+
res.status(409).json({ error: "会话列表已更新,请重新加载。", revision: page.revision });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
res.json(page);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
res.status(500).json({ error: getErrorMessage(error, "无法加载会话列表。") });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
322
381
|
app.get("/api/sessions", (_req, res) => {
|
|
323
382
|
res.json(sessions.listSlim().map(toSessionListItemDTO));
|
|
324
383
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ConversationTurn, SessionSnapshot } from "./types.js";
|
|
2
2
|
export declare const SESSION_TRANSPORT_OUTPUT_LIMIT = 200000;
|
|
3
|
-
export type SessionBaseDTO = Omit<SessionSnapshot, "output" | "messages"
|
|
3
|
+
export type SessionBaseDTO = Omit<SessionSnapshot, "output" | "messages" | "title"> & {
|
|
4
|
+
/** Canonical server-resolved title. Clients must not invent their own fallback. */
|
|
5
|
+
title: string;
|
|
6
|
+
};
|
|
4
7
|
export interface SessionListItemDTO extends SessionBaseDTO {
|
|
5
8
|
/** Kept for compatibility with clients that initialize terminal state from the list. */
|
|
6
9
|
output: "";
|
|
@@ -17,6 +20,8 @@ export interface SessionDetailDTO extends SessionBaseDTO {
|
|
|
17
20
|
leadingBlockOffset?: number;
|
|
18
21
|
leadingBlockTotal?: number;
|
|
19
22
|
}
|
|
23
|
+
/** Resolve every client-visible title on the server so all clients show the same value. */
|
|
24
|
+
export declare function resolveSessionDisplayTitle(snapshot: SessionSnapshot): string;
|
|
20
25
|
export declare function toSessionListItemDTO(snapshot: SessionSnapshot): SessionListItemDTO;
|
|
21
26
|
export interface SessionDetailDTOOptions {
|
|
22
27
|
output?: string;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
2
2
|
export const SESSION_TRANSPORT_OUTPUT_LIMIT = 200_000;
|
|
3
|
+
function cleanDisplayTitle(value) {
|
|
4
|
+
return value?.replace(/\s+/g, " ").trim().slice(0, 40) ?? "";
|
|
5
|
+
}
|
|
6
|
+
/** Resolve every client-visible title on the server so all clients show the same value. */
|
|
7
|
+
export function resolveSessionDisplayTitle(snapshot) {
|
|
8
|
+
for (const candidate of [snapshot.title, snapshot.description, snapshot.summary]) {
|
|
9
|
+
const title = cleanDisplayTitle(candidate);
|
|
10
|
+
if (title)
|
|
11
|
+
return title;
|
|
12
|
+
}
|
|
13
|
+
const cwd = snapshot.cwd.replace(/[\\/]+$/, "");
|
|
14
|
+
const directory = cleanDisplayTitle(cwd.split(/[\\/]/).pop());
|
|
15
|
+
return directory || "会话";
|
|
16
|
+
}
|
|
3
17
|
/** Explicit allow-list separating the server's session object from its wire DTO. */
|
|
4
18
|
function sessionBase(snapshot) {
|
|
5
19
|
return {
|
|
@@ -36,7 +50,7 @@ function sessionBase(snapshot) {
|
|
|
36
50
|
autoApprovePermissions: snapshot.autoApprovePermissions,
|
|
37
51
|
approvalStats: snapshot.approvalStats,
|
|
38
52
|
summary: snapshot.summary,
|
|
39
|
-
title: snapshot
|
|
53
|
+
title: resolveSessionDisplayTitle(snapshot),
|
|
40
54
|
description: snapshot.description,
|
|
41
55
|
currentTaskTitle: snapshot.currentTaskTitle,
|
|
42
56
|
selectedModel: snapshot.selectedModel,
|
|
@@ -65,6 +65,16 @@ export function normalizeClaudeToolInput(name, input) {
|
|
|
65
65
|
}
|
|
66
66
|
catch { /* Preserve malformed provider data verbatim. */ }
|
|
67
67
|
}
|
|
68
|
+
if (name === "TodoWrite" && Array.isArray(record.todos)) {
|
|
69
|
+
record.todos = record.todos.map((item) => {
|
|
70
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
71
|
+
return item;
|
|
72
|
+
const todo = item;
|
|
73
|
+
return typeof todo.content !== "string" && typeof todo.description === "string"
|
|
74
|
+
? { ...todo, content: todo.description }
|
|
75
|
+
: item;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
68
78
|
return record;
|
|
69
79
|
}
|
|
70
80
|
export function extractClaudeUsage(source) {
|
|
@@ -77,7 +77,7 @@ function tasksFromSegment(messages, start, end) {
|
|
|
77
77
|
return [];
|
|
78
78
|
return [{
|
|
79
79
|
id: text(todo.id) ?? String(index + 1),
|
|
80
|
-
content: text(todo.content) ?? text(todo.subject) ?? "",
|
|
80
|
+
content: text(todo.content) ?? text(todo.subject) ?? text(todo.description) ?? "",
|
|
81
81
|
status: text(todo.status) ?? "pending",
|
|
82
82
|
...(text(todo.activeForm) ? { activeForm: text(todo.activeForm) } : {}),
|
|
83
83
|
}];
|
package/dist/types.d.ts
CHANGED
|
@@ -536,7 +536,7 @@ export interface SessionSnapshot {
|
|
|
536
536
|
description?: string;
|
|
537
537
|
/** 会话标题正在根据最新消息重新生成;仅用于实时展示,重启时强制清空。 */
|
|
538
538
|
titleGenerating?: boolean;
|
|
539
|
-
/**
|
|
539
|
+
/** 当前正在执行的任务标题(用于进度展示,不作为会话标题) */
|
|
540
540
|
currentTaskTitle?: string;
|
|
541
541
|
/** 用户为此会话选定的 Claude 模型(别名或完整 ID)。结构化会话下次 spawn 时使用;PTY 会话仅用于展示。 */
|
|
542
542
|
selectedModel?: string | null;
|