let-them-talk 4.0.2 → 4.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.2.0] - 2026-03-17
4
+
5
+ ### Major — Team Intelligence, Dashboard Upgrade, Performance
6
+
7
+ Built by a 4-agent team (Architect, Tester, Protocol, Builder) working in parallel.
8
+
9
+ ### Added — Team Automation
10
+ - **Auto-escalation** — blocked tasks auto-broadcast `[ESCALATION]` to team after 5 minutes. File-based dedup via `task.escalated_at` field (cross-process safe). Clears on unblock.
11
+ - **Stand-up meetings** — config-driven periodic team check-ins (`standup_interval_hours` in config.json). File-based dedup, 5+ agent gate. Broadcasts task summary with in-progress/blocked/done counts.
12
+ - **Quality gates** — `update_task(done)` auto-broadcasts `[REVIEW NEEDED]` (from v4.1.0, now with auto-escalation integration).
13
+
14
+ ### Added — Agent Intelligence
15
+ - **Workload metrics** — reputation tracks `task_times[]` (completion seconds), leaderboard shows `avg_task_time_sec` per agent.
16
+ - **Smarter suggest_task** — caps at 3 in-progress tasks ("finish first"), suggests blocked tasks when no pending ones, workload-aware.
17
+ - **KB hints in listen_group** — batch messages checked against KB keys, returns `kb_hints` with relevant entries.
18
+ - **Thread reply context** — `listen_group` includes `_reply_context` preview of parent message for threaded replies.
19
+ - **Decision overlap hints** — `send_message` checks content against logged decisions, returns `_decision_hint` to prevent re-debating.
20
+ - **Auto-status board** — `update_task` auto-writes `_status` to agent workspace ("Working on: X"). `list_agents` includes `current_status` field.
21
+
22
+ ### Added — Dashboard
23
+ - **Agent intent display** — dashboard shows what each agent is currently working on (from workspace `_status`)
24
+ - **Channel badges** — messages show colored `#channel` badges
25
+ - **Channel filter bar** — horizontal scrollable tabs to filter messages by channel
26
+ - **Channel history merging** — `/api/history` merges channel-specific + general history files
27
+ - **`/api/channels` endpoint** — channel list with member counts for dashboard
28
+ - **`/api/decisions` endpoint** — decision log display in dashboard
29
+ - **Decision log UI** — chronological cards with topic, decision, reasoning, author
30
+
31
+ ### Improved — Performance & Safety
32
+ - **Escalation dedup fix** — replaced in-memory `_escalatedTasks` Set with file-based `task.escalated_at` field (cross-process safe for 10 agents)
33
+ - **Dashboard current_status API** — `/api/agents` includes workspace `_status` for agent intent board
34
+
35
+ ## [4.1.0] - 2026-03-17
36
+
37
+ ### Added — Agent Reliability & Intelligence
38
+
39
+ - **Auto-recovery (crash resume)** — when an agent's process dies, the server snapshots its state (active tasks, locked files, channels, workspace keys, last 5 messages) to `recovery-{name}.json`. When a replacement registers with the same name, the snapshot is included in the register response with instructions to resume, not restart. 1-hour TTL, auto-deletes after load.
40
+ - **Quality gates** — `update_task(id, "done")` auto-broadcasts `[REVIEW NEEDED]` to all alive agents. Teams get automatic review cycles without manually calling `request_review()`.
41
+ - **Decision overlap hints** — `send_message` in group mode checks content against existing logged decisions. Returns `_decision_hint` if a related decision exists, preventing teams from re-debating settled topics.
42
+ - **Enhanced `check_messages`** — now returns rich summary: `senders`, `addressed_to_you`, `preview`, `urgency` level. The proactive counterpart to the enhanced nudge.
43
+
44
+ ### Fixed
45
+ - **Recovery lock notes** — snapshot correctly labels locked files as `locked_files_released` with note that locks were auto-released.
46
+
3
47
  ## [4.0.0] - 2026-03-17
4
48
 
5
49
  ### Major Release — 10-Agent Free Group Mode
package/cli.js CHANGED
@@ -9,7 +9,7 @@ const command = process.argv[2];
9
9
 
