let-them-talk 4.0.2 → 4.3.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.
package/dashboard.html CHANGED
@@ -356,6 +356,22 @@
356
356
  .agent-badge.sleeping { background: var(--orange-dim); color: var(--orange); }
357
357
  .agent-badge.dead { background: var(--red-dim); color: var(--red); }
358
358
 
359
+ .agent-status-intent {
360
+ font-size: 11px;
361
+ color: var(--text-dim);
362
+ padding: 3px 0;
363
+ white-space: nowrap;
364
+ overflow: hidden;
365
+ text-overflow: ellipsis;
366
+ font-style: italic;
367
+ }
368
+
369
+ .agent-status-intent::before {
370
+ content: '\1F4AD';
371
+ margin-right: 4px;
372
+ font-style: normal;
373
+ }
374
+
359
375
  .listen-badge {
360
376
  font-size: 9px;
361
377
  padding: 2px 6px;
@@ -478,6 +494,90 @@
478
494
  border-color: var(--red);
479
495
  }
480
496
 
497
+ .respawn-btn {
498
+ background: var(--green-dim);
499
+ color: var(--green);
500
+ border: 1px solid rgba(63, 185, 80, 0.3);
501
+ padding: 4px 10px;
502
+ border-radius: 6px;
503
+ cursor: pointer;
504
+ font-size: 11px;
505
+ font-weight: 500;
506
+ margin-top: 4px;
507
+ width: 100%;
508
+ text-align: center;
509
+ }
510
+ .respawn-btn:hover {
511
+ background: rgba(63, 185, 80, 0.25);
512
+ border-color: var(--green);
513
+ }
514
+
515
+ /* Respawn modal */
516
+ .respawn-modal-overlay {
517
+ position: fixed;
518
+ top: 0; left: 0; right: 0; bottom: 0;
519
+ background: rgba(0,0,0,0.7);
520
+ z-index: 10000;
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: center;
524
+ }
525
+ .respawn-modal {
526
+ background: var(--bg-secondary);
527
+ border: 1px solid var(--border);
528
+ border-radius: 12px;
529
+ padding: 20px;
530
+ max-width: 700px;
531
+ width: 90%;
532
+ max-height: 80vh;
533
+ display: flex;
534
+ flex-direction: column;
535
+ }
536
+ .respawn-modal h3 {
537
+ margin: 0 0 12px 0;
538
+ color: var(--green);
539
+ font-size: 16px;
540
+ }
541
+ .respawn-modal-prompt {
542
+ background: var(--bg-primary);
543
+ border: 1px solid var(--border);
544
+ border-radius: 8px;
545
+ padding: 12px;
546
+ font-family: monospace;
547
+ font-size: 12px;
548
+ color: var(--text-primary);
549
+ white-space: pre-wrap;
550
+ overflow-y: auto;
551
+ flex: 1;
552
+ max-height: 50vh;
553
+ user-select: all;
554
+ }
555
+ .respawn-modal-actions {
556
+ display: flex;
557
+ gap: 8px;
558
+ margin-top: 12px;
559
+ justify-content: flex-end;
560
+ }
561
+ .respawn-modal-actions button {
562
+ padding: 6px 16px;
563
+ border-radius: 6px;
564
+ border: 1px solid var(--border);
565
+ cursor: pointer;
566
+ font-size: 12px;
567
+ font-weight: 500;
568
+ }
569
+ .respawn-copy-btn {
570
+ background: var(--green-dim);
571
+ color: var(--green);
572
+ border-color: rgba(63, 185, 80, 0.3) !important;
573
+ }
574
+ .respawn-copy-btn:hover { background: rgba(63, 185, 80, 0.25); }
575
+ .respawn-close-btn {
576
+ background: var(--bg-tertiary);
577
+ color: var(--text-secondary);
578
+ }
579
+ .respawn-close-btn:hover { background: var(--border); }
580
+
481
581
  /* ===== PROJECT SWITCHER ===== */
