@i14ks/ccv 0.4.1 → 0.6.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 (27) hide show
  1. package/bundle/ccv.mjs +412 -32
  2. package/package.json +1 -1
  3. package/web/assets/{codemirror-ixGO-cbX.js → codemirror-DILVW5V7.js} +1 -1
  4. package/web/assets/index-5NnNKmcv.js +129 -0
  5. package/web/assets/{index-D1EGRMsF.js → index-BTNk6BN3.js} +1 -1
  6. package/web/assets/{index-_Y-EJULt.js → index-BjUFOQlC.js} +1 -1
  7. package/web/assets/index-Brn_uUCW.css +1 -0
  8. package/web/assets/{index-BuniMO1x.js → index-C2x9TM0V.js} +1 -1
  9. package/web/assets/{index-DqcI6biV.js → index-C41Hz81g.js} +1 -1
  10. package/web/assets/{index-Byl4r_xt.js → index-CC6apbYr.js} +1 -1
  11. package/web/assets/{index-EaPgYsZb.js → index-CP6Bv1lU.js} +1 -1
  12. package/web/assets/{index-D3avVSzn.js → index-CSzD2Fvc.js} +1 -1
  13. package/web/assets/{index-EHaCUl16.js → index-CVhjeQrh.js} +1 -1
  14. package/web/assets/{index-u0GQ-BLb.js → index-Cp4y8ln8.js} +1 -1
  15. package/web/assets/{index-GQ3tJswl.js → index-Cptzajqg.js} +1 -1
  16. package/web/assets/{index-CXiddYxZ.js → index-D18_1PH5.js} +1 -1
  17. package/web/assets/{index-CzWhd9WN.js → index-D2sBuEGv.js} +1 -1
  18. package/web/assets/{index-D2QcAee0.js → index-DJ1r3yCI.js} +1 -1
  19. package/web/assets/{index-DcGZZXlE.js → index-DJhubbAh.js} +1 -1
  20. package/web/assets/{index-CELwK_ze.js → index-Dr-2FTDh.js} +1 -1
  21. package/web/assets/{index-DAN-BZ4M.js → index-DuJE332D.js} +1 -1
  22. package/web/assets/{index-CtUwaQ4R.js → index-fBBR-7pG.js} +1 -1
  23. package/web/assets/{index-CbrZsnLR.js → index-qQ4Nk5pP.js} +1 -1
  24. package/web/assets/{index-jausLDAl.js → index-zZliJVYN.js} +1 -1
  25. package/web/index.html +2 -2
  26. package/web/assets/index-CQJq2xwX.css +0 -1
  27. package/web/assets/index-VtR2fk7J.js +0 -128
package/bundle/ccv.mjs CHANGED
@@ -215,7 +215,7 @@ var AsyncQueue = class {
215
215
  };
216
216
 
217
217
  // ../host/dist/files.js
218
- import { lstat, mkdir, readFile as readFile2, readdir, realpath, rename as renamePath, rm, stat, writeFile } from "node:fs/promises";
218
+ import { cp, lstat, mkdir, readFile as readFile2, readdir, realpath, rename as renamePath, rm, stat, writeFile } from "node:fs/promises";
219
219
  import path3 from "node:path";
220
220
  var MAX_FILE = 4 * 1024 * 1024;
221
221
  var SNIFF = 8192;
