@i14ks/ccv 0.10.0 → 0.11.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.
Files changed (25) hide show
  1. package/bundle/ccv.mjs +359 -121
  2. package/package.json +1 -1
  3. package/web/assets/{codemirror-DKsnpugS.js → codemirror-BVMwXG_R.js} +1 -1
  4. package/web/assets/{index-cSZKYcZu.js → index-AByrn5FE.js} +1 -1
  5. package/web/assets/{index-NZmn_bzd.js → index-BCAmULlL.js} +1 -1
  6. package/web/assets/{index-yzGIJ5xe.js → index-BFLfK-j6.js} +1 -1
  7. package/web/assets/{index-AUibZMHd.js → index-BZ8Mi5_8.js} +1 -1
  8. package/web/assets/{index-C3yVGgV2.js → index-BlZDoET-.js} +1 -1
  9. package/web/assets/{index-u3kUuJkV.js → index-BsH-IJBy.js} +1 -1
  10. package/web/assets/{index-MFoRbG31.js → index-Bu_Ukh1_.js} +1 -1
  11. package/web/assets/{index-CT4Ig6AX.js → index-C-geC5VJ.js} +1 -1
  12. package/web/assets/{index-ClY-NTKp.js → index-C1BT9XMC.js} +1 -1
  13. package/web/assets/{index-Cw4Jg8_d.js → index-C59fVtiO.js} +1 -1
  14. package/web/assets/{index-CNmvdMxt.js → index-CPWAVXhz.js} +1 -1
  15. package/web/assets/{index-nV6tsVmQ.js → index-CrW8SGCA.js} +1 -1
  16. package/web/assets/{index-BCiq-pRA.js → index-DAjl7nVl.js} +1 -1
  17. package/web/assets/{index-B7Kjuy7B.js → index-DJYXqBtm.js} +1 -1
  18. package/web/assets/{index-BwqyDurx.js → index-DKofSKA7.js} +1 -1
  19. package/web/assets/{index-vmz_RdEe.js → index-DLDXVZaf.js} +1 -1
  20. package/web/assets/index-DRlKZHMW.js +132 -0
  21. package/web/assets/{index-BaxFm2-f.js → index-Ey94wZ1k.js} +1 -1
  22. package/web/assets/{index-CiZ3hw4e.js → index-gl1MvN4b.js} +1 -1
  23. package/web/assets/{index-CZMbzOzb.js → index-mnefozuu.js} +1 -1
  24. package/web/index.html +1 -1
  25. package/web/assets/index-BVL_B_hC.js +0 -132
package/bundle/ccv.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // dist/ccv.js
4
4
  import { spawn as spawn2 } from "node:child_process";
5
- import { readFile as readFile13 } from "node:fs/promises";
6
- import path18 from "node:path";
5
+ import { readFile as readFile14 } from "node:fs/promises";
6
+ import path19 from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { parseArgs } from "node:util";
9
9
 
