claude-code-kanban 2.2.0 → 2.3.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/lib/parsers.js CHANGED
@@ -541,6 +541,7 @@ function buildAgentProgressMap(jsonlPath) {
541
541
  const tmToolIdRe = /"tool_use_id":"([^"]+)"/;
542
542
  const tmAgentIdRe = /agent_id: ([a-zA-Z0-9_@-]+)/;
543
543
  const nameByToolUseId = {};
544
+ const descByToolUseId = {};
544
545
  for (const line of content.split('\n')) {
545
546
  if (line.includes('"agent_progress"')) {
546
547
  const agentMatch = re.exec(line);
@@ -574,8 +575,24 @@ function buildAgentProgressMap(jsonlPath) {
574
575
  const blocks = obj.message?.content;
575
576
  if (Array.isArray(blocks)) {
576
577
  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;
578
+ if (b.type === 'tool_use' && b.name === 'Agent' && b.id) {
579
+ if (b.input?.name) nameByToolUseId[b.id] = b.input.name;
580
+ if (b.input?.description) descByToolUseId[b.id] = b.input.description;
581
+ }
582
+ }
583
+ }
584
+ } catch (_) {}
585
+ } else if (line.includes('"toolUseResult"') && line.includes('"agentId"') && line.includes('"tool_result"')) {
586
+ try {
587
+ const obj = JSON.parse(line);
588
+ const tur = obj.toolUseResult;
589
+ if (tur?.agentId) {
590
+ const blocks = obj.message?.content;
591
+ if (Array.isArray(blocks)) {
592
+ for (const b of blocks) {
593
+ if (b.type === 'tool_result' && b.tool_use_id && !map[b.tool_use_id]) {
594
+ map[b.tool_use_id] = { agentId: tur.agentId, prompt: tur.prompt || null };
595
+ }
579
596
  }
580
597
  }
581
598
  }
@@ -584,6 +601,7 @@ function buildAgentProgressMap(jsonlPath) {
584
601
  }
585
602
  for (const [key, entry] of Object.entries(map)) {
586
603
  if (nameByToolUseId[key]) entry.name = nameByToolUseId[key];
604
+ if (descByToolUseId[key]) entry.description = descByToolUseId[key];
587
605
  }
588
606
  } catch (_) {}
589
607
  return map;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.2.0",
3
+ "version": "2.3.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/app.js CHANGED
@@ -25,6 +25,7 @@ let agentPollInterval = null;
25
25
  let selectedTaskId = null;
26
26
  let selectedSessionId = null;
27
27
  let focusZone = 'board'; // 'board' | 'sidebar'
28
+ let appConfig = { marketplaceUrl: null };
28
29
  let selectedSessionIdx = -1;
29
30
  let selectedSessionKbId = null;
30
31
  let sessionJustSelected = false;
@@ -36,6 +37,7 @@ let msgUserScrolledUp = false;
36
37
  const MSG_MAX_LOADED = 200;
37
38
  let currentProjectPath = null;
38
39
  let currentProjectSessionIds = [];
40
+ const dismissedSessionIds = new Set();
39
41
 
40
42
  function resetMessageScrollState() {
41
43
  msgUserScrolledUp = false;
@@ -627,7 +629,10 @@ async function viewAgentLog(agentId) {
627
629
  await fetchAgents(currentSessionId);
628
630
  agent = findAgentById(agentId);
629
631
  }
630
- if (!agent) return;
632
+ if (!agent) {
633
+ if (!currentSessionId) return;
634
+ agent = { agentId: agentId, type: 'Agent', _sourceSessionId: currentSessionId };
635
+ }
631
636
  const resolvedId = agent.agentId;
632
637
  const shortId = resolvedId.length > 8 ? resolvedId.slice(0, 8) : resolvedId;
633
638
  const agentSessionId = agent._sourceSessionId || currentSessionId;
@@ -652,6 +657,7 @@ async function viewAgentLog(agentId) {
652
657
  const data = JSON.parse(e.data);
653
658
  currentMessages = data.messages;
654
659
  if (messagePanelOpen) renderMessages(data.messages);
660
+ maybeFollowLatest();
655
661
  } catch (_) {}
656
662
  });
657
663
  agentLogSSE.onerror = () => {};
