claude-code-kanban 2.0.0 → 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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/public/index.html +201 -95
  3. package/server.js +28 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.0.0",
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
@@ -1208,6 +1208,7 @@
1208
1208
  border-radius: 4px;
1209
1209
  border: 1px solid transparent;
1210
1210
  transition: border-color 0.15s ease;
1211
+ overflow-x: auto;
1211
1212
  }
1212
1213
 
1213
1214
  .detail-desc:hover {
@@ -1665,13 +1666,13 @@
1665
1666
  .pinned-sessions-divider {
1666
1667
  height: 1px; margin: 4px 8px; background: color-mix(in srgb, var(--accent) 30%, transparent);
1667
1668
  }
1668
- .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; }
1669
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; }
1670
1671
  .agent-tab:hover { color: var(--text-secondary); border-bottom-color: var(--text-muted); }
1671
1672
  .agent-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
1672
1673
  .agent-tab-panel { display: none; padding-top: 12px; overflow: hidden; position: relative; }
1673
1674
  .agent-tab-panel.active { display: block; }
1674
- .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; }
1675
1676
  .agent-tab-copy:hover { opacity: 1; color: var(--text-primary); }
1676
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); }
1677
1678
  .toast.visible { opacity: 1; transform: translateY(0); }
@@ -1764,9 +1765,9 @@
1764
1765
  .agent-footer.collapsed .agent-footer-content { display: none; }
1765
1766
  .agent-card {
1766
1767
  display: flex;
1767
- align-items: center;
1768
- gap: 8px;
1769
- padding: 8px 14px;
1768
+ flex-direction: column;
1769
+ gap: 3px;
1770
+ padding: 8px 12px;
1770
1771
  background: var(--bg-elevated);
1771
1772
  border: 1px solid var(--border);
1772
1773
  border-radius: 8px;
@@ -1775,11 +1776,30 @@
1775
1776
  overflow: hidden;
1776
1777
  transition: opacity 0.3s;
1777
1778
  cursor: pointer;
1779
+ position: relative;
1778
1780
  }
1779
1781
  .agent-card:hover { border-color: var(--accent); }
1780
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
+ }
1781
1801
  .agent-dot {
1782
- width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
1802
+ width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
1783
1803
  }
1784
1804
  .agent-dot.active { background: var(--success); box-shadow: 0 0 6px var(--success); }
1785
1805
  .agent-dot.idle { background: var(--warning); box-shadow: 0 0 6px var(--warning); }
@@ -1792,9 +1812,14 @@
1792
1812
  }
1793
1813
  .agent-message {
1794
1814
  font-size: 11px; color: var(--text-muted);
1795
- max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1815
+ max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1796
1816
  }
1797
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
+ }
1798
1823
  .agent-badge { font-size: 12px; cursor: default; }
1799
1824
 
1800
1825
  /* Permission pending — indicated by ❓ badge only */
@@ -1948,10 +1973,17 @@
1948
1973
  background: rgba(0, 0, 0, 0.6);
1949
1974
  }
1950
1975
 
1976
+ .modal.fullscreen {
1977
+ width: 76vw !important;
1978
+ max-width: 76vw !important;
1979
+ height: 85vh !important;
1980
+ max-height: 85vh !important;
1981
+ }
1982
+
1951
1983
  .modal.plan-modal {
1952
1984
  width: 60vw;
1953
1985
  max-width: 60vw;
1954
- max-height: 90vh;
1986
+ max-height: 85vh;
1955
1987
  display: flex;
1956
1988
  flex-direction: column;
1957
1989
  }
@@ -3470,9 +3502,18 @@
3470
3502
  }
3471
3503
  const toolParamsHtml = renderToolParamsHtml(m.params);
3472
3504
  const toolResultHtml = renderToolResultHtml(m.toolResult, m.toolResultTruncated, m.toolResultFull);
