@co0ontty/wand 4.39.0 → 4.40.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/storage.js CHANGED
@@ -255,12 +255,65 @@ function mapWorktreeMergeFields(row) {
255
255
  }
256
256
  function sessionSelectFields() {
257
257
  return `id, session_source, automation_id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, pty_output_seq, archived, archived_at, claude_session_id, messages, queued_messages, queued_message_skills, structured_state
258
- , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
258
+ , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options, workspace_id, workspace_task_id`;
259
+ }
260
+ function mapWorkspaceRow(row) {
261
+ return {
262
+ id: row.id,
263
+ name: row.name,
264
+ cwd: row.cwd,
265
+ defaultProvider: (row.default_provider ?? undefined),
266
+ layout: row.layout_json ? safeJsonParse(row.layout_json) ?? null : null,
267
+ createdAt: row.created_at,
268
+ lastOpenedAt: row.last_opened_at,
269
+ };
270
+ }
271
+ function mapWorkspaceTaskWorktree(raw) {
272
+ const parsed = safeJsonParse(raw);
273
+ if (!parsed || typeof parsed.path !== "string" || typeof parsed.branch !== "string")
274
+ return null;
275
+ return parsed;
276
+ }
277
+ function firstLayoutTabId(node) {
278
+ if (node.type === "pane")
279
+ return node.tabs[node.active]?.id ?? node.tabs[0]?.id;
280
+ return firstLayoutTabId(node.children[0]) ?? firstLayoutTabId(node.children[1]);
281
+ }
282
+ /** 读取旧版单棵分屏树时就地包成一个工作窗口,避免升级后丢失布局。 */
283
+ function mapWorkspaceTaskLayout(raw) {
284
+ const parsed = safeJsonParse(raw);
285
+ if (!parsed || typeof parsed !== "object")
286
+ return null;
287
+ const record = parsed;
288
+ if (record.type === "windows" && Array.isArray(record.windows)) {
289
+ return parsed;
290
+ }
291
+ if (record.type === "pane" || record.type === "split") {
292
+ const legacy = parsed;
293
+ return {
294
+ type: "windows",
295
+ windows: [{ id: "window-legacy", layout: legacy, activeTabId: firstLayoutTabId(legacy) }],
296
+ activeWindowId: "window-legacy",
297
+ };
298
+ }
299
+ return null;
300
+ }
301
+ function mapWorkspaceTaskRow(row) {
302
+ return {
303
+ id: row.id,
304
+ workspaceId: row.workspace_id,
305
+ name: row.name,
306
+ worktree: mapWorkspaceTaskWorktree(row.worktree_json),
307
+ layout: mapWorkspaceTaskLayout(row.layout_json),
308
+ status: (row.status === "done" ? "done" : "active"),
309
+ createdAt: row.created_at,
310
+ lastOpenedAt: row.last_opened_at,
311
+ };
259
312
  }