@@ -146,9 +146,9 @@ function envDefaults() {
146
146
  // ../host/dist/server.js
147
147
  import { createServer } from "node:http";
148
148
  import { createReadStream, existsSync as existsSync3, statSync as statSync3 } from "node:fs";
149
- import path17 from "node:path";
149
+ import path18 from "node:path";
150
150
  import { homedir as homedir5 } from "node:os";
151
- import { randomUUID as randomUUID3 } from "node:crypto";
151
+ import { randomUUID as randomUUID4 } from "node:crypto";
152
152
  import { WebSocketServer } from "ws";
153
153
  import { renameSession as renameSession2 } from "@anthropic-ai/claude-agent-sdk";
154
154
 
@@ -3003,9 +3003,9 @@ function parsePatch(raw) {
3003
3003
  body = "";
3004
3004
  }
3005
3005
  total += body.length;
3006
- const path19 = status === "deleted" ? oldPath : newPath || oldPath;
3006
+ const path20 = status === "deleted" ? oldPath : newPath || oldPath;
3007
3007
  files.push({
3008
- path: path19,
3008
+ path: path20,
3009
3009
  oldPath: status === "renamed" ? oldPath : "",
3010
3010
  status,
3011
3011
  added: count(body, "+"),
@@ -3017,10 +3017,10 @@ function parsePatch(raw) {
3017
3017
  return { files, truncated };
3018
3018
  }
3019
3019
  function stripPrefix(value) {
3020
- const path19 = unquote(value.trim());
3021
- if (path19 === "/dev/null")
3020
+ const path20 = unquote(value.trim());
3021
+ if (path20 === "/dev/null")
3022
3022
  return "";
3023
- return path19.replace(/^[ab]\//, "");
3023
+ return path20.replace(/^[ab]\//, "");
3024
3024
  }
3025
3025
  function count(body, mark) {
3026
3026
  let n = 0;
@@ -3552,7 +3552,7 @@ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from
3552
3552
  import path9 from "node:path";
3553
3553
  import { listSessions as listSessions2 } from "@anthropic-ai/claude-agent-sdk";
3554
3554
  var WorkspaceRegistry = class {
3555
- #paths = [];
3555
+ #entries = [];
3556
3556
  #storePath;
3557
3557
  constructor(dataDir) {
3558
3558
  this.#storePath = path9.join(dataDir, "workspaces.json");
@@ -3561,52 +3561,91 @@ var WorkspaceRegistry = class {
3561
3561
  try {
3562
3562
  const raw = await readFile6(this.#storePath, "utf8");
3563
3563
  const parsed = JSON.parse(raw);
3564
- if (Array.isArray(parsed)) {
3565
- this.#paths = parsed.filter((p) => typeof p === "string" && existsSync2(p));
3564
+ if (!Array.isArray(parsed)) {
3565
+ this.#entries = [];
3566
+ return;
3566
3567
  }
3568
+ this.#entries = parsed.map((item) => {
3569
+ if (typeof item === "string")
3570
+ return { path: item };
3571
+ if (isEntry(item))
3572
+ return item;
3573
+ return null;
3574
+ }).filter((e) => e !== null && existsSync2(e.path));
3567
3575
  } catch {
3568
- this.#paths = [];
3576
+ this.#entries = [];
3569
3577
  }
3570
3578
  }
3571
3579
  async #persist() {
3572
3580
  await mkdir3(path9.dirname(this.#storePath), { recursive: true });
3573
- await writeFile4(this.#storePath, JSON.stringify(this.#paths, null, 2), "utf8");
3581
+ await writeFile4(this.#storePath, JSON.stringify(this.#entries, null, 2), "utf8");
3574
3582
  }
3583
+ /** Активные проекты. */
3575
3584
  list() {
3576
- return this.#paths.map(toInfo);
3585
+ return this.#entries.filter((e) => !e.archivedAt).map((e) => toInfo(e));
3586
+ }
3587
+ /** Закрытые проекты — архив, симметричный `list()`. */
3588
+ listArchived() {
3589
+ return this.#entries.filter((e) => e.archivedAt).map((e) => toInfo(e));
3577
3590
  }
3578
3591
  find(id) {
3579
- return this.list().find((w) => w.id === id);
3592
+ const entry = this.#entries.find((e) => workspaceIdFromPath(e.path) === id);
3593
+ return entry ? toInfo(entry) : void 0;
3580
3594
  }
3581
3595
  async open(rawPath) {
3582
3596
  const absolute = path9.resolve(rawPath);
3583
3597
  if (!existsSync2(absolute)) {
3584
3598
  throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${absolute}`);
3585
3599
  }
3586
- if (!this.#paths.includes(absolute)) {
3587
- this.#paths.unshift(absolute);
3600
+ const existing = this.#entries.find((e) => e.path === absolute);
3601
+ if (!existing) {
3602
+ this.#entries.unshift({ path: absolute });
3603
+ await this.#persist();
3604
+ } else if (existing.archivedAt) {
3605
+ delete existing.archivedAt;
3588
3606
  await this.#persist();
3589
3607
  }
3590
- return toInfo(absolute);
3608
+ return toInfo({ path: absolute });
3591
3609
  }
3592
3610
  /**
3593
- * Убрать проекты из списка открытых.
3611
+ * Убрать проекты из списка открытых — в архив, а не с концами.
3594
3612
  *
3595
- * Только реестр: каталог, его файлы и транскрипты сессий остаются на месте —
3596
- * закрытый проект открывается обратно тем же путём и с той же историей.
3597
- * Пачкой, а не по одному, потому что список чистят разом и переписывать файл
3598
- * на каждый проект незачем.
3613
+ * Только реестр: каталог, его файлы и транскрипты сессий остаются на месте.
3614
+ * Запись при этом не удаляется, а помечается временем закрытия проект
3615
+ * возвращается в активный список через `restore()`, без повторного поиска
3616
+ * каталога. Пачкой, а не по одному, потому что список чистят разом и
3617
+ * переписывать файл на каждый проект незачем.
3599
3618
  */
3600
3619
  async close(ids) {
3601
3620
  const doomed = new Set(ids);
3602
- const closed = this.list().filter((w) => doomed.has(w.id));
3621
+ const closed = [];
3622
+ const at = Date.now();
3623
+ for (const entry of this.#entries) {
3624
+ if (entry.archivedAt || !doomed.has(workspaceIdFromPath(entry.path)))
3625
+ continue;
3626
+ entry.archivedAt = at;
3627
+ closed.push(toInfo(entry));
3628
+ }
3603
3629
  if (closed.length === 0)
3604
3630
  return [];
3605
- const gone = new Set(closed.map((w) => w.path));
3606
- this.#paths = this.#paths.filter((p) => !gone.has(p));
3607
3631
  await this.#persist();
3608
3632
  return closed;
3609
3633
  }
3634
+ /** Вернуть проекты из архива в активный список. */
3635
+ async restore(ids) {
3636
+ const wanted = new Set(ids);
3637
+ const restored = [];
3638
+ for (const entry of this.#entries) {
3639
+ if (!entry.archivedAt || !wanted.has(workspaceIdFromPath(entry.path)))
3640
+ continue;
3641
+ delete entry.archivedAt;
3642
+ restored.push(toInfo(entry));
3643
+ }
3644
+ if (restored.length === 0)
3645
+ return [];
3646
+ await this.#persist();
3647
+ return restored;
3648
+ }
3610
3649
  /**
3611
3650
  * Создать каталог проекта и открыть его.
3612
3651
  *
@@ -3705,14 +3744,19 @@ function claudeMdStub(name) {
3705
3744
  function readmeStub(name) {
3706
3745
  return [`# ${name}`, "", "<!-- \u041E\u0434\u043D\u0430-\u0434\u0432\u0435 \u0444\u0440\u0430\u0437\u044B: \u0447\u0442\u043E \u044D\u0442\u043E \u0438 \u0437\u0430\u0447\u0435\u043C. -->", ""].join("\n");
3707
3746
  }
3708
- function toInfo(absolute) {
3747
+ function toInfo(entry) {
3748
+ const { path: absolute, archivedAt } = entry;
3709
3749
  return {
3710
3750
  id: workspaceIdFromPath(absolute),
3711
3751
  path: absolute,
3712
3752
  name: path9.basename(absolute) || absolute,
3713
- hasClaudeMd: existsSync2(path9.join(absolute, "CLAUDE.md"))
3753
+ hasClaudeMd: existsSync2(path9.join(absolute, "CLAUDE.md")),
3754
+ ...archivedAt ? { archivedAt } : {}
3714
3755
  };
3715
3756
  }
3757
+ function isEntry(value) {
3758
+ return typeof value === "object" && value !== null && typeof value.path === "string" && (value.archivedAt === void 0 || typeof value.archivedAt === "number");
3759
+ }
3716
3760
 
3717
3761
  // ../host/dist/archive.js
3718
3762
  import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
@@ -3912,38 +3956,181 @@ var SessionPins = class {
3912
3956
  await this.#persist();
3913
3957
  return changed;
3914
3958
  }
3959
+ async #persist() {
3960
+ const file = { pinned: Object.fromEntries(this.#pinned) };
3961
+ await mkdir5(path11.dirname(this.#storePath), { recursive: true });
3962
+ await writeFile6(this.#storePath, JSON.stringify(file, null, 2), "utf8");
3963
+ }
3964
+ };
3965
+ function isRecord2(value) {
3966
+ return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
3967
+ }
3968
+
3969
+ // ../host/dist/folders.js
3970
+ import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
3971
+ import path12 from "node:path";
3972
+ import { randomUUID as randomUUID2 } from "node:crypto";
3973
+ var Folders = class {
3974
+ #workspaceFolders = /* @__PURE__ */ new Map();
3975
+ #sessionFolders = /* @__PURE__ */ new Map();
3976
+ #workspaceAssignments = /* @__PURE__ */ new Map();
3977
+ #sessionAssignments = /* @__PURE__ */ new Map();
3978
+ #storePath;
3979
+ constructor(dataDir) {
3980
+ this.#storePath = path12.join(dataDir, "folders.json");
3981
+ }
3982
+ async load() {
3983
+ try {
3984
+ const raw = await readFile9(this.#storePath, "utf8");
3985
+ const parsed = JSON.parse(raw);
3986
+ this.#workspaceFolders = new Map(Object.entries(parsed.workspaceFolders ?? {}).filter(([, r]) => isWorkspaceFolder(r)));
3987
+ this.#sessionFolders = new Map(Object.entries(parsed.sessionFolders ?? {}).filter(([, r]) => isSessionFolder(r)));
3988
+ this.#workspaceAssignments = new Map(Object.entries(parsed.workspaceAssignments ?? {}).filter((entry) => typeof entry[1] === "string"));
3989
+ this.#sessionAssignments = new Map(Object.entries(parsed.sessionAssignments ?? {}).filter((entry) => typeof entry[1] === "string"));
3990
+ } catch {
3991
+ this.#workspaceFolders = /* @__PURE__ */ new Map();
3992
+ this.#sessionFolders = /* @__PURE__ */ new Map();
3993
+ this.#workspaceAssignments = /* @__PURE__ */ new Map();
3994
+ this.#sessionAssignments = /* @__PURE__ */ new Map();
3995
+ }
3996
+ }
3997
+ /** Все папки — для кадра `folder.list`. */
3998
+ list() {
3999
+ return {
4000
+ workspaceFolders: [...this.#workspaceFolders].map(([id, r]) => ({ id, name: r.name })),
4001
+ sessionFolders: [...this.#sessionFolders].map(([id, r]) => ({
4002
+ id,
4003
+ name: r.name,
4004
+ workspaceId: r.workspaceId
4005
+ }))
4006
+ };
4007
+ }
4008
+ async createWorkspaceFolder(name) {
4009
+ const id = randomUUID2();
4010
+ this.#workspaceFolders.set(id, { name });
4011
+ await this.#persist();
4012
+ return id;
4013
+ }
4014
+ async createSessionFolder(workspaceId, name) {
4015
+ const id = randomUUID2();
4016
+ this.#sessionFolders.set(id, { name, workspaceId });
4017
+ await this.#persist();
4018
+ return id;
4019
+ }
4020
+ /** Переименовать папку любого вида — id уникален между обоими видами. */
4021
+ async rename(folderId, name) {
4022
+ const ws = this.#workspaceFolders.get(folderId);
4023
+ if (ws) {
4024
+ this.#workspaceFolders.set(folderId, { ...ws, name });
4025
+ await this.#persist();
4026
+ return true;
4027
+ }
4028
+ const sess = this.#sessionFolders.get(folderId);
4029
+ if (sess) {
4030
+ this.#sessionFolders.set(folderId, { ...sess, name });
4031
+ await this.#persist();
4032
+ return true;
4033
+ }
4034
+ return false;
4035
+ }
3915
4036
  /**
3916
- * Забыть всё закреплённое в проектепри его закрытии.
4037
+ * Удалить папку. Элементы внутри неё не трогаются только перестают на неё
4038
+ * указывать, то есть становятся вне папок.
3917
4039
  *
3918
- * Закрытый проект не показывается нигде, и закреплённая сессия из него
3919
- * висела бы в панели строкой, по клику на которую открывать нечего.
4040
+ * Возвращает, какого вида была удалённая папка (и, для папки сессий, её
4041
+ * проект) вызывающему это нужно, чтобы разослать обновление только туда,
4042
+ * где папка была видна, а не всем спискам сразу.
3920
4043
  */
3921
- async forgetWorkspace(workspaceId) {
3922
- let changed = false;
3923
- for (const [id, record] of this.#pinned) {
3924
- if (record.workspaceId !== workspaceId)
3925
- continue;
3926
- this.#pinned.delete(id);
3927
- changed = true;
4044
+ async delete(folderId) {
4045
+ let removed = null;
4046
+ if (this.#workspaceFolders.delete(folderId)) {
4047
+ removed = { scope: "workspace" };
4048
+ } else {
4049
+ const sess = this.#sessionFolders.get(folderId);
4050
+ if (sess && this.#sessionFolders.delete(folderId)) {
4051
+ removed = { scope: "session", workspaceId: sess.workspaceId };
4052
+ }
3928
4053
  }
3929
- if (changed)
4054
+ if (!removed)
4055
+ return null;
4056
+ for (const [id, assigned] of this.#workspaceAssignments) {
4057
+ if (assigned === folderId)
4058
+ this.#workspaceAssignments.delete(id);
4059
+ }
4060
+ for (const [id, assigned] of this.#sessionAssignments) {
4061
+ if (assigned === folderId)
4062
+ this.#sessionAssignments.delete(id);
4063
+ }
4064
+ await this.#persist();
4065
+ return removed;
4066
+ }
4067
+ /** Разложить проект по папке или вынуть из неё (`folderId: null`). */
4068
+ async assignWorkspace(workspaceId, folderId) {
4069
+ if (folderId === null) {
4070
+ if (!this.#workspaceAssignments.delete(workspaceId))
4071
+ return false;
3930
4072
  await this.#persist();
3931
- return changed;
4073
+ return true;
4074
+ }
4075
+ if (!this.#workspaceFolders.has(folderId)) {
4076
+ throw new Error(`\u041F\u0430\u043F\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430: ${folderId}`);
4077
+ }
4078
+ this.#workspaceAssignments.set(workspaceId, folderId);
4079
+ await this.#persist();
4080
+ return true;
4081
+ }
4082
+ /**
4083
+ * Разложить сессию по папке или вынуть из неё.
4084
+ *
4085
+ * Папка сессии привязана к проекту — раскладывать сессию в папку чужого
4086
+ * проекта нельзя, иначе список одного проекта показывал бы группу, для
4087
+ * которой в нём нет ни одной другой строки.
4088
+ */
4089
+ async assignSession(sessionId, workspaceId, folderId) {
4090
+ if (folderId === null) {
4091
+ if (!this.#sessionAssignments.delete(sessionId))
4092
+ return false;
4093
+ await this.#persist();
4094
+ return true;
4095
+ }
4096
+ const folder = this.#sessionFolders.get(folderId);
4097
+ if (!folder)
4098
+ throw new Error(`\u041F\u0430\u043F\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430: ${folderId}`);
4099
+ if (folder.workspaceId !== workspaceId) {
4100
+ throw new Error("\u041F\u0430\u043F\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u0439 \u043F\u0440\u0438\u043D\u0430\u0434\u043B\u0435\u0436\u0438\u0442 \u0434\u0440\u0443\u0433\u043E\u043C\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0443");
4101
+ }
4102
+ this.#sessionAssignments.set(sessionId, folderId);
4103
+ await this.#persist();
4104
+ return true;
4105
+ }
4106
+ folderIdForWorkspace(workspaceId) {
4107
+ return this.#workspaceAssignments.get(workspaceId);
4108
+ }
4109
+ folderIdForSession(sessionId) {
4110
+ return this.#sessionAssignments.get(sessionId);
3932
4111
  }
3933
4112
  async #persist() {
3934
- const file = { pinned: Object.fromEntries(this.#pinned) };
3935
- await mkdir5(path11.dirname(this.#storePath), { recursive: true });
3936
- await writeFile6(this.#storePath, JSON.stringify(file, null, 2), "utf8");
4113
+ const file = {
4114
+ workspaceFolders: Object.fromEntries(this.#workspaceFolders),
4115
+ sessionFolders: Object.fromEntries(this.#sessionFolders),
4116
+ workspaceAssignments: Object.fromEntries(this.#workspaceAssignments),
4117
+ sessionAssignments: Object.fromEntries(this.#sessionAssignments)
4118
+ };
4119
+ await mkdir6(path12.dirname(this.#storePath), { recursive: true });
4120
+ await writeFile7(this.#storePath, JSON.stringify(file, null, 2), "utf8");
3937
4121
  }
3938
4122
  };
3939
- function isRecord2(value) {
3940
- return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
4123
+ function isWorkspaceFolder(value) {
4124
+ return typeof value === "object" && value !== null && typeof value.name === "string";
4125
+ }
4126
+ function isSessionFolder(value) {
4127
+ return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.workspaceId === "string";
3941
4128
  }
3942
4129
 
3943
4130
  // ../host/dist/app-settings.js
3944
- import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
4131
+ import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
3945
4132
  import { homedir as homedir3 } from "node:os";
3946
- import path12 from "node:path";
4133
+ import path13 from "node:path";
3947
4134
  var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
3948
4135
  var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
3949
4136
  var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
@@ -3978,11 +4165,11 @@ var AppSettingsStore = class {
3978
4165
  #settings = { ...DEFAULT_APP_SETTINGS };
3979
4166
  #storePath;
3980
4167
  constructor(dataDir) {
3981
- this.#storePath = path12.join(dataDir, "app-settings.json");
4168
+ this.#storePath = path13.join(dataDir, "app-settings.json");
3982
4169
  }
3983
4170
  async load() {
3984
4171
  try {
3985
- const raw = await readFile9(this.#storePath, "utf8");
4172
+ const raw = await readFile10(this.#storePath, "utf8");
3986
4173
  this.#settings = sanitize(JSON.parse(raw), DEFAULT_APP_SETTINGS);
3987
4174
  } catch {
3988
4175
  this.#settings = { ...DEFAULT_APP_SETTINGS };
@@ -4002,8 +4189,8 @@ var AppSettingsStore = class {
4002
4189
  async update(patch) {
4003
4190
  const next = sanitize(patch, this.#settings);
4004
4191
  this.#settings = next;
4005
- await mkdir6(path12.dirname(this.#storePath), { recursive: true });
4006
- await writeFile7(this.#storePath, JSON.stringify(next, null, 2), "utf8");
4192
+ await mkdir7(path13.dirname(this.#storePath), { recursive: true });
4193
+ await writeFile8(this.#storePath, JSON.stringify(next, null, 2), "utf8");
4007
4194
  return this.current;
4008
4195
  }
4009
4196
  };
@@ -4041,7 +4228,7 @@ function sanitize(value, fallback) {
4041
4228
  return result;
4042
4229
  }
4043
4230
  function claudeHome() {
4044
- return path12.join(homedir3(), ".claude");
4231
+ return path13.join(homedir3(), ".claude");
4045
4232
  }
4046
4233
  function writableRoots(mode, cwd, workspacePaths) {
4047
4234
  if (mode === "all")
@@ -4051,7 +4238,7 @@ function writableRoots(mode, cwd, workspacePaths) {
4051
4238
  return [...roots];
4052
4239
  for (const workspace of [cwd, ...workspacePaths]) {
4053
4240
  roots.add(workspace);
4054
- const parent = path12.dirname(workspace);
4241
+ const parent = path13.dirname(workspace);
4055
4242
  if (parent !== workspace)
4056
4243
  roots.add(parent);
4057
4244
  }
@@ -4059,9 +4246,9 @@ function writableRoots(mode, cwd, workspacePaths) {
4059
4246
  }
4060
4247
 
4061
4248
  // ../host/dist/plugins.js
4062
- import { readFile as readFile10 } from "node:fs/promises";
4249
+ import { readFile as readFile11 } from "node:fs/promises";
4063
4250
  import { homedir as homedir4 } from "node:os";
4064
- import path13 from "node:path";
4251
+ import path14 from "node:path";
4065
4252
  var TIMEOUT_MS2 = 12e4;
4066
4253
  var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
4067
4254
  var PluginCli = class {
@@ -4160,10 +4347,10 @@ async function readCatalog(markets, installed) {
4160
4347
  return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
4161
4348
  }
4162
4349
  async function readMarketplace(market, installed) {
4163
- const root = market.installLocation || path13.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
4350
+ const root = market.installLocation || path14.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
4164
4351
  let manifest;
4165
4352
  try {
4166
- const text = await readFile10(path13.join(root, ".claude-plugin", "marketplace.json"), "utf8");
4353
+ const text = await readFile11(path14.join(root, ".claude-plugin", "marketplace.json"), "utf8");
4167
4354
  manifest = JSON.parse(text);
4168
4355
  } catch {
4169
4356
  return [];
@@ -4404,8 +4591,8 @@ function describe4(error) {
4404
4591
 
4405
4592
  // ../host/dist/relay-link.js
4406
4593
  import { EventEmitter as EventEmitter3 } from "node:events";
4407
- import { readFile as readFile11, writeFile as writeFile8, rm as rm3, mkdir as mkdir7 } from "node:fs/promises";
4408
- import path14 from "node:path";
4594
+ import { readFile as readFile12, writeFile as writeFile9, rm as rm3, mkdir as mkdir8 } from "node:fs/promises";
4595
+ import path15 from "node:path";
4409
4596
  import { hostname } from "node:os";
4410
4597
  import WebSocket from "ws";
4411
4598
 
@@ -4460,7 +4647,7 @@ var RelayLink = class extends EventEmitter3 {
4460
4647
  #log;
4461
4648
  constructor(options2) {
4462
4649
  super();
4463
- this.#file = path14.join(options2.dataDir, "relay.json");
4650
+ this.#file = path15.join(options2.dataDir, "relay.json");
4464
4651
  this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
4465
4652
  this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
4466
4653
  }
@@ -4541,7 +4728,7 @@ var RelayLink = class extends EventEmitter3 {
4541
4728
  }
4542
4729
  async #read() {
4543
4730
  try {
4544
- const raw = await readFile11(this.#file, "utf8");
4731
+ const raw = await readFile12(this.#file, "utf8");
4545
4732
  const parsed = JSON.parse(raw);
4546
4733
  if (typeof parsed !== "object" || parsed === null)
4547
4734
  return null;
@@ -4554,8 +4741,8 @@ var RelayLink = class extends EventEmitter3 {
4554
4741
  }
4555
4742
  }
4556
4743
  async #write(credentials) {
4557
- await mkdir7(path14.dirname(this.#file), { recursive: true });
4558
- await writeFile8(this.#file, `${JSON.stringify(credentials, null, 2)}
4744
+ await mkdir8(path15.dirname(this.#file), { recursive: true });
4745
+ await writeFile9(this.#file, `${JSON.stringify(credentials, null, 2)}
4559
4746
  `, {
4560
4747
  encoding: "utf8",
4561
4748
  mode: 384
@@ -4812,8 +4999,8 @@ function humanize(error) {
4812
4999
  // ../host/dist/terminal.js
4813
5000
  import { EventEmitter as EventEmitter4 } from "node:events";
4814
5001
  import { access as access2, constants as constants2 } from "node:fs/promises";
4815
- import path15 from "node:path";
4816
- import { randomUUID as randomUUID2 } from "node:crypto";
5002
+ import path16 from "node:path";
5003
+ import { randomUUID as randomUUID3 } from "node:crypto";
4817
5004
  var SCROLLBACK_BYTES = 256 * 1024;
4818
5005
  var FLUSH_MS = 12;
4819
5006
  var MAX_TERMINALS = 24;
@@ -4850,7 +5037,7 @@ var TerminalHub = class extends EventEmitter4 {
4850
5037
  env: environment()
4851
5038
  });
4852
5039
  const info = {
4853
- id: randomUUID2(),
5040
+ id: randomUUID3(),
4854
5041
  workspaceId: options2.workspaceId,
4855
5042
  title: shell.title,
4856
5043
  shell: shell.file,
@@ -4982,22 +5169,22 @@ async function defaultShell() {
4982
5169
  if (pwsh)
4983
5170
  return { file: pwsh, args: [], title: "pwsh" };
4984
5171
  const system = process.env.SystemRoot ?? "C:\\Windows";
4985
- const powershell = path15.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
5172
+ const powershell = path16.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
4986
5173
  if (await exists2(powershell))
4987
5174
  return { file: powershell, args: [], title: "powershell" };
4988
5175
  return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
4989
5176
  }
4990
5177
  const shell = process.env.SHELL;
4991
5178
  if (shell && await exists2(shell))
4992
- return { file: shell, args: [], title: path15.basename(shell) };
5179
+ return { file: shell, args: [], title: path16.basename(shell) };
4993
5180
  if (await exists2("/bin/bash"))
4994
5181
  return { file: "/bin/bash", args: [], title: "bash" };
4995
5182
  return { file: "/bin/sh", args: [], title: "sh" };
4996
5183
  }
4997
5184
  async function findOnPath(name) {
4998
- const dirs = (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean);
5185
+ const dirs = (process.env.PATH ?? "").split(path16.delimiter).filter(Boolean);
4999
5186
  for (const dir of dirs) {
5000
- const candidate = path15.join(dir, name);
5187
+ const candidate = path16.join(dir, name);
5001
5188
  if (await exists2(candidate))
5002
5189
  return candidate;
5003
5190
  }
@@ -5008,10 +5195,10 @@ function exists2(file) {
5008
5195
  }
5009
5196
 
5010
5197
  // ../host/dist/mcp-config.js
5011
- import { readFile as readFile12, writeFile as writeFile9, mkdir as mkdir8 } from "node:fs/promises";
5012
- import path16 from "node:path";
5198
+ import { readFile as readFile13, writeFile as writeFile10, mkdir as mkdir9 } from "node:fs/promises";
5199
+ import path17 from "node:path";
5013
5200
  var MCP_FILE = ".mcp.json";
5014
- var LOCAL_SETTINGS2 = path16.join(".claude", "settings.local.json");
5201
+ var LOCAL_SETTINGS2 = path17.join(".claude", "settings.local.json");
5015
5202
  var queues = /* @__PURE__ */ new Map();
5016
5203
  function serialize(cwd, work) {
5017
5204
  const previous = queues.get(cwd) ?? Promise.resolve();
@@ -5020,7 +5207,7 @@ function serialize(cwd, work) {
5020
5207
  return next;
5021
5208
  }
5022
5209
  async function readProjectServers(cwd) {
5023
- const file = await readJson(path16.join(cwd, MCP_FILE));
5210
+ const file = await readJson(path17.join(cwd, MCP_FILE));
5024
5211
  const servers = file?.mcpServers;
5025
5212
  if (!servers || typeof servers !== "object")
5026
5213
  return {};
@@ -5028,23 +5215,23 @@ async function readProjectServers(cwd) {
5028
5215
  }
5029
5216
  function addProjectServer(cwd, name, config) {
5030
5217
  return serialize(cwd, async () => {
5031
- const file = await readJson(path16.join(cwd, MCP_FILE)) ?? {};
5218
+ const file = await readJson(path17.join(cwd, MCP_FILE)) ?? {};
5032
5219
  const servers = file.mcpServers ?? {};
5033
5220
  servers[name] = config;
5034
5221
  file.mcpServers = servers;
5035
- await writeJson(path16.join(cwd, MCP_FILE), file);
5222
+ await writeJson(path17.join(cwd, MCP_FILE), file);
5036
5223
  await approve(cwd, name);
5037
5224
  });
5038
5225
  }
5039
5226
  function removeProjectServer(cwd, name) {
5040
5227
  return serialize(cwd, async () => {
5041
- const file = await readJson(path16.join(cwd, MCP_FILE));
5228
+ const file = await readJson(path17.join(cwd, MCP_FILE));
5042
5229
  const servers = file?.mcpServers;
5043
5230
  if (!servers || !(name in servers))
5044
5231
  return false;
5045
5232
  delete servers[name];
5046
- await writeJson(path16.join(cwd, MCP_FILE), file);
5047
- const settingsPath = path16.join(cwd, LOCAL_SETTINGS2);
5233
+ await writeJson(path17.join(cwd, MCP_FILE), file);
5234
+ const settingsPath = path17.join(cwd, LOCAL_SETTINGS2);
5048
5235
  const settings = await readJson(settingsPath);
5049
5236
  const enabled = settings?.enabledMcpjsonServers;
5050
5237
  if (settings && Array.isArray(enabled) && enabled.includes(name)) {
@@ -5055,7 +5242,7 @@ function removeProjectServer(cwd, name) {
5055
5242
  });
5056
5243
  }
5057
5244
  async function approve(cwd, name) {
5058
- const file = path16.join(cwd, LOCAL_SETTINGS2);
5245
+ const file = path17.join(cwd, LOCAL_SETTINGS2);
5059
5246
  const settings = await readJson(file) ?? {};
5060
5247
  const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
5061
5248
  if (!enabled.includes(name))
@@ -5094,21 +5281,21 @@ function describeConfig(config) {
5094
5281
  }
5095
5282
  async function readJson(file) {
5096
5283
  try {
5097
- const raw = await readFile12(file, "utf8");
5284
+ const raw = await readFile13(file, "utf8");
5098
5285
  const parsed = JSON.parse(raw);
5099
5286
  return parsed && typeof parsed === "object" ? parsed : null;
5100
5287
  } catch (error) {
5101
5288
  if (error.code === "ENOENT")
5102
5289
  return null;
5103
5290
  if (error instanceof SyntaxError) {
5104
- throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path16.basename(file)}: ${error.message}`);
5291
+ throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path17.basename(file)}: ${error.message}`);
5105
5292
  }
5106
5293
  throw error;
5107
5294
  }
5108
5295
  }
5109
5296
  async function writeJson(file, data) {
5110
- await mkdir8(path16.dirname(file), { recursive: true });
5111
- await writeFile9(file, `${JSON.stringify(data, null, 2)}
5297
+ await mkdir9(path17.dirname(file), { recursive: true });
5298
+ await writeFile10(file, `${JSON.stringify(data, null, 2)}
5112
5299
  `, "utf8");
5113
5300
  }
5114
5301
 
@@ -5122,6 +5309,7 @@ var HostServer = class {
5122
5309
  #workspaces;
5123
5310
  #archive;
5124
5311
  #pins;
5312
+ #folders;
5125
5313
  #settings;
5126
5314
  #finder;
5127
5315
  #sessions = /* @__PURE__ */ new Map();
@@ -5139,7 +5327,7 @@ var HostServer = class {
5139
5327
  #clients = /* @__PURE__ */ new Set();
5140
5328
  /** Удалённые вкладки по их id в relay — чтобы адресовать ответ именно той. */
5141
5329
  #remotes = /* @__PURE__ */ new Map();
5142
- #hostId = randomUUID3();
5330
+ #hostId = randomUUID4();
5143
5331
  #plugins = new PluginCli();
5144
5332
  #auth = new AuthCli();
5145
5333
  #relay;
@@ -5159,6 +5347,7 @@ var HostServer = class {
5159
5347
  this.#workspaces = new WorkspaceRegistry(options2.dataDir);
5160
5348
  this.#archive = new SessionArchive(options2.dataDir);
5161
5349
  this.#pins = new SessionPins(options2.dataDir);
5350
+ this.#folders = new Folders(options2.dataDir);
5162
5351
  this.#settings = new AppSettingsStore(options2.dataDir);
5163
5352
  this.#finder = new ProjectFinder(this.#workspaces);
5164
5353
  this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
@@ -5176,6 +5365,7 @@ var HostServer = class {
5176
5365
  await this.#workspaces.load();
5177
5366
  await this.#archive.load();
5178
5367
  await this.#pins.load();
5368
+ await this.#folders.load();
5179
5369
  await this.#settings.load();
5180
5370
  await this.#github.load();
5181
5371
  await warmClaudeLauncher();
@@ -5325,6 +5515,27 @@ var HostServer = class {
5325
5515
  client.send(frame);
5326
5516
  }
5327
5517
  }
5518
+ /** Активные и архивные проекты разом, с папкой каждого — для `workspace.list`. */
5519
+ #workspaceLists() {
5520
+ const withFolder = (w) => {
5521
+ const folderId = this.#folders.folderIdForWorkspace(w.id);
5522
+ return folderId ? { ...w, folderId } : w;
5523
+ };
5524
+ return {
5525
+ workspaces: this.#workspaces.list().map(withFolder),
5526
+ archived: this.#workspaces.listArchived().map(withFolder)
5527
+ };
5528
+ }
5529
+ #broadcastWorkspaces() {
5530
+ this.#broadcast({ type: "workspace.list", ...this.#workspaceLists() });
5531
+ }
5532
+ #sendFolders(client) {
5533
+ const frame = { type: "folder.list", ...this.#folders.list() };
5534
+ if (client)
5535
+ this.#send(client, frame);
5536
+ else
5537
+ this.#broadcast(frame);
5538
+ }
5328
5539
  async #handleFrame(socket, frame) {
5329
5540
  switch (frame.type) {
5330
5541
  case "hello":
@@ -5335,10 +5546,8 @@ var HostServer = class {
5335
5546
  hostVersion: this.#options.version,
5336
5547
  platform: process.platform
5337
5548
  });
5338
- this.#send(socket, {
5339
- type: "workspace.list",
5340
- workspaces: this.#workspaces.list()
5341
- });
5549
+ this.#send(socket, { type: "workspace.list", ...this.#workspaceLists() });
5550
+ this.#sendFolders(socket);
5342
5551
  this.#auth.status().then((status) => this.#send(socket, { type: "auth.status", status })).catch(() => void 0);
5343
5552
  this.#send(socket, { type: "relay.state", state: this.#relay.state });
5344
5553
  return;
@@ -5346,17 +5555,11 @@ var HostServer = class {
5346
5555
  this.#send(socket, { type: "pong", nonce: frame.nonce });
5347
5556
  return;
5348
5557
  case "workspace.list":
5349
- this.#send(socket, {
5350
- type: "workspace.list",
5351
- workspaces: this.#workspaces.list()
5352
- });
5558
+ this.#send(socket, { type: "workspace.list", ...this.#workspaceLists() });
5353
5559
  return;
5354
5560
  case "workspace.open":
5355
5561
  await this.#workspaces.open(frame.path);
5356
- this.#broadcast({
5357
- type: "workspace.list",
5358
- workspaces: this.#workspaces.list()
5359
- });
5562
+ this.#broadcastWorkspaces();
5360
5563
  await this.#closeFinder();
5361
5564
  return;
5362
5565
  case "workspace.close": {
@@ -5369,17 +5572,14 @@ var HostServer = class {
5369
5572
  continue;
5370
5573
  await this.#dropSession(session.id, "workspace closed");
5371
5574
  }
5372
- let unpinned = false;
5373
- for (const id of gone) {
5374
- if (await this.#pins.forgetWorkspace(id))
5375
- unpinned = true;
5376
- }
5377
- this.#broadcast({
5378
- type: "workspace.list",
5379
- workspaces: this.#workspaces.list()
5380
- });
5381
- if (unpinned)
5382
- await this.#sendPinned();
5575
+ this.#broadcastWorkspaces();
5576
+ return;
5577
+ }
5578
+ case "workspace.restore": {
5579
+ const restored = await this.#workspaces.restore(frame.workspaceIds);
5580
+ if (restored.length === 0)
5581
+ return;
5582
+ this.#broadcastWorkspaces();
5383
5583
  return;
5384
5584
  }
5385
5585
  case "workspace.find": {
@@ -5401,10 +5601,7 @@ var HostServer = class {
5401
5601
  return;
5402
5602
  case "workspace.create": {
5403
5603
  const { workspace, warnings } = await this.#workspaces.create(frame.parent, frame.name, frame.init);
5404
- this.#broadcast({
5405
- type: "workspace.list",
5406
- workspaces: this.#workspaces.list()
5407
- });
5604
+ this.#broadcastWorkspaces();
5408
5605
  this.#send(socket, { type: "workspace.created", workspace, warnings });
5409
5606
  await this.#closeFinder();
5410
5607
  return;
@@ -5466,6 +5663,42 @@ var HostServer = class {
5466
5663
  case "session.pins":
5467
5664
  await this.#sendPinned();
5468
5665
  return;
5666
+ case "folder.create": {
5667
+ if (frame.scope === "session") {
5668
+ if (!frame.workspaceId)
5669
+ throw new Error("\u0414\u043B\u044F \u043F\u0430\u043F\u043A\u0438 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0443\u0436\u0435\u043D workspaceId");
5670
+ await this.#folders.createSessionFolder(frame.workspaceId, frame.name);
5671
+ } else {
5672
+ await this.#folders.createWorkspaceFolder(frame.name);
5673
+ }
5674
+ this.#sendFolders();
5675
+ return;
5676
+ }
5677
+ case "folder.rename":
5678
+ if (await this.#folders.rename(frame.folderId, frame.name))
5679
+ this.#sendFolders();
5680
+ return;
5681
+ case "folder.delete": {
5682
+ const removed = await this.#folders.delete(frame.folderId);
5683
+ if (!removed)
5684
+ return;
5685
+ this.#sendFolders();
5686
+ if (removed.scope === "workspace")
5687
+ this.#broadcastWorkspaces();
5688
+ else
5689
+ await this.#sendSessionList(removed.workspaceId);
5690
+ return;
5691
+ }
5692
+ case "folder.assign": {
5693
+ if (frame.workspaceId) {
5694
+ await this.#folders.assignSession(frame.itemId, frame.workspaceId, frame.folderId);
5695
+ await this.#sendSessionList(frame.workspaceId);
5696
+ } else {
5697
+ await this.#folders.assignWorkspace(frame.itemId, frame.folderId);
5698
+ this.#broadcastWorkspaces();
5699
+ }
5700
+ return;
5701
+ }
5469
5702
  case "session.rename":
5470
5703
  await this.#renameSession(frame.sessionId, frame.title, frame.workspaceId);
5471
5704
  return;
@@ -6532,7 +6765,12 @@ var HostServer = class {
6532
6765
  const archived = [];
6533
6766
  for (const base of [...live, ...historical].sort((a, b) => b.updatedAt - a.updatedAt)) {
6534
6767
  const pin = this.#pins.find(base.claudeSessionId ?? base.id);
6535
- const summary = pin ? { ...base, pinned: true, pinnedAt: pin.at } : base;
6768
+ const folderId = this.#folders.folderIdForSession(base.claudeSessionId ?? base.id);
6769
+ const summary = {
6770
+ ...base,
6771
+ ...pin ? { pinned: true, pinnedAt: pin.at } : {},
6772
+ ...folderId ? { folderId } : {}
6773
+ };
6536
6774
  const record = this.#archive.find(summary.claudeSessionId ?? summary.id);
6537
6775
  if (!record) {
6538
6776
  sessions.push(summary);
@@ -6570,8 +6808,8 @@ var HostServer = class {
6570
6808
  return;
6571
6809
  }
6572
6810
  const url2 = new URL(req.url ?? "/", "http://localhost");
6573
- const requested = path17.join(webRoot, path17.normalize(url2.pathname));
6574
- const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested : path17.join(webRoot, "index.html");
6811
+ const requested = path18.join(webRoot, path18.normalize(url2.pathname));
6812
+ const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested : path18.join(webRoot, "index.html");
6575
6813
  if (!existsSync3(target)) {
6576
6814
  res.writeHead(404).end("not found");
6577
6815
  return;
@@ -6597,12 +6835,12 @@ function contentType(file) {
6597
6835
  ".svg": "image/svg+xml",
6598
6836
  ".woff2": "font/woff2"
6599
6837
  };
6600
- return types[path17.extname(file)] ?? "application/octet-stream";
6838
+ return types[path18.extname(file)] ?? "application/octet-stream";
6601
6839
  }
6602
6840
 
6603
6841
  // dist/ccv.js
6604
- var HERE = path18.dirname(fileURLToPath(import.meta.url));
6605
- var PACKAGE_ROOT = path18.resolve(HERE, "..");
6842
+ var HERE = path19.dirname(fileURLToPath(import.meta.url));
6843
+ var PACKAGE_ROOT = path19.resolve(HERE, "..");
6606
6844
  var USAGE = `ccv \u2014 Claude Code Visual: \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u0445\u043E\u0441\u0442 Claude Code \u0441 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043E\u043C \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.
6607
6845
 
6608
6846
  \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
@@ -6669,7 +6907,7 @@ var server = new HostServer({
6669
6907
  dataDir: defaults.dataDir,
6670
6908
  // UI лежит в пакете рядом с бандлом — это и есть разница между установленной
6671
6909
  // утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
6672
- webRoot: process.env.CCV_WEB_ROOT ?? path18.join(PACKAGE_ROOT, "web"),
6910
+ webRoot: process.env.CCV_WEB_ROOT ?? path19.join(PACKAGE_ROOT, "web"),
6673
6911
  version,
6674
6912
  relayUrl: defaults.relayUrl
6675
6913
  });
@@ -6698,7 +6936,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
6698
6936
  }
6699
6937
  async function packageVersion() {
6700
6938
  try {
6701
- const text = await readFile13(path18.join(PACKAGE_ROOT, "package.json"), "utf8");
6939
+ const text = await readFile14(path19.join(PACKAGE_ROOT, "package.json"), "utf8");
6702
6940
  return JSON.parse(text).version ?? "0.0.0";
6703
6941
  } catch {
6704
6942
  return "0.0.0";