@@ -684,6 +690,7 @@ async function fetchAgentMessages() {
684
690
  if (!agentLogMode || agentLogMode.agentId !== agentId) return;
685
691
  currentMessages = data.messages;
686
692
  if (messagePanelOpen) renderMessages(data.messages);
693
+ maybeFollowLatest();
687
694
  } catch (e) {
688
695
  console.error('[fetchAgentMessages]', e);
689
696
  }
@@ -737,9 +744,7 @@ async function fetchMessages(sessionId) {
737
744
  }
738
745
  }
739
746
 
740
- if (msgDetailFollowLatest && currentMessages.length) {
741
- showMsgDetail(currentMessages.length - 1);
742
- }
747
+ maybeFollowLatest();
743
748
  } catch (e) {
744
749
  console.error('[fetchMessages]', e);
745
750
  }
@@ -1317,6 +1322,8 @@ function _renderPinToDetail(pin) {
1317
1322
  }
1318
1323
 
1319
1324
  const SESSION_PIN_SVG = PIN_SVG.replace('width="14" height="14"', 'width="12" height="12"');
1325
+ const MARKETPLACE_SVG =
1326
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>';
1320
1327
 
1321
1328
  //#endregion
1322
1329
 
@@ -1538,8 +1545,15 @@ function formatTaskToolDetail(params) {
1538
1545
  }
1539
1546
  function getToolDetail(tool, params, detail) {
1540
1547
  if (TASK_TOOLS.has(tool)) return formatTaskToolDetail(params);
1541
- if (detail) return ` <span style="color:var(--text-muted)">${escapeHtml(detail)}</span>`;
1542
- return '';
1548
+ if (!detail) return '';
1549
+ let extra = '';
1550
+ if (tool === 'Read' && params) {
1551
+ const parts = [];
1552
+ if (params.offset) parts.push(`L${params.offset}`);
1553
+ if (params.limit) parts.push(`+${params.limit}`);
1554
+ if (parts.length) extra = ` <span style="color:var(--text-muted);opacity:.7">${parts.join(' ')}</span>`;
1555
+ }
1556
+ return ` <span style="color:var(--text-muted)">${escapeHtml(detail)}</span>${extra}`;
1543
1557
  }