260
313
  function sessionPersistFields() {
261
314
  return `id, session_source, automation_id, command, cwd, mode, status, exit_code, started_at, ended_at, output, pty_output_seq
262
315
  , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, queued_message_skills, structured_state
263
- , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options`;
316
+ , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options, workspace_id, workspace_task_id`;
264
317
  }
265
318
  function sessionPersistAssignments() {
266
319
  return `session_source = excluded.session_source,
@@ -292,7 +345,9 @@ function sessionPersistAssignments() {
292
345
  worktree_merge_info = excluded.worktree_merge_info,
293
346
  title = excluded.title,
294
347
  description = excluded.description,
295
- session_options = excluded.session_options`;
348
+ session_options = excluded.session_options,
349
+ workspace_id = excluded.workspace_id,
350
+ workspace_task_id = excluded.workspace_task_id`;
296
351
  }
297
352
  function sessionRuntimeMetadataAssignments() {
298
353
  return `session_source = ?, automation_id = ?,
@@ -337,6 +392,8 @@ function sessionPersistValues(snapshot) {
337
392
  snapshot.title ?? null,
338
393
  snapshot.description ?? null,
339
394
  serializeSessionOptions(snapshot),
395
+ snapshot.workspaceId ?? null,
396
+ snapshot.workspaceTaskId ?? null,
340
397
  ];
341
398
  }
342
399
  function sessionRuntimeMetadataValues(snapshot) {
@@ -404,6 +461,8 @@ function mapSessionCore(row) {
404
461
  worktree: parseWorktreeInfo(row.worktree_info) ?? null,
405
462
  title: row.title ?? undefined,
406
463
  description: row.description ?? undefined,
464
+ workspaceId: row.workspace_id ?? undefined,
465
+ workspaceTaskId: row.workspace_task_id ?? undefined,
407
466
  ...mapWorktreeMergeFields(row),
408
467
  ...sessionOptions,
409
468
  ...(Object.prototype.hasOwnProperty.call(sessionOptions, "pendingEscalation")
@@ -566,6 +625,34 @@ const INIT_SQL = `
566
625
  CREATE UNIQUE INDEX IF NOT EXISTS idx_mission_attempts_session ON mission_attempts(session_id) WHERE session_id IS NOT NULL;
567
626
  CREATE INDEX IF NOT EXISTS idx_mission_comments_attempt ON mission_review_comments(attempt_id, status);
568
627
  CREATE INDEX IF NOT EXISTS idx_agent_activity_state ON agent_activity(state, updated_at);
628
+
629
+ CREATE TABLE IF NOT EXISTS workspaces (
630
+ id TEXT PRIMARY KEY,
631
+ name TEXT NOT NULL,
632
+ cwd TEXT NOT NULL,
633
+ default_provider TEXT,
634
+ layout_json TEXT,
635
+ created_at TEXT NOT NULL,
636
+ last_opened_at TEXT
637
+ );
638
+
639
+ CREATE INDEX IF NOT EXISTS idx_workspaces_cwd ON workspaces(cwd);
640
+ CREATE INDEX IF NOT EXISTS idx_workspaces_last_opened ON workspaces(last_opened_at);
641
+
642
+ CREATE TABLE IF NOT EXISTS workspace_tasks (
643
+ id TEXT PRIMARY KEY,
644
+ workspace_id TEXT NOT NULL,
645
+ name TEXT NOT NULL,
646
+ worktree_json TEXT,
647
+ layout_json TEXT,
648
+ status TEXT NOT NULL DEFAULT 'active',
649
+ created_at TEXT NOT NULL,
650
+ last_opened_at TEXT,
651
+ FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
652
+ );
653
+
654
+ CREATE INDEX IF NOT EXISTS idx_workspace_tasks_workspace ON workspace_tasks(workspace_id);
655
+ CREATE INDEX IF NOT EXISTS idx_workspace_tasks_last_opened ON workspace_tasks(last_opened_at);
569
656
  `;
570
657
  export function ensureDatabaseFile(dbPath) {
571
658
  const dir = path.dirname(dbPath);
@@ -689,6 +776,174 @@ export class WandStorage {
689
776
  updated_at = excluded.updated_at`)
690
777
  .run(normalizedPath, normalizedName, nowIso());
691
778
  }
