clay-server 2.26.0-beta.1 → 2.26.0-beta.3
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/bin/cli.js +5 -9
- package/lib/os-users.js +23 -0
- package/lib/project.js +11 -2
- package/lib/public/app.js +31 -5
- package/lib/public/modules/rewind.js +36 -0
- package/lib/public/modules/terminal.js +8 -0
- package/lib/public/modules/tools.js +22 -1
- package/lib/sdk-bridge.js +18 -6
- package/lib/sessions.js +13 -2
- package/lib/terminal-manager.js +16 -2
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1507,6 +1507,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1507
1507
|
if (prevProjectMap[cwd]) {
|
|
1508
1508
|
if (prevProjectMap[cwd].visibility) cwdEntry.visibility = prevProjectMap[cwd].visibility;
|
|
1509
1509
|
if (prevProjectMap[cwd].allowedUsers) cwdEntry.allowedUsers = prevProjectMap[cwd].allowedUsers;
|
|
1510
|
+
if (prevProjectMap[cwd].ownerId) cwdEntry.ownerId = prevProjectMap[cwd].ownerId;
|
|
1510
1511
|
}
|
|
1511
1512
|
allProjects.push(cwdEntry);
|
|
1512
1513
|
usedSlugs.push(slug);
|
|
@@ -1525,6 +1526,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1525
1526
|
if (prevProjectMap[rp.path]) {
|
|
1526
1527
|
if (prevProjectMap[rp.path].visibility) rpEntry.visibility = prevProjectMap[rp.path].visibility;
|
|
1527
1528
|
if (prevProjectMap[rp.path].allowedUsers) rpEntry.allowedUsers = prevProjectMap[rp.path].allowedUsers;
|
|
1529
|
+
if (prevProjectMap[rp.path].ownerId) rpEntry.ownerId = prevProjectMap[rp.path].ownerId;
|
|
1528
1530
|
}
|
|
1529
1531
|
allProjects.push(rpEntry);
|
|
1530
1532
|
}
|
|
@@ -1879,19 +1881,13 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1879
1881
|
}
|
|
1880
1882
|
clearStaleConfig();
|
|
1881
1883
|
|
|
1882
|
-
// Re-fork with TLS
|
|
1883
|
-
var newConfig = {
|
|
1884
|
+
// Re-fork with TLS (preserve all existing config fields)
|
|
1885
|
+
var newConfig = Object.assign({}, config, {
|
|
1884
1886
|
pid: null,
|
|
1885
|
-
port: config.port,
|
|
1886
|
-
pinHash: config.pinHash || null,
|
|
1887
1887
|
tls: true,
|
|
1888
1888
|
builtinCert: hasBuiltinCert,
|
|
1889
1889
|
mkcertDetected: mkcertDetected,
|
|
1890
|
-
|
|
1891
|
-
keepAwake: config.keepAwake || false,
|
|
1892
|
-
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
1893
|
-
projects: config.projects || [],
|
|
1894
|
-
};
|
|
1890
|
+
});
|
|
1895
1891
|
|
|
1896
1892
|
ensureConfigDir();
|
|
1897
1893
|
saveConfig(newConfig);
|
package/lib/os-users.js
CHANGED
|
@@ -254,6 +254,26 @@ function toLinuxUsername(clayUsername) {
|
|
|
254
254
|
return name;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Ensure linger is enabled for a Linux user so systemd creates /run/user/<uid>.
|
|
259
|
+
* Required for CLI tools like gcloud and gh that need XDG_RUNTIME_DIR.
|
|
260
|
+
*/
|
|
261
|
+
function ensureLinger(username) {
|
|
262
|
+
try {
|
|
263
|
+
var uid = execSync("id -u " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
264
|
+
var lingerFile = "/var/lib/systemd/linger/" + username;
|
|
265
|
+
if (fs.existsSync(lingerFile)) return;
|
|
266
|
+
execSync("loginctl enable-linger " + username, {
|
|
267
|
+
encoding: "utf8",
|
|
268
|
+
timeout: 10000,
|
|
269
|
+
stdio: "pipe",
|
|
270
|
+
});
|
|
271
|
+
console.log("[os-users] Enabled linger for " + username + " (uid " + uid + ")");
|
|
272
|
+
} catch (e) {
|
|
273
|
+
console.warn("[os-users] Failed to enable linger for " + username + ": " + (e.stderr || e.message || "").trim());
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
/**
|
|
258
278
|
* Check if a Linux user already exists.
|
|
259
279
|
*/
|
|
@@ -353,6 +373,7 @@ function provisionLinuxUser(clayUsername) {
|
|
|
353
373
|
timeout: 15000,
|
|
354
374
|
stdio: "pipe",
|
|
355
375
|
});
|
|
376
|
+
ensureLinger(linuxName);
|
|
356
377
|
console.log("[os-users] Provisioned Linux user: " + linuxName + " (Clay user: " + clayUsername + ")");
|
|
357
378
|
installClaudeCli(linuxName);
|
|
358
379
|
return { ok: true, linuxUser: linuxName };
|
|
@@ -383,6 +404,8 @@ function provisionAllUsers(usersModule) {
|
|
|
383
404
|
console.log("[os-users] Claude CLI missing for " + user.linuxUser + ", installing...");
|
|
384
405
|
installClaudeCli(user.linuxUser);
|
|
385
406
|
}
|
|
407
|
+
// Ensure linger is enabled for existing users
|
|
408
|
+
ensureLinger(user.linuxUser);
|
|
386
409
|
result.skipped.push({ id: user.id, username: user.username, linuxUser: user.linuxUser });
|
|
387
410
|
continue;
|
|
388
411
|
}
|
package/lib/project.js
CHANGED
|
@@ -2204,6 +2204,8 @@ function createProjectContext(opts) {
|
|
|
2204
2204
|
if (msg.type === "rewind_preview") {
|
|
2205
2205
|
var session = getSessionForWs(ws);
|
|
2206
2206
|
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2207
|
+
// Reject preview requests while a rewind is executing
|
|
2208
|
+
if (session._rewindInProgress) return;
|
|
2207
2209
|
|
|
2208
2210
|
(async function () {
|
|
2209
2211
|
var result;
|
|
@@ -2233,6 +2235,12 @@ function createProjectContext(opts) {
|
|
|
2233
2235
|
if (msg.type === "rewind_execute") {
|
|
2234
2236
|
var session = getSessionForWs(ws);
|
|
2235
2237
|
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2238
|
+
// Guard against concurrent rewind executions
|
|
2239
|
+
if (session._rewindInProgress) {
|
|
2240
|
+
sendTo(ws, { type: "rewind_error", text: "Rewind already in progress." });
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
session._rewindInProgress = true;
|
|
2236
2244
|
var mode = msg.mode || "both";
|
|
2237
2245
|
|
|
2238
2246
|
(async function () {
|
|
@@ -2293,6 +2301,7 @@ function createProjectContext(opts) {
|
|
|
2293
2301
|
} catch (err) {
|
|
2294
2302
|
sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
|
|
2295
2303
|
} finally {
|
|
2304
|
+
session._rewindInProgress = false;
|
|
2296
2305
|
if (result && result.isTemp) result.cleanup();
|
|
2297
2306
|
}
|
|
2298
2307
|
})();
|
|
@@ -3328,7 +3337,7 @@ function createProjectContext(opts) {
|
|
|
3328
3337
|
return;
|
|
3329
3338
|
}
|
|
3330
3339
|
}
|
|
3331
|
-
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws));
|
|
3340
|
+
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws), ws);
|
|
3332
3341
|
if (!t) {
|
|
3333
3342
|
sendTo(ws, { type: "term_error", error: "Cannot create terminal (node-pty not available or limit reached)" });
|
|
3334
3343
|
return;
|
|
@@ -3356,7 +3365,7 @@ function createProjectContext(opts) {
|
|
|
3356
3365
|
|
|
3357
3366
|
if (msg.type === "term_resize") {
|
|
3358
3367
|
if (msg.id && msg.cols > 0 && msg.rows > 0) {
|
|
3359
|
-
tm.resize(msg.id, msg.cols, msg.rows);
|
|
3368
|
+
tm.resize(msg.id, msg.cols, msg.rows, ws);
|
|
3360
3369
|
}
|
|
3361
3370
|
return;
|
|
3362
3371
|
}
|
package/lib/public/app.js
CHANGED
|
@@ -6,15 +6,15 @@ import { initSidebar, renderSessionList, handleSearchResults, updateSessionPrese
|
|
|
6
6
|
import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
|
|
7
7
|
import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
|
|
8
8
|
import { initMateMemory, renderMemoryList, hideMemory } from './modules/mate-memory.js';
|
|
9
|
-
import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
|
|
9
|
+
import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton, onRewindComplete, onRewindError } from './modules/rewind.js';
|
|
10
10
|
import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
|
|
11
11
|
import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage, hasSendableContent, setScheduleBtnDisabled, setScheduleDelayMs, clearScheduleDelay } from './modules/input.js';
|
|
12
12
|
import { initQrCode, triggerShare } from './modules/qrcode.js';
|
|
13
13
|
import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
|
|
14
|
-
import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
|
|
14
|
+
import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
|
|
15
15
|
import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
|
|
16
16
|
import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
|
|
17
|
-
import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
|
|
17
|
+
import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
|
|
18
18
|
import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
|
|
19
19
|
import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './modules/project-settings.js';
|
|
20
20
|
import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
|
|
@@ -2468,7 +2468,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
2468
2468
|
}
|
|
2469
2469
|
|
|
2470
2470
|
function accumulateUsage(cost, usage) {
|
|
2471
|
-
|
|
2471
|
+
// cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
|
|
2472
|
+
// Assign directly instead of summing to avoid overcounting.
|
|
2473
|
+
if (cost != null) sessionUsage.cost = cost;
|
|
2472
2474
|
if (usage) {
|
|
2473
2475
|
sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
|
|
2474
2476
|
sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
|
|
@@ -2666,7 +2668,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
2666
2668
|
}
|
|
2667
2669
|
|
|
2668
2670
|
function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
|
|
2669
|
-
|
|
2671
|
+
// cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
|
|
2672
|
+
if (cost != null) contextData.cost = cost;
|
|
2670
2673
|
// Use latest turn values (not cumulative) since each turn's input_tokens
|
|
2671
2674
|
// already includes the full conversation context up to that point
|
|
2672
2675
|
if (usage) {
|
|
@@ -3718,6 +3721,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
3718
3721
|
setStatus("connected");
|
|
3719
3722
|
if (!loopActive) enableMainInput();
|
|
3720
3723
|
resetUsage();
|
|
3724
|
+
resetTurnMetaCost();
|
|
3721
3725
|
resetContext();
|
|
3722
3726
|
// Clear header indicators
|
|
3723
3727
|
clearRateLimitIndicator();
|
|
@@ -4711,6 +4715,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4711
4715
|
break;
|
|
4712
4716
|
|
|
4713
4717
|
case "rewind_complete":
|
|
4718
|
+
onRewindComplete();
|
|
4714
4719
|
setRewindMode(false);
|
|
4715
4720
|
var rewindText = "Rewound to earlier point. Files have been restored.";
|
|
4716
4721
|
if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
|
|
@@ -4719,6 +4724,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4719
4724
|
break;
|
|
4720
4725
|
|
|
4721
4726
|
case "rewind_error":
|
|
4727
|
+
onRewindError();
|
|
4722
4728
|
clearPendingRewindUuid();
|
|
4723
4729
|
addSystemMessage(msg.text || "Rewind failed.", true);
|
|
4724
4730
|
break;
|
|
@@ -4809,6 +4815,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4809
4815
|
handleTermOutput(msg);
|
|
4810
4816
|
break;
|
|
4811
4817
|
|
|
4818
|
+
case "term_resized":
|
|
4819
|
+
handleTermResized(msg);
|
|
4820
|
+
break;
|
|
4821
|
+
|
|
4812
4822
|
case "term_exited":
|
|
4813
4823
|
handleTermExited(msg);
|
|
4814
4824
|
break;
|
|
@@ -4976,6 +4986,22 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4976
4986
|
// Update mate sidebar if currently viewing this mate
|
|
4977
4987
|
if (dmMode && dmTargetUser && dmTargetUser.isMate && dmTargetUser.id === msg.mate.id) {
|
|
4978
4988
|
updateMateSidebarProfile(msg.mate);
|
|
4989
|
+
// Sync dmTargetUser so subsequent renders use fresh data
|
|
4990
|
+
var mp2 = msg.mate.profile || {};
|
|
4991
|
+
dmTargetUser.displayName = mp2.displayName || msg.mate.name || dmTargetUser.displayName;
|
|
4992
|
+
dmTargetUser.avatarStyle = mp2.avatarStyle || dmTargetUser.avatarStyle;
|
|
4993
|
+
dmTargetUser.avatarSeed = mp2.avatarSeed || dmTargetUser.avatarSeed;
|
|
4994
|
+
dmTargetUser.avatarColor = mp2.avatarColor || dmTargetUser.avatarColor;
|
|
4995
|
+
dmTargetUser.avatarCustom = mp2.avatarCustom || "";
|
|
4996
|
+
dmTargetUser.profile = mp2;
|
|
4997
|
+
// Refresh body dataset so new chat bubbles use the updated avatar
|
|
4998
|
+
document.body.dataset.mateAvatarUrl = mateAvatarUrl(dmTargetUser, 36);
|
|
4999
|
+
document.body.dataset.mateName = mp2.displayName || msg.mate.name || "";
|
|
5000
|
+
// Update existing chat bubble avatars
|
|
5001
|
+
var mateAvis = document.querySelectorAll(".dm-bubble-avatar-mate");
|
|
5002
|
+
for (var mbi = 0; mbi < mateAvis.length; mbi++) {
|
|
5003
|
+
mateAvis[mbi].src = document.body.dataset.mateAvatarUrl;
|
|
5004
|
+
}
|
|
4979
5005
|
}
|
|
4980
5006
|
// Update DM header if currently chatting with this mate
|
|
4981
5007
|
if (dmMode && dmTargetUser && dmTargetUser.id === msg.mate.id) {
|
|
@@ -4,6 +4,8 @@ import { iconHtml, refreshIcons } from './icons.js';
|
|
|
4
4
|
var ctx;
|
|
5
5
|
var rewindMode = false;
|
|
6
6
|
var pendingRewindUuid = null;
|
|
7
|
+
var rewindPreviewInFlight = false;
|
|
8
|
+
var rewindExecuting = false;
|
|
7
9
|
var rewindBannerEl = null;
|
|
8
10
|
var rewindScrollHandler = null;
|
|
9
11
|
var rewindModal, rewindSummary, rewindFilesList, rewindConfirmBtn, rewindCancelBtn, rewindModeOptions;
|
|
@@ -50,6 +52,17 @@ export function clearPendingRewindUuid() {
|
|
|
50
52
|
pendingRewindUuid = null;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
export function onRewindComplete() {
|
|
56
|
+
rewindExecuting = false;
|
|
57
|
+
rewindPreviewInFlight = false;
|
|
58
|
+
pendingRewindUuid = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function onRewindError() {
|
|
62
|
+
rewindExecuting = false;
|
|
63
|
+
rewindPreviewInFlight = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
function initiateRewind(uuid) {
|
|
54
67
|
if (ctx.processing) {
|
|
55
68
|
ctx.addSystemMessage("Cannot rewind while processing. Stop the current operation first.", true);
|
|
@@ -59,7 +72,16 @@ function initiateRewind(uuid) {
|
|
|
59
72
|
ctx.addSystemMessage("No rewind point found for this turn.", true);
|
|
60
73
|
return;
|
|
61
74
|
}
|
|
75
|
+
if (rewindPreviewInFlight) {
|
|
76
|
+
// Debounce: ignore clicks while a preview is already in flight
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (rewindExecuting) {
|
|
80
|
+
ctx.addSystemMessage("Rewind already in progress.", true);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
62
83
|
pendingRewindUuid = uuid;
|
|
84
|
+
rewindPreviewInFlight = true;
|
|
63
85
|
if (ctx.ws && ctx.connected) {
|
|
64
86
|
ctx.ws.send(JSON.stringify({ type: "rewind_preview", uuid: uuid }));
|
|
65
87
|
}
|
|
@@ -98,6 +120,17 @@ function updateSummaryForMode() {
|
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
export function showRewindModal(data) {
|
|
123
|
+
rewindPreviewInFlight = false;
|
|
124
|
+
|
|
125
|
+
// Ignore stale preview results that don't match current pending UUID
|
|
126
|
+
if (data.uuid && pendingRewindUuid && data.uuid !== pendingRewindUuid) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Ignore if a rewind is already executing
|
|
130
|
+
if (rewindExecuting) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
101
134
|
var p = data.preview || data;
|
|
102
135
|
var filePaths = p.filesChanged || p.filePaths || p.files || [];
|
|
103
136
|
var fileCount = filePaths.length;
|
|
@@ -171,6 +204,7 @@ export function showRewindModal(data) {
|
|
|
171
204
|
export function hideRewindModal() {
|
|
172
205
|
rewindModal.classList.add("hidden");
|
|
173
206
|
pendingRewindUuid = null;
|
|
207
|
+
rewindPreviewInFlight = false;
|
|
174
208
|
}
|
|
175
209
|
|
|
176
210
|
export function renderDiffPre(text) {
|
|
@@ -333,7 +367,9 @@ export function initRewind(_ctx) {
|
|
|
333
367
|
});
|
|
334
368
|
|
|
335
369
|
rewindConfirmBtn.addEventListener("click", function() {
|
|
370
|
+
if (rewindExecuting) return;
|
|
336
371
|
if (pendingRewindUuid && ctx.ws && ctx.connected) {
|
|
372
|
+
rewindExecuting = true;
|
|
337
373
|
var mode = getSelectedMode();
|
|
338
374
|
ctx.ws.send(JSON.stringify({ type: "rewind_execute", uuid: pendingRewindUuid, mode: mode }));
|
|
339
375
|
}
|
|
@@ -653,6 +653,14 @@ export function handleTermOutput(msg) {
|
|
|
653
653
|
}
|
|
654
654
|
}
|
|
655
655
|
|
|
656
|
+
export function handleTermResized(msg) {
|
|
657
|
+
if (!msg.id) return;
|
|
658
|
+
var tab = tabs.get(msg.id);
|
|
659
|
+
if (tab && tab.xterm && msg.cols > 0 && msg.rows > 0) {
|
|
660
|
+
tab.xterm.resize(msg.cols, msg.rows);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
656
664
|
export function handleTermExited(msg) {
|
|
657
665
|
if (!msg.id) return;
|
|
658
666
|
var tab = tabs.get(msg.id);
|
|
@@ -2063,13 +2063,29 @@ export function markSubagentDone(parentToolId, status, summary, usage) {
|
|
|
2063
2063
|
if (usage) updateSubagentProgress(parentToolId, usage, null);
|
|
2064
2064
|
}
|
|
2065
2065
|
|
|
2066
|
+
var _lastCumulativeCost = 0;
|
|
2067
|
+
|
|
2068
|
+
export function resetTurnMetaCost() {
|
|
2069
|
+
_lastCumulativeCost = 0;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2066
2072
|
export function addTurnMeta(cost, duration) {
|
|
2067
2073
|
closeToolGroup();
|
|
2068
2074
|
var div = document.createElement("div");
|
|
2069
2075
|
div.className = "turn-meta";
|
|
2070
2076
|
div.dataset.turn = ctx.turnCounter;
|
|
2071
2077
|
var parts = [];
|
|
2072
|
-
if (cost != null)
|
|
2078
|
+
if (cost != null) {
|
|
2079
|
+
// cost is cumulative total_cost_usd from the SDK.
|
|
2080
|
+
// When the SDK session restarts, total_cost_usd resets to 0 so cost
|
|
2081
|
+
// can drop below _lastCumulativeCost. In that case the entire cost
|
|
2082
|
+
// value IS the delta for this turn (fresh SDK session).
|
|
2083
|
+
var delta = cost - _lastCumulativeCost;
|
|
2084
|
+
if (delta < 0) delta = cost;
|
|
2085
|
+
_lastCumulativeCost = cost;
|
|
2086
|
+
var deltaStr = delta > 0 ? "+$" + delta.toFixed(4) : "$0.0000";
|
|
2087
|
+
parts.push(deltaStr + " \u2192 $" + cost.toFixed(4));
|
|
2088
|
+
}
|
|
2073
2089
|
if (duration != null) parts.push((duration / 1000).toFixed(1) + "s");
|
|
2074
2090
|
if (parts.length) {
|
|
2075
2091
|
div.textContent = parts.join(" \u00b7 ");
|
|
@@ -2114,6 +2130,7 @@ export function saveToolState() {
|
|
|
2114
2130
|
currentToolGroup: currentToolGroup,
|
|
2115
2131
|
toolGroupCounter: toolGroupCounter,
|
|
2116
2132
|
toolGroups: toolGroups,
|
|
2133
|
+
lastCumulativeCost: _lastCumulativeCost,
|
|
2117
2134
|
};
|
|
2118
2135
|
}
|
|
2119
2136
|
|
|
@@ -2126,6 +2143,7 @@ export function restoreToolState(saved) {
|
|
|
2126
2143
|
currentToolGroup = saved.currentToolGroup;
|
|
2127
2144
|
toolGroupCounter = saved.toolGroupCounter;
|
|
2128
2145
|
toolGroups = saved.toolGroups;
|
|
2146
|
+
_lastCumulativeCost = saved.lastCumulativeCost || 0;
|
|
2129
2147
|
if (todoWidgetEl) {
|
|
2130
2148
|
setupTodoObserver();
|
|
2131
2149
|
}
|
|
@@ -2146,6 +2164,9 @@ export function resetToolState() {
|
|
|
2146
2164
|
currentToolGroup = null;
|
|
2147
2165
|
toolGroupCounter = 0;
|
|
2148
2166
|
toolGroups = {};
|
|
2167
|
+
// NOTE: do NOT reset _lastCumulativeCost here — it must persist across
|
|
2168
|
+
// turns so addTurnMeta can compute per-turn deltas. It is only cleared
|
|
2169
|
+
// on new conversation via resetTurnMetaCost().
|
|
2149
2170
|
var stickyEl = document.getElementById("todo-sticky");
|
|
2150
2171
|
if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
|
|
2151
2172
|
}
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -1138,6 +1138,8 @@ function createSDKBridge(opts) {
|
|
|
1138
1138
|
if (session.lastRewindUuid) {
|
|
1139
1139
|
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
1140
1140
|
delete session.lastRewindUuid;
|
|
1141
|
+
// Persist the deletion so server restarts don't re-use a stale UUID
|
|
1142
|
+
sm.saveSessionFile(session);
|
|
1141
1143
|
}
|
|
1142
1144
|
}
|
|
1143
1145
|
|
|
@@ -1687,8 +1689,13 @@ function createSDKBridge(opts) {
|
|
|
1687
1689
|
}
|
|
1688
1690
|
|
|
1689
1691
|
async function processQueryStream(session) {
|
|
1692
|
+
// Capture references at start so we only clean up OUR resources in finally,
|
|
1693
|
+
// not resources from a newer query that may have been created after an abort.
|
|
1694
|
+
var myQueryInstance = session.queryInstance;
|
|
1695
|
+
var myMessageQueue = session.messageQueue;
|
|
1696
|
+
var myAbortController = session.abortController;
|
|
1690
1697
|
try {
|
|
1691
|
-
for await (var msg of
|
|
1698
|
+
for await (var msg of myQueryInstance) {
|
|
1692
1699
|
processSDKMessage(session, msg);
|
|
1693
1700
|
}
|
|
1694
1701
|
// Stream ended normally after a task stop — no "result" message was sent,
|
|
@@ -1704,7 +1711,7 @@ function createSDKBridge(opts) {
|
|
|
1704
1711
|
if (session.isProcessing) {
|
|
1705
1712
|
session.isProcessing = false;
|
|
1706
1713
|
onProcessingChanged();
|
|
1707
|
-
if (err.name === "AbortError" || (
|
|
1714
|
+
if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
|
|
1708
1715
|
if (!session.destroying) {
|
|
1709
1716
|
sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
|
|
1710
1717
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
@@ -1777,16 +1784,19 @@ function createSDKBridge(opts) {
|
|
|
1777
1784
|
} finally {
|
|
1778
1785
|
// Close the SDK query to terminate the underlying claude child process.
|
|
1779
1786
|
// Without this, the process stays alive indefinitely (single-user mode).
|
|
1780
|
-
if
|
|
1787
|
+
// Only clean up if the session still references OUR resources.
|
|
1788
|
+
// A rewind + new startQuery may have already replaced these with
|
|
1789
|
+
// a newer query — clobbering them would kill the new query.
|
|
1790
|
+
if (session.queryInstance === myQueryInstance) {
|
|
1781
1791
|
try {
|
|
1782
1792
|
if (typeof session.queryInstance.close === "function") {
|
|
1783
1793
|
session.queryInstance.close();
|
|
1784
1794
|
}
|
|
1785
1795
|
} catch (e) {}
|
|
1796
|
+
session.queryInstance = null;
|
|
1786
1797
|
}
|
|
1787
|
-
session.
|
|
1788
|
-
session.
|
|
1789
|
-
session.abortController = null;
|
|
1798
|
+
if (session.messageQueue === myMessageQueue) session.messageQueue = null;
|
|
1799
|
+
if (session.abortController === myAbortController) session.abortController = null;
|
|
1790
1800
|
session.taskStopRequested = false;
|
|
1791
1801
|
session.pendingPermissions = {};
|
|
1792
1802
|
session.pendingAskUser = {};
|
|
@@ -1952,6 +1962,8 @@ function createSDKBridge(opts) {
|
|
|
1952
1962
|
if (session.lastRewindUuid) {
|
|
1953
1963
|
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
1954
1964
|
delete session.lastRewindUuid;
|
|
1965
|
+
// Persist the deletion so server restarts don't re-use a stale UUID
|
|
1966
|
+
sm.saveSessionFile(session);
|
|
1955
1967
|
}
|
|
1956
1968
|
}
|
|
1957
1969
|
|
package/lib/sessions.js
CHANGED
|
@@ -96,10 +96,14 @@ function createSessionManager(opts) {
|
|
|
96
96
|
lines.push(JSON.stringify(session.history[i]));
|
|
97
97
|
}
|
|
98
98
|
var sfPath = sessionFilePath(session.cliSessionId);
|
|
99
|
-
|
|
99
|
+
// Atomic write: write to temp file then rename, so a crash mid-write
|
|
100
|
+
// cannot leave a truncated/corrupted session file.
|
|
101
|
+
var tmpPath = sfPath + ".tmp." + process.pid;
|
|
102
|
+
fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
|
|
100
103
|
if (process.platform !== "win32") {
|
|
101
|
-
try { fs.chmodSync(
|
|
104
|
+
try { fs.chmodSync(tmpPath, 0o600); } catch (chmodErr) {}
|
|
102
105
|
}
|
|
106
|
+
fs.renameSync(tmpPath, sfPath);
|
|
103
107
|
} catch(e) {
|
|
104
108
|
console.error("[session] Failed to save session file:", e.message);
|
|
105
109
|
}
|
|
@@ -123,6 +127,13 @@ function createSessionManager(opts) {
|
|
|
123
127
|
var files;
|
|
124
128
|
try { files = fs.readdirSync(sessionsDir); } catch { return; }
|
|
125
129
|
|
|
130
|
+
// Clean up stale temp files from interrupted atomic writes
|
|
131
|
+
for (var ti = 0; ti < files.length; ti++) {
|
|
132
|
+
if (files[ti].indexOf(".tmp.") !== -1) {
|
|
133
|
+
try { fs.unlinkSync(path.join(sessionsDir, files[ti])); } catch (e) {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
var loaded = [];
|
|
127
138
|
for (var i = 0; i < files.length; i++) {
|
|
128
139
|
if (!files[i].endsWith(".jsonl")) continue;
|
package/lib/terminal-manager.js
CHANGED
|
@@ -16,7 +16,7 @@ function createTerminalManager(opts) {
|
|
|
16
16
|
var nextId = 1;
|
|
17
17
|
var terminals = new Map(); // id -> terminal session
|
|
18
18
|
|
|
19
|
-
function create(cols, rows, osUserInfo) {
|
|
19
|
+
function create(cols, rows, osUserInfo, ownerWs) {
|
|
20
20
|
if (terminals.size >= MAX_TERMINALS) return null;
|
|
21
21
|
|
|
22
22
|
var pty = createTerminal(cwd, cols, rows, osUserInfo);
|
|
@@ -34,6 +34,7 @@ function createTerminalManager(opts) {
|
|
|
34
34
|
exited: false,
|
|
35
35
|
exitCode: null,
|
|
36
36
|
subscribers: new Set(),
|
|
37
|
+
ownerWs: ownerWs || null,
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
pty.onData(function (data) {
|
|
@@ -84,6 +85,11 @@ function createTerminalManager(opts) {
|
|
|
84
85
|
sendTo(ws, { type: "term_output", id: id, data: replay });
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// Send current terminal dimensions so the client renders at the correct size
|
|
89
|
+
if (!alreadySubscribed && session.cols && session.rows) {
|
|
90
|
+
sendTo(ws, { type: "term_resized", id: id, cols: session.cols, rows: session.rows });
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
// If already exited, notify
|
|
88
94
|
if (session.exited) {
|
|
89
95
|
sendTo(ws, { type: "term_exited", id: id });
|
|
@@ -111,14 +117,22 @@ function createTerminalManager(opts) {
|
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
function resize(id, cols, rows) {
|
|
120
|
+
function resize(id, cols, rows, sourceWs) {
|
|
115
121
|
var session = terminals.get(id);
|
|
116
122
|
if (!session || !session.pty) return;
|
|
123
|
+
// Only the terminal owner can resize the PTY.
|
|
124
|
+
// Observers resizing would cause SIGWINCH and flood the owner with escape sequences.
|
|
125
|
+
if (session.ownerWs && sourceWs && sourceWs !== session.ownerWs) return;
|
|
117
126
|
if (cols > 0 && rows > 0) {
|
|
118
127
|
try {
|
|
119
128
|
session.pty.resize(cols, rows);
|
|
120
129
|
session.cols = cols;
|
|
121
130
|
session.rows = rows;
|
|
131
|
+
// Notify other subscribers about the resize so their xterm stays in sync
|
|
132
|
+
var msg = JSON.stringify({ type: "term_resized", id: id, cols: cols, rows: rows });
|
|
133
|
+
for (var ws of session.subscribers) {
|
|
134
|
+
if (ws.readyState === 1 && ws !== sourceWs) ws.send(msg);
|
|
135
|
+
}
|
|
122
136
|
} catch (e) {}
|
|
123
137
|
}
|
|
124
138
|
}
|