labgate 0.5.33 → 0.5.35

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
@@ -1462,6 +1462,31 @@
1462
1462
  font-family: 'GeistMono', monospace;
1463
1463
  }
1464
1464
 
1465
+ .ui-update-row-actions {
1466
+ display: flex;
1467
+ flex-wrap: wrap;
1468
+ align-items: center;
1469
+ gap: 8px;
1470
+ }
1471
+
1472
+ .ui-update-row-actions .settings-info-value {
1473
+ flex: 1 1 220px;
1474
+ min-width: 0;
1475
+ margin: 0;
1476
+ }
1477
+
1478
+ .ui-update-status.warning {
1479
+ color: #92400e;
1480
+ }
1481
+
1482
+ .ui-update-status.error {
1483
+ color: #b91c1c;
1484
+ }
1485
+
1486
+ .ui-update-status.success {
1487
+ color: #166534;
1488
+ }
1489
+
1465
1490
  /* ── Terminal compact footer ──────────────────── */
1466
1491
  .terminal-compact-footer {
1467
1492
  display: flex;
@@ -4745,26 +4770,153 @@
4745
4770
  flex-direction: column;
4746
4771
  align-items: flex-start;
4747
4772
  }
4748
- .sidebar-section-body .slurm-summary {
4749
- gap: 6px;
4773
+ /* ── Compact SLURM sidebar ── */
4774
+ .slurm-compact-toolbar {
4775
+ display: flex;
4776
+ align-items: center;
4777
+ justify-content: space-between;
4778
+ gap: 8px;
4779
+ margin-bottom: 8px;
4750
4780
  }
4751
- .sidebar-section-body .slurm-stat {
4752
- padding: 8px 12px;
4753
- min-width: 60px;
4781
+ .slurm-scope-toggle {
4782
+ display: inline-flex;
4783
+ border: 1px solid var(--border-color);
4784
+ border-radius: 6px;
4785
+ background: var(--bg-secondary);
4786
+ padding: 2px;
4787
+ gap: 1px;
4754
4788
  }
4755
- .sidebar-section-body .slurm-stat-value {
4756
- font-size: 20px;
4789
+ .slurm-scope-btn {
4790
+ border: none;
4791
+ border-radius: 4px;
4792
+ background: transparent;
4793
+ color: var(--text-muted);
4794
+ font-family: 'GeistMono', monospace;
4795
+ font-size: 0.625rem;
4796
+ font-weight: 600;
4797
+ padding: 3px 8px;
4798
+ cursor: pointer;
4799
+ transition: background 0.15s, color 0.15s;
4800
+ }
4801
+ .slurm-scope-btn:hover { color: var(--text-primary); }
4802
+ .slurm-scope-btn.active {
4803
+ background: var(--card-bg);
4804
+ color: var(--text-primary);
4805
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
4806
+ }
4807
+ .slurm-compact-stats {
4808
+ display: flex;
4809
+ gap: 8px;
4757
4810
  }
4758
- .sidebar-section-body .slurm-filters {
4811
+ .slurm-mini-stat {
4812
+ font-family: 'GeistMono', monospace;
4813
+ font-size: 0.625rem;
4814
+ color: var(--text-muted);
4815
+ }
4816
+ .slurm-mini-stat span { font-weight: 600; }
4817
+ .slurm-mini-pending span { color: #f59e0b; }
4818
+ .slurm-mini-running span { color: #16a34a; }
4819
+ .slurm-compact-filters {
4820
+ display: flex;
4821
+ gap: 6px;
4822
+ margin-bottom: 8px;
4823
+ }
4824
+ .slurm-compact-filters select,
4825
+ .slurm-compact-filters input {
4826
+ flex: 1;
4827
+ padding: 4px 8px;
4828
+ border: 1px solid var(--border-color);
4829
+ border-radius: 6px;
4830
+ background: var(--bg-secondary);
4831
+ font-family: 'GeistMono', monospace;
4832
+ font-size: 0.6875rem;
4833
+ color: var(--text-primary);
4834
+ }
4835
+ .slurm-compact-filters select { flex: 0 0 auto; min-width: 90px; }
4836
+
4837
+ /* ── SLURM job cards ── */
4838
+ .slurm-job-grid {
4839
+ display: flex;
4759
4840
  flex-direction: column;
4841
+ gap: 2px;
4842
+ }
4843
+ .slurm-job-card {
4844
+ padding: 7px 8px;
4845
+ border-radius: 6px;
4846
+ background: transparent;
4847
+ cursor: pointer;
4848
+ transition: background 0.15s;
4849
+ }
4850
+ .slurm-job-card:hover { background: var(--bg-secondary); }
4851
+ .slurm-job-card-row {
4852
+ display: flex;
4853
+ align-items: center;
4760
4854
  gap: 6px;
4761
4855
  }
4762
- .sidebar-section-body .slurm-filters input[type="text"] {
4763
- max-width: unset;
4856
+ .slurm-job-id {
4857
+ font-family: 'GeistMono', monospace;
4858
+ font-size: 0.8125rem;
4859
+ font-weight: 600;
4860
+ color: var(--text-primary);
4861
+ white-space: nowrap;
4764
4862
  }
4765
- .sidebar-section-body .card {
4766
- overflow-x: auto;
4863
+ .slurm-job-name {
4864
+ font-size: 0.75rem;
4865
+ color: var(--text-secondary);
4866
+ overflow: hidden;
4867
+ text-overflow: ellipsis;
4868
+ white-space: nowrap;
4869
+ flex: 1;
4870
+ min-width: 0;
4767
4871
  }
4872
+ .slurm-job-state {
4873
+ display: inline-block;
4874
+ padding: 1px 6px;
4875
+ border-radius: 3px;
4876
+ font-family: 'GeistMono', monospace;
4877
+ font-size: 0.5625rem;
4878
+ font-weight: 600;
4879
+ text-transform: uppercase;
4880
+ letter-spacing: 0.03em;
4881
+ white-space: nowrap;
4882
+ }
4883
+ .slurm-job-state-PENDING { background: rgba(245,158,11,0.12); color: #b45309; }
4884
+ .slurm-job-state-RUNNING { background: rgba(22,163,74,0.12); color: #15803d; }
4885
+ .slurm-job-state-COMPLETED { background: rgba(96,165,250,0.12); color: #2563eb; }
4886
+ .slurm-job-state-FAILED { background: rgba(220,38,38,0.12); color: #dc2626; }
4887
+ .slurm-job-state-CANCELLED { background: rgba(156,163,175,0.12); color: #6b7280; }
4888
+ .slurm-job-state-TIMEOUT { background: rgba(168,85,247,0.12); color: #7c3aed; }
4889
+ .slurm-job-meta {
4890
+ font-family: 'GeistMono', monospace;
4891
+ font-size: 0.625rem;
4892
+ color: var(--text-muted);
4893
+ margin-top: 2px;
4894
+ display: flex;
4895
+ gap: 8px;
4896
+ align-items: center;
4897
+ }
4898
+ .slurm-job-actions {
4899
+ display: flex;
4900
+ gap: 4px;
4901
+ margin-left: auto;
4902
+ }
4903
+ .slurm-job-action-btn {
4904
+ border: none;
4905
+ background: transparent;
4906
+ cursor: pointer;
4907
+ font-family: 'GeistMono', monospace;
4908
+ font-size: 0.5625rem;
4909
+ color: var(--text-muted);
4910
+ padding: 1px 4px;
4911
+ border-radius: 3px;
4912
+ transition: background 0.15s, color 0.15s;
4913
+ }
4914
+ .slurm-job-action-btn:hover { background: var(--bg-secondary); color: var(--text-primary); }
4915
+ .slurm-job-action-btn.danger { color: #dc2626; }
4916
+ .slurm-job-action-btn.danger:hover { background: rgba(220,38,38,0.08); }
4917
+
4918
+ .sidebar-section-body .slurm-summary { display: none; }
4919
+ .sidebar-section-body .card { overflow-x: auto; }
4768
4920
  /* ── Enterprise: locked fields ─────────────── */
4769
4921
  .field-locked label::before { content: '\1F512 '; font-size: 0.75em; }
4770
4922
  .field-locked input, .field-locked select, .field-locked textarea {
@@ -5221,9 +5373,6 @@
5221
5373
  background: var(--card-bg);
5222
5374
  border-color: var(--border-color);
5223
5375
  }
5224
- .terminal-chat-msg.assistant {
5225
- margin-right: auto;
5226
- }
5227
5376
  .terminal-chat-msg.error {
5228
5377
  border-color: #fecaca;
5229
5378
  background: #fef2f2;
@@ -5273,131 +5422,157 @@
5273
5422
  font-size: inherit;
5274
5423
  color: inherit;
5275
5424
  }
5276
- .terminal-chat-msg.pending .terminal-chat-msg-header::after {
5277
- content: '';
5278
- display: inline-block;
5279
- width: 6px;
5280
- height: 6px;
5281
- margin-left: 6px;
5282
- border-radius: 50%;
5283
- background: var(--accent);
5284
- animation: chatPulse 1.4s ease-in-out infinite;
5285
- vertical-align: middle;
5286
- }
5287
5425
  @keyframes chatPulse {
5288
5426
  0%, 100% { opacity: 0.25; transform: scale(0.85); }
5289
5427
  50% { opacity: 1; transform: scale(1); }
5290
5428
  }
5291
5429
 
5292
- /* ── Tool use cards (inline in chat transcript) ── */
5293
- .terminal-chat-tool {
5294
- display: flex;
5295
- align-items: center;
5296
- gap: 7px;
5297
- max-width: min(600px, 90%);
5430
+ /* ── Claude Turn Container ── */
5431
+ .claude-turn {
5432
+ max-width: min(820px, 100%);
5298
5433
  margin-right: auto;
5299
- padding: 4px 10px;
5300
- border-radius: 6px;
5301
- background: transparent;
5302
- font-family: 'GeistMono', monospace;
5303
- font-size: 0.6875rem;
5304
- color: var(--text-secondary);
5305
- transition: opacity 0.2s;
5306
- border-left: 2px solid var(--border-color);
5434
+ border-radius: 10px;
5435
+ border: 1px solid var(--border-color);
5436
+ background: var(--card-bg);
5437
+ overflow: hidden;
5307
5438
  flex-shrink: 0;
5308
5439
  }
5309
- .terminal-chat-tool + .terminal-chat-tool {
5310
- margin-top: -6px;
5311
- }
5312
- .terminal-chat-tool .tool-icon {
5313
- flex-shrink: 0;
5314
- font-size: 0.625rem;
5315
- width: 14px;
5316
- text-align: center;
5440
+ .claude-turn.error {
5441
+ border-color: #fecaca;
5442
+ background: #fef2f2;
5317
5443
  }
5318
- .terminal-chat-tool .tool-name {
5444
+ .claude-turn-header {
5445
+ padding: 5px 10px;
5319
5446
  font-family: 'GeistPixelSquare', monospace;
5320
5447
  font-size: 0.5625rem;
5448
+ letter-spacing: 0.05em;
5321
5449
  font-weight: 600;
5322
5450
  text-transform: uppercase;
5323
- letter-spacing: 0.04em;
5324
- color: var(--text-secondary);
5325
- white-space: nowrap;
5326
- }
5327
- .terminal-chat-tool .tool-detail {
5328
5451
  color: var(--text-muted);
5329
- overflow: hidden;
5330
- text-overflow: ellipsis;
5331
- white-space: nowrap;
5332
- }
5333
- .terminal-chat-tool.pending {
5334
- border-left-color: var(--accent);
5335
5452
  }
5336
- .terminal-chat-tool.pending .tool-icon {
5453
+ .claude-turn.pending .claude-turn-header::after {
5454
+ content: '';
5337
5455
  display: inline-block;
5338
5456
  width: 6px;
5339
5457
  height: 6px;
5458
+ margin-left: 6px;
5340
5459
  border-radius: 50%;
5341
5460
  background: var(--accent);
5342
5461
  animation: chatPulse 1.4s ease-in-out infinite;
5343
- font-size: 0;
5344
- line-height: 0;
5462
+ vertical-align: middle;
5345
5463
  }
5346
- .terminal-chat-tool.done {
5347
- border-left-color: #16a34a;
5464
+ .claude-turn-body {
5465
+ padding: 0 12px 10px;
5348
5466
  }
5349
- .terminal-chat-tool.done .tool-icon {
5350
- color: #16a34a;
5467
+
5468
+ /* ── Text segments inside a turn ── */
5469
+ .claude-turn-text {
5470
+ font-family: 'GeistMono', monospace;
5471
+ font-size: 0.8rem;
5472
+ line-height: 1.55;
5473
+ color: var(--text-primary);
5474
+ white-space: pre-wrap;
5475
+ word-break: break-word;
5351
5476
  }
5352
- .terminal-chat-tool.error {
5353
- border-left-color: #dc2626;
5477
+ .claude-turn-text + .claude-turn-text,
5478
+ .claude-turn-tools + .claude-turn-text {
5479
+ margin-top: 8px;
5354
5480
  }
5355
- .terminal-chat-tool.error .tool-icon {
5356
- color: #dc2626;
5481
+ .claude-turn-text code {
5482
+ background: var(--bg-secondary);
5483
+ border: 1px solid var(--border-color);
5484
+ border-radius: 4px;
5485
+ padding: 1px 4px;
5486
+ font-size: 0.75rem;
5487
+ }
5488
+ .claude-turn-text pre {
5489
+ background: #1e1e2e;
5490
+ color: #cdd6f4;
5491
+ border-radius: 6px;
5492
+ padding: 10px 12px;
5493
+ margin: 6px 0;
5494
+ overflow-x: auto;
5495
+ font-size: 0.75rem;
5496
+ line-height: 1.5;
5497
+ }
5498
+ .claude-turn-text pre code {
5499
+ background: none;
5500
+ border: none;
5501
+ padding: 0;
5502
+ font-size: inherit;
5503
+ color: inherit;
5357
5504
  }
5358
5505
 
5359
- /* ── Collapsible tool groups ── */
5360
- .terminal-chat-tool-group {
5506
+ /* ── Tool chips (inline in turn) ── */
5507
+ .claude-turn-tools {
5361
5508
  display: flex;
5362
- flex-direction: column;
5363
- gap: 0;
5364
- flex-shrink: 0;
5509
+ flex-wrap: wrap;
5510
+ gap: 6px;
5511
+ margin: 8px 0;
5512
+ align-items: center;
5513
+ }
5514
+ .tool-chip {
5515
+ display: inline-flex;
5516
+ align-items: center;
5517
+ gap: 4px;
5518
+ padding: 2px 8px;
5519
+ border-radius: 4px;
5520
+ background: var(--bg-secondary);
5521
+ font-family: 'GeistMono', monospace;
5522
+ font-size: 0.6875rem;
5523
+ color: var(--text-secondary);
5524
+ cursor: default;
5525
+ white-space: nowrap;
5526
+ border: 1px solid transparent;
5527
+ transition: background 0.15s, border-color 0.15s;
5365
5528
  }
5366
- .terminal-chat-tool-group .terminal-chat-tool + .terminal-chat-tool {
5367
- margin-top: -6px;
5529
+ .tool-chip:hover {
5530
+ background: var(--accent-light);
5531
+ border-color: var(--border-color);
5368
5532
  }
5369
- .terminal-chat-tool-group .tool-group-collapsed {
5370
- display: none;
5533
+ .tool-chip-icon {
5534
+ font-size: 0.625rem;
5535
+ flex-shrink: 0;
5536
+ width: 12px;
5537
+ text-align: center;
5371
5538
  }
5372
- .terminal-chat-tool-group.expanded .tool-group-collapsed {
5373
- display: flex;
5539
+ .tool-chip.done .tool-chip-icon { color: #16a34a; }
5540
+ .tool-chip.error .tool-chip-icon { color: #dc2626; }
5541
+ .tool-chip.pending .tool-chip-icon {
5542
+ display: inline-block;
5543
+ width: 6px;
5544
+ height: 6px;
5545
+ border-radius: 50%;
5546
+ background: var(--accent);
5547
+ animation: chatPulse 1.4s ease-in-out infinite;
5548
+ font-size: 0;
5549
+ line-height: 0;
5374
5550
  }
5375
- .tool-group-toggle {
5376
- display: flex;
5551
+ .tool-chip-label { font-weight: 500; }
5552
+
5553
+ /* Collapsed chips */
5554
+ .tool-chip.tool-chip-collapsed { display: none; }
5555
+ .claude-turn-tools.expanded .tool-chip.tool-chip-collapsed { display: inline-flex; }
5556
+
5557
+ /* Toggle button for collapsed chips */
5558
+ .tool-chips-toggle {
5559
+ display: inline-flex;
5377
5560
  align-items: center;
5378
- gap: 6px;
5379
- padding: 2px 10px;
5380
- margin-bottom: 2px;
5381
- border: none;
5561
+ padding: 2px 8px;
5562
+ border: 1px solid var(--border-color);
5563
+ border-radius: 4px;
5382
5564
  background: transparent;
5383
5565
  cursor: pointer;
5384
5566
  font-family: 'GeistMono', monospace;
5385
5567
  font-size: 0.625rem;
5386
5568
  color: var(--text-muted);
5387
- border-left: 2px solid var(--border-color);
5388
- transition: color 0.15s;
5569
+ transition: color 0.15s, background 0.15s;
5389
5570
  }
5390
- .tool-group-toggle:hover {
5571
+ .tool-chips-toggle:hover {
5391
5572
  color: var(--text-secondary);
5573
+ background: var(--accent-light);
5392
5574
  }
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
- }
5575
+ .claude-turn-tools.expanded .tool-chips-toggle { display: none; }
5401
5576
 
5402
5577
  /* ── Rich content widgets (inline in chat transcript) ── */
5403
5578
  .terminal-chat-widget {
@@ -5956,10 +6131,6 @@
5956
6131
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
5957
6132
  <span>Settings</span>
5958
6133
  </button>
5959
- <button class="left-sidebar-footer-item" id="explorerModeToggleBtn" onclick="toggleExplorerDevMode()" title="Toggle Solution Explorer mode">
5960
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3v12"/><path d="M18 9v12"/><path d="M6 7c0 2.5 3 2.5 6 5s6 2.5 6 5"/><circle cx="6" cy="3" r="2"/><circle cx="18" cy="9" r="2"/><circle cx="18" cy="21" r="2"/></svg>
5961
- <span id="explorerModeToggleLabel">Explorer: Off</span>
5962
- </button>
5963
6134
  <!-- Hidden buttons to preserve JS getElementById references -->
5964
6135
  <button id="notifyToggleBtn" onclick="toggleNotify()" aria-label="Toggle desktop notifications" aria-pressed="false" style="display:none"></button>
5965
6136
  <button id="sidebarToggleBtn" onclick="toggleSidebar()" style="display:none"></button>
@@ -6048,7 +6219,6 @@
6048
6219
  <div class="terminal-input-toolbar">
6049
6220
  <div class="terminal-input-mode-toggle">
6050
6221
  <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>
6052
6222
  </div>
6053
6223
  </div>
6054
6224
  <textarea id="terminalChatInput" class="terminal-input-chat" placeholder="Type command or prompt. Enter sends, Shift+Enter inserts newline." onkeydown="handleTerminalChatInputKeydown(event)" spellcheck="false"></textarea>
@@ -6114,14 +6284,17 @@
6114
6284
  </div>
6115
6285
  </div>
6116
6286
  <div id="jobsSlurmContent" style="display:none">
6117
- <div class="slurm-summary" id="slurmSummary">
6118
- <div class="slurm-stat slurm-stat-pending"><span class="slurm-stat-value" id="slurmPending">0</span><span class="slurm-stat-label">Pending</span></div>
6119
- <div class="slurm-stat slurm-stat-running"><span class="slurm-stat-value" id="slurmRunning">0</span><span class="slurm-stat-label">Running</span></div>
6120
- <div class="slurm-stat slurm-stat-completed"><span class="slurm-stat-value" id="slurmCompleted">0</span><span class="slurm-stat-label">Completed</span></div>
6121
- <div class="slurm-stat slurm-stat-failed"><span class="slurm-stat-value" id="slurmFailed">0</span><span class="slurm-stat-label">Failed</span></div>
6122
- <div class="slurm-stat slurm-stat-other"><span class="slurm-stat-value" id="slurmOther">0</span><span class="slurm-stat-label">Other</span></div>
6287
+ <div class="slurm-compact-toolbar">
6288
+ <div class="slurm-scope-toggle">
6289
+ <button class="slurm-scope-btn active" id="slurmScopeMine" type="button" onclick="setSlurmScope('mine')">My session</button>
6290
+ <button class="slurm-scope-btn" id="slurmScopeAll" type="button" onclick="setSlurmScope('all')">All jobs</button>
6291
+ </div>
6292
+ <div class="slurm-compact-stats">
6293
+ <span class="slurm-mini-stat slurm-mini-pending" title="Pending"><span id="slurmPending">0</span> pending</span>
6294
+ <span class="slurm-mini-stat slurm-mini-running" title="Running"><span id="slurmRunning">0</span> running</span>
6295
+ </div>
6123
6296
  </div>
6124
- <div class="slurm-filters">
6297
+ <div class="slurm-compact-filters">
6125
6298
  <select id="slurmStateFilter" onchange="loadSlurmJobs()">
6126
6299
  <option value="">All states</option>
6127
6300
  <option value="PENDING">Pending</option>
@@ -6132,11 +6305,12 @@
6132
6305
  <option value="TIMEOUT">Timeout</option>
6133
6306
  </select>
6134
6307
  <input type="text" id="slurmSearch" placeholder="Search..." oninput="debounceSlurmSearch()">
6135
- <button class="refresh-btn" onclick="loadSlurmJobs()">Refresh</button>
6136
- </div>
6137
- <div class="card">
6138
- <div id="slurmJobsContent"><div class="empty-state">Loading SLURM jobs...</div></div>
6139
6308
  </div>
6309
+ <div id="slurmJobsContent"><div class="empty-state" style="padding:12px">Loading...</div></div>
6310
+ <!-- hidden stats for backward compat -->
6311
+ <span id="slurmCompleted" style="display:none">0</span>
6312
+ <span id="slurmFailed" style="display:none">0</span>
6313
+ <span id="slurmOther" style="display:none">0</span>
6140
6314
  </div>
6141
6315
  </div>
6142
6316
  </div>
@@ -6360,6 +6534,18 @@
6360
6534
  <h3>Session Info</h3>
6361
6535
  <p class="card-description">Reference metadata for this LabGate UI session.</p>
6362
6536
  <div class="settings-info-list">
6537
+ <div class="settings-info-row">
6538
+ <span class="settings-info-label">LabGate Version</span>
6539
+ <code class="settings-info-value" id="labgateVersionValue">Loading...</code>
6540
+ </div>
6541
+ <div class="settings-info-row">
6542
+ <span class="settings-info-label">UI Update</span>
6543
+ <div class="ui-update-row-actions">
6544
+ <code class="settings-info-value ui-update-status" id="uiUpdateStatusValue">Checking...</code>
6545
+ <button type="button" class="agent-update-btn" id="uiUpdateCheckBtn" onclick="checkUiVersion(true)">Search for Update</button>
6546
+ <button type="button" class="agent-update-btn" id="uiUpdateApplyBtn" style="display:none" onclick="startUiUpdate()">Update</button>
6547
+ </div>
6548
+ </div>
6363
6549
  <div class="settings-info-row">
6364
6550
  <span class="settings-info-label">Config JSON Path</span>
6365
6551
  <code class="settings-info-value" id="configPathValue">Loading...</code>
@@ -6375,6 +6561,13 @@
6375
6561
  <span>Enable headless chat mode</span>
6376
6562
  </label>
6377
6563
  </div>
6564
+ <div class="field" style="margin-top: 12px">
6565
+ <label class="toggle-row" style="display:flex;align-items:center;gap:10px;cursor:pointer">
6566
+ <input type="checkbox" id="headlessAllowedPermissions" onchange="collectConfig();markDirty();">
6567
+ <span>Run Claude headless with allowed permissions</span>
6568
+ </label>
6569
+ <p class="card-description" style="margin-top:6px">Adds <code>--dangerously-skip-permissions</code> to Claude headless runs so tools can execute without interactive approval prompts.</p>
6570
+ </div>
6378
6571
  </div>
6379
6572
  </div>
6380
6573
 
@@ -6713,6 +6906,8 @@ var LABGATE_WRITE_TOKEN = '__LABGATE_WRITE_TOKEN__';
6713
6906
  var mcpAutoRefreshTimer = null;
6714
6907
  var mcpAutoRegisterInFlight = false;
6715
6908
  var MCP_AUTO_REFRESH_MS = 12000;
6909
+ var uiUpdateInProgress = false;
6910
+ var uiUpdateStatusPollTimer = null;
6716
6911
  var resultsCache = [];
6717
6912
  var resultEditorId = null;
6718
6913
  var resultsHasPendingRefresh = false;
@@ -7012,8 +7207,13 @@ function getWebTerminalSessionRuntime(session) {
7012
7207
  function toggleHeadlessEnabled(enabled) {
7013
7208
  webTerm.headlessEnabled = !!enabled;
7014
7209
  try { localStorage.setItem('labgate_headless_enabled', enabled ? '1' : '0'); } catch(e) {}
7015
- if (!enabled && webTerm.inputMode === 'chat') {
7210
+ if (enabled) {
7211
+ setTerminalInputMode('chat', { focus: false });
7212
+ return;
7213
+ }
7214
+ if (webTerm.inputMode === 'chat') {
7016
7215
  setTerminalInputMode('raw', { focus: false });
7216
+ return;
7017
7217
  }
7018
7218
  updateTerminalInputModeUi();
7019
7219
  updateTerminalInputAvailability();
@@ -7095,6 +7295,19 @@ function updateHeadlessTranscriptMessage(sessionId, messageId, patch) {
7095
7295
  renderTerminalChatTranscript();
7096
7296
  }
7097
7297
 
7298
+ function shortenToolName(raw) {
7299
+ var name = String(raw || 'tool');
7300
+ // Strip MCP prefix: "mcp__server-name__tool_name" or "MCP__SERVER__TOOL"
7301
+ name = name.replace(/^mcp__[^_]+(?:[-][^_]+)*__/i, '');
7302
+ // Strip remaining double-underscore prefixed segments
7303
+ name = name.replace(/^[A-Za-z0-9_-]+__/, '');
7304
+ // Replace underscores with spaces
7305
+ name = name.replace(/_/g, ' ');
7306
+ // Title case
7307
+ name = name.replace(/\b\w/g, function(c) { return c.toUpperCase(); });
7308
+ return name.trim() || 'Tool';
7309
+ }
7310
+
7098
7311
  function renderTerminalChatTranscript() {
7099
7312
  var el = document.getElementById('terminalChatTranscript');
7100
7313
  if (!el) return;
@@ -7125,87 +7338,143 @@ function renderTerminalChatTranscript() {
7125
7338
  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>';
7126
7339
  return;
7127
7340
  }
7128
- // Group consecutive tool entries for collapsible rendering
7129
- var groups = [];
7130
- var currentToolGroup = [];
7341
+ // Build turn-based grouping: merge consecutive assistant/tool/widget entries
7342
+ // into a single "Claude turn" between user messages
7343
+ var turns = [];
7344
+ var currentTurn = null;
7345
+ var currentToolBatch = [];
7346
+
7347
+ function _flushToolBatch() {
7348
+ if (currentToolBatch.length > 0) {
7349
+ if (!currentTurn) currentTurn = { type: 'claude-turn', segments: [] };
7350
+ currentTurn.segments.push({ kind: 'tools', items: currentToolBatch.slice() });
7351
+ currentToolBatch = [];
7352
+ }
7353
+ }
7354
+ function _flushTurn() {
7355
+ _flushToolBatch();
7356
+ if (currentTurn && currentTurn.segments.length > 0) {
7357
+ turns.push(currentTurn);
7358
+ }
7359
+ currentTurn = null;
7360
+ }
7361
+
7131
7362
  for (var ti = 0; ti < transcript.length; ti++) {
7132
7363
  var entry = transcript[ti];
7133
7364
  var role = String(entry.role || 'assistant');
7134
- if (role === 'tool') {
7135
- currentToolGroup.push(entry);
7365
+ if (role === 'user') {
7366
+ _flushTurn();
7367
+ turns.push({ type: 'user', entry: entry });
7368
+ } else if (role === 'tool') {
7369
+ if (!currentTurn) currentTurn = { type: 'claude-turn', segments: [] };
7370
+ currentToolBatch.push(entry);
7371
+ } else if (role === 'widget') {
7372
+ _flushToolBatch();
7373
+ if (!currentTurn) currentTurn = { type: 'claude-turn', segments: [] };
7374
+ currentTurn.segments.push({ kind: 'widget', entry: entry });
7136
7375
  } else {
7137
- if (currentToolGroup.length) {
7138
- groups.push({ type: 'tools', items: currentToolGroup });
7139
- currentToolGroup = [];
7376
+ // assistant or system
7377
+ _flushToolBatch();
7378
+ if (!currentTurn) currentTurn = { type: 'claude-turn', segments: [] };
7379
+ currentTurn.segments.push({ kind: 'text', entry: entry });
7380
+ }
7381
+ }
7382
+ _flushTurn();
7383
+
7384
+ var html = turns.map(function(turn, turnIdx) {
7385
+ // User message — unchanged
7386
+ if (turn.type === 'user') {
7387
+ var ue = turn.entry;
7388
+ var uClasses = ['terminal-chat-msg', 'user'];
7389
+ if (ue.pending) uClasses.push('pending');
7390
+ if (ue.error) uClasses.push('error');
7391
+ return '<div class="' + uClasses.join(' ') + '">'
7392
+ + '<div class="terminal-chat-msg-header">You</div>'
7393
+ + '<div class="terminal-chat-msg-body">' + renderChatMarkdown(String(ue.text || '')) + '</div>'
7394
+ + '</div>';
7395
+ }
7396
+
7397
+ // Claude turn — merge all segments into one card
7398
+ var segments = turn.segments;
7399
+ var hasVisibleContent = false;
7400
+ var turnPending = false;
7401
+ var turnError = false;
7402
+ for (var si = 0; si < segments.length; si++) {
7403
+ var seg = segments[si];
7404
+ if (seg.kind === 'tools' && seg.items.length > 0) hasVisibleContent = true;
7405
+ if (seg.kind === 'widget') hasVisibleContent = true;
7406
+ if (seg.kind === 'text') {
7407
+ if (String(seg.entry.text || '').trim()) hasVisibleContent = true;
7408
+ if (seg.entry.pending) turnPending = true;
7409
+ if (seg.entry.error) turnError = true;
7140
7410
  }
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('');
7411
+ }
7412
+ // Skip empty pending turns (no text, no tools, no widgets yet)
7413
+ if (!hasVisibleContent && turnPending) return '';
7414
+
7415
+ var turnClasses = ['claude-turn'];
7416
+ if (turnPending) turnClasses.push('pending');
7417
+ if (turnError) turnClasses.push('error');
7418
+
7419
+ var out = '<div class="' + turnClasses.join(' ') + '">';
7420
+ out += '<div class="claude-turn-header">Claude</div>';
7421
+ out += '<div class="claude-turn-body">';
7422
+
7423
+ for (var si2 = 0; si2 < segments.length; si2++) {
7424
+ var seg2 = segments[si2];
7425
+
7426
+ if (seg2.kind === 'text') {
7427
+ var text = String(seg2.entry.text || '').trim();
7428
+ if (!text) continue;
7429
+ out += '<div class="claude-turn-text">' + renderChatMarkdown(text) + '</div>';
7167
7430
  }
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');
7431
+
7432
+ else if (seg2.kind === 'widget') {
7433
+ var wEntry = seg2.entry;
7434
+ var widgetId = 'widget-' + wEntry.id;
7435
+ out += '<div class="terminal-chat-widget" id="' + widgetId + '" style="margin:8px 0">';
7436
+ if (wEntry.widgetTitle) {
7437
+ out += '<div class="widget-title">' + escapeHtml(String(wEntry.widgetTitle)) + '</div>';
7438
+ }
7439
+ out += '<div class="widget-body" data-widget-type="' + escapeHtml(String(wEntry.widget || 'markdown')) + '" data-entry-id="' + escapeHtml(wEntry.id) + '">';
7440
+ out += '<div class="widget-placeholder">Loading widget...</div>';
7441
+ out += '</div></div>';
7177
7442
  }
7178
- for (var vi = items.length - VISIBLE_COUNT; vi < items.length; vi++) {
7179
- out += renderToolDiv(items[vi]);
7443
+
7444
+ else if (seg2.kind === 'tools') {
7445
+ var items = seg2.items;
7446
+ var VISIBLE = 1;
7447
+ var needsCollapse = items.length > VISIBLE + 1;
7448
+ var toolGroupId = 'tchips-' + turnIdx + '-' + si2;
7449
+
7450
+ out += '<div class="claude-turn-tools" id="' + toolGroupId + '">';
7451
+ // Toggle button first (before collapsed chips)
7452
+ if (needsCollapse) {
7453
+ var hiddenN = items.length - VISIBLE;
7454
+ out += '<button type="button" class="tool-chips-toggle" onclick="var g=document.getElementById(\'' + toolGroupId + '\');g.classList.toggle(\'expanded\')">+' + hiddenN + ' more</button>';
7455
+ }
7456
+ for (var tci = 0; tci < items.length; tci++) {
7457
+ var te = items[tci];
7458
+ var chipClass = 'tool-chip';
7459
+ if (te.toolDone) chipClass += ' done';
7460
+ else if (te.toolError) chipClass += ' error';
7461
+ else chipClass += ' pending';
7462
+ // Hide earlier chips, keep last VISIBLE always visible
7463
+ if (needsCollapse && tci < items.length - VISIBLE) chipClass += ' tool-chip-collapsed';
7464
+ var chipIcon = te.toolDone ? '\u2713' : te.toolError ? '\u2717' : '';
7465
+ var shortName = shortenToolName(te.toolName);
7466
+ var fullTitle = escapeHtml(String(te.toolName || '')) + (te.toolDetail ? ' \u2014 ' + escapeHtml(String(te.toolDetail)) : '');
7467
+ out += '<div class="' + chipClass + '" title="' + fullTitle + '">'
7468
+ + '<span class="tool-chip-icon">' + chipIcon + '</span>'
7469
+ + '<span class="tool-chip-label">' + escapeHtml(shortName) + '</span>'
7470
+ + '</div>';
7471
+ }
7472
+ out += '</div>';
7180
7473
  }
7181
- out += '</div>';
7182
- return out;
7183
7474
  }
7184
7475
 
7185
- var entry = group.entry;
7186
- var role = String(entry.role || 'assistant');
7187
-
7188
- // Render widget entries as rich content cards
7189
- if (role === 'widget') {
7190
- var widgetId = 'widget-' + entry.id;
7191
- var widgetHtml = '<div class="terminal-chat-widget" id="' + widgetId + '">';
7192
- if (entry.widgetTitle) {
7193
- widgetHtml += '<div class="widget-title">' + escapeHtml(String(entry.widgetTitle)) + '</div>';
7194
- }
7195
- widgetHtml += '<div class="widget-body" data-widget-type="' + escapeHtml(String(entry.widget || 'markdown')) + '" data-entry-id="' + escapeHtml(entry.id) + '">';
7196
- widgetHtml += '<div class="widget-placeholder">Loading widget...</div>';
7197
- widgetHtml += '</div></div>';
7198
- return widgetHtml;
7199
- }
7200
-
7201
- var classes = ['terminal-chat-msg', role];
7202
- if (entry.pending) classes.push('pending');
7203
- if (entry.error) classes.push('error');
7204
- var label = role === 'user' ? 'You' : role === 'system' ? 'System' : 'Claude';
7205
- return '<div class="' + classes.join(' ') + '">'
7206
- + '<div class="terminal-chat-msg-header">' + escapeHtml(label) + '</div>'
7207
- + '<div class="terminal-chat-msg-body">' + renderChatMarkdown(String(entry.text || '')) + '</div>'
7208
- + '</div>';
7476
+ out += '</div></div>';
7477
+ return out;
7209
7478
  }).join('');
7210
7479
  el.innerHTML = html;
7211
7480
  // Hydrate widget bodies
@@ -8291,6 +8560,10 @@ function updateTerminalInputModeUi() {
8291
8560
  var attachedSession = getAttachedWebTerminalSession();
8292
8561
  var isRaw = webTerm.inputMode === 'raw';
8293
8562
  var headlessCapable = isHeadlessClaudeChatSupported();
8563
+ if (headlessCapable && isRaw) {
8564
+ webTerm.inputMode = 'chat';
8565
+ isRaw = false;
8566
+ }
8294
8567
  if (attachedSession && !headlessCapable && !isRaw) {
8295
8568
  webTerm.inputMode = 'raw';
8296
8569
  isRaw = true;
@@ -8304,6 +8577,7 @@ function updateTerminalInputModeUi() {
8304
8577
  }
8305
8578
  // Hide the Chat/Raw toggle entirely when headless is not enabled
8306
8579
  if (modeToggle) modeToggle.style.display = webTerm.headlessEnabled ? '' : 'none';
8580
+ if (rawBtn) rawBtn.style.display = webTerm.headlessEnabled ? 'none' : '';
8307
8581
  if (chatBtn) chatBtn.classList.toggle('active', !isRaw && headlessCapable);
8308
8582
  if (rawBtn) rawBtn.classList.toggle('active', isRaw);
8309
8583
  if (hint) {
@@ -8376,7 +8650,11 @@ function updateTerminalInputAvailability() {
8376
8650
  function setTerminalInputMode(mode, opts) {
8377
8651
  var options = opts || {};
8378
8652
  var requested = mode === 'raw' ? 'raw' : 'chat';
8379
- if (requested === 'chat' && getAttachedWebTerminalSession() && !isHeadlessClaudeChatSupported()) {
8653
+ var attachedSession = getAttachedWebTerminalSession();
8654
+ var headlessCapable = isHeadlessClaudeChatSupported();
8655
+ if (headlessCapable) {
8656
+ requested = 'chat';
8657
+ } else if (requested === 'chat' && attachedSession && !headlessCapable) {
8380
8658
  requested = 'raw';
8381
8659
  }
8382
8660
  webTerm.inputMode = requested;
@@ -9815,10 +10093,7 @@ function toggleExplorerDevMode() {
9815
10093
  showToast('Explorer mode enabled.', 'success');
9816
10094
  }
9817
10095
 
9818
- var explorerPref = readExplorerDevModePref();
9819
- if (EXPLORER_DEV_UI_QUERY_DISABLE) EXPLORER_DEV_UI_ENABLED = false;
9820
- else if (EXPLORER_DEV_UI_QUERY_ENABLE) EXPLORER_DEV_UI_ENABLED = true;
9821
- else EXPLORER_DEV_UI_ENABLED = explorerPref === true;
10096
+ EXPLORER_DEV_UI_ENABLED = false;
9822
10097
 
9823
10098
  // Restore sidebar state from localStorage
9824
10099
  try {
@@ -11456,6 +11731,14 @@ function populateUI() {
11456
11731
  if (!config.slurm) config.slurm = {};
11457
11732
  if (typeof config.slurm.enabled !== 'boolean') config.slurm.enabled = true;
11458
11733
  document.getElementById('slurmEnabled').checked = !!config.slurm.enabled;
11734
+ if (!config.headless) config.headless = {};
11735
+ if (typeof config.headless.claude_run_with_allowed_permissions !== 'boolean') {
11736
+ config.headless.claude_run_with_allowed_permissions = true;
11737
+ }
11738
+ var headlessAllowedPermissions = document.getElementById('headlessAllowedPermissions');
11739
+ if (headlessAllowedPermissions) {
11740
+ headlessAllowedPermissions.checked = !!config.headless.claude_run_with_allowed_permissions;
11741
+ }
11459
11742
 
11460
11743
  renderList('domainsList', config.network ? config.network.allowed_domains : [], removeDomain);
11461
11744
  renderList('blockedList', config.filesystem ? config.filesystem.blocked_patterns : [], removeBlocked);
@@ -11478,6 +11761,11 @@ function collectConfig() {
11478
11761
  config.audit.log_dir = document.getElementById('logDir').value;
11479
11762
  if (!config.slurm) config.slurm = {};
11480
11763
  config.slurm.enabled = document.getElementById('slurmEnabled').checked;
11764
+ if (!config.headless) config.headless = {};
11765
+ var headlessAllowedPermissions = document.getElementById('headlessAllowedPermissions');
11766
+ config.headless.claude_run_with_allowed_permissions = headlessAllowedPermissions
11767
+ ? !!headlessAllowedPermissions.checked
11768
+ : true;
11481
11769
  updateSlurmUIState({ skipLoadJobs: true });
11482
11770
  }
11483
11771
 
@@ -12538,6 +12826,203 @@ function loadConfigPath() {
12538
12826
  });
12539
12827
  }
12540
12828
 
12829
+ function formatUiVersion(version) {
12830
+ var cleaned = String(version || '').trim();
12831
+ if (!cleaned) return '';
12832
+ return cleaned.charAt(0).toLowerCase() === 'v' ? cleaned : ('v' + cleaned);
12833
+ }
12834
+
12835
+ function setUiUpdateStatus(message, tone) {
12836
+ var el = document.getElementById('uiUpdateStatusValue');
12837
+ if (!el) return;
12838
+ el.textContent = message || '';
12839
+ el.className = 'settings-info-value ui-update-status' + (tone ? (' ' + tone) : '');
12840
+ }
12841
+
12842
+ function setUiUpdateButtonsState(opts) {
12843
+ var options = opts || {};
12844
+ var showApply = !!options.showApply;
12845
+ var checkBtn = document.getElementById('uiUpdateCheckBtn');
12846
+ var applyBtn = document.getElementById('uiUpdateApplyBtn');
12847
+ if (checkBtn) checkBtn.disabled = !!options.disableCheck;
12848
+ if (applyBtn) {
12849
+ applyBtn.style.display = showApply ? '' : 'none';
12850
+ applyBtn.disabled = !!options.disableApply;
12851
+ }
12852
+ }
12853
+
12854
+ function renderUiVersionState(data, opts) {
12855
+ var options = opts || {};
12856
+ var runningVersion = typeof data.runningVersion === 'string' ? data.runningVersion : '';
12857
+ var latestVersion = typeof data.latestVersion === 'string' ? data.latestVersion : '';
12858
+ var checkError = typeof data.checkError === 'string' ? data.checkError : '';
12859
+ var updateAvailable = !!data.updateAvailable;
12860
+ var latestCheckedAt = typeof data.latestCheckedAt === 'string' ? data.latestCheckedAt : '';
12861
+
12862
+ var versionEl = document.getElementById('labgateVersionValue');
12863
+ if (versionEl) {
12864
+ versionEl.textContent = runningVersion ? formatUiVersion(runningVersion) : 'Unknown';
12865
+ }
12866
+
12867
+ setUiUpdateButtonsState({
12868
+ showApply: updateAvailable && !!latestVersion,
12869
+ disableCheck: uiUpdateInProgress,
12870
+ disableApply: uiUpdateInProgress
12871
+ });
12872
+
12873
+ if (updateAvailable && latestVersion) {
12874
+ var updateCommand = typeof data.updateCommand === 'string' ? data.updateCommand : 'npm install -g labgate@latest';
12875
+ setUiUpdateStatus(formatUiVersion(latestVersion) + ' is available. Click "Update". If it fails, run "' + updateCommand + '".', 'warning');
12876
+ return;
12877
+ }
12878
+
12879
+ if (checkError) {
12880
+ setUiUpdateButtonsState({
12881
+ showApply: false,
12882
+ disableCheck: uiUpdateInProgress,
12883
+ disableApply: uiUpdateInProgress
12884
+ });
12885
+ setUiUpdateStatus('Update check unavailable: ' + checkError, 'error');
12886
+ return;
12887
+ }
12888
+
12889
+ if (latestVersion) {
12890
+ var checkedSuffix = latestCheckedAt ? (' Last checked: ' + new Date(latestCheckedAt).toLocaleTimeString() + '.') : '';
12891
+ setUiUpdateStatus('Up to date (' + formatUiVersion(latestVersion) + ').' + checkedSuffix, 'success');
12892
+ return;
12893
+ }
12894
+
12895
+ if (options.force) {
12896
+ setUiUpdateStatus('Update check finished with no version data.', 'warning');
12897
+ } else {
12898
+ setUiUpdateStatus('Click "Search for Update" to check npm for newer versions.', '');
12899
+ }
12900
+ }
12901
+
12902
+ function checkUiVersion(force) {
12903
+ var shouldForce = !!force;
12904
+ if (uiUpdateInProgress) return Promise.resolve();
12905
+ var checkBtn = document.getElementById('uiUpdateCheckBtn');
12906
+ var originalLabel = checkBtn ? checkBtn.textContent : '';
12907
+ if (checkBtn) {
12908
+ checkBtn.disabled = true;
12909
+ checkBtn.textContent = 'Checking...';
12910
+ }
12911
+ var endpoint = '/api/ui/version' + (shouldForce ? '?refresh=1' : '');
12912
+ return fetch(endpoint).then(parseApiResponse).then(function(resp) {
12913
+ var data = resp.data || {};
12914
+ if (!resp.ok || !data.ok) {
12915
+ throw new Error(data.error || ('HTTP ' + resp.status));
12916
+ }
12917
+ renderUiVersionState(data, { force: shouldForce });
12918
+ }).catch(function(err) {
12919
+ var msg = err && err.message ? err.message : String(err || 'unknown error');
12920
+ setUiUpdateStatus('Could not check for updates: ' + msg, 'error');
12921
+ if (shouldForce) showToast('Update check failed: ' + msg, 'error');
12922
+ }).finally(function() {
12923
+ if (checkBtn) {
12924
+ checkBtn.disabled = false;
12925
+ checkBtn.textContent = originalLabel || 'Search for Update';
12926
+ }
12927
+ if (!uiUpdateInProgress) {
12928
+ var applyBtn = document.getElementById('uiUpdateApplyBtn');
12929
+ if (applyBtn) applyBtn.disabled = false;
12930
+ }
12931
+ });
12932
+ }
12933
+
12934
+ function stopUiUpdateStatusPolling() {
12935
+ if (!uiUpdateStatusPollTimer) return;
12936
+ clearTimeout(uiUpdateStatusPollTimer);
12937
+ uiUpdateStatusPollTimer = null;
12938
+ }
12939
+
12940
+ function pollUiUpdateStatus() {
12941
+ return fetch('/api/ui/update/status').then(parseApiResponse).then(function(resp) {
12942
+ var data = resp.data || {};
12943
+ if (!resp.ok || !data.ok) {
12944
+ throw new Error(data.error || ('HTTP ' + resp.status));
12945
+ }
12946
+
12947
+ var status = String(data.status || 'idle');
12948
+ var latestVersion = typeof data.latestVersion === 'string' ? data.latestVersion : '';
12949
+ var error = typeof data.error === 'string' ? data.error : '';
12950
+
12951
+ if (status === 'running') {
12952
+ uiUpdateInProgress = true;
12953
+ setUiUpdateButtonsState({ showApply: true, disableCheck: true, disableApply: true });
12954
+ setUiUpdateStatus('Updating LabGate... write actions are temporarily locked.', 'warning');
12955
+ stopUiUpdateStatusPolling();
12956
+ uiUpdateStatusPollTimer = setTimeout(function() {
12957
+ pollUiUpdateStatus();
12958
+ }, 1500);
12959
+ return;
12960
+ }
12961
+
12962
+ uiUpdateInProgress = false;
12963
+ stopUiUpdateStatusPolling();
12964
+ if (status === 'success') {
12965
+ var next = latestVersion ? formatUiVersion(latestVersion) : 'the latest version';
12966
+ setUiUpdateStatus('Update complete (' + next + '). Restart `labgate ui`, then reload this page.', 'success');
12967
+ showToast('Update complete. Restart `labgate ui`.', 'success');
12968
+ setUiUpdateButtonsState({ showApply: false, disableCheck: false, disableApply: false });
12969
+ return;
12970
+ }
12971
+ if (status === 'error') {
12972
+ setUiUpdateStatus('Update failed: ' + (error || 'unknown error'), 'error');
12973
+ showToast('Update failed: ' + (error || 'unknown error'), 'error');
12974
+ setUiUpdateButtonsState({ showApply: true, disableCheck: false, disableApply: false });
12975
+ return;
12976
+ }
12977
+
12978
+ setUiUpdateButtonsState({ showApply: false, disableCheck: false, disableApply: false });
12979
+ }).catch(function(err) {
12980
+ uiUpdateInProgress = false;
12981
+ stopUiUpdateStatusPolling();
12982
+ var msg = err && err.message ? err.message : String(err || 'unknown error');
12983
+ setUiUpdateStatus('Could not read update status: ' + msg, 'error');
12984
+ setUiUpdateButtonsState({ showApply: true, disableCheck: false, disableApply: false });
12985
+ });
12986
+ }
12987
+
12988
+ function startUiUpdate() {
12989
+ if (uiUpdateInProgress) return;
12990
+ showConfirm({
12991
+ title: 'Update LabGate',
12992
+ message: 'LabGate will only update when no active sessions are running. Continue?',
12993
+ okLabel: 'Update',
12994
+ destructive: false
12995
+ }).then(function(ok) {
12996
+ if (!ok) return;
12997
+ uiUpdateInProgress = true;
12998
+ setUiUpdateButtonsState({ showApply: true, disableCheck: true, disableApply: true });
12999
+ setUiUpdateStatus('Starting update...', 'warning');
13000
+ fetch('/api/ui/update', {
13001
+ method: 'POST',
13002
+ headers: apiWriteHeaders(),
13003
+ body: '{}'
13004
+ }).then(parseApiResponse).then(function(resp) {
13005
+ var data = resp.data || {};
13006
+ if (!resp.ok || !data.ok) {
13007
+ var detail = data.error || ('HTTP ' + resp.status);
13008
+ if (data.code === 'in_use' && data.activeUsage) {
13009
+ var usage = data.activeUsage;
13010
+ detail = 'Stop active sessions first (' + (usage.containerSessions || 0) + ' sessions, '
13011
+ + (usage.webTerminalSessions || 0) + ' web terminals).';
13012
+ }
13013
+ throw new Error(detail);
13014
+ }
13015
+ pollUiUpdateStatus();
13016
+ }).catch(function(err) {
13017
+ uiUpdateInProgress = false;
13018
+ var msg = err && err.message ? err.message : String(err || 'unknown error');
13019
+ setUiUpdateStatus('Update blocked: ' + msg, 'error');
13020
+ setUiUpdateButtonsState({ showApply: true, disableCheck: false, disableApply: false });
13021
+ showToast('Update blocked: ' + msg, 'error');
13022
+ });
13023
+ });
13024
+ }
13025
+
12541
13026
  // ── Network mode switcher ────────────────────
12542
13027
  function updateNetSwitcher() {
12543
13028
  var mode = normalizeUiNetworkMode(config.network ? config.network.mode : 'host');
@@ -15065,6 +15550,7 @@ var slurmSystemAvailable = false;
15065
15550
  var slurmSidebarVisible = false;
15066
15551
  var slurmMissingCommands = [];
15067
15552
  var slurmSearchTimeout = null;
15553
+ var slurmScope = 'mine'; // 'mine' or 'all'
15068
15554
  var currentSlurmOutputJobId = null;
15069
15555
  var currentSlurmOutputStream = 'stdout';
15070
15556
  var slurmOutputFollowTimer = null;
@@ -15149,6 +15635,15 @@ function updateSlurmUIState(opts) {
15149
15635
  if (!(opts && opts.skipLoadJobs)) loadSlurmJobs();
15150
15636
  }
15151
15637
 
15638
+ function setSlurmScope(scope) {
15639
+ slurmScope = scope === 'all' ? 'all' : 'mine';
15640
+ var mineBtn = document.getElementById('slurmScopeMine');
15641
+ var allBtn = document.getElementById('slurmScopeAll');
15642
+ if (mineBtn) mineBtn.classList.toggle('active', slurmScope === 'mine');
15643
+ if (allBtn) allBtn.classList.toggle('active', slurmScope === 'all');
15644
+ loadSlurmJobs();
15645
+ }
15646
+
15152
15647
  function loadSlurmJobs() {
15153
15648
  if (!slurmEnabled || !slurmSidebarVisible) return;
15154
15649
  var el = document.getElementById('slurmJobsContent');
@@ -15157,58 +15652,69 @@ function loadSlurmJobs() {
15157
15652
  var url = '/api/slurm/jobs?limit=100';
15158
15653
  if (state) url += '&state=' + encodeURIComponent(state);
15159
15654
  if (search) url += '&search=' + encodeURIComponent(search);
15655
+ // Session-aware filtering
15656
+ if (slurmScope === 'mine') {
15657
+ var sid = getAttachedWebTerminalId();
15658
+ if (!sid) {
15659
+ el.innerHTML = '<div class="empty-state" style="padding:12px">Attach to a terminal session to view jobs for this session.</div>';
15660
+ return;
15661
+ }
15662
+ url += '&session_id=' + encodeURIComponent(sid);
15663
+ }
15160
15664
 
15161
15665
  fetch(url).then(function(r) { return r.json(); }).then(function(data) {
15162
15666
  if (!data.ok) {
15163
- el.innerHTML = '<div class="empty-state">Error loading SLURM jobs: ' + escapeHtml(data.error || 'unknown error') + '</div>';
15667
+ el.innerHTML = '<div class="empty-state" style="padding:12px">Error: ' + escapeHtml(data.error || 'unknown') + '</div>';
15164
15668
  return;
15165
15669
  }
15166
15670
  if (!data.jobs || data.jobs.length === 0) {
15167
- el.innerHTML = '<div class="empty-state">No SLURM jobs found</div>';
15671
+ var msg = slurmScope === 'mine' ? 'No jobs from this session' : 'No SLURM jobs found';
15672
+ el.innerHTML = '<div class="empty-state" style="padding:12px">' + msg + '</div>';
15168
15673
  return;
15169
15674
  }
15170
- renderSlurmJobTable(data.jobs, el);
15675
+ renderSlurmJobCards(data.jobs, el);
15171
15676
  }).catch(function(err) {
15172
- el.innerHTML = '<div class="empty-state">Error loading SLURM jobs: ' + escapeHtml(err.message) + '</div>';
15677
+ el.innerHTML = '<div class="empty-state" style="padding:12px">Error: ' + escapeHtml(err.message) + '</div>';
15173
15678
  });
15174
15679
  }
15175
15680
 
15176
15681
  var _slurmJobsCache = [];
15177
15682
 
15178
- function renderSlurmJobTable(jobs, el) {
15683
+ function renderSlurmJobCards(jobs, el) {
15179
15684
  _slurmJobsCache = jobs;
15180
- var html = '<table class="slurm-table"><thead><tr>';
15181
- html += '<th>Job ID</th><th>Name</th><th>State</th><th>Runtime</th>';
15182
- html += '<th>Partition</th><th>Nodes</th><th>Output</th><th>Notes</th><th>Actions</th>';
15183
- html += '</tr></thead><tbody>';
15184
-
15685
+ var currentSessionId = getAttachedWebTerminalId();
15686
+ var html = '<div class="slurm-job-grid">';
15185
15687
  for (var i = 0; i < jobs.length; i++) {
15186
15688
  var j = jobs[i];
15187
15689
  var runtime = computeRuntime(j);
15188
- var stateClass = 'slurm-state slurm-state-' + (j.state || 'UNKNOWN');
15189
-
15190
- html += '<tr>';
15191
- html += '<td style="font-family:var(--font-mono)">' + escapeHtml(j.job_id) + '</td>';
15192
- html += '<td>' + escapeHtml(j.name || '-') + '</td>';
15193
- html += '<td>';
15194
- if (j.state === 'RUNNING') html += '<span class="slurm-running-dot"></span>';
15195
- html += '<span class="' + stateClass + '">' + escapeHtml(j.state || 'UNKNOWN') + '</span>';
15196
- html += '</td>';
15197
- html += '<td style="font-family:var(--font-mono)">' + escapeHtml(runtime) + '</td>';
15198
- html += '<td>' + escapeHtml(j.partition || '-') + '</td>';
15199
- html += '<td>' + escapeHtml(j.nodes || '-') + '</td>';
15200
- html += '<td>' + renderOutputCell(j) + '</td>';
15201
- html += '<td class="slurm-notes-cell"><span class="slurm-notes-display" onclick="editSlurmNotes(\'' + escapeHtml(j.job_id) + '\')">';
15202
- html += j.notes ? escapeHtml(j.notes) : '<span style="color:var(--muted);font-style:italic">+ note</span>';
15203
- html += '</span></td>';
15204
- html += '<td>';
15690
+ var stateClass = 'slurm-job-state slurm-job-state-' + (j.state || 'UNKNOWN');
15691
+ var isMySession = currentSessionId && j.session_id === currentSessionId;
15692
+
15693
+ html += '<div class="slurm-job-card">';
15694
+ html += '<div class="slurm-job-card-row">';
15695
+ html += '<span class="slurm-job-id">' + escapeHtml(j.job_id) + '</span>';
15696
+ html += '<span class="' + stateClass + '">' + escapeHtml(j.state || '?') + '</span>';
15697
+ if (j.name) html += '<span class="slurm-job-name">' + escapeHtml(j.name) + '</span>';
15698
+ html += '</div>';
15699
+ html += '<div class="slurm-job-meta">';
15700
+ html += '<span>' + escapeHtml(runtime) + '</span>';
15701
+ if (j.partition) html += '<span>' + escapeHtml(j.partition) + '</span>';
15702
+ if (j.stdout_path) {
15703
+ html += '<button class="slurm-job-action-btn" onclick="event.stopPropagation();openSlurmOutput(\'' + escapeHtml(j.job_id) + '\',\'stdout\')" title="View stdout">out</button>';
15704
+ }
15705
+ if (j.stderr_path) {
15706
+ html += '<button class="slurm-job-action-btn" onclick="event.stopPropagation();openSlurmOutput(\'' + escapeHtml(j.job_id) + '\',\'stderr\')" title="View stderr">err</button>';
15707
+ }
15708
+ html += '<div class="slurm-job-actions">';
15709
+ html += '<button class="slurm-job-action-btn" onclick="event.stopPropagation();editSlurmNotes(\'' + escapeHtml(j.job_id) + '\')" title="Notes">' + (j.notes ? '\u270E' : '+note') + '</button>';
15205
15710
  if (j.state === 'PENDING' || j.state === 'RUNNING') {
15206
- html += '<button class="slurm-cancel-btn" onclick="cancelSlurmJob(\'' + escapeHtml(j.job_id) + '\')">Cancel</button>';
15711
+ html += '<button class="slurm-job-action-btn danger" onclick="event.stopPropagation();cancelSlurmJob(\'' + escapeHtml(j.job_id) + '\')" title="Cancel job">\u2717</button>';
15207
15712
  }
15208
- html += '</td>';
15209
- html += '</tr>';
15713
+ html += '</div>';
15714
+ html += '</div>';
15715
+ html += '</div>';
15210
15716
  }
15211
- html += '</tbody></table>';
15717
+ html += '</div>';
15212
15718
  el.innerHTML = html;
15213
15719
  }
15214
15720
 
@@ -15530,7 +16036,8 @@ function applyFieldLocks(data) {
15530
16036
  'audit.enabled': 'auditEnabled',
15531
16037
  'audit.log_dir': 'logDir',
15532
16038
  'session_timeout_hours': 'timeout',
15533
- 'slurm.enabled': 'slurmEnabled'
16039
+ 'slurm.enabled': 'slurmEnabled',
16040
+ 'headless.claude_run_with_allowed_permissions': 'headlessAllowedPermissions'
15534
16041
  };
15535
16042
  locked.forEach(function(fieldPath) {
15536
16043
  var elId = fieldMap[fieldPath];
@@ -15764,6 +16271,7 @@ function loadAdminLicense() {
15764
16271
  // ── Init ─────────────────────────────────────
15765
16272
  loadConfig();
15766
16273
  loadConfigPath();
16274
+ checkUiVersion(false);
15767
16275
  setViewMode('terminal_attached', '');
15768
16276
  loadSessions();
15769
16277
  updateNotifyButton();