10
10
  function printUsage() {
11
11
  console.log(`
12
- Let Them Talk — Agent Bridge v4.0.2
12
+ Let Them Talk — Agent Bridge v4.2.0
13
13
  MCP message broker for inter-agent communication
14
14
  Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
15
 
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;
@@ -757,6 +773,49 @@
757
773
  .msg-to { font-size: 12px; color: var(--text-dim); }
758
774
  .msg-time { font-size: 10px; color: var(--text-muted); }
759
775
 
776
+ .badge-channel {
777
+ background: var(--purple-dim);
778
+ color: var(--purple);
779
+ font-size: 9px;
780
+ padding: 1px 5px;
781
+ border-radius: 6px;
782
+ font-weight: 600;
783
+ cursor: pointer;
784
+ }
785
+
786
+ .badge-channel:hover { opacity: 0.8; }
787
+
788
+ .channel-filter-bar {
789
+ display: flex;
790
+ gap: 4px;
791
+ padding: 4px 12px;
792
+ overflow-x: auto;
793
+ flex-wrap: nowrap;
794
+ scrollbar-width: none;
795
+ }
796
+
797
+ .channel-filter-bar::-webkit-scrollbar { display: none; }
798
+
799
+ .channel-tab {
800
+ font-size: 11px;
801
+ padding: 3px 10px;
802
+ border-radius: 12px;
803
+ border: 1px solid var(--border);
804
+ background: var(--surface-2);
805
+ color: var(--text-dim);
806
+ cursor: pointer;
807
+ white-space: nowrap;
808
+ transition: all 0.15s;
809
+ }
810
+
811
+ .channel-tab:hover { border-color: var(--border-light); color: var(--text); }
812
+
813
+ .channel-tab.active {
814
+ background: var(--purple-dim);
815
+ color: var(--purple);
816
+ border-color: var(--purple);
817
+ }
818
+
760
819
  .msg-badges {
761
820
  display: flex;
762
821
  gap: 4px;
@@ -2254,6 +2313,28 @@
2254
2313
  color: var(--text-dim);
2255
2314
  }
2256
2315
 
2316
+ .pipeline-bar {
2317
+ height: 4px;
2318
+ background: var(--surface-3);
2319
+ border-radius: 2px;
2320
+ margin-bottom: 10px;
2321
+ overflow: hidden;
2322
+ }
2323
+
2324
+ .pipeline-bar-fill {
2325
+ height: 100%;
2326
+ border-radius: 2px;
2327
+ transition: width 0.3s ease;
2328
+ }
2329
+
2330
+ .pipeline-meta {
2331
+ font-size: 10px;
2332
+ color: var(--text-muted);
2333
+ display: flex;
2334
+ gap: 12px;
2335
+ margin-bottom: 8px;
2336
+ }
2337
+
2257
2338
  .pipeline-steps {
2258
2339
  display: flex;
2259
2340
  gap: 0;
@@ -2379,6 +2460,63 @@
2379
2460
  .docs-section { padding: 14px 16px; }
2380
2461
  }
2381
2462
 
2463
+ /* ===== DECISIONS LOG ===== */
2464
+ .decisions-section { margin-bottom: 16px; }
2465
+ .decisions-header {
2466
+ display: flex;
2467
+ align-items: center;
2468
+ justify-content: space-between;
2469
+ margin-bottom: 10px;
2470
+ }
2471
+ .decisions-header h3 { font-size: 15px; font-weight: 700; color: var(--accent); margin: 0; }
2472
+ .decisions-count { font-size: 11px; color: var(--text-muted); }
2473
+
2474
+ .decision-card {
2475
+ background: var(--surface);
2476
+ border: 1px solid var(--border);
2477
+ border-left: 3px solid var(--accent);
2478
+ border-radius: 8px;
2479
+ padding: 12px 14px;
2480
+ margin-bottom: 8px;
2481
+ transition: border-color 0.15s;
2482
+ }
2483
+ .decision-card:hover { border-color: var(--border-light); }
2484
+
2485
+ .decision-topic {
2486
+ font-size: 9px;
2487
+ font-weight: 700;
2488
+ text-transform: uppercase;
2489
+ letter-spacing: 0.5px;
2490
+ color: var(--purple);
2491
+ background: var(--purple-dim);
2492
+ padding: 1px 6px;
2493
+ border-radius: 6px;
2494
+ display: inline-block;
2495
+ margin-bottom: 4px;
2496
+ }
2497
+
2498
+ .decision-text {
2499
+ font-size: 13px;
2500
+ font-weight: 600;
2501
+ color: var(--text);
2502
+ margin-bottom: 4px;
2503
+ line-height: 1.4;
2504
+ }
2505
+
2506
+ .decision-reasoning {
2507
+ font-size: 12px;
2508
+ color: var(--text-dim);
2509
+ line-height: 1.5;
2510
+ margin-bottom: 6px;
2511
+ }
2512
+
2513
+ .decision-meta {
2514
+ font-size: 10px;
2515
+ color: var(--text-muted);
2516
+ display: flex;
2517
+ gap: 10px;
2518
+ }
2519
+
2382
2520
  /* ===== VIRTUAL OFFICE ===== */
2383
2521
  .office-area {
2384
2522
  flex: 1;
@@ -3294,9 +3432,11 @@
3294
3432
  <div class="search-bar" id="search-bar">
3295
3433
  <input class="search-input" id="search-input" placeholder="Search messages... ( / )" oninput="onSearch()">
3296
3434
  <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>
3435
+ <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
3436
  <span class="search-count" id="search-count"></span>
3298
3437
  <button class="compact-toggle" id="compact-toggle" onclick="toggleCompactMode()" title="Toggle compact view">Compact</button>
3299
3438
  </div>
3439
+ <div class="channel-filter-bar" id="channel-filter-bar" style="display:none"></div>
3300
3440
  <div class="pinned-section" id="pinned-section">
3301
3441
  <div class="pinned-header" onclick="togglePinnedSection()"><span>Pinned Messages</span><span class="pinned-toggle" id="pinned-toggle">Hide</span></div>
3302
3442
  <div id="pinned-list"></div>
@@ -3332,7 +3472,7 @@
3332
3472
  </div>
3333
3473
  <div class="launch-area" id="launch-area"></div>
3334
3474
  <div class="stats-area" id="stats-area"></div>
3335
- <div class="docs-area" id="docs-area"></div>
3475
+ <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
3476
  <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
3477
  <div class="typing-bar" id="typing-bar"></div>
3338
3478
 
@@ -3765,6 +3905,7 @@ function lttFetch(url, opts) {
3765
3905
  }
3766
3906
 
3767
3907
  var activeThread = null;
3908
+ var activeChannel = null; // null = all channels
3768
3909
  var activeProject = ''; // empty = default/local
3769
3910
  var cachedHistory = [];
3770
3911
  var cachedAgents = {};
@@ -4077,6 +4218,7 @@ function renderAgents(agents) {
4077
4218
  '<span class="agent-activity-icon ' + state + '"></span>' +
4078
4219
  activityText +
4079
4220
  '</div>' +
4221
+ (info.current_status ? '<div class="agent-status-intent" title="' + escapeHtml(info.current_status) + '">' + escapeHtml(info.current_status) + '</div>' : '') +
4080
4222
  listenHtml +
4081
4223
  nudgeHtml +
4082
4224
  removeHtml +
@@ -4273,6 +4415,14 @@ function renderMessages(messages) {
4273
4415
  filtered = filtered.filter(function(m) { return !!bookmarks[m.id]; });
4274
4416
  }
4275
4417
 
4418
+ // Channel filter
4419
+ if (activeChannel) {
4420
+ filtered = filtered.filter(function(m) {
4421
+ if (activeChannel === 'general') return !m.channel || m.channel === 'general';
4422
+ return m.channel === activeChannel;
4423
+ });
4424
+ }
4425
+
4276
4426
  // Search filter
4277
4427
  if (searchQuery) {
4278
4428
  filtered = filtered.filter(function(m) {
@@ -4333,6 +4483,7 @@ function renderMessages(messages) {
4333
4483
  var groupClass = isGrouped ? ' grouped' : '';
4334
4484
 
4335
4485
  var badges = '';
4486
+ 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
4487
  if (m.acked) badges += '<span class="badge badge-ack">ACK</span>';
4337
4488
  if (m.thread_id) badges += '<span class="badge badge-thread">Thread</span>';
4338
4489
  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 +4581,39 @@ function clearThreadFilter() {
4430
4581
  renderMessages(cachedHistory);
4431
4582
  }
4432
4583
 
4584
+ function filterChannel(ch) {
4585
+ activeChannel = activeChannel === ch ? null : ch;
4586
+ lastMessageCount = 0;
4587
+ renderChannelBar(cachedHistory);
4588
+ renderMessages(cachedHistory);
4589
+ }
4590
+
4591
+ function renderChannelBar(messages) {
4592
+ var bar = document.getElementById('channel-filter-bar');
4593
+ if (!messages || !messages.length) { bar.style.display = 'none'; return; }
4594
+
4595
+ // Collect unique channels from messages
4596
+ var channels = {};
4597
+ for (var i = 0; i < messages.length; i++) {
4598
+ var ch = messages[i].channel || 'general';
4599
+ channels[ch] = (channels[ch] || 0) + 1;
4600
+ }
4601
+
4602
+ var keys = Object.keys(channels);
4603
+ // Only show channel bar if there are non-general channels
4604
+ if (keys.length <= 1 && keys[0] === 'general') { bar.style.display = 'none'; return; }
4605
+
4606
+ bar.style.display = 'flex';
4607
+ var html = '<div class="channel-tab' + (!activeChannel ? ' active' : '') + '" onclick="filterChannel(null)">All</div>';
4608
+ keys.sort();
4609
+ for (var j = 0; j < keys.length; j++) {
4610
+ var name = keys[j];
4611
+ var active = activeChannel === name ? ' active' : '';
4612
+ html += '<div class="channel-tab' + active + '" onclick="filterChannel(\'' + escapeHtml(name) + '\')">#' + escapeHtml(name) + ' <span style="opacity:0.5;font-size:9px">' + channels[name] + '</span></div>';
4613
+ }
4614
+ bar.innerHTML = html;
4615
+ }
4616
+
4433
4617
  function toggleSidebar() {
4434
4618
  document.getElementById('sidebar').classList.toggle('open');
4435
4619
  document.getElementById('sidebar-overlay').classList.toggle('open');
@@ -4500,6 +4684,33 @@ function onSearch() {
4500
4684
  renderMessages(cachedHistory);
4501
4685
  }
4502
4686
 
4687
+ function deepSearch() {
4688
+ var query = document.getElementById('search-input').value.trim();
4689
+ if (query.length < 2) return;
4690
+ var pq = activeProject ? '&project=' + encodeURIComponent(activeProject) : '';
4691
+ var countEl = document.getElementById('search-count');
4692
+ countEl.textContent = 'Searching...';
4693
+ lttFetch('/api/search?q=' + encodeURIComponent(query) + '&limit=100' + pq).then(function(r) { return r.json(); }).then(function(data) {
4694
+ if (data.error) { countEl.textContent = data.error; return; }
4695
+ countEl.textContent = data.results_count + ' deep result' + (data.results_count !== 1 ? 's' : '');
4696
+ // Convert search results to message format for renderMessages
4697
+ var messages = data.results.map(function(r) {
4698
+ return { id: r.id, from: r.from, to: r.to, content: r.preview, timestamp: r.timestamp, channel: r.channel || null };
4699
+ });
4700
+ lastMessageCount = 0;
4701
+ var el = document.getElementById('messages');
4702
+ if (!messages.length) {
4703
+ 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>';
4704
+ return;
4705
+ }
4706
+ // Render search results directly (don't overwrite cachedHistory)
4707
+ searchQuery = query.toLowerCase();
4708
+ renderMessages(messages);
4709
+ }).catch(function(e) {
4710
+ countEl.textContent = 'Search failed';
4711
+ });
4712
+ }
4713
+
4503
4714
  // ==================== READ RECEIPTS ====================
4504
4715
 
4505
4716
  var cachedReadReceipts = {};
@@ -5025,17 +5236,11 @@ function exportConversation() {
5025
5236
 
5026
5237
  function exportJSON() {
5027
5238
  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);
5239
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
5034
5240
  var a = document.createElement('a');
5035
- a.href = url;
5036
- a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '.json';
5241
+ a.href = '/api/export-json' + pq;
5242
+ a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '-full.json';
5037
5243
  a.click();
5038
- URL.revokeObjectURL(url);
5039
5244
  }
5040
5245
 
5041
5246
  // ==================== VIEW SWITCHING ====================
@@ -5061,12 +5266,33 @@ function switchView(view) {
5061
5266
  document.getElementById('stats-area').classList.toggle('visible', view === 'stats');
5062
5267
  document.getElementById('docs-area').classList.toggle('visible', view === 'docs');
5063
5268
  document.getElementById('search-bar').style.display = view === 'messages' ? 'flex' : 'none';
5269
+ document.getElementById('channel-filter-bar').style.display = view === 'messages' && activeChannel !== undefined ? '' : 'none';
5270
+ if (view === 'messages') renderChannelBar(cachedHistory);
5064
5271
  if (view === 'tasks') fetchTasks();
5065
5272
  if (view === 'workspaces') fetchWorkspaces();
5066
5273
  if (view === 'workflows') fetchWorkflows();
5067
5274
  if (view === 'docs') renderDocs();
5068
5275
  if (view === 'office') {
5069
- if (window.office3dStart) window.office3dStart();
5276
+ if (window.office3dStart) {
5277
+ window.office3dStart();
5278
+ } else {
5279
+ // Show helpful error if 3D engine failed to load (Three.js missing)
5280
+ var officeEl = document.getElementById('office-area');
5281
+ if (officeEl && !officeEl.querySelector('.office-error')) {
5282
+ var errDiv = document.createElement('div');
5283
+ errDiv.className = 'office-error';
5284
+ 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';
5285
+ errDiv.innerHTML = '<div style="font-size:48px;margin-bottom:16px">&#x1f3d7;</div>' +
5286
+ '<div style="font-size:16px;font-weight:600;color:var(--text);margin-bottom:8px">3D Hub loading...</div>' +
5287
+ '<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>';
5288
+ officeEl.appendChild(errDiv);
5289
+ // Remove error message once 3D loads
5290
+ var checkInterval = setInterval(function() {
5291
+ if (window.office3dStart) { errDiv.remove(); clearInterval(checkInterval); window.office3dStart(); }
5292
+ }, 2000);
5293
+ setTimeout(function() { clearInterval(checkInterval); }, 30000);
5294
+ }
5295
+ }
5070
5296
  }
5071
5297
  if (view === 'launch') renderLaunchPanel();
5072
5298
  if (view === 'stats') fetchStats();
@@ -5162,9 +5388,20 @@ function buildTaskCard(t) {
5162
5388
  '<option value="blocked"' + (t.status === 'blocked' ? ' selected' : '') + '>Blocked</option>' +
5163
5389
  '</select>';
5164
5390
 
5391
+ var badgesHtml = '';
5392
+ 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>';
5393
+ if (t.channel) badgesHtml += '<span class="badge-channel" onclick="filterChannel(\'' + escapeHtml(t.channel) + '\')">#' + escapeHtml(t.channel) + '</span>';
5394
+
5395
+ var notesHtml = '';
5396
+ if (t.notes && t.notes.length) {
5397
+ var lastNote = t.notes[t.notes.length - 1];
5398
+ 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>';
5399
+ }
5400
+
5165
5401
  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>' +
5402
+ '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + (badgesHtml ? ' ' + badgesHtml : '') + '</div>' +
5167
5403
  (t.description ? '<div class="task-desc">' + escapeHtml(t.description) + '</div>' : '') +
5404
+ notesHtml +
5168
5405
  '<div class="task-footer">' +
5169
5406
  assigneeHtml +
5170
5407
  statusOpts +
@@ -5815,22 +6052,41 @@ function renderWorkflows(workflows) {
5815
6052
  var pct = Math.round((doneCount / wf.steps.length) * 100);
5816
6053
  var statusColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
5817
6054
 
6055
+ var barColor = wf.status === 'completed' ? 'var(--green)' : 'var(--accent)';
6056
+ var metaHtml = '<div class="pipeline-meta">';
6057
+ if (wf.created_by) metaHtml += '<span>Created by ' + escapeHtml(wf.created_by) + '</span>';
6058
+ if (wf.created_at) metaHtml += '<span>' + timeAgo(wf.created_at) + '</span>';
6059
+ if (wf.updated_at && wf.updated_at !== wf.created_at) metaHtml += '<span>Updated ' + timeAgo(wf.updated_at) + '</span>';
6060
+ metaHtml += '</div>';
6061
+
5818
6062
  html += '<div class="pipeline">' +
5819
6063
  '<div class="pipeline-header">' +
5820
6064
  '<div class="pipeline-name">' + escapeHtml(wf.name) + ' <span style="font-size:11px;color:' + statusColor + ';font-weight:600">' + escapeHtml(wf.status) + '</span></div>' +
5821
6065
  '<div class="pipeline-progress">' + doneCount + '/' + wf.steps.length + ' (' + pct + '%)</div>' +
5822
6066
  '</div>' +
6067
+ metaHtml +
6068
+ '<div class="pipeline-bar"><div class="pipeline-bar-fill" style="width:' + pct + '%;background:' + barColor + '"></div></div>' +
5823
6069
  '<div class="pipeline-steps">';
5824
6070
 
5825
6071
  for (var j = 0; j < wf.steps.length; j++) {
5826
6072
  var s = wf.steps[j];
5827
6073
  var stepColor = s.status === 'done' ? 'var(--green)' : s.status === 'in_progress' ? 'var(--accent)' : 'var(--text-muted)';
5828
6074
  if (j > 0) html += '<span class="step-arrow">&rarr;</span>';
6075
+ var stepTimeHtml = '';
6076
+ if (s.status === 'done' && s.started_at && s.completed_at) {
6077
+ var dur = Math.round((new Date(s.completed_at) - new Date(s.started_at)) / 60000);
6078
+ stepTimeHtml = '<div style="font-size:9px;color:var(--text-muted);margin-top:2px">' + (dur > 0 ? dur + 'm' : '<1m') + '</div>';
6079
+ } else if (s.status === 'in_progress' && s.started_at) {
6080
+ var elapsed = Math.round((Date.now() - new Date(s.started_at)) / 60000);
6081
+ stepTimeHtml = '<div style="font-size:9px;color:var(--accent);margin-top:2px">' + elapsed + 'm elapsed</div>';
6082
+ }
6083
+
5829
6084
  html += '<div class="step-card ' + s.status + '" title="' + escapeHtml(s.notes || '') + '">' +
5830
6085
  '<span class="step-num">' + s.id + '</span>' +
5831
6086
  '<span style="font-size:10px;color:' + stepColor + ';font-weight:600;text-transform:uppercase">' + s.status.replace('_', ' ') + '</span>' +
5832
6087
  '<div class="step-desc">' + escapeHtml(s.description) + '</div>' +
5833
6088
  (s.assignee ? '<div class="step-assignee">&#x1f464; ' + escapeHtml(s.assignee) + '</div>' : '') +
6089
+ stepTimeHtml +
5834
6090
  '</div>';
5835
6091
  }
5836
6092
  html += '</div>';
@@ -6111,6 +6367,7 @@ function poll() {
6111
6367
  renderAgentStats();
6112
6368
  renderThreads(cachedHistory);
6113
6369
  if (!replayActive) renderMessages(cachedHistory);
6370
+ renderChannelBar(cachedHistory);
6114
6371
  renderPinnedMessages();
6115
6372
  renderBookmarksSidebar();
6116
6373
  fetchActivity();
@@ -6751,8 +7008,42 @@ function showConvTemplate(tid){var pq=activeProject?'?project='+encodeURICompone
6751
7008
 
6752
7009
  // ==================== v3.6: DOCS VIEW ====================
6753
7010
 
7011
+ function fetchDecisions() {
7012
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
7013
+ lttFetch('/api/decisions' + pq).then(function(r) { return r.json(); }).then(function(decisions) {
7014
+ var el = document.getElementById('decisions-panel');
7015
+ if (!decisions || !decisions.length) {
7016
+ el.innerHTML = '';
7017
+ return;
7018
+ }
7019
+ // Show most recent first
7020
+ var sorted = decisions.slice().reverse();
7021
+ var html = '<div class="decisions-section">' +
7022
+ '<div class="decisions-header">' +
7023
+ '<h3>Team Decisions</h3>' +
7024
+ '<span class="decisions-count">' + decisions.length + ' decision' + (decisions.length !== 1 ? 's' : '') + '</span>' +
7025
+ '</div>';
7026
+ for (var i = 0; i < sorted.length; i++) {
7027
+ var d = sorted[i];
7028
+ var dateStr = d.decided_at ? new Date(d.decided_at).toLocaleString() : '';
7029
+ html += '<div class="decision-card">' +
7030
+ (d.topic ? '<span class="decision-topic">' + escapeHtml(d.topic) + '</span>' : '') +
7031
+ '<div class="decision-text">' + escapeHtml(d.decision) + '</div>' +
7032
+ (d.reasoning ? '<div class="decision-reasoning">' + escapeHtml(d.reasoning) + '</div>' : '') +
7033
+ '<div class="decision-meta">' +
7034
+ '<span>By ' + escapeHtml(d.decided_by || 'unknown') + '</span>' +
7035
+ '<span>' + dateStr + '</span>' +
7036
+ '</div>' +
7037
+ '</div>';
7038
+ }
7039
+ html += '</div>';
7040
+ el.innerHTML = html;
7041
+ }).catch(function() {});
7042
+ }
7043
+
6754
7044
  function renderDocs() {
6755
- var el = document.getElementById('docs-area');
7045
+ fetchDecisions();
7046
+ var el = document.getElementById('docs-content');
6756
7047
  if (el.dataset.rendered) return; // already rendered
6757
7048
  el.dataset.rendered = '1';
6758
7049