claude-code-kanban 2.0.0-rc.1 → 2.0.1

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/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
  [![license](https://img.shields.io/npm/l/claude-code-kanban)](LICENSE)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/claude-code-kanban)](https://www.npmjs.com/package/claude-code-kanban)
6
6
 
7
- > Watch Claude Code think, in real time.
7
+ **[Live Demo & Docs](https://nikiforovall.blog/claude-code-kanban/)**
8
+
9
+ > Watch Claude Code work, in real time.
8
10
 
9
11
  ![Dark mode](assets/screenshot-dark-v2.png)
10
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.0.0-rc.1",
3
+ "version": "2.0.1",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -366,14 +366,49 @@
366
366
  }
367
367
 
368
368
  /* Live Updates */
369
+ .collapse-chevron {
370
+ width: 14px;
371
+ height: 14px;
372
+ stroke: var(--text-muted);
373
+ fill: none;
374
+ stroke-width: 2;
375
+ transition: transform 0.2s ease;
376
+ flex-shrink: 0;
377
+ }
378
+
379
+ .collapse-chevron.rotated {
380
+ transform: rotate(-90deg);
381
+ }
382
+
383
+ .collapsible-section {
384
+ transition: max-height 0.2s ease, padding 0.2s ease, opacity 0.2s ease;
385
+ overflow: hidden;
386
+ }
387
+
388
+ .collapsible-section.collapsed {
389
+ max-height: 0 !important;
390
+ padding-top: 0 !important;
391
+ padding-bottom: 0 !important;
392
+ opacity: 0;
393
+ overflow: hidden;
394
+ }
395
+
369
396
  .live-updates {
370
- padding: 0 16px 12px;
371
- max-height: 180px;
397
+ padding: 0 16px 8px;
398
+ max-height: 140px;
372
399
  overflow-y: auto;
400
+ transition: max-height 0.2s ease, padding 0.2s ease, opacity 0.2s ease;
401
+ }
402
+
403
+ .live-updates.collapsed {
404
+ max-height: 0;
405
+ padding: 0 16px;
406
+ overflow: hidden;
407
+ opacity: 0;
373
408
  }
374
409
 
375
410
  .live-empty {
376
- padding: 16px;
411
+ padding: 8px;
377
412
  text-align: center;
378
413
  font-size: 11px;
379
414
  color: var(--text-muted);
@@ -382,12 +417,12 @@
382
417
  .live-item {
383
418
  display: flex;
384
419
  align-items: flex-start;
385
- gap: 10px;
386
- padding: 10px 12px;
420
+ gap: 8px;
421
+ padding: 6px 10px;
387
422
  background: var(--bg-deep);
388
423
  border: 1px solid transparent;
389
- border-radius: 8px;
390
- margin-bottom: 4px;
424
+ border-radius: 6px;
425
+ margin-bottom: 3px;
391
426
  cursor: pointer;
392
427
  transition: all 0.15s ease;
393
428
  }
@@ -397,14 +432,14 @@
397
432
  }
398
433
 
399
434
  .live-item .pulse {
400
- width: 8px;
401
- height: 8px;
435
+ width: 6px;
436
+ height: 6px;
402
437
  margin-top: 4px;
403
438
  background: var(--accent);
404
439
  border-radius: 50%;
405
440
  flex-shrink: 0;
406
441
  animation: pulse 2s ease-in-out infinite;
407
- box-shadow: 0 0 12px var(--accent-glow);
442
+ box-shadow: 0 0 8px var(--accent-glow);
408
443
  }
409
444
 
410
445
  .live-item-content {
@@ -413,7 +448,7 @@
413
448
  }
414
449
 
415
450
  .live-item-action {
416
- font-size: 13px;
451
+ font-size: 11px;
417
452
  color: var(--text-primary);
418
453
  white-space: nowrap;
419
454
  overflow: hidden;
@@ -421,9 +456,9 @@
421
456
  }
422
457
 
423
458
  .live-item-session {
424
- font-size: 11px;
459
+ font-size: 10px;
425
460
  color: var(--text-tertiary);
426
- margin-top: 2px;
461
+ margin-top: 1px;
427
462
  white-space: nowrap;
428
463
  overflow: hidden;
429
464
  text-overflow: ellipsis;
@@ -1173,6 +1208,7 @@
1173
1208
  border-radius: 4px;
1174
1209
  border: 1px solid transparent;
1175
1210
  transition: border-color 0.15s ease;
1211
+ overflow-x: auto;
1176
1212
  }
1177
1213
 
1178
1214
  .detail-desc:hover {
@@ -1630,13 +1666,13 @@
1630
1666
  .pinned-sessions-divider {
1631
1667
  height: 1px; margin: 4px 8px; background: color-mix(in srgb, var(--accent) 30%, transparent);
1632
1668
  }
1633
- .agent-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-top: 16px; }
1669
+ .agent-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-top: 16px; align-items: center; }
1634
1670
  .agent-tab { padding: 8px 16px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-tertiary); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.15s, border-color 0.15s; user-select: none; }
1635
1671
  .agent-tab:hover { color: var(--text-secondary); border-bottom-color: var(--text-muted); }
1636
1672
  .agent-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
1637
1673
  .agent-tab-panel { display: none; padding-top: 12px; overflow: hidden; position: relative; }
1638
1674
  .agent-tab-panel.active { display: block; }
1639
- .agent-tab-copy { position: absolute; top: 14px; right: 8px; background: var(--surface-hover); border: 1px solid var(--border); border-radius: 6px; padding: 4px 6px; cursor: pointer; color: var(--text-tertiary); opacity: 0.7; transition: opacity 0.15s, color 0.15s; z-index: 2; }
1675
+ .agent-tab-copy { margin-left: auto; background: var(--surface-hover); border: 1px solid var(--border); border-radius: 6px; padding: 4px 6px; cursor: pointer; color: var(--text-tertiary); opacity: 0.7; transition: opacity 0.15s, color 0.15s; margin-bottom: -1px; }
1640
1676
  .agent-tab-copy:hover { opacity: 1; color: var(--text-primary); }
1641
1677
  .toast { position: fixed; bottom: 24px; left: 24px; transform: translateY(20px); background: var(--bg-elevated); color: var(--accent-text); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 8px; padding: 10px 20px; font-size: 13px; font-weight: 600; z-index: 10000; opacity: 0; transition: opacity 0.25s, transform 0.25s; pointer-events: none; box-shadow: 0 4px 16px rgba(0,0,0,0.3); }
1642
1678
  .toast.visible { opacity: 1; transform: translateY(0); }
@@ -1729,9 +1765,9 @@
1729
1765
  .agent-footer.collapsed .agent-footer-content { display: none; }
1730
1766
  .agent-card {
1731
1767
  display: flex;
1732
- align-items: center;
1733
- gap: 8px;
1734
- padding: 8px 14px;
1768
+ flex-direction: column;
1769
+ gap: 3px;
1770
+ padding: 8px 12px;
1735
1771
  background: var(--bg-elevated);
1736
1772
  border: 1px solid var(--border);
1737
1773
  border-radius: 8px;
@@ -1740,11 +1776,30 @@
1740
1776
  overflow: hidden;
1741
1777
  transition: opacity 0.3s;
1742
1778
  cursor: pointer;
1779
+ position: relative;
1743
1780
  }
1744
1781
  .agent-card:hover { border-color: var(--accent); }
1745
1782
  .agent-card.fading { opacity: 0.4; }