@@ -353,9 +353,12 @@ var ProjectFiles = class {
353
353
  if (!info || info.size > MAX_FILE)
354
354
  continue;
355
355
  const buffer = await readFile2(full).catch(() => null);
356
- if (!buffer || isBinary(buffer))
356
+ if (!buffer)
357
357
  continue;
358
- const lines = buffer.toString("utf8").split(/\r\n|\n/);
358
+ const decoded = decode(buffer);
359
+ if (!decoded)
360
+ continue;
361
+ const lines = decoded.text.split(/\r\n|\n/);
359
362
  for (let i = 0; i < lines.length; i++) {
360
363
  const line = lines[i];
361
364
  const lower = line.toLowerCase();
@@ -400,6 +403,7 @@ var ProjectFiles = class {
400
403
  path: normalize(relative),
401
404
  content: "",
402
405
  eol: "lf",
406
+ encoding: "utf8",
403
407
  mtime: Math.round(info.mtimeMs),
404
408
  size: info.size,
405
409
  binary: false,
@@ -408,14 +412,15 @@ var ProjectFiles = class {
408
412
  if (info.size > MAX_FILE)
409
413
  return { ...blank, tooLarge: true };
410
414
  const buffer = await readFile2(file);
411
- if (isBinary(buffer))
415
+ const decoded = decode(buffer);
416
+ if (!decoded)
412
417
  return { ...blank, binary: true };
413
- const text = buffer.toString("utf8");
414
- const crlf = text.includes("\r\n");
418
+ const crlf = decoded.text.includes("\r\n");
415
419
  return {
416
420
  ...blank,
417
- content: crlf ? text.replace(/\r\n/g, "\n") : text,
418
- eol: crlf ? "crlf" : "lf"
421
+ content: crlf ? decoded.text.replace(/\r\n/g, "\n") : decoded.text,
422
+ eol: crlf ? "crlf" : "lf",
423
+ encoding: decoded.encoding
419
424
  };
420
425
  }
421
426
  /**
@@ -426,7 +431,7 @@ var ProjectFiles = class {
426
431
  * молчаливая перезапись стёрла бы его работу. `force` снимает проверку —
427
432
  * это сознательное решение пользователя, принятое после отказа.
428
433
  */
429
- async write(relative, content, eol, baseMtime, force = false) {
434
+ async write(relative, content, eol, encoding, baseMtime, force = false) {
430
435
  const file = await this.#resolve(relative);
431
436
  const info = await stat(file).catch(() => null);
432
437
  if (info?.isDirectory())
@@ -437,7 +442,7 @@ var ProjectFiles = class {
437
442
  throw new ConflictError(info ? "\u0424\u0430\u0439\u043B \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u0442\u043E\u0433\u043E, \u043A\u0430\u043A \u0435\u0433\u043E \u043E\u0442\u043A\u0440\u044B\u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435" : "\u0424\u0430\u0439\u043B \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u0438\u0441\u0447\u0435\u0437 \u043F\u043E\u0441\u043B\u0435 \u0442\u043E\u0433\u043E, \u043A\u0430\u043A \u0435\u0433\u043E \u043E\u0442\u043A\u0440\u044B\u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435");
438
443
  }
439
444
  }
440
- await writeFile(file, eol === "crlf" ? content.replace(/\n/g, "\r\n") : content, "utf8");
445
+ await writeFile(file, encode(content, eol, encoding));
441
446
  const saved = await stat(file);
442
447
  return { mtime: Math.round(saved.mtimeMs), size: saved.size };
443
448
  }
@@ -462,6 +467,40 @@ var ProjectFiles = class {
462
467
  await mkdir(path3.dirname(target), { recursive: true });
463
468
  await renamePath(from, target);
464
469
  }
470
+ /**
471
+ * Скопировать файл или каталог внутрь `toDir`; возвращает путь копии.
472
+ *
473
+ * Имя подбирает хост, а не клиент: обычная вставка — рядом с оригиналом, в
474
+ * тот же каталог, и там исходное имя занято. Свободное имя ищется перебором
475
+ * (`имя copy`, `имя copy 2`, …) — так же, как это делает проводник; на
476
+ * гонку между проверкой и копированием это не рассчитано, но `cp` с
477
+ * `errorOnExist` в ней откажет, а не затрёт чужое.
478
+ */
479
+ async copy(relative, toDir) {
480
+ const source = normalize(relative);
481
+ if (!source)
482
+ throw new Error("\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043E\u0440\u0435\u043D\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043D\u0435\u043B\u044C\u0437\u044F");
483
+ const from = await this.#resolve(source);
484
+ const info = await stat(from);
485
+ const dir = normalize(toDir);
486
+ if (info.isDirectory() && (dir === source || dir.startsWith(`${source}/`))) {
487
+ throw new Error("\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435\u043B\u044C\u0437\u044F \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u043D\u0443\u0442\u0440\u044C \u0441\u0430\u043C\u043E\u0433\u043E \u0441\u0435\u0431\u044F");
488
+ }
489
+ const name = source.slice(source.lastIndexOf("/") + 1);
490
+ let target = "";
491
+ let to = "";
492
+ for (let attempt = 0; ; attempt++) {
493
+ if (attempt > COPY_NAME_LIMIT)
494
+ throw new Error("\u041D\u0435\u043A\u0443\u0434\u0430 \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u044C \u043A\u043E\u043F\u0438\u044E: \u0438\u043C\u0435\u043D\u0430 \u0437\u0430\u043D\u044F\u0442\u044B");
495
+ target = join(dir, copyName(name, attempt, info.isDirectory()));
496
+ to = await this.#resolve(target);
497
+ if (!await exists(to))
498
+ break;
499
+ }
500
+ await mkdir(path3.dirname(to), { recursive: true });
501
+ await cp(from, to, { recursive: true, errorOnExist: true, force: false });
502
+ return target;
503
+ }
465
504
  async remove(relative) {
466
505
  const target = await this.#resolve(relative);
467
506
  if (!normalize(relative))
@@ -513,9 +552,88 @@ function contains(root, target) {
513
552
  async function exists(file) {
514
553
  return lstat(file).then(() => true, () => false);
515
554
  }
516
- function isBinary(buffer) {
555
+ var COPY_NAME_LIMIT = 200;
556
+ function copyName(name, attempt, isDir) {
557
+ if (attempt === 0)
558
+ return name;
559
+ const dot = isDir ? -1 : name.lastIndexOf(".");
560
+ const stem = dot > 0 ? name.slice(0, dot) : name;
561
+ const ext = dot > 0 ? name.slice(dot) : "";
562
+ return attempt === 1 ? `${stem} copy${ext}` : `${stem} copy ${attempt}${ext}`;
563
+ }
564
+ function decode(buffer) {
565
+ if (buffer.length >= 3 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191) {
566
+ return { encoding: "utf8bom", text: buffer.toString("utf8", 3) };
567
+ }
568
+ if (buffer.length >= 2 && buffer[0] === 255 && buffer[1] === 254) {
569
+ return { encoding: "utf16le", text: fromUtf16(buffer.subarray(2), "le") };
570
+ }
571
+ if (buffer.length >= 2 && buffer[0] === 254 && buffer[1] === 255) {
572
+ return { encoding: "utf16be", text: fromUtf16(buffer.subarray(2), "be") };
573
+ }
517
574
  const head = buffer.subarray(0, SNIFF);
518
- return head.includes(0);
575
+ if (!head.includes(0))
576
+ return { encoding: "utf8", text: buffer.toString("utf8") };
577
+ const guess = sniffUtf16(head);
578
+ if (!guess)
579
+ return null;
580
+ const text = fromUtf16(buffer, guess === "utf16le" ? "le" : "be");
581
+ return looksTextual(text) ? { encoding: guess, text } : null;
582
+ }
583
+ function encode(content, eol, encoding) {
584
+ const text = eol === "crlf" ? content.replace(/\n/g, "\r\n") : content;
585
+ switch (encoding) {
586
+ case "utf8bom":
587
+ return Buffer.concat([Buffer.from([239, 187, 191]), Buffer.from(text, "utf8")]);
588
+ // BOM пишем и тогда, когда его не было: без него кодировку каждый читатель
589
+ // файла определяет тем же гаданием, что и мы, — и ошибается там же.
590
+ case "utf16le":
591
+ return Buffer.concat([Buffer.from([255, 254]), Buffer.from(text, "utf16le")]);
592
+ case "utf16be": {
593
+ const body = Buffer.from(text, "utf16le");
594
+ body.swap16();
595
+ return Buffer.concat([Buffer.from([254, 255]), body]);
596
+ }
597
+ default:
598
+ return Buffer.from(text, "utf8");
599
+ }
600
+ }
601
+ function fromUtf16(body, order) {
602
+ const even = body.length % 2 === 0 ? body : body.subarray(0, body.length - 1);
603
+ if (order === "le")
604
+ return even.toString("utf16le");
605
+ const swapped = Buffer.from(even);
606
+ swapped.swap16();
607
+ return swapped.toString("utf16le");
608
+ }
609
+ function sniffUtf16(head) {
610
+ const len = head.length - head.length % 2;
611
+ if (len < 2)
612
+ return null;
613
+ let evenZero = 0;
614
+ let oddZero = 0;
615
+ for (let i = 0; i < len; i += 2) {
616
+ if (head[i] === 0)
617
+ evenZero++;
618
+ if (head[i + 1] === 0)
619
+ oddZero++;
620
+ }
621
+ if (oddZero > 0 && evenZero === 0)
622
+ return "utf16le";
623
+ if (evenZero > 0 && oddZero === 0)
624
+ return "utf16be";
625
+ return null;
626
+ }
627
+ function looksTextual(text) {
628
+ const head = text.length > SNIFF ? text.slice(0, SNIFF) : text;
629
+ for (const char of head) {
630
+ const code = char.codePointAt(0);
631
+ if (code === 65533)
632
+ return false;
633
+ if (code < 32 && char !== " " && char !== "\n" && char !== "\r")
634
+ return false;
635
+ }
636
+ return true;
519
637
  }
520
638
 
521
639
  // ../host/dist/journal.js
@@ -835,11 +953,16 @@ var STOPPED_REASONS = /* @__PURE__ */ new Set([
835
953
  "tool_deferred_unavailable",
836
954
  "background_requested"
837
955
  ]);
956
+ var API_ERROR_REASON = "api_error";
957
+ var PERMANENT_API_STATUS = /* @__PURE__ */ new Set([400, 404, 413, 422, 429]);
958
+ var RECONNECT_DELAYS_MS = [1e3, 5e3, 2e4, 6e4];
838
959
  function turnOutcome(terminalReason, subtype, interruptRequested) {
839
960
  if (terminalReason && ABORTED_REASONS.has(terminalReason))
840
961
  return "interrupted";
841
962
  if (terminalReason && STOPPED_REASONS.has(terminalReason))
842
963
  return "stopped";
964
+ if (terminalReason === API_ERROR_REASON)
965
+ return "failed";
843
966
  if (subtype === "success")
844
967
  return "completed";
845
968
  if (interruptRequested)
@@ -1016,6 +1139,37 @@ var ClaudeSession = class extends EventEmitter {
1016
1139
  /** Когда была последняя попытка его получить — удачная или нет. */
1017
1140
  #contextAt = 0;
1018
1141
  #contextInFlight = false;
1142
+ /**
1143
+ * Сессию закрывают намеренно — переподключаться не надо.
1144
+ *
1145
+ * Обрыв потока сам по себе не говорит, что случилось: и `close()`, и упавший
1146
+ * VPN выглядят одинаково — итератор кончился. Отличает их только этот флаг.
1147
+ */
1148
+ #closing = false;
1149
+ /** Сколько раз подряд уже пробовали переподключиться; сбрасывается удачей. */
1150
+ #reconnectAttempt = 0;
1151
+ #reconnectTimer = null;
1152
+ /**
1153
+ * Промпты, которые не доехали: их отправят заново, когда сессия вернётся.
1154
+ *
1155
+ * Повторяем только то, что заведомо ничего не успело сделать (см.
1156
+ * `#turnDidWork`), — иначе повтор доделывал бы работу второй раз.
1157
+ *
1158
+ * Список, а не одно значение: пока сессия ищет себе новый процесс, человек
1159
+ * вполне может дописать ещё реплику — и в ленте она уже есть. Затереть ею
1160
+ * несостоявшуюся первую значило бы показывать вопрос, на который никто
1161
+ * никогда не ответит.
1162
+ */
1163
+ #pendingRetry = [];
1164
+ /** Промпт текущего хода — то, что придётся отправить заново, если ход сорвётся. */
1165
+ #lastPrompt = null;
1166
+ /**
1167
+ * Ход успел что-то предпринять: вызвал инструмент или сказал что-то своё.
1168
+ *
1169
+ * Граница между «запрос не ушёл» и «работа началась». Повторять можно только
1170
+ * первое: ход, успевший записать файл, при повторе запишет его дважды.
1171
+ */
1172
+ #turnDidWork = false;
1019
1173
  #createdAt = Date.now();
1020
1174
  #updatedAt = Date.now();
1021
1175
  #options;
@@ -1096,6 +1250,17 @@ var ClaudeSession = class extends EventEmitter {
1096
1250
  const until = Date.now();
1097
1251
  this.#history = readTranscript(this.#options.resume, { dir: this.cwd, until });
1098
1252
  }
1253
+ this.#spawn(false);
1254
+ }
1255
+ /**
1256
+ * Поднять сессии процесс CLI — первый раз или взамен оборвавшегося.
1257
+ *
1258
+ * Всё, что отличает переподключение от старта, собрано в `restarted`:
1259
+ * транскрипт при перезапуске не перечитывается (разговор уже в журнале),
1260
+ * продолжаем всегда свою собственную сессию, а настройки, выбранные человеком
1261
+ * на лету, отправляются в новый процесс заново — из файлов он их не возьмёт.
1262
+ */
1263
+ #spawn(restarted) {
1099
1264
  const options2 = {
1100
1265
  cwd: this.cwd,
1101
1266
  // Без этого SDK не читает с диска ничего: ни CLAUDE.md, ни кастомные
@@ -1138,9 +1303,10 @@ var ClaudeSession = class extends EventEmitter {
1138
1303
  }
1139
1304
  if (this.#model)
1140
1305
  options2.model = this.#model;
1141
- if (this.#options.resume)
1142
- options2.resume = this.#options.resume;
1143
- if (this.#options.fork)
1306
+ const resume = this.#claudeSessionId ?? this.#options.resume;
1307
+ if (resume)
1308
+ options2.resume = resume;
1309
+ if (this.#options.fork && !this.#claudeSessionId)
1144
1310
  options2.forkSession = true;
1145
1311
  if (this.#options.outputFormat)
1146
1312
  options2.outputFormat = this.#options.outputFormat;
@@ -1149,9 +1315,60 @@ var ClaudeSession = class extends EventEmitter {
1149
1315
  options2.allowedTools = READ_ONLY_TOOLS;
1150
1316
  options2.strictMcpConfig = true;
1151
1317
  }
1152
- this.#query = query({ prompt: this.#inbox, options: options2 });
1153
- void this.#consume(this.#query);
1154
- void this.#loadCatalog();
1318
+ const q = query({ prompt: this.#inbox, options: options2 });
1319
+ this.#query = q;
1320
+ void this.#consume(q);
1321
+ if (restarted)
1322
+ void this.#resume(q);
1323
+ else
1324
+ void this.#loadCatalog();
1325
+ }
1326
+ /**
1327
+ * Довести поднятый заново процесс до рабочего состояния.
1328
+ *
1329
+ * Ждём, пока он отзовётся, возвращаем ему настройки, выбранные на лету, и
1330
+ * только потом отпускаем накопленные промпты.
1331
+ *
1332
+ * Ждать приходится явно: `system/init` в streaming-режиме приезжает вместе с
1333
+ * первым ответом, то есть уже после промпта, — и сессия, которая ждала бы
1334
+ * его, чтобы промпт отправить, ждала бы вечно. `initializationResult()`,
1335
+ * наоборот, поднимает CLI сам и не требует, чтобы его о чём-то спросили.
1336
+ */
1337
+ async #resume(q) {
1338
+ const ready = await q.initializationResult().then(() => true, () => false);
1339
+ if (q !== this.#query)
1340
+ return;
1341
+ if (!ready) {
1342
+ this.#scheduleReconnect("\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 CLI \u043D\u0435 \u043E\u0442\u043E\u0437\u0432\u0430\u043B\u0441\u044F");
1343
+ return;
1344
+ }
1345
+ try {
1346
+ await q.applyFlagSettings({
1347
+ ...this.#effort ? { effortLevel: this.#effort } : {},
1348
+ ultracode: this.#ultracode,
1349
+ alwaysThinkingEnabled: this.#thinking,
1350
+ switchModelsOnFlag: this.#switchModelsOnFlag
1351
+ });
1352
+ } catch {
1353
+ }
1354
+ if (q !== this.#query)
1355
+ return;
1356
+ const retry = this.#pendingRetry;
1357
+ this.#pendingRetry = [];
1358
+ if (retry.length === 0) {
1359
+ this.#setState("idle");
1360
+ return;
1361
+ }
1362
+ this.#record({
1363
+ kind: "error",
1364
+ message: retry.length === 1 ? "\u0421\u0435\u0441\u0441\u0438\u044F \u043F\u043E\u0434\u043D\u044F\u0442\u0430 \u0437\u0430\u043D\u043E\u0432\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E \u0437\u0430\u043F\u0440\u043E\u0441." : `\u0421\u0435\u0441\u0441\u0438\u044F \u043F\u043E\u0434\u043D\u044F\u0442\u0430 \u0437\u0430\u043D\u043E\u0432\u043E, \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u044E \u043D\u0430\u043A\u043E\u043F\u043B\u0435\u043D\u043D\u043E\u0435 (${retry.length}).`
1365
+ });
1366
+ this.#lastTurn = null;
1367
+ this.#turnDidWork = false;
1368
+ this.#lastPrompt = retry[retry.length - 1] ?? null;
1369
+ this.#setState("running");
1370
+ for (const text of retry)
1371
+ this.#push(text);
1155
1372
  }
1156
1373
  /**
1157
1374
  * Что известно о сессии до system/init.
@@ -1176,6 +1393,22 @@ var ClaudeSession = class extends EventEmitter {
1176
1393
  };
1177
1394
  }
1178
1395
  async close(reason = "closed by client") {
1396
+ this.#closing = true;
1397
+ if (this.#reconnectTimer) {
1398
+ clearTimeout(this.#reconnectTimer);
1399
+ this.#reconnectTimer = null;
1400
+ }
1401
+ this.#teardown(reason);
1402
+ this.#setState("closed");
1403
+ }
1404
+ /**
1405
+ * Отпустить текущий процесс CLI и всё, что на нём висело.
1406
+ *
1407
+ * Общее у закрытия и переподключения: и там, и там прежний процесс больше не
1408
+ * ответит, а незакрытые запросы прав, если их не разрешить, повиснут навсегда
1409
+ * — SDK ждёт промис, которому уже некому прийти.
1410
+ */
1411
+ #teardown(reason) {
1179
1412
  this.#inbox.close();
1180
1413
  for (const [requestId, pending] of this.#pendingPermissions) {
1181
1414
  pending.resolve({ behavior: "deny", message: reason });
@@ -1185,29 +1418,119 @@ var ClaudeSession = class extends EventEmitter {
1185
1418
  this.#query?.close();
1186
1419
  } catch {
1187
1420
  }
1188
- this.#setState("closed");
1421
+ this.#query = null;
1422
+ }
1423
+ // -------------------------------------------------------------------------
1424
+ // Переподключение
1425
+ // -------------------------------------------------------------------------
1426
+ /**
1427
+ * Поднять сессии новый процесс CLI, сохранив разговор.
1428
+ *
1429
+ * Зачем вообще: у CLI, которому запрос к API вернул 403 (упал VPN — сеть
1430
+ * отвечает вместо API), состояние аутентификации портится на всю жизнь
1431
+ * процесса. Разговор при этом цел, ключи целы, VPN через минуту вернулся — а
1432
+ * сессия так и отвечает «Failed to authenticate» на каждый промпт, пока её не
1433
+ * закроют и не откроют заново. Собственно, единственным лекарством и был этот
1434
+ * ручной круг: закрыть, открыть, продолжить.
1435
+ *
1436
+ * Здесь тот же круг делается сам. Новый процесс продолжает (`resume`) ту же
1437
+ * сессию Claude Code, поэтому разговор, файлы и контекст остаются на месте —
1438
+ * человек видит паузу, а не потерю.
1439
+ */
1440
+ reconnect(reason) {
1441
+ this.#reconnectAttempt = 0;
1442
+ this.#restart(reason);
1443
+ }
1444
+ /** Собственно смена процесса — без разговора о том, чья это была идея. */
1445
+ #restart(reason) {
1446
+ if (this.#closing || this.#state === "closed")
1447
+ return;
1448
+ if (this.#reconnectTimer) {
1449
+ clearTimeout(this.#reconnectTimer);
1450
+ this.#reconnectTimer = null;
1451
+ }
1452
+ this.#setState("reconnecting");
1453
+ this.#teardown("\u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435");
1454
+ this.#inbox = new AsyncQueue();
1455
+ this.#streamMessageId = null;
1456
+ this.#blockCounts.clear();
1457
+ this.#record({ kind: "error", message: `\u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u044E\u0441\u044C: ${reason}` });
1458
+ this.#spawn(true);
1459
+ }
1460
+ /**
1461
+ * Попробовать переподключиться ещё раз — с паузой и с оглядкой на счётчик.
1462
+ *
1463
+ * Процесс отпускаем сразу, а поднимаем новый через паузу: между этими двумя
1464
+ * моментами сессия должна быть заведомо без процесса, иначе промпт, посланный
1465
+ * в эту щель, уехал бы в очередь, которую никто не читает, и завис бы там
1466
+ * навсегда — `sendPrompt` отличает эти состояния именно по `#query`.
1467
+ *
1468
+ * Попытки конечны намеренно. Обрыв VPN лечится сам за секунды, а вот
1469
+ * отозванный ключ или снесённый CLI не вылечится никогда, и бесконечный
1470
+ * круг перезапусков в этом случае только жёг бы процессы, показывая человеку
1471
+ * ровно ту же ошибку. Кончились попытки — говорим об этом прямо и ждём
1472
+ * человека: и промпт, и кнопка «переподключить» начинают отсчёт заново.
1473
+ */
1474
+ #scheduleReconnect(reason) {
1475
+ if (this.#closing || this.#reconnectTimer)
1476
+ return;
1477
+ this.#teardown("\u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435");
1478
+ const delay = RECONNECT_DELAYS_MS[this.#reconnectAttempt];
1479
+ if (delay === void 0) {
1480
+ this.#pendingRetry = [];
1481
+ this.#record({
1482
+ kind: "error",
1483
+ message: `\u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${reason}. \u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 \u0441\u0435\u0442\u044C \u0438 VPN, \u0430 \u043F\u043E\u0442\u043E\u043C \u043E\u0442\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0440\u043E\u043C\u043F\u0442 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438\u0442\u0435 \xAB\u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\xBB \u2014 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D, \u0441\u0435\u0441\u0441\u0438\u044F \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442 \u0441 \u044D\u0442\u043E\u0433\u043E \u043C\u0435\u0441\u0442\u0430.`
1484
+ });
1485
+ this.#setState("error");
1486
+ return;
1487
+ }
1488
+ this.#reconnectAttempt += 1;
1489
+ this.#record({
1490
+ kind: "error",
1491
+ message: `\u0421\u0432\u044F\u0437\u044C \u0441 Claude \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430: ${reason}. \u041F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 ${Math.round(delay / 1e3)} \u0441 (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${this.#reconnectAttempt} \u0438\u0437 ${RECONNECT_DELAYS_MS.length}).`
1492
+ });
1493
+ this.#setState("reconnecting");
1494
+ this.#reconnectTimer = setTimeout(() => {
1495
+ this.#reconnectTimer = null;
1496
+ this.#restart(reason);
1497
+ }, delay);
1498
+ this.#reconnectTimer.unref?.();
1189
1499
  }
1190
1500
  // -------------------------------------------------------------------------
1191
1501
  // Ввод от пользователя
1192
1502
  // -------------------------------------------------------------------------
1193
1503
  sendPrompt(text) {
1194
- if (this.#inbox.closed)
1504
+ if (this.#closing)
1195
1505
  throw new Error("session is closed");
1196
1506
  this.#record({ kind: "user.message", text });
1197
1507
  if (!this.#title || this.#titleIsDefault) {
1198
1508
  this.#title = text.slice(0, 80);
1199
1509
  this.#titleIsDefault = false;
1200
1510
  }
1201
- if (this.#state === "error") {
1202
- this.#record({
1203
- kind: "error",
1204
- message: "\u0421\u0435\u0441\u0441\u0438\u044F \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430, \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u043F\u0440\u043E\u043C\u043F\u0442 \u043D\u0435\u043A\u0443\u0434\u0430 \u2014 \u0441\u043C. \u043E\u0448\u0438\u0431\u043A\u0443 \u0432\u044B\u0448\u0435."
1205
- });
1206
- return;
1207
- }
1208
1511
  this.#lastTurn = null;
1209
1512
  this.#interruptRequested = false;
1513
+ this.#lastPrompt = text;
1514
+ this.#turnDidWork = false;
1515
+ if (!this.#query) {
1516
+ this.#pendingRetry.push(text);
1517
+ this.reconnect("\u0441\u0435\u0441\u0441\u0438\u0438 \u043D\u0443\u0436\u0435\u043D \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0434\u043B\u044F \u043D\u043E\u0432\u043E\u0433\u043E \u043F\u0440\u043E\u043C\u043F\u0442\u0430");
1518
+ return;
1519
+ }
1210
1520
  this.#setState("running");
1521
+ this.#push(text);
1522
+ }
1523
+ /**
1524
+ * Стоит ли вообще переподключаться из-за этого кода ответа.
1525
+ *
1526
+ * Отсутствие кода — тоже повод: ответа не было вовсе (сеть оборвалась на
1527
+ * полуслове), а это ровно тот случай, ради которого всё и затевалось.
1528
+ */
1529
+ #retryable(status) {
1530
+ return status === void 0 || !PERMANENT_API_STATUS.has(status);
1531
+ }
1532
+ /** Промпт в очередь SDK — без записи в журнал: при повторе она уже там. */
1533
+ #push(text) {
1211
1534
  this.#inbox.push({
1212
1535
  type: "user",
1213
1536
  message: { role: "user", content: [{ type: "text", text }] },
@@ -1576,13 +1899,27 @@ var ClaudeSession = class extends EventEmitter {
1576
1899
  async #consume(q) {
1577
1900
  try {
1578
1901
  for await (const message of q) {
1902
+ if (q !== this.#query)
1903
+ return;
1579
1904
  this.#handleMessage(message);
1580
1905
  }
1581
- this.#setState("closed");
1906
+ if (q !== this.#query)
1907
+ return;
1908
+ if (this.#closing)
1909
+ this.#setState("closed");
1910
+ else
1911
+ this.#scheduleReconnect("\u043F\u0440\u043E\u0446\u0435\u0441\u0441 CLI \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F");
1582
1912
  } catch (error) {
1913
+ if (q !== this.#query || this.#closing)
1914
+ return;
1583
1915
  const message = error instanceof Error ? error.message : String(error);
1584
- this.#record({ kind: "error", message: message + this.#executableHint() });
1585
- this.#setState("error");
1916
+ if (!this.#capabilities) {
1917
+ this.#record({ kind: "error", message: message + this.#executableHint() });
1918
+ this.#setState("error");
1919
+ return;
1920
+ }
1921
+ this.#record({ kind: "error", message });
1922
+ this.#scheduleReconnect(message);
1586
1923
  }
1587
1924
  }
1588
1925
  /**
@@ -1614,7 +1951,10 @@ var ClaudeSession = class extends EventEmitter {
1614
1951
  const usage = message.message.usage;
1615
1952
  this.#contextTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
1616
1953
  }
1954
+ const synthetic2 = isApiErrorMessage(message);
1617
1955
  for (const block of message.message.content) {
1956
+ if (!synthetic2)
1957
+ this.#turnDidWork = true;
1618
1958
  const blockId = `${message.message.id}:${this.#nextBlockIndex(message.message.id)}`;
1619
1959
  if (block.type === "text") {
1620
1960
  this.#record({ kind: "assistant.text", text: block.text, blockId });
@@ -1666,8 +2006,21 @@ var ClaudeSession = class extends EventEmitter {
1666
2006
  text: "result" in message ? message.result : null
1667
2007
  });
1668
2008
  }
1669
- this.#lastTurn = turnOutcome("terminal_reason" in message ? message.terminal_reason : void 0, message.subtype, this.#interruptRequested);
2009
+ const terminalReason = "terminal_reason" in message ? message.terminal_reason : void 0;
2010
+ this.#lastTurn = turnOutcome(terminalReason, message.subtype, this.#interruptRequested);
2011
+ const interrupted = this.#interruptRequested;
1670
2012
  this.#interruptRequested = false;
2013
+ if (terminalReason === API_ERROR_REASON && !interrupted) {
2014
+ const status = "api_error_status" in message ? message.api_error_status : void 0;
2015
+ if (this.#retryable(status ?? void 0)) {
2016
+ if (!this.#turnDidWork && this.#lastPrompt !== null) {
2017
+ this.#pendingRetry.push(this.#lastPrompt);
2018
+ }
2019
+ this.#scheduleReconnect(apiErrorReason(status ?? void 0));
2020
+ break;
2021
+ }
2022
+ }
2023
+ this.#reconnectAttempt = 0;
1671
2024
  const alreadyIdle = this.#state === "idle";
1672
2025
  this.#setState("idle");
1673
2026
  if (alreadyIdle)
@@ -1838,6 +2191,12 @@ var ClaudeSession = class extends EventEmitter {
1838
2191
  function describe(error) {
1839
2192
  return error instanceof Error ? error.message : String(error);
1840
2193
  }
2194
+ function isApiErrorMessage(message) {
2195
+ return message.is_api_error_message === true;
2196
+ }
2197
+ function apiErrorReason(status) {
2198
+ return status === void 0 ? "\u0437\u0430\u043F\u0440\u043E\u0441 \u043A API \u043D\u0435 \u0434\u043E\u0448\u0451\u043B" : `API \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${status}`;
2199
+ }
1841
2200
  function toSlashCommand(command) {
1842
2201
  return {
1843
2202
  name: command.name,
@@ -4324,6 +4683,9 @@ var HostServer = class {
4324
4683
  case "session.close":
4325
4684
  await this.#dropSession(frame.sessionId, "closed by client");
4326
4685
  return;
4686
+ case "session.reconnect":
4687
+ this.#requireSession(frame.sessionId).reconnect("\u043F\u043E \u043F\u0440\u043E\u0441\u044C\u0431\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F");
4688
+ return;
4327
4689
  case "session.archive": {
4328
4690
  const ids = new Set(frame.sessionIds);
4329
4691
  for (const session of [...this.#sessions.values()]) {
@@ -4597,7 +4959,7 @@ var HostServer = class {
4597
4959
  const files = this.#files(frame.workspaceId);
4598
4960
  let saved;
4599
4961
  try {
4600
- saved = await files.write(frame.path, frame.content, frame.eol, frame.baseMtime, frame.force === true);
4962
+ saved = await files.write(frame.path, frame.content, frame.eol, frame.encoding ?? "utf8", frame.baseMtime, frame.force === true);
4601
4963
  } catch (error) {
4602
4964
  this.#fsFailed(frame.workspaceId, frame.path, error);
4603
4965
  return;
@@ -4643,6 +5005,24 @@ var HostServer = class {
4643
5005
  }
4644
5006
  return;
4645
5007
  }
5008
+ case "fs.copy": {
5009
+ let created;
5010
+ try {
5011
+ created = await this.#files(frame.workspaceId).copy(frame.path, frame.toDir);
5012
+ } catch (error) {
5013
+ this.#fsFailed(frame.workspaceId, frame.path, error);
5014
+ return;
5015
+ }
5016
+ this.#broadcast({
5017
+ type: "fs.result",
5018
+ workspaceId: frame.workspaceId,
5019
+ ok: true,
5020
+ message: `\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u043E \u0432 ${created}`,
5021
+ path: created
5022
+ });
5023
+ await this.#sendDir(frame.workspaceId, parentOf(created));
5024
+ return;
5025
+ }
4646
5026
  // Терминал. Каждая ручка начинается с одной и той же проверки права: их
4647
5027
  // пять, и «забыл в одной» здесь означает не потерянную кнопку, а чужую
4648
5028
  // оболочку в руках у того, кому её не давали.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i14ks/ccv",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "license": "UNLICENSED",
5
5
  "description": "GUI для Claude Code: сессии агента в каталогах проектов, дерево файлов и редактор рядом с разговором.",
6
6
  "homepage": "https://claudevisual.com",
@@ -1 +1 @@
1
- import{s as r,H as a}from"./index-EaPgYsZb.js";import{E as e}from"./index-891wE-Rj.js";import{t as o}from"./index-VtR2fk7J.js";import"./index-BHot-lnj.js";const m=e.theme({"&":{height:"100%",color:"var(--text)",backgroundColor:"var(--bg)",fontSize:"13px"},".cm-scroller":{fontFamily:"var(--mono)",lineHeight:"1.6"},".cm-content":{caretColor:"var(--accent)"},".cm-cursor, .cm-dropCursor":{borderLeftColor:"var(--accent)"},"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":{backgroundColor:"var(--cm-selection)"},".cm-gutters":{backgroundColor:"var(--bg)",color:"var(--muted)",borderRight:"1px solid var(--border-soft)"},".cm-activeLineGutter":{backgroundColor:"var(--surface-2)",color:"var(--text-2)"},".cm-activeLine":{backgroundColor:"var(--cm-active)"},".cm-selectionMatch":{backgroundColor:"var(--cm-match)"},"&.cm-focused .cm-matchingBracket":{backgroundColor:"var(--cm-match)",outline:"1px solid var(--accent-line)"},".cm-foldPlaceholder":{backgroundColor:"var(--surface-2)",border:"1px solid var(--border)",color:"var(--muted)"},".cm-panels":{backgroundColor:"var(--surface)",color:"var(--text)",border:"1px solid var(--border)"},".cm-panels input, .cm-panels button, .cm-panels select":{fontFamily:"var(--sans)",fontSize:"12px",background:"var(--surface-2)",color:"var(--text)",border:"1px solid var(--border)",borderRadius:"6px",padding:"3px 6px"},".cm-searchMatch":{backgroundColor:"var(--cm-match)"},".cm-searchMatch.cm-searchMatch-selected":{backgroundColor:"var(--accent-soft)"},".cm-tooltip":{background:"var(--surface)",border:"1px solid var(--border)",borderRadius:"8px",boxShadow:"var(--shadow-md)",color:"var(--text)"},".cm-tooltip-autocomplete ul li[aria-selected]":{background:"var(--surface-2)",color:"var(--text)"}}),i=r(a.define([{tag:[o.keyword,o.modifier,o.controlKeyword,o.moduleKeyword],color:"var(--cm-key)"},{tag:[o.name,o.deleted,o.character,o.macroName],color:"var(--text)"},{tag:[o.propertyName],color:"var(--cm-prop)"},{tag:[o.function(o.variableName),o.labelName],color:"var(--cm-fn)"},{tag:[o.color,o.constant(o.name),o.standard(o.name),o.bool],color:"var(--cm-const)"},{tag:[o.definition(o.name),o.separator],color:"var(--text)"},{tag:[o.typeName,o.className,o.changed,o.annotation,o.self,o.namespace],color:"var(--cm-type)"},{tag:[o.number,o.integer,o.float],color:"var(--cm-num)"},{tag:[o.string,o.special(o.string),o.regexp],color:"var(--cm-str)"},{tag:[o.operator,o.operatorKeyword,o.escape],color:"var(--cm-op)"},{tag:[o.meta,o.comment],color:"var(--cm-comment)",fontStyle:"italic"},{tag:o.strong,fontWeight:"650"},{tag:o.emphasis,fontStyle:"italic"},{tag:o.strikethrough,textDecoration:"line-through"},{tag:o.link,color:"var(--cm-str)",textDecoration:"underline"},{tag:o.heading,fontWeight:"650",color:"var(--cm-key)"},{tag:o.invalid,color:"var(--danger)"}]));export{i as editorHighlight,m as editorTheme};
1
+ import{s as r,H as a}from"./index-CP6Bv1lU.js";import{E as e}from"./index-891wE-Rj.js";import{t as o}from"./index-5NnNKmcv.js";import"./index-BHot-lnj.js";const m=e.theme({"&":{height:"100%",color:"var(--text)",backgroundColor:"var(--bg)",fontSize:"13px"},".cm-scroller":{fontFamily:"var(--mono)",lineHeight:"1.6"},".cm-content":{caretColor:"var(--accent)"},".cm-cursor, .cm-dropCursor":{borderLeftColor:"var(--accent)"},"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":{backgroundColor:"var(--cm-selection)"},".cm-gutters":{backgroundColor:"var(--bg)",color:"var(--muted)",borderRight:"1px solid var(--border-soft)"},".cm-activeLineGutter":{backgroundColor:"var(--surface-2)",color:"var(--text-2)"},".cm-activeLine":{backgroundColor:"var(--cm-active)"},".cm-selectionMatch":{backgroundColor:"var(--cm-match)"},"&.cm-focused .cm-matchingBracket":{backgroundColor:"var(--cm-match)",outline:"1px solid var(--accent-line)"},".cm-foldPlaceholder":{backgroundColor:"var(--surface-2)",border:"1px solid var(--border)",color:"var(--muted)"},".cm-panels":{backgroundColor:"var(--surface)",color:"var(--text)",border:"1px solid var(--border)"},".cm-panels input, .cm-panels button, .cm-panels select":{fontFamily:"var(--sans)",fontSize:"12px",background:"var(--surface-2)",color:"var(--text)",border:"1px solid var(--border)",borderRadius:"6px",padding:"3px 6px"},".cm-searchMatch":{backgroundColor:"var(--cm-match)"},".cm-searchMatch.cm-searchMatch-selected":{backgroundColor:"var(--accent-soft)"},".cm-tooltip":{background:"var(--surface)",border:"1px solid var(--border)",borderRadius:"8px",boxShadow:"var(--shadow-md)",color:"var(--text)"},".cm-tooltip-autocomplete ul li[aria-selected]":{background:"var(--surface-2)",color:"var(--text)"}}),i=r(a.define([{tag:[o.keyword,o.modifier,o.controlKeyword,o.moduleKeyword],color:"var(--cm-key)"},{tag:[o.name,o.deleted,o.character,o.macroName],color:"var(--text)"},{tag:[o.propertyName],color:"var(--cm-prop)"},{tag:[o.function(o.variableName),o.labelName],color:"var(--cm-fn)"},{tag:[o.color,o.constant(o.name),o.standard(o.name),o.bool],color:"var(--cm-const)"},{tag:[o.definition(o.name),o.separator],color:"var(--text)"},{tag:[o.typeName,o.className,o.changed,o.annotation,o.self,o.namespace],color:"var(--cm-type)"},{tag:[o.number,o.integer,o.float],color:"var(--cm-num)"},{tag:[o.string,o.special(o.string),o.regexp],color:"var(--cm-str)"},{tag:[o.operator,o.operatorKeyword,o.escape],color:"var(--cm-op)"},{tag:[o.meta,o.comment],color:"var(--cm-comment)",fontStyle:"italic"},{tag:o.strong,fontWeight:"650"},{tag:o.emphasis,fontStyle:"italic"},{tag:o.strikethrough,textDecoration:"line-through"},{tag:o.link,color:"var(--cm-str)",textDecoration:"underline"},{tag:o.heading,fontWeight:"650",color:"var(--cm-key)"},{tag:o.invalid,color:"var(--danger)"}]));export{i as editorHighlight,m as editorTheme};