claude-code-kanban 2.2.0-rc.13 → 2.2.0-rc.15

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/lib/parsers.js CHANGED
@@ -494,6 +494,11 @@ function readRecentMessages(jsonlPath, limit = 10) {
494
494
  require('fs').closeSync(fd);
495
495
  fd = null;
496
496
  messages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
497
+ for (let i = messages.length - 1; i > 0; i--) {
498
+ if (messages[i].systemLabel === 'Compacted' && messages[i - 1].systemLabel === 'Compacted') {
499
+ messages.splice(i, 1);
500
+ }
501
+ }
497
502
  return messages.slice(-limit);
498
503
  } catch (e) {
499
504
  if (fd) try { require('fs').closeSync(fd); } catch (_) {}
@@ -535,6 +540,7 @@ function buildAgentProgressMap(jsonlPath) {
535
540
  const bgAgentIdRe = /agentId: ([a-zA-Z0-9_@-]+)/;
536
541
  const tmToolIdRe = /"tool_use_id":"([^"]+)"/;
537
542
  const tmAgentIdRe = /agent_id: ([a-zA-Z0-9_@-]+)/;
543
+ const nameByToolUseId = {};
538
544
  for (const line of content.split('\n')) {
539
545
  if (line.includes('"agent_progress"')) {
540
546
  const agentMatch = re.exec(line);
@@ -562,8 +568,23 @@ function buildAgentProgressMap(jsonlPath) {
562
568
  if (toolIdMatch && agentMatch && !map[toolIdMatch[1]]) {
563
569
  map[toolIdMatch[1]] = { agentId: agentMatch[1], prompt: null };
564
570
  }
571
+ } else if (line.includes('"assistant"') && line.includes('"tool_use"') && line.includes('"Agent"')) {
572
+ try {
573
+ const obj = JSON.parse(line);
574
+ const blocks = obj.message?.content;
575
+ if (Array.isArray(blocks)) {
576
+ for (const b of blocks) {
577
+ if (b.type === 'tool_use' && b.name === 'Agent' && b.id && b.input?.name) {
578
+ nameByToolUseId[b.id] = b.input.name;
579
+ }
580
+ }
581
+ }
582
+ } catch (_) {}
565
583
  }
566
584
  }
585
+ for (const [key, entry] of Object.entries(map)) {
586
+ if (nameByToolUseId[key]) entry.name = nameByToolUseId[key];
587
+ }
567
588
  } catch (_) {}
568
589
  return map;
569
590
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.2.0-rc.13",
3
+ "version": "2.2.0-rc.15",
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/app.js CHANGED
@@ -1254,17 +1254,26 @@ function savePinnedSessions() {
1254
1254
  localStorage.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
1255
1255
  }
1256
1256
 
1257
- // unpinned → pinned → sticky → unpinned
1258
1257
  // biome-ignore lint/correctness/noUnusedVariables: used in HTML
