clay-server 2.5.1 → 2.7.0

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.
Files changed (41) hide show
  1. package/bin/claude-relay.js +6 -0
  2. package/bin/cli.js +77 -11
  3. package/lib/cli-sessions.js +2 -7
  4. package/lib/config.js +86 -7
  5. package/lib/daemon.js +279 -6
  6. package/lib/ipc.js +12 -0
  7. package/lib/notes.js +4 -3
  8. package/lib/project.js +1174 -28
  9. package/lib/public/app.js +879 -31
  10. package/lib/public/css/diff.css +15 -4
  11. package/lib/public/css/filebrowser.css +363 -3
  12. package/lib/public/css/icon-strip.css +317 -1
  13. package/lib/public/css/input.css +127 -50
  14. package/lib/public/css/loop.css +780 -0
  15. package/lib/public/css/messages.css +1 -1
  16. package/lib/public/css/mobile-nav.css +6 -2
  17. package/lib/public/css/rewind.css +23 -0
  18. package/lib/public/css/server-settings.css +67 -20
  19. package/lib/public/css/sidebar.css +10 -4
  20. package/lib/public/css/skills.css +789 -0
  21. package/lib/public/css/sticky-notes.css +486 -0
  22. package/lib/public/css/title-bar.css +157 -7
  23. package/lib/public/index.html +366 -55
  24. package/lib/public/modules/diff.js +3 -3
  25. package/lib/public/modules/filebrowser.js +169 -45
  26. package/lib/public/modules/input.js +123 -56
  27. package/lib/public/modules/project-settings.js +906 -0
  28. package/lib/public/modules/qrcode.js +23 -26
  29. package/lib/public/modules/server-settings.js +449 -53
  30. package/lib/public/modules/sidebar.js +732 -1
  31. package/lib/public/modules/skills.js +794 -0
  32. package/lib/public/modules/sticky-notes.js +617 -52
  33. package/lib/public/modules/terminal.js +7 -0
  34. package/lib/public/modules/theme.js +9 -19
  35. package/lib/public/modules/tools.js +16 -2
  36. package/lib/public/style.css +2 -0
  37. package/lib/sdk-bridge.js +46 -7
  38. package/lib/server.js +305 -1
  39. package/lib/sessions.js +11 -5
  40. package/lib/utils.js +18 -0
  41. package/package.json +3 -2
@@ -9,33 +9,32 @@ function getShareUrl() {
9
9
  return url;
10
10
  }
11
11
 