779
+ // ============ Workspaces(多标签 / 分屏项目)============
780
+ listWorkspaces() {
781
+ const rows = this.db
782
+ .prepare("SELECT id, name, cwd, default_provider, layout_json, created_at, last_opened_at FROM workspaces ORDER BY COALESCE(last_opened_at, created_at) DESC")
783
+ .all();
784
+ return rows.map(mapWorkspaceRow);
785
+ }
786
+ getWorkspace(id) {
787
+ const row = this.db
788
+ .prepare("SELECT id, name, cwd, default_provider, layout_json, created_at, last_opened_at FROM workspaces WHERE id = ?")
789
+ .get(id);
790
+ return row ? mapWorkspaceRow(row) : null;
791
+ }
792
+ createWorkspace(input) {
793
+ const id = crypto.randomUUID();
794
+ const createdAt = nowIso();
795
+ this.db
796
+ .prepare(`INSERT INTO workspaces (id, name, cwd, default_provider, layout_json, created_at, last_opened_at)
797
+ VALUES (?, ?, ?, ?, NULL, ?, NULL)`)
798
+ .run(id, input.name, input.cwd, input.defaultProvider ?? null, createdAt);
799
+ return {
800
+ id,
801
+ name: input.name,
802
+ cwd: input.cwd,
803
+ defaultProvider: input.defaultProvider,
804
+ layout: null,
805
+ createdAt,
806
+ lastOpenedAt: null,
807
+ };
808
+ }
809
+ updateWorkspace(id, patch) {
810
+ const assignments = [];
811
+ const values = [];
812
+ if (patch.name !== undefined) {
813
+ assignments.push("name = ?");
814
+ values.push(patch.name);
815
+ }
816
+ if (patch.cwd !== undefined) {
817
+ assignments.push("cwd = ?");
818
+ values.push(patch.cwd);
819
+ }
820
+ if (patch.defaultProvider !== undefined) {
821
+ assignments.push("default_provider = ?");
822
+ values.push(patch.defaultProvider ?? null);
823
+ }
824
+ if (assignments.length === 0)
825
+ return;
826
+ this.db.prepare(`UPDATE workspaces SET ${assignments.join(", ")} WHERE id = ?`).run(...values, id);
827
+ }
828
+ saveWorkspaceLayout(id, layout) {
829
+ this.db
830
+ .prepare("UPDATE workspaces SET layout_json = ? WHERE id = ?")
831
+ .run(layout ? JSON.stringify(layout) : null, id);
832
+ }
833
+ touchWorkspace(id) {
834
+ this.db.prepare("UPDATE workspaces SET last_opened_at = ? WHERE id = ?").run(nowIso(), id);
835
+ }
836
+ deleteWorkspace(id, options = {}) {
837
+ if (options.cascade) {
838
+ this.db.prepare(`DELETE FROM command_sessions
839
+ WHERE workspace_id = ?
840
+ OR workspace_task_id IN (SELECT id FROM workspace_tasks WHERE workspace_id = ?)`).run(id, id);
841
+ }
842
+ else {
843
+ // 解绑:保留会话,同时清空 workspace 与即将级联删除的 task 归属。
844
+ this.db.prepare(`UPDATE command_sessions
845
+ SET workspace_id = NULL, workspace_task_id = NULL
846
+ WHERE workspace_id = ?
847
+ OR workspace_task_id IN (SELECT id FROM workspace_tasks WHERE workspace_id = ?)`).run(id, id);
848
+ }
849
+ this.db.prepare("DELETE FROM workspaces WHERE id = ?").run(id);
850
+ }
851
+ listSessionsByWorkspace(workspaceId) {
852
+ const rows = this.db
853
+ .prepare(`${sessionRowQuery("SELECT")}
854
+ FROM command_sessions
855
+ WHERE workspace_id = ?
856
+ ORDER BY started_at DESC`)
857
+ .all(workspaceId);
858
+ return rows.map((row) => this.mapSessionRow(row));
859
+ }
860
+ /** 显式更新某会话的工作空间归属(用于创建时绑定)。 */
861
+ setSessionWorkspaceId(sessionId, workspaceId) {
862
+ this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE id = ?").run(workspaceId, sessionId);
863
+ }
864
+ // ── Workspace tasks(任务 = 命名 + 独立 worktree + 一组标签)──
865
+ listWorkspaceTasks(workspaceId) {
866
+ const rows = this.db
867
+ .prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, created_at, last_opened_at
868
+ FROM workspace_tasks WHERE workspace_id = ?
869
+ ORDER BY COALESCE(last_opened_at, created_at) DESC`)
870
+ .all(workspaceId);
871
+ return rows.map(mapWorkspaceTaskRow);
872
+ }
873
+ getWorkspaceTask(id) {
874
+ const row = this.db
875
+ .prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, created_at, last_opened_at
876
+ FROM workspace_tasks WHERE id = ?`)
877
+ .get(id);
878
+ return row ? mapWorkspaceTaskRow(row) : null;
879
+ }
880
+ createWorkspaceTask(input) {
881
+ const id = crypto.randomUUID();
882
+ const createdAt = nowIso();
883
+ const status = input.status ?? "active";
884
+ this.db
885
+ .prepare(`INSERT INTO workspace_tasks (id, workspace_id, name, worktree_json, layout_json, status, created_at, last_opened_at)
886
+ VALUES (?, ?, ?, ?, NULL, ?, ?, NULL)`)
887
+ .run(id, input.workspaceId, input.name, input.worktree ? JSON.stringify(input.worktree) : null, status, createdAt);
888
+ return {
889
+ id,
890
+ workspaceId: input.workspaceId,
891
+ name: input.name,
892
+ worktree: input.worktree ?? null,
893
+ layout: null,
894
+ status,
895
+ createdAt,
896
+ lastOpenedAt: null,
897
+ };
898
+ }
899
+ updateWorkspaceTask(id, patch) {
900
+ const assignments = [];
901
+ const values = [];
902
+ if (patch.name !== undefined) {
903
+ assignments.push("name = ?");
904
+ values.push(patch.name);
905
+ }
906
+ if (patch.status !== undefined) {
907
+ assignments.push("status = ?");
908
+ values.push(patch.status);
909
+ }
910
+ if (patch.worktree !== undefined) {
911
+ assignments.push("worktree_json = ?");
912
+ values.push(patch.worktree ? JSON.stringify(patch.worktree) : null);
913
+ }
914
+ if (assignments.length === 0)
915
+ return;
916
+ this.db.prepare(`UPDATE workspace_tasks SET ${assignments.join(", ")} WHERE id = ?`).run(...values, id);
917
+ }
918
+ saveWorkspaceTaskLayout(id, layout) {
919
+ this.db
920
+ .prepare("UPDATE workspace_tasks SET layout_json = ? WHERE id = ?")
921
+ .run(layout ? JSON.stringify(layout) : null, id);
922
+ }
923
+ touchWorkspaceTask(id) {
924
+ this.db.prepare("UPDATE workspace_tasks SET last_opened_at = ? WHERE id = ?").run(nowIso(), id);
925
+ }
926
+ deleteWorkspaceTask(id, options = {}) {
927
+ if (options.cascade) {
928
+ this.db.prepare("DELETE FROM command_sessions WHERE workspace_task_id = ?").run(id);
929
+ }
930
+ else {
931
+ this.db.prepare("UPDATE command_sessions SET workspace_task_id = NULL WHERE workspace_task_id = ?").run(id);
932
+ }
933
+ this.db.prepare("DELETE FROM workspace_tasks WHERE id = ?").run(id);
934
+ }
935
+ listSessionsByWorkspaceTask(taskId) {
936
+ const rows = this.db
937
+ .prepare(`${sessionRowQuery("SELECT")}
938
+ FROM command_sessions
939
+ WHERE workspace_task_id = ?
940
+ ORDER BY started_at DESC`)
941
+ .all(taskId);
942
+ return rows.map((row) => this.mapSessionRow(row));
943
+ }
944
+ setSessionWorkspaceTaskId(sessionId, taskId) {
945
+ this.db.prepare("UPDATE command_sessions SET workspace_task_id = ? WHERE id = ?").run(taskId, sessionId);
946
+ }
692
947
  /** Get password from database */