1259
1258
  function toggleSessionPin(sessionId) {
1259
+ if (pinnedSessionIds.has(sessionId)) {
1260
+ pinnedSessionIds.delete(sessionId);
1261
+ stickySessionIds.delete(sessionId);
1262
+ } else {
1263
+ pinnedSessionIds.add(sessionId);
1264
+ }
1265
+ savePinnedSessions();
1266
+ renderSessions();
1267
+ }
1268
+
1269
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
1270
+ function toggleSessionSticky(sessionId) {
1260
1271
  if (stickySessionIds.has(sessionId)) {
1261
1272
  stickySessionIds.delete(sessionId);
1262
1273
  pinnedSessionIds.delete(sessionId);
1263
- } else if (pinnedSessionIds.has(sessionId)) {
1264
- pinnedSessionIds.delete(sessionId);
1265
- stickySessionIds.add(sessionId);
1266
1274
  } else {
1267
1275
  pinnedSessionIds.add(sessionId);
1276
+ stickySessionIds.add(sessionId);
1268
1277
  }
1269
1278
  savePinnedSessions();
1270
1279
  renderSessions();
@@ -1653,6 +1662,13 @@ function makeExpandToggle(_truncatedHtml, fullHtml, opts = {}) {
1653
1662
  }
1654
1663
 
1655
1664
  function autoSizeModal(modal, body) {
1665
+ modal.style.maxWidth = '';
1666
+ modal.classList.remove('has-mermaid');
1667
+ const hasMermaid = body.querySelector('pre.mermaid') !== null;
1668
+ if (hasMermaid) {
1669
+ modal.classList.add('has-mermaid');
1670
+ return;
1671
+ }
1656
1672
  const hasTable = body.querySelector('table') !== null;
1657
1673
  const hasPre = body.querySelector('pre') !== null;
1658
1674
  const desired = hasTable ? 1100 : body.textContent.length > 2000 || hasPre ? 960 : 860;
@@ -1745,8 +1761,9 @@ function renderAgentFooter() {
1745
1761
  // or started >30s after previous stopped (legitimate re-spawn). Filter the rest.
1746
1762
  const byType = {};
1747
1763
  for (const a of agents) {
1748
- if (!byType[a.type]) byType[a.type] = [];
1749
- byType[a.type].push(a);
1764
+ const groupKey = a.agentName || a.type;
1765
+ if (!byType[groupKey]) byType[groupKey] = [];
1766
+ byType[groupKey].push(a);
1750
1767
  }
1751
1768
  const filtered = [];
1752
1769
  for (const group of Object.values(byType)) {
@@ -1820,10 +1837,15 @@ function renderAgentFooter() {
1820
1837
  const colonIdx = rawType.indexOf(':');
1821
1838
  const typeNs = colonIdx > 0 ? rawType.substring(0, colonIdx + 1) : '';
1822
1839
  const typeName = colonIdx > 0 ? rawType.substring(colonIdx + 1) : rawType;
1840
+ const agentNameVal = a.agentName || null;
1841
+ const nameColor = agentNameVal ? getOwnerColor(agentNameVal) : null;
1842
+ const nameBadgeHtml = nameColor
1843
+ ? `<span class="task-owner-badge task-owner-badge--compact" style="background:${nameColor.bg};color:${nameColor.color}">${escapeHtml(agentNameVal)}</span>`
1844
+ : '';
1823
1845
  const agentColor = resolveNamedColor(a.color);
1824
1846
  const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
1825
1847
  return `<div class="agent-card"${colorStyle} onclick="showAgentModal('${a.agentId}')">
1826
- <div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span></div>
1848
+ <div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span>${nameBadgeHtml}</div>
1827
1849
  <div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
1828
1850
  ${msgHtml}
1829
1851
  </div>`;
@@ -1923,13 +1945,21 @@ function showAgentModal(agentId) {
1923
1945
  const elapsed = stopped && started ? stopped.getTime() - started.getTime() : started ? now - started.getTime() : 0;
1924
1946
 
1925
1947
  const statusDot = `<span class="agent-dot ${agent.status}" style="display:inline-block;vertical-align:middle;margin-right:6px;"></span>`;
1926
- title.innerHTML = `${statusDot} ${escapeHtml(agent.type || 'unknown')}`;
1948
+ const modalNameLabel = agent.agentName ? ` · ${escapeHtml(agent.agentName)}` : '';
1949
+ title.innerHTML = `${statusDot} ${escapeHtml(agent.type || 'unknown')}${modalNameLabel}`;
1927
1950
 
1928
1951
  const rows = [
1929
1952
  ['Status', agent.status],
1930
1953
  ['Agent ID', `<code style="font-size:12px;color:var(--text-tertiary)">${escapeHtml(agent.agentId)}</code>`],
1931
1954
  ['Duration', formatDuration(elapsed)],
1932
1955
  ];
1956
+ if (agent.agentName) {
1957
+ const ownerColor = getOwnerColor(agent.agentName);
1958
+ rows.push([
1959
+ 'Owner',
1960
+ `<span class="task-owner-badge" style="background:${ownerColor.bg};color:${ownerColor.color}">${escapeHtml(agent.agentName)}</span>`,
1961
+ ]);
1962
+ }
1933
1963
  if (agent.model)
1934
1964
  rows.push(['Model', `<code style="font-size:12px;color:var(--text-tertiary)">${escapeHtml(agent.model)}</code>`]);
1935
1965
  if (started) rows.push(['Started', started.toLocaleTimeString()]);
@@ -2167,7 +2197,7 @@ function renderSessions() {
2167
2197
 
2168
2198
  const pinState = getSessionPinState(session.id);
2169
2199
  const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
2170
- const pinTitle = pinState === 'sticky' ? 'Unpin' : pinState === 'pinned' ? 'Sticky pin' : 'Pin';
2200
+ const pinTitle = pinState === 'pinned' || pinState === 'sticky' ? 'Unpin' : 'Pin';
2171
2201
  const showCtx = !!session.contextStatus;
2172
2202
  return `
2173
2203
  <button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${!session.hasRecentLog && !session.inProgress && !session.hasWaitingForUser ? 'stale' : ''} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
@@ -4243,6 +4273,33 @@ function renderMarkdown(text) {
4243
4273
  return `<pre style="white-space:pre-wrap;margin:0;">${escapeHtml(text)}</pre>`;
4244
4274
  }
4245
4275
 
4276
+ function isLightTheme() {
4277
+ const saved = localStorage.getItem('theme');
4278
+ return (
4279
+ document.body.classList.contains('light') || (!saved && window.matchMedia('(prefers-color-scheme: light)').matches)
4280
+ );
4281
+ }
4282
+
4283
+ function getMermaidTheme() {
4284
+ return isLightTheme() ? 'default' : 'dark';
4285
+ }
4286
+
4287
+ function initMermaidBlocks(container) {
4288
+ if (typeof mermaid === 'undefined') return;
4289
+ const blocks = (container || document).querySelectorAll('pre.mermaid:not([data-processed])');
4290
+ if (blocks.length) mermaid.run({ nodes: [...blocks] });
4291
+ }
4292
+
4293
+ function reinitMermaidTheme() {
4294
+ if (typeof mermaid === 'undefined') return;
4295
+ mermaid.initialize({ startOnLoad: false, theme: getMermaidTheme() });
4296
+ document.querySelectorAll('pre.mermaid[data-processed]').forEach((el) => {
4297
+ el.removeAttribute('data-processed');
4298
+ el.innerHTML = escapeHtml(el.getAttribute('data-original') || '');
4299
+ });
4300
+ initMermaidBlocks();
4301
+ }
4302
+
4246
4303
  const _agentTabTexts = {};
4247
4304
 
4248
4305
  function renderAgentTabs(promptHtml, responseHtml, promptText, responseText) {
@@ -4506,22 +4563,21 @@ function toggleTheme() {
4506
4563
  updateThemeIcon();
4507
4564
  updateThemeColor(!isCurrentlyLight);
4508
4565
  syncHljsTheme();
4566
+ reinitMermaidTheme();
4509
4567
  }
4510
4568
 
4511
4569
  function syncHljsTheme() {
4512
- const isLight = document.body.classList.contains('light');
4513
- const dark = document.getElementById('hljs-theme-dark');
4514
- const light = document.getElementById('hljs-theme-light');
4515
- if (dark) dark.disabled = isLight;
4516
- if (light) light.disabled = !isLight;
4570
+ const light = isLightTheme();
4571
+ const dark$ = document.getElementById('hljs-theme-dark');
4572
+ const light$ = document.getElementById('hljs-theme-light');
4573
+ if (dark$) dark$.disabled = light;
4574
+ if (light$) light$.disabled = !light;
4517
4575
  }
4518
4576
 
4519
4577
  function updateThemeIcon() {
4520
- const saved = localStorage.getItem('theme');
4521
- const isLight =
4522
- document.body.classList.contains('light') || (!saved && window.matchMedia('(prefers-color-scheme: light)').matches);
4523
- document.getElementById('theme-icon-dark').style.display = isLight ? 'none' : 'block';
4524
- document.getElementById('theme-icon-light').style.display = isLight ? 'block' : 'none';
4578
+ const light = isLightTheme();
4579
+ document.getElementById('theme-icon-dark').style.display = light ? 'none' : 'block';
4580
+ document.getElementById('theme-icon-light').style.display = light ? 'block' : 'none';
4525
4581
  }
4526
4582
 
4527
4583
  function loadTheme() {
@@ -4695,8 +4751,20 @@ async function showSessionInfoModal(sessionId) {
4695
4751
  showInfoModal(session, teamConfig, tasks, planContent);
4696
4752
  }
4697
4753
 
4754
+ let _infoModalSessionId = null;
4698
4755
  let _pendingPlanContent = null;
4699
4756
 
4757
+ function updateStickyBtnState() {
4758
+ const stickyBtn = document.getElementById('session-info-sticky-btn');
4759
+ if (!stickyBtn || !_infoModalSessionId) return;
4760
+ const isSticky = stickySessionIds.has(_infoModalSessionId);
4761
+ stickyBtn.style.display = '';
4762
+ stickyBtn.classList.toggle('active', isSticky);
4763
+ stickyBtn.title = isSticky ? 'Remove sticky pin' : 'Sticky pin — always show at top';
4764
+ const svg = stickyBtn.querySelector('svg');
4765
+ if (svg) svg.setAttribute('fill', isSticky ? 'currentColor' : 'none');
4766
+ }
4767
+
4700
4768
  function showInfoModal(session, teamConfig, tasks, planContent) {
4701
4769
  const modal = document.getElementById('team-modal');
4702
4770
  const titleEl = document.getElementById('team-modal-title');
@@ -4834,6 +4902,8 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
4834
4902
  }
4835
4903
 
4836
4904
  bodyEl.innerHTML = html;
4905
+ _infoModalSessionId = session.id;
4906
+ updateStickyBtnState();
4837
4907
  modal.classList.add('visible');
4838
4908
 
4839
4909
  const keyHandler = (e) => {
@@ -4996,6 +5066,9 @@ document.addEventListener('DOMContentLoaded', () => {
4996
5066
  if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
4997
5067
  const renderer = new marked.Renderer();
4998
5068
  renderer.code = ({ text, lang }) => {
5069
+ if (lang === 'mermaid') {
5070
+ return `<pre class="mermaid" data-original="${escapeHtml(text)}">${escapeHtml(text)}</pre>`;
5071
+ }
4999
5072
  let highlighted;
5000
5073
  if (lang && hljs.getLanguage(lang)) {
5001
5074
  highlighted = hljs.highlight(text, { language: lang }).value;
@@ -5006,6 +5079,20 @@ document.addEventListener('DOMContentLoaded', () => {
5006
5079
  };
5007
5080
  marked.use({ renderer });
5008
5081
  }
5082
+
5083
+ if (typeof mermaid !== 'undefined') {
5084
+ mermaid.initialize({ startOnLoad: false, theme: getMermaidTheme() });
5085
+ let mermaidPending = false;
5086
+ const mo = new MutationObserver(() => {
5087
+ if (mermaidPending) return;
5088
+ mermaidPending = true;
5089
+ queueMicrotask(() => {
5090
+ mermaidPending = false;
5091
+ initMermaidBlocks();
5092
+ });
5093
+ });
5094
+ mo.observe(document.body, { childList: true, subtree: true });
5095
+ }
5009
5096
  });
5010
5097
 
5011
5098
  loadSidebarState();
package/public/index.html CHANGED
@@ -20,6 +20,7 @@
20
20
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css" id="hljs-theme-dark">
21
21
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css" id="hljs-theme-light" disabled>
22
22
  <script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
23
+ <script defer src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
23
24
  <script defer src="/app.js"></script>
24
25
  </head>
25
26
  <body>
@@ -482,11 +483,16 @@
482
483
  <div class="modal" onclick="event.stopPropagation()">
483
484
  <div class="modal-header">
484
485
  <h3 id="team-modal-title" class="modal-title">Team</h3>
485
- <button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
486
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
487
- <path d="M18 6L6 18M6 6l12 12"/>
488
- </svg>
489
- </button>
486
+ <div style="display:flex;align-items:center;gap:4px;">
487
+ <button id="session-info-sticky-btn" class="icon-btn" style="display:none" title="Sticky pin — always show at top" onclick="toggleSessionSticky(_infoModalSessionId); updateStickyBtnState()">
488
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 L15 9 L22 9 L17 14 L19 22 L12 18 L5 22 L7 14 L2 9 L9 9 Z"/></svg>
489
+ </button>
490
+ <button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
491
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
492
+ <path d="M18 6L6 18M6 6l12 12"/>
493
+ </svg>
494
+ </button>
495
+ </div>
490
496
  </div>
491
497
  <div id="team-modal-body" class="modal-body"></div>
492
498
  <div class="modal-footer">
package/public/style.css CHANGED
@@ -2009,6 +2009,9 @@ body::before {
2009
2009
  fill: var(--warning);
2010
2010
  stroke: var(--warning);
2011
2011
  }
2012
+ #session-info-sticky-btn.active {
2013
+ color: var(--warning);
2014
+ }
2012
2015
  .pinned-sessions-divider {
2013
2016
  height: 1px;
2014
2017
  margin: 4px 8px;
@@ -2418,6 +2421,13 @@ body::before {
2418
2421
  overflow: hidden;
2419
2422
  text-overflow: ellipsis;
2420
2423
  }
2424
+ .task-owner-badge--compact {
2425
+ font-size: 9px;
2426
+ padding: 2px 6px;
2427
+ border-radius: 3px;
2428
+ margin-left: 6px;
2429
+ vertical-align: middle;
2430
+ }
2421
2431
  .agent-status-row {
2422
2432
  display: flex;
2423
2433
  align-items: center;
@@ -3165,6 +3175,22 @@ select.form-input option:checked {
3165
3175
 
3166
3176
  /* #endregion */
3167
3177
 
3178
+ /* #region MERMAID */
3179
+ pre.mermaid {
3180
+ text-align: center;
3181
+ background: transparent;
3182
+ overflow-x: auto;
3183
+ }
3184
+ pre.mermaid svg {
3185
+ max-width: 100%;
3186
+ }
3187
+ .modal.has-mermaid {
3188
+ width: 80vw;
3189
+ max-width: 80vw;
3190
+ max-height: 90vh;
3191
+ }
3192
+ /* #endregion */
3193
+
3168
3194
  /* #region ANIMATIONS */
3169
3195
  @keyframes fadeSlideIn {
3170
3196
  from {
package/server.js CHANGED
@@ -1015,12 +1015,15 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1015
1015
  }
1016
1016
 
1017
1017
  const agentsNeedingPrompt = agents.filter(a => !a.prompt);
1018
- if (agentsNeedingPrompt.length && meta.jsonlPath) {
1018
+ const agentsNeedingName = agents.filter(a => !a.agentName);
1019
+ if ((agentsNeedingPrompt.length || agentsNeedingName.length) && meta.jsonlPath) {
1019
1020
  let byAgentId = {};
1021
+ let nameByAgentId = {};
1020
1022
  try {
1021
1023
  const progressMap = getProgressMap(meta.jsonlPath);
1022
1024
  for (const entry of Object.values(progressMap)) {
1023
1025
  if (entry.prompt && !byAgentId[entry.agentId]) byAgentId[entry.agentId] = entry.prompt;
1026
+ if (entry.name && !nameByAgentId[entry.agentId]) nameByAgentId[entry.agentId] = entry.name;
1024
1027
  }
1025
1028
  } catch (_) {}
1026
1029
  for (const agent of agentsNeedingPrompt) {
@@ -1028,6 +1031,9 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1028
1031
  || (() => { try { return extractPromptFromTranscript(subagentJsonlPath(meta, agent.agentId)); } catch (_) { return null; } })();
1029
1032
  if (prompt) persistPrompt(agent, prompt);
1030
1033
  }
1034
+ for (const agent of agentsNeedingName) {
1035
+ if (nameByAgentId[agent.agentId]) agent.agentName = nameByAgentId[agent.agentId];
1036
+ }
1031
1037
  }
1032
1038
 
1033
1039
  const agentsNeedingModel = agents.filter(a => !a.model);