1783
+ .agent-type-row {
1784
+ display: flex;
1785
+ align-items: center;
1786
+ gap: 4px;
1787
+ min-width: 0;
1788
+ }
1789
+ .agent-type-ns {
1790
+ font-size: 11px; color: var(--text-muted); font-weight: 400;
1791
+ }
1792
+ .agent-type-name {
1793
+ font-size: 13px; font-weight: 600; color: var(--text-primary);
1794
+ overflow: hidden; text-overflow: ellipsis;
1795
+ }
1796
+ .agent-status-row {
1797
+ display: flex;
1798
+ align-items: center;
1799
+ gap: 5px;
1800
+ }
1746
1801
  .agent-dot {
1747
- width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
1802
+ width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
1748
1803
  }
1749
1804
  .agent-dot.active { background: var(--success); box-shadow: 0 0 6px var(--success); }
1750
1805
  .agent-dot.idle { background: var(--warning); box-shadow: 0 0 6px var(--warning); }
@@ -1757,9 +1812,14 @@
1757
1812
  }
1758
1813
  .agent-message {
1759
1814
  font-size: 11px; color: var(--text-muted);
1760
- max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1815
+ max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1761
1816
  }
1762
1817
 
1818
+ .agent-dismiss-btn {
1819
+ font-size: 12px; padding: 4px 10px;
1820
+ background: var(--bg-tertiary); color: var(--text-secondary);
1821
+ border: 1px solid var(--border);
1822
+ }
1763
1823
  .agent-badge { font-size: 12px; cursor: default; }
1764
1824
 
1765
1825
  /* Permission pending — indicated by ❓ badge only */
@@ -1913,10 +1973,17 @@
1913
1973
  background: rgba(0, 0, 0, 0.6);
1914
1974
  }
1915
1975
 
1976
+ .modal.fullscreen {
1977
+ width: 76vw !important;
1978
+ max-width: 76vw !important;
1979
+ height: 85vh !important;
1980
+ max-height: 85vh !important;
1981
+ }
1982
+
1916
1983
  .modal.plan-modal {
1917
1984
  width: 60vw;
1918
1985
  max-width: 60vw;
1919
- max-height: 90vh;
1986
+ max-height: 85vh;
1920
1987
  display: flex;
1921
1988
  flex-direction: column;
1922
1989
  }
@@ -2233,6 +2300,12 @@
2233
2300
  color: var(--text-primary);
2234
2301
  }
2235
2302
 
2303
+ .project-group-header.kb-selected {
2304
+ color: var(--text-primary);
2305
+ background: var(--bg-hover);
2306
+ border-radius: 4px;
2307
+ }
2308
+
2236
2309
  .project-group-header .group-chevron {
2237
2310
  transition: transform 0.15s ease;
2238
2311
  flex-shrink: 0;
@@ -2395,8 +2468,11 @@
2395
2468
 
2396
2469
  <!-- Live Updates -->
2397
2470
  <div class="sidebar-section">
2398
- <div class="section-header">
2471
+ <div class="section-header" onclick="toggleLiveUpdates()" style="cursor: pointer;">
2399
2472
  <span>Live Updates</span>
2473
+ <svg id="live-updates-chevron" class="collapse-chevron" viewBox="0 0 24 24">
2474
+ <path d="M6 9l6 6 6-6"/>
2475
+ </svg>
2400
2476
  </div>
2401
2477
  <div id="live-updates" class="live-updates">
2402
2478
  <div class="live-empty">No active tasks</div>
@@ -2405,46 +2481,50 @@
2405
2481
 
2406
2482
  <!-- Tasks -->
2407
2483
  <div class="sidebar-section flex-1">
2408
- <div class="section-header">
2484
+ <div class="section-header" onclick="toggleSection('sessions-filters', 'sessions-chevron')" style="cursor: pointer;">
2409
2485
  <span>Sessions</span>
2410
- <button onclick="showAllTasks()" style="background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; padding: 3px 8px; font-size: 10px; color: var(--text-secondary); cursor: pointer; font-family: var(--mono);">All Tasks</button>
2411
- </div>
2412
- <div class="search-container">
2413
- <input
2414
- id="search-input"
2415
- type="text"
2416
- class="search-input"
2417
- placeholder="Search tasks, sessions, projects..."
2418
- oninput="handleSearch(this.value)"
2419
- />
2420
- <button id="search-clear-btn" class="search-clear" onclick="clearSearch()" title="Clear search" aria-label="Clear search">
2421
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2422
- <path d="M18 6L6 18M6 6l12 12"/>
2423
- </svg>
2424
- </button>
2425
- </div>
2426
- <div class="filter-row">
2427
- <select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)" aria-label="Filter by project">
2428
- <option value="">All Projects</option>
2429
- </select>
2430
- <select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)" aria-label="Filter by session status">
2431
- <option value="all">All Sessions</option>
2432
- <option value="active">Active Only</option>
2433
- </select>
2486
+ <svg id="sessions-chevron" class="collapse-chevron" viewBox="0 0 24 24">
2487
+ <path d="M6 9l6 6 6-6"/>
2488
+ </svg>
2434
2489
  </div>
2435
- <div class="filter-row">
2436
- <select id="session-limit" class="filter-dropdown" onchange="changeSessionLimit(this.value)" aria-label="Number of sessions to show">
2437
- <option value="10">Show 10</option>
2438
- <option value="20">Show 20</option>
2439
- <option value="50">Show 50</option>
2440
- <option value="all">Show All</option>
2441
- </select>
2442
- <button class="icon-btn reset-btn" onclick="resetState()" title="Reset all filters" aria-label="Reset all filters">
2443
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
2444
- <path d="M3 12a9 9 0 1 1 9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
2445
- <path d="M3 22v-6h6"/>
2446
- </svg>
2447
- </button>
2490
+ <div id="sessions-filters" class="collapsible-section">
2491
+ <div class="search-container">
2492
+ <input
2493
+ id="search-input"
2494
+ type="text"
2495
+ class="search-input"
2496
+ placeholder="Search tasks, sessions, projects..."
2497
+ oninput="handleSearch(this.value)"
2498
+ />
2499
+ <button id="search-clear-btn" class="search-clear" onclick="clearSearch()" title="Clear search" aria-label="Clear search">
2500
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2501
+ <path d="M18 6L6 18M6 6l12 12"/>
2502
+ </svg>
2503
+ </button>
2504
+ </div>
2505
+ <div class="filter-row">
2506
+ <select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)" aria-label="Filter by project">
2507
+ <option value="">All Projects</option>
2508
+ </select>
2509
+ <select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)" aria-label="Filter by session status">
2510
+ <option value="all">All Sessions</option>
2511
+ <option value="active">Active Only</option>
2512
+ </select>
2513
+ </div>
2514
+ <div class="filter-row">
2515
+ <select id="session-limit" class="filter-dropdown" onchange="changeSessionLimit(this.value)" aria-label="Number of sessions to show">
2516
+ <option value="10">Show 10</option>
2517
+ <option value="20">Show 20</option>
2518
+ <option value="50">Show 50</option>
2519
+ <option value="all">Show All</option>
2520
+ </select>
2521
+ <button class="icon-btn reset-btn" onclick="resetState()" title="Reset all filters" aria-label="Reset all filters">
2522
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
2523
+ <path d="M3 12a9 9 0 1 1 9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
2524
+ <path d="M3 22v-6h6"/>
2525
+ </svg>
2526
+ </button>
2527
+ </div>
2448
2528
  </div>
