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 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-send">' + iconHtml("sparkles") +
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
 
@@ -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: 6px;
525
- padding: 8px 6px;
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: stretch;
539
- padding: 0;
540
- border-radius: 16px;
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(--bg-alt);
701
+ background: rgba(var(--overlay-rgb), 0.08);
543
702
  color: var(--text-secondary);
544
- font-size: 13px;
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.3;
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: 14px;
560
- height: 14px;
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
  ========================================================================== */
@@ -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;
@@ -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
+ }
@@ -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
- session.scrollback.push(data);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.26.0-beta.3",
3
+ "version": "2.26.0-beta.4",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",