@co0ontty/wand 4.71.0 → 4.71.2-beta.g1a04a06

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.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108 108"><rect width="108" height="108" rx="24" fill="#E3E8EE"/><g transform="translate(24 24)"><path fill="#6B7B8D" d="M6,0h6v6h-6z M48,0h6v6h-6z M0,6h6v6h-6z M12,6h6v6h-6z M42,6h6v6h-6z M54,6h6v6h-6z M0,12h6v6h-6z M54,12h6v6h-6z M6,36h6v6h-6z M48,36h6v6h-6z M18,48h6v6h-6z M36,48h6v6h-6z"/><path fill="#9EAAB8" d="M6,6h6v6h-6z M48,6h6v6h-6z M6,12h48v6h-48z M0,18h12v6h-12z M24,18h12v6h-12z M48,18h12v6h-12z M0,24h12v6h-12z M24,24h12v6h-12z M48,24h12v6h-12z M0,30h24v6h-24z M36,30h24v6h-24z M0,36h6v6h-6z M12,36h6v6h-6z M24,36h12v6h-12z M42,36h6v6h-6z M54,36h6v6h-6z M6,42h48v6h-48z M12,48h6v6h-6z M24,48h12v6h-12z M42,48h6v6h-6z M18,54h6v6h-6z M36,54h6v6h-6z"/><path fill="#FFFFFF" d="M12,18h6v6h-6z M36,18h6v6h-6z M12,24h12v6h-12z M36,24h12v6h-12z"/><path fill="#7EC88B" d="M18,18h6v6h-6z M42,18h6v6h-6z"/><path fill="#F28B9A" d="M24,30h12v6h-12z"/><path fill="#C5CED8" d="M18,36h6v6h-6z M36,36h6v6h-6z"/></g></svg>
@@ -2,7 +2,13 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Wand Passwords",
4
4
  "description": "Wand browser password manager with autofill, save prompts, cards, identities, TOTP, and security checks.",
5
- "version": "0.1.0",
5
+ "version": "0.1.1",
6
+ "icons": {
7
+ "16": "icons/wand-16.png",
8
+ "32": "icons/wand-32.png",
9
+ "48": "icons/wand-48.png",
10
+ "128": "icons/wand-128.png"
11
+ },
6
12
  "permissions": [
7
13
  "activeTab",
8
14
  "clipboardWrite",
@@ -25,7 +31,11 @@
25
31
  },
26
32
  "action": {
27
33
  "default_title": "Wand Passwords",
28
- "default_popup": "src/popup.html"
34
+ "default_popup": "src/popup.html",
35
+ "default_icon": {
36
+ "16": "icons/wand-16.png",
37
+ "32": "icons/wand-32.png"
38
+ }
29
39
  },
30
40
  "options_page": "src/options.html",
