clauderipple 0.11.1 → 0.12.1
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/CHANGELOG.md +46 -0
- package/README.es.md +48 -0
- package/README.ja.md +42 -0
- package/README.ko.md +39 -0
- package/README.md +44 -0
- package/README.pt-BR.md +46 -0
- package/README.zh-CN.md +40 -0
- package/dist/app/dist/claude-desktop.js +31 -0
- package/dist/app/dist/main.js +127 -8
- package/dist/app/dist/status-model.js +12 -0
- package/dist/app/dist/update-model.js +32 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/index.js +5 -1
- package/dist/mod/hooks/i18n.ts +6 -0
- package/dist/mod/hooks/modes.ts +15 -8
- package/dist/mod/hooks/register.tsx +72 -20
- package/dist/mod/hooks/settings.js +37 -2
- package/dist/mod/hooks/settings.ts +31 -3
- package/dist/mod/hooks/ui/agents.tsx +1 -1
- package/dist/mod/hooks/ui/art.ts +8 -3
- package/dist/mod/hooks/ui/band.tsx +6 -2
- package/dist/mod/hooks/ui/flat.ts +11 -8
- package/dist/mod/hooks/ui/look.ts +3 -1
- package/dist/mod/hooks/ui/view.ts +2 -0
- package/dist/mod/hooks/workers.ts +22 -1
- package/dist/mod/tests/delegation.test.tsx +50 -0
- package/dist/mod/tests/register.test.tsx +154 -1
- package/dist/mod/tests/ui.test.tsx +6 -3
- package/dist/mod/types/index.d.ts +14 -0
- package/dist/router/src/admin.js +41 -76
- package/dist/router/src/autoupdate.js +299 -0
- package/dist/router/src/bootstrap.js +3 -2
- package/dist/router/src/cli-control.js +65 -0
- package/dist/router/src/config.js +70 -11
- package/dist/router/src/index.js +11 -1
- package/dist/router/src/picker.js +25 -1
- package/dist/router/src/presets.js +21 -13
- package/dist/router/src/providers/chatgpt/index.js +11 -0
- package/dist/router/src/providers/google/index.js +7 -2
- package/dist/router/src/providers/google/transport.js +16 -1
- package/dist/router/src/proxy.js +847 -717
- package/dist/router/src/requestlog.js +3 -2
- package/dist/router/src/version.js +1 -1
- package/dist/router/src/webfetch.js +56 -0
- package/dist/router/src/websearch.js +2 -2
- package/dist/ui/app.js +247 -42
- package/dist/ui/i18n.js +160 -18
- package/dist/ui/style.css +21 -0
- package/docs/ARCHITECTURE.md +111 -14
- package/package.json +1 -1
package/dist/app/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
17
17
|
const node_os_1 = __importDefault(require("node:os"));
|
|
18
18
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
19
|
const claude_desktop_1 = require("./claude-desktop");
|
|
20
|
+
const update_model_1 = require("./update-model");
|
|
20
21
|
const protocol_1 = require("./protocol");
|
|
21
22
|
const status_model_1 = require("./status-model");
|
|
22
23
|
const window_1 = require("./window");
|
|
@@ -100,6 +101,8 @@ const STRINGS = {
|
|
|
100
101
|
updateNow: "Update",
|
|
101
102
|
later: "Later",
|
|
102
103
|
updating: (v) => `Updating to ClaudeRipple ${v}…`,
|
|
104
|
+
autoUpdated: (v) => `Updated to ClaudeRipple ${v}`,
|
|
105
|
+
autoAvailable: (v) => `New version ${v} — click to update`,
|
|
103
106
|
updateDone: (v) => `Updated to ClaudeRipple ${v}`,
|
|
104
107
|
updateFailed: "The update did not finish",
|
|
105
108
|
updateCheckFailed: "Could not check for updates",
|
|
@@ -182,6 +185,8 @@ const STRINGS = {
|
|
|
182
185
|
updateNow: "업데이트",
|
|
183
186
|
later: "나중에",
|
|
184
187
|
updating: (v) => `ClaudeRipple ${v} 업데이트 중…`,
|
|
188
|
+
autoUpdated: (v) => `ClaudeRipple ${v}로 업데이트됨`,
|
|
189
|
+
autoAvailable: (v) => `새 버전 ${v} — 클릭해서 업데이트`,
|
|
185
190
|
updateDone: (v) => `ClaudeRipple ${v} 업데이트 완료`,
|
|
186
191
|
updateFailed: "업데이트를 완료하지 못했습니다",
|
|
187
192
|
updateCheckFailed: "업데이트 정보를 확인하지 못했습니다",
|
|
@@ -264,6 +269,8 @@ const STRINGS = {
|
|
|
264
269
|
updateNow: "更新",
|
|
265
270
|
later: "稍后",
|
|
266
271
|
updating: (v) => `正在更新到 ClaudeRipple ${v}…`,
|
|
272
|
+
autoUpdated: (v) => `已更新到 ClaudeRipple ${v}`,
|
|
273
|
+
autoAvailable: (v) => `新版本 ${v} — 点击更新`,
|
|
267
274
|
updateDone: (v) => `已更新到 ClaudeRipple ${v}`,
|
|
268
275
|
updateFailed: "更新未完成",
|
|
269
276
|
updateCheckFailed: "无法检查更新",
|
|
@@ -346,6 +353,8 @@ const STRINGS = {
|
|
|
346
353
|
updateNow: "アップデート",
|
|
347
354
|
later: "後で",
|
|
348
355
|
updating: (v) => `ClaudeRipple ${v} にアップデート中…`,
|
|
356
|
+
autoUpdated: (v) => `ClaudeRipple ${v} にアップデートしました`,
|
|
357
|
+
autoAvailable: (v) => `新バージョン ${v} — クリックして更新`,
|
|
349
358
|
updateDone: (v) => `ClaudeRipple ${v} にアップデートしました`,
|
|
350
359
|
updateFailed: "アップデートが完了しませんでした",
|
|
351
360
|
updateCheckFailed: "アップデートを確認できませんでした",
|
|
@@ -428,6 +437,8 @@ const STRINGS = {
|
|
|
428
437
|
updateNow: "Actualizar",
|
|
429
438
|
later: "Más tarde",
|
|
430
439
|
updating: (v) => `Actualizando a ClaudeRipple ${v}…`,
|
|
440
|
+
autoUpdated: (v) => `Actualizado a ClaudeRipple ${v}`,
|
|
441
|
+
autoAvailable: (v) => `Nueva versión ${v} — haz clic para actualizar`,
|
|
431
442
|
updateDone: (v) => `Actualizado a ClaudeRipple ${v}`,
|
|
432
443
|
updateFailed: "La actualización no se completó",
|
|
433
444
|
updateCheckFailed: "No se pudo comprobar si hay actualizaciones",
|
|
@@ -510,6 +521,8 @@ const STRINGS = {
|
|
|
510
521
|
updateNow: "Atualizar",
|
|
511
522
|
later: "Depois",
|
|
512
523
|
updating: (v) => `Atualizando para o ClaudeRipple ${v}…`,
|
|
524
|
+
autoUpdated: (v) => `Atualizado para o ClaudeRipple ${v}`,
|
|
525
|
+
autoAvailable: (v) => `Nova versão ${v} — clique para atualizar`,
|
|
513
526
|
updateDone: (v) => `Atualizado para o ClaudeRipple ${v}`,
|
|
514
527
|
updateFailed: "A atualização não foi concluída",
|
|
515
528
|
updateCheckFailed: "Não foi possível verificar atualizações",
|
|
@@ -621,6 +634,7 @@ async function poll() {
|
|
|
621
634
|
downSince = null;
|
|
622
635
|
downNotified = false;
|
|
623
636
|
answered = true;
|
|
637
|
+
void handleRouterUpdate(last).catch((error) => console.error(`update notification: ${error}`));
|
|
624
638
|
void reconcileRouter(last);
|
|
625
639
|
}
|
|
626
640
|
catch (e) {
|
|
@@ -632,7 +646,7 @@ async function poll() {
|
|
|
632
646
|
// the outage the notification is for. Neither is the direct mode, where the router being off
|
|
633
647
|
// is the point — so doctor is asked first, and once per outage. With the window on screen the
|
|
634
648
|
// user is already looking at the same news.
|
|
635
|
-
if ((0, status_model_1.shouldNotifyDown)({ downForMs: Date.now() - downSince, graceMs: DOWN_NOTIFY_AFTER_MS, alreadyNotified: downNotified, updating, busy: !!rec.busy || (0, window_1.windowVisible)(), mode: rec.doctor?.mode ?? null })) {
|
|
649
|
+
if ((0, status_model_1.shouldNotifyDown)({ downForMs: Date.now() - downSince, graceMs: DOWN_NOTIFY_AFTER_MS, alreadyNotified: downNotified, updating: updating || Date.now() < routerUpdateUntil, busy: !!rec.busy || (0, window_1.windowVisible)(), mode: rec.doctor?.mode ?? null })) {
|
|
636
650
|
downNotified = true;
|
|
637
651
|
void refreshDoctor().then(() => {
|
|
638
652
|
if (!last && rec.doctor?.mode !== "direct")
|
|
@@ -643,6 +657,12 @@ async function poll() {
|
|
|
643
657
|
// Up or down changed: what doctor said about the rest (direct, mixed) may have changed with it.
|
|
644
658
|
if (before !== null && before !== answered && !rec.busy)
|
|
645
659
|
void refreshDoctor();
|
|
660
|
+
// Claude quit or reopened by hand has applied the change as surely as the button would.
|
|
661
|
+
if (rec.restartClaude && !rec.busy && Date.now() - restartCheckedAt >= RESTART_CHECK_MS) {
|
|
662
|
+
restartCheckedAt = Date.now();
|
|
663
|
+
if (!(0, status_model_1.restartStillOwed)(restartOwedSince, await (0, claude_desktop_1.claudeStartedAt)()))
|
|
664
|
+
rec.restartClaude = false;
|
|
665
|
+
}
|
|
646
666
|
render();
|
|
647
667
|
}
|
|
648
668
|
// Held so the handlers are not collected with it before the user gets to the notification.
|
|
@@ -856,6 +876,11 @@ const rec = {
|
|
|
856
876
|
result: null,
|
|
857
877
|
restartClaude: false,
|
|
858
878
|
};
|
|
879
|
+
/** When the connection last changed in a way Claude only picks up on its next start. */
|
|
880
|
+
let restartOwedSince = 0;
|
|
881
|
+
let restartCheckedAt = 0;
|
|
882
|
+
/** How often a pending "reopen Claude" step asks whether Claude was reopened some other way. */
|
|
883
|
+
const RESTART_CHECK_MS = 15_000;
|
|
859
884
|
let doctorRun = null;
|
|
860
885
|
/** One `doctor` at a time; callers that arrive while one runs share it. */
|
|
861
886
|
function refreshDoctor() {
|
|
@@ -985,8 +1010,11 @@ async function runAction(action, args) {
|
|
|
985
1010
|
? (0, status_model_1.repairDetails)(out, r.output)
|
|
986
1011
|
: [out?.detail ?? "", ...(ok ? [] : (0, status_model_1.tailLines)(out ? "" : r.output, 8))].filter(Boolean);
|
|
987
1012
|
rec.result = { action, ok, timedOut: r.timedOut, details, at: Date.now() };
|
|
988
|
-
if (ok && out?.restartClaude)
|
|
989
|
-
|
|
1013
|
+
if (ok && out?.restartClaude) {
|
|
1014
|
+
// A Claude that is not running picks the change up when it next starts: nothing to ask for.
|
|
1015
|
+
restartOwedSince = Date.now();
|
|
1016
|
+
rec.restartClaude = (0, status_model_1.restartStillOwed)(restartOwedSince - 1, await (0, claude_desktop_1.claudeStartedAt)());
|
|
1017
|
+
}
|
|
990
1018
|
if (out?.doctor) {
|
|
991
1019
|
rec.doctor = out.doctor;
|
|
992
1020
|
rec.doctorFailed = false;
|
|
@@ -1025,6 +1053,83 @@ async function restartClaude() {
|
|
|
1025
1053
|
let availableUpdate = null;
|
|
1026
1054
|
let updating = false;
|
|
1027
1055
|
const UPDATE_CHECK_MS = 12 * 60 * 60 * 1000;
|
|
1056
|
+
let routerUpdateUntil = 0;
|
|
1057
|
+
let updateNotification = null;
|
|
1058
|
+
let updateSeen;
|
|
1059
|
+
const updateSeenFile = node_path_1.default.join(home, "update-notifications.json");
|
|
1060
|
+
function loadUpdateSeen() {
|
|
1061
|
+
try {
|
|
1062
|
+
return (0, update_model_1.readUpdateSeen)(JSON.parse(node_fs_1.default.readFileSync(updateSeenFile, "utf8")));
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
if (error.code !== "ENOENT")
|
|
1066
|
+
console.error(`update notification state: ${error}`);
|
|
1067
|
+
return { updated: [], available: [] };
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
async function relaunchTray() {
|
|
1071
|
+
electron_1.app.releaseSingleInstanceLock();
|
|
1072
|
+
const result = await runCliResult(["tray"]);
|
|
1073
|
+
if (!result.ok) {
|
|
1074
|
+
// Keep a functioning tray if its replacement could not start.
|
|
1075
|
+
if (!electron_1.app.requestSingleInstanceLock())
|
|
1076
|
+
electron_1.app.quit();
|
|
1077
|
+
throw new Error(result.output || "could not relaunch tray");
|
|
1078
|
+
}
|
|
1079
|
+
electron_1.app.quit();
|
|
1080
|
+
}
|
|
1081
|
+
async function handleRouterUpdate(status) {
|
|
1082
|
+
if (status.autoUpdate?.running)
|
|
1083
|
+
routerUpdateUntil = Date.now() + 10 * 60_000;
|
|
1084
|
+
else
|
|
1085
|
+
routerUpdateUntil = 0;
|
|
1086
|
+
if (updating)
|
|
1087
|
+
return;
|
|
1088
|
+
updateSeen ??= loadUpdateSeen();
|
|
1089
|
+
const notice = (0, update_model_1.updateNotice)(status.version, electron_1.app.getVersion(), status.autoUpdate, updateSeen);
|
|
1090
|
+
if (!notice)
|
|
1091
|
+
return;
|
|
1092
|
+
// Memory dedup is immediate; disk dedup must succeed before a relaunch to avoid restart loops.
|
|
1093
|
+
updateSeen = (0, update_model_1.rememberUpdate)(updateSeen, notice);
|
|
1094
|
+
node_fs_1.default.mkdirSync(home, { recursive: true });
|
|
1095
|
+
const tmp = `${updateSeenFile}.${process.pid}.tmp`;
|
|
1096
|
+
try {
|
|
1097
|
+
node_fs_1.default.writeFileSync(tmp, JSON.stringify(updateSeen) + "\n", { mode: 0o600 });
|
|
1098
|
+
node_fs_1.default.renameSync(tmp, updateSeenFile);
|
|
1099
|
+
}
|
|
1100
|
+
finally {
|
|
1101
|
+
try {
|
|
1102
|
+
node_fs_1.default.unlinkSync(tmp);
|
|
1103
|
+
}
|
|
1104
|
+
catch (error) {
|
|
1105
|
+
if (error.code !== "ENOENT")
|
|
1106
|
+
throw error;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (notice.kind === "available") {
|
|
1110
|
+
const check = { current: status.version, latest: notice.version, newer: true, kind: status.autoUpdate?.kind ?? "unknown" };
|
|
1111
|
+
availableUpdate = check;
|
|
1112
|
+
if (electron_1.Notification.isSupported()) {
|
|
1113
|
+
updateNotification = new electron_1.Notification({ title: "ClaudeRipple", body: L.autoAvailable(notice.version), icon: notifyIcon() });
|
|
1114
|
+
updateNotification.on("click", () => { void offerUpdate(check).catch((error) => console.error(`offer update: ${error}`)); });
|
|
1115
|
+
updateNotification.show();
|
|
1116
|
+
}
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (electron_1.Notification.isSupported()) {
|
|
1120
|
+
updateNotification = new electron_1.Notification({ title: "ClaudeRipple", body: L.autoUpdated(notice.version), icon: notifyIcon() });
|
|
1121
|
+
updateNotification.show();
|
|
1122
|
+
}
|
|
1123
|
+
if (notice.relaunch) {
|
|
1124
|
+
updating = true;
|
|
1125
|
+
try {
|
|
1126
|
+
await relaunchTray();
|
|
1127
|
+
}
|
|
1128
|
+
finally {
|
|
1129
|
+
updating = false;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1028
1133
|
async function checkForUpdate() {
|
|
1029
1134
|
const r = await runCliResult(["update", "--check", "--json"]);
|
|
1030
1135
|
try {
|
|
@@ -1068,6 +1173,23 @@ async function offerUpdate(check) {
|
|
|
1068
1173
|
const version = check.latest ?? "";
|
|
1069
1174
|
if (electron_1.Notification.isSupported())
|
|
1070
1175
|
new electron_1.Notification({ title: "ClaudeRipple", body: L.updating(version), icon: notifyIcon() }).show();
|
|
1176
|
+
if (last?.autoUpdate) {
|
|
1177
|
+
// Share the router's coordinator with its scheduler instead of racing a second installer.
|
|
1178
|
+
try {
|
|
1179
|
+
const response = await fetch(`${adminUrl()}api/update`, { method: "POST", signal: AbortSignal.timeout(35_000) });
|
|
1180
|
+
if (!response.ok)
|
|
1181
|
+
throw new Error(await response.text());
|
|
1182
|
+
routerUpdateUntil = Date.now() + 10 * 60_000;
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
await electron_1.dialog.showMessageBox({ type: "warning", message: L.updateFailed, detail: String(error) });
|
|
1186
|
+
}
|
|
1187
|
+
finally {
|
|
1188
|
+
updating = false;
|
|
1189
|
+
render();
|
|
1190
|
+
}
|
|
1191
|
+
return; // /api/status will confirm completion, notify and relaunch this tray.
|
|
1192
|
+
}
|
|
1071
1193
|
const r = await runCliResult(["update"]);
|
|
1072
1194
|
updating = false;
|
|
1073
1195
|
if (!r.ok) {
|
|
@@ -1078,11 +1200,8 @@ async function offerUpdate(check) {
|
|
|
1078
1200
|
availableUpdate = null;
|
|
1079
1201
|
if (electron_1.Notification.isSupported())
|
|
1080
1202
|
new electron_1.Notification({ title: "ClaudeRipple", body: L.updateDone(version), icon: notifyIcon() }).show();
|
|
1081
|
-
// A new tray from the new files, then this one goes.
|
|
1082
|
-
|
|
1083
|
-
electron_1.app.releaseSingleInstanceLock();
|
|
1084
|
-
await runCliResult(["tray"]);
|
|
1085
|
-
electron_1.app.quit();
|
|
1203
|
+
// A new tray from the new files, then this one goes.
|
|
1204
|
+
await relaunchTray();
|
|
1086
1205
|
}
|
|
1087
1206
|
// Offer setup only when there is no working installation at all. A developer who runs the router
|
|
1088
1207
|
// from a source checkout keeps that; switching to the app's bundled runtime is a menu action.
|
|
@@ -7,6 +7,7 @@ exports.parseJsonOutput = parseJsonOutput;
|
|
|
7
7
|
exports.tailLines = tailLines;
|
|
8
8
|
exports.trayState = trayState;
|
|
9
9
|
exports.shouldNotifyDown = shouldNotifyDown;
|
|
10
|
+
exports.restartStillOwed = restartStillOwed;
|
|
10
11
|
exports.repairDetails = repairDetails;
|
|
11
12
|
exports.routeRequest = routeRequest;
|
|
12
13
|
/**
|
|
@@ -54,6 +55,17 @@ function trayState(health, doctor) {
|
|
|
54
55
|
function shouldNotifyDown(o) {
|
|
55
56
|
return !o.alreadyNotified && !o.updating && !o.busy && o.downForMs >= o.graceMs && o.mode !== "direct";
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether the "reopen Claude" step is still owed after a connection change at `changedAt`. Claude
|
|
60
|
+
* picks the change up on its next start, whoever restarts it: a Claude that is not running, or one
|
|
61
|
+
* that started after the change, owes nothing. Only an unknown start time keeps the step — the
|
|
62
|
+
* button used to be the only way to clear it, so reopening by hand left it on screen for good.
|
|
63
|
+
*/
|
|
64
|
+
function restartStillOwed(changedAt, claudeStartedAt) {
|
|
65
|
+
if (claudeStartedAt === undefined)
|
|
66
|
+
return true;
|
|
67
|
+
return claudeStartedAt !== null && claudeStartedAt <= changedAt;
|
|
68
|
+
}
|
|
57
69
|
/** Lines for the "Details" fold after a repair: each step as ✓/✗ with what it said. */
|
|
58
70
|
function repairDetails(out, raw) {
|
|
59
71
|
if (!out?.steps?.length)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readUpdateSeen = readUpdateSeen;
|
|
4
|
+
exports.updateNotice = updateNotice;
|
|
5
|
+
exports.rememberUpdate = rememberUpdate;
|
|
6
|
+
function readUpdateSeen(value) {
|
|
7
|
+
if (!value || typeof value !== "object")
|
|
8
|
+
throw new Error("invalid update notification state");
|
|
9
|
+
const s = value;
|
|
10
|
+
if (!Array.isArray(s.updated) || !Array.isArray(s.available)
|
|
11
|
+
|| [...s.updated, ...s.available].some((v) => typeof v !== "string"))
|
|
12
|
+
throw new Error("invalid update notification versions");
|
|
13
|
+
return { updated: s.updated, available: s.available };
|
|
14
|
+
}
|
|
15
|
+
/** No I/O or Electron: only confirmed completion is a reason to replace a running tray. */
|
|
16
|
+
function updateNotice(routerVersion, trayVersion, state, seen) {
|
|
17
|
+
if (!state)
|
|
18
|
+
return null;
|
|
19
|
+
if (!state.running && state.lastUpdatedTo === routerVersion && state.lastUpdatedFrom && state.lastUpdatedFrom !== routerVersion
|
|
20
|
+
&& typeof state.lastUpdatedAt === "number" && Number.isFinite(state.lastUpdatedAt) && !seen.updated.includes(routerVersion)) {
|
|
21
|
+
return { kind: "updated", version: routerVersion, relaunch: trayVersion !== routerVersion && (state.kind === "script" || state.kind === "npm") };
|
|
22
|
+
}
|
|
23
|
+
const version = state.available;
|
|
24
|
+
const automatic = state.enabled !== false && (state.kind === "script" || state.kind === "npm");
|
|
25
|
+
if (version && version !== routerVersion && !state.running && !automatic && !seen.available.includes(version)) {
|
|
26
|
+
return { kind: "available", version };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function rememberUpdate(seen, notice) {
|
|
31
|
+
return { ...seen, [notice.kind]: [...new Set([...seen[notice.kind], notice.version])] };
|
|
32
|
+
}
|
package/dist/app/package.json
CHANGED
package/dist/cli/src/index.js
CHANGED
|
@@ -146,9 +146,13 @@ async function image() {
|
|
|
146
146
|
...(images.length ? { images } : {}),
|
|
147
147
|
};
|
|
148
148
|
const port = adminPort(new ConfigStore(configPath()).get());
|
|
149
|
+
// Claude Code exports its session to Bash commands; the router logs it with the image so the
|
|
150
|
+
// request list shows which session spent the subscription.
|
|
151
|
+
const session = process.env.CLAUDE_CODE_SESSION_ID;
|
|
152
|
+
const headers = { "content-type": "application/json", ...(session && /^[\w-]{1,128}$/.test(session) ? { "x-claude-code-session-id": session } : {}) };
|
|
149
153
|
let res;
|
|
150
154
|
try {
|
|
151
|
-
res = await fetch(`http://127.0.0.1:${port}/api/image`, { method: "POST", headers
|
|
155
|
+
res = await fetch(`http://127.0.0.1:${port}/api/image`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
152
156
|
}
|
|
153
157
|
catch {
|
|
154
158
|
throw new Error(`the router is not answering on 127.0.0.1:${port}; start it with: clauderipple start`);
|
package/dist/mod/hooks/i18n.ts
CHANGED
|
@@ -66,6 +66,7 @@ const EN = {
|
|
|
66
66
|
'band.compact': 'Compact',
|
|
67
67
|
'band.clear': 'Clear',
|
|
68
68
|
'band.compactAlt': 'Compact — summarise the conversation to free context',
|
|
69
|
+
'band.compactedTimes': 'compacted {count}×',
|
|
69
70
|
'band.clearAlt': 'New start (/clear) — clear the conversation and start fresh',
|
|
70
71
|
'band.log': 'Log',
|
|
71
72
|
'band.agents': 'Agents',
|
|
@@ -563,6 +564,7 @@ const KO: Record<Key, string> = {
|
|
|
563
564
|
'band.compact': '압축',
|
|
564
565
|
'band.clear': '새로 시작',
|
|
565
566
|
'band.compactAlt': '대화 압축 — 대화를 요약해 컨텍스트를 줄입니다',
|
|
567
|
+
'band.compactedTimes': '{count}번 압축됨',
|
|
566
568
|
'band.clearAlt': '새로 시작 (/clear) — 대화 내용을 비우고 새로 시작합니다',
|
|
567
569
|
'band.log': '로그',
|
|
568
570
|
'band.agents': '에이전트',
|
|
@@ -1058,6 +1060,7 @@ const ZH: Record<Key, string> = {
|
|
|
1058
1060
|
'band.compact': '压缩',
|
|
1059
1061
|
'band.clear': '新开',
|
|
1060
1062
|
'band.compactAlt': '压缩对话 — 总结对话以释放上下文',
|
|
1063
|
+
'band.compactedTimes': '已压缩 {count} 次',
|
|
1061
1064
|
'band.clearAlt': '重新开始 (/clear) — 清空对话上下文,重新开始',
|
|
1062
1065
|
'band.log': '日志',
|
|
1063
1066
|
'band.agents': '代理',
|
|
@@ -1553,6 +1556,7 @@ const JA: Record<Key, string> = {
|
|
|
1553
1556
|
'band.compact': '圧縮',
|
|
1554
1557
|
'band.clear': '新しく開始',
|
|
1555
1558
|
'band.compactAlt': '圧縮 — これまでの会話を要約してコンテキストを減らします',
|
|
1559
|
+
'band.compactedTimes': '{count} 回圧縮済み',
|
|
1556
1560
|
'band.clearAlt': '新しく開始 (/clear) — 会話の文脈を空にして新しく始めます',
|
|
1557
1561
|
'band.log': 'ログ',
|
|
1558
1562
|
'band.agents': 'エージェント',
|
|
@@ -2048,6 +2052,7 @@ const ES: Record<Key, string> = {
|
|
|
2048
2052
|
'band.compact': 'Compactar',
|
|
2049
2053
|
'band.clear': 'Reiniciar',
|
|
2050
2054
|
'band.compactAlt': 'Compactar — resume la conversación para liberar contexto',
|
|
2055
|
+
'band.compactedTimes': 'compactada {count} veces',
|
|
2051
2056
|
'band.clearAlt': 'Empezar de nuevo (/clear) — vacía la conversación y empieza desde cero',
|
|
2052
2057
|
'band.log': 'Registro',
|
|
2053
2058
|
'band.agents': 'Agentes',
|
|
@@ -2543,6 +2548,7 @@ const PT: Record<Key, string> = {
|
|
|
2543
2548
|
'band.compact': 'Compactar',
|
|
2544
2549
|
'band.clear': 'Recomeçar',
|
|
2545
2550
|
'band.compactAlt': 'Compactar — resume a conversa para liberar contexto',
|
|
2551
|
+
'band.compactedTimes': 'compactada {count} vezes',
|
|
2546
2552
|
'band.clearAlt': 'Começar de novo (/clear) — limpa a conversa e começa do zero',
|
|
2547
2553
|
'band.log': 'Log',
|
|
2548
2554
|
'band.agents': 'Agentes',
|
package/dist/mod/hooks/modes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Operating modes in the session (docs/specs/2026-10-12-worker-tiers.md "Operating modes"): which
|
|
2
|
-
// mode is on, when it ends by itself, and the eager-delegation note
|
|
2
|
+
// mode is on, when it ends by itself, and the eager-delegation note (Base's while no mode is on).
|
|
3
|
+
// Pure; register.tsx wires them.
|
|
3
4
|
|
|
4
5
|
import type { Hot, LimitItem, Mode, Settings } from '../types'
|
|
5
6
|
|
|
@@ -18,21 +19,27 @@ export function modeNote(name: string): string {
|
|
|
18
19
|
return `ClaudeRipple mode ${name}: delegate everything except judgment and final review to ripple:auto.`
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/** What it reads while no mode is on and Base is eager. */
|
|
23
|
+
const BASE_NOTE = 'ClaudeRipple: delegate everything except judgment and final review to ripple:auto.'
|
|
24
|
+
|
|
21
25
|
/**
|
|
22
|
-
* The eager note for this prompt:
|
|
23
|
-
* the first after
|
|
26
|
+
* The eager note for this prompt: Base's while no mode is on, the active mode's otherwise. Sent on
|
|
27
|
+
* the first prompt after that turned eager (a mode turning on, or Base made eager), and again on
|
|
28
|
+
* the first after a compaction (which clears `noted`), never on the turns between. When the
|
|
29
|
+
* effective delegation is not eager, nothing is said and `noted` becomes null, so making it eager
|
|
30
|
+
* again later sends the note afresh.
|
|
24
31
|
*/
|
|
25
|
-
export function eagerNote(s: Pick<Settings, 'modes' | 'activeMode' | 'activeSince'>, noted: string | null): { note?: string; noted: string | null } {
|
|
32
|
+
export function eagerNote(s: Pick<Settings, 'modes' | 'activeMode' | 'activeSince' | 'delegation'>, noted: string | null): { note?: string; noted: string | null } {
|
|
26
33
|
const mode = activeModeOf(s)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return noted === key ? { noted } : { note: modeNote(mode.name), noted: key }
|
|
34
|
+
if ((mode?.delegation ?? s.delegation) !== 'eager') return { noted: null }
|
|
35
|
+
const key = mode ? modeKey(s) : 'base'
|
|
36
|
+
return noted === key ? { noted } : { note: mode ? modeNote(mode.name) : BASE_NOTE, noted: key }
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/** Modes replace the base worker set, without changing fallback, allow-list or provider policy. */
|
|
33
40
|
export function routingSettings(s: Settings): Settings {
|
|
34
41
|
const mode = activeModeOf(s)
|
|
35
|
-
return mode ? { ...s, rules: { general: { mode: 'ladder', rungs: mode.rungs } } } : s
|
|
42
|
+
return mode ? { ...s, rules: { general: { mode: 'ladder', rungs: mode.rungs } }, domainRules: mode.domainRules } : s
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/** The provider whose quota reset ends the mode: 'anthropic' is Claude, any other a ChatGPT provider. */
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Engine wiring lives here: imported logic and drawings never receive the engine interface.
|
|
2
2
|
// Timers own background work; hooks only admit/rewrite a request and record its result.
|
|
3
3
|
import { atom, read, update } from 'claude-code'
|
|
4
|
-
import type { AgentSpawnInput, EngineInterface, ModelCompleteResult, ModelForkResult, Register, RenderElement, RenderInput, SessionRateLimit, ToolCallInput, TurnStepInput, TurnUsage } from 'claude-code'
|
|
4
|
+
import type { AgentSpawnInput, EngineInterface, ModelCompleteResult, ModelForkResult, Register, RenderElement, RenderInput, SessionCompactResult, SessionRateLimit, ToolCallInput, TurnStepInput, TurnUsage } from 'claude-code'
|
|
5
5
|
import type { AgentRec, Effort, JudgeInput, JudgeKind, JudgeRun, LimitItem, ModSettingsSnapshot, Pick, Settings, StatusRequest, TierTarget, Verdict, WorkerCandidate, WorkerPick, WorkerVerdict } from '../types'
|
|
6
6
|
import { MOD_SETTINGS_SCHEMA_VERSION, RUNG_EFFORT, applySettingsChanges, normalizeSettings, settingsChanges } from './settings'
|
|
7
7
|
import { rungOf } from './ladder'
|
|
8
8
|
import { debugText, langFrom, t } from './i18n'
|
|
9
9
|
import type { Lang } from './i18n'
|
|
10
10
|
import { EFFORT_JUDGE_SYSTEM, finalEffort, isCacheSafeModel, isShortFollowUp, jevEffortRequest, jevFailure, jevUsage, jevWorkerRequest, judgeContext, parseEffortVerdict, parseJevEffort, parseJevKey, parseJevWorker, shouldJudge } from './judge'
|
|
11
|
-
import { AUTO_DESCRIPTION, WORKER_JUDGE_SYSTEM, agentProvider, candidateUsable, chatgptUsable, claudeCandidates, ladderOptionsFrom, ladderPick, parseWorkerVerdict, readMarker, routerModelOptions, spawnModel, spawnTitle, takesOver, TIER_MARKERS, tierOf, workerJudgeInput, zenJev } from './workers'
|
|
11
|
+
import { AUTO_DESCRIPTION, WORKER_JUDGE_SYSTEM, agentProvider, candidateUsable, chatgptUsable, claudeCandidates, domainVerdict, ladderOptionsFrom, ladderPick, parseWorkerVerdict, readMarker, routerModelOptions, spawnModel, spawnTitle, takesOver, TIER_MARKERS, tierOf, workerJudgeInput, workerRule, zenJev } from './workers'
|
|
12
12
|
import { MARKER, agentModel, fixSpawn } from './worker'
|
|
13
13
|
import { activeModeOf, chatgptProviders, claudeWindow, eagerNote, gptLimitsFrom, modeKey, modeOver, quotaWindows, resetProvider, routingSettings } from './modes'
|
|
14
14
|
import type { QuotaWindow } from './modes'
|
|
@@ -86,6 +86,8 @@ const setupStep = atom({ plugin: 'ripple', key: 'setupStep' } as const, 1)
|
|
|
86
86
|
const compactNote = atom({ plugin: 'ripple', key: 'compactNote' } as const, '')
|
|
87
87
|
const effortMenuOpen = atom({ plugin: 'ripple', key: 'effortMenuOpen' } as const, false)
|
|
88
88
|
const agentsOpen = atom({ plugin: 'ripple', key: 'agentsOpen' } as const, false)
|
|
89
|
+
/** Compactions of the main conversation that finished, for the Compact icon's count; /clear zeroes it. */
|
|
90
|
+
const compactions = atom({ plugin: 'ripple', key: 'compactions' } as const, 0)
|
|
89
91
|
const agentsShowDone = atom({ plugin: 'ripple', key: 'agentsShowDone' } as const, false)
|
|
90
92
|
const agentOpen = atom({ plugin: 'ripple', key: 'agentOpen' } as const, null)
|
|
91
93
|
const agentRunsOpen = atom({ plugin: 'ripple', key: 'agentRunsOpen' } as const, [])
|
|
@@ -251,6 +253,16 @@ async function installSettings($: EngineInterface, s: Settings): Promise<void> {
|
|
|
251
253
|
if (s.keepWarm && !previous.keepWarm) await update($, warm, value => ({ ...value, stopped: undefined }))
|
|
252
254
|
// Settings caught up with this chat's own choice: it follows Settings again.
|
|
253
255
|
if (await read($, warmHere) === s.keepWarm) await update($, warmHere, () => null)
|
|
256
|
+
// Setup was finished (here, in another chat, or the settings came back from the router): a setup
|
|
257
|
+
// card still open is stale, and the judge's key is looked up for the source now chosen. Both
|
|
258
|
+
// survive a reload, so after a router outage they asked for setup and a Jev key again although
|
|
259
|
+
// the router held both (2026-10-20).
|
|
260
|
+
if (s.setupDone && await read($, panel) === 'setup') {
|
|
261
|
+
await update($, panel, () => null)
|
|
262
|
+
await update($, settingsDraft, () => null)
|
|
263
|
+
await update($, settingsBase, () => null)
|
|
264
|
+
}
|
|
265
|
+
if (s.judge !== previous.judge || s.jevSource !== previous.jevSource || (s.judge === 'jev' && (await read($, jev)).source === null)) await jevKey($)
|
|
254
266
|
}
|
|
255
267
|
function settingsSnapshot(text: string): ModSettingsSnapshot {
|
|
256
268
|
const value = object(JSON.parse(text))
|
|
@@ -295,6 +307,13 @@ async function syncSettings($: EngineInterface, admin: string): Promise<void> {
|
|
|
295
307
|
await update($, settingsBase, () => null)
|
|
296
308
|
}
|
|
297
309
|
if (snapshot.stored && sync.migration) sync = await update($, settingsSync, value => ({ ...value, migration: null }))
|
|
310
|
+
// A router with no settings stored (a fresh or emptied home, or a stray test router that took the
|
|
311
|
+
// live one's launchd label, 2026-10-20) is not a reset: settings this chat holds from a finished
|
|
312
|
+
// setup are kept and written back, instead of taking defaults and asking for setup again.
|
|
313
|
+
if (!snapshot.stored && !sync.migration) {
|
|
314
|
+
const held = await read($, settings)
|
|
315
|
+
if (held !== null && normalizeSettings(held).setupDone) sync = await update($, settingsSync, value => ({ ...value, migrationChecked: true, migration: normalizeSettings(held) }))
|
|
316
|
+
}
|
|
298
317
|
if (!sync.migrationChecked) {
|
|
299
318
|
let migration: Settings | null = null
|
|
300
319
|
if (!snapshot.stored) {
|
|
@@ -703,6 +722,8 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
703
722
|
let rawVerdict: WorkerVerdict | null = null
|
|
704
723
|
let judgeError: string | undefined
|
|
705
724
|
let target: TierTarget | undefined
|
|
725
|
+
const mode = activeModeOf(s)
|
|
726
|
+
const routing = routingSettings(s)
|
|
706
727
|
if (explicit) {
|
|
707
728
|
// Agent names and their file-model ids are both valid explicit routing notes.
|
|
708
729
|
const name = files.has(explicit.name) ? explicit.name : [...files].find(([, f]) => agentModel(f)?.replace(/@[^@]*$/, '').toLowerCase() === explicit.name)?.[0]
|
|
@@ -710,12 +731,21 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
710
731
|
verdict = { domain: 'general', level: 'standard', effort: effortOf(explicit.level) ?? 'high', why: t(language, 'toasts.explicitMarker') }
|
|
711
732
|
by = 'marker'
|
|
712
733
|
} else if (tier) {
|
|
713
|
-
//
|
|
714
|
-
//
|
|
734
|
+
// A marker owns the tier. Only a set with domain ladders needs the shadow judge's domain
|
|
735
|
+
// before dispatch; otherwise leave the existing unawaited path (and latency) unchanged.
|
|
715
736
|
const named = TIER_MARKERS[tier]!
|
|
716
737
|
verdict = { domain: 'general', ...named, effort: RUNG_EFFORT[rungOf(named)], why: t(language, 'toasts.sessionTier', { tier }) }
|
|
717
738
|
by = 'session'
|
|
718
|
-
|
|
739
|
+
const shadow = shadowJudge($, s, id, { description: e.description, prompt: e.prompt })
|
|
740
|
+
if (routing.domainRules?.ui || routing.domainRules?.backend) {
|
|
741
|
+
const judged = await shadow
|
|
742
|
+
if (judged?.verdict && 'domain' in judged.verdict) {
|
|
743
|
+
rawVerdict = judged.verdict
|
|
744
|
+
verdict.domain = judged.verdict.domain
|
|
745
|
+
}
|
|
746
|
+
ms = judged?.ms ?? 0
|
|
747
|
+
judgeError = judged?.error
|
|
748
|
+
}
|
|
719
749
|
} else {
|
|
720
750
|
const judged = await judge($, s, { description: e.description, prompt: e.prompt }, true)
|
|
721
751
|
if (judged.verdict && 'domain' in judged.verdict) { rawVerdict = judged.verdict; verdict = judged.verdict; by = judged.by }
|
|
@@ -730,12 +760,15 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
730
760
|
const providers = options && options.until > now ? Object.fromEntries(Object.entries(options.providers ?? {}).filter(([, p]) => p.exhaustedUntil === undefined || p.exhaustedUntil > now)) : {}
|
|
731
761
|
const explicitCandidate = target ? pinnedCandidates.find(c => c.target.agent === target!.agent) ?? (target.agent === 'general-purpose' ? { target, model: target.model ?? 'sonnet', provider: (await read($, routerModels)).find(m => m.value.toLowerCase() === target!.model)?.provider ?? 'anthropic' } : undefined) : undefined
|
|
732
762
|
if (explicitCandidate && s.providers[explicitCandidate.provider] === false) return { deny: t(language, 'workers.disabled', { provider: explicitCandidate.provider }) }
|
|
733
|
-
// An active mode
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const
|
|
763
|
+
// An active mode replaces the whole set; an explicit marker still names its own model. Design under
|
|
764
|
+
// the ui ladder takes the heavy level even past a tier marker: the main chat tiers by difficulty,
|
|
765
|
+
// and a design task is "standard" by that measure while its result depends on the model's taste.
|
|
766
|
+
const { rule, ladder } = workerRule(verdict.domain, routing)
|
|
767
|
+
const explicitUsable = !!explicitCandidate && candidateUsable(explicitCandidate, s, usable, providers)
|
|
768
|
+
const domained: ReturnType<typeof domainVerdict> = explicitUsable ? { verdict } : domainVerdict(verdict, rule)
|
|
769
|
+
const resolved = explicitUsable
|
|
737
770
|
? { target: target!, effort: verdict.effort }
|
|
738
|
-
: ladderPick(verdict,
|
|
771
|
+
: ladderPick(domained.verdict, ladder, routing, pinnedCandidates, usable, language, routing.allow, providers, routing.allowKnown)
|
|
739
772
|
const selected = resolved.target
|
|
740
773
|
if (!selected) return { deny: t(language, 'workers.none') }
|
|
741
774
|
const fallbackReason = resolved.fallbackReason
|
|
@@ -743,10 +776,10 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
743
776
|
const model = spawnModel(selected, verdict.effort, files.has(selected.agent) ? agentModel(files.get(selected.agent)!) : undefined)
|
|
744
777
|
let prompt = e.prompt
|
|
745
778
|
if (marker) prompt = prompt.replace(MARKER, '').replace(/^\s*\n/, '')
|
|
746
|
-
const value: WorkerPick = { ...verdict, ...resolved, target: selected, by, ms, id, ...(fallbackReason ? { fallbackReason } : {}) }
|
|
779
|
+
const value: WorkerPick = { ...verdict, ...resolved, rule, target: selected, by, ms, id, ...(fallbackReason ? { fallbackReason } : {}) }
|
|
747
780
|
// The verdict behind a pick is otherwise only in the agents panel; one debug line per pick keeps
|
|
748
781
|
// it checkable: what the judge said, the rung it landed on and what ran.
|
|
749
|
-
const entry = { task: e.description.slice(0, 200), domain: value.domain, level: value.level, kind: value.kind ?? 'do', sure: value.sure, rung: value.rung, position: value.position, skipped: value.skipped, target: value.target, model: model ?? null, effort: value.effort, by, ...(mode ? { mode: mode.name } : {}), ...(fallbackReason ? { fallbackReason } : {}) }
|
|
782
|
+
const entry = { task: e.description.slice(0, 200), domain: value.domain, level: value.level, kind: value.kind ?? 'do', sure: value.sure, rule: value.rule, ...(domained.raised ? { raised: domained.raised } : {}), rung: value.rung, position: value.position, skipped: value.skipped, target: value.target, model: model ?? null, effort: value.effort, by, ...(mode ? { mode: mode.name } : {}), ...(fallbackReason ? { fallbackReason } : {}) }
|
|
750
783
|
await $.ui.log('ClaudeRipple worker pick: ' + JSON.stringify(entry), { to: 'debug' })
|
|
751
784
|
// The brief itself is never logged (it can hold anything); its length is. To re-judge a pick,
|
|
752
785
|
// its brief is in the session's own transcript under the same description.
|
|
@@ -760,15 +793,16 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
760
793
|
|
|
761
794
|
/**
|
|
762
795
|
* What the judge would have said of a brief the main chat already tiered: logged beside the pick
|
|
763
|
-
* (same `id`)
|
|
764
|
-
*
|
|
796
|
+
* (same `id`). With domain ladders the spawn awaits this same result for its domain only; without
|
|
797
|
+
* them it never waits or acts on it. The marker's tier always stands.
|
|
765
798
|
*/
|
|
766
|
-
async function shadowJudge($: EngineInterface, s: Settings, id: string, input: { description: string; prompt: string }): Promise<
|
|
799
|
+
async function shadowJudge($: EngineInterface, s: Settings, id: string, input: { description: string; prompt: string }): Promise<Judged | undefined> {
|
|
767
800
|
try {
|
|
768
801
|
const judged = await judge($, s, input, true)
|
|
769
802
|
const verdict = judged.verdict && 'domain' in judged.verdict ? judged.verdict : null
|
|
770
803
|
await persistWorkerPick($, { type: 'shadow', pick: id, at: new Date(await $.clock.now()).toISOString(), verdict, rung: verdict ? rungOf(verdict) : null, judge: judged.by, ms: judged.ms, ...(judged.error ? { error: judged.error } : {}) })
|
|
771
|
-
|
|
804
|
+
return judged
|
|
805
|
+
} catch (error) { await report($, 'shadow judge', error); return undefined }
|
|
772
806
|
}
|
|
773
807
|
|
|
774
808
|
async function toggleAuto($: EngineInterface): Promise<void> {
|
|
@@ -1219,6 +1253,7 @@ async function dashboard($: EngineInterface, working: boolean): Promise<Dashboar
|
|
|
1219
1253
|
limits: await read($, limits) ?? [],
|
|
1220
1254
|
auto: s.effortAuto, handoff: await read($, advice) ? 'advised' : s.handoffButton === 'always' ? 'always' : 'off',
|
|
1221
1255
|
agents: { live: list.filter(a => ['running', 'waiting'].includes(a.state)).length, total: list.length },
|
|
1256
|
+
compactions: await read($, compactions),
|
|
1222
1257
|
logOpen: await read($, logOpen), agentsOpen: await read($, agentsOpen), effortMenu: await read($, effortMenuOpen),
|
|
1223
1258
|
swampAt: s.swampAt, hide: s.hide, extras: s.extras, isWorking: working,
|
|
1224
1259
|
...(activeModeOf(s) ? { mode: activeModeOf(s)!.name } : {}),
|
|
@@ -1404,8 +1439,16 @@ export const register: Register = on => {
|
|
|
1404
1439
|
await registerAuto($, await currentSettings($))
|
|
1405
1440
|
startTimers($)
|
|
1406
1441
|
// Setup keeps its draft across a reload; quick settings never keep one.
|
|
1407
|
-
|
|
1442
|
+
let open = await read($, panel)
|
|
1443
|
+
// A setup card kept from a reload while setup is done (left by a router outage) goes.
|
|
1444
|
+
if (open === 'setup' && (await currentSettings($)).setupDone) {
|
|
1445
|
+
await update($, panel, () => null)
|
|
1446
|
+
await update($, settingsDraft, () => null)
|
|
1447
|
+
await update($, settingsBase, () => null)
|
|
1448
|
+
open = null
|
|
1449
|
+
}
|
|
1408
1450
|
if (open === 'setup') await rebaseDraft($)
|
|
1451
|
+
if ((await currentSettings($)).judge === 'jev' && (await read($, jev)).source === null) await jevKey($)
|
|
1409
1452
|
if (open === 'settings') { await update($, settingsDraft, () => null); await update($, settingsBase, () => null); await jevKey($) }
|
|
1410
1453
|
if (!(await currentSettings($)).setupDone && !open) await startPanel($, 'setup')
|
|
1411
1454
|
} catch (error) { await report($, 'session.start', error) }
|
|
@@ -1426,6 +1469,7 @@ export const register: Register = on => {
|
|
|
1426
1469
|
await update($, context, () => null)
|
|
1427
1470
|
await update($, lastRealReplyAt, () => null)
|
|
1428
1471
|
if (await read($, kept)) await update($, kept, () => null)
|
|
1472
|
+
await update($, compactions, () => 0)
|
|
1429
1473
|
}
|
|
1430
1474
|
if (e.model) await update($, modelIs, () => e.model!)
|
|
1431
1475
|
await update($, paused, () => e.model ? !isCacheSafeModel(e.model) : false)
|
|
@@ -1632,11 +1676,19 @@ export const register: Register = on => {
|
|
|
1632
1676
|
}
|
|
1633
1677
|
})
|
|
1634
1678
|
on('session.compact', async ($, e, next) => {
|
|
1679
|
+
// A finished compaction of the main conversation adds one to the Compact icon's count; a skipped
|
|
1680
|
+
// one, a subagent's own and the ahead-of-time precompute do not.
|
|
1681
|
+
const counted = async (result: SessionCompactResult): Promise<SessionCompactResult> => {
|
|
1682
|
+
try {
|
|
1683
|
+
if (!e.agentId && e.trigger !== 'precompute' && 'messages' in result) await update($, compactions, n => n + 1)
|
|
1684
|
+
} catch (error) { await report($, 'session.compact count', error) }
|
|
1685
|
+
return result
|
|
1686
|
+
}
|
|
1635
1687
|
try {
|
|
1636
1688
|
const language = await read($, lang)
|
|
1637
1689
|
// The summary drops the eager note: the first prompt after it carries it again.
|
|
1638
1690
|
if (!e.agentId && e.trigger !== 'precompute' && await read($, modeNoted) !== null) await update($, modeNoted, () => null)
|
|
1639
|
-
if (e.agentId || (await currentSettings($)).compactWith !== 'haiku') return next(e)
|
|
1691
|
+
if (e.agentId || (await currentSettings($)).compactWith !== 'haiku') return next(e).then(counted)
|
|
1640
1692
|
if (e.trigger === 'precompute') return { skip: t(language, 'commands.compactSkip') }
|
|
1641
1693
|
const transcript = serializeTranscript(e.messages.length ? e.messages : await $.session.messages())
|
|
1642
1694
|
if (!transcript) throw new Error(t(language, 'toasts.transcriptEmpty'))
|
|
@@ -1646,12 +1698,12 @@ export const register: Register = on => {
|
|
|
1646
1698
|
await update($, context, () => null)
|
|
1647
1699
|
if (await read($, kept)) await update($, kept, () => null)
|
|
1648
1700
|
await update($, resultCard, () => ({ stage: 'compacted' }))
|
|
1649
|
-
return compactResult(answer.text)
|
|
1701
|
+
return counted(compactResult(answer.text))
|
|
1650
1702
|
} catch (error) {
|
|
1651
1703
|
await $.ui.toast(t(await read($, lang), 'toasts.compactFallback'))
|
|
1652
1704
|
await report($, 'session.compact', error)
|
|
1653
1705
|
}
|
|
1654
|
-
return next(e)
|
|
1706
|
+
return counted(await next(e))
|
|
1655
1707
|
})
|
|
1656
1708
|
on('command.run', { command: 'ripple' }, async ($, e, next) => {
|
|
1657
1709
|
try {
|