2449
2529
  <div id="sessions-list" class="sessions-list"></div>
2450
2530
  </div>
@@ -2943,6 +3023,18 @@
2943
3023
  renderLiveUpdates(activeTasks);
2944
3024
  }
2945
3025
 
3026
+ function toggleSection(containerId, chevronId) {
3027
+ const container = document.getElementById(containerId);
3028
+ const chevron = document.getElementById(chevronId);
3029
+ const collapsed = container.classList.toggle('collapsed');
3030
+ chevron.classList.toggle('rotated', collapsed);
3031
+ localStorage.setItem(containerId + 'Collapsed', collapsed);
3032
+ }
3033
+
3034
+ function toggleLiveUpdates() {
3035
+ toggleSection('live-updates', 'live-updates-chevron');
3036
+ }
3037
+
2946
3038
  function renderLiveUpdates(activeTasks) {
2947
3039
  const container = document.getElementById('live-updates');
2948
3040
 
@@ -3410,9 +3502,18 @@
3410
3502
  }
3411
3503
  const toolParamsHtml = renderToolParamsHtml(m.params);
3412
3504
  const toolResultHtml = renderToolResultHtml(m.toolResult, m.toolResultTruncated, m.toolResultFull);
3413
- const detailEscaped = escapeHtml(fullText);
3414
- const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
3415
- body.innerHTML = (fullText ? descHtml + `<pre class="msg-detail-pre">${detailRendered}</pre>` : '<em>No details</em>') + toolParamsHtml + toolResultHtml + agentExtraHtml;
3505
+ const hasAgentTabs = m.tool === 'Agent' && m.agentId && (m.agentLastMessage || m.agentPrompt);
3506
+ let mainHtml;
3507
+ if (hasAgentTabs) {
3508
+ mainHtml = descHtml || '';
3509
+ } else if (fullText) {
3510
+ const detailEscaped = escapeHtml(fullText);
3511
+ const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
3512
+ mainHtml = descHtml + `<pre class="msg-detail-pre">${detailRendered}</pre>`;
3513
+ } else {
3514
+ mainHtml = '<em>No details</em>';
3515
+ }
3516
+ body.innerHTML = mainHtml + toolParamsHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
3416
3517
  } else {
3417
3518
  const text = stripAnsi(m.fullText || m.text);
3418
3519
  document.getElementById('msg-detail-title').textContent = m.type === 'assistant' ? 'Claude' : (m.systemLabel ? 'System' : 'User');
@@ -3431,6 +3532,7 @@
3431
3532
 
3432
3533
  const meta = [formatDate(m.timestamp)];
3433
3534
  if (m.model) meta.unshift(m.model);
3535
+ meta.push(`${idx + 1} of ${currentMessages.length}`);
3434
3536
  document.getElementById('msg-detail-meta').textContent = meta.join(' · ');
3435
3537
  currentPinDetailId = null;
3436
3538
  updateMsgDetailPinState();
@@ -3438,10 +3540,32 @@
3438
3540
  }
3439
3541
 
3440
3542
  function closeMsgDetailModal() {
3441
- document.getElementById('msg-detail-modal').classList.remove('visible');
3543
+ resetModalFullscreen('msg-detail-modal');
3442
3544
  msgDetailFollowLatest = false;
3443
3545
  }
3444
3546
 
3547
+ function toggleModalFullscreen(modalId) {
3548
+ const modal = document.querySelector(`#${modalId} .modal`);
3549
+ const isFs = modal.classList.toggle('fullscreen');
3550
+ updateFullscreenBtnIcon(`${modalId}-fullscreen-btn`, isFs);
3551
+ }
3552
+
3553
+ function resetModalFullscreen(modalId) {
3554
+ const modal = document.getElementById(modalId);
3555
+ modal.classList.remove('visible');
3556
+ modal.querySelector('.modal').classList.remove('fullscreen');
3557
+ updateFullscreenBtnIcon(`${modalId}-fullscreen-btn`, false);
3558
+ return modal;
3559
+ }
3560
+
3561
+ function updateFullscreenBtnIcon(btnId, isFullscreen) {
3562
+ const btn = document.getElementById(btnId);
3563
+ if (!btn) return;
3564
+ btn.innerHTML = isFullscreen
3565
+ ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>'
3566
+ : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';
3567
+ }
3568
+
3445
3569
  let _toastTimer = null;
3446
3570
  function showToast(msg) {
3447
3571
  const el = document.getElementById('toast');
@@ -3536,7 +3660,7 @@
3536
3660
  const id = `expand-${++_expandIdCounter}`;
3537
3661
  const fontSize = opts.fontSize || '0.8rem';
3538
3662
  const maxHeight = opts.maxHeight || '';
3539
- const btn = `<button onclick="var f=document.getElementById('${id}'),t=this.parentElement.nextElementSibling,expand=f.style.display==='none';f.style.display=expand?'block':'none';t.style.display=expand?'none':'block';this.textContent=expand?'Show less':'Show more'" style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:${fontSize};text-decoration:underline">Show more</button>`;
3663
+ const btn = `<button onclick="var f=document.getElementById('${id}'),t=this.parentElement.nextElementSibling,expand=f.style.display==='none';f.style.display=expand?'block':'none';t.style.display=expand?'none':'block';this.textContent=expand?'Show less':'Show more'" style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:${fontSize};text-decoration:underline;margin-left:6px">Show more</button>`;
3540
3664
  const mhStyle = maxHeight ? `max-height:${maxHeight};` : '';
3541
3665
  const full = `<pre id="${id}" class="msg-detail-pre" style="${mhStyle}overflow:auto;display:none">${fullHtml}</pre>`;
3542
3666
  return { btn, full };
@@ -3545,7 +3669,9 @@
3545
3669
  function autoSizeModal(modal, body) {
3546
3670
  const hasTable = body.querySelector('table') !== null;
3547
3671
  const hasPre = body.querySelector('pre') !== null;
3548
- modal.style.maxWidth = hasTable ? '1100px' : (body.textContent.length > 2000 || hasPre) ? '960px' : '860px';
3672
+ const desired = hasTable ? 1100 : (body.textContent.length > 2000 || hasPre) ? 960 : 860;
3673
+ const current = parseFloat(getComputedStyle(modal).maxWidth) || 0;
3674
+ if (desired > current) modal.style.maxWidth = desired + 'px';
3549
3675
  }
3550
3676
 
3551
3677
  function renderToolResultHtml(toolResult, isTruncated, fullResult) {
@@ -3683,16 +3809,14 @@
3683
3809
  const promptTrunc = promptTrimmed.length > 60 ? promptTrimmed.substring(0, 60) + '…' : promptTrimmed;
3684
3810
  const msgHtml = promptTrunc
3685
3811
  ? `<div class="agent-message" title="${escapeHtml(promptTrimmed)}">${escapeHtml(promptTrunc)}</div>` : '';
3686
- const agentPinned = isAgentPinned(a.agentId);
3687
- const agentPinBtn = `<button class="msg-pin-btn${agentPinned ? ' pinned' : ''}" onclick="event.stopPropagation();toggleAgentPin('${a.agentId}')" title="${agentPinned ? 'Unpin' : 'Pin'} agent">${PIN_SVG}</button>`;
3812
+ const rawType = a.type || 'unknown';
3813
+ const colonIdx = rawType.indexOf(':');
3814
+ const typeNs = colonIdx > 0 ? rawType.substring(0, colonIdx + 1) : '';
3815
+ const typeName = colonIdx > 0 ? rawType.substring(colonIdx + 1) : rawType;
3688
3816
  return `<div class="agent-card" onclick="showAgentModal('${a.agentId}')">
3689
- <span class="agent-dot ${a.status}"></span>
3690
- <div style="flex:1;min-width:0">
3691
- <div class="agent-type">${escapeHtml(a.type || 'unknown')}</div>
3692
- <div class="agent-status">${statusText}</div>
3693
- ${msgHtml}
3694
- </div>
3695
- ${agentPinBtn}
3817
+ <div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span></div>
3818
+ <div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
3819
+ ${msgHtml}
3696
3820
  </div>`;
3697
3821
  }).join('');