482
582
  .project-switcher {
483
583
  padding: 12px;
@@ -757,6 +857,49 @@
757
857
  .msg-to { font-size: 12px; color: var(--text-dim); }
758
858
  .msg-time { font-size: 10px; color: var(--text-muted); }
759
859
 
860
+ .badge-channel {
861
+ background: var(--purple-dim);
862
+ color: var(--purple);
863
+ font-size: 9px;
864
+ padding: 1px 5px;
865
+ border-radius: 6px;
866
+ font-weight: 600;
867
+ cursor: pointer;
868
+ }
869
+
870
+ .badge-channel:hover { opacity: 0.8; }
871
+
872
+ .channel-filter-bar {
873
+ display: flex;
874
+ gap: 4px;
875
+ padding: 4px 12px;
876
+ overflow-x: auto;
877
+ flex-wrap: nowrap;
878
+ scrollbar-width: none;
879
+ }
880
+
881
+ .channel-filter-bar::-webkit-scrollbar { display: none; }
882
+
883
+ .channel-tab {
884
+ font-size: 11px;
885
+ padding: 3px 10px;
886
+ border-radius: 12px;
887
+ border: 1px solid var(--border);
888
+ background: var(--surface-2);
889
+ color: var(--text-dim);
890
+ cursor: pointer;
891
+ white-space: nowrap;
892
+ transition: all 0.15s;
893
+ }
894
+
895
+ .channel-tab:hover { border-color: var(--border-light); color: var(--text); }
896
+
897
+ .channel-tab.active {
898
+ background: var(--purple-dim);
899
+ color: var(--purple);
900
+ border-color: var(--purple);
901
+ }
902
+
760
903
  .msg-badges {
761
904
  display: flex;
762
905
  gap: 4px;
@@ -2254,6 +2397,28 @@
2254
2397
  color: var(--text-dim);
2255
2398
  }
2256
2399
 
2400
+ .pipeline-bar {
2401
+ height: 4px;
2402
+ background: var(--surface-3);
2403
+ border-radius: 2px;
2404
+ margin-bottom: 10px;
2405
+ overflow: hidden;
2406
+ }
2407
+
2408
+ .pipeline-bar-fill {
2409
+ height: 100%;
2410
+ border-radius: 2px;
2411
+ transition: width 0.3s ease;
2412
+ }
2413
+
2414
+ .pipeline-meta {
2415
+ font-size: 10px;
2416
+ color: var(--text-muted);
2417
+ display: flex;
2418
+ gap: 12px;
2419
+ margin-bottom: 8px;
2420
+ }
2421
+
2257
2422
  .pipeline-steps {
2258
2423
  display: flex;
2259
2424
  gap: 0;
@@ -2379,6 +2544,63 @@
2379
2544
  .docs-section { padding: 14px 16px; }
2380
2545
  }
2381
2546
 
2547
+ /* ===== DECISIONS LOG ===== */
2548
+ .decisions-section { margin-bottom: 16px; }
2549
+ .decisions-header {
2550
+ display: flex;
2551
+ align-items: center;
2552
+ justify-content: space-between;
2553
+ margin-bottom: 10px;
2554
+ }
2555
+ .decisions-header h3 { font-size: 15px; font-weight: 700; color: var(--accent); margin: 0; }
2556
+ .decisions-count { font-size: 11px; color: var(--text-muted); }
2557
+
2558
+ .decision-card {
2559
+ background: var(--surface);
2560
+ border: 1px solid var(--border);
2561
+ border-left: 3px solid var(--accent);
2562
+ border-radius: 8px;
2563
+ padding: 12px 14px;
2564
+ margin-bottom: 8px;
2565
+ transition: border-color 0.15s;
2566
+ }
2567
+ .decision-card:hover { border-color: var(--border-light); }
2568
+
2569
+ .decision-topic {
2570
+ font-size: 9px;
2571
+ font-weight: 700;
2572
+ text-transform: uppercase;
2573
+ letter-spacing: 0.5px;
2574
+ color: var(--purple);
2575
+ background: var(--purple-dim);
2576
+ padding: 1px 6px;
2577
+ border-radius: 6px;
2578
+ display: inline-block;
2579
+ margin-bottom: 4px;
2580
+ }
2581
+
2582
+ .decision-text {
2583
+ font-size: 13px;
2584
+ font-weight: 600;
2585
+ color: var(--text);
2586
+ margin-bottom: 4px;
2587
+ line-height: 1.4;
2588
+ }
2589
+
2590
+ .decision-reasoning {
2591
+ font-size: 12px;
2592
+ color: var(--text-dim);
2593
+ line-height: 1.5;
2594
+ margin-bottom: 6px;
2595
+ }
2596
+
2597
+ .decision-meta {
2598
+ font-size: 10px;
2599
+ color: var(--text-muted);
2600
+ display: flex;
2601
+ gap: 10px;
2602
+ }
2603
+
2382
2604
  /* ===== VIRTUAL OFFICE ===== */
2383
2605
  .office-area {
2384
2606
  flex: 1;
@@ -3294,9 +3516,11 @@
3294
3516
  <div class="search-bar" id="search-bar">
3295
3517
  <input class="search-input" id="search-input" placeholder="Search messages... ( / )" oninput="onSearch()">
3296
3518
  <button id="search-all-btn" onclick="toggleSearchAll()" title="Search across all projects" style="background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:10px;cursor:pointer;color:var(--text-muted);white-space:nowrap;transition:all 0.2s">All Projects</button>
3519
+ <button id="deep-search-btn" onclick="deepSearch()" title="Search full history (server-side, includes compacted messages and channels)" style="background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:10px;cursor:pointer;color:var(--text-muted);white-space:nowrap;transition:all 0.2s">Deep Search</button>
3297
3520
  <span class="search-count" id="search-count"></span>
3298
3521
  <button class="compact-toggle" id="compact-toggle" onclick="toggleCompactMode()" title="Toggle compact view">Compact</button>
3299
3522
  </div>
3523
+ <div class="channel-filter-bar" id="channel-filter-bar" style="display:none"></div>
3300
3524
  <div class="pinned-section" id="pinned-section">
3301
3525
  <div class="pinned-header" onclick="togglePinnedSection()"><span>Pinned Messages</span><span class="pinned-toggle" id="pinned-toggle">Hide</span></div>
3302
3526
  <div id="pinned-list"></div>
@@ -3320,6 +3544,7 @@
3320
3544
  <span style="color:var(--text-dim);font-size:9px;min-width:16px" id="office-speed-val">4</span>
3321
3545
  <span style="color:var(--text-muted);font-size:9px;opacity:0.6;margin-left:8px" title="WASD=Move, Right-Drag=Look, Scroll=Dolly, Shift=Fast, Q/E=Down/Up">WASD + Mouse</span>
3322
3546
  <span style="color:var(--text-dim);font-size:10px" id="office-fps"></span>
3547
+ <button id="fullscreen-btn" onclick="toggleFullscreen()" style="background:var(--surface-2);border:1px solid var(--border);color:var(--text-dim);padding:3px 10px;border-radius:6px;font-size:10px;cursor:pointer;font-family:inherit;margin-left:8px;transition:all 0.2s" title="Toggle fullscreen (End key to exit)">&#x26F6; Fullscreen</button>
3323
3548
  </div>
3324
3549
  <div class="office-canvas-wrap">
3325
3550
  <div id="office-3d-container"></div>
@@ -3332,7 +3557,7 @@
3332
3557
  </div>
3333
3558
  <div class="launch-area" id="launch-area"></div>
3334
3559
  <div class="stats-area" id="stats-area"></div>
3335
- <div class="docs-area" id="docs-area"></div>
3560
+ <div class="docs-area" id="docs-area"><div id="decisions-panel" style="max-width:820px;margin:0 auto"></div><div id="docs-content"></div></div>
3336
3561
  <button class="scroll-bottom" id="scroll-bottom" onclick="scrollToBottom()">&#x2193;<span class="new-count" id="new-msg-count" style="display:none">0</span></button>
3337
3562
  <div class="typing-bar" id="typing-bar"></div>
3338
3563
 
@@ -3765,6 +3990,7 @@ function lttFetch(url, opts) {
3765
3990
  }
3766
3991
 
3767
3992
  var activeThread = null;
3993
+ var activeChannel = null; // null = all channels
3768
3994
  var activeProject = ''; // empty = default/local
3769
3995
  var cachedHistory = [];
3770
3996
  var cachedAgents = {};
@@ -4036,8 +4262,10 @@ function renderAgents(agents) {
4036
4262
  nudgeHtml = '<button class="nudge-btn" onclick="sendNudge(\'' + escapeHtml(name) + '\')">Send Nudge</button>';
4037
4263
  }
4038
4264
  var removeHtml = '';
4265
+ var respawnHtml = '';
4039
4266
  if (state === 'dead') {
4040
4267
  removeHtml = '<button class="remove-agent-btn" onclick="event.stopPropagation();removeAgent(\'' + escapeHtml(name) + '\')" title="Remove this agent">Remove</button>';
4268
+ respawnHtml = '<button class="respawn-btn" onclick="event.stopPropagation();respawnAgent(\'' + escapeHtml(name) + '\')" title="Generate resume prompt for this agent">&#x1F504; Respawn</button>';
4041
4269
  }
4042
4270
 
4043
4271
  // Listening status — simplified: skip for dead (already shown via badge)
@@ -4077,8 +4305,10 @@ function renderAgents(agents) {
4077
4305
  '<span class="agent-activity-icon ' + state + '"></span>' +
4078
4306
  activityText +
4079
4307
  '</div>' +
4308
+ (info.current_status ? '<div class="agent-status-intent" title="' + escapeHtml(info.current_status) + '">' + escapeHtml(info.current_status) + '</div>' : '') +
4080
4309
  listenHtml +
4081
4310
  nudgeHtml +
4311
+ respawnHtml +
4082
4312
  removeHtml +
4083
4313
  '</div>';
4084
4314
  }
@@ -4123,6 +4353,83 @@ function removeAgent(agentName) {
4123
4353
  }).catch(function(e) { console.error('Remove agent failed:', e); });
4124
4354
  }
