clay-server 2.26.0-beta.3 → 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 +127 -0
- package/lib/public/app.js +14 -12
- 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/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
|
|
|
@@ -3374,6 +3401,14 @@ function createProjectContext(opts) {
|
|
|
3374
3401
|
if (msg.id) {
|
|
3375
3402
|
tm.close(msg.id);
|
|
3376
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
|
+
}
|
|
3377
3412
|
}
|
|
3378
3413
|
return;
|
|
3379
3414
|
}
|
|
@@ -3386,6 +3421,13 @@ function createProjectContext(opts) {
|
|
|
3386
3421
|
return;
|
|
3387
3422
|
}
|
|
3388
3423
|
|
|
3424
|
+
// --- Context Sources ---
|
|
3425
|
+
if (msg.type === "context_sources_save") {
|
|
3426
|
+
var activeIds = msg.active || [];
|
|
3427
|
+
saveContextSources(slug, activeIds);
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3389
3431
|
// --- Scheduled tasks permission gate ---
|
|
3390
3432
|
if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
|
|
3391
3433
|
msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
|
|
@@ -3844,6 +3886,91 @@ function createProjectContext(opts) {
|
|
|
3844
3886
|
fullText = mentionPrefix + "\n\n" + fullText;
|
|
3845
3887
|
}
|
|
3846
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
|
+
|
|
3847
3974
|
if (!session.isProcessing) {
|
|
3848
3975
|
session.isProcessing = true;
|
|
3849
3976
|
onProcessingChanged();
|
package/lib/public/app.js
CHANGED
|
@@ -12,6 +12,7 @@ import { initInput, clearPendingImages, handleInputSync, autoResize, builtinComm
|
|
|
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
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';
|
|
@@ -3663,23 +3664,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
3663
3664
|
suggestionChipsEl.innerHTML = "";
|
|
3664
3665
|
var chip = document.createElement("button");
|
|
3665
3666
|
chip.className = "suggestion-chip";
|
|
3666
|
-
chip.innerHTML =
|
|
3667
|
-
'<span class="suggestion-chip-
|
|
3668
|
-
'<span class="suggestion-chip-text">' + escapeHtml(suggestion) + '</span></span>' +
|
|
3669
|
-
'<span class="suggestion-chip-edit">' + iconHtml("pencil") + '</span>';
|
|
3667
|
+
chip.innerHTML = iconHtml("sparkles") +
|
|
3668
|
+
'<span class="suggestion-chip-text">' + escapeHtml(suggestion) + '</span>';
|
|
3670
3669
|
chip.addEventListener("click", function () {
|
|
3671
3670
|
inputEl.value = suggestion;
|
|
3672
3671
|
hideSuggestionChips();
|
|
3673
3672
|
sendMessage();
|
|
3674
3673
|
});
|
|
3675
|
-
chip.querySelector(".suggestion-chip-edit").addEventListener("click", function (e) {
|
|
3676
|
-
e.stopPropagation();
|
|
3677
|
-
inputEl.value = suggestion;
|
|
3678
|
-
inputEl.focus();
|
|
3679
|
-
inputEl.select();
|
|
3680
|
-
autoResize();
|
|
3681
|
-
hideSuggestionChips();
|
|
3682
|
-
});
|
|
3683
3674
|
suggestionChipsEl.appendChild(chip);
|
|
3684
3675
|
suggestionChipsEl.classList.remove("hidden");
|
|
3685
3676
|
refreshIcons();
|
|
@@ -4797,6 +4788,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4797
4788
|
|
|
4798
4789
|
case "term_list":
|
|
4799
4790
|
handleTermList(msg);
|
|
4791
|
+
updateTerminalList(msg.terminals);
|
|
4792
|
+
break;
|
|
4793
|
+
|
|
4794
|
+
case "context_sources_state":
|
|
4795
|
+
handleContextSourcesState(msg);
|
|
4800
4796
|
break;
|
|
4801
4797
|
|
|
4802
4798
|
case "term_created":
|
|
@@ -5763,6 +5759,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
5763
5759
|
fileViewerEl: $("file-viewer"),
|
|
5764
5760
|
});
|
|
5765
5761
|
|
|
5762
|
+
// --- Context Sources ---
|
|
5763
|
+
initContextSources({
|
|
5764
|
+
get ws() { return ws; },
|
|
5765
|
+
get connected() { return connected; },
|
|
5766
|
+
});
|
|
5767
|
+
|
|
5766
5768
|
// --- Playbook Engine ---
|
|
5767
5769
|
initPlaybook();
|
|
5768
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
|
+
}
|
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
|
}
|