31
41
  "content_scripts": [
@@ -30,6 +30,14 @@ body {
30
30
  box-shadow: 0 16px 40px rgba(18, 35, 64, .08);
31
31
  }
32
32
 
33
+ .brand-heading {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 12px;
37
+ }
38
+
39
+ .brand-logo { flex: 0 0 auto; }
40
+
33
41
  h1 {
34
42
  margin: 0;
35
43
  font-size: 22px;
@@ -4,14 +4,18 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
6
  <title>Wand Passwords Options</title>
7
+ <link rel="icon" type="image/svg+xml" href="../icons/wand.svg" />
7
8
  <link rel="stylesheet" href="options.css" />
8
9
  </head>
9
10
  <body>
10
11
  <main class="page">
11
12
  <section class="panel">
12
- <header>
13
- <h1>Wand Passwords</h1>
14
- <p>Connect this browser extension to your Wand server.</p>
13
+ <header class="brand-heading">
14
+ <img src="../icons/wand.svg" class="brand-logo" width="44" height="44" alt="" />
15
+ <div>
16
+ <h1>Wand Passwords</h1>
17
+ <p>Connect this browser extension to your Wand server.</p>
18
+ </div>
15
19
  </header>
16
20
  <label class="field">
17
21
  <span>Server URL</span>
@@ -29,6 +29,15 @@ body {
29
29
  margin-bottom: 12px;
30
30
  }
31
31
 
32
+ .brand-heading {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 8px;
36
+ min-width: 0;
37
+ }
38
+
39
+ .brand-logo { flex: 0 0 auto; }
40
+
32
41
  .top-actions {
33
42
  display: flex;
34
43
  gap: 6px;
@@ -4,14 +4,18 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
6
  <title>Wand Passwords</title>
7
+ <link rel="icon" type="image/svg+xml" href="../icons/wand.svg" />
7
8
  <link rel="stylesheet" href="popup.css" />
8
9
  </head>
9
10
  <body>
10
11
  <main class="shell">
11
12
  <header class="topbar">
12
- <div>
13
- <h1>Wand Passwords</h1>
14
- <p id="statusText">Locked</p>
13
+ <div class="brand-heading">
14
+ <img src="../icons/wand.svg" class="brand-logo" width="32" height="32" alt="" />
15
+ <div>
16
+ <h1>Wand Passwords</h1>
17
+ <p id="statusText">Locked</p>
18
+ </div>
15
19
  </div>
16
20
  <div class="top-actions">
17
21
  <button id="siteToggleButton" class="icon-button" type="button" title="Toggle this site">Site</button>
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "fad4d4b5db4c4be548f414c6de249f84700daa33",
3
- "builtAt": "2026-09-19T07:12:02.224Z",
4
- "version": "4.71.0",
5
- "channel": "stable"
2
+ "commit": "1a04a06e5944858ecf2a357251d46aafdc943e32",
3
+ "builtAt": "2026-09-20T15:56:10.251Z",
4
+ "version": "4.71.2-beta.g1a04a06",
5
+ "channel": "beta"
6
6
  }
@@ -387,14 +387,14 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
387
387
  if (cascade) {
388
388
  deleteSessions(storage, sessions, [
389
389
  ...storage.listSessionsByWorkspace(existing.id).map((session) => session.id),
390
- ...tasks.flatMap((task) => storage.listSessionsByWorkspaceTask(task.id).map((session) => session.id)),
390
+ ...tasks.flatMap((task) => storage.listSessionsByWorkspaceTaskSlim(task.id).map((session) => session.id)),
391
391
  ]);
392
392
  }
393
393
  else {
394
394
  // 非级联删除可以保留项目根目录里的会话,但隔离任务的 cwd 会随
395
395
  // worktree 一起消失,因此必须删除这些会话,不能留下僵尸记录。
396
396
  deleteSessions(storage, sessions, tasks.flatMap((task) => task.worktree
397
- ? storage.listSessionsByWorkspaceTask(task.id).map((session) => session.id)
397
+ ? storage.listSessionsByWorkspaceTaskSlim(task.id).map((session) => session.id)
398
398
  : []));
399
399
  }
400
400
  for (const task of tasks) {
@@ -467,6 +467,9 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
467
467
  const taskLimit = parseBoundedCount(req.query.limit);
468
468
  const sessionLimit = parseBoundedCount(req.query.maxSessions);
469
469
  backfillSessionWorkspaces(storage);
470
+ // 目录组只需要会话元数据:用 slim 读,否则每次轮询都要把每个会话的
471
+ // messages 大字段 JSON 解析一遍(单会话可达十几 MB,整表 ~30MB)。
472
+ const allSessions = storage.loadSessionsSlim();
470
473
  const workspaces = storage.listWorkspaces();
471
474
  const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions);
472
475
  // 目录自定义名(工作区名称):没有项目实体的合成目录也按这个名字显示。
@@ -521,7 +524,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
521
524
  const taskSessions = (taskId) => {
522
525
  let snapshots = sessionsByTask.get(taskId);
523
526
  if (!snapshots) {
524
- snapshots = storage.listSessionsByWorkspaceTask(taskId);
527
+ snapshots = storage.listSessionsByWorkspaceTaskSlim(taskId);
525
528
  sessionsByTask.set(taskId, snapshots);
526
529
  }
527
530
  return snapshots;
@@ -566,7 +569,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
566
569
  taskBoundSessionIds.add(session.id);
567
570
  }
568
571
  }
569
- for (const session of storage.loadSessions()) {
572
+ for (const session of allSessions) {
570
573
  if (taskBoundSessionIds.has(session.id))
571
574
  continue;
572
575
  const direct = session.workspaceId ? groups.get(session.workspaceId) : undefined;
@@ -748,7 +751,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
748
751
  res.json({
749
752
  ...task,
750
753
  cwd: taskRuntimeCwd(task, workspace),
751
- sessions: storage.listSessionsByWorkspaceTask(task.id).map((session) => ({
754
+ sessions: storage.listSessionsByWorkspaceTaskSlim(task.id).map((session) => ({
752
755
  ...workspaceSessionSummary(session, {
753
756
  taskName: task.name,
754
757
  workspaceName: workspace?.name,
@@ -826,7 +829,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
826
829
  || req.query.cascade === "true"
827
830
  || existing.worktree !== null;
828
831
  if (cascade) {
829
- deleteSessions(storage, sessions, storage.listSessionsByWorkspaceTask(existing.id).map((session) => session.id));
832
+ deleteSessions(storage, sessions, storage.listSessionsByWorkspaceTaskSlim(existing.id).map((session) => session.id));
830
833
  }
831
834
  // 尽力清理 worktree 与分支;失败不阻塞删除任务行。
832
835
  if (cascade)
package/dist/storage.d.ts CHANGED
@@ -168,6 +168,13 @@ export declare class WandStorage {
168
168
  bindGithubIssueSession(owner: string, repo: string, issueNumber: number, sessionId: string): void;
169
169
  unbindGithubIssueSession(owner: string, repo: string, issueNumber: number, sessionId: string): void;
170
170
  listSessionsByWorkspaceTask(taskId: string): SessionSnapshot[];
171
+ /**
172
+ * 同 `listSessionsByWorkspaceTask`,但不读 `output`/`messages` 大字段。
173
+ *
174
+ * 侧栏的目录组、任务详情、自动命名只需要元数据;完整行会把整个会话消息
175
+ * 历史 JSON 解析一遍(单会话可达十几 MB),每轮轮询都会卡住事件循环。
176
+ */
177
+ listSessionsByWorkspaceTaskSlim(taskId: string): SessionSnapshot[];
171
178
  setSessionWorkspaceTaskId(sessionId: string, taskId: string | null): void;
172
179
  /** Get password from database */
173
180
  getPassword(): string | null;
@@ -234,6 +241,8 @@ export declare class WandStorage {
234
241
  */
235
242
  checkpointSessionMessages(id: string, messages: ConversationTurn[], structuredState?: StructuredSessionState | null, output?: string, ptyOutputSeq?: number): void;
236
243
  getSession(id: string): SessionSnapshot | null;
244
+ /** 同 `getSession`,但不读 `output`/`messages` 大字段(只取元数据的路径用)。 */
245
+ getSessionSlim(id: string): SessionSnapshot | null;
237
246
  getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
238
247
  loadSessions(): SessionSnapshot[];
239
248
  /** List durable metadata without reading or parsing output/messages payloads. */
package/dist/storage.js CHANGED
@@ -1372,6 +1372,21 @@ export class WandStorage {
1372
1372
  .all(taskId);
1373
1373
  return rows.map((row) => this.mapSessionRow(row));
1374
1374
  }
1375
+ /**
1376
+ * 同 `listSessionsByWorkspaceTask`,但不读 `output`/`messages` 大字段。
1377
+ *
1378
+ * 侧栏的目录组、任务详情、自动命名只需要元数据;完整行会把整个会话消息
1379
+ * 历史 JSON 解析一遍(单会话可达十几 MB),每轮轮询都会卡住事件循环。
1380
+ */
1381
+ listSessionsByWorkspaceTaskSlim(taskId) {
1382
+ const rows = this.db
1383
+ .prepare(`SELECT ${sessionSelectFields(true)}
1384
+ FROM command_sessions
1385
+ WHERE workspace_task_id = ?
1386
+ ORDER BY started_at DESC`)
1387
+ .all(taskId);
1388
+ return rows.map((row) => this.mapSessionRow(row));
1389
+ }
1375
1390
  setSessionWorkspaceTaskId(sessionId, taskId) {
1376
1391
  this.db.prepare("UPDATE command_sessions SET workspace_task_id = ? WHERE id = ?").run(taskId, sessionId);
1377
1392
  }
@@ -1769,6 +1784,15 @@ export class WandStorage {
1769
1784
  .get(id);
1770
1785
  return row ? this.mapSessionRow(row) : null;
1771
1786
  }
1787
+ /** 同 `getSession`,但不读 `output`/`messages` 大字段(只取元数据的路径用)。 */
1788
+ getSessionSlim(id) {
1789
+ const row = this.db
1790
+ .prepare(`SELECT ${sessionSelectFields(true)}
1791
+ FROM command_sessions
1792
+ WHERE id = ?`)
1793
+ .get(id);
1794
+ return row ? this.mapSessionRow(row) : null;
1795
+ }
1772
1796
  getLatestSessionByClaudeSessionId(claudeSessionId) {
1773
1797
  const row = this.db
1774
1798
  .prepare(`${sessionRowQuery("SELECT")}
@@ -1949,6 +1973,13 @@ function ensureCommandSessionSchema(db) {
1949
1973
  db.exec(sql);
1950
1974
  }
1951
1975
  }
1976
+ if (columns.length > 0) {
1977
+ // 侧栏 / 任务面板按 workspace_task_id 反复拉会话:没有索引时每次都是
1978
+ // 46MB 表全扫(单次 ~500ms),而 command_sessions 里有 10MB+ 的 output/
1979
+ // messages 溢出行。索引只加不删,老库首次启动时建一次。
1980
+ db.exec("CREATE INDEX IF NOT EXISTS idx_command_sessions_workspace_task ON command_sessions(workspace_task_id)");
1981
+ db.exec("CREATE INDEX IF NOT EXISTS idx_command_sessions_workspace ON command_sessions(workspace_id)");
1982
+ }
1952
1983
  }
1953
1984
  const AUTH_SCOPES = new Set([
1954
1985
  "admin",
@@ -44,12 +44,27 @@ function taskNamingSessions(storage, card) {
44
44
  if (session && !sessions.has(session.id))
45
45
  sessions.set(session.id, session);
46
46
  };
47
+ const needsMessageFallback = [];
48
+ const addSlim = (session) => {
49
+ if (!session)
50
+ return;
51
+ add(session);
52
+ // boardTitleFromSession 只在 title/description 都缺时才翻 messages 找首条用户输入。
53
+ if (!session.title?.trim() && !session.description?.trim())
54
+ needsMessageFallback.push(session.id);
55
+ };
47
56
  if (card.workspaceTaskId) {
48
- for (const session of storage.listSessionsByWorkspaceTask(card.workspaceTaskId))
49
- add(session);
57
+ for (const session of storage.listSessionsByWorkspaceTaskSlim(card.workspaceTaskId))
58
+ addSlim(session);
50
59
  }
51
60
  for (const sessionId of storage.listWandTaskSessionIds(card.id))
52
- add(storage.getSession(sessionId));
61
+ addSlim(storage.getSessionSlim(sessionId));
62
+ // 只有真的需要 messages 的那几个会话才去解析大字段。
63
+ for (const sessionId of needsMessageFallback) {
64
+ const full = storage.getSession(sessionId);
65
+ if (full && (full.messages?.length ?? 0) > 0)
66
+ sessions.set(sessionId, full);
67
+ }
53
68
  return [...sessions.values()].sort((left, right) => (right.startedAt ?? "").localeCompare(left.startedAt ?? ""));
54
69
  }
55
70
  /**
@@ -184,7 +199,8 @@ export function syncSidebarTasksFromBoard(storage) {
184
199
  continue;
185
200
  const task = ensureWorkspaceTaskForBoardTask(storage, card);
186
201
  for (const sessionId of storage.listWandTaskSessionIds(card.id)) {
187
- const session = storage.getSession(sessionId);
202
+ // 只需要归属两列:不要用 getSession(),那会解析十几 MB 的 messages。
203
+ const session = storage.getSessionWorkspace(sessionId);
188
204
  if (!session)
189
205
  continue;
190
206
  if (session.workspaceTaskId && session.workspaceTaskId !== task.id) {
@@ -202,7 +218,7 @@ export function syncSidebarTasksFromBoard(storage) {
202
218
  function syncTaskSessionsToBoard(storage, task, workspace) {
203
219
  storage.transaction(() => {
204
220
  let card = ensureBoardTaskForWorkspaceTask(storage, task, workspace);
205
- const sessions = storage.listSessionsByWorkspaceTask(task.id);
221
+ const sessions = storage.listSessionsByWorkspaceTaskSlim(task.id);
206
222
  const agent = sessions.map(agentFromSession).find((value) => value !== null);
207
223
  const bound = new Set(storage.listWandTaskSessionIds(card.id));
208
224
  const started = card.status === "todo" && sessions.some((session) => !bound.has(session.id));
@@ -0,0 +1,3 @@
1
+ /** Wand's Android pixel cat, shared by favicon, legacy login and React chrome. */
2
+ export declare function renderWandBrandMarkup(className?: string): string;
3
+ export declare const WAND_FAVICON_URL: string;
@@ -0,0 +1,13 @@
1
+ import { WAND_BRAND } from "./brand-logo-data.js";
2
+ /** Wand's Android pixel cat, shared by favicon, legacy login and React chrome. */
3
+ export function renderWandBrandMarkup(className = "wand-brand-mark") {
4
+ const safeClass = className.replace(/&/g, "&amp;").replace(/"/g, "&quot;")
5
+ .replace(/</g, "&lt;").replace(/>/g, "&gt;");
6
+ const { viewport, inset, background, paths } = WAND_BRAND;
7
+ return `<svg class="${safeClass}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${viewport} ${viewport}" aria-hidden="true" focusable="false" data-wand-brand="pixel-cat">` +
8
+ `<rect width="${viewport}" height="${viewport}" rx="24" fill="${background}"/>` +
9
+ `<g transform="translate(${inset} ${inset})">` +
10
+ paths.map(({ fill, d }) => `<path fill="${fill}" d="${d}"/>`).join("") +
11
+ "</g></svg>";
12
+ }
13
+ export const WAND_FAVICON_URL = "data:image/svg+xml," + encodeURIComponent(renderWandBrandMarkup());
@@ -0,0 +1,24 @@
1
+ export declare const WAND_BRAND: {
2
+ readonly viewport: 108;
3
+ readonly inset: 24;
4
+ readonly background: "#E3E8EE";
5
+ readonly paths: readonly [{
6
+ readonly fill: "#6B7B8D";
7
+ readonly d: "M6,0h6v6h-6z M48,0h6v6h-6z M0,6h6v6h-6z M12,6h6v6h-6z M42,6h6v6h-6z M54,6h6v6h-6z M0,12h6v6h-6z M54,12h6v6h-6z M6,36h6v6h-6z M48,36h6v6h-6z M18,48h6v6h-6z M36,48h6v6h-6z";
8
+ }, {
9
+ readonly fill: "#9EAAB8";
10
+ readonly d: "M6,6h6v6h-6z M48,6h6v6h-6z M6,12h48v6h-48z M0,18h12v6h-12z M24,18h12v6h-12z M48,18h12v6h-12z M0,24h12v6h-12z M24,24h12v6h-12z M48,24h12v6h-12z M0,30h24v6h-24z M36,30h24v6h-24z M0,36h6v6h-6z M12,36h6v6h-6z M24,36h12v6h-12z M42,36h6v6h-6z M54,36h6v6h-6z M6,42h48v6h-48z M12,48h6v6h-6z M24,48h12v6h-12z M42,48h6v6h-6z M18,54h6v6h-6z M36,54h6v6h-6z";
11
+ }, {
12
+ readonly fill: "#FFFFFF";
13
+ readonly d: "M12,18h6v6h-6z M36,18h6v6h-6z M12,24h12v6h-12z M36,24h12v6h-12z";
14
+ }, {
15
+ readonly fill: "#7EC88B";
16
+ readonly d: "M18,18h6v6h-6z M42,18h6v6h-6z";
17
+ }, {
18
+ readonly fill: "#F28B9A";
19
+ readonly d: "M24,30h12v6h-12z";
20
+ }, {
21
+ readonly fill: "#C5CED8";
22
+ readonly d: "M18,36h6v6h-6z M36,36h6v6h-6z";
23
+ }];
24
+ };
@@ -0,0 +1,32 @@
1
+ // Generated from Android launcher vectors by npm run sync:brand-assets. Do not edit.
2
+ export const WAND_BRAND = {
3
+ "viewport": 108,
4
+ "inset": 24,
5
+ "background": "#E3E8EE",
6
+ "paths": [
7
+ {
8
+ "fill": "#6B7B8D",
9
+ "d": "M6,0h6v6h-6z M48,0h6v6h-6z M0,6h6v6h-6z M12,6h6v6h-6z M42,6h6v6h-6z M54,6h6v6h-6z M0,12h6v6h-6z M54,12h6v6h-6z M6,36h6v6h-6z M48,36h6v6h-6z M18,48h6v6h-6z M36,48h6v6h-6z"
10
+ },
11
+ {
12
+ "fill": "#9EAAB8",
13
+ "d": "M6,6h6v6h-6z M48,6h6v6h-6z M6,12h48v6h-48z M0,18h12v6h-12z M24,18h12v6h-12z M48,18h12v6h-12z M0,24h12v6h-12z M24,24h12v6h-12z M48,24h12v6h-12z M0,30h24v6h-24z M36,30h24v6h-24z M0,36h6v6h-6z M12,36h6v6h-6z M24,36h12v6h-12z M42,36h6v6h-6z M54,36h6v6h-6z M6,42h48v6h-48z M12,48h6v6h-6z M24,48h12v6h-12z M42,48h6v6h-6z M18,54h6v6h-6z M36,54h6v6h-6z"
14
+ },
15
+ {
16
+ "fill": "#FFFFFF",
17
+ "d": "M12,18h6v6h-6z M36,18h6v6h-6z M12,24h12v6h-12z M36,24h12v6h-12z"
18
+ },
19
+ {
20
+ "fill": "#7EC88B",
21
+ "d": "M18,18h6v6h-6z M42,18h6v6h-6z"
22
+ },
23
+ {
24
+ "fill": "#F28B9A",
25
+ "d": "M24,30h12v6h-12z"
26
+ },
27
+ {
28
+ "fill": "#C5CED8",
29
+ "d": "M18,36h6v6h-6z M36,36h6v6h-6z"
30
+ }
31
+ ]
32
+ };