1544
1558
  function renderTaskResult(toolResult) {
1545
1559
  if (!toolResult) return '';
@@ -1828,10 +1842,12 @@ function renderAgentFooter() {
1828
1842
  : a.status === 'idle'
1829
1843
  ? `idle · ${formatDuration(elapsed)}`
1830
1844
  : `active · ${formatDuration(elapsed)}`;
1845
+ const descText = a.description || '';
1831
1846
  const promptTrimmed = stripAnsi(stripTeammateWrapper((a.prompt || '').trim())).replace(/[\r\n]+/g, ' ');
1832
- const promptTrunc = promptTrimmed.length > 60 ? `${promptTrimmed.substring(0, 60)}…` : promptTrimmed;
1833
- const msgHtml = promptTrunc
1834
- ? `<div class="agent-message" title="${escapeHtml(promptTrimmed)}">${escapeHtml(promptTrunc)}</div>`
1847
+ const displayText = descText || promptTrimmed;
1848
+ const displayTrunc = displayText.length > 60 ? `${displayText.substring(0, 60)}…` : displayText;
1849
+ const msgHtml = displayTrunc
1850
+ ? `<div class="agent-message" title="${escapeHtml(displayText)}">${escapeHtml(displayTrunc)}</div>`
1835
1851
  : '';
1836
1852
  const rawType = a.type || 'unknown';
1837
1853
  const colonIdx = rawType.indexOf(':');
@@ -2091,6 +2107,7 @@ function renderSessions() {
2091
2107
  const now = Date.now();
2092
2108
  const activeSessionIds = new Set();
2093
2109
  filteredSessions = filteredSessions.filter((s) => {
2110
+ if (dismissedSessionIds.has(s.id)) return false;
2094
2111
  const isActive =
2095
2112
  s.hasMessages &&
2096
2113
  ((!s.sharedTaskList && (s.pending > 0 || s.inProgress > 0)) ||
@@ -2219,6 +2236,7 @@ function renderSessions() {
2219
2236
  ${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
2220
2237
  ${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">📋</span>` : ''}
2221
2238
  ${session.hasWaitingForUser ? '<span class="agent-badge" title="Waiting for user">❓</span>' : ''}
2239
+ ${(window.__HUB__?.enabled || appConfig.marketplaceUrl) && session.project ? `<span class="marketplace-btn" data-project-path="${escapeHtml(session.project)}" onclick="event.stopPropagation(); openMarketplace(this.dataset.projectPath)" title="Open in Marketplace">${MARKETPLACE_SVG}</span>` : ''}
2222
2240
  ${isLive ? '<span class="pulse"></span>' : ''}
2223
2241
  </span>
2224
2242
  <div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
@@ -2770,7 +2788,7 @@ function navigateSession(direction, items) {
2770
2788
  }
2771
2789
  const currentEl = items[selectedSessionIdx];
2772
2790
  let newIdx = selectedSessionIdx + direction;
2773
- if (!currentEl || !currentEl.isConnected) {
2791
+ if (!currentEl?.isConnected) {
2774
2792
  const restoredIdx = selectedSessionKbId ? items.findIndex((el) => getKbId(el) === selectedSessionKbId) : -1;
2775
2793
  newIdx = restoredIdx >= 0 ? restoredIdx : 0;
2776
2794
  }
@@ -4081,7 +4099,12 @@ function setupEventSource() {
4081
4099
  }
4082
4100
 
4083
4101
  if (data.type === 'team-update') {
4084
- debouncedRefresh(data.teamName, false);
4102
+ const teamSession = sessions.find((s) => s.isTeam && s.teamName === data.teamName);
4103
+ if (teamSession) {
4104
+ debouncedRefresh(teamSession.id, false);
4105
+ } else if (currentSessionId) {
4106
+ debouncedRefresh(currentSessionId, false);
4107
+ }
4085
4108
  }
4086
4109
  };
4087
4110
  }
@@ -4238,6 +4261,12 @@ function renderContextDetail(raw) {
4238
4261
  //#endregion
4239
4262
 
4240
4263
  //#region UTILS
4264
+ function maybeFollowLatest() {
4265
+ if (msgDetailFollowLatest && currentMessages.length) {
4266
+ showMsgDetail(currentMessages.length - 1);
4267
+ }
4268
+ }
4269
+
4241
4270
  function isSessionActive(s) {
4242
4271
  return s.hasRecentLog || s.inProgress > 0 || s.hasActiveAgents || s.hasWaitingForUser;
4243
4272
  }
@@ -4908,6 +4937,7 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
4908
4937
  bodyEl.innerHTML = html;
4909
4938
  _infoModalSessionId = session.id;
4910
4939
  updateStickyBtnState();
4940
+ updateDismissBtnState();
4911
4941
  modal.classList.add('visible');
4912
4942
 
4913
4943
  const keyHandler = (e) => {
@@ -4925,6 +4955,25 @@ function closeTeamModal() {
4925
4955
  document.getElementById('team-modal').classList.remove('visible');
4926
4956
  }
4927
4957
 
4958
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
4959
+ function toggleDismissSession(sessionId) {
4960
+ if (dismissedSessionIds.has(sessionId)) {
4961
+ dismissedSessionIds.delete(sessionId);
4962
+ } else {
4963
+ dismissedSessionIds.add(sessionId);
4964
+ }
4965
+ updateDismissBtnState();
4966
+ renderSessions();
4967
+ }
4968
+
4969
+ function updateDismissBtnState() {
4970
+ const btn = document.getElementById('session-info-dismiss-btn');
4971
+ if (!btn || !_infoModalSessionId) return;
4972
+ const isDismissed = dismissedSessionIds.has(_infoModalSessionId);
4973
+ btn.textContent = isDismissed ? 'Restore' : 'Dismiss';
4974
+ btn.title = isDismissed ? 'Restore — show in active list again' : 'Dismiss — hide from active list';
4975
+ }
4976
+
4928
4977
  let _planSessionId = null;
4929
4978
 
4930
4979
  //#endregion
@@ -4991,6 +5040,18 @@ function openFolderInEditor(folder, file) {
4991
5040
  postAndToast('/api/open-folder', body, 'folder');
4992
5041
  }
4993
5042
 
5043
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
5044
+ function openMarketplace(projectPath) {
5045
+ const params = new URLSearchParams({ project: projectPath });
5046
+ if (window.__HUB__?.enabled) {
5047
+ hubNavigate('marketplace', `?${params}`);
5048
+ } else if (appConfig.marketplaceUrl) {
5049
+ const url = new URL(appConfig.marketplaceUrl);
5050
+ url.search = params.toString();
5051
+ window.open(url.toString(), '_blank');
5052
+ }
5053
+ }
5054
+
4994
5055
  //#endregion
4995
5056
 
4996
5057
  //#region OWNER_FILTER
@@ -4999,7 +5060,7 @@ function updateOwnerFilter() {
4999
5060
  const select = document.getElementById('owner-filter');
5000
5061
 
5001
5062
  const session = sessions.find((s) => s.id === currentSessionId);
5002
- if (!session || !session.isTeam) {
5063
+ if (!session?.isTeam) {
5003
5064
  bar.classList.remove('visible');
5004
5065
  return;
5005
5066
  }
@@ -5169,6 +5230,13 @@ if (urlState.search) {
5169
5230
  document.getElementById('search-clear-btn').classList.add('visible');
5170
5231
  }
5171
5232
 
5233
+ fetch('/api/config')
5234
+ .then((r) => r.json())
5235
+ .then((c) => {
5236
+ appConfig = c;
5237
+ })
5238
+ .catch(() => {});
5239
+
5172
5240
  fetchSessions().then(async () => {
5173
5241
  if (urlState.projectView) {
5174
5242
  try {
@@ -5209,3 +5277,24 @@ window.addEventListener('popstate', () => {
5209
5277
  if (s.messages !== messagePanelOpen) toggleMessagePanel();
5210
5278
  });
5211
5279
  //#endregion
5280
+
5281
+ // #region HUB_INTEGRATION
5282
+ (async function initHub() {
5283
+ const cfg = await fetch('/hub-config')
5284
+ .then((r) => r.json())
5285
+ .catch(() => ({}));
5286
+ if (!cfg.enabled) return;
5287
+ window.__HUB__ = cfg;
5288
+ document.addEventListener('keydown', (e) => {
5289
+ if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
5290
+ e.preventDefault();
5291
+ window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
5292
+ }
5293
+ });
5294
+ })();
5295
+
5296
+ window.hubNavigate = function hubNavigate(app, url) {
5297
+ if (!window.__HUB__?.enabled) return;
5298
+ window.parent?.postMessage({ type: 'hub:navigate', app, url }, '*');
5299
+ };
5300
+ // #endregion HUB_INTEGRATION
package/public/index.html CHANGED
@@ -496,6 +496,7 @@
496
496
  </div>
497
497
  <div id="team-modal-body" class="modal-body"></div>
498
498
  <div class="modal-footer">
499
+ <button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId)">Dismiss</button>
499
500
  <button class="btn btn-primary" onclick="closeTeamModal()">Close</button>
500
501
  </div>
501
502
  </div>
package/public/style.css CHANGED
@@ -3330,6 +3330,22 @@ pre.mermaid svg {
3330
3330
  color: var(--accent);
3331
3331
  }
3332
3332
 
3333
+ .marketplace-btn {
3334
+ color: #888;
3335
+ cursor: pointer;
3336
+ display: inline-flex;
3337
+ align-items: center;
3338
+ transition:
3339
+ color 0.15s,
3340
+ filter 0.15s;
3341
+ border-radius: 3px;
3342
+ }
3343
+
3344
+ .marketplace-btn:hover {
3345
+ color: var(--accent);
3346
+ filter: drop-shadow(0 0 3px var(--accent));
3347
+ }
3348
+
3333
3349
  .project-group-header .group-count {
3334
3350
  font-weight: 400;
3335
3351
  color: var(--text-muted);
package/server.js CHANGED
@@ -48,6 +48,17 @@ function getClaudeDir() {
48
48
  return process.env.CLAUDE_DIR || path.join(os.homedir(), '.claude');
49
49
  }
50
50
 
51
+ function getMarketplaceUrl() {
52
+ const idx = process.argv.findIndex(arg => arg.startsWith('--marketplace-url'));
53
+ if (idx !== -1) {
54
+ const arg = process.argv[idx];
55
+ if (arg.includes('=')) return arg.split('=').slice(1).join('=');
56
+ if (process.argv[idx + 1]) return process.argv[idx + 1];
57
+ }
58
+ return process.env.MARKETPLACE_URL || null;
59
+ }
60
+
61
+ const MARKETPLACE_URL = getMarketplaceUrl();
51
62
  const CLAUDE_DIR = getClaudeDir();
52
63
  const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
53
64
  const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
@@ -181,6 +192,10 @@ app.param('taskId', (req, res, next, val) => {
181
192
  // Parse JSON bodies
182
193
  app.use(express.json());
183
194
 
195
+ app.get('/hub-config', (_req, res) => {
196
+ res.json({ enabled: !!process.env.CLAUDE_HUB, url: process.env.HUB_URL || null });
197
+ });
198
+
184
199
  // Serve static files
185
200
  app.get('/sw.js', (req, res) => {
186
201
  res.setHeader('Cache-Control', 'no-cache');
@@ -1016,14 +1031,17 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1016
1031
 
1017
1032
  const agentsNeedingPrompt = agents.filter(a => !a.prompt);
1018
1033
  const agentsNeedingName = agents.filter(a => !a.agentName);
1019
- if ((agentsNeedingPrompt.length || agentsNeedingName.length) && meta.jsonlPath) {
1034
+ const agentsNeedingDesc = agents.filter(a => !a.description);
1035
+ if ((agentsNeedingPrompt.length || agentsNeedingName.length || agentsNeedingDesc.length) && meta.jsonlPath) {
1020
1036
  let byAgentId = {};
1021
1037
  let nameByAgentId = {};
1038
+ let descByAgentId = {};
1022
1039
  try {
1023
1040
  const progressMap = getProgressMap(meta.jsonlPath);
1024
1041
  for (const entry of Object.values(progressMap)) {
1025
1042
  if (entry.prompt && !byAgentId[entry.agentId]) byAgentId[entry.agentId] = entry.prompt;
1026
1043
  if (entry.name && !nameByAgentId[entry.agentId]) nameByAgentId[entry.agentId] = entry.name;
1044
+ if (entry.description && !descByAgentId[entry.agentId]) descByAgentId[entry.agentId] = entry.description;
1027
1045
  }
1028
1046
  } catch (_) {}
1029
1047
  for (const agent of agentsNeedingPrompt) {
@@ -1034,6 +1052,9 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1034
1052
  for (const agent of agentsNeedingName) {
1035
1053
  if (nameByAgentId[agent.agentId]) agent.agentName = nameByAgentId[agent.agentId];
1036
1054
  }
1055
+ for (const agent of agentsNeedingDesc) {
1056
+ if (descByAgentId[agent.agentId]) agent.description = descByAgentId[agent.agentId];
1057
+ }
1037
1058
  }
1038
1059
 
1039
1060
  const agentsNeedingModel = agents.filter(a => !a.model);
@@ -1199,6 +1220,7 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
1199
1220
  const entry = progressMap[msg.toolUseId];
1200
1221
  if (entry) {
1201
1222
  msg.agentId = entry.agentId;
1223
+ if (entry.description) msg.agentDescription = entry.description;
1202
1224
  if (entry.prompt && !msg.agentPrompt) msg.agentPrompt = entry.prompt;
1203
1225
  try {
1204
1226
  const agentFile = path.join(agentDir, entry.agentId + '.json');
@@ -1244,6 +1266,10 @@ app.get('/api/version', (req, res) => {
1244
1266
  res.json({ version: pkg.version });
1245
1267
  });
1246
1268
 
1269
+ app.get('/api/config', (req, res) => {
1270
+ res.json({ marketplaceUrl: MARKETPLACE_URL });
1271
+ });
1272
+
1247
1273
  // API: Get all tasks across all sessions
1248
1274
  app.get('/api/tasks/all', async (req, res) => {
1249
1275
  try {
@@ -1445,9 +1471,16 @@ watcher.on('all', (event, filePath) => {
1445
1471
  function broadcastToMappedSessions(taskListName, event, filePath) {
1446
1472
  const { listToSessions } = loadAllTaskMaps();
1447
1473
  const map = listToSessions[taskListName];
1448
- if (!map) return;
1449
- for (const sid of Object.keys(map)) {
1450
- broadcast({ type: 'update', event, sessionId: sid, file: path.basename(filePath) });
1474
+ if (map) {
1475
+ for (const sid of Object.keys(map)) {
1476
+ broadcast({ type: 'update', event, sessionId: sid, file: path.basename(filePath) });
1477
+ }
1478
+ return;
1479
+ }
1480
+ // Fallback: check if taskListName is a team name
1481
+ const cfg = loadTeamConfig(taskListName);
1482
+ if (cfg?.leadSessionId) {
1483
+ broadcast({ type: 'update', event, sessionId: cfg.leadSessionId, file: path.basename(filePath) });
1451
1484
  }
1452
1485
  }
1453
1486