clay-server 2.26.0-beta.2 → 2.26.0-beta.4
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/lib/project.js +136 -0
- package/lib/public/app.js +40 -16
- package/lib/public/css/input.css +173 -59
- package/lib/public/css/menus.css +7 -0
- package/lib/public/css/sidebar.css +10 -0
- package/lib/public/index.html +7 -0
- package/lib/public/modules/context-sources.js +226 -0
- package/lib/public/modules/rewind.js +36 -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 +20 -4
- package/package.json +1 -1
package/lib/project.js
CHANGED
|
@@ -18,6 +18,32 @@ var userPresence = require("./user-presence");
|
|
|
18
18
|
var { attachDebate } = require("./project-debate");
|
|
19
19
|
var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
20
20
|
|
|
21
|
+
// --- Context Sources persistence ---
|
|
22
|
+
var _ctxSrcConfig = require("./config");
|
|
23
|
+
var _ctxSrcDir = path.join(_ctxSrcConfig.CONFIG_DIR, "context-sources");
|
|
24
|
+
|
|
25
|
+
function loadContextSources(slug) {
|
|
26
|
+
try {
|
|
27
|
+
var filePath = path.join(_ctxSrcDir, slug + ".json");
|
|
28
|
+
var data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
29
|
+
return data.active || [];
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function saveContextSources(slug, activeIds) {
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(_ctxSrcDir)) {
|
|
38
|
+
fs.mkdirSync(_ctxSrcDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
var filePath = path.join(_ctxSrcDir, slug + ".json");
|
|
41
|
+
fs.writeFileSync(filePath, JSON.stringify({ active: activeIds }), "utf8");
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error("[context-sources] Failed to save:", e.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
21
47
|
// Validate environment variable string (KEY=VALUE per line)
|
|
22
48
|
// Returns null if valid, or an error string if invalid
|
|
23
49
|
function validateEnvString(str) {
|
|
@@ -1235,6 +1261,7 @@ function createProjectContext(opts) {
|
|
|
1235
1261
|
}
|
|
1236
1262
|
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1237
1263
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
1264
|
+
sendTo(ws, { type: "context_sources_state", active: loadContextSources(slug) });
|
|
1238
1265
|
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
1239
1266
|
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
1240
1267
|
|
|
@@ -2204,6 +2231,8 @@ function createProjectContext(opts) {
|
|
|
2204
2231
|
if (msg.type === "rewind_preview") {
|
|
2205
2232
|
var session = getSessionForWs(ws);
|
|
2206
2233
|
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2234
|
+
// Reject preview requests while a rewind is executing
|
|
2235
|
+
if (session._rewindInProgress) return;
|
|
2207
2236
|
|
|
2208
2237
|
(async function () {
|
|
2209
2238
|
var result;
|
|
@@ -2233,6 +2262,12 @@ function createProjectContext(opts) {
|
|
|
2233
2262
|
if (msg.type === "rewind_execute") {
|
|
2234
2263
|
var session = getSessionForWs(ws);
|
|
2235
2264
|
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2265
|
+
// Guard against concurrent rewind executions
|
|
2266
|
+
if (session._rewindInProgress) {
|
|
2267
|
+
sendTo(ws, { type: "rewind_error", text: "Rewind already in progress." });
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
session._rewindInProgress = true;
|
|
2236
2271
|
var mode = msg.mode || "both";
|
|
2237
2272
|
|
|
2238
2273
|
(async function () {
|
|
@@ -2293,6 +2328,7 @@ function createProjectContext(opts) {
|
|
|
2293
2328
|
} catch (err) {
|
|
2294
2329
|
sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
|
|
2295
2330
|
} finally {
|
|
2331
|
+
session._rewindInProgress = false;
|
|
2296
2332
|
if (result && result.isTemp) result.cleanup();
|
|
2297
2333
|
}
|
|
2298
2334
|
})();
|
|
@@ -3365,6 +3401,14 @@ function createProjectContext(opts) {
|
|
|
3365
3401
|
if (msg.id) {
|
|
3366
3402
|
tm.close(msg.id);
|
|
3367
3403
|
send({ type: "term_list", terminals: tm.list() });
|
|
3404
|
+
// Remove closed terminal from context sources
|
|
3405
|
+
var saved = loadContextSources(slug);
|
|
3406
|
+
var termKey = "term:" + msg.id;
|
|
3407
|
+
var filtered = saved.filter(function(id) { return id !== termKey; });
|
|
3408
|
+
if (filtered.length !== saved.length) {
|
|
3409
|
+
saveContextSources(slug, filtered);
|
|
3410
|
+
send({ type: "context_sources_state", active: filtered });
|
|
3411
|
+
}
|
|
3368
3412
|
}
|
|
3369
3413
|
return;
|
|
3370
3414
|
}
|
|
@@ -3377,6 +3421,13 @@ function createProjectContext(opts) {
|
|
|
3377
3421
|
return;
|
|
3378
3422
|
}
|
|
3379
3423
|
|
|
3424
|
+
// --- Context Sources ---
|
|
3425
|
+
if (msg.type === "context_sources_save") {
|
|
3426
|
+
var activeIds = msg.active || [];
|
|
3427
|
+
saveContextSources(slug, activeIds);
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3380
3431
|
// --- Scheduled tasks permission gate ---
|
|
3381
3432
|
if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
|
|
3382
3433
|
msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
|
|
@@ -3835,6 +3886,91 @@ function createProjectContext(opts) {
|
|
|
3835
3886
|
fullText = mentionPrefix + "\n\n" + fullText;
|
|
3836
3887
|
}
|
|
3837
3888
|
|
|
3889
|
+
// Inject active terminal context sources (delta only: send new output since last message)
|
|
3890
|
+
var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
|
|
3891
|
+
var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
|
|
3892
|
+
var TERM_TAIL_SIZE = 6144; // keep last 6KB for recent state
|
|
3893
|
+
var ctxSources = loadContextSources(slug);
|
|
3894
|
+
if (ctxSources.length > 0) {
|
|
3895
|
+
if (!session._termContextCursors) session._termContextCursors = {};
|
|
3896
|
+
var termContextParts = [];
|
|
3897
|
+
for (var ci = 0; ci < ctxSources.length; ci++) {
|
|
3898
|
+
var srcId = ctxSources[ci];
|
|
3899
|
+
if (srcId.startsWith("term:")) {
|
|
3900
|
+
var termId = parseInt(srcId.split(":")[1], 10);
|
|
3901
|
+
var sb = tm.getScrollback(termId);
|
|
3902
|
+
if (sb) {
|
|
3903
|
+
var lastCursor;
|
|
3904
|
+
if (termId in session._termContextCursors) {
|
|
3905
|
+
lastCursor = session._termContextCursors[termId];
|
|
3906
|
+
// Terminal was recycled (closed and reopened with same ID) — reset cursor
|
|
3907
|
+
if (lastCursor > sb.totalBytesWritten) lastCursor = 0;
|
|
3908
|
+
} else {
|
|
3909
|
+
// First time seeing this terminal — include last 8KB (what user can see now)
|
|
3910
|
+
lastCursor = Math.max(0, sb.totalBytesWritten - TERM_CONTEXT_MAX);
|
|
3911
|
+
}
|
|
3912
|
+
var newBytes = sb.totalBytesWritten - lastCursor;
|
|
3913
|
+
session._termContextCursors[termId] = sb.totalBytesWritten;
|
|
3914
|
+
if (newBytes <= 0) continue;
|
|
3915
|
+
// Build timestamped delta from chunks
|
|
3916
|
+
var deltaChunks = [];
|
|
3917
|
+
var bytePos = sb.bufferStart;
|
|
3918
|
+
for (var chunkIdx = 0; chunkIdx < sb.chunks.length; chunkIdx++) {
|
|
3919
|
+
var chunk = sb.chunks[chunkIdx];
|
|
3920
|
+
var chunkEnd = bytePos + chunk.data.length;
|
|
3921
|
+
if (chunkEnd > lastCursor) {
|
|
3922
|
+
// This chunk has new content
|
|
3923
|
+
var chunkData = chunk.data;
|
|
3924
|
+
if (bytePos < lastCursor) {
|
|
3925
|
+
// Partial chunk: only the part after lastCursor
|
|
3926
|
+
chunkData = chunkData.slice(lastCursor - bytePos);
|
|
3927
|
+
}
|
|
3928
|
+
deltaChunks.push({ ts: chunk.ts, data: chunkData });
|
|
3929
|
+
}
|
|
3930
|
+
bytePos = chunkEnd;
|
|
3931
|
+
}
|
|
3932
|
+
if (deltaChunks.length === 0) continue;
|
|
3933
|
+
// Format with timestamps: group by second to avoid excessive timestamps
|
|
3934
|
+
var lines = [];
|
|
3935
|
+
var lastTimeSec = 0;
|
|
3936
|
+
for (var di = 0; di < deltaChunks.length; di++) {
|
|
3937
|
+
var dc = deltaChunks[di];
|
|
3938
|
+
var cleaned = dc.data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
3939
|
+
if (!cleaned) continue;
|
|
3940
|
+
var timeSec = Math.floor(dc.ts / 1000);
|
|
3941
|
+
if (timeSec !== lastTimeSec) {
|
|
3942
|
+
var d = new Date(dc.ts);
|
|
3943
|
+
var timeStr = d.toTimeString().slice(0, 8); // HH:MM:SS
|
|
3944
|
+
lines.push("[" + timeStr + "] " + cleaned);
|
|
3945
|
+
lastTimeSec = timeSec;
|
|
3946
|
+
} else {
|
|
3947
|
+
lines.push(cleaned);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
var delta = lines.join("").trim();
|
|
3951
|
+
if (!delta) continue;
|
|
3952
|
+
var termInfo = tm.list().find(function(t) { return t.id === termId; });
|
|
3953
|
+
var termTitle = termInfo ? termInfo.title : "Terminal " + termId;
|
|
3954
|
+
var header;
|
|
3955
|
+
if (delta.length > TERM_CONTEXT_MAX) {
|
|
3956
|
+
var head = delta.slice(0, TERM_HEAD_SIZE);
|
|
3957
|
+
var tail = delta.slice(-TERM_TAIL_SIZE);
|
|
3958
|
+
var omittedBytes = delta.length - TERM_HEAD_SIZE - TERM_TAIL_SIZE;
|
|
3959
|
+
var omittedLines = delta.slice(TERM_HEAD_SIZE, delta.length - TERM_TAIL_SIZE).split("\n").length;
|
|
3960
|
+
delta = head + "\n\n... (" + omittedLines + " lines / " + Math.round(omittedBytes / 1024) + "KB omitted) ...\n\n" + tail;
|
|
3961
|
+
header = "[New terminal output from " + termTitle + " (large output, head+tail shown)]";
|
|
3962
|
+
} else {
|
|
3963
|
+
header = "[New terminal output from " + termTitle + "]";
|
|
3964
|
+
}
|
|
3965
|
+
termContextParts.push(header + "\n```\n" + delta + "\n```");
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
if (termContextParts.length > 0) {
|
|
3970
|
+
fullText = termContextParts.join("\n\n") + "\n\n" + fullText;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3838
3974
|
if (!session.isProcessing) {
|
|
3839
3975
|
session.isProcessing = true;
|
|
3840
3976
|
onProcessingChanged();
|
package/lib/public/app.js
CHANGED
|
@@ -6,15 +6,16 @@ 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
14
|
import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
|
|
15
|
+
import { initContextSources, updateTerminalList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
|
|
15
16
|
import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
|
|
16
17
|
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';
|
|
18
|
+
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
19
|
import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
|
|
19
20
|
import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './modules/project-settings.js';
|
|
20
21
|
import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
|
|
@@ -2468,7 +2469,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
2468
2469
|
}
|
|
2469
2470
|
|
|
2470
2471
|
function accumulateUsage(cost, usage) {
|
|
2471
|
-
|
|
2472
|
+
// cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
|
|
2473
|
+
// Assign directly instead of summing to avoid overcounting.
|
|
2474
|
+
if (cost != null) sessionUsage.cost = cost;
|
|
2472
2475
|
if (usage) {
|
|
2473
2476
|
sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
|
|
2474
2477
|
sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
|
|
@@ -2666,7 +2669,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
2666
2669
|
}
|
|
2667
2670
|
|
|
2668
2671
|
function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
|
|
2669
|
-
|
|
2672
|
+
// cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
|
|
2673
|
+
if (cost != null) contextData.cost = cost;
|
|
2670
2674
|
// Use latest turn values (not cumulative) since each turn's input_tokens
|
|
2671
2675
|
// already includes the full conversation context up to that point
|
|
2672
2676
|
if (usage) {
|
|
@@ -3660,23 +3664,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
3660
3664
|
suggestionChipsEl.innerHTML = "";
|
|
3661
3665
|
var chip = document.createElement("button");
|
|
3662
3666
|
chip.className = "suggestion-chip";
|
|
3663
|
-
chip.innerHTML =
|
|
3664
|
-
'<span class="suggestion-chip-
|
|
3665
|
-
'<span class="suggestion-chip-text">' + escapeHtml(suggestion) + '</span></span>' +
|
|
3666
|
-
'<span class="suggestion-chip-edit">' + iconHtml("pencil") + '</span>';
|
|
3667
|
+
chip.innerHTML = iconHtml("sparkles") +
|
|
3668
|
+
'<span class="suggestion-chip-text">' + escapeHtml(suggestion) + '</span>';
|
|
3667
3669
|
chip.addEventListener("click", function () {
|
|
3668
3670
|
inputEl.value = suggestion;
|
|
3669
3671
|
hideSuggestionChips();
|
|
3670
3672
|
sendMessage();
|
|
3671
3673
|
});
|
|
3672
|
-
chip.querySelector(".suggestion-chip-edit").addEventListener("click", function (e) {
|
|
3673
|
-
e.stopPropagation();
|
|
3674
|
-
inputEl.value = suggestion;
|
|
3675
|
-
inputEl.focus();
|
|
3676
|
-
inputEl.select();
|
|
3677
|
-
autoResize();
|
|
3678
|
-
hideSuggestionChips();
|
|
3679
|
-
});
|
|
3680
3674
|
suggestionChipsEl.appendChild(chip);
|
|
3681
3675
|
suggestionChipsEl.classList.remove("hidden");
|
|
3682
3676
|
refreshIcons();
|
|
@@ -3718,6 +3712,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
3718
3712
|
setStatus("connected");
|
|
3719
3713
|
if (!loopActive) enableMainInput();
|
|
3720
3714
|
resetUsage();
|
|
3715
|
+
resetTurnMetaCost();
|
|
3721
3716
|
resetContext();
|
|
3722
3717
|
// Clear header indicators
|
|
3723
3718
|
clearRateLimitIndicator();
|
|
@@ -4711,6 +4706,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4711
4706
|
break;
|
|
4712
4707
|
|
|
4713
4708
|
case "rewind_complete":
|
|
4709
|
+
onRewindComplete();
|
|
4714
4710
|
setRewindMode(false);
|
|
4715
4711
|
var rewindText = "Rewound to earlier point. Files have been restored.";
|
|
4716
4712
|
if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
|
|
@@ -4719,6 +4715,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4719
4715
|
break;
|
|
4720
4716
|
|
|
4721
4717
|
case "rewind_error":
|
|
4718
|
+
onRewindError();
|
|
4722
4719
|
clearPendingRewindUuid();
|
|
4723
4720
|
addSystemMessage(msg.text || "Rewind failed.", true);
|
|
4724
4721
|
break;
|
|
@@ -4791,6 +4788,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4791
4788
|
|
|
4792
4789
|
case "term_list":
|
|
4793
4790
|
handleTermList(msg);
|
|
4791
|
+
updateTerminalList(msg.terminals);
|
|
4792
|
+
break;
|
|
4793
|
+
|
|
4794
|
+
case "context_sources_state":
|
|
4795
|
+
handleContextSourcesState(msg);
|
|
4794
4796
|
break;
|
|
4795
4797
|
|
|
4796
4798
|
case "term_created":
|
|
@@ -4980,6 +4982,22 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4980
4982
|
// Update mate sidebar if currently viewing this mate
|
|
4981
4983
|
if (dmMode && dmTargetUser && dmTargetUser.isMate && dmTargetUser.id === msg.mate.id) {
|
|
4982
4984
|
updateMateSidebarProfile(msg.mate);
|
|
4985
|
+
// Sync dmTargetUser so subsequent renders use fresh data
|
|
4986
|
+
var mp2 = msg.mate.profile || {};
|
|
4987
|
+
dmTargetUser.displayName = mp2.displayName || msg.mate.name || dmTargetUser.displayName;
|
|
4988
|
+
dmTargetUser.avatarStyle = mp2.avatarStyle || dmTargetUser.avatarStyle;
|
|
4989
|
+
dmTargetUser.avatarSeed = mp2.avatarSeed || dmTargetUser.avatarSeed;
|
|
4990
|
+
dmTargetUser.avatarColor = mp2.avatarColor || dmTargetUser.avatarColor;
|
|
4991
|
+
dmTargetUser.avatarCustom = mp2.avatarCustom || "";
|
|
4992
|
+
dmTargetUser.profile = mp2;
|
|
4993
|
+
// Refresh body dataset so new chat bubbles use the updated avatar
|
|
4994
|
+
document.body.dataset.mateAvatarUrl = mateAvatarUrl(dmTargetUser, 36);
|
|
4995
|
+
document.body.dataset.mateName = mp2.displayName || msg.mate.name || "";
|
|
4996
|
+
// Update existing chat bubble avatars
|
|
4997
|
+
var mateAvis = document.querySelectorAll(".dm-bubble-avatar-mate");
|
|
4998
|
+
for (var mbi = 0; mbi < mateAvis.length; mbi++) {
|
|
4999
|
+
mateAvis[mbi].src = document.body.dataset.mateAvatarUrl;
|
|
5000
|
+
}
|
|
4983
5001
|
}
|
|
4984
5002
|
// Update DM header if currently chatting with this mate
|
|
4985
5003
|
if (dmMode && dmTargetUser && dmTargetUser.id === msg.mate.id) {
|
|
@@ -5741,6 +5759,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
5741
5759
|
fileViewerEl: $("file-viewer"),
|
|
5742
5760
|
});
|
|
5743
5761
|
|
|
5762
|
+
// --- Context Sources ---
|
|
5763
|
+
initContextSources({
|
|
5764
|
+
get ws() { return ws; },
|
|
5765
|
+
get connected() { return connected; },
|
|
5766
|
+
});
|
|
5767
|
+
|
|
5744
5768
|
// --- Playbook Engine ---
|
|
5745
5769
|
initPlaybook();
|
|
5746
5770
|
|
package/lib/public/css/input.css
CHANGED
|
@@ -271,6 +271,165 @@
|
|
|
271
271
|
border-color: var(--error);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/* ==========================================================================
|
|
275
|
+
Context Sources — chips above input
|
|
276
|
+
========================================================================== */
|
|
277
|
+
|
|
278
|
+
#context-sources-bar {
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
flex-wrap: wrap;
|
|
282
|
+
gap: 4px;
|
|
283
|
+
padding: 0 8px 4px;
|
|
284
|
+
position: relative;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#context-sources-add {
|
|
288
|
+
display: inline-flex;
|
|
289
|
+
align-items: center;
|
|
290
|
+
gap: 6px;
|
|
291
|
+
padding: 6px 12px;
|
|
292
|
+
border-radius: 6px;
|
|
293
|
+
border: 1px dashed var(--border);
|
|
294
|
+
background: transparent;
|
|
295
|
+
color: var(--text-dimmer);
|
|
296
|
+
font-family: inherit;
|
|
297
|
+
font-size: 12px;
|
|
298
|
+
font-weight: 500;
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#context-sources-add .lucide { width: 12px; height: 12px; }
|
|
304
|
+
|
|
305
|
+
#context-sources-add:hover {
|
|
306
|
+
color: var(--text-secondary);
|
|
307
|
+
border-color: var(--text-dimmer);
|
|
308
|
+
background: var(--sidebar-hover);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#context-sources-chips {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
flex-wrap: wrap;
|
|
315
|
+
gap: 4px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.context-chip {
|
|
319
|
+
display: inline-flex;
|
|
320
|
+
align-items: stretch;
|
|
321
|
+
padding: 0;
|
|
322
|
+
border-radius: 8px;
|
|
323
|
+
background: var(--bg-alt);
|
|
324
|
+
color: var(--text);
|
|
325
|
+
font-size: 13px;
|
|
326
|
+
font-weight: 500;
|
|
327
|
+
line-height: 1;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
border: 1px solid var(--border);
|
|
330
|
+
transition: border-color 0.15s;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.context-chip-label {
|
|
334
|
+
display: inline-flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
gap: 6px;
|
|
337
|
+
padding: 6px 10px 6px 12px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.context-chip-label .lucide { width: 14px; height: 14px; flex-shrink: 0; color: var(--accent); }
|
|
341
|
+
|
|
342
|
+
.context-chip-remove {
|
|
343
|
+
display: inline-flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
justify-content: center;
|
|
346
|
+
width: 30px;
|
|
347
|
+
border: none;
|
|
348
|
+
border-left: 1px solid var(--border);
|
|
349
|
+
background: transparent;
|
|
350
|
+
color: var(--text-muted);
|
|
351
|
+
cursor: pointer;
|
|
352
|
+
padding: 0;
|
|
353
|
+
border-radius: 0 8px 8px 0;
|
|
354
|
+
transition: color 0.15s, background 0.15s;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.context-chip-remove:hover {
|
|
358
|
+
color: var(--text);
|
|
359
|
+
background: rgba(var(--overlay-rgb), 0.08);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.context-chip-remove .lucide { width: 14px; height: 14px; }
|
|
363
|
+
|
|
364
|
+
#context-sources-picker.hidden { display: none; }
|
|
365
|
+
|
|
366
|
+
#context-sources-picker {
|
|
367
|
+
position: absolute;
|
|
368
|
+
bottom: calc(100% + 4px);
|
|
369
|
+
left: 0;
|
|
370
|
+
min-width: 200px;
|
|
371
|
+
background: var(--sidebar-bg);
|
|
372
|
+
border: 1px solid var(--border);
|
|
373
|
+
border-radius: 10px;
|
|
374
|
+
padding: 4px 0;
|
|
375
|
+
box-shadow: 0 4px 12px rgba(var(--shadow-rgb), 0.15);
|
|
376
|
+
z-index: 200;
|
|
377
|
+
animation: ctxPickerAppear 0.12s ease-out;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@keyframes ctxPickerAppear {
|
|
381
|
+
from { opacity: 0; transform: scale(0.95); }
|
|
382
|
+
to { opacity: 1; transform: scale(1); }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.context-picker-section-label {
|
|
386
|
+
font-size: 10px;
|
|
387
|
+
font-weight: 600;
|
|
388
|
+
color: var(--text-dimmer);
|
|
389
|
+
text-transform: uppercase;
|
|
390
|
+
letter-spacing: 0.5px;
|
|
391
|
+
padding: 8px 12px 4px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.context-picker-item {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 8px;
|
|
398
|
+
width: 100%;
|
|
399
|
+
padding: 8px 12px;
|
|
400
|
+
font-size: 13px;
|
|
401
|
+
color: var(--text-secondary);
|
|
402
|
+
background: none;
|
|
403
|
+
border: none;
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
transition: background 0.15s;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.context-picker-item:hover {
|
|
409
|
+
background: rgba(var(--overlay-rgb), 0.05);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.context-picker-item .lucide { width: 14px; height: 14px; flex-shrink: 0; }
|
|
413
|
+
|
|
414
|
+
.context-picker-check {
|
|
415
|
+
margin-left: auto;
|
|
416
|
+
width: 14px;
|
|
417
|
+
height: 14px;
|
|
418
|
+
color: var(--accent);
|
|
419
|
+
display: none;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.context-picker-item.active .context-picker-check {
|
|
423
|
+
display: block;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.context-picker-empty {
|
|
427
|
+
padding: 12px;
|
|
428
|
+
color: var(--text-dimmer);
|
|
429
|
+
font-size: 13px;
|
|
430
|
+
text-align: center;
|
|
431
|
+
}
|
|
432
|
+
|
|
274
433
|
/* ==========================================================================
|
|
275
434
|
Input Area — Claude-style unified container
|
|
276
435
|
========================================================================== */
|
|
@@ -521,98 +680,53 @@
|
|
|
521
680
|
#suggestion-chips {
|
|
522
681
|
display: flex;
|
|
523
682
|
flex-wrap: wrap;
|
|
524
|
-
gap:
|
|
525
|
-
padding:
|
|
683
|
+
gap: 4px;
|
|
684
|
+
padding: 4px 6px;
|
|
526
685
|
position: absolute;
|
|
527
686
|
bottom: 100%;
|
|
528
687
|
left: 0;
|
|
529
688
|
right: 0;
|
|
530
689
|
z-index: 5;
|
|
531
|
-
background: transparent;
|
|
532
690
|
}
|
|
533
691
|
|
|
534
692
|
#suggestion-chips.hidden { display: none; }
|
|
535
693
|
|
|
536
694
|
.suggestion-chip {
|
|
537
695
|
display: inline-flex;
|
|
538
|
-
align-items:
|
|
539
|
-
|
|
540
|
-
|
|
696
|
+
align-items: center;
|
|
697
|
+
gap: 5px;
|
|
698
|
+
padding: 4px 10px 4px 8px;
|
|
699
|
+
border-radius: 6px;
|
|
541
700
|
border: 1px solid var(--border);
|
|
542
|
-
background: var(--
|
|
701
|
+
background: rgba(var(--overlay-rgb), 0.08);
|
|
543
702
|
color: var(--text-secondary);
|
|
544
|
-
font-size:
|
|
703
|
+
font-size: 12px;
|
|
545
704
|
font-family: inherit;
|
|
546
705
|
cursor: pointer;
|
|
547
|
-
transition: border-color 0.15s;
|
|
706
|
+
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
|
548
707
|
text-align: left;
|
|
549
708
|
max-width: 100%;
|
|
550
|
-
line-height: 1.
|
|
551
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
709
|
+
line-height: 1.2;
|
|
552
710
|
}
|
|
553
711
|
|
|
554
712
|
.suggestion-chip:hover {
|
|
555
713
|
border-color: var(--accent);
|
|
714
|
+
background: var(--accent-bg);
|
|
715
|
+
color: var(--accent);
|
|
556
716
|
}
|
|
557
717
|
|
|
558
718
|
.suggestion-chip .lucide {
|
|
559
|
-
width:
|
|
560
|
-
height:
|
|
719
|
+
width: 12px;
|
|
720
|
+
height: 12px;
|
|
561
721
|
flex-shrink: 0;
|
|
562
722
|
color: var(--accent);
|
|
563
723
|
}
|
|
564
724
|
|
|
565
|
-
.suggestion-chip-send {
|
|
566
|
-
display: inline-flex;
|
|
567
|
-
align-items: center;
|
|
568
|
-
gap: 5px;
|
|
569
|
-
flex: 1;
|
|
570
|
-
min-width: 0;
|
|
571
|
-
padding: 8px 10px 8px 14px;
|
|
572
|
-
border-radius: 16px 0 0 16px;
|
|
573
|
-
transition: background 0.15s, color 0.15s;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
.suggestion-chip-send:hover {
|
|
577
|
-
background: var(--accent-bg);
|
|
578
|
-
color: var(--accent);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
725
|
.suggestion-chip-text {
|
|
582
726
|
flex: 1;
|
|
583
727
|
min-width: 0;
|
|
584
728
|
}
|
|
585
729
|
|
|
586
|
-
.suggestion-chip-edit {
|
|
587
|
-
display: inline-flex;
|
|
588
|
-
align-items: center;
|
|
589
|
-
justify-content: center;
|
|
590
|
-
padding: 8px 12px;
|
|
591
|
-
border-left: 1px solid var(--border);
|
|
592
|
-
border-radius: 0 16px 16px 0;
|
|
593
|
-
background: rgba(128, 128, 128, 0.07);
|
|
594
|
-
cursor: pointer;
|
|
595
|
-
transition: background 0.15s, border-color 0.15s;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.suggestion-chip:hover .suggestion-chip-edit {
|
|
599
|
-
border-left-color: var(--accent);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
.suggestion-chip-edit:hover {
|
|
603
|
-
background: var(--accent-bg);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
.suggestion-chip-edit .lucide {
|
|
607
|
-
width: 14px;
|
|
608
|
-
height: 14px;
|
|
609
|
-
color: var(--text-secondary);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
.suggestion-chip-edit:hover .lucide {
|
|
613
|
-
color: var(--accent);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
730
|
/* ==========================================================================
|
|
617
731
|
Animations
|
|
618
732
|
========================================================================== */
|
package/lib/public/css/menus.css
CHANGED
|
@@ -474,6 +474,13 @@
|
|
|
474
474
|
|
|
475
475
|
#config-chip .lucide { width: 10px; height: 10px; }
|
|
476
476
|
#config-chip .config-chip-icon { display: none; }
|
|
477
|
+
|
|
478
|
+
@media (max-width: 1000px) {
|
|
479
|
+
#config-chip .config-chip-icon { display: inline-flex; width: 16px; height: 16px; }
|
|
480
|
+
#config-chip-label { display: none; }
|
|
481
|
+
#config-chip .lucide:last-child { display: none; }
|
|
482
|
+
#config-chip { padding: 0 6px; }
|
|
483
|
+
}
|
|
477
484
|
#config-chip:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.06); }
|
|
478
485
|
#config-chip.active { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.06); }
|
|
479
486
|
|
|
@@ -257,6 +257,16 @@
|
|
|
257
257
|
letter-spacing: 0.5px;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/* --- Section labels --- */
|
|
261
|
+
.sidebar-section-label {
|
|
262
|
+
font-size: 11px;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
color: var(--text-dimmer);
|
|
265
|
+
text-transform: uppercase;
|
|
266
|
+
letter-spacing: 0.5px;
|
|
267
|
+
padding: 4px 12px 2px;
|
|
268
|
+
}
|
|
269
|
+
|
|
260
270
|
/* --- Tools section --- */
|
|
261
271
|
#sidebar-tools {
|
|
262
272
|
flex-shrink: 0;
|
package/lib/public/index.html
CHANGED
|
@@ -415,6 +415,13 @@
|
|
|
415
415
|
<div id="input-wrapper">
|
|
416
416
|
<div id="mention-menu"></div>
|
|
417
417
|
<div id="slash-menu"></div>
|
|
418
|
+
<div id="context-sources-bar">
|
|
419
|
+
<div id="context-sources-chips"></div>
|
|
420
|
+
<button id="context-sources-add" type="button" title="Add context source"><i data-lucide="plus"></i><span>Context Sources</span></button>
|
|
421
|
+
<div id="context-sources-picker" class="hidden">
|
|
422
|
+
<div class="context-picker-section" id="context-picker-terminals"></div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
418
425
|
<div id="suggestion-chips" class="hidden"></div>
|
|
419
426
|
<div id="input-row">
|
|
420
427
|
<div id="context-mini" class="hidden">
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Context Sources — attach terminal output (and future browser tabs) as context for Claude
|
|
2
|
+
|
|
3
|
+
var ctx = null;
|
|
4
|
+
var activeSourceIds = new Set();
|
|
5
|
+
var terminalList = []; // synced from terminal module's term_list
|
|
6
|
+
|
|
7
|
+
export function initContextSources(_ctx) {
|
|
8
|
+
ctx = _ctx;
|
|
9
|
+
|
|
10
|
+
var addBtn = document.getElementById("context-sources-add");
|
|
11
|
+
var picker = document.getElementById("context-sources-picker");
|
|
12
|
+
|
|
13
|
+
addBtn.addEventListener("click", function(e) {
|
|
14
|
+
e.stopPropagation();
|
|
15
|
+
if (picker.classList.contains("hidden")) {
|
|
16
|
+
renderPicker();
|
|
17
|
+
picker.classList.remove("hidden");
|
|
18
|
+
document.addEventListener("click", closePicker, true);
|
|
19
|
+
} else {
|
|
20
|
+
closePicker();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
picker.addEventListener("click", function(e) {
|
|
25
|
+
e.stopPropagation();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function closePicker() {
|
|
30
|
+
var picker = document.getElementById("context-sources-picker");
|
|
31
|
+
picker.classList.add("hidden");
|
|
32
|
+
document.removeEventListener("click", closePicker, true);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Restore state from server
|
|
36
|
+
export function handleContextSourcesState(msg) {
|
|
37
|
+
var saved = msg.active || [];
|
|
38
|
+
activeSourceIds = new Set(saved);
|
|
39
|
+
renderChips();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Save active sources to server
|
|
43
|
+
function saveToServer() {
|
|
44
|
+
if (ctx && ctx.ws && ctx.connected) {
|
|
45
|
+
ctx.ws.send(JSON.stringify({
|
|
46
|
+
type: "context_sources_save",
|
|
47
|
+
active: Array.from(activeSourceIds)
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Called when term_list arrives from server
|
|
53
|
+
export function updateTerminalList(terminals) {
|
|
54
|
+
terminalList = terminals || [];
|
|
55
|
+
|
|
56
|
+
// Remove active sources that no longer exist
|
|
57
|
+
var changed = false;
|
|
58
|
+
for (var id of activeSourceIds) {
|
|
59
|
+
if (id.startsWith("term:")) {
|
|
60
|
+
var termId = parseInt(id.split(":")[1], 10);
|
|
61
|
+
var found = false;
|
|
62
|
+
for (var i = 0; i < terminalList.length; i++) {
|
|
63
|
+
if (terminalList[i].id === termId) { found = true; break; }
|
|
64
|
+
}
|
|
65
|
+
if (!found) {
|
|
66
|
+
activeSourceIds.delete(id);
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (changed) saveToServer();
|
|
73
|
+
renderChips();
|
|
74
|
+
|
|
75
|
+
// If picker is open, re-render it
|
|
76
|
+
var picker = document.getElementById("context-sources-picker");
|
|
77
|
+
if (!picker.classList.contains("hidden")) {
|
|
78
|
+
renderPicker();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toggleSource(sourceId) {
|
|
83
|
+
if (activeSourceIds.has(sourceId)) {
|
|
84
|
+
activeSourceIds.delete(sourceId);
|
|
85
|
+
} else {
|
|
86
|
+
activeSourceIds.add(sourceId);
|
|
87
|
+
}
|
|
88
|
+
saveToServer();
|
|
89
|
+
renderChips();
|
|
90
|
+
renderPicker();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeSource(sourceId) {
|
|
94
|
+
activeSourceIds.delete(sourceId);
|
|
95
|
+
saveToServer();
|
|
96
|
+
renderChips();
|
|
97
|
+
|
|
98
|
+
var picker = document.getElementById("context-sources-picker");
|
|
99
|
+
if (!picker.classList.contains("hidden")) {
|
|
100
|
+
renderPicker();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderChips() {
|
|
105
|
+
var container = document.getElementById("context-sources-chips");
|
|
106
|
+
container.innerHTML = "";
|
|
107
|
+
|
|
108
|
+
for (var id of activeSourceIds) {
|
|
109
|
+
var chip = document.createElement("div");
|
|
110
|
+
chip.className = "context-chip";
|
|
111
|
+
|
|
112
|
+
var label = getSourceLabel(id);
|
|
113
|
+
var iconName = getSourceIcon(id);
|
|
114
|
+
|
|
115
|
+
var labelEl = document.createElement("span");
|
|
116
|
+
labelEl.className = "context-chip-label";
|
|
117
|
+
labelEl.innerHTML =
|
|
118
|
+
'<i data-lucide="' + iconName + '"></i>' +
|
|
119
|
+
'<span>' + escapeHtml(label) + '</span>';
|
|
120
|
+
chip.appendChild(labelEl);
|
|
121
|
+
|
|
122
|
+
var removeBtn = document.createElement("button");
|
|
123
|
+
removeBtn.type = "button";
|
|
124
|
+
removeBtn.className = "context-chip-remove";
|
|
125
|
+
removeBtn.title = "Remove";
|
|
126
|
+
removeBtn.innerHTML = '<i data-lucide="minus"></i>';
|
|
127
|
+
removeBtn.setAttribute("data-source-id", id);
|
|
128
|
+
removeBtn.addEventListener("click", function(e) {
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
removeSource(this.getAttribute("data-source-id"));
|
|
131
|
+
if (typeof lucide !== "undefined") lucide.createIcons();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
chip.appendChild(removeBtn);
|
|
135
|
+
container.appendChild(chip);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Update add button label
|
|
139
|
+
var addBtn = document.getElementById("context-sources-add");
|
|
140
|
+
var labelSpan = addBtn.querySelector("span");
|
|
141
|
+
if (activeSourceIds.size > 0) {
|
|
142
|
+
labelSpan.textContent = "";
|
|
143
|
+
labelSpan.style.display = "none";
|
|
144
|
+
} else {
|
|
145
|
+
labelSpan.textContent = "Context Sources";
|
|
146
|
+
labelSpan.style.display = "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof lucide !== "undefined") lucide.createIcons();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderPicker() {
|
|
153
|
+
var section = document.getElementById("context-picker-terminals");
|
|
154
|
+
section.innerHTML = "";
|
|
155
|
+
|
|
156
|
+
var sectionLabel = document.createElement("div");
|
|
157
|
+
sectionLabel.className = "context-picker-section-label";
|
|
158
|
+
sectionLabel.textContent = "Terminals";
|
|
159
|
+
section.appendChild(sectionLabel);
|
|
160
|
+
|
|
161
|
+
if (terminalList.length === 0) {
|
|
162
|
+
var empty = document.createElement("div");
|
|
163
|
+
empty.className = "context-picker-empty";
|
|
164
|
+
empty.textContent = "No terminals open";
|
|
165
|
+
section.appendChild(empty);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (var i = 0; i < terminalList.length; i++) {
|
|
170
|
+
var term = terminalList[i];
|
|
171
|
+
var sourceId = "term:" + term.id;
|
|
172
|
+
var isActive = activeSourceIds.has(sourceId);
|
|
173
|
+
|
|
174
|
+
var item = document.createElement("div");
|
|
175
|
+
item.className = "context-picker-item" + (isActive ? " active" : "");
|
|
176
|
+
item.setAttribute("data-source-id", sourceId);
|
|
177
|
+
|
|
178
|
+
item.innerHTML =
|
|
179
|
+
'<i data-lucide="square-terminal"></i>' +
|
|
180
|
+
'<span>' + escapeHtml(term.title || ("Terminal " + term.id)) + '</span>' +
|
|
181
|
+
'<i data-lucide="check" class="context-picker-check"></i>';
|
|
182
|
+
|
|
183
|
+
item.addEventListener("click", function() {
|
|
184
|
+
toggleSource(this.getAttribute("data-source-id"));
|
|
185
|
+
if (typeof lucide !== "undefined") lucide.createIcons();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
section.appendChild(item);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (typeof lucide !== "undefined") lucide.createIcons();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getSourceLabel(id) {
|
|
195
|
+
if (id.startsWith("term:")) {
|
|
196
|
+
var termId = parseInt(id.split(":")[1], 10);
|
|
197
|
+
for (var i = 0; i < terminalList.length; i++) {
|
|
198
|
+
if (terminalList[i].id === termId) {
|
|
199
|
+
return terminalList[i].title || ("Terminal " + termId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return "Terminal " + termId;
|
|
203
|
+
}
|
|
204
|
+
return id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getSourceIcon(id) {
|
|
208
|
+
if (id.startsWith("term:")) return "square-terminal";
|
|
209
|
+
return "circle";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Get active source IDs (for use when sending messages)
|
|
213
|
+
export function getActiveSources() {
|
|
214
|
+
return Array.from(activeSourceIds);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if any sources are active
|
|
218
|
+
export function hasActiveSources() {
|
|
219
|
+
return activeSourceIds.size > 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function escapeHtml(str) {
|
|
223
|
+
var div = document.createElement("div");
|
|
224
|
+
div.textContent = str;
|
|
225
|
+
return div.innerHTML;
|
|
226
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
@@ -28,6 +28,7 @@ function createTerminalManager(opts) {
|
|
|
28
28
|
pty: pty,
|
|
29
29
|
scrollback: [],
|
|
30
30
|
scrollbackSize: 0,
|
|
31
|
+
totalBytesWritten: 0,
|
|
31
32
|
cols: cols || 80,
|
|
32
33
|
rows: rows || 24,
|
|
33
34
|
title: "Terminal " + id,
|
|
@@ -38,11 +39,13 @@ function createTerminalManager(opts) {
|
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
pty.onData(function (data) {
|
|
41
|
-
// Buffer scrollback
|
|
42
|
-
|
|
42
|
+
// Buffer scrollback with timestamps
|
|
43
|
+
var ts = Date.now();
|
|
44
|
+
session.scrollback.push({ ts: ts, data: data });
|
|
43
45
|
session.scrollbackSize += data.length;
|
|
46
|
+
session.totalBytesWritten += data.length;
|
|
44
47
|
while (session.scrollbackSize > SCROLLBACK_MAX && session.scrollback.length > 1) {
|
|
45
|
-
session.scrollbackSize -= session.scrollback[0].length;
|
|
48
|
+
session.scrollbackSize -= session.scrollback[0].data.length;
|
|
46
49
|
session.scrollback.shift();
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -81,7 +84,7 @@ function createTerminalManager(opts) {
|
|
|
81
84
|
|
|
82
85
|
// Replay scrollback only for newly attached clients
|
|
83
86
|
if (!alreadySubscribed && session.scrollback.length > 0) {
|
|
84
|
-
var replay = session.scrollback.join("");
|
|
87
|
+
var replay = session.scrollback.map(function(c) { return c.data; }).join("");
|
|
85
88
|
sendTo(ws, { type: "term_output", id: id, data: replay });
|
|
86
89
|
}
|
|
87
90
|
|
|
@@ -176,6 +179,18 @@ function createTerminalManager(opts) {
|
|
|
176
179
|
return result;
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
function getScrollback(id) {
|
|
183
|
+
var session = terminals.get(id);
|
|
184
|
+
if (!session) return null;
|
|
185
|
+
var content = session.scrollback.map(function(c) { return c.data; }).join("");
|
|
186
|
+
return {
|
|
187
|
+
content: content,
|
|
188
|
+
chunks: session.scrollback,
|
|
189
|
+
totalBytesWritten: session.totalBytesWritten,
|
|
190
|
+
bufferStart: session.totalBytesWritten - content.length
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
179
194
|
function destroyAll() {
|
|
180
195
|
for (var session of terminals.values()) {
|
|
181
196
|
if (session.pty) {
|
|
@@ -196,6 +211,7 @@ function createTerminalManager(opts) {
|
|
|
196
211
|
close: close,
|
|
197
212
|
rename: rename,
|
|
198
213
|
list: list,
|
|
214
|
+
getScrollback: getScrollback,
|
|
199
215
|
destroyAll: destroyAll,
|
|
200
216
|
};
|
|
201
217
|
}
|