3698
3822
 
@@ -3737,6 +3861,17 @@
3737
3861
  updateAgentModalPinState();
3738
3862
  }
3739
3863
 
3864
+ async function dismissAgent(agentId) {
3865
+ if (!currentSessionId || !agentId) return;
3866
+ try {
3867
+ const res = await fetch(`/api/sessions/${currentSessionId}/agents/${agentId}/stop`, { method: 'POST' });
3868
+ if (res.ok) {
3869
+ currentWaiting = null;
3870
+ fetchAgents(currentSessionId);
3871
+ }
3872
+ } catch (e) { console.error('[dismissAgent]', e); }
3873
+ }
3874
+
3740
3875
  function showAgentModal(agentId) {
3741
3876
  const agent = currentAgents.find(a => a.agentId === agentId);
3742
3877
  if (!agent) return;
@@ -3779,6 +3914,9 @@
3779
3914
 
3780
3915
  body.innerHTML = html;
3781
3916
  updateAgentModalPinState();
3917
+ autoSizeModal(modal.querySelector('.modal'), body);
3918
+ const dismissBtn = document.getElementById('agent-modal-dismiss-btn');
3919
+ dismissBtn.style.display = (agent.status === 'active' || agent.status === 'idle') ? '' : 'none';
3782
3920
  modal.classList.add('visible');
3783
3921
  const keyHandler = (e) => {
3784
3922
  if (e.key === 'Escape') {
@@ -3791,7 +3929,7 @@
3791
3929
  }
3792
3930
 
3793
3931
  function closeAgentModal() {
3794
- document.getElementById('agent-modal').classList.remove('visible');
3932
+ resetModalFullscreen('agent-modal');
3795
3933
  currentAgentModalId = null;
3796
3934
  }
3797
3935
 
@@ -4047,29 +4185,34 @@
4047
4185
  sessionsList.innerHTML = filteredSessions.map(renderSessionCard).join('');
4048
4186
  }
4049
4187
 
4050
- const items = getSessionItems();
4051
- const activeIdx = items.findIndex(el => el.classList.contains('active'));
4188
+ const navItems = getNavigableItems();
4189
+ const allSessions = getSessionItems();
4190
+ const activeIdx = allSessions.findIndex(el => el.classList.contains('active'));
4052
4191
  if (activeIdx >= 0 && (selectedSessionIdx < 0 || sessionJustSelected)) {
4053
- selectedSessionIdx = activeIdx;
4054
- selectedSessionKbId = items[activeIdx].dataset.sessionId || null;
4192
+ const navIdx = navItems.indexOf(allSessions[activeIdx]);
4193
+ selectedSessionIdx = navIdx >= 0 ? navIdx : 0;
4194
+ selectedSessionKbId = allSessions[activeIdx].dataset.sessionId || null;
4055
4195
  sessionJustSelected = false;
4056
4196
  }
4057
4197
 
4058
4198
  if (selectedSessionKbId && focusZone === 'sidebar') {
4059
- const restoredIdx = items.findIndex(el => el.dataset.sessionId === selectedSessionKbId);
4199
+ const restoredIdx = navItems.findIndex(el =>
4200
+ getKbId(el) === selectedSessionKbId
4201
+ );
4060
4202
  if (restoredIdx >= 0) {
4061
4203
  selectedSessionIdx = restoredIdx;
4062
- items[restoredIdx].classList.add('kb-selected');
4204
+ navItems[restoredIdx].classList.add('kb-selected');
4063
4205
  } else {
4064
4206
  selectedSessionIdx = -1;
4065
4207
  selectedSessionKbId = null;
4066
4208
  }
4067
4209
  } else if (focusZone === 'sidebar' && selectedSessionIdx >= 0) {
4068
- if (items.length > 0) {
4069
- const clamped = Math.min(selectedSessionIdx, items.length - 1);
4210
+ if (navItems.length > 0) {
4211
+ const clamped = Math.min(selectedSessionIdx, navItems.length - 1);
4070
4212
  selectedSessionIdx = clamped;
4071
- selectedSessionKbId = items[clamped].dataset.sessionId || null;
4072
- items[clamped].classList.add('kb-selected');
4213
+ const el = navItems[clamped];
4214
+ selectedSessionKbId = getKbId(el);
4215
+ el.classList.add('kb-selected');
4073
4216
  } else {
4074
4217
  selectedSessionIdx = -1;
4075
4218
  selectedSessionKbId = null;
@@ -4303,31 +4446,71 @@
4303
4446
  }
4304
4447
  }
4305
4448
 
4449
+ function getKbId(el) {
4450
+ return el.dataset.sessionId || el.dataset.groupPath || null;
4451
+ }
4452
+
4453
+ function getGroupSessionsContainer(header) {
4454
+ let el = header.nextElementSibling;
4455
+ while (el && !el.classList.contains('project-group-sessions')) el = el.nextElementSibling;
4456
+ return el;
4457
+ }
4458
+
4459
+ function getNavigableItems() {
4460
+ const items = [];
4461
+ for (const el of sessionsList.children) {
4462
+ if (el.classList.contains('project-group-header')) {
4463
+ items.push(el);
4464
+ if (!collapsedProjectGroups.has(el.dataset.groupPath)) {
4465
+ const container = getGroupSessionsContainer(el);
4466
+ if (container) {
4467
+ for (const s of container.querySelectorAll('.session-item')) items.push(s);
4468
+ }
4469
+ }
4470
+ } else if (el.classList.contains('session-item')) {
4471
+ items.push(el);
4472
+ }
4473
+ }
4474
+ return items;
4475
+ }
4476
+
4306
4477
  function getSessionItems() {
4307
4478
  return Array.from(sessionsList.querySelectorAll('.session-item'));
4308
4479
  }
4309
4480
 
4310
- function selectSessionByIndex(idx) {
4311
- const items = getSessionItems();
4312
- if (items.length === 0) return;
4313
- const prev = sessionsList.querySelector('.session-item.kb-selected');
4481
+ function clearKbSelection() {
4482
+ const prev = sessionsList.querySelector('.kb-selected');
4314
4483
  if (prev) prev.classList.remove('kb-selected');
4484
+ }
4485
+
4486
+ function selectSessionByIndex(idx, items) {
4487
+ items = items || getNavigableItems();
4488
+ if (items.length === 0) return;
4489
+ clearKbSelection();
4315
4490
  selectedSessionIdx = Math.max(0, Math.min(idx, items.length - 1));
4316
- selectedSessionKbId = items[selectedSessionIdx].dataset.sessionId || null;
4317
- items[selectedSessionIdx].classList.add('kb-selected');
4318
- items[selectedSessionIdx].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
4491
+ const el = items[selectedSessionIdx];
4492
+ selectedSessionKbId = getKbId(el);
4493
+ el.classList.add('kb-selected');
4494
+ el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
4319
4495
  }
4320
4496
 
4321
- function navigateSession(direction) {
4322
- const items = getSessionItems();
4497
+ function navigateSession(direction, items) {
4498
+ items = items || getNavigableItems();
4323
4499
  if (items.length === 0) return;
4324
4500
  if (selectedSessionIdx < 0) {
4325
- selectSessionByIndex(0);
4501
+ selectSessionByIndex(0, items);
4326
4502
  return;
4327
4503
  }
4328
- const newIdx = selectedSessionIdx + direction;
4504
+ const currentEl = items[selectedSessionIdx];
4505
+ let newIdx = selectedSessionIdx + direction;
4506
+ if (!currentEl || !currentEl.isConnected) {
4507
+ const restoredIdx = selectedSessionKbId
4508
+ ? items.findIndex(el => getKbId(el) === selectedSessionKbId)
4509
+ : -1;
4510
+ newIdx = restoredIdx >= 0 ? restoredIdx : 0;
4511
+ }
4329
4512
  if (newIdx >= 0 && newIdx < items.length) {
4330
- selectSessionByIndex(newIdx);
4513
+ selectSessionByIndex(newIdx, items);
4331
4514
  }
4332
4515
  }
4333
4516
 
@@ -4338,26 +4521,56 @@
4338
4521
  if (collapsed) collapsedProjectGroups.add(projectPath);
4339
4522
  else collapsedProjectGroups.delete(projectPath);
4340
4523
  header.classList.toggle('collapsed', collapsed);
4341
- let el = header.nextElementSibling;
4342
- while (el && !el.classList.contains('project-group-sessions')) el = el.nextElementSibling;
4343
- if (el) el.classList.toggle('collapsed', collapsed);
4524
+ const container = getGroupSessionsContainer(header);
4525
+ if (container) container.classList.toggle('collapsed', collapsed);
4344
4526
  try { localStorage.setItem('collapsedGroups', JSON.stringify([...collapsedProjectGroups])); } catch(_) {}
4345
4527
  }
4346
4528
 
4347
- function toggleSelectedSessionGroup(collapse) {
4348
- const items = getSessionItems();
4529
+ function handleSidebarHorizontal(direction) {
4530
+ const items = getNavigableItems();
4349
4531
  if (selectedSessionIdx < 0 || selectedSessionIdx >= items.length) return;
4350
- const container = items[selectedSessionIdx].closest('.project-group-sessions');
4351
- if (!container) return;
4352
- let header = container.previousElementSibling;
4353
- while (header && !header.classList.contains('project-group-header')) header = header.previousElementSibling;
4354
- setGroupCollapsed(header, collapse);
4532
+ const el = items[selectedSessionIdx];
4533
+ const isHeader = el.classList.contains('project-group-header');
4534
+ const collapse = direction < 0;
4535
+
4536
+ if (isHeader) {
4537
+ const groupPath = el.dataset.groupPath;
4538
+ const isCollapsed = collapsedProjectGroups.has(groupPath);
4539
+ if (collapse) {
4540
+ if (!isCollapsed) setGroupCollapsed(el, true);
4541
+ } else {
4542
+ if (isCollapsed) {
4543
+ setGroupCollapsed(el, false);
4544
+ } else {
4545
+ navigateSession(1);
4546
+ }
4547
+ }
4548
+ } else {
4549
+ if (collapse) {
4550
+ const container = el.closest('.project-group-sessions');
4551
+ if (container) {
4552
+ let header = container.previousElementSibling;
4553
+ while (header && !header.classList.contains('project-group-header')) header = header.previousElementSibling;
4554
+ if (header) {
4555
+ const headerIdx = items.indexOf(header);
4556
+ if (headerIdx >= 0) selectSessionByIndex(headerIdx, items);
4557
+ }
4558
+ }
4559
+ } else {
4560
+ activateSelectedSession(items);
4561
+ }
4562
+ }
4355
4563
  }
4356
4564
 
4357
- function activateSelectedSession() {
4358
- const items = getSessionItems();
4359
- if (selectedSessionIdx >= 0 && selectedSessionIdx < items.length) {
4360
- items[selectedSessionIdx].click();
4565
+ function activateSelectedSession(items) {
4566
+ items = items || getNavigableItems();
4567
+ if (selectedSessionIdx < 0 || selectedSessionIdx >= items.length) return;
4568
+ const el = items[selectedSessionIdx];
4569
+ if (el.classList.contains('project-group-header')) {
4570
+ const groupPath = el.dataset.groupPath;
4571
+ setGroupCollapsed(el, !collapsedProjectGroups.has(groupPath));
4572
+ } else {
4573
+ el.click();
4361
4574
  }
4362
4575
  }
4363
4576
 
@@ -4365,8 +4578,7 @@
4365
4578
  const sidebar = document.querySelector('.sidebar');
4366
4579
  // Clear all zone visuals
4367
4580
  sidebar.classList.remove('sidebar-focused');
4368
- const prevSession = sessionsList.querySelector('.session-item.kb-selected');
4369
- if (prevSession) prevSession.classList.remove('kb-selected');
4581
+ clearKbSelection();
4370
4582
  const selCard = document.querySelector('.task-card.selected');
4371
4583
  if (selCard) selCard.classList.remove('selected');
4372
4584
 
@@ -4377,13 +4589,13 @@
4377
4589
  localStorage.setItem('sidebar-collapsed', false);
4378
4590
  }
4379
4591
  sidebar.classList.add('sidebar-focused');
4380
- const items = getSessionItems();
4592
+ const items = getNavigableItems();
4381
4593
  if (items.length > 0) {
4382
4594
  const activeIdx = items.findIndex(el => el.classList.contains('active'));
4383
4595
  if (activeIdx >= 0) {
4384
4596
  selectSessionByIndex(activeIdx);
4385
4597
  } else if (selectedSessionKbId) {
4386
- const restoredIdx = items.findIndex(el => el.dataset.sessionId === selectedSessionKbId);
4598
+ const restoredIdx = items.findIndex(el => getKbId(el) === selectedSessionKbId);
4387
4599
  selectSessionByIndex(restoredIdx >= 0 ? restoredIdx : 0);
4388
4600
  } else {
4389
4601
  selectSessionByIndex(0);
@@ -4731,11 +4943,11 @@
4731
4943
  if (e.key === 'Escape') {
4732
4944
  e.preventDefault();
4733
4945
  closeDeleteConfirmModal();
4734
- } else if (e.key === 'ArrowLeft' || e.key === 'h') {
4946
+ } else if (matchKey(e, 'ArrowLeft', 'KeyH')) {
4735
4947
  e.preventDefault();
4736
4948
  focusIdx = 0;
4737
4949
  buttons[focusIdx].focus();
4738
- } else if (e.key === 'ArrowRight' || e.key === 'l') {
4950
+ } else if (matchKey(e, 'ArrowRight', 'KeyL')) {
4739
4951
  e.preventDefault();
4740
4952
  focusIdx = 1;
4741
4953
  buttons[focusIdx].focus();
@@ -4817,35 +5029,58 @@
4817
5029
 
4818
5030
  document.getElementById('close-detail').onclick = closeDetailPanel;
4819
5031
 
5032
+ // Layout-independent key matching: pass Arrow* for e.key, Key* for e.code (physical position)
5033
+ function matchKey(e, ...keys) {
5034
+ if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return false;
5035
+ return keys.some(k => e.key === k || e.code === k);
5036
+ }
5037
+
4820
5038
  document.addEventListener('keydown', (e) => {
4821
5039
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
4822
5040
  return;
4823
5041
  }
4824
5042
 
5043
+ // Modal guard — only Escape, Shift+M, and msg-detail J/K navigation pass through
4825
5044
  if (document.querySelector('.modal-overlay.visible')) {
4826
5045
  if (e.key === 'Escape') {
4827
5046
  document.querySelectorAll('.modal-overlay.visible').forEach(m => m.classList.remove('visible'));
4828
5047
  msgDetailFollowLatest = false;
4829
- } else if ((e.key === 'M' || e.key === 'm') && e.shiftKey && document.getElementById('msg-detail-modal').classList.contains('visible')) {
5048
+ } else if (e.code === 'KeyM' && e.shiftKey && document.getElementById('msg-detail-modal').classList.contains('visible')) {
4830
5049
  e.preventDefault();
4831
5050
  closeMsgDetailModal();
5051
+ } else if (document.getElementById('msg-detail-modal').classList.contains('visible')) {
5052
+ if (matchKey(e, 'ArrowDown', 'KeyJ')) {
5053
+ e.preventDefault();
5054
+ if (currentMsgDetailIdx < currentMessages.length - 1) {
5055
+ msgDetailFollowLatest = false;
5056
+ showMsgDetail(currentMsgDetailIdx + 1);
5057
+ } else if (currentMsgDetailIdx === currentMessages.length - 1) {
5058
+ msgDetailFollowLatest = true;
5059
+ showMsgDetail(currentMsgDetailIdx);
5060
+ }
5061
+ } else if (matchKey(e, 'ArrowUp', 'KeyK')) {
5062
+ e.preventDefault();
5063
+ if (currentMsgDetailIdx > 0) {
5064
+ msgDetailFollowLatest = false;
5065
+ showMsgDetail(currentMsgDetailIdx - 1);
5066
+ }
5067
+ }
4832
5068
  }
4833
5069
  return;
4834
5070
  }
4835
5071
 
5072
+ // Global shortcuts
4836
5073
  if (e.key === '[') {
4837
5074
  e.preventDefault();
4838
5075
  toggleSidebar();
4839
5076
  return;
4840
5077
  }
4841
-
4842
- if ((e.key === 'L' || e.key === 'l') && e.shiftKey) {
5078
+ if (e.code === 'KeyL' && e.shiftKey) {
4843
5079
  e.preventDefault();
4844
5080
  toggleMessagePanel();
4845
5081
  return;
4846
5082
  }
4847
-
4848
- if ((e.key === 'M' || e.key === 'm') && e.shiftKey) {
5083
+ if (e.code === 'KeyM' && e.shiftKey) {
4849
5084
  e.preventDefault();
4850
5085
  const msgDetailModal = document.getElementById('msg-detail-modal');
4851
5086
  if (msgDetailModal.classList.contains('visible')) {
@@ -4860,30 +5095,34 @@
4860
5095
  // Tab toggles focus zone
4861
5096
  if (e.key === 'Tab') {
4862
5097
  e.preventDefault();
5098
+ if (focusZone === 'sidebar') {
5099
+ const hasCards = document.querySelector('.task-card');
5100
+ if (!hasCards) return;
5101
+ }
4863
5102
  setFocusZone(focusZone === 'board' ? 'sidebar' : 'board');
4864
5103
  return;
4865
5104
  }
4866
5105
 
4867
5106
  // Sidebar navigation
4868
5107
  if (focusZone === 'sidebar') {
4869
- if (e.key === 'j' || e.key === 'ArrowDown') {
5108
+ if (matchKey(e, 'ArrowDown', 'KeyJ')) {
4870
5109
  e.preventDefault();
4871
5110
  navigateSession(1);
4872
5111
  return;
4873
5112
  }
4874
- if (e.key === 'k' || e.key === 'ArrowUp') {
5113
+ if (matchKey(e, 'ArrowUp', 'KeyK')) {
4875
5114
  e.preventDefault();
4876
5115
  navigateSession(-1);
4877
5116
  return;
4878
5117
  }
4879
- if (e.key === 'h' || e.key === 'ArrowLeft') {
5118
+ if (matchKey(e, 'ArrowLeft', 'KeyH')) {
4880
5119
  e.preventDefault();
4881
- toggleSelectedSessionGroup(true);
5120
+ handleSidebarHorizontal(-1);
4882
5121
  return;
4883
5122
  }
4884
- if (e.key === 'l' || e.key === 'ArrowRight') {
5123
+ if (matchKey(e, 'ArrowRight', 'KeyL')) {
4885
5124
  e.preventDefault();
4886
- toggleSelectedSessionGroup(false);
5125
+ handleSidebarHorizontal(1);
4887
5126
  return;
4888
5127
  }
4889
5128
  if (e.key === 'Enter' || e.key === ' ') {
@@ -4895,82 +5134,70 @@
4895
5134
  setFocusZone('board');
4896
5135
  return;
4897
5136
  }
4898
- if (e.key === 'p' || e.key === 'P') {
4899
- e.preventDefault();
4900
- const highlighted = sessionsList.querySelector('.session-item.kb-selected');
4901
- const sid = highlighted?.dataset.sessionId || currentSessionId;
4902
- if (sid) openPlanForSession(sid);
4903
- return;
4904
- }
4905
- if (e.key === 'i' || e.key === 'I') {
4906
- e.preventDefault();
4907
- const highlighted = sessionsList.querySelector('.session-item.kb-selected');
4908
- const sid = highlighted?.dataset.sessionId || currentSessionId;
4909
- if (sid) showSessionInfoModal(sid);
4910
- return;
4911
- }
4912
- if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
4913
- e.preventDefault();
4914
- showHelpModal();
4915
- }
4916
- return;
4917
5137
  }
4918
5138
 
4919
5139
  // Board navigation
4920
- const navKeys = ['j','k','h','l','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'];
4921
- if (navKeys.includes(e.key)) {
4922
- e.preventDefault();
4923
- if (e.key === 'j' || e.key === 'ArrowDown') navigateVertical(1);
4924
- else if (e.key === 'k' || e.key === 'ArrowUp') navigateVertical(-1);
4925
- else if (e.key === 'h' || e.key === 'ArrowLeft') navigateHorizontal(-1);
4926
- else if (e.key === 'l' || e.key === 'ArrowRight') navigateHorizontal(1);
5140
+ if (focusZone === 'board') {
5141
+ if (matchKey(e, 'ArrowDown', 'KeyJ', 'ArrowUp', 'KeyK', 'ArrowLeft', 'KeyH', 'ArrowRight', 'KeyL')) {
5142
+ e.preventDefault();
5143
+ if (!selectedTaskId && !document.querySelector('.task-card.selected')) {
5144
+ setFocusZone('sidebar');
5145
+ return;
5146
+ }
5147
+ if (matchKey(e, 'ArrowDown', 'KeyJ')) navigateVertical(1);
5148
+ else if (matchKey(e, 'ArrowUp', 'KeyK')) navigateVertical(-1);
5149
+ else if (matchKey(e, 'ArrowLeft', 'KeyH')) navigateHorizontal(-1);
5150
+ else if (matchKey(e, 'ArrowRight', 'KeyL')) navigateHorizontal(1);
4927
5151
 
4928
- if (selectedTaskId && detailPanel.classList.contains('visible')) {
4929
- showTaskDetail(selectedTaskId, selectedSessionId);
5152
+ if (selectedTaskId && detailPanel.classList.contains('visible')) {
5153
+ showTaskDetail(selectedTaskId, selectedSessionId);
5154
+ }
5155
+ return;
4930
5156
  }
4931
- return;
4932
- }
4933
5157
 
4934
- if ((e.key === 'Enter' || e.key === ' ') && selectedTaskId && e.target.tagName !== 'BUTTON') {
4935
- e.preventDefault();
4936
- if (detailPanel.classList.contains('visible')) {
4937
- const labelEl = document.querySelector('.detail-label');
4938
- const shownId = labelEl?.textContent.match(/\d+/)?.[0];
4939
- if (shownId === selectedTaskId) {
4940
- closeDetailPanel();
5158
+ if ((e.key === 'Enter' || e.key === ' ') && selectedTaskId && e.target.tagName !== 'BUTTON') {
5159
+ e.preventDefault();
5160
+ if (detailPanel.classList.contains('visible')) {
5161
+ const labelEl = document.querySelector('.detail-label');
5162
+ const shownId = labelEl?.textContent.match(/\d+/)?.[0];
5163
+ if (shownId === selectedTaskId) {
5164
+ closeDetailPanel();
5165
+ } else {
5166
+ showTaskDetail(selectedTaskId, selectedSessionId);
5167
+ }
4941
5168
  } else {
4942
5169
  showTaskDetail(selectedTaskId, selectedSessionId);
4943
5170
  }
4944
- } else {
4945
- showTaskDetail(selectedTaskId, selectedSessionId);
5171
+ return;
5172
+ }
5173
+
5174
+ if (matchKey(e, 'KeyD') && selectedTaskId) {
5175
+ e.preventDefault();
5176
+ deleteTask(selectedTaskId, selectedSessionId || currentSessionId);
5177
+ return;
4946
5178
  }
4947
- return;
4948
5179
  }
4949
5180
 
4950
5181
  if (e.key === 'Escape') {
4951
5182
  if (detailPanel.classList.contains('visible')) closeDetailPanel();
4952
5183
  else if (messagePanelOpen) toggleMessagePanel();
4953
- }
4954
-
4955
- if (e.key === 'p' || e.key === 'P') {
4956
- e.preventDefault();
4957
- const sid = selectedSessionId || currentSessionId;
4958
- if (sid) openPlanForSession(sid);
4959
5184
  return;
4960
5185
  }
4961
5186
 
4962
- if (e.key === 'i' || e.key === 'I') {
5187
+ // Shared actions work in both sidebar and board
5188
+ const contextSid = focusZone === 'sidebar'
5189
+ ? (sessionsList.querySelector('.kb-selected')?.dataset.sessionId || currentSessionId)
5190
+ : (selectedSessionId || currentSessionId);
5191
+ if (matchKey(e, 'KeyP') && !e.shiftKey) {
4963
5192
  e.preventDefault();
4964
- const sid = selectedSessionId || currentSessionId;
4965
- if (sid) showSessionInfoModal(sid);
5193
+ if (contextSid) openPlanForSession(contextSid);
4966
5194
  return;
4967
5195
  }
4968
-
4969
- if ((e.key === 'd' || e.key === 'D') && selectedTaskId) {
5196
+ if (matchKey(e, 'KeyI') && !e.shiftKey) {
4970
5197
  e.preventDefault();
4971
- deleteTask(selectedTaskId, selectedSessionId || currentSessionId);
5198
+ if (contextSid) showSessionInfoModal(contextSid);
5199
+ return;
4972
5200
  }
4973
-
4974
5201
  if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
4975
5202
  e.preventDefault();
4976
5203
  showHelpModal();
@@ -5137,14 +5364,14 @@
5137
5364
  }
5138
5365
  if (!tabs.length) return '';
5139
5366
  const defaultTab = responseHtml ? 'response' : tabs[0].key;
5140
- const copyBtn = (key) => `<button class="agent-tab-copy" title="Copy" onclick="copyAgentTab('${id}-${key}',this)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>`;
5367
+ const copyBtnHtml = `<button class="agent-tab-copy" title="Copy" onclick="copyAgentTabActive('${id}',this)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>`;
5141
5368
  const tabsHtml = tabs.map(t =>
5142
5369
  `<div class="agent-tab${t.key === defaultTab ? ' active' : ''}" data-tab-group="${id}" data-tab-key="${t.key}" onclick="document.querySelectorAll('[data-tab-group=\\'${id}\\']').forEach(el=>{el.classList.toggle('active',el.dataset.tabKey==='${t.key}')})">${t.label}</div>`
5143
5370
  ).join('');
5144
5371
  const panelsHtml = panels.map(p =>
5145
- `<div class="agent-tab-panel${p.key === defaultTab ? ' active' : ''}" data-tab-group="${id}" data-tab-key="${p.key}">${copyBtn(p.key)}<div class="detail-desc rendered-md" style="font-size:13px;">${p.html}</div></div>`
5372
+ `<div class="agent-tab-panel${p.key === defaultTab ? ' active' : ''}" data-tab-group="${id}" data-tab-key="${p.key}"><div class="detail-desc rendered-md" style="font-size:13px;">${p.html}</div></div>`
5146
5373
  ).join('');
5147
- return `<div class="agent-tabs">${tabsHtml}</div>${panelsHtml}`;
5374
+ return `<div class="agent-tabs">${tabsHtml}${copyBtnHtml}</div>${panelsHtml}`;
5148
5375
  }
5149
5376
 
5150
5377
  async function copyAgentTab(key, btn) {
@@ -5153,6 +5380,13 @@
5153
5380
  copyWithFeedback(text, btn);
5154
5381
  }
5155
5382
 
5383
+ async function copyAgentTabActive(groupId, btn) {
5384
+ const activePanel = document.querySelector(`.agent-tab-panel.active[data-tab-group="${groupId}"]`);
5385
+ if (!activePanel) return;
5386
+ const key = groupId + '-' + activePanel.dataset.tabKey;
5387
+ copyAgentTab(key, btn);
5388
+ }
5389
+
5156
5390
  const ownerColors = [
5157
5391
  { bg: 'rgba(37, 99, 235, 0.14)', color: '#1d5bbf' }, // blue
5158
5392
  { bg: 'rgba(168, 85, 247, 0.14)', color: '#7c3aed' }, // purple
@@ -5641,7 +5875,7 @@
5641
5875
  }
5642
5876
 
5643
5877
  function closePlanModal() {
5644
- document.getElementById('plan-modal').classList.remove('visible');
5878
+ resetModalFullscreen('plan-modal');
5645
5879
  }
5646
5880
 
5647
5881
  function openPlanInEditor() {
@@ -5702,6 +5936,12 @@
5702
5936
 
5703
5937
  // Init
5704
5938
  loadTheme();
5939
+ ['live-updates', 'sessions-filters'].forEach(id => {
5940
+ if (localStorage.getItem(id + 'Collapsed') === 'true') {
5941
+ document.getElementById(id).classList.add('collapsed');
5942
+ document.getElementById(id === 'live-updates' ? 'live-updates-chevron' : 'sessions-chevron').classList.add('rotated');
5943
+ }
5944
+ });
5705
5945
 
5706
5946
  document.addEventListener('DOMContentLoaded', () => {
5707
5947
  if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
@@ -5788,6 +6028,9 @@
5788
6028
  <button class="icon-btn" aria-label="Open in editor" title="Open in editor" onclick="openMsgInEditor()">
5789
6029
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
5790
6030
  </button>
6031
+ <button id="msg-detail-fullscreen-btn" class="icon-btn" aria-label="Toggle fullscreen" title="Toggle fullscreen" onclick="toggleModalFullscreen('msg-detail-modal')">
6032
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
6033
+ </button>
5791
6034
  <button class="modal-close" aria-label="Close" onclick="closeMsgDetailModal()">
5792
6035
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
5793
6036
  </button>
@@ -5882,6 +6125,10 @@
5882
6125
  <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Shift+M</kbd></td>
5883
6126
  <td style="padding: 4px 0; color: var(--text-primary);">Open last message detail</td>
5884
6127
  </tr>
6128
+ <tr>
6129
+ <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">J/K</kbd></td>
6130
+ <td style="padding: 4px 0; color: var(--text-primary);">Navigate messages in detail modal</td>
6131
+ </tr>
5885
6132
  </table>
5886
6133
  </div>
5887
6134
  </div>
@@ -5986,6 +6233,9 @@
5986
6233
  <button class="icon-btn" aria-label="Copy plan" title="Copy plan" onclick="copyWithFeedback(_pendingPlanContent||'',this)">
5987
6234
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
5988
6235
  </button>
6236
+ <button id="plan-modal-fullscreen-btn" class="icon-btn" aria-label="Toggle fullscreen" title="Toggle fullscreen" onclick="toggleModalFullscreen('plan-modal')">
6237
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
6238
+ </button>
5989
6239
  <button class="modal-close" aria-label="Close dialog" onclick="closePlanModal()">
5990
6240
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
5991
6241
  <path d="M18 6L6 18M6 6l12 12"/>
@@ -6038,6 +6288,9 @@
6038
6288
  <button class="icon-btn" id="agent-modal-copy-all" aria-label="Copy all" title="Copy prompt + response" onclick="copyAgentModalAll(this)">
6039
6289
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
6040
6290
  </button>
6291
+ <button id="agent-modal-fullscreen-btn" class="icon-btn" aria-label="Toggle fullscreen" title="Toggle fullscreen" onclick="toggleModalFullscreen('agent-modal')">
6292
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
6293
+ </button>
6041
6294
  <button class="modal-close" aria-label="Close dialog" onclick="closeAgentModal()">
6042
6295
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
6043
6296
  <path d="M18 6L6 18M6 6l12 12"/>
@@ -6047,6 +6300,7 @@
6047
6300
  </div>
6048
6301
  <div id="agent-modal-body" class="modal-body" style="overflow-y: auto; overflow-x: hidden; flex: 0 1 auto; min-height: 0;"></div>
6049
6302
  <div class="modal-footer">
6303
+ <button id="agent-modal-dismiss-btn" class="btn agent-dismiss-btn" style="display:none" onclick="dismissAgent(currentAgentModalId);closeAgentModal()">Dismiss</button>
6050
6304
  <button class="btn btn-primary" onclick="closeAgentModal()">Close</button>
6051
6305
  </div>
6052
6306
  </div>
package/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const express = require('express');
4
4
  const path = require('path');
5
5
  const fs = require('fs').promises;
6
- const { existsSync, readdirSync, readFileSync, statSync, createReadStream } = require('fs');
6
+ const { existsSync, readdirSync, readFileSync, writeFileSync, statSync, createReadStream, unlinkSync } = require('fs');
7
7
  const readline = require('readline');
8
8
  const chokidar = require('chokidar');
9
9
  const os = require('os');
@@ -128,6 +128,11 @@ function loadTeamConfig(teamName) {
128
128
  }
129
129
  }
130
130
 
131
+ function resolveSessionId(sessionId) {
132
+ const teamConfig = loadTeamConfig(sessionId);
133
+ return (teamConfig && teamConfig.leadSessionId) ? teamConfig.leadSessionId : sessionId;
134
+ }
135
+
131
136
  // SSE clients for live updates
132
137
  const clients = new Set();
133
138
 
@@ -604,7 +609,7 @@ app.get('/api/sessions', async (req, res) => {
604
609
  }
605
610
 
606
611
  // Convert map to array and sort by most recently modified
607
- sessions = Array.from(sessionsMap.values());
612
+ let sessions = Array.from(sessionsMap.values());
608
613
  sessions.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt));
609
614
 
610
615
  // Apply limit if specified, but always include pinned sessions
@@ -755,12 +760,7 @@ app.get('/api/teams/:name', (req, res) => {
755
760
 
756
761
  // API: Get agents for a session
757
762
  app.get('/api/sessions/:sessionId/agents', (req, res) => {
758
- let sessionId = req.params.sessionId;
759
- // For team sessions, resolve to leader's session UUID
760
- const teamConfig = loadTeamConfig(sessionId);
761
- if (teamConfig && teamConfig.leadSessionId) {
762
- sessionId = teamConfig.leadSessionId;
763
- }
763
+ const sessionId = resolveSessionId(req.params.sessionId);
764
764
  const agentDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
765
765
  if (!existsSync(agentDir)) return res.json({ agents: [], waitingForUser: null });
766
766
  try {
@@ -809,6 +809,25 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
809
809
  }
810
810
  });
811
811
 
812
+ app.post('/api/sessions/:sessionId/agents/:agentId/stop', (req, res) => {
813
+ const sessionId = resolveSessionId(req.params.sessionId);
814
+ const agentId = path.basename(req.params.agentId).replace(/[^a-zA-Z0-9_-]/g, '');
815
+ const agentFile = path.join(AGENT_ACTIVITY_DIR, sessionId, agentId + '.json');
816
+ if (!existsSync(agentFile)) return res.status(404).json({ error: 'Agent not found' });
817
+ try {
818
+ const agent = JSON.parse(readFileSync(agentFile, 'utf8'));
819
+ agent.status = 'stopped';
820
+ agent.stoppedAt = new Date().toISOString();
821
+ writeFileSync(agentFile, JSON.stringify(agent), 'utf8');
822
+ // Also remove waiting state if present
823
+ const waitingFile = path.join(AGENT_ACTIVITY_DIR, sessionId, '_waiting.json');
824
+ if (existsSync(waitingFile)) unlinkSync(waitingFile);
825
+ res.json({ ok: true });
826
+ } catch (e) {
827
+ res.status(500).json({ error: 'Failed to stop agent' });
828
+ }
829
+ });
830
+
812
831
  app.get('/api/sessions/:sessionId/messages', (req, res) => {
813
832
  const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
814
833
  const metadata = loadSessionMetadata();
@@ -819,10 +838,8 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
819
838
  const agentMessages = messages.filter(m => m.tool === 'Agent' && m.toolUseId);
820
839
  if (agentMessages.length) {
821
840
  const progressMap = getProgressMap(jsonlPath);
822
- let sessionId = req.params.sessionId;
823
- const teamConfig = loadTeamConfig(sessionId);
824
- if (teamConfig && teamConfig.leadSessionId) sessionId = teamConfig.leadSessionId;
825
- const agentDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
841
+ const resolvedSid = resolveSessionId(req.params.sessionId);
842
+ const agentDir = path.join(AGENT_ACTIVITY_DIR, resolvedSid);
826
843
  for (const msg of agentMessages) {
827
844
  const entry = progressMap[msg.toolUseId];
828
845
  if (entry) {