labgate 0.5.32 → 0.5.33

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/dist/lib/ui.html CHANGED
@@ -5213,6 +5213,7 @@
5213
5213
  border: 1px solid var(--border-color);
5214
5214
  background: var(--card-bg);
5215
5215
  overflow: hidden;
5216
+ flex-shrink: 0;
5216
5217
  }
5217
5218
  .terminal-chat-msg.user {
5218
5219
  margin-left: auto;
@@ -5275,15 +5276,18 @@
5275
5276
  .terminal-chat-msg.pending .terminal-chat-msg-header::after {
5276
5277
  content: '';
5277
5278
  display: inline-block;
5278
- width: 8px;
5279
- height: 8px;
5279
+ width: 6px;
5280
+ height: 6px;
5280
5281
  margin-left: 6px;
5281
5282
  border-radius: 50%;
5282
- border: 1.5px solid var(--border-color);
5283
- border-top-color: var(--accent);
5284
- animation: terminalHistorySpin 0.75s linear infinite;
5283
+ background: var(--accent);
5284
+ animation: chatPulse 1.4s ease-in-out infinite;
5285
5285
  vertical-align: middle;
5286
5286
  }
5287
+ @keyframes chatPulse {
5288
+ 0%, 100% { opacity: 0.25; transform: scale(0.85); }
5289
+ 50% { opacity: 1; transform: scale(1); }
5290
+ }
5287
5291
 
5288
5292
  /* ── Tool use cards (inline in chat transcript) ── */
5289
5293
  .terminal-chat-tool {
@@ -5300,6 +5304,7 @@
5300
5304
  color: var(--text-secondary);
5301
5305
  transition: opacity 0.2s;
5302
5306
  border-left: 2px solid var(--border-color);
5307
+ flex-shrink: 0;
5303
5308
  }
5304
5309
  .terminal-chat-tool + .terminal-chat-tool {
5305
5310
  margin-top: -6px;
@@ -5330,12 +5335,11 @@
5330
5335
  }
5331
5336
  .terminal-chat-tool.pending .tool-icon {
5332
5337
  display: inline-block;
5333
- width: 10px;
5334
- height: 10px;
5338
+ width: 6px;
5339
+ height: 6px;
5335
5340
  border-radius: 50%;
5336
- border: 1.5px solid var(--border-color);
5337
- border-top-color: var(--accent);
5338
- animation: terminalHistorySpin 0.75s linear infinite;
5341
+ background: var(--accent);
5342
+ animation: chatPulse 1.4s ease-in-out infinite;
5339
5343
  font-size: 0;
5340
5344
  line-height: 0;
5341
5345
  }
@@ -5352,6 +5356,49 @@
5352
5356
  color: #dc2626;
5353
5357
  }
5354
5358
 
5359
+ /* ── Collapsible tool groups ── */
5360
+ .terminal-chat-tool-group {
5361
+ display: flex;
5362
+ flex-direction: column;
5363
+ gap: 0;
5364
+ flex-shrink: 0;
5365
+ }
5366
+ .terminal-chat-tool-group .terminal-chat-tool + .terminal-chat-tool {
5367
+ margin-top: -6px;
5368
+ }
5369
+ .terminal-chat-tool-group .tool-group-collapsed {
5370
+ display: none;
5371
+ }
5372
+ .terminal-chat-tool-group.expanded .tool-group-collapsed {
5373
+ display: flex;
5374
+ }
5375
+ .tool-group-toggle {
5376
+ display: flex;
5377
+ align-items: center;
5378
+ gap: 6px;
5379
+ padding: 2px 10px;
5380
+ margin-bottom: 2px;
5381
+ border: none;
5382
+ background: transparent;
5383
+ cursor: pointer;
5384
+ font-family: 'GeistMono', monospace;
5385
+ font-size: 0.625rem;
5386
+ color: var(--text-muted);
5387
+ border-left: 2px solid var(--border-color);
5388
+ transition: color 0.15s;
5389
+ }
5390
+ .tool-group-toggle:hover {
5391
+ color: var(--text-secondary);
5392
+ }
5393
+ .tool-group-toggle .toggle-caret {
5394
+ display: inline-block;
5395
+ transition: transform 0.15s;
5396
+ font-size: 0.5rem;
5397
+ }
5398
+ .terminal-chat-tool-group.expanded .tool-group-toggle .toggle-caret {
5399
+ transform: rotate(90deg);
5400
+ }
5401
+
5355
5402
  /* ── Rich content widgets (inline in chat transcript) ── */