12
- export function initQrCode() {
13
- var $ = function (id) { return document.getElementById(id); };
14
- var qrBtn = $("qr-btn");
15
- var qrOverlay = $("qr-overlay");
16
- var qrCanvas = $("qr-canvas");
17
- var qrUrl = $("qr-url");
12
+ export function triggerShare() {
13
+ var url = getShareUrl();
18
14
 
19
- qrBtn.addEventListener("click", function (e) {
20
- e.stopPropagation();
21
- var url = getShareUrl();
15
+ // Use Web Share API if available
16
+ if (navigator.share) {
17
+ navigator.share({ title: document.title || "Clay", url: url }).catch(function () {});
18
+ return;
19
+ }
22
20
 
23
- // Use Web Share API if available
24
- if (navigator.share) {
25
- navigator.share({ title: document.title || "Clay", url: url }).catch(function () {});
26
- return;
27
- }
21
+ // Fallback: show QR overlay
22
+ var qrOverlay = document.getElementById("qr-overlay");
23
+ var qrCanvas = document.getElementById("qr-canvas");
24
+ var qrUrl = document.getElementById("qr-url");
28
25
 
29
- // Fallback: show QR overlay
30
- var qr = qrcode(0, "M");
31
- qr.addData(url);
32
- qr.make();
33
- qrCanvas.innerHTML = qr.createSvgTag(5, 0);
34
- qrUrl.innerHTML = url + '<span class="qr-hint">click to copy</span>';
26
+ var qr = qrcode(0, "M");
27
+ qr.addData(url);
28
+ qr.make();
29
+ qrCanvas.innerHTML = qr.createSvgTag(5, 0);
30
+ qrUrl.innerHTML = url + '<span class="qr-hint">click to copy</span>';
35
31
 
36
- qrOverlay.classList.remove("hidden");
37
- qrBtn.classList.add("active");
38
- });
32
+ qrOverlay.classList.remove("hidden");
33
+ }
34
+
35
+ export function initQrCode() {
36
+ var qrOverlay = document.getElementById("qr-overlay");
37
+ var qrUrl = document.getElementById("qr-url");
39
38
 
40
39
  // click URL to copy
41
40
  qrUrl.addEventListener("click", function () {
@@ -52,11 +51,10 @@ export function initQrCode() {
52
51
 
53
52
  qrOverlay.addEventListener("click", function () {
54
53
  qrOverlay.classList.add("hidden");
55
- qrBtn.classList.remove("active");
56
54
  });
57
55
 
58
56
  // prevent closing when clicking the inner card
59
- $("qr-overlay-inner").addEventListener("click", function (e) {
57
+ document.getElementById("qr-overlay-inner").addEventListener("click", function (e) {
60
58
  e.stopPropagation();
61
59
  });
62
60
 
@@ -64,7 +62,6 @@ export function initQrCode() {
64
62
  document.addEventListener("keydown", function (e) {
65
63
  if (e.key === "Escape" && !qrOverlay.classList.contains("hidden")) {
66
64
  qrOverlay.classList.add("hidden");
67
- qrBtn.classList.remove("active");
68
65
  }
69
66
  });
70
67
  }
@@ -1,7 +1,7 @@
1
1
  // server-settings.js — Full-screen server settings overlay
2
2
  import { refreshIcons } from './icons.js';
3
- import { getCurrentTheme, openSettingsThemePicker } from './theme.js';
4
- import { showToast } from './utils.js';
3
+ import { showToast, copyToClipboard } from './utils.js';
4
+ import { parseEnvString, looksLikeEnv } from './project-settings.js';
5
5
 
6
6
  var ctx = null;
7
7
  var settingsEl = null;
@@ -11,6 +11,26 @@ var navItems = null;
11
11
  var sections = null;
12
12
  var statsTimer = null;
13
13
 
14
+ var SS_MODE_OPTIONS = [
15
+ { value: "default", label: "Default", desc: "Claude asks for permission before running tools and editing files." },
16
+ { value: "plan", label: "Plan", desc: "Claude creates a plan first and asks for approval before making changes." },
17
+ { value: "acceptEdits", label: "Auto-accept edits", desc: "File edits are applied automatically. Claude still asks before running commands." },
18
+ ];
19
+
20
+ var SS_EFFORT_LEVELS = [
21
+ { value: "low", desc: "Quick, concise responses. Best for simple questions." },
22
+ { value: "medium", desc: "Balanced responses with moderate reasoning. Good for most tasks." },
23
+ { value: "high", desc: "Thorough responses with deeper analysis. Good for complex tasks." },
24
+ { value: "max", desc: "Maximum reasoning depth. Best for the most difficult problems." },
25
+ ];
26
+
27
+ var SS_MODEL_DESCRIPTIONS = {
28
+ "default": "Automatically selects the best model for the task.",
29
+ "sonnet": "Fast and capable. Great balance of speed and intelligence.",
30
+ "haiku": "Fastest model. Best for quick tasks and simple questions.",
31
+ "opus": "Most powerful model. Best for complex reasoning and analysis.",
32
+ };
33
+
14
34
  export function initServerSettings(appCtx) {
15
35
  ctx = appCtx;
16
36
  settingsEl = document.getElementById("server-settings");
@@ -47,18 +67,22 @@ export function initServerSettings(appCtx) {
47
67
  });
48
68
  }
49
69
 
50
- // Context view buttons
51
- var contextViewEl = document.getElementById("settings-context-view");
52
- if (contextViewEl) {
53
- var btns = contextViewEl.querySelectorAll(".settings-btn-option");
54
- for (var b = 0; b < btns.length; b++) {
55
- btns[b].addEventListener("click", function () {
56
- var view = this.dataset.view;
57
- if (ctx.setContextView) ctx.setContextView(view);
58
- if (ctx.applyContextView) ctx.applyContextView(view);
59
- updateContextViewButtons();
70
+ // Copyable command blocks
71
+ var copyables = settingsEl.querySelectorAll(".settings-copyable");
72
+ for (var c = 0; c < copyables.length; c++) {
73
+ copyables[c].addEventListener("click", function () {
74
+ var text = this.dataset.copy;
75
+ if (!text) return;
76
+ var btn = this.querySelector(".settings-copy-btn");
77
+ copyToClipboard(text).then(function () {
78
+ if (btn) {
79
+ var orig = btn.textContent;
80
+ btn.textContent = "✓";
81
+ setTimeout(function () { btn.textContent = orig; }, 1500);
82
+ }
83
+ showToast("Copied to clipboard");
60
84
  });
61
- }
85
+ });
62
86
  }
63
87
 
64
88
  // Notification toggles
@@ -104,7 +128,7 @@ export function initServerSettings(appCtx) {
104
128
  if (!model) return;
105
129
  var ws = ctx.ws;
106
130
  if (ws && ws.readyState === 1) {
107
- ws.send(JSON.stringify({ type: "set_model", model: model }));
131
+ ws.send(JSON.stringify({ type: "set_server_default_model", model: model }));
108
132
  }
109
133
  });
110
134
 
@@ -140,6 +164,34 @@ export function initServerSettings(appCtx) {
140
164
  });
141
165
  }
142
166
 
167
+ // Global CLAUDE.md: save button
168
+ var ssClaudeMdSave = document.getElementById("ss-claudemd-save");
169
+ if (ssClaudeMdSave) {
170
+ ssClaudeMdSave.addEventListener("click", function () { saveGlobalClaudeMd(); });
171
+ }
172
+
173
+ // Shared environment: add button
174
+ var ssEnvAddBtn = document.getElementById("ss-env-add-btn");
175
+ if (ssEnvAddBtn) {
176
+ ssEnvAddBtn.addEventListener("click", function () {
177
+ addSharedEnvRow("", "", true);
178
+ autoSaveSharedEnv();
179
+ });
180
+ }
181
+
182
+ // Restart server
183
+ var restartBtn = document.getElementById("settings-restart-btn");
184
+ if (restartBtn) {
185
+ restartBtn.addEventListener("click", function () {
186
+ var ws = ctx.ws;
187
+ if (ws && ws.readyState === 1) {
188
+ restartBtn.disabled = true;
189
+ restartBtn.textContent = "Restarting...";
190
+ ws.send(JSON.stringify({ type: "restart_server" }));
191
+ }
192
+ });
193
+ }
194
+
143
195
  // Shutdown server
144
196
  var shutdownInput = document.getElementById("settings-shutdown-input");
145
197
  var shutdownBtn = document.getElementById("settings-shutdown-btn");
@@ -184,6 +236,10 @@ function switchSection(sectionName) {
184
236
  var isActive2 = sections[j].dataset.section === sectionName;
185
237
  sections[j].classList.toggle("active", isActive2);
186
238
  }
239
+
240
+ // Lazy-load section data
241
+ if (sectionName === "claudemd") loadGlobalClaudeMd();
242
+ if (sectionName === "environment") loadSharedEnv();
187
243
  }
188
244
 
189
245
  function openSettings() {
@@ -192,6 +248,7 @@ function openSettings() {
192
248
  refreshIcons(settingsEl);
193
249
  populateSettings();
194
250
  requestDaemonConfig();
251
+ resetRestartButton();
195
252
  resetShutdownForm();
196
253
 
197
254
  // Start periodic stats refresh
@@ -199,6 +256,13 @@ function openSettings() {
199
256
  statsTimer = setInterval(requestStats, 5000);
200
257
  }
201
258
 
259
+ function resetRestartButton() {
260
+ var btn = document.getElementById("settings-restart-btn");
261
+ var errorEl = document.getElementById("settings-restart-error");
262
+ if (btn) { btn.disabled = false; btn.innerHTML = '<i data-lucide="refresh-cw" style="width:14px;height:14px;vertical-align:-2px;margin-right:4px;"></i>Restart Server'; }
263
+ if (errorEl) errorEl.classList.add("hidden");
264
+ }
265
+
202
266
  function resetShutdownForm() {
203
267
  var input = document.getElementById("settings-shutdown-input");
204
268
  var btn = document.getElementById("settings-shutdown-btn");
@@ -262,14 +326,11 @@ function populateSettings() {
262
326
  // Sync notification toggles
263
327
  syncNotifToggles();
264
328
 
265
- // Theme
266
- updateThemeDisplay();
267
-
268
- // Context view
269
- updateContextViewButtons();
270
-
271
- // Models
329
+ // Session defaults
272
330
  updateModelList();
331
+ updateModeList();
332
+ updateEffortBar();
333
+ updateSsBetaCard();
273
334
  }
274
335
 
275
336
  function syncNotifToggles() {
@@ -285,39 +346,26 @@ function syncNotifToggles() {
285
346
  }
286
347
  }
287
348
 
288
- function updateThemeDisplay() {
289
- var container = document.getElementById("settings-theme-picker-container");
290
- if (container) {
291
- openSettingsThemePicker(container);
349
+ function ssGetModelDesc(model) {
350
+ if (!model) return "";
351
+ var lower = model.toLowerCase();
352
+ for (var key in SS_MODEL_DESCRIPTIONS) {
353
+ if (lower.indexOf(key) !== -1) return SS_MODEL_DESCRIPTIONS[key];
292
354
  }
355
+ return "";
293
356
  }
294
357
 
295
- function updateContextViewButtons() {
296
- var view = "off";
297
- try { view = localStorage.getItem("clay-context-view") || "off"; } catch (e) {}
298
- var btns = document.querySelectorAll("#settings-context-view .settings-btn-option");
299
- for (var i = 0; i < btns.length; i++) {
300
- btns[i].classList.toggle("active", btns[i].dataset.view === view);
301
- }
358
+ function ssIsSonnetModel(model) {
359
+ if (!model) return false;
360
+ return model.toLowerCase().indexOf("sonnet") !== -1;
302
361
  }
303
362
 
304
363
  function updateModelList() {
305
364
  var listEl = document.getElementById("settings-model-list");
306
- var currentEl = document.getElementById("settings-current-model");
307
365
  if (!listEl) return;
308
366
 
309
367
  var models = ctx.currentModels || [];
310
- var currentModel = ctx._currentModelValue || "";
311
-
312
- // Look up display name for settings panel
313
- var displayName = currentModel;
314
- for (var j = 0; j < models.length; j++) {
315
- if (models[j].value === currentModel && models[j].displayName) {
316
- displayName = models[j].displayName;
317
- break;
318
- }
319
- }
320
- if (currentEl) currentEl.textContent = displayName || "-";
368
+ var currentModel = ctx.currentModel || ctx._currentModelValue || "";
321
369
 
322
370
  listEl.innerHTML = "";
323
371
  if (models.length === 0) {
@@ -326,15 +374,145 @@ function updateModelList() {
326
374
  }
327
375
 
328
376
  for (var i = 0; i < models.length; i++) {
329
- var m = models[i];
330
- var value = m.value || "";
331
- var label = m.displayName || value;
332
- var item = document.createElement("div");
333
- item.className = "settings-model-item";
334
- if (label === currentModel || value === currentModel) item.classList.add("active");
335
- item.dataset.model = value;
336
- item.textContent = label;
337
- listEl.appendChild(item);
377
+ (function (m) {
378
+ var value = m.value || "";
379
+ var label = m.displayName || value;
380
+ var item = document.createElement("div");
381
+ item.className = "settings-model-item";
382
+ if (value === currentModel) item.classList.add("active");
383
+ item.dataset.model = value;
384
+
385
+ var nameSpan = document.createElement("span");
386
+ nameSpan.className = "settings-model-name";
387
+ nameSpan.textContent = label;
388
+ item.appendChild(nameSpan);
389
+
390
+ var desc = ssGetModelDesc(value);
391
+ if (desc) {
392
+ var descSpan = document.createElement("span");
393
+ descSpan.className = "settings-model-desc";
394
+ descSpan.textContent = desc;
395
+ item.appendChild(descSpan);
396
+ }
397
+
398
+ item.addEventListener("click", function () {
399
+ var ws = ctx.ws;
400
+ if (ws && ws.readyState === 1) {
401
+ ws.send(JSON.stringify({ type: "set_model", model: value }));
402
+ }
403
+ var items = listEl.querySelectorAll(".settings-model-item");
404
+ for (var j = 0; j < items.length; j++) items[j].classList.remove("active");
405
+ item.classList.add("active");
406
+ updateSsBetaCard(value);
407
+ });
408
+
409
+ listEl.appendChild(item);
410
+ })(models[i]);
411
+ }
412
+ }
413
+
414
+ function updateModeList() {
415
+ var listEl = document.getElementById("ss-mode-list");
416
+ if (!listEl) return;
417
+
418
+ var currentMode = ctx.currentMode || "default";
419
+ listEl.innerHTML = "";
420
+
421
+ for (var i = 0; i < SS_MODE_OPTIONS.length; i++) {
422
+ (function (opt) {
423
+ var item = document.createElement("div");
424
+ item.className = "settings-model-item" + (opt.value === currentMode ? " active" : "");
425
+
426
+ var nameSpan = document.createElement("span");
427
+ nameSpan.className = "settings-model-name";
428
+ nameSpan.textContent = opt.label;
429
+ item.appendChild(nameSpan);
430
+
431
+ var descSpan = document.createElement("span");
432
+ descSpan.className = "settings-model-desc";
433
+ descSpan.textContent = opt.desc;
434
+ item.appendChild(descSpan);
435
+
436
+ item.addEventListener("click", function () {
437
+ var ws = ctx.ws;
438
+ if (ws && ws.readyState === 1) {
439
+ ws.send(JSON.stringify({ type: "set_server_default_mode", mode: opt.value }));
440
+ }
441
+ var items = listEl.querySelectorAll(".settings-model-item");
442
+ for (var j = 0; j < items.length; j++) items[j].classList.remove("active");
443
+ item.classList.add("active");
444
+ });
445
+
446
+ listEl.appendChild(item);
447
+ })(SS_MODE_OPTIONS[i]);
448
+ }
449
+ }
450
+
451
+ function updateEffortBar() {
452
+ var bar = document.getElementById("ss-effort-bar");
453
+ if (!bar) return;
454
+
455
+ var currentEffort = ctx.currentEffort || "medium";
456
+ bar.innerHTML = "";
457
+
458
+ for (var i = 0; i < SS_EFFORT_LEVELS.length; i++) {
459
+ (function (lvl) {
460
+ var btn = document.createElement("button");
461
+ btn.className = "settings-btn-option" + (lvl.value === currentEffort ? " active" : "");
462
+ btn.textContent = lvl.value.charAt(0).toUpperCase() + lvl.value.slice(1);
463
+ btn.title = lvl.desc;
464
+ btn.addEventListener("click", function () {
465
+ var ws = ctx.ws;
466
+ if (ws && ws.readyState === 1) {
467
+ ws.send(JSON.stringify({ type: "set_server_default_effort", effort: lvl.value }));
468
+ }
469
+ var btns = bar.querySelectorAll(".settings-btn-option");
470
+ for (var j = 0; j < btns.length; j++) btns[j].classList.remove("active");
471
+ btn.classList.add("active");
472
+ });
473
+ bar.appendChild(btn);
474
+ })(SS_EFFORT_LEVELS[i]);
475
+ }
476
+ }
477
+
478
+ function updateSsBetaCard(overrideModel) {
479
+ var model = overrideModel || ctx.currentModel || ctx._currentModelValue || "";
480
+ var card = document.getElementById("ss-beta-card");
481
+ if (card) {
482
+ card.style.display = ssIsSonnetModel(model) ? "" : "none";
483
+ }
484
+
485
+ var toggle = document.getElementById("ss-beta-1m");
486
+ if (toggle) {
487
+ var betas = ctx.currentBetas || [];
488
+ var hasBeta = false;
489
+ for (var i = 0; i < betas.length; i++) {
490
+ if (betas[i].indexOf("context-1m") !== -1) { hasBeta = true; break; }
491
+ }
492
+ toggle.checked = hasBeta;
493
+ toggle.onchange = function () {
494
+ ssToggleBeta1m(this.checked);
495
+ };
496
+ }
497
+ }
498
+
499
+ function ssToggleBeta1m(enable) {
500
+ var betas = ctx.currentBetas || [];
501
+ var newBetas;
502
+ if (enable) {
503
+ newBetas = betas.slice();
504
+ newBetas.push("context-1m-2025-08-07");
505
+ } else {
506
+ newBetas = [];
507
+ for (var i = 0; i < betas.length; i++) {
508
+ if (betas[i].indexOf("context-1m") === -1) {
509
+ newBetas.push(betas[i]);
510
+ }
511
+ }
512
+ }
513
+ var ws = ctx.ws;
514
+ if (ws && ws.readyState === 1) {
515
+ ws.send(JSON.stringify({ type: "set_betas", betas: newBetas }));
338
516
  }
339
517
  }
340
518
 
@@ -359,6 +537,9 @@ export function updateSettingsModels(current, models) {
359
537
  ctx._currentModelValue = current;
360
538
  if (isSettingsOpen()) {
361
539
  updateModelList();
540
+ updateModeList();
541
+ updateEffortBar();
542
+ updateSsBetaCard();
362
543
  }
363
544
  }
364
545
 
@@ -420,6 +601,25 @@ export function handleKeepAwakeChanged(msg) {
420
601
  if (keepAwakeToggle) keepAwakeToggle.checked = !!msg.keepAwake;
421
602
  }
422
603
 
604
+ export function handleRestartResult(msg) {
605
+ var restartBtn = document.getElementById("settings-restart-btn");
606
+ var errorEl = document.getElementById("settings-restart-error");
607
+
608
+ if (msg.ok) {
609
+ if (restartBtn) restartBtn.textContent = "Server restarting...";
610
+ showToast("Server is restarting...");
611
+ } else {
612
+ if (restartBtn) {
613
+ restartBtn.textContent = "Restart Server";
614
+ restartBtn.disabled = false;
615
+ }
616
+ if (errorEl) {
617
+ errorEl.textContent = msg.error || "Restart failed";
618
+ errorEl.classList.remove("hidden");
619
+ }
620
+ }
621
+ }
622
+
423
623
  export function handleShutdownResult(msg) {
424
624
  var shutdownInput = document.getElementById("settings-shutdown-input");
425
625
  var shutdownBtn = document.getElementById("settings-shutdown-btn");
@@ -492,6 +692,202 @@ function updatePinStatus(enabled) {
492
692
  if (actionLabel) actionLabel.textContent = enabled ? "Change PIN" : "Set PIN";
493
693
  }
494
694
 
695
+ // ===== Global CLAUDE.md =====
696
+ function loadGlobalClaudeMd() {
697
+ var editor = document.getElementById("ss-claudemd-editor");
698
+ var status = document.getElementById("ss-claudemd-status");
699
+ var saveStatus = document.getElementById("ss-claudemd-save-status");
700
+ if (saveStatus) saveStatus.textContent = "";
701
+ if (status) status.textContent = "Loading...";
702
+
703
+ var ws = ctx.ws;
704
+ if (ws && ws.readyState === 1) {
705
+ ws.send(JSON.stringify({ type: "read_global_claude_md" }));
706
+ }
707
+ }
708
+
709
+ export function handleGlobalClaudeMdRead(msg) {
710
+ var editor = document.getElementById("ss-claudemd-editor");
711
+ var status = document.getElementById("ss-claudemd-status");
712
+ if (!editor) return;
713
+
714
+ if (msg.error) {
715
+ editor.value = "";
716
+ if (status) status.textContent = "No global CLAUDE.md found. Save to create one.";
717
+ } else {
718
+ editor.value = msg.content || "";
719
+ if (status) status.textContent = "";
720
+ }
721
+ }
722
+
723
+ function saveGlobalClaudeMd() {
724
+ var editor = document.getElementById("ss-claudemd-editor");
725
+ var saveStatus = document.getElementById("ss-claudemd-save-status");
726
+ if (!editor) return;
727
+
728
+ var ws = ctx.ws;
729
+ if (ws && ws.readyState === 1) {
730
+ ws.send(JSON.stringify({ type: "write_global_claude_md", content: editor.value }));
731
+ if (saveStatus) saveStatus.textContent = "Saving...";
732
+ }
733
+ }
734
+
735
+ export function handleGlobalClaudeMdWrite(msg) {
736
+ var saveStatus = document.getElementById("ss-claudemd-save-status");
737
+ if (!saveStatus) return;
738
+ if (msg.ok) {
739
+ saveStatus.textContent = "Saved";
740
+ setTimeout(function () { saveStatus.textContent = ""; }, 2000);
741
+ } else {
742
+ saveStatus.textContent = "Error: " + (msg.error || "Failed to save");
743
+ }
744
+ }
745
+
746
+ // ===== Shared Environment Variables =====
747
+ var sharedEnvSaveTimer = null;
748
+
749
+ function loadSharedEnv() {
750
+ var saveStatus = document.getElementById("ss-env-save-status");
751
+ if (saveStatus) saveStatus.textContent = "";
752
+
753
+ var ws = ctx.ws;
754
+ if (ws && ws.readyState === 1) {
755
+ ws.send(JSON.stringify({ type: "get_shared_env" }));
756
+ }
757
+ }
758
+
759
+ export function handleSharedEnv(msg) {
760
+ var list = document.getElementById("ss-env-list");
761
+ if (!list) return;
762
+ list.innerHTML = "";
763
+
764
+ var pairs = parseEnvString(msg.envrc || "");
765
+ for (var i = 0; i < pairs.length; i++) {
766
+ addSharedEnvRow(pairs[i].key, pairs[i].value, false);
767
+ }
768
+ refreshIcons();
769
+ }
770
+
771
+ export function handleSharedEnvSaved(msg) {
772
+ var saveStatus = document.getElementById("ss-env-save-status");
773
+ if (!saveStatus) return;
774
+ if (msg.ok) {
775
+ saveStatus.textContent = "Saved";
776
+ setTimeout(function () { saveStatus.textContent = ""; }, 2000);
777
+ } else {
778
+ saveStatus.textContent = "Error: " + (msg.error || "Failed to save");
779
+ }
780
+ }
781
+
782
+ function buildSharedEnvString() {
783
+ var list = document.getElementById("ss-env-list");
784
+ if (!list) return "";
785
+ var rows = list.querySelectorAll(".ps-env-row");
786
+ var lines = [];
787
+ for (var i = 0; i < rows.length; i++) {
788
+ var keyInput = rows[i].querySelector(".ps-env-key");
789
+ var valInput = rows[i].querySelector(".ps-env-val");
790
+ var key = keyInput ? keyInput.value.trim() : "";
791
+ var val = valInput ? valInput.value : "";
792
+ if (key) lines.push("export " + key + "=" + val);
793
+ }
794
+ return lines.join("\n");
795
+ }
796
+
797
+ function addSharedEnvRow(key, value, focus) {
798
+ var list = document.getElementById("ss-env-list");
799
+ if (!list) return;
800
+
801
+ var row = document.createElement("div");
802
+ row.className = "ps-env-row";
803
+
804
+ var keyInput = document.createElement("input");
805
+ keyInput.type = "text";
806
+ keyInput.className = "ps-env-key";
807
+ keyInput.placeholder = "KEY";
808
+ keyInput.value = key;
809
+ keyInput.spellcheck = false;
810
+ keyInput.autocomplete = "off";
811
+
812
+ var valInput = document.createElement("input");
813
+ valInput.type = "text";
814
+ valInput.className = "ps-env-val";
815
+ valInput.placeholder = "value";
816
+ valInput.value = value;
817
+ valInput.spellcheck = false;
818
+ valInput.autocomplete = "off";
819
+
820
+ var delBtn = document.createElement("button");
821
+ delBtn.className = "ps-env-del";
822
+ delBtn.title = "Remove";
823
+ delBtn.innerHTML = '<i data-lucide="x"></i>';
824
+
825
+ delBtn.addEventListener("click", function () {
826
+ row.remove();
827
+ autoSaveSharedEnv();
828
+ });
829
+
830
+ keyInput.addEventListener("input", function () { autoSaveSharedEnv(); });
831
+ valInput.addEventListener("input", function () { autoSaveSharedEnv(); });
832
+
833
+ // Paste detection
834
+ keyInput.addEventListener("paste", function (e) {
835
+ var text = (e.clipboardData || window.clipboardData).getData("text");
836
+ if (text && looksLikeEnv(text)) {
837
+ e.preventDefault();
838
+ var pairs = parseEnvString(text);
839
+ if (pairs.length > 0) {
840
+ keyInput.value = pairs[0].key;
841
+ valInput.value = pairs[0].value;
842
+ for (var p = 1; p < pairs.length; p++) {
843
+ addSharedEnvRow(pairs[p].key, pairs[p].value, false);
844
+ }
845
+ autoSaveSharedEnv();
846
+ }
847
+ }
848
+ });
849
+
850
+ valInput.addEventListener("paste", function (e) {
851
+ var text = (e.clipboardData || window.clipboardData).getData("text");
852
+ if (text && text.indexOf("\n") !== -1 && text.indexOf("=") !== -1) {
853
+ e.preventDefault();
854
+ var pairs = parseEnvString(text);
855
+ if (pairs.length > 0) {
856
+ keyInput.value = pairs[0].key;
857
+ valInput.value = pairs[0].value;
858
+ for (var p = 1; p < pairs.length; p++) {
859
+ addSharedEnvRow(pairs[p].key, pairs[p].value, false);
860
+ }
861
+ autoSaveSharedEnv();
862
+ }
863
+ }
864
+ });
865
+
866
+ row.appendChild(keyInput);
867
+ row.appendChild(valInput);
868
+ row.appendChild(delBtn);
869
+ list.appendChild(row);
870
+ refreshIcons();
871
+
872
+ if (focus) keyInput.focus();
873
+ }
874
+
875
+ function autoSaveSharedEnv() {
876
+ if (sharedEnvSaveTimer) clearTimeout(sharedEnvSaveTimer);
877
+ sharedEnvSaveTimer = setTimeout(function () {
878
+ var envrc = buildSharedEnvString();
879
+ var ws = ctx.ws;
880
+ if (ws && ws.readyState === 1) {
881
+ ws.send(JSON.stringify({ type: "set_shared_env", envrc: envrc }));
882
+ var saveStatus = document.getElementById("ss-env-save-status");
883
+ if (saveStatus) {
884
+ saveStatus.textContent = "Saved";
885
+ setTimeout(function () { saveStatus.textContent = ""; }, 2000);
886
+ }
887
+ }
888
+ }, 800);
889
+ }
890
+
495
891
  function formatBytes(n) {
496
892
  if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
497
893
  if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";