3473
- const detailEscaped = escapeHtml(fullText);
3474
- const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
3475
- 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;
3476
3517
  } else {
3477
3518
  const text = stripAnsi(m.fullText || m.text);
3478
3519
  document.getElementById('msg-detail-title').textContent = m.type === 'assistant' ? 'Claude' : (m.systemLabel ? 'System' : 'User');
@@ -3491,6 +3532,7 @@
3491
3532
 
3492
3533
  const meta = [formatDate(m.timestamp)];
3493
3534
  if (m.model) meta.unshift(m.model);
3535
+ meta.push(`${idx + 1} of ${currentMessages.length}`);
3494
3536
  document.getElementById('msg-detail-meta').textContent = meta.join(' · ');
3495
3537
  currentPinDetailId = null;
3496
3538
  updateMsgDetailPinState();
@@ -3498,10 +3540,32 @@
3498
3540
  }
3499
3541
 
3500
3542
  function closeMsgDetailModal() {
3501
- document.getElementById('msg-detail-modal').classList.remove('visible');
3543
+ resetModalFullscreen('msg-detail-modal');
3502
3544
  msgDetailFollowLatest = false;
3503
3545
  }
3504
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
+
3505
3569
  let _toastTimer = null;
3506
3570
  function showToast(msg) {
3507
3571
  const el = document.getElementById('toast');
@@ -3605,7 +3669,9 @@
3605
3669
  function autoSizeModal(modal, body) {
3606
3670
  const hasTable = body.querySelector('table') !== null;
3607
3671
  const hasPre = body.querySelector('pre') !== null;
3608
- 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';
3609
3675
  }
3610
3676
 
3611
3677
  function renderToolResultHtml(toolResult, isTruncated, fullResult) {
@@ -3743,16 +3809,14 @@
3743
3809
  const promptTrunc = promptTrimmed.length > 60 ? promptTrimmed.substring(0, 60) + '…' : promptTrimmed;
3744
3810
  const msgHtml = promptTrunc
3745
3811
  ? `<div class="agent-message" title="${escapeHtml(promptTrimmed)}">${escapeHtml(promptTrunc)}</div>` : '';
3746
- const agentPinned = isAgentPinned(a.agentId);
3747
- 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;
3748
3816
  return `<div class="agent-card" onclick="showAgentModal('${a.agentId}')">
3749
- <span class="agent-dot ${a.status}"></span>
3750
- <div style="flex:1;min-width:0">
3751
- <div class="agent-type">${escapeHtml(a.type || 'unknown')}</div>
3752
- <div class="agent-status">${statusText}</div>
3753
- ${msgHtml}
3754
- </div>
3755
- ${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}
3756
3820
  </div>`;
3757
3821
  }).join('');
3758
3822
 
@@ -3797,6 +3861,17 @@
3797
3861
  updateAgentModalPinState();
3798
3862
  }
