@i14ks/ccv 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/ccv.mjs +431 -62
- package/package.json +50 -50
- package/web/assets/{codemirror-wcb7BToF.js → codemirror-XwUmM20V.js} +1 -1
- package/web/assets/{index-CLUF7ja0.js → index-B3KPQ0Jq.js} +1 -1
- package/web/assets/{index-BebgK8qS.js → index-B3X6gKS4.js} +1 -1
- package/web/assets/{index-DaeBIEG7.js → index-B3ZG7Mgh.js} +1 -1
- package/web/assets/{index-B8ioctog.js → index-B3mW0ScK.js} +1 -1
- package/web/assets/{index-qJpQucwj.js → index-BAMLmkAm.js} +1 -1
- package/web/assets/{index-CKo4-duU.js → index-BBdH0WyS.js} +1 -1
- package/web/assets/{index-DzuxHBqN.js → index-BCeVhzZf.js} +1 -1
- package/web/assets/{index-ZyET8YfL.js → index-BiGxRIQL.js} +1 -1
- package/web/assets/{index-B7uOCyzk.js → index-Bjmeb0mL.js} +1 -1
- package/web/assets/{index-D8i-20tT.js → index-C3lLfrzk.js} +1 -1
- package/web/assets/index-CPM-bl8z.css +1 -0
- package/web/assets/{index-jRANpi-W.js → index-CWJg4zA3.js} +1 -1
- package/web/assets/{index-ggJz6TAQ.js → index-CXp-N-a3.js} +1 -1
- package/web/assets/{index-tW3KsNuy.js → index-CdFloc-n.js} +1 -1
- package/web/assets/{index-s31Rk-Wi.js → index-Ceqv0JQi.js} +1 -1
- package/web/assets/{index-BgYZP6B5.js → index-Cu0rli8K.js} +1 -1
- package/web/assets/{index-BoWGjyq1.js → index-DaN46jfw.js} +1 -1
- package/web/assets/{index-Bug5lJuD.js → index-EhM5ESQH.js} +1 -1
- package/web/assets/{index-H1KO3hWw.js → index-PcpAtAyR.js} +1 -1
- package/web/assets/index-_rsPujXt.js +134 -0
- package/web/assets/{index-B1iGktMS.js → index-byQstcIo.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-BKeYZb3W.css +0 -1
- package/web/assets/index-DfzFdzpd.js +0 -134
package/bundle/ccv.mjs
CHANGED
|
@@ -1112,6 +1112,8 @@ var USAGE_MIN_INTERVAL_MS = 2e4;
|
|
|
1112
1112
|
var HANDOFF_TOOL_DENIAL = "\u041B\u0438\u043C\u0438\u0442 \u043F\u043B\u0430\u043D\u0430 \u043F\u043E\u0447\u0442\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D, \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u044B. \u041E\u0442\u0432\u0435\u0442\u044C \u0445\u044D\u043D\u0434\u043E\u0444\u0444\u043E\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C, \u0431\u0435\u0437 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432.";
|
|
1113
1113
|
var USAGE_TURN_INTERVAL_MS = 3e4;
|
|
1114
1114
|
var INTERRUPT_SETTLE_MS = 15e3;
|
|
1115
|
+
var TASK_STOP_TIMEOUT_MS = 5e3;
|
|
1116
|
+
var TASK_WAKE_MS = 5e3;
|
|
1115
1117
|
var CONTEXT_MIN_INTERVAL_MS = 5e3;
|
|
1116
1118
|
var ClaudeSession = class extends EventEmitter {
|
|
1117
1119
|
id;
|
|
@@ -1176,6 +1178,39 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1176
1178
|
* `#setState` делает это сам, см. там же.
|
|
1177
1179
|
*/
|
|
1178
1180
|
#backgroundWork = /* @__PURE__ */ new Set();
|
|
1181
|
+
/**
|
|
1182
|
+
* Запущенные задачи основного разговора — по `system/task_*` (см.
|
|
1183
|
+
* `SessionTask`), в порядке запуска.
|
|
1184
|
+
*
|
|
1185
|
+
* Края (`task_started` / `task_notification`) могут потеряться, поэтому
|
|
1186
|
+
* список ещё и подрезается: по `background_tasks_changed` уходят фоновые,
|
|
1187
|
+
* которых нет в уровне, по `result` основного хода — задачи переднего
|
|
1188
|
+
* плана, а перезапуск процесса CLI обнуляет его целиком.
|
|
1189
|
+
*/
|
|
1190
|
+
#tasks = /* @__PURE__ */ new Map();
|
|
1191
|
+
/**
|
|
1192
|
+
* Живые фоновые задачи по `background_tasks_changed` — уровень, а не края:
|
|
1193
|
+
* каждое событие заменяет набор целиком. В отличие от `#backgroundWork`,
|
|
1194
|
+
* новый ход его не трогает — фоновый агент переживает любой ход.
|
|
1195
|
+
*
|
|
1196
|
+
* `null` — этот процесс CLI уровня ещё не присылал (или не умеет вовсе);
|
|
1197
|
+
* тогда о фоновой работе судит `#backgroundWork`.
|
|
1198
|
+
*/
|
|
1199
|
+
#backgroundLevel = null;
|
|
1200
|
+
/**
|
|
1201
|
+
* toolUseId вызовов, сделанных сабагентами. `owned_by_subagent` CLI ставит
|
|
1202
|
+
* не всем их задачам — команда агента на переднем плане приходит без него, —
|
|
1203
|
+
* поэтому вложенность узнаём ещё и по тому, чей вызов задачу запустил.
|
|
1204
|
+
*/
|
|
1205
|
+
#subagentToolUses = /* @__PURE__ */ new Set();
|
|
1206
|
+
/** Отложенная рассылка сводки по `task_progress` — он приходит на каждый инструмент агента. */
|
|
1207
|
+
#taskSummaryTimer = null;
|
|
1208
|
+
/**
|
|
1209
|
+
* Когда сессия в последний раз работала — шла ход или крутились задачи.
|
|
1210
|
+
* Сторожу лимита: порог мог перейти между опросами, и сессия, которая
|
|
1211
|
+
* только что его перешла, к моменту проверки уже может простаивать.
|
|
1212
|
+
*/
|
|
1213
|
+
#lastActiveAt = 0;
|
|
1179
1214
|
#capabilities = null;
|
|
1180
1215
|
#pendingPermissions = /* @__PURE__ */ new Map();
|
|
1181
1216
|
#model = null;
|
|
@@ -1342,7 +1377,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1342
1377
|
title: this.#title,
|
|
1343
1378
|
state: this.#state,
|
|
1344
1379
|
lastTurn: this.#lastTurn,
|
|
1345
|
-
backgroundWork: this.#backgroundWork.size > 0,
|
|
1380
|
+
backgroundWork: this.#backgroundLevel ? this.#backgroundLevel.size > 0 : this.#backgroundWork.size > 0,
|
|
1381
|
+
// Команда на переднем плане — это просто идущий вызов инструмента, он и
|
|
1382
|
+
// так виден в ленте; задачей она становится, только уйдя в фон.
|
|
1383
|
+
tasks: [...this.#tasks.values()].filter((t) => t.kind !== "shell" || t.background),
|
|
1346
1384
|
model: this.#model,
|
|
1347
1385
|
permissionMode: this.#permissionMode,
|
|
1348
1386
|
effort: this.#effort,
|
|
@@ -1445,6 +1483,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1445
1483
|
options2.allowedTools = READ_ONLY_TOOLS;
|
|
1446
1484
|
options2.strictMcpConfig = true;
|
|
1447
1485
|
}
|
|
1486
|
+
this.#resetTasks();
|
|
1448
1487
|
const q = query({ prompt: this.#inbox, options: options2 });
|
|
1449
1488
|
this.#query = q;
|
|
1450
1489
|
void this.#consume(q);
|
|
@@ -1565,6 +1604,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1565
1604
|
} catch {
|
|
1566
1605
|
}
|
|
1567
1606
|
this.#query = null;
|
|
1607
|
+
this.#resetTasks();
|
|
1568
1608
|
}
|
|
1569
1609
|
// -------------------------------------------------------------------------
|
|
1570
1610
|
// Переподключение
|
|
@@ -1621,8 +1661,8 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1621
1661
|
if (this.#closing || this.#reconnectTimer)
|
|
1622
1662
|
return;
|
|
1623
1663
|
this.#teardown("\u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435");
|
|
1624
|
-
const
|
|
1625
|
-
if (
|
|
1664
|
+
const delay2 = RECONNECT_DELAYS_MS[this.#reconnectAttempt];
|
|
1665
|
+
if (delay2 === void 0) {
|
|
1626
1666
|
this.#pendingRetry = [];
|
|
1627
1667
|
this.#record({
|
|
1628
1668
|
kind: "error",
|
|
@@ -1634,13 +1674,13 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1634
1674
|
this.#reconnectAttempt += 1;
|
|
1635
1675
|
this.#record({
|
|
1636
1676
|
kind: "error",
|
|
1637
|
-
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(
|
|
1677
|
+
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(delay2 / 1e3)} \u0441 (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${this.#reconnectAttempt} \u0438\u0437 ${RECONNECT_DELAYS_MS.length}).`
|
|
1638
1678
|
});
|
|
1639
1679
|
this.#setState("reconnecting");
|
|
1640
1680
|
this.#reconnectTimer = setTimeout(() => {
|
|
1641
1681
|
this.#reconnectTimer = null;
|
|
1642
1682
|
this.#restart(reason);
|
|
1643
|
-
},
|
|
1683
|
+
}, delay2);
|
|
1644
1684
|
this.#reconnectTimer.unref?.();
|
|
1645
1685
|
}
|
|
1646
1686
|
// -------------------------------------------------------------------------
|
|
@@ -1782,27 +1822,61 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1782
1822
|
this.#interruptRequested = true;
|
|
1783
1823
|
await this.#query?.interrupt();
|
|
1784
1824
|
}
|
|
1825
|
+
/** Когда сессия в последний раз работала (см. `#lastActiveAt`); идущий ход — сейчас. */
|
|
1826
|
+
get lastActiveAt() {
|
|
1827
|
+
return BUSY_STATES.has(this.#state) || this.#tasks.size > 0 ? Date.now() : this.#lastActiveAt;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Остановить одну задачу. Из списка она уйдёт по уведомлению CLI
|
|
1831
|
+
* (`task_notification` со `stopped`), а не здесь: не остановилась — пусть
|
|
1832
|
+
* и видно будет, что работает.
|
|
1833
|
+
*/
|
|
1834
|
+
async stopTask(taskId) {
|
|
1835
|
+
await this.#query?.stopTask(taskId);
|
|
1836
|
+
}
|
|
1785
1837
|
/**
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1789
|
-
|
|
1838
|
+
* Остановить все задачи сессии — и из списка, и из уровня фоновых (там же
|
|
1839
|
+
* и вложенные). `interrupt()` фоновых агентов не трогает: они переживают
|
|
1840
|
+
* ход. `true` — было что останавливать.
|
|
1841
|
+
*/
|
|
1842
|
+
async stopTasks() {
|
|
1843
|
+
const q = this.#query;
|
|
1844
|
+
const ids = /* @__PURE__ */ new Set([...this.#tasks.keys(), ...this.#backgroundLevel ?? []]);
|
|
1845
|
+
if (!q || ids.size === 0)
|
|
1846
|
+
return false;
|
|
1847
|
+
await Promise.all([...ids].map((id) => Promise.race([q.stopTask(id), delay(TASK_STOP_TIMEOUT_MS)]).catch(() => void 0)));
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Ход хэндоффа: остановить задачи сессии, прервать текущий ход (вместе с
|
|
1852
|
+
* субагентами переднего плана), затем отправить служебный `prompt` с
|
|
1853
|
+
* запретом на любые инструменты и вернуть текст ответа. `null` — хода не
|
|
1854
|
+
* вышло: ошибка, пустой ответ, ход не кончился за `timeoutMs` или `prompt`
|
|
1855
|
+
* не задан (спрашивать агента бессмысленно — например, окно уже исчерпано);
|
|
1856
|
+
* тогда хэндофф собирают из журнала.
|
|
1790
1857
|
*/
|
|
1791
1858
|
async runHandoff(prompt, timeoutMs) {
|
|
1792
1859
|
if (this.#closing)
|
|
1793
1860
|
return null;
|
|
1794
1861
|
this.#handoffMode = true;
|
|
1795
1862
|
try {
|
|
1863
|
+
if (await this.stopTasks())
|
|
1864
|
+
await this.#waitForState("running", TASK_WAKE_MS);
|
|
1796
1865
|
if (this.#state === "running" || this.#state === "waiting_permission") {
|
|
1797
1866
|
const settled = this.#nextTurnEnd(INTERRUPT_SETTLE_MS);
|
|
1798
1867
|
await this.interrupt().catch(() => void 0);
|
|
1799
1868
|
await settled;
|
|
1800
1869
|
}
|
|
1801
|
-
if (this.#closing || !this.#query || this.#state !== "idle")
|
|
1870
|
+
if (!prompt || this.#closing || !this.#query || this.#state !== "idle")
|
|
1802
1871
|
return null;
|
|
1803
1872
|
const reply = this.#nextTurnEnd(timeoutMs);
|
|
1804
1873
|
this.sendPrompt(prompt, void 0, true);
|
|
1805
1874
|
const turn = await reply;
|
|
1875
|
+
if (!turn && !this.#closing && this.#state !== "idle") {
|
|
1876
|
+
const settled = this.#nextTurnEnd(INTERRUPT_SETTLE_MS);
|
|
1877
|
+
await this.interrupt().catch(() => void 0);
|
|
1878
|
+
await settled;
|
|
1879
|
+
}
|
|
1806
1880
|
const text = turn?.text?.trim();
|
|
1807
1881
|
return turn && !turn.isError && text ? text : null;
|
|
1808
1882
|
} catch {
|
|
@@ -1811,6 +1885,25 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1811
1885
|
this.#handoffMode = false;
|
|
1812
1886
|
}
|
|
1813
1887
|
}
|
|
1888
|
+
/** Дождаться состояния `state` не дольше `timeoutMs`; `true` — дождались. */
|
|
1889
|
+
#waitForState(state, timeoutMs) {
|
|
1890
|
+
if (this.#state === state)
|
|
1891
|
+
return Promise.resolve(true);
|
|
1892
|
+
return new Promise((resolve) => {
|
|
1893
|
+
const onSummary = (summary) => {
|
|
1894
|
+
if (summary.state !== state)
|
|
1895
|
+
return;
|
|
1896
|
+
clearTimeout(timer);
|
|
1897
|
+
this.off("summary", onSummary);
|
|
1898
|
+
resolve(true);
|
|
1899
|
+
};
|
|
1900
|
+
const timer = setTimeout(() => {
|
|
1901
|
+
this.off("summary", onSummary);
|
|
1902
|
+
resolve(false);
|
|
1903
|
+
}, timeoutMs);
|
|
1904
|
+
this.on("summary", onSummary);
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1814
1907
|
/** Ближайший конец хода (`result`) или `null`, если не дождались. */
|
|
1815
1908
|
#nextTurnEnd(timeoutMs) {
|
|
1816
1909
|
return new Promise((resolve) => {
|
|
@@ -2203,7 +2296,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2203
2296
|
\u0421\u0435\u0441\u0441\u0438\u044E \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u043B Claude Code, ${whence}: ${this.#executable.path}. \u0415\u0441\u043B\u0438 \u0434\u0435\u043B\u043E \u0432 \u043D\u0451\u043C, CCV_CLAUDE_PATH \u0443\u043A\u0430\u0436\u0435\u0442 \u0434\u0440\u0443\u0433\u043E\u0439, \u0430 CCV_BUNDLED_CLAUDE=1 \u0432\u0435\u0440\u043D\u0451\u0442 ccv \u043D\u0430 \u0431\u0438\u043D\u0430\u0440\u043D\u0438\u043A \u0438\u0437 Agent SDK.`;
|
|
2204
2297
|
}
|
|
2205
2298
|
#handleMessage(message) {
|
|
2206
|
-
if (this.#state === "idle" && (message.type === "assistant" || message.type === "stream_event")) {
|
|
2299
|
+
if (this.#state === "idle" && (message.type === "assistant" || message.type === "stream_event") && message.parent_tool_use_id === null) {
|
|
2207
2300
|
this.#setState("running");
|
|
2208
2301
|
}
|
|
2209
2302
|
switch (message.type) {
|
|
@@ -2237,6 +2330,14 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2237
2330
|
});
|
|
2238
2331
|
if (isBackgroundLaunch(block.input))
|
|
2239
2332
|
this.#backgroundWork.add(block.id);
|
|
2333
|
+
if (message.parent_tool_use_id !== null) {
|
|
2334
|
+
this.#subagentToolUses.add(block.id);
|
|
2335
|
+
if (this.#subagentToolUses.size > 256) {
|
|
2336
|
+
const oldest = this.#subagentToolUses.values().next().value;
|
|
2337
|
+
if (oldest !== void 0)
|
|
2338
|
+
this.#subagentToolUses.delete(oldest);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2240
2341
|
}
|
|
2241
2342
|
}
|
|
2242
2343
|
break;
|
|
@@ -2293,6 +2394,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2293
2394
|
}
|
|
2294
2395
|
}
|
|
2295
2396
|
this.#reconnectAttempt = 0;
|
|
2397
|
+
for (const [id, task] of this.#tasks) {
|
|
2398
|
+
if (!task.background && !this.#backgroundLevel?.has(id))
|
|
2399
|
+
this.#tasks.delete(id);
|
|
2400
|
+
}
|
|
2296
2401
|
const alreadyIdle = this.#state === "idle";
|
|
2297
2402
|
this.#setState("idle");
|
|
2298
2403
|
if (alreadyIdle)
|
|
@@ -2346,9 +2451,120 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2346
2451
|
void this.refreshContext(true);
|
|
2347
2452
|
return;
|
|
2348
2453
|
}
|
|
2454
|
+
if (this.#handleTask(message))
|
|
2455
|
+
return;
|
|
2349
2456
|
const { type: _type, subtype, ...rest } = message;
|
|
2350
2457
|
this.#record({ kind: "system", subtype, data: rest });
|
|
2351
2458
|
}
|
|
2459
|
+
/**
|
|
2460
|
+
* События задач CLI → `#tasks` и `#backgroundLevel`. `true` — событие
|
|
2461
|
+
* разобрано и в журнал не идёт; `task_started` и `task_notification`
|
|
2462
|
+
* (редкие края) журнал по-прежнему получает.
|
|
2463
|
+
*
|
|
2464
|
+
* Типы SDK тут не помогают: подтипов в союзе `SDKMessage` больше, чем
|
|
2465
|
+
* объявлено, и поля вроде `owned_by_subagent` в них не описаны — читаем
|
|
2466
|
+
* как есть и проверяем форму сами.
|
|
2467
|
+
*/
|
|
2468
|
+
#handleTask(message) {
|
|
2469
|
+
const subtype = message.subtype;
|
|
2470
|
+
const taskId = typeof message.task_id === "string" ? message.task_id : null;
|
|
2471
|
+
switch (subtype) {
|
|
2472
|
+
case "task_started": {
|
|
2473
|
+
this.#lastActiveAt = Date.now();
|
|
2474
|
+
const toolUseId = str(message.tool_use_id);
|
|
2475
|
+
if (!taskId || message.owned_by_subagent === true || toolUseId !== null && this.#subagentToolUses.has(toolUseId)) {
|
|
2476
|
+
return false;
|
|
2477
|
+
}
|
|
2478
|
+
this.#tasks.set(taskId, {
|
|
2479
|
+
id: taskId,
|
|
2480
|
+
toolUseId,
|
|
2481
|
+
kind: taskKind(str(message.task_type) ?? (message.subagent_type ? "local_agent" : null)),
|
|
2482
|
+
agentType: str(message.subagent_type),
|
|
2483
|
+
description: str(message.description) ?? "",
|
|
2484
|
+
activity: null,
|
|
2485
|
+
lastTool: null,
|
|
2486
|
+
background: message.is_backgrounded === true || this.#backgroundLevel?.has(taskId) === true,
|
|
2487
|
+
startedAt: Date.now(),
|
|
2488
|
+
tokens: 0,
|
|
2489
|
+
toolUses: 0
|
|
2490
|
+
});
|
|
2491
|
+
this.emit("summary", this.summary());
|
|
2492
|
+
return false;
|
|
2493
|
+
}
|
|
2494
|
+
case "task_progress": {
|
|
2495
|
+
this.#lastActiveAt = Date.now();
|
|
2496
|
+
if (Date.now() - this.#usageAt >= USAGE_TURN_INTERVAL_MS)
|
|
2497
|
+
void this.refreshUsage();
|
|
2498
|
+
const task = taskId ? this.#tasks.get(taskId) : void 0;
|
|
2499
|
+
if (!task)
|
|
2500
|
+
return true;
|
|
2501
|
+
const usage = message.usage ?? {};
|
|
2502
|
+
task.activity = str(message.summary) ?? str(message.description) ?? task.activity;
|
|
2503
|
+
task.lastTool = str(message.last_tool_name) ?? task.lastTool;
|
|
2504
|
+
if (typeof usage.total_tokens === "number")
|
|
2505
|
+
task.tokens = usage.total_tokens;
|
|
2506
|
+
if (typeof usage.tool_uses === "number")
|
|
2507
|
+
task.toolUses = usage.tool_uses;
|
|
2508
|
+
this.#emitTasksSoon();
|
|
2509
|
+
return true;
|
|
2510
|
+
}
|
|
2511
|
+
case "task_updated": {
|
|
2512
|
+
const task = taskId ? this.#tasks.get(taskId) : void 0;
|
|
2513
|
+
if (!task)
|
|
2514
|
+
return true;
|
|
2515
|
+
const patch = message.patch ?? {};
|
|
2516
|
+
const status = patch.status;
|
|
2517
|
+
if (status === "completed" || status === "failed" || status === "killed") {
|
|
2518
|
+
this.#tasks.delete(task.id);
|
|
2519
|
+
} else {
|
|
2520
|
+
if (typeof patch.is_backgrounded === "boolean")
|
|
2521
|
+
task.background = patch.is_backgrounded;
|
|
2522
|
+
if (typeof patch.description === "string")
|
|
2523
|
+
task.description = patch.description;
|
|
2524
|
+
}
|
|
2525
|
+
this.emit("summary", this.summary());
|
|
2526
|
+
return true;
|
|
2527
|
+
}
|
|
2528
|
+
case "task_notification":
|
|
2529
|
+
this.#lastActiveAt = Date.now();
|
|
2530
|
+
if (taskId && this.#tasks.delete(taskId))
|
|
2531
|
+
this.emit("summary", this.summary());
|
|
2532
|
+
return false;
|
|
2533
|
+
case "background_tasks_changed": {
|
|
2534
|
+
this.#lastActiveAt = Date.now();
|
|
2535
|
+
const live = Array.isArray(message.tasks) ? message.tasks.flatMap((t) => typeof t.task_id === "string" ? [t.task_id] : []) : [];
|
|
2536
|
+
this.#backgroundLevel = new Set(live);
|
|
2537
|
+
for (const [id, task] of this.#tasks) {
|
|
2538
|
+
if (this.#backgroundLevel.has(id))
|
|
2539
|
+
task.background = true;
|
|
2540
|
+
else if (task.background)
|
|
2541
|
+
this.#tasks.delete(id);
|
|
2542
|
+
}
|
|
2543
|
+
this.emit("summary", this.summary());
|
|
2544
|
+
return true;
|
|
2545
|
+
}
|
|
2546
|
+
default:
|
|
2547
|
+
return false;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/** Сводка по прогрессу задач — не чаще раза в секунду. */
|
|
2551
|
+
#emitTasksSoon() {
|
|
2552
|
+
if (this.#taskSummaryTimer)
|
|
2553
|
+
return;
|
|
2554
|
+
this.#taskSummaryTimer = setTimeout(() => {
|
|
2555
|
+
this.#taskSummaryTimer = null;
|
|
2556
|
+
this.emit("summary", this.summary());
|
|
2557
|
+
}, 1e3);
|
|
2558
|
+
}
|
|
2559
|
+
#resetTasks() {
|
|
2560
|
+
this.#tasks.clear();
|
|
2561
|
+
this.#backgroundLevel = null;
|
|
2562
|
+
this.#backgroundWork.clear();
|
|
2563
|
+
this.#subagentToolUses.clear();
|
|
2564
|
+
if (this.#taskSummaryTimer)
|
|
2565
|
+
clearTimeout(this.#taskSummaryTimer);
|
|
2566
|
+
this.#taskSummaryTimer = null;
|
|
2567
|
+
}
|
|
2352
2568
|
/**
|
|
2353
2569
|
* Слэш-команды и модели — то, из чего UI строит меню «/» и выбор модели.
|
|
2354
2570
|
*
|
|
@@ -2459,6 +2675,8 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2459
2675
|
#setState(state) {
|
|
2460
2676
|
if (this.#state === state)
|
|
2461
2677
|
return;
|
|
2678
|
+
if (BUSY_STATES.has(this.#state) || BUSY_STATES.has(state))
|
|
2679
|
+
this.#lastActiveAt = Date.now();
|
|
2462
2680
|
this.#state = state;
|
|
2463
2681
|
if (state === "running")
|
|
2464
2682
|
this.#backgroundWork.clear();
|
|
@@ -2466,6 +2684,26 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2466
2684
|
this.emit("summary", this.summary());
|
|
2467
2685
|
}
|
|
2468
2686
|
};
|
|
2687
|
+
var BUSY_STATES = /* @__PURE__ */ new Set(["running", "waiting_permission"]);
|
|
2688
|
+
function delay(ms) {
|
|
2689
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2690
|
+
}
|
|
2691
|
+
function str(value) {
|
|
2692
|
+
return typeof value === "string" && value ? value : null;
|
|
2693
|
+
}
|
|
2694
|
+
function taskKind(type) {
|
|
2695
|
+
switch (type) {
|
|
2696
|
+
case "local_agent":
|
|
2697
|
+
case "remote_agent":
|
|
2698
|
+
return "agent";
|
|
2699
|
+
case "local_bash":
|
|
2700
|
+
return "shell";
|
|
2701
|
+
case "local_workflow":
|
|
2702
|
+
return "workflow";
|
|
2703
|
+
default:
|
|
2704
|
+
return "other";
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2469
2707
|
function describe(error) {
|
|
2470
2708
|
return error instanceof Error ? error.message : String(error);
|
|
2471
2709
|
}
|
|
@@ -2845,6 +3083,7 @@ var AntigravitySession = class extends EventEmitter2 {
|
|
|
2845
3083
|
state: this.#state,
|
|
2846
3084
|
lastTurn: this.#lastTurn,
|
|
2847
3085
|
backgroundWork: false,
|
|
3086
|
+
tasks: [],
|
|
2848
3087
|
model: this.#record.model,
|
|
2849
3088
|
permissionMode: this.#record.permissionMode,
|
|
2850
3089
|
effort: this.#record.effort,
|
|
@@ -4392,20 +4631,20 @@ var GitHubAccount = class {
|
|
|
4392
4631
|
auto_init: false
|
|
4393
4632
|
}
|
|
4394
4633
|
});
|
|
4395
|
-
const cloneUrl =
|
|
4634
|
+
const cloneUrl = str2(created.clone_url);
|
|
4396
4635
|
if (!cloneUrl)
|
|
4397
4636
|
throw new Error("GitHub \u0441\u043E\u0437\u0434\u0430\u043B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439, \u043D\u043E \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u0430\u0434\u0440\u0435\u0441 \u0434\u043B\u044F git");
|
|
4398
4637
|
return {
|
|
4399
4638
|
cloneUrl,
|
|
4400
|
-
htmlUrl:
|
|
4401
|
-
fullName:
|
|
4639
|
+
htmlUrl: str2(created.html_url) || cloneUrl.replace(/\.git$/, ""),
|
|
4640
|
+
fullName: str2(created.full_name) || `${options2.owner}/${options2.name}`
|
|
4402
4641
|
};
|
|
4403
4642
|
}
|
|
4404
4643
|
// ---------------------------------------------------------------- внутреннее
|
|
4405
4644
|
/** Логин владельца токена; заодно это и есть проверка токена на живость. */
|
|
4406
4645
|
async #login(token) {
|
|
4407
4646
|
const user = await this.#api(token, "/user");
|
|
4408
|
-
const login =
|
|
4647
|
+
const login = str2(user.login);
|
|
4409
4648
|
if (!login)
|
|
4410
4649
|
throw new Error("GitHub \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B \u0443\u0447\u0451\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u2014 \u0442\u043E\u043A\u0435\u043D \u043D\u0435 \u043F\u043E\u0434\u043E\u0448\u0451\u043B");
|
|
4411
4650
|
return login;
|
|
@@ -4466,9 +4705,9 @@ function apiError(status, body) {
|
|
|
4466
4705
|
let details = "";
|
|
4467
4706
|
try {
|
|
4468
4707
|
const parsed = JSON.parse(body);
|
|
4469
|
-
message =
|
|
4708
|
+
message = str2(parsed.message);
|
|
4470
4709
|
if (Array.isArray(parsed.errors)) {
|
|
4471
|
-
details = parsed.errors.map((item) =>
|
|
4710
|
+
details = parsed.errors.map((item) => str2(item.message)).filter(Boolean).join("; ");
|
|
4472
4711
|
}
|
|
4473
4712
|
} catch {
|
|
4474
4713
|
message = body.slice(0, 200).trim();
|
|
@@ -4487,7 +4726,7 @@ function apiError(status, body) {
|
|
|
4487
4726
|
return `GitHub \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${status}${text ? `: ${text}` : ""}`;
|
|
4488
4727
|
}
|
|
4489
4728
|
}
|
|
4490
|
-
function
|
|
4729
|
+
function str2(value) {
|
|
4491
4730
|
return typeof value === "string" ? value : "";
|
|
4492
4731
|
}
|
|
4493
4732
|
function describe4(error) {
|
|
@@ -5170,6 +5409,28 @@ var SessionPins = class {
|
|
|
5170
5409
|
await this.#persist();
|
|
5171
5410
|
return true;
|
|
5172
5411
|
}
|
|
5412
|
+
/**
|
|
5413
|
+
* Передать закрепление другой сессии — «Продолжить в новой сессии» из
|
|
5414
|
+
* хэндоффа: работа переехала в новую, и закреплённой должна быть она.
|
|
5415
|
+
*
|
|
5416
|
+
* `fromIds` — все id прежней сессии (дисковый и, если она ещё жива,
|
|
5417
|
+
* локальный uuid): закрепление пишется под оба, и снять его надо с обоих,
|
|
5418
|
+
* иначе прежняя так и останется в панели. Время берётся у первой найденной
|
|
5419
|
+
* записи — новая встаёт ровно на место прежней, а не в конец панели.
|
|
5420
|
+
* Возвращает `true`, если прежняя была закреплена.
|
|
5421
|
+
*/
|
|
5422
|
+
async transfer(fromIds, toId) {
|
|
5423
|
+
let record;
|
|
5424
|
+
for (const id of fromIds) {
|
|
5425
|
+
record ??= this.#pinned.get(id);
|
|
5426
|
+
this.#pinned.delete(id);
|
|
5427
|
+
}
|
|
5428
|
+
if (!record)
|
|
5429
|
+
return false;
|
|
5430
|
+
this.#pinned.set(toId, record);
|
|
5431
|
+
await this.#persist();
|
|
5432
|
+
return true;
|
|
5433
|
+
}
|
|
5173
5434
|
/**
|
|
5174
5435
|
* Забыть записи о сессиях, которых больше нет на диске.
|
|
5175
5436
|
*
|
|
@@ -5451,6 +5712,21 @@ var Folders = class {
|
|
|
5451
5712
|
await this.#persist();
|
|
5452
5713
|
return true;
|
|
5453
5714
|
}
|
|
5715
|
+
/**
|
|
5716
|
+
* Перенести место в папке закреплённых с одного id сессии на другой — вслед
|
|
5717
|
+
* за самим закреплением (см. `SessionPins.migrate` и `SessionPins.transfer`).
|
|
5718
|
+
* Без этого строка, переехав, выпала бы из своей папки в общий список.
|
|
5719
|
+
*/
|
|
5720
|
+
async migratePinnedSession(fromId, toId) {
|
|
5721
|
+
const folderId = this.#pinnedAssignments.get(fromId);
|
|
5722
|
+
if (!folderId || fromId === toId)
|
|
5723
|
+
return false;
|
|
5724
|
+
this.#pinnedAssignments.delete(fromId);
|
|
5725
|
+
if (!this.#pinnedAssignments.has(toId))
|
|
5726
|
+
this.#pinnedAssignments.set(toId, folderId);
|
|
5727
|
+
await this.#persist();
|
|
5728
|
+
return true;
|
|
5729
|
+
}
|
|
5454
5730
|
folderIdForWorkspace(workspaceId) {
|
|
5455
5731
|
return this.#workspaceAssignments.get(workspaceId);
|
|
5456
5732
|
}
|
|
@@ -5704,6 +5980,7 @@ var LimitStopState = class {
|
|
|
5704
5980
|
// ../host/dist/limit-guard.js
|
|
5705
5981
|
var HANDOFF_TIMEOUT_MS = 3 * 6e4;
|
|
5706
5982
|
var LIMIT_SWEEP_MS = 6e4;
|
|
5983
|
+
var LIMIT_RECENT_MS = 2 * LIMIT_SWEEP_MS;
|
|
5707
5984
|
var WINDOW_SPAN_MS = {
|
|
5708
5985
|
fiveHour: 5 * 60 * 6e4,
|
|
5709
5986
|
sevenDay: 7 * 24 * 60 * 6e4
|
|
@@ -5732,7 +6009,7 @@ function crossedLimit(usage, settings, fired, now = Date.now()) {
|
|
|
5732
6009
|
}
|
|
5733
6010
|
function handoffPrompt(hit) {
|
|
5734
6011
|
return [
|
|
5735
|
-
`[ccv] ${capitalize(LIMIT_LABEL[hit.limit])} \u043F\u043E\u0447\u0442\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${Math.round(hit.window.utilization)} %), \u0440\u0430\u0431\u043E\u0442\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430
|
|
6012
|
+
hit ? `[ccv] ${capitalize(LIMIT_LABEL[hit.limit])} \u043F\u043E\u0447\u0442\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${Math.round(hit.window.utilization)} %), \u0440\u0430\u0431\u043E\u0442\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430.` : "[ccv] \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u044D\u0442\u043E\u0442 \u044D\u0442\u0430\u043F: \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E\u0439\u0434\u0451\u0442 \u0432 \u043D\u043E\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438.",
|
|
5736
6013
|
"\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u0432 \u044D\u0442\u043E\u043C \u0445\u043E\u0434\u0435 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u044B \u2014 \u043D\u0435 \u0432\u044B\u0437\u044B\u0432\u0430\u0439 \u0438\u0445.",
|
|
5737
6014
|
"\u041E\u0442\u0432\u0435\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435\u043C \u2014 \u0445\u044D\u043D\u0434\u043E\u0444\u0444\u043E\u043C, \u0441 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0430\u0431\u043E\u0442\u0443 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442 \u0432 \u043D\u043E\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438.",
|
|
5738
6015
|
"Markdown, \u0440\u0430\u0437\u0434\u0435\u043B\u044B: \xAB## \u0426\u0435\u043B\u044C\xBB, \xAB## \u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435\xBB (\u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u043D\u043E, \u0447\u0442\u043E \u043D\u0435\u0442, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u043E),",
|
|
@@ -6127,13 +6404,13 @@ function parseStatus2(raw) {
|
|
|
6127
6404
|
}
|
|
6128
6405
|
return {
|
|
6129
6406
|
loggedIn: parsed.loggedIn === true,
|
|
6130
|
-
method:
|
|
6131
|
-
email:
|
|
6132
|
-
organization:
|
|
6133
|
-
plan:
|
|
6407
|
+
method: str3(parsed.authMethod) || "none",
|
|
6408
|
+
email: str3(parsed.email),
|
|
6409
|
+
organization: str3(parsed.orgName),
|
|
6410
|
+
plan: str3(parsed.subscriptionType)
|
|
6134
6411
|
};
|
|
6135
6412
|
}
|
|
6136
|
-
function
|
|
6413
|
+
function str3(value) {
|
|
6137
6414
|
return typeof value === "string" ? value : "";
|
|
6138
6415
|
}
|
|
6139
6416
|
function describe5(error) {
|
|
@@ -6157,8 +6434,8 @@ var RETRY_MAX_MS = 6e4;
|
|
|
6157
6434
|
var HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
6158
6435
|
var CONNECT_TIMEOUT_MS = 3e4;
|
|
6159
6436
|
var SILENCE_TIMEOUT_MS = 95e3;
|
|
6160
|
-
function jitter(
|
|
6161
|
-
return Math.round(
|
|
6437
|
+
function jitter(delay2) {
|
|
6438
|
+
return Math.round(delay2 * (0.8 + Math.random() * 0.4));
|
|
6162
6439
|
}
|
|
6163
6440
|
var RelayLink = class extends EventEmitter4 {
|
|
6164
6441
|
#credentials = null;
|
|
@@ -6451,12 +6728,12 @@ var RelayLink = class extends EventEmitter4 {
|
|
|
6451
6728
|
#scheduleRetry(rejected) {
|
|
6452
6729
|
if (this.#closed || !this.#credentials || this.#retryTimer)
|
|
6453
6730
|
return;
|
|
6454
|
-
const
|
|
6731
|
+
const delay2 = rejected ? RETRY_MAX_MS : this.#retry;
|
|
6455
6732
|
this.#retry = Math.min(this.#retry * 2, RETRY_MAX_MS);
|
|
6456
6733
|
this.#retryTimer = setTimeout(() => {
|
|
6457
6734
|
this.#retryTimer = null;
|
|
6458
6735
|
this.#connect();
|
|
6459
|
-
}, jitter(
|
|
6736
|
+
}, jitter(delay2));
|
|
6460
6737
|
this.#retryTimer.unref();
|
|
6461
6738
|
}
|
|
6462
6739
|
#onMessage(socket, raw) {
|
|
@@ -6856,7 +7133,7 @@ var FINDER_TITLE = "\xB7 \u043F\u043E\u0438\u0441\u043A \u043F\u0440\u043E\u0435
|
|
|
6856
7133
|
var HISTORY_PAGE = 250;
|
|
6857
7134
|
var HISTORY_PAGE_MAX = 500;
|
|
6858
7135
|
var AUTO_STOP_SWEEP_MS = 30 * 1e3;
|
|
6859
|
-
var
|
|
7136
|
+
var BUSY_STATES2 = /* @__PURE__ */ new Set([
|
|
6860
7137
|
"running",
|
|
6861
7138
|
"waiting_permission",
|
|
6862
7139
|
"reconnecting"
|
|
@@ -6906,6 +7183,8 @@ var HostServer = class {
|
|
|
6906
7183
|
#limitTimer = null;
|
|
6907
7184
|
/** Идёт остановка у лимита — второй снимок лимитов её не запускает. */
|
|
6908
7185
|
#limitStopping = false;
|
|
7186
|
+
/** Живые id сессий, у которых сейчас пишется ручной хэндофф. */
|
|
7187
|
+
#handoffBusy = /* @__PURE__ */ new Set();
|
|
6909
7188
|
#hostId = randomUUID5();
|
|
6910
7189
|
#plugins = new PluginCli();
|
|
6911
7190
|
#auth = new AuthCli();
|
|
@@ -7735,6 +8014,19 @@ var HostServer = class {
|
|
|
7735
8014
|
await this.#handoffs.remove(frame.sessionId);
|
|
7736
8015
|
this.#broadcast({ type: "handoff.state", sessionId: frame.sessionId, handoff: null });
|
|
7737
8016
|
return;
|
|
8017
|
+
case "task.stop": {
|
|
8018
|
+
const session = this.#sessions.get(frame.sessionId);
|
|
8019
|
+
if (session instanceof ClaudeSession)
|
|
8020
|
+
await session.stopTask(frame.taskId);
|
|
8021
|
+
return;
|
|
8022
|
+
}
|
|
8023
|
+
case "session.handoff":
|
|
8024
|
+
this.#send(socket, {
|
|
8025
|
+
type: "handoff.written",
|
|
8026
|
+
sessionId: frame.sessionId,
|
|
8027
|
+
handoff: await this.#manualHandoff(frame.sessionId)
|
|
8028
|
+
});
|
|
8029
|
+
return;
|
|
7738
8030
|
case "settings.update": {
|
|
7739
8031
|
const before = this.#settings.current;
|
|
7740
8032
|
const settings = await this.#settings.update(frame.settings);
|
|
@@ -7981,10 +8273,10 @@ var HostServer = class {
|
|
|
7981
8273
|
continue;
|
|
7982
8274
|
if (drafted.has(session.id))
|
|
7983
8275
|
continue;
|
|
7984
|
-
if (
|
|
8276
|
+
if (BUSY_STATES2.has(summary.state) || summary.backgroundWork)
|
|
7985
8277
|
continue;
|
|
7986
8278
|
const side = this.#sides.get(session.id);
|
|
7987
|
-
if (side &&
|
|
8279
|
+
if (side && BUSY_STATES2.has(side.state))
|
|
7988
8280
|
continue;
|
|
7989
8281
|
const lastActivity = Math.max(summary.updatedAt, side?.summary().updatedAt ?? 0, this.#draftTouchedAt.get(session.id) ?? 0);
|
|
7990
8282
|
if (now - lastActivity < after)
|
|
@@ -8006,7 +8298,7 @@ var HostServer = class {
|
|
|
8006
8298
|
}
|
|
8007
8299
|
#isBusy(session) {
|
|
8008
8300
|
const summary = session.summary();
|
|
8009
|
-
return
|
|
8301
|
+
return BUSY_STATES2.has(summary.state) || summary.backgroundWork || summary.tasks.length > 0;
|
|
8010
8302
|
}
|
|
8011
8303
|
/**
|
|
8012
8304
|
* Перечитать лимиты, пока хоть одна сессия занята. Сама сессия делает это
|
|
@@ -8035,40 +8327,41 @@ var HostServer = class {
|
|
|
8035
8327
|
}
|
|
8036
8328
|
}
|
|
8037
8329
|
/**
|
|
8038
|
-
*
|
|
8039
|
-
*
|
|
8330
|
+
* Прервать работу у лимита: занятые сессии пишут хэндофф (или он
|
|
8331
|
+
* собирается из журнала, если агент не ответил), простаивающие не
|
|
8332
|
+
* трогаются. Процессы сессий остаются жить — гаснет сессия только тогда,
|
|
8333
|
+
* когда из её хэндоффа продолжили в новой (см. `continueFromHandoff` в
|
|
8334
|
+
* окне).
|
|
8040
8335
|
*/
|
|
8041
8336
|
async #stopAtLimit(hit) {
|
|
8042
8337
|
const sessions = this.#claudeUserSessions();
|
|
8043
8338
|
if (sessions.length === 0)
|
|
8044
8339
|
return;
|
|
8045
|
-
const
|
|
8046
|
-
const
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8340
|
+
const now = Date.now();
|
|
8341
|
+
const busy = sessions.filter((s) => this.#isBusy(s) || now - s.lastActiveAt < LIMIT_RECENT_MS);
|
|
8342
|
+
const prompt = hit.window.utilization >= 100 ? null : handoffPrompt(hit);
|
|
8343
|
+
const results = await Promise.all(busy.map(async (session) => {
|
|
8344
|
+
const text = await session.runHandoff(prompt, HANDOFF_TIMEOUT_MS);
|
|
8345
|
+
const summary = session.summary();
|
|
8346
|
+
const handoff = {
|
|
8347
|
+
sessionId: summary.claudeSessionId ?? session.id,
|
|
8348
|
+
workspaceId: session.workspaceId,
|
|
8349
|
+
title: summary.title ?? "",
|
|
8350
|
+
limit: hit.limit,
|
|
8351
|
+
utilization: hit.window.utilization,
|
|
8352
|
+
resetsAt: hit.window.resetsAt,
|
|
8353
|
+
stoppedAt: Date.now(),
|
|
8354
|
+
source: text ? "agent" : "journal",
|
|
8355
|
+
text: text ?? journalHandoff(session.journal.since(0))
|
|
8356
|
+
};
|
|
8357
|
+
try {
|
|
8358
|
+
await this.#handoffs.save(handoff);
|
|
8359
|
+
this.#broadcast({ type: "handoff.state", sessionId: handoff.sessionId, handoff });
|
|
8360
|
+
return true;
|
|
8361
|
+
} catch (error) {
|
|
8362
|
+
console.error("[limit-stop]", error instanceof Error ? error.message : String(error));
|
|
8363
|
+
return false;
|
|
8069
8364
|
}
|
|
8070
|
-
await this.#dropSession(session.id, "limit").catch(() => false);
|
|
8071
|
-
return saved;
|
|
8072
8365
|
}));
|
|
8073
8366
|
this.#broadcast({
|
|
8074
8367
|
type: "limit.stopped",
|
|
@@ -8079,6 +8372,42 @@ var HostServer = class {
|
|
|
8079
8372
|
handoffs: results.filter(Boolean).length
|
|
8080
8373
|
});
|
|
8081
8374
|
}
|
|
8375
|
+
/**
|
|
8376
|
+
* «Хэндофф и новая сессия»: тот же ход хэндоффа, что у лимита, только по
|
|
8377
|
+
* просьбе пользователя. Сессия после него останавливается — работа
|
|
8378
|
+
* переезжает в новую, которую заведёт спросившее окно (см.
|
|
8379
|
+
* `handoff.written`). `null` — писать не из чего: сессии нет, она не
|
|
8380
|
+
* Claude или уже пишет хэндофф (например, второе нажатие).
|
|
8381
|
+
*/
|
|
8382
|
+
async #manualHandoff(sessionId) {
|
|
8383
|
+
const session = this.#sessions.get(sessionId);
|
|
8384
|
+
if (!(session instanceof ClaudeSession) || session.summary().state === "closed")
|
|
8385
|
+
return null;
|
|
8386
|
+
if (this.#handoffBusy.has(sessionId))
|
|
8387
|
+
return null;
|
|
8388
|
+
this.#handoffBusy.add(sessionId);
|
|
8389
|
+
try {
|
|
8390
|
+
const text = await session.runHandoff(handoffPrompt(null), HANDOFF_TIMEOUT_MS);
|
|
8391
|
+
const summary = session.summary();
|
|
8392
|
+
const handoff = {
|
|
8393
|
+
sessionId: summary.claudeSessionId ?? session.id,
|
|
8394
|
+
workspaceId: session.workspaceId,
|
|
8395
|
+
title: summary.title ?? "",
|
|
8396
|
+
limit: null,
|
|
8397
|
+
utilization: 0,
|
|
8398
|
+
resetsAt: "",
|
|
8399
|
+
stoppedAt: Date.now(),
|
|
8400
|
+
source: text ? "agent" : "journal",
|
|
8401
|
+
text: text ?? journalHandoff(session.journal.since(0))
|
|
8402
|
+
};
|
|
8403
|
+
await this.#handoffs.save(handoff);
|
|
8404
|
+
this.#broadcast({ type: "handoff.state", sessionId: handoff.sessionId, handoff });
|
|
8405
|
+
await this.#dropSession(session.id, "handoff").catch(() => false);
|
|
8406
|
+
return handoff;
|
|
8407
|
+
} finally {
|
|
8408
|
+
this.#handoffBusy.delete(sessionId);
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8082
8411
|
/**
|
|
8083
8412
|
* Живые сессии всех проектов — вкладке запущенных.
|
|
8084
8413
|
*
|
|
@@ -8176,6 +8505,44 @@ var HostServer = class {
|
|
|
8176
8505
|
session.start();
|
|
8177
8506
|
this.#sendLive();
|
|
8178
8507
|
this.#send(socket, { type: "session.summary", session: session.summary() });
|
|
8508
|
+
if (frame.continuesFrom)
|
|
8509
|
+
await this.#carryPin(workspace.id, frame.continuesFrom, session.id);
|
|
8510
|
+
}
|
|
8511
|
+
/**
|
|
8512
|
+
* «Продолжить в новой сессии» из хэндоффа: закрепление прежней сессии
|
|
8513
|
+
* переезжает на новую — вместе с местом в панели и папкой закреплённых.
|
|
8514
|
+
* Работа продолжается уже там, и мёртвая сессия у лимита не должна
|
|
8515
|
+
* занимать её строку, а новая — теряться в общем списке.
|
|
8516
|
+
*
|
|
8517
|
+
* Новая закрепляется под своим локальным uuid: дискового id у неё ещё нет,
|
|
8518
|
+
* на него запись переедет сама, как у любой свежей закреплённой (см.
|
|
8519
|
+
* `#wire`). У прежней снимаем оба id — она может быть ещё жива, и тогда
|
|
8520
|
+
* закрепление записано и под её uuid (см. `case 'session.pin'`).
|
|
8521
|
+
*/
|
|
8522
|
+
async #carryPin(workspaceId, fromId, toId) {
|
|
8523
|
+
const fromIds = [fromId];
|
|
8524
|
+
for (const live of this.#sessions.values()) {
|
|
8525
|
+
if (live.id !== toId && live.claudeSessionId === fromId)
|
|
8526
|
+
fromIds.push(live.id);
|
|
8527
|
+
}
|
|
8528
|
+
if (!await this.#pins.transfer(fromIds, toId))
|
|
8529
|
+
return;
|
|
8530
|
+
for (const id of fromIds)
|
|
8531
|
+
await this.#folders.migratePinnedSession(id, toId);
|
|
8532
|
+
await this.#sendSessionList(workspaceId);
|
|
8533
|
+
}
|
|
8534
|
+
/**
|
|
8535
|
+
* Переезд закрепления на новый id той же сессии — вместе с папкой
|
|
8536
|
+
* закреплённых, иначе строка выпала бы из своей папки в общий список.
|
|
8537
|
+
*/
|
|
8538
|
+
async #migratePin(fromId, toId) {
|
|
8539
|
+
try {
|
|
8540
|
+
const moved = await this.#pins.migrate(fromId, toId);
|
|
8541
|
+
await this.#folders.migratePinnedSession(fromId, toId);
|
|
8542
|
+
if (moved)
|
|
8543
|
+
await this.#sendPinned();
|
|
8544
|
+
} catch {
|
|
8545
|
+
}
|
|
8179
8546
|
}
|
|
8180
8547
|
/**
|
|
8181
8548
|
* Подписка на события сессии.
|
|
@@ -8191,12 +8558,12 @@ var HostServer = class {
|
|
|
8191
8558
|
this.#broadcast({ type: "session.summary", session: summary });
|
|
8192
8559
|
if (!summary.claudeSessionId || !this.#pins.has(summary.id))
|
|
8193
8560
|
return;
|
|
8194
|
-
void this.#
|
|
8561
|
+
void this.#migratePin(summary.id, summary.claudeSessionId);
|
|
8195
8562
|
});
|
|
8196
8563
|
session.on("claudeSessionId", ({ from, to }) => {
|
|
8197
8564
|
if (!this.#pins.has(from))
|
|
8198
8565
|
return;
|
|
8199
|
-
void this.#
|
|
8566
|
+
void this.#migratePin(from, to);
|
|
8200
8567
|
});
|
|
8201
8568
|
session.on("capabilities", () => {
|
|
8202
8569
|
if (!session.capabilities)
|
|
@@ -8574,6 +8941,7 @@ var HostServer = class {
|
|
|
8574
8941
|
lastTurn: null,
|
|
8575
8942
|
// Сессия с диска не поднята — фоновой задаче взяться неоткуда.
|
|
8576
8943
|
backgroundWork: false,
|
|
8944
|
+
tasks: [],
|
|
8577
8945
|
model: null,
|
|
8578
8946
|
permissionMode: "default",
|
|
8579
8947
|
// Сессия с диска не поднята: чем она была настроена, знает только её
|
|
@@ -8685,6 +9053,7 @@ function antigravitySummary(record) {
|
|
|
8685
9053
|
state: "closed",
|
|
8686
9054
|
lastTurn: null,
|
|
8687
9055
|
backgroundWork: false,
|
|
9056
|
+
tasks: [],
|
|
8688
9057
|
model: record.model,
|
|
8689
9058
|
permissionMode: record.permissionMode,
|
|
8690
9059
|
effort: record.effort,
|