693
948
  getPassword() {
694
949
  return this.getConfigValue("password");
@@ -954,7 +1209,7 @@ export class WandStorage {
954
1209
  this.db
955
1210
  .prepare(`INSERT INTO command_sessions (
956
1211
  ${sessionPersistFields()}
957
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1212
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
958
1213
  ON CONFLICT(id) DO UPDATE SET
959
1214
  ${sessionPersistAssignments()}`)
960
1215
  .run(...sessionPersistValues(snapshot));
@@ -1148,6 +1403,8 @@ const SCHEMA_MIGRATIONS = [
1148
1403
  ["description", "ALTER TABLE command_sessions ADD COLUMN description TEXT"],
1149
1404
  ["pty_output_seq", "ALTER TABLE command_sessions ADD COLUMN pty_output_seq INTEGER NOT NULL DEFAULT 0"],
1150
1405
  ["session_options", `ALTER TABLE command_sessions ADD COLUMN session_options TEXT NOT NULL DEFAULT '{"schemaVersion":1}'`],
1406
+ ["workspace_id", "ALTER TABLE command_sessions ADD COLUMN workspace_id TEXT"],
1407
+ ["workspace_task_id", "ALTER TABLE command_sessions ADD COLUMN workspace_task_id TEXT"],
1151
1408
  ];
1152
1409
  const AUTH_SESSION_MIGRATIONS = [
1153
1410
  ["kind", "ALTER TABLE auth_sessions ADD COLUMN kind TEXT NOT NULL DEFAULT 'browser-admin'"],
@@ -26,6 +26,10 @@ interface CreateStructuredSessionOptions {
26
26
  thinkingEffort?: SessionSnapshot["thinkingEffort"];
27
27
  sessionSource?: SessionSource;
28
28
  automationId?: string;
29
+ /** 所属工作空间 ID(多标签 / 分屏项目)。 */
30
+ workspaceId?: string;
31
+ /** 所属工作空间任务 ID(任务 = 独立 worktree + 一组标签)。 */
32
+ workspaceTaskId?: string;
29
33
  /**
30
34
  * 恢复用的初始会话 id:
31
35
  * - Codex:历史 thread id,首条消息即 `codex exec ... resume <id>` 续接。
@@ -545,6 +545,8 @@ export class StructuredSessionManager {
545
545
  sessionKind: "structured",
546
546
  sessionSource: options.sessionSource ?? "interactive",
547
547
  automationId: options.automationId,
548
+ workspaceId: options.workspaceId,
549
+ workspaceTaskId: options.workspaceTaskId,
548
550
  provider,
549
551
  runner,
550
552
  command: provider === "codex"
package/dist/types.d.ts CHANGED
@@ -324,6 +324,10 @@ export interface CommandRequest {
324
324
  rows?: number;
325
325
  /** 思考深度。null/缺省 视为 off(不启用思考)。 */
326
326
  thinkingEffort?: ThinkingEffort | null;
327
+ /** 创建会话时绑定到的工作空间 ID(多标签 / 分屏项目)。 */
328
+ workspaceId?: string;
329
+ /** 创建会话时绑定到的工作空间任务 ID(任务 = 独立 worktree + 一组标签)。 */
330
+ workspaceTaskId?: string;
327
331
  }
328
332
  export interface InputRequest {
329
333
  input?: string;
@@ -490,6 +494,10 @@ export interface SessionSnapshot {
490
494
  sessionSource?: SessionSource;
491
495
  /** 自动化创建会话时关联的自动化任务 ID。 */
492
496
  automationId?: string;
497
+ /** 所属工作空间 ID(多标签 / 分屏项目)。会话在该工作空间窗口内作为一个标签。 */
498
+ workspaceId?: string;
499
+ /** 所属工作空间任务 ID;任务独占一个 worktree,其下所有会话共享该 worktree 目录。 */
500
+ workspaceTaskId?: string;
493
501
  sessionKind?: SessionKind;
494
502
  provider?: SessionProvider;
495
503
  /** True while the provider CLI owns the PTY; false after it returns to the persistent shell. */
@@ -574,6 +582,80 @@ export interface SessionSnapshot {
574
582
  /** Internal shell-wrapper marker needed to keep parsing a daemon-owned PTY after reattach. */
575
583
  ptyLaunchMarkerToken?: string | null;
576
584
  }
585
+ /** 工作空间默认 IDE;新建 Agent 标签时缺省回落到该 provider。 */
586
+ export type WorkspaceDefaultProvider = SessionProvider;
587
+ /** 工作空间内一个标签页:会话(IDE/终端)/ 编辑器 / 预览。 */
588
+ export type PaneTab = {
589
+ id: string;
590
+ kind: "session";
591
+ sessionId: string;
592
+ } | {
593
+ id: string;
594
+ kind: "editor";
595
+ path: string;
596
+ } | {
597
+ id: string;
598
+ kind: "preview";
599
+ path: string;
600
+ };
601
+ /**
602
+ * 标签 / 分屏布局树。叶子是 tabset(一组标签),内部节点是二分屏。
603
+ * `split.ratio` 为左/上子节点占比(0~1),由前端 allotment 回写持久化。
604
+ */
605
+ export type LayoutNode = {
606
+ type: "pane";
607
+ tabs: PaneTab[];
608
+ active: number;
609
+ } | {
610
+ type: "split";
611
+ dir: "h" | "v";
612
+ ratio: number;
613
+ children: [LayoutNode, LayoutNode];
614
+ };
615
+ /** 顶部一个工作窗口 Tab;内部可以包含一棵终端分屏树。 */
616
+ export interface WorkWindowLayout {
617
+ id: string;
618
+ layout: LayoutNode;
619
+ activeTabId?: string;
620
+ }
621
+ /** 任务级窗口集合:顶部 Tab 与 windows 一一对应。 */
622
+ export interface TaskWindowLayout {
623
+ type: "windows";
624
+ windows: WorkWindowLayout[];
625
+ activeWindowId: string | null;
626
+ }
627
+ /** 项目 / 工作空间。锚定一个目录(如 wand 仓库),内含多个并行「任务」。 */
628
+ export interface Workspace {
629
+ id: string;
630
+ name: string;
631
+ cwd: string;
632
+ defaultProvider?: WorkspaceDefaultProvider;
633
+ /**
634
+ * 工作空间级的布局占位(保留字段)。标签 / 分屏布局实际挂在 Task 上
635
+ * (见 WorkspaceTask.layout),因为每个任务独占一个 worktree 与一组标签。
636
+ */
637
+ layout: LayoutNode | null;
638
+ createdAt: string;
639
+ lastOpenedAt: string | null;
640
+ }
641
+ /** 任务所属 worktree 信息(复用 WorktreeInfo);非 git 目录时为 null(退化为直接在项目目录运行)。 */
642
+ export type WorkspaceTaskWorktree = WorktreeInfo;
643
+ export type WorkspaceTaskStatus = "active" | "done";
644
+ /**
645
+ * 工作空间内的一个「任务」:命名、独立 worktree 隔离、自带一组标签(LayoutNode)。
646
+ * 一个工作空间下可有多个任务,任务在侧栏列表里单独展示;每个任务的会话共享该任务的 worktree。
647
+ */
648
+ export interface WorkspaceTask {
649
+ id: string;
650
+ workspaceId: string;
651
+ name: string;
652
+ worktree: WorkspaceTaskWorktree | null;
653
+ /** 该任务的工作窗口 Tabs;每个窗口内部可含一棵分屏树。 */
654
+ layout: TaskWindowLayout | null;
655
+ status: WorkspaceTaskStatus;
656
+ createdAt: string;
657
+ lastOpenedAt: string | null;
658
+ }
577
659
  /** Unified event type emitted by ClaudePtyBridge for WebSocket broadcast */
578
660
  export type SessionEventType = "output.raw" | "output.chat" | "chat.turn" | "permission.prompt" | "permission.resolved" | "session.id" | "task" | "ended";
579
661
  export interface SessionEvent {