3799
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
+
3800
3875
  function showAgentModal(agentId) {
3801
3876
  const agent = currentAgents.find(a => a.agentId === agentId);
3802
3877
  if (!agent) return;
@@ -3839,6 +3914,9 @@
3839
3914
 
3840
3915
  body.innerHTML = html;
3841
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';
3842
3920
  modal.classList.add('visible');
3843
3921
  const keyHandler = (e) => {
3844
3922
  if (e.key === 'Escape') {
@@ -3851,7 +3929,7 @@
3851
3929
  }
3852
3930
 
3853
3931
  function closeAgentModal() {
3854
- document.getElementById('agent-modal').classList.remove('visible');
3932
+ resetModalFullscreen('agent-modal');
3855
3933
  currentAgentModalId = null;
3856
3934
  }
3857
3935
 
@@ -4865,11 +4943,11 @@
4865
4943
  if (e.key === 'Escape') {
4866
4944
  e.preventDefault();
4867
4945
  closeDeleteConfirmModal();
4868
- } else if (e.key === 'ArrowLeft' || e.key === 'h') {
4946
+ } else if (matchKey(e, 'ArrowLeft', 'KeyH')) {
4869
4947
  e.preventDefault();
4870
4948
  focusIdx = 0;
4871
4949
  buttons[focusIdx].focus();
4872
- } else if (e.key === 'ArrowRight' || e.key === 'l') {
4950
+ } else if (matchKey(e, 'ArrowRight', 'KeyL')) {
4873
4951
  e.preventDefault();
4874
4952
  focusIdx = 1;
4875
4953
  buttons[focusIdx].focus();
@@ -4951,35 +5029,58 @@
4951
5029
 
4952
5030
  document.getElementById('close-detail').onclick = closeDetailPanel;
4953
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
+
4954
5038
  document.addEventListener('keydown', (e) => {
4955
5039
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
4956
5040
  return;
4957
5041
  }
4958
5042
 
5043
+ // Modal guard — only Escape, Shift+M, and msg-detail J/K navigation pass through
4959
5044
  if (document.querySelector('.modal-overlay.visible')) {
4960
5045
  if (e.key === 'Escape') {
4961
5046
  document.querySelectorAll('.modal-overlay.visible').forEach(m => m.classList.remove('visible'));
4962
5047
  msgDetailFollowLatest = false;
4963
- } 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')) {
4964
5049
  e.preventDefault();
4965
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
+ }
4966
5068
  }
4967
5069
  return;
4968
5070
  }
4969
5071
 
5072
+ // Global shortcuts
4970
5073
  if (e.key === '[') {
4971
5074
  e.preventDefault();
4972
5075
  toggleSidebar();
4973
5076
  return;
4974
5077
  }
4975
-
4976
- if ((e.key === 'L' || e.key === 'l') && e.shiftKey) {
5078
+ if (e.code === 'KeyL' && e.shiftKey) {
4977
5079
  e.preventDefault();
4978
5080
  toggleMessagePanel();
4979
5081
  return;
4980
5082
  }
4981
-
4982
- if ((e.key === 'M' || e.key === 'm') && e.shiftKey) {
5083
+ if (e.code === 'KeyM' && e.shiftKey) {
4983
5084
  e.preventDefault();
4984
5085
  const msgDetailModal = document.getElementById('msg-detail-modal');
4985
5086
  if (msgDetailModal.classList.contains('visible')) {
@@ -5004,22 +5105,22 @@
5004
5105
 
5005
5106
  // Sidebar navigation
5006
5107
  if (focusZone === 'sidebar') {
5007
- if (e.key === 'j' || e.key === 'ArrowDown') {
5108
+ if (matchKey(e, 'ArrowDown', 'KeyJ')) {
5008
5109
  e.preventDefault();
5009
5110
  navigateSession(1);
5010
5111
  return;
5011
5112
  }
5012
- if (e.key === 'k' || e.key === 'ArrowUp') {
5113
+ if (matchKey(e, 'ArrowUp', 'KeyK')) {
5013
5114
  e.preventDefault();
5014
5115
  navigateSession(-1);
5015
5116
  return;
5016
5117
  }
5017
- if (e.key === 'h' || e.key === 'ArrowLeft') {
5118
+ if (matchKey(e, 'ArrowLeft', 'KeyH')) {
5018
5119
  e.preventDefault();
5019
5120
  handleSidebarHorizontal(-1);
5020
5121
  return;
5021
5122
  }
5022
- if (e.key === 'l' || e.key === 'ArrowRight') {
5123
+ if (matchKey(e, 'ArrowRight', 'KeyL')) {
5023
5124
  e.preventDefault();
5024
5125
  handleSidebarHorizontal(1);
5025
5126
  return;
@@ -5033,86 +5134,70 @@
5033
5134
  setFocusZone('board');
5034
5135
  return;
5035
5136
  }
5036
- if (e.key === 'p' || e.key === 'P') {
5037
- e.preventDefault();
5038
- const highlighted = sessionsList.querySelector('.kb-selected');
5039
- const sid = highlighted?.dataset.sessionId || currentSessionId;
5040
- if (sid) openPlanForSession(sid);
5041
- return;
5042
- }
5043
- if (e.key === 'i' || e.key === 'I') {
5044
- e.preventDefault();
5045
- const highlighted = sessionsList.querySelector('.kb-selected');
5046
- const sid = highlighted?.dataset.sessionId || currentSessionId;
5047
- if (sid) showSessionInfoModal(sid);
5048
- return;
5049
- }
5050
- if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
5051
- e.preventDefault();
5052
- showHelpModal();
5053
- }
5054
- return;
5055
5137
  }
5056
5138
 
5057
5139
  // Board navigation
5058
- const navKeys = ['j','k','h','l','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'];
5059
- if (navKeys.includes(e.key)) {
5060
- e.preventDefault();
5061
- if (!selectedTaskId && !document.querySelector('.task-card.selected')) {
5062
- setFocusZone('sidebar');
5063
- return;
5064
- }
5065
- if (e.key === 'j' || e.key === 'ArrowDown') navigateVertical(1);
5066
- else if (e.key === 'k' || e.key === 'ArrowUp') navigateVertical(-1);
5067
- else if (e.key === 'h' || e.key === 'ArrowLeft') navigateHorizontal(-1);
5068
- 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);
5069
5151
 
5070
- if (selectedTaskId && detailPanel.classList.contains('visible')) {
5071
- showTaskDetail(selectedTaskId, selectedSessionId);
5152
+ if (selectedTaskId && detailPanel.classList.contains('visible')) {
5153
+ showTaskDetail(selectedTaskId, selectedSessionId);
5154
+ }
5155
+ return;
5072
5156
  }
5073
- return;
5074
- }
5075
5157
 
5076
- if ((e.key === 'Enter' || e.key === ' ') && selectedTaskId && e.target.tagName !== 'BUTTON') {
5077
- e.preventDefault();
5078
- if (detailPanel.classList.contains('visible')) {
5079
- const labelEl = document.querySelector('.detail-label');
5080
- const shownId = labelEl?.textContent.match(/\d+/)?.[0];
5081
- if (shownId === selectedTaskId) {
5082
- 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
+ }
5083
5168
  } else {
5084
5169
  showTaskDetail(selectedTaskId, selectedSessionId);
5085
5170
  }
5086
- } else {
5087
- showTaskDetail(selectedTaskId, selectedSessionId);
5171
+ return;
5172
+ }
5173
+
5174
+ if (matchKey(e, 'KeyD') && selectedTaskId) {
5175
+ e.preventDefault();
5176
+ deleteTask(selectedTaskId, selectedSessionId || currentSessionId);
5177
+ return;
5088
5178
  }
5089
- return;
5090
5179
  }
5091
5180
 
5092
5181
  if (e.key === 'Escape') {
5093
5182
  if (detailPanel.classList.contains('visible')) closeDetailPanel();
5094
5183
  else if (messagePanelOpen) toggleMessagePanel();
5095
- }
5096
-
5097
- if (e.key === 'p' || e.key === 'P') {
5098
- e.preventDefault();
5099
- const sid = selectedSessionId || currentSessionId;
5100
- if (sid) openPlanForSession(sid);
5101
5184
  return;
5102
5185
  }
5103
5186
 
5104
- 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) {
5105
5192
  e.preventDefault();
5106
- const sid = selectedSessionId || currentSessionId;
5107
- if (sid) showSessionInfoModal(sid);
5193
+ if (contextSid) openPlanForSession(contextSid);
5108
5194
  return;
5109
5195
  }
5110
-
5111
- if ((e.key === 'd' || e.key === 'D') && selectedTaskId) {
5196
+ if (matchKey(e, 'KeyI') && !e.shiftKey) {
5112
5197
  e.preventDefault();
5113
- deleteTask(selectedTaskId, selectedSessionId || currentSessionId);
5198
+ if (contextSid) showSessionInfoModal(contextSid);
5199
+ return;
5114
5200
  }
5115
-
5116
5201
  if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
5117
5202
  e.preventDefault();
5118
5203
  showHelpModal();
@@ -5279,14 +5364,14 @@
5279
5364
  }
5280
5365
  if (!tabs.length) return '';
5281
5366
  const defaultTab = responseHtml ? 'response' : tabs[0].key;
5282
- 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>`;
5283
5368
  const tabsHtml = tabs.map(t =>
5284
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>`
5285
5370
  ).join('');
5286
5371
  const panelsHtml = panels.map(p =>
5287
- `<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>`
5288
5373
  ).join('');
5289
- return `<div class="agent-tabs">${tabsHtml}</div>${panelsHtml}`;
5374
+ return `<div class="agent-tabs">${tabsHtml}${copyBtnHtml}</div>${panelsHtml}`;
5290
5375
  }
5291
5376
 
5292
5377
  async function copyAgentTab(key, btn) {
@@ -5295,6 +5380,13 @@
5295
5380
  copyWithFeedback(text, btn);
5296
5381
  }
5297
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
+
5298
5390
  const ownerColors = [
5299
5391
  { bg: 'rgba(37, 99, 235, 0.14)', color: '#1d5bbf' }, // blue
5300
5392
  { bg: 'rgba(168, 85, 247, 0.14)', color: '#7c3aed' }, // purple
@@ -5783,7 +5875,7 @@
5783
5875
  }
5784
5876
 
5785
5877
  function closePlanModal() {
5786
- document.getElementById('plan-modal').classList.remove('visible');
5878
+ resetModalFullscreen('plan-modal');
5787
5879
  }
5788
5880
 
5789
5881
  function openPlanInEditor() {
@@ -5936,6 +6028,9 @@
5936
6028
  <button class="icon-btn" aria-label="Open in editor" title="Open in editor" onclick="openMsgInEditor()">
5937
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>
5938
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>
5939
6034
  <button class="modal-close" aria-label="Close" onclick="closeMsgDetailModal()">
5940
6035
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
5941
6036
  </button>
@@ -6030,6 +6125,10 @@
6030
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>
6031
6126
  <td style="padding: 4px 0; color: var(--text-primary);">Open last message detail</td>
6032
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>
6033
6132
  </table>
6034
6133
  </div>
6035
6134
  </div>
@@ -6134,6 +6233,9 @@
6134
6233
  <button class="icon-btn" aria-label="Copy plan" title="Copy plan" onclick="copyWithFeedback(_pendingPlanContent||'',this)">
6135
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>
6136
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>
6137
6239
  <button class="modal-close" aria-label="Close dialog" onclick="closePlanModal()">
6138
6240
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
6139
6241
  <path d="M18 6L6 18M6 6l12 12"/>
@@ -6186,6 +6288,9 @@
6186
6288
  <button class="icon-btn" id="agent-modal-copy-all" aria-label="Copy all" title="Copy prompt + response" onclick="copyAgentModalAll(this)">
6187
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>
6188
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>
6189
6294
  <button class="modal-close" aria-label="Close dialog" onclick="closeAgentModal()">
6190
6295
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
6191
6296
  <path d="M18 6L6 18M6 6l12 12"/>
@@ -6195,6 +6300,7 @@
6195
6300
  </div>
6196
6301
  <div id="agent-modal-body" class="modal-body" style="overflow-y: auto; overflow-x: hidden; flex: 0 1 auto; min-height: 0;"></div>
6197
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>
6198
6304
  <button class="btn btn-primary" onclick="closeAgentModal()">Close</button>
6199
6305
  </div>
6200
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
 
@@ -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) {