5356
5403
  .terminal-chat-widget {
5357
5404
  max-width: min(820px, 100%);
@@ -6000,11 +6047,11 @@
6000
6047
  <div class="terminal-input-shell" id="terminalInputShell">
6001
6048
  <div class="terminal-input-toolbar">
6002
6049
  <div class="terminal-input-mode-toggle">
6003
- <button class="terminal-input-mode-btn active" id="terminalInputModeChatBtn" type="button" onclick="setTerminalInputMode('chat', { focus: true })">Chat</button>
6004
- <button class="terminal-input-mode-btn" id="terminalInputModeRawBtn" type="button" onclick="setTerminalInputMode('raw', { focus: true })">Raw</button>
6050
+ <button class="terminal-input-mode-btn" id="terminalInputModeChatBtn" type="button" onclick="setTerminalInputMode('chat', { focus: true })">Chat</button>
6051
+ <button class="terminal-input-mode-btn active" id="terminalInputModeRawBtn" type="button" onclick="setTerminalInputMode('raw', { focus: true })">Raw</button>
6005
6052
  </div>
6006
6053
  </div>
6007
- <textarea id="terminalChatInput" class="terminal-input-chat" placeholder="Type command or prompt. Enter sends, Shift+Enter inserts newline." onfocus="activateTerminalChatInput()" onkeydown="handleTerminalChatInputKeydown(event)" spellcheck="false"></textarea>
6054
+ <textarea id="terminalChatInput" class="terminal-input-chat" placeholder="Type command or prompt. Enter sends, Shift+Enter inserts newline." onkeydown="handleTerminalChatInputKeydown(event)" spellcheck="false"></textarea>
6008
6055
  <div class="terminal-input-actions">
6009
6056
  <span class="terminal-input-hint" id="terminalInputHint">Enter sends to terminal. Shift+Enter inserts newline.</span>
6010
6057
  <span class="terminal-input-spacer"></span>
@@ -6131,7 +6178,7 @@
6131
6178
  <div class="explorer-experiment-row">
6132
6179
  <select id="explorerAgentModeSelect" onchange="explorerOnAgentModeChanged()">
6133
6180
  <option value="stub">Agent: Stub (deterministic)</option>
6134
- <option value="claude_headless">Agent: Claude headless</option>
6181
+ <option value="claude_headless">Agent: Claude headless (beta)</option>
6135
6182
  </select>
6136
6183
  </div>
6137
6184
  <div class="explorer-experiment-row">
@@ -6319,6 +6366,16 @@
6319
6366
  </div>
6320
6367
  </div>
6321
6368
  </div>
6369
+ <div class="settings-section">
6370
+ <h3>Headless Chat <span style="display:inline-block;padding:1px 6px;border-radius:4px;background:var(--accent);color:#fff;font-size:0.625rem;font-weight:700;vertical-align:middle;margin-left:6px;letter-spacing:0.03em">BETA</span></h3>
6371
+ <p class="card-description">Enable a structured chat interface for Claude sessions running on Apptainer. This replaces the raw terminal view with a chat transcript showing tool calls and responses. <strong>Experimental</strong> &mdash; may not work reliably in all configurations.</p>
6372
+ <div class="field">
6373
+ <label class="toggle-row" style="display:flex;align-items:center;gap:10px;cursor:pointer">
6374
+ <input type="checkbox" id="headlessEnabledToggle" onchange="toggleHeadlessEnabled(this.checked)">
6375
+ <span>Enable headless chat mode</span>
6376
+ </label>
6377
+ </div>
6378
+ </div>
6322
6379
  </div>
6323
6380
 
6324
6381
  <!-- Network sub-panel -->
@@ -6675,7 +6732,8 @@ var webTerm = {
6675
6732
  outOfSyncOverlayDismissed: false,
6676
6733
  outOfSyncAutoReconnectTimer: 0,
6677
6734
  outOfSyncAutoReconnectCooldownUntil: 0,
6678
- inputMode: 'chat',
6735
+ inputMode: 'raw',
6736
+ headlessEnabled: false,
6679
6737
  headlessSocket: null,
6680
6738
  headlessRunning: false,
6681
6739
  headlessResumeBySession: {},
@@ -6709,7 +6767,8 @@ var AUTO_COPY_SELECTION_DEDUPE_WINDOW_MS = 1400;
6709
6767
  var autoCopySelectionState = {
6710
6768
  inFlight: false,
6711
6769
  lastText: '',
6712
- lastCopiedAt: 0
6770
+ lastCopiedAt: 0,
6771
+ lastToastAt: 0
6713
6772
  };
6714
6773
 
6715
6774
  function getCachedClaudeEmail() {
@@ -6950,7 +7009,19 @@ function getWebTerminalSessionRuntime(session) {
6950
7009
  return '';
6951
7010
  }
6952
7011
 
7012
+ function toggleHeadlessEnabled(enabled) {
7013
+ webTerm.headlessEnabled = !!enabled;
7014
+ try { localStorage.setItem('labgate_headless_enabled', enabled ? '1' : '0'); } catch(e) {}
7015
+ if (!enabled && webTerm.inputMode === 'chat') {
7016
+ setTerminalInputMode('raw', { focus: false });
7017
+ }
7018
+ updateTerminalInputModeUi();
7019
+ updateTerminalInputAvailability();
7020
+ updateTerminalPresentationMode();
7021
+ }
7022
+
6953
7023
  function isHeadlessClaudeChatSupported() {
7024
+ if (!webTerm.headlessEnabled) return false;
6954
7025
  var session = getAttachedWebTerminalSession();
6955
7026
  if (!session) return false;
6956
7027
  var agent = String(session.agent || '').toLowerCase();
@@ -7051,26 +7122,69 @@ function renderTerminalChatTranscript() {
7051
7122
  }
7052
7123
  var transcript = getHeadlessTranscript(sessionId);
7053
7124
  if (!transcript.length) {
7054
- el.innerHTML = '<div class="terminal-chat-empty">Claude headless chat is ready. Your prompts and responses will appear here.</div>';
7125
+ el.innerHTML = '<div class="terminal-chat-empty"><strong style="color:var(--accent)">Beta</strong> &mdash; Claude headless chat is ready. This feature is experimental and under active development. Your prompts and responses will appear here.</div>';
7055
7126
  return;
7056
7127
  }
7057
- var html = transcript.map(function(entry) {
7128
+ // Group consecutive tool entries for collapsible rendering
7129
+ var groups = [];
7130
+ var currentToolGroup = [];
7131
+ for (var ti = 0; ti < transcript.length; ti++) {
7132
+ var entry = transcript[ti];
7058
7133
  var role = String(entry.role || 'assistant');
7059
-
7060
- // Render tool entries as compact inline cards
7061
7134
  if (role === 'tool') {
7062
- var toolClasses = ['terminal-chat-tool'];
7063
- if (entry.toolDone) toolClasses.push('done');
7064
- else if (entry.toolError) toolClasses.push('error');
7065
- else toolClasses.push('pending');
7066
- var icon = entry.toolDone ? '\u2713' : entry.toolError ? '\u2717' : '\u26a1';
7067
- return '<div class="' + toolClasses.join(' ') + '">'
7068
- + '<span class="tool-icon">' + icon + '</span>'
7069
- + '<span class="tool-name">' + escapeHtml(String(entry.toolName || 'tool')) + '</span>'
7070
- + (entry.toolDetail ? '<span class="tool-detail">' + escapeHtml(String(entry.toolDetail)) + '</span>' : '')
7071
- + '</div>';
7135
+ currentToolGroup.push(entry);
7136
+ } else {
7137
+ if (currentToolGroup.length) {
7138
+ groups.push({ type: 'tools', items: currentToolGroup });
7139
+ currentToolGroup = [];
7140
+ }
7141
+ groups.push({ type: 'entry', entry: entry });
7142
+ }
7143
+ }
7144
+ if (currentToolGroup.length) {
7145
+ groups.push({ type: 'tools', items: currentToolGroup });
7146
+ }
7147
+
7148
+ var html = groups.map(function(group, gi) {
7149
+ if (group.type === 'tools') {
7150
+ var items = group.items;
7151
+ var VISIBLE_COUNT = 2;
7152
+ var needsCollapse = items.length > VISIBLE_COUNT + 1;
7153
+ var renderToolDiv = function(e) {
7154
+ var toolClasses = ['terminal-chat-tool'];
7155
+ if (e.toolDone) toolClasses.push('done');
7156
+ else if (e.toolError) toolClasses.push('error');
7157
+ else toolClasses.push('pending');
7158
+ var icon = e.toolDone ? '\u2713' : e.toolError ? '\u2717' : '\u26a1';
7159
+ return '<div class="' + toolClasses.join(' ') + '">'
7160
+ + '<span class="tool-icon">' + icon + '</span>'
7161
+ + '<span class="tool-name">' + escapeHtml(String(e.toolName || 'tool')) + '</span>'
7162
+ + (e.toolDetail ? '<span class="tool-detail">' + escapeHtml(String(e.toolDetail)) + '</span>' : '')
7163
+ + '</div>';
7164
+ };
7165
+ if (!needsCollapse) {
7166
+ return items.map(renderToolDiv).join('');
7167
+ }
7168
+ var hiddenCount = items.length - VISIBLE_COUNT;
7169
+ var groupId = 'tool-group-' + gi;
7170
+ var out = '<div class="terminal-chat-tool-group" id="' + groupId + '">';
7171
+ out += '<button type="button" class="tool-group-toggle" onclick="var g=document.getElementById(\'' + groupId + '\');g.classList.toggle(\'expanded\');this.querySelector(\'.toggle-label\').textContent=g.classList.contains(\'expanded\')?\'\u25BE Hide '+hiddenCount+' tool calls\':\'\u25B8 '+hiddenCount+' more tool calls\';this.querySelector(\'.toggle-caret\').textContent=g.classList.contains(\'expanded\')?\'\u25BE\':\'\u25B8\'">'
7172
+ + '<span class="toggle-caret">\u25B8</span>'
7173
+ + '<span class="toggle-label">' + hiddenCount + ' more tool calls</span>'
7174
+ + '</button>';
7175
+ for (var hi = 0; hi < items.length - VISIBLE_COUNT; hi++) {
7176
+ out += renderToolDiv(items[hi]).replace('class="terminal-chat-tool', 'class="terminal-chat-tool tool-group-collapsed');
7177
+ }
7178
+ for (var vi = items.length - VISIBLE_COUNT; vi < items.length; vi++) {
7179
+ out += renderToolDiv(items[vi]);
7180
+ }
7181
+ out += '</div>';
7182
+ return out;
7072
7183
  }
7073
7184
 
7185
+ var entry = group.entry;
7186
+ var role = String(entry.role || 'assistant');
7187
+
7074
7188
  // Render widget entries as rich content cards
7075
7189
  if (role === 'widget') {
7076
7190
  var widgetId = 'widget-' + entry.id;
@@ -8183,10 +8297,13 @@ function updateTerminalInputModeUi() {
8183
8297
  }
8184
8298
  var headless = isHeadlessClaudeChatAvailable();
8185
8299
 
8300
+ var modeToggle = inputShell ? inputShell.querySelector('.terminal-input-mode-toggle') : null;
8186
8301
  if (inputShell) {
8187
8302
  inputShell.classList.toggle('raw-mode', isRaw);
8188
8303
  inputShell.classList.toggle('out-of-sync', !!webTerm.outOfSync);
8189
8304
  }
8305
+ // Hide the Chat/Raw toggle entirely when headless is not enabled
8306
+ if (modeToggle) modeToggle.style.display = webTerm.headlessEnabled ? '' : 'none';
8190
8307
  if (chatBtn) chatBtn.classList.toggle('active', !isRaw && headlessCapable);
8191
8308
  if (rawBtn) rawBtn.classList.toggle('active', isRaw);
8192
8309
  if (hint) {
@@ -8992,6 +9109,33 @@ function ensureWebTerminalReady() {
8992
9109
  if (!clean) return;
8993
9110
  sendTerminalInputData(clean, { silent: true });
8994
9111
  });
9112
+ if (typeof webTerm.terminal.onSelectionChange === 'function') {
9113
+ webTerm.terminal.onSelectionChange(function() {
9114
+ runAutoCopySelection({
9115
+ getText: getTerminalSelectionText,
9116
+ allowFallback: true,
9117
+ allowTextareaFallback: false
9118
+ });
9119
+ });
9120
+ }
9121
+ if (typeof webTerm.terminal.attachCustomKeyEventHandler === 'function') {
9122
+ webTerm.terminal.attachCustomKeyEventHandler(function(event) {
9123
+ if (!isTerminalCopyShortcut(event)) return true;
9124
+ var selectedText = getTerminalSelectionText();
9125
+ if (!selectedText) return true;
9126
+ event.preventDefault();
9127
+ runAutoCopySelection({
9128
+ text: selectedText,
9129
+ allowEditable: true,
9130
+ force: true,
9131
+ allowFallback: true,
9132
+ fallbackRestoreFocus: false,
9133
+ notifyCopied: true,
9134
+ copiedToastMessage: 'Copied'
9135
+ });
9136
+ return false;
9137
+ });
9138
+ }
8995
9139
  webTerm.terminal.onResize(function(size) {
8996
9140
  if (!webTerm.socket || webTerm.socket.readyState !== WebSocket.OPEN) return;
8997
9141
  webTerm.socket.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
@@ -9694,6 +9838,11 @@ try {
9694
9838
  }
9695
9839
  } catch(e) {}
9696
9840
 
9841
+ // Restore headless chat preference
9842
+ try {
9843
+ webTerm.headlessEnabled = localStorage.getItem('labgate_headless_enabled') === '1';
9844
+ } catch(e) {}
9845
+
9697
9846
  applyExplorerDevModeVisibility();
9698
9847
 
9699
9848
  function saveSidebarState() {
@@ -10560,9 +10709,57 @@ function isEditableSelectionNode(node) {
10560
10709
  return false;
10561
10710
  }
10562
10711
 
10563
- function fallbackCopyTextToClipboard(text) {
10564
- if (!text) return false;
10712
+ function getTerminalSelectionText() {
10713
+ if (!webTerm || !webTerm.terminal) return '';
10714
+ try {
10715
+ if (typeof webTerm.terminal.hasSelection === 'function' && webTerm.terminal.hasSelection()) {
10716
+ return webTerm.terminal.getSelection() || '';
10717
+ }
10718
+ } catch (_errSelection) {}
10719
+ return '';
10720
+ }
10721
+
10722
+ function isTerminalCopyShortcut(event) {
10723
+ if (!event || event.type !== 'keydown') return false;
10724
+ var key = String(event.key || '').toLowerCase();
10725
+ if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && key === 'c') return true;
10726
+ if (event.ctrlKey && event.shiftKey && !event.altKey && key === 'c') return true;
10727
+ if (event.ctrlKey && !event.altKey && !event.metaKey && key === 'insert') return true;
10728
+ return false;
10729
+ }
10730
+
10731
+ function isNodeInsideTerminalView(node) {
10732
+ var el = node && node.nodeType === 1 ? node : (node && node.parentElement ? node.parentElement : null);
10733
+ while (el) {
10734
+ if (el.id === 'terminalView') return true;
10735
+ el = el.parentElement;
10736
+ }
10737
+ return false;
10738
+ }
10739
+
10740
+ function isTerminalSelectionContext() {
10741
+ var terminalSelection = getTerminalSelectionText();
10742
+ if (terminalSelection && String(terminalSelection).trim()) return true;
10565
10743
  var selection = window.getSelection();
10744
+ if (!selection || selection.isCollapsed) return false;
10745
+ return isNodeInsideTerminalView(selection.anchorNode) || isNodeInsideTerminalView(selection.focusNode);
10746
+ }
10747
+
10748
+ function maybeShowCopiedToast(opts) {
10749
+ var options = opts || {};
10750
+ var now = Date.now();
10751
+ var minIntervalMs = typeof options.minIntervalMs === 'number' ? options.minIntervalMs : 800;
10752
+ if ((now - autoCopySelectionState.lastToastAt) < minIntervalMs) return;
10753
+ autoCopySelectionState.lastToastAt = now;
10754
+ showToast(options.message || 'Copied', options.type || 'success');
10755
+ }
10756
+
10757
+ function fallbackCopyTextToClipboard(text, opts) {
10758
+ if (!text) return false;
10759
+ var options = opts || {};
10760
+ var preserveSelection = options.preserveSelection !== false;
10761
+ var restoreFocus = options.restoreFocus !== false;
10762
+ var selection = preserveSelection ? window.getSelection() : null;
10566
10763
  var preservedRanges = [];
10567
10764
  if (selection && typeof selection.rangeCount === 'number') {
10568
10765
  for (var i = 0; i < selection.rangeCount; i++) {
@@ -10595,15 +10792,40 @@ function fallbackCopyTextToClipboard(text) {
10595
10792
  try { selection.addRange(range); } catch (_errAdd) {}
10596
10793
  });
10597
10794
  }
10598
- if (activeElement && typeof activeElement.focus === 'function') {
10795
+ if (restoreFocus && activeElement && typeof activeElement.focus === 'function') {
10599
10796
  try { activeElement.focus(); } catch (_errFocus) {}
10600
10797
  }
10601
10798
  return ok;
10602
10799
  }
10603
10800
 
10801
+ function execCommandCopyTextToClipboard(text) {
10802
+ var rawText = typeof text === 'string' ? text : '';
10803
+ if (!rawText || typeof document.execCommand !== 'function') return false;
10804
+ var copied = false;
10805
+ function onCopy(event) {
10806
+ if (!event || !event.clipboardData || typeof event.clipboardData.setData !== 'function') return;
10807
+ try {
10808
+ event.clipboardData.setData('text/plain', rawText);
10809
+ event.preventDefault();
10810
+ copied = true;
10811
+ } catch (_errSetData) {}
10812
+ }
10813
+ document.addEventListener('copy', onCopy);
10814
+ try {
10815
+ copied = document.execCommand('copy') || copied;
10816
+ } catch (_errExec) {
10817
+ copied = copied || false;
10818
+ }
10819
+ document.removeEventListener('copy', onCopy);
10820
+ return copied;
10821
+ }
10822
+
10604
10823
  function writeTextToClipboard(text, opts) {
10605
10824
  var options = opts || {};
10606
10825
  var allowFallback = options.allowFallback !== false;
10826
+ var allowTextareaFallback = options.allowTextareaFallback !== false;
10827
+ var fallbackRestoreFocus = options.fallbackRestoreFocus !== false;
10828
+ var fallbackPreserveSelection = options.fallbackPreserveSelection !== false;
10607
10829
  var rawText = typeof text === 'string' ? text : '';
10608
10830
  if (!rawText) return Promise.resolve(false);
10609
10831
  if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
@@ -10611,11 +10833,21 @@ function writeTextToClipboard(text, opts) {
10611
10833
  return true;
10612
10834
  }).catch(function() {
10613
10835
  if (!allowFallback) return false;
10614
- return fallbackCopyTextToClipboard(rawText);
10836
+ if (execCommandCopyTextToClipboard(rawText)) return true;
10837
+ if (!allowTextareaFallback) return false;
10838
+ return fallbackCopyTextToClipboard(rawText, {
10839
+ restoreFocus: fallbackRestoreFocus,
10840
+ preserveSelection: fallbackPreserveSelection
10841
+ });
10615
10842
  });
10616
10843
  }
10617
10844
  if (!allowFallback) return Promise.resolve(false);
10618
- return Promise.resolve(fallbackCopyTextToClipboard(rawText));
10845
+ if (execCommandCopyTextToClipboard(rawText)) return Promise.resolve(true);
10846
+ if (!allowTextareaFallback) return Promise.resolve(false);
10847
+ return Promise.resolve(fallbackCopyTextToClipboard(rawText, {
10848
+ restoreFocus: fallbackRestoreFocus,
10849
+ preserveSelection: fallbackPreserveSelection
10850
+ }));
10619
10851
  }
10620
10852
 
10621
10853
  function runAutoCopySelection(opts) {
@@ -10632,11 +10864,14 @@ function runAutoCopySelection(opts) {
10632
10864
  }
10633
10865
  if (!text) {
10634
10866
  var selection = window.getSelection();
10635
- if (!selection || selection.isCollapsed) return;
10636
- if (!options.allowEditable && (isEditableSelectionNode(selection.anchorNode) || isEditableSelectionNode(selection.focusNode))) {
10637
- return;
10867
+ if (selection && !selection.isCollapsed) {
10868
+ if (!options.allowEditable && (isEditableSelectionNode(selection.anchorNode) || isEditableSelectionNode(selection.focusNode))) {
10869
+ return;
10870
+ }
10871
+ text = selection.toString();
10872
+ } else {
10873
+ text = getTerminalSelectionText();
10638
10874
  }
10639
- text = selection.toString();
10640
10875
  }
10641
10876
  if (!text) return;
10642
10877
  var normalized = String(text).replace(/\s+/g, ' ').trim();
@@ -10644,17 +10879,30 @@ function runAutoCopySelection(opts) {
10644
10879
 
10645
10880
  var now = Date.now();
10646
10881
  if (
10882
+ !options.force
10883
+ &&
10647
10884
  normalized === autoCopySelectionState.lastText
10648
10885
  && (now - autoCopySelectionState.lastCopiedAt) < AUTO_COPY_SELECTION_DEDUPE_WINDOW_MS
10649
10886
  ) {
10887
+ if (options.notifyCopied) {
10888
+ maybeShowCopiedToast({ message: options.copiedToastMessage || 'Copied' });
10889
+ }
10650
10890
  return;
10651
10891
  }
10652
10892
  if (autoCopySelectionState.inFlight) return;
10653
10893
  autoCopySelectionState.inFlight = true;
10654
- writeTextToClipboard(text, { allowFallback: true }).then(function(copied) {
10894
+ writeTextToClipboard(text, {
10895
+ allowFallback: options.allowFallback !== false,
10896
+ allowTextareaFallback: options.allowTextareaFallback !== false,
10897
+ fallbackRestoreFocus: options.fallbackRestoreFocus !== false,
10898
+ fallbackPreserveSelection: options.fallbackPreserveSelection !== false
10899
+ }).then(function(copied) {
10655
10900
  if (copied) {
10656
10901
  autoCopySelectionState.lastText = normalized;
10657
10902
  autoCopySelectionState.lastCopiedAt = Date.now();
10903
+ if (options.notifyCopied) {
10904
+ maybeShowCopiedToast({ message: options.copiedToastMessage || 'Copied' });
10905
+ }
10658
10906
  }
10659
10907
  autoCopySelectionState.inFlight = false;
10660
10908
  }).catch(function() {
@@ -11025,6 +11273,9 @@ function syncHeroStartForm() {
11025
11273
  function openSettingsModal() {
11026
11274
  var modal = document.getElementById('settingsModal');
11027
11275
  modal.classList.add('visible');
11276
+ // Sync headless toggle
11277
+ var headlessToggle = document.getElementById('headlessEnabledToggle');
11278
+ if (headlessToggle) headlessToggle.checked = !!webTerm.headlessEnabled;
11028
11279
  // Load data for active sub-tab
11029
11280
  var activeSubTab = document.querySelector('.sub-tab.active');
11030
11281
  if (activeSubTab && activeSubTab.dataset.subtab === 'mcp') { setMcpTabActive(true); loadMcpServers(); }
@@ -14409,7 +14660,6 @@ function removeWidget(widgetId) {
14409
14660
  }
14410
14661
 
14411
14662
  function clearWidgets() {
14412
- if (widgetsCache.length === 0) return;
14413
14663
  fetch('/api/widgets/clear', {
14414
14664
  method: 'POST',
14415
14665
  headers: apiWriteHeaders(),
@@ -15570,7 +15820,12 @@ document.addEventListener('keydown', function(e) {
15570
15820
  });
15571
15821
  document.addEventListener('mouseup', function(e) {
15572
15822
  if (e && e.button !== 0) return;
15573
- runAutoCopySelection();
15823
+ runAutoCopySelection({
15824
+ allowFallback: true,
15825
+ allowTextareaFallback: false,
15826
+ notifyCopied: isTerminalSelectionContext(),
15827
+ copiedToastMessage: 'Copied'
15828
+ });
15574
15829
  });
15575
15830
  document.addEventListener('keyup', function(e) {
15576
15831
  if (!e) return;
@@ -15583,7 +15838,12 @@ document.addEventListener('keyup', function(e) {
15583
15838
  || key.indexOf('Arrow') === 0
15584
15839
  || ((e.ctrlKey || e.metaKey) && key.toLowerCase() === 'a');
15585
15840
  if (!isSelectionKey) return;
15586
- runAutoCopySelection();
15841
+ runAutoCopySelection({
15842
+ allowFallback: true,
15843
+ allowTextareaFallback: false,
15844
+ notifyCopied: isTerminalSelectionContext(),
15845
+ copiedToastMessage: 'Copied'
15846
+ });
15587
15847
  });
15588
15848
  </script>
15589
15849
 
package/dist/lib/ui.js CHANGED
@@ -1195,27 +1195,32 @@ function closeWebTerminalBridgeClients(bridge, code = 4001, reason = 'labgate-br
1195
1195
  }
1196
1196
  async function ensureWebTerminalBridge(record) {
1197
1197
  const existing = webTerminalBridges.get(record.id);
1198
- if (existing && existing.pty)
1199
- return existing;
1200
- const ptyModule = await loadNodePtyModule();
1201
- if (!ptyModule) {
1202
- return null;
1203
- }
1204
1198
  let tmuxBin = 'tmux';
1205
1199
  try {
1206
1200
  tmuxBin = await (0, web_terminal_js_1.getTmuxBinary)();
1207
1201
  }
1208
1202
  catch (err) {
1203
+ if (existing && existing.pty) {
1204
+ log.warn(`Could not resolve tmux binary for web terminal bridge ${record.id}: ${err?.message ?? String(err)}`);
1205
+ return existing;
1206
+ }
1209
1207
  log.warn(`Could not resolve tmux binary for web terminal bridge ${record.id}: ${err?.message ?? String(err)}`);
1210
1208
  return null;
1211
1209
  }
1212
1210
  try {
1213
- // Keep wheel scrolling intuitive for both new and existing sessions.
1214
- await execFileAsync(tmuxBin, ['set-option', '-t', record.tmuxSession, 'mouse', 'on'], { timeout: 10_000 });
1211
+ // Keep web terminal copy/selection behavior reliable by letting xterm own
1212
+ // mouse selection instead of tmux copy-mode selection.
1213
+ await execFileAsync(tmuxBin, ['set-option', '-t', record.tmuxSession, 'mouse', 'off'], { timeout: 10_000 });
1215
1214
  }
1216
1215
  catch {
1217
1216
  // Best effort only; attach should still proceed.
1218
1217
  }
1218
+ if (existing && existing.pty)
1219
+ return existing;
1220
+ const ptyModule = await loadNodePtyModule();
1221
+ if (!ptyModule) {
1222
+ return null;
1223
+ }
1219
1224
  const env = {};
1220
1225
  for (const [k, v] of Object.entries(process.env)) {
1221
1226
  if (v !== undefined)
@@ -1376,7 +1381,6 @@ async function startClaudeHeadlessWsRun(ws, record, prompt, resumeSessionId) {
1376
1381
  return () => { };
1377
1382
  }
1378
1383
  const args = buildClaudeHeadlessApptainerArgs(config, record.workdir, trimmedPrompt, resumeSessionId);
1379
- send({ type: 'status', stage: 'run', message: 'Running Claude in headless mode...' });
1380
1384
  const child = (0, child_process_1.spawn)('apptainer', args, {
1381
1385
  cwd: record.workdir,
1382
1386
  env: process.env,