4125
4355
 
4356
+ // ==================== RESPAWN AGENT ====================
4357
+
4358
+ function respawnAgent(agentName) {
4359
+ var pq = projectParam();
4360
+ var sep = pq ? '&' : '?';
4361
+ lttFetch('/api/agents/' + encodeURIComponent(agentName) + '/respawn-prompt' + pq, {
4362
+ method: 'GET'
4363
+ }).then(function(r) {
4364
+ if (!r.ok) throw new Error('API returned ' + r.status);
4365
+ return r.json();
4366
+ }).then(function(res) {
4367
+ if (res.error) {
4368
+ alert('Respawn failed: ' + res.error);
4369
+ return;
4370
+ }
4371
+ showRespawnModal(agentName, res.prompt || res.resume_prompt || 'No prompt available');
4372
+ }).catch(function(e) {
4373
+ // Fallback: generate a basic prompt client-side if API not ready
4374
+ var fallbackPrompt = generateFallbackRespawnPrompt(agentName);
4375
+ showRespawnModal(agentName, fallbackPrompt);
4376
+ });
4377
+ }
4378
+
4379
+ function generateFallbackRespawnPrompt(agentName) {
4380
+ return 'Register as \'' + agentName + '\', then call get_briefing() and listen_group() to rejoin the conversation. ' +
4381
+ 'You are resuming a previous session — call get_compressed_history() to catch up on what you missed. ' +
4382
+ 'Check your workspace with workspace_read() for any saved state. ' +
4383
+ 'Then call listen_group() and respond to any pending messages.';
4384
+ }
4385
+
4386
+ function showRespawnModal(agentName, prompt) {
4387
+ // Remove existing modal if any
4388
+ dismissRespawnModal();
4389
+
4390
+ var overlay = document.createElement('div');
4391
+ overlay.className = 'respawn-modal-overlay';
4392
+ overlay.id = 'respawn-modal';
4393
+ overlay.onclick = function(e) { if (e.target === overlay) dismissRespawnModal(); };
4394
+
4395
+ overlay.innerHTML =
4396
+ '<div class="respawn-modal">' +
4397
+ '<h3>&#x1F504; Respawn ' + escapeHtml(agentName) + '</h3>' +
4398
+ '<p style="color:var(--text-secondary);font-size:12px;margin:0 0 8px">Copy this prompt and paste it into a fresh CLI terminal to respawn the agent:</p>' +
4399
+ '<div class="respawn-modal-prompt" id="respawn-prompt-text">' + escapeHtml(prompt) + '</div>' +
4400
+ '<div class="respawn-modal-actions">' +
4401
+ '<button class="respawn-close-btn" onclick="dismissRespawnModal()">Close</button>' +
4402
+ '<button class="respawn-copy-btn" onclick="copyRespawnPrompt()">&#x1F4CB; Copy to Clipboard</button>' +
4403
+ '</div>' +
4404
+ '</div>';
4405
+
4406
+ document.body.appendChild(overlay);
4407
+ }
4408
+
4409
+ function dismissRespawnModal() {
4410
+ var modal = document.getElementById('respawn-modal');
4411
+ if (modal) modal.remove();
4412
+ }
4413
+
4414
+ function copyRespawnPrompt() {
4415
+ var el = document.getElementById('respawn-prompt-text');
4416
+ if (!el) return;
4417
+ var text = el.textContent;
4418
+ navigator.clipboard.writeText(text).then(function() {
4419
+ var btn = document.querySelector('.respawn-copy-btn');
4420
+ if (btn) {
4421
+ btn.textContent = '\u2705 Copied!';
4422
+ setTimeout(function() { btn.innerHTML = '&#x1F4CB; Copy to Clipboard'; }, 2000);
4423
+ }
4424
+ }).catch(function() {
4425
+ // Fallback: select all text
4426
+ var range = document.createRange();
4427
+ range.selectNodeContents(el);
4428
+ window.getSelection().removeAllRanges();
4429
+ window.getSelection().addRange(range);
4430
+ });
4431
+ }
4432
+
4126
4433
  // ==================== INJECT TARGETS ====================
4127
4434
 
4128
4435
  var lastAgentKeys = '';
@@ -4273,6 +4580,14 @@ function renderMessages(messages) {
4273
4580
  filtered = filtered.filter(function(m) { return !!bookmarks[m.id]; });
4274
4581
  }
4275
4582
 
4583
+ // Channel filter
4584
+ if (activeChannel) {
4585
+ filtered = filtered.filter(function(m) {
4586
+ if (activeChannel === 'general') return !m.channel || m.channel === 'general';
4587
+ return m.channel === activeChannel;
4588
+ });
4589
+ }
4590
+
4276
4591
  // Search filter
4277
4592
  if (searchQuery) {
4278
4593
  filtered = filtered.filter(function(m) {
@@ -4333,6 +4648,7 @@ function renderMessages(messages) {
4333
4648
  var groupClass = isGrouped ? ' grouped' : '';
4334
4649
 
4335
4650
  var badges = '';
4651
+ if (m.channel && m.channel !== 'general') badges += '<span class="badge-channel" onclick="filterChannel(\'' + escapeHtml(m.channel) + '\')" title="Channel: #' + escapeHtml(m.channel) + '">#' + escapeHtml(m.channel) + '</span>';
4336
4652
  if (m.acked) badges += '<span class="badge badge-ack">ACK</span>';
4337
4653
  if (m.thread_id) badges += '<span class="badge badge-thread">Thread</span>';
4338
4654
  if (m.edited) badges += '<span class="badge" style="background:var(--orange-dim);color:var(--orange)" title="Edited ' + (m.edited_at ? new Date(m.edited_at).toLocaleString() : '') + '">edited</span>';
@@ -4430,6 +4746,39 @@ function clearThreadFilter() {
4430
4746
  renderMessages(cachedHistory);
4431
4747
  }
4432
4748
 
4749
+ function filterChannel(ch) {
4750
+ activeChannel = activeChannel === ch ? null : ch;
4751
+ lastMessageCount = 0;
4752
+ renderChannelBar(cachedHistory);
4753
+ renderMessages(cachedHistory);
4754
+ }
4755
+
4756
+ function renderChannelBar(messages) {
4757
+ var bar = document.getElementById('channel-filter-bar');
4758
+ if (!messages || !messages.length) { bar.style.display = 'none'; return; }
4759
+
4760
+ // Collect unique channels from messages
4761
+ var channels = {};
4762
+ for (var i = 0; i < messages.length; i++) {
4763
+ var ch = messages[i].channel || 'general';
4764
+ channels[ch] = (channels[ch] || 0) + 1;
4765
+ }
4766
+
4767
+ var keys = Object.keys(channels);
4768
+ // Only show channel bar if there are non-general channels
4769
+ if (keys.length <= 1 && keys[0] === 'general') { bar.style.display = 'none'; return; }
4770
+
4771
+ bar.style.display = 'flex';
4772
+ var html = '<div class="channel-tab' + (!activeChannel ? ' active' : '') + '" onclick="filterChannel(null)">All</div>';
4773
+ keys.sort();
4774
+ for (var j = 0; j < keys.length; j++) {
4775
+ var name = keys[j];
4776
+ var active = activeChannel === name ? ' active' : '';
4777
+ html += '<div class="channel-tab' + active + '" onclick="filterChannel(\'' + escapeHtml(name) + '\')">#' + escapeHtml(name) + ' <span style="opacity:0.5;font-size:9px">' + channels[name] + '</span></div>';
4778
+ }
4779
+ bar.innerHTML = html;
4780
+ }
4781
+
4433
4782
  function toggleSidebar() {
4434
4783
  document.getElementById('sidebar').classList.toggle('open');
4435
4784
  document.getElementById('sidebar-overlay').classList.toggle('open');
@@ -4500,6 +4849,33 @@ function onSearch() {
4500
4849
  renderMessages(cachedHistory);
4501
4850
  }
4502
4851
 
4852
+ function deepSearch() {
4853
+ var query = document.getElementById('search-input').value.trim();
4854
+ if (query.length < 2) return;
4855
+ var pq = activeProject ? '&project=' + encodeURIComponent(activeProject) : '';
4856
+ var countEl = document.getElementById('search-count');
4857
+ countEl.textContent = 'Searching...';
4858
+ lttFetch('/api/search?q=' + encodeURIComponent(query) + '&limit=100' + pq).then(function(r) { return r.json(); }).then(function(data) {
4859
+ if (data.error) { countEl.textContent = data.error; return; }
4860
+ countEl.textContent = data.results_count + ' deep result' + (data.results_count !== 1 ? 's' : '');
4861
+ // Convert search results to message format for renderMessages
4862
+ var messages = data.results.map(function(r) {
4863
+ return { id: r.id, from: r.from, to: r.to, content: r.preview, timestamp: r.timestamp, channel: r.channel || null };
4864
+ });
4865
+ lastMessageCount = 0;
4866
+ var el = document.getElementById('messages');
4867
+ if (!messages.length) {
4868
+ el.innerHTML = '<div class="empty-state"><div class="empty-icon">&#x1f50d;</div><div class="empty-text">No results for "' + escapeHtml(query) + '"</div><div class="empty-sub">Searched full history including channels</div></div>';
4869
+ return;
4870
+ }
4871
+ // Render search results directly (don't overwrite cachedHistory)
4872
+ searchQuery = query.toLowerCase();
4873
+ renderMessages(messages);
4874
+ }).catch(function(e) {
4875
+ countEl.textContent = 'Search failed';
4876
+ });
4877
+ }
4878
+
4503
4879
  // ==================== READ RECEIPTS ====================
4504
4880
 
4505
4881
  var cachedReadReceipts = {};
@@ -5025,17 +5401,11 @@ function exportConversation() {
5025
5401
 
5026
5402
  function exportJSON() {
5027
5403
  if (!cachedHistory.length) return;
5028
- var data = cachedHistory.map(function(m) {
5029
- return { id: m.id, from: m.from, to: m.to, content: m.content, timestamp: m.timestamp, thread_id: m.thread_id || null };
5030
- });
5031
- var json = JSON.stringify(data, null, 2);
5032
- var blob = new Blob([json], { type: 'application/json' });
5033
- var url = URL.createObjectURL(blob);
5404
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
5034
5405
  var a = document.createElement('a');
5035
- a.href = url;
5036
- a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '.json';
5406
+ a.href = '/api/export-json' + pq;
5407
+ a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '-full.json';
5037
5408
  a.click();
5038
- URL.revokeObjectURL(url);
5039
5409
  }
5040
5410
 
5041
5411
  // ==================== VIEW SWITCHING ====================
@@ -5061,12 +5431,33 @@ function switchView(view) {
5061
5431
  document.getElementById('stats-area').classList.toggle('visible', view === 'stats');
5062
5432
  document.getElementById('docs-area').classList.toggle('visible', view === 'docs');
5063
5433
  document.getElementById('search-bar').style.display = view === 'messages' ? 'flex' : 'none';
5434
+ document.getElementById('channel-filter-bar').style.display = view === 'messages' && activeChannel !== undefined ? '' : 'none';
5435
+ if (view === 'messages') renderChannelBar(cachedHistory);
5064
5436
  if (view === 'tasks') fetchTasks();
5065
5437
  if (view === 'workspaces') fetchWorkspaces();
5066
5438
  if (view === 'workflows') fetchWorkflows();
5067
5439
  if (view === 'docs') renderDocs();
5068
5440
  if (view === 'office') {
5069
- if (window.office3dStart) window.office3dStart();
5441
+ if (window.office3dStart) {
5442
+ window.office3dStart();
5443
+ } else {
5444
+ // Show helpful error if 3D engine failed to load (Three.js missing)
5445
+ var officeEl = document.getElementById('office-area');
5446
+ if (officeEl && !officeEl.querySelector('.office-error')) {
5447
+ var errDiv = document.createElement('div');
5448
+ errDiv.className = 'office-error';
5449
+ errDiv.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text-dim);text-align:center;padding:40px';
5450
+ errDiv.innerHTML = '<div style="font-size:48px;margin-bottom:16px">&#x1f3d7;</div>' +
5451
+ '<div style="font-size:16px;font-weight:600;color:var(--text);margin-bottom:8px">3D Hub loading...</div>' +
5452
+ '<div style="font-size:12px;max-width:400px;line-height:1.6">If the 3D world doesn\'t appear, Three.js may not have loaded correctly. Try refreshing the page. If the problem persists, check the browser console for errors.</div>';
5453
+ officeEl.appendChild(errDiv);
5454
+ // Remove error message once 3D loads
5455
+ var checkInterval = setInterval(function() {
5456
+ if (window.office3dStart) { errDiv.remove(); clearInterval(checkInterval); window.office3dStart(); }
5457
+ }, 2000);
5458
+ setTimeout(function() { clearInterval(checkInterval); }, 30000);
5459
+ }
5460
+ }
5070
5461
  }
5071
5462
  if (view === 'launch') renderLaunchPanel();
5072
5463
  if (view === 'stats') fetchStats();
@@ -5162,9 +5553,20 @@ function buildTaskCard(t) {
5162
5553
  '<option value="blocked"' + (t.status === 'blocked' ? ' selected' : '') + '>Blocked</option>' +
5163
5554
  '</select>';
5164
5555
 
5556
+ var badgesHtml = '';
5557
+ if (t.escalated_at) badgesHtml += '<span style="font-size:9px;padding:1px 5px;border-radius:6px;font-weight:600;background:var(--red-dim);color:var(--red)">ESCALATED</span>';
5558
+ if (t.channel) badgesHtml += '<span class="badge-channel" onclick="filterChannel(\'' + escapeHtml(t.channel) + '\')">#' + escapeHtml(t.channel) + '</span>';
5559
+
5560
+ var notesHtml = '';
5561
+ if (t.notes && t.notes.length) {
5562
+ var lastNote = t.notes[t.notes.length - 1];
5563
+ notesHtml = '<div style="font-size:10px;color:var(--text-muted);margin-top:4px;font-style:italic">' + escapeHtml(lastNote.by) + ': ' + escapeHtml((lastNote.text || '').substring(0, 80)) + '</div>';
5564
+ }
5565
+
5165
5566
  return '<div class="task-card" draggable="true" data-task-id="' + t.id + '" ondragstart="onTaskDragStart(event)" ondragend="onTaskDragEnd(event)">' +
5166
- '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + '</div>' +
5567
+ '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + (badgesHtml ? ' ' + badgesHtml : '') + '</div>' +
5167
5568
  (t.description ? '<div class="task-desc">' + escapeHtml(t.description) + '</div>' : '') +
5569
+ notesHtml +
5168
5570
  '<div class="task-footer">' +
5169
5571
  assigneeHtml +
5170
5572
  statusOpts +
@@ -5815,22 +6217,41 @@ function renderWorkflows(workflows) {
5815
6217
  var pct = Math.round((doneCount / wf.steps.length) * 100);
5816
6218
  var statusColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
5817
6219
 
6220
+ var barColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
6221
+ var metaHtml = '<div class="pipeline-meta">';
6222
+ if (wf.created_by) metaHtml += '<span>Created by ' + escapeHtml(wf.created_by) + '</span>';
6223
+ if (wf.created_at) metaHtml += '<span>' + timeAgo(wf.created_at) + '</span>';
6224
+ if (wf.updated_at && wf.updated_at !== wf.created_at) metaHtml += '<span>Updated ' + timeAgo(wf.updated_at) + '</span>';
6225
+ metaHtml += '</div>';
6226
+
5818
6227
  html += '<div class="pipeline">' +
5819
6228
  '<div class="pipeline-header">' +
5820
6229
  '<div class="pipeline-name">' + escapeHtml(wf.name) + ' <span style="font-size:11px;color:' + statusColor + ';font-weight:600">' + escapeHtml(wf.status) + '</span></div>' +
5821
6230
  '<div class="pipeline-progress">' + doneCount + '/' + wf.steps.length + ' (' + pct + '%)</div>' +
5822
6231
  '</div>' +
6232
+ metaHtml +
6233
+ '<div class="pipeline-bar"><div class="pipeline-bar-fill" style="width:' + pct + '%;background:' + barColor + '"></div></div>' +
5823
6234
  '<div class="pipeline-steps">';
5824
6235
 
5825
6236
  for (var j = 0; j < wf.steps.length; j++) {
5826
6237
  var s = wf.steps[j];
5827
6238
  var stepColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : 'var(--text-muted)';
5828
6239
  if (j > 0) html += '<span class="step-arrow">&rarr;</span>';
6240
+ var stepTimeHtml = '';
6241
+ if (s.status === 'done' && s.started_at && s.completed_at) {
6242
+ var dur = Math.round((new Date(s.completed_at) - new Date(s.started_at)) / 60000);
6243
+ stepTimeHtml = '<div style="font-size:9px;color:var(--text-muted);margin-top:2px">' + (dur > 0 ? dur + 'm' : '<1m') + '</div>';
6244
+ } else if (s.status === 'in_progress' && s.started_at) {
6245
+ var elapsed = Math.round((Date.now() - new Date(s.started_at)) / 60000);
6246
+ stepTimeHtml = '<div style="font-size:9px;color:var(--accent);margin-top:2px">' + elapsed + 'm elapsed</div>';
6247
+ }
6248
+
5829
6249
  html += '<div class="step-card ' + s.status + '" title="' + escapeHtml(s.notes || '') + '">' +
5830
6250
  '<span class="step-num">' + s.id + '</span>' +
5831
6251
  '<span style="font-size:10px;color:' + stepColor + ';font-weight:600;text-transform:uppercase">' + s.status.replace('_', ' ') + '</span>' +
5832
6252
  '<div class="step-desc">' + escapeHtml(s.description) + '</div>' +
5833
6253
  (s.assignee ? '<div class="step-assignee">&#x1f464; ' + escapeHtml(s.assignee) + '</div>' : '') +
6254
+ stepTimeHtml +
5834
6255
  '</div>';
5835
6256
  }
5836
6257
  html += '</div>';
@@ -6111,6 +6532,7 @@ function poll() {
6111
6532
  renderAgentStats();
6112
6533
  renderThreads(cachedHistory);
6113
6534
  if (!replayActive) renderMessages(cachedHistory);
6535
+ renderChannelBar(cachedHistory);
6114
6536
  renderPinnedMessages();
6115
6537
  renderBookmarksSidebar();
6116
6538
  fetchActivity();
@@ -6751,8 +7173,42 @@ function showConvTemplate(tid){var pq=activeProject?'?project='+encodeURICompone
6751
7173
 
6752
7174
  // ==================== v3.6: DOCS VIEW ====================
6753
7175
 
7176
+ function fetchDecisions() {
7177
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
7178
+ lttFetch('/api/decisions' + pq).then(function(r) { return r.json(); }).then(function(decisions) {
7179
+ var el = document.getElementById('decisions-panel');
7180
+ if (!decisions || !decisions.length) {
7181
+ el.innerHTML = '';
7182
+ return;
7183
+ }
7184
+ // Show most recent first
7185
+ var sorted = decisions.slice().reverse();
7186
+ var html = '<div class="decisions-section">' +
7187
+ '<div class="decisions-header">' +
7188
+ '<h3>Team Decisions</h3>' +
7189
+ '<span class="decisions-count">' + decisions.length + ' decision' + (decisions.length !== 1 ? 's' : '') + '</span>' +
7190
+ '</div>';
7191
+ for (var i = 0; i < sorted.length; i++) {
7192
+ var d = sorted[i];
7193
+ var dateStr = d.decided_at ? new Date(d.decided_at).toLocaleString() : '';
7194
+ html += '<div class="decision-card">' +
7195
+ (d.topic ? '<span class="decision-topic">' + escapeHtml(d.topic) + '</span>' : '') +
7196
+ '<div class="decision-text">' + escapeHtml(d.decision) + '</div>' +
7197
+ (d.reasoning ? '<div class="decision-reasoning">' + escapeHtml(d.reasoning) + '</div>' : '') +
7198
+ '<div class="decision-meta">' +
7199
+ '<span>By ' + escapeHtml(d.decided_by || 'unknown') + '</span>' +
7200
+ '<span>' + dateStr + '</span>' +
7201
+ '</div>' +
7202
+ '</div>';
7203
+ }
7204
+ html += '</div>';
7205
+ el.innerHTML = html;
7206
+ }).catch(function() {});
7207
+ }
7208
+
6754
7209
  function renderDocs() {
6755
- var el = document.getElementById('docs-area');
7210
+ fetchDecisions();
7211
+ var el = document.getElementById('docs-content');
6756
7212
  if (el.dataset.rendered) return; // already rendered
6757
7213
  el.dataset.rendered = '1';
6758
7214
 
@@ -7148,6 +7604,34 @@ function officeCamSpeed(val) {
7148
7604
  }
7149
7605
 
7150
7606
  // Player avatar mode
7607
+ // --- Fullscreen toggle ---
7608
+ function toggleFullscreen() {
7609
+ if (document.fullscreenElement) {
7610
+ document.exitFullscreen();
7611
+ } else {
7612
+ // If on 3D Hub, fullscreen only the 3D container (game mode)
7613
+ var officeContainer = document.getElementById('office-3d-container');
7614
+ if (window.activeView === 'office' && officeContainer) {
7615
+ officeContainer.requestFullscreen().catch(function() {});
7616
+ } else {
7617
+ document.documentElement.requestFullscreen().catch(function() {});
7618
+ }
7619
+ }
7620
+ }
7621
+ // End key exits fullscreen
7622
+ document.addEventListener('keydown', function(e) {
7623
+ if (e.code === 'End' && document.fullscreenElement) {
7624
+ document.exitFullscreen();
7625
+ }
7626
+ });
7627
+ // Update button text on fullscreen change
7628
+ document.addEventListener('fullscreenchange', function() {
7629
+ var btn = document.getElementById('fullscreen-btn');
7630
+ if (btn) {
7631
+ btn.innerHTML = document.fullscreenElement ? '&#x2716; Exit Fullscreen' : '&#x26F6; Fullscreen';
7632
+ }
7633
+ });
7634
+
7151
7635
  function togglePlayerMode() {
7152
7636
  var btn = document.getElementById('player-mode-btn');
7153
7637
  if (window.office3dIsPlayerMode && window.office3dIsPlayerMode()) {