claude-code-kanban 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
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": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/NikiforovAll/claude-task-viewer.git"
19
+ "url": "git+https://github.com/NikiforovAll/claude-code-kanban.git"
20
20
  },
21
21
  "keywords": [
22
22
  "claude",
@@ -31,16 +31,16 @@
31
31
  "author": "NikiforovAll",
32
32
  "license": "MIT",
33
33
  "bugs": {
34
- "url": "https://github.com/NikiforovAll/claude-task-viewer/issues"
34
+ "url": "https://github.com/NikiforovAll/claude-code-kanban/issues"
35
35
  },
36
- "homepage": "https://github.com/NikiforovAll/claude-task-viewer#readme",
36
+ "homepage": "https://github.com/NikiforovAll/claude-code-kanban#readme",
37
37
  "dependencies": {
38
38
  "chokidar": "^3.5.3",
39
39
  "express": "^4.18.2",
40
- "open": "^10.0.0"
40
+ "open": "^11.0.0"
41
41
  },
42
42
  "engines": {
43
- "node": ">=18.0.0"
43
+ "node": ">=20.0.0"
44
44
  },
45
45
  "files": [
46
46
  "server.js",
package/public/app.js CHANGED
@@ -25,7 +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
+ let appConfig = { marketplaceUrl: null, costUrl: null };
29
29
  let selectedSessionIdx = -1;
30
30
  let selectedSessionKbId = null;
31
31
  let sessionJustSelected = false;
@@ -129,27 +129,33 @@ let lastTasksHash = '';
129
129
  //#endregion
130
130
 
131
131
  //#region DATA_FETCHING
132
- async function fetchSessions() {
132
+ async function fetchSessions(includeTasks = true) {
133
133
  try {
134
134
  const allPinnedIds = new Set([...pinnedSessionIds, ...stickySessionIds]);
135
135
  if (revealedPlanSessionId) allPinnedIds.add(revealedPlanSessionId);
136
136
  if (revealedStorageSessionId) allPinnedIds.add(revealedStorageSessionId);
137
137
  const pinnedParam = allPinnedIds.size > 0 ? `&pinned=${[...allPinnedIds].join(',')}` : '';
138
- const [newSessions, newTasks] = await Promise.all([
139
- fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}`).then((r) => r.json()),
140
- fetch('/api/tasks/all').then((r) => r.json()),
141
- ]);
138
+ const sessionsPromise = fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}`).then((r) => r.json());
139
+
140
+ let newSessions, newTasks;
141
+ if (includeTasks) {
142
+ [newSessions, newTasks] = await Promise.all([sessionsPromise, fetch('/api/tasks/all').then((r) => r.json())]);
143
+ } else {
144
+ newSessions = await sessionsPromise;
145
+ }
142
146
 
143
147
  const sessionsHash = JSON.stringify(newSessions);
144
- const tasksHash = JSON.stringify(newTasks);
145
- if (sessionsHash === lastSessionsHash && tasksHash === lastTasksHash) {
146
- return;
148
+ if (includeTasks) {
149
+ const tasksHash = JSON.stringify(newTasks);
150
+ if (sessionsHash === lastSessionsHash && tasksHash === lastTasksHash) return;
151
+ lastTasksHash = tasksHash;
152
+ allTasksCache = newTasks;
153
+ } else {
154
+ if (sessionsHash === lastSessionsHash) return;
147
155
  }
148
156
  lastSessionsHash = sessionsHash;
149
- lastTasksHash = tasksHash;
150
157
 
151
158
  sessions = newSessions;
152
- allTasksCache = newTasks;
153
159
  renderSessions();
154
160
  renderLiveUpdatesFromCache();
155
161
  } catch (error) {
@@ -2792,9 +2798,9 @@ function navigateSession(direction, items) {
2792
2798
  const restoredIdx = selectedSessionKbId ? items.findIndex((el) => getKbId(el) === selectedSessionKbId) : -1;
2793
2799
  newIdx = restoredIdx >= 0 ? restoredIdx : 0;
2794
2800
  }
2795
- if (newIdx >= 0 && newIdx < items.length) {
2796
- selectSessionByIndex(newIdx, items);
2797
- }
2801
+ if (newIdx < 0) newIdx = items.length - 1;
2802
+ else if (newIdx >= items.length) newIdx = 0;
2803
+ selectSessionByIndex(newIdx, items);
2798
2804
  }
2799
2805
 
2800
2806
  function setGroupCollapsed(header, collapsed) {
@@ -3975,6 +3981,22 @@ document.addEventListener('keydown', (e) => {
3975
3981
  toggleScratchpad();
3976
3982
  return;
3977
3983
  }
3984
+ if (e.key === '$' && !e.ctrlKey && !e.altKey && !e.metaKey) {
3985
+ e.preventDefault();
3986
+ hubNavigate('cost', contextSid ? `?view=detail&session=${encodeURIComponent(contextSid)}` : undefined);
3987
+ return;
3988
+ }
3989
+ if (matchKey(e, 'KeyM')) {
3990
+ e.preventDefault();
3991
+ const mSession = contextSid ? sessions.find((s) => s.id === contextSid) : null;
3992
+ hubNavigate('marketplace', mSession?.project ? `?project=${encodeURIComponent(mSession.project)}` : undefined);
3993
+ return;
3994
+ }
3995
+ if (matchKey(e, 'KeyT')) {
3996
+ e.preventDefault();
3997
+ toggleTheme();
3998
+ return;
3999
+ }
3978
4000
  if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
3979
4001
  e.preventDefault();
3980
4002
  showHelpModal();
@@ -4043,7 +4065,7 @@ function setupEventSource() {
4043
4065
  if (isMetadata) {
4044
4066
  clearTimeout(metadataRefreshTimer);
4045
4067
  metadataRefreshTimer = setTimeout(async () => {
4046
- fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
4068
+ fetchSessions(false).catch((err) => console.error('[SSE] fetchSessions failed:', err));
4047
4069
  if (currentSessionId) {
4048
4070
  await fetchAgents(currentSessionId);
4049
4071
  if (!agentLogMode) fetchMessages(currentSessionId);
@@ -4084,7 +4106,7 @@ function setupEventSource() {
4084
4106
  pendingAgentSessionIds.add(data.sessionId);
4085
4107
  clearTimeout(agentRefreshTimer);
4086
4108
  agentRefreshTimer = setTimeout(() => {
4087
- fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
4109
+ fetchSessions(false).catch((err) => console.error('[SSE] fetchSessions failed:', err));
4088
4110
  if (viewMode === 'project' && currentProjectSessionIds.some((id) => pendingAgentSessionIds.has(id))) {
4089
4111
  refreshProjectAgents();
4090
4112
  } else if (currentSessionId && pendingAgentSessionIds.has(currentSessionId)) {
@@ -4109,8 +4131,22 @@ function setupEventSource() {
4109
4131
  };
4110
4132
  }
4111
4133
 
4112
- // Fallback poll every 30s in case SSE silently drops
4134
+ // When the tab becomes visible after being hidden, catch up immediately
4135
+ let _pollMissed = false;
4136
+ document.addEventListener('visibilitychange', () => {
4137
+ if (!document.hidden && _pollMissed) {
4138
+ _pollMissed = false;
4139
+ fetchSessions().catch(() => {});
4140
+ if (currentSessionId) fetchTasks(currentSessionId).catch(() => {});
4141
+ }
4142
+ });
4143
+
4144
+ // Fallback poll every 30s in case SSE silently drops; skip when tab is hidden
4113
4145
  setInterval(() => {
4146
+ if (document.hidden) {
4147
+ _pollMissed = true;
4148
+ return;
4149
+ }
4114
4150
  fetchSessions().catch(() => {});
4115
4151
  }, 30000);
4116
4152
 
@@ -4581,7 +4617,6 @@ function updateThemeColor(isLight) {
4581
4617
  //#endregion
4582
4618
 
4583
4619
  //#region THEME
4584
- // biome-ignore lint/correctness/noUnusedVariables: used in HTML
4585
4620
  function toggleTheme() {
4586
4621
  const isCurrentlyLight = document.body.classList.contains('light');
4587
4622
  if (isCurrentlyLight) {
@@ -4941,6 +4976,8 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
4941
4976
  _infoModalSessionId = session.id;
4942
4977
  updateStickyBtnState();
4943
4978
  updateDismissBtnState();
4979
+ const costBtn = document.getElementById('session-info-cost-btn');
4980
+ if (costBtn) costBtn.style.display = window.__HUB__?.enabled || appConfig.costUrl ? '' : 'none';
4944
4981
  modal.classList.add('visible');
4945
4982
 
4946
4983
  const keyHandler = (e) => {
@@ -5043,6 +5080,15 @@ function openFolderInEditor(folder, file) {
5043
5080
  postAndToast('/api/open-folder', body, 'folder');
5044
5081
  }
5045
5082
 
5083
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
5084
+ function openCost(sessionId) {
5085
+ if (window.__HUB__?.enabled) {
5086
+ hubNavigate('cost', `?view=detail&session=${encodeURIComponent(sessionId)}`);
5087
+ } else if (appConfig.costUrl) {
5088
+ window.open(`${appConfig.costUrl}?view=detail&session=${encodeURIComponent(sessionId)}`, '_blank');
5089
+ }
5090
+ }
5091
+
5046
5092
  // biome-ignore lint/correctness/noUnusedVariables: used in HTML
5047
5093
  function openMarketplace(projectPath) {
5048
5094
  const params = new URLSearchParams({ project: projectPath });
@@ -5233,33 +5279,42 @@ if (urlState.search) {
5233
5279
  document.getElementById('search-clear-btn').classList.add('visible');
5234
5280
  }
5235
5281
 
5236
- fetch('/api/config')
5237
- .then((r) => r.json())
5238
- .then((c) => {
5239
- appConfig = c;
5240
- })
5241
- .catch(() => {});
5242
-
5243
- fetchSessions().then(async () => {
5244
- if (urlState.projectView) {
5245
- try {
5246
- await fetchProjectView(atob(urlState.projectView));
5247
- } catch (_) {
5282
+ Promise.all([
5283
+ fetch('/hub-config')
5284
+ .then((r) => r.json())
5285
+ .then((cfg) => {
5286
+ if (!cfg.enabled) return;
5287
+ window.__HUB__ = cfg;
5288
+ })
5289
+ .catch(() => {}),
5290
+ fetch('/api/config')
5291
+ .then((r) => r.json())
5292
+ .then((c) => {
5293
+ appConfig = c;
5294
+ })
5295
+ .catch(() => {}),
5296
+ ])
5297
+ .then(() => fetchSessions())
5298
+ .then(async () => {
5299
+ if (urlState.projectView) {
5300
+ try {
5301
+ await fetchProjectView(atob(urlState.projectView));
5302
+ } catch (_) {
5303
+ showAllTasks();
5304
+ }
5305
+ } else if (urlState.session) {
5306
+ await fetchTasks(urlState.session);
5307
+ } else {
5248
5308
  showAllTasks();
5249
5309
  }
5250
- } else if (urlState.session) {
5251
- await fetchTasks(urlState.session);
5252
- } else {
5253
- showAllTasks();
5254
- }
5255
- if (urlState.messages && currentSessionId) {
5256
- toggleMessagePanel();
5257
- // Re-render after panel layout settles so scroll dimensions are correct
5258
- requestAnimationFrame(() => {
5259
- if (currentMessages.length) renderMessages(currentMessages);
5260
- });
5261
- }
5262
- });
5310
+ if (urlState.messages && currentSessionId) {
5311
+ toggleMessagePanel();
5312
+ // Re-render after panel layout settles so scroll dimensions are correct
5313
+ requestAnimationFrame(() => {
5314
+ if (currentMessages.length) renderMessages(currentMessages);
5315
+ });
5316
+ }
5317
+ });
5263
5318
 
5264
5319
  window.addEventListener('popstate', () => {
5265
5320
  const s = getUrlState();
@@ -5282,23 +5337,17 @@ window.addEventListener('popstate', () => {
5282
5337
  //#endregion
5283
5338
 
5284
5339
  // #region HUB_INTEGRATION
5285
- (async function initHub() {
5286
- const cfg = await fetch('/hub-config')
5287
- .then((r) => r.json())
5288
- .catch(() => ({}));
5289
- if (!cfg.enabled) return;
5290
- window.__HUB__ = cfg;
5291
- document.addEventListener('keydown', (e) => {
5292
- if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
5293
- e.preventDefault();
5294
- window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
5295
- }
5296
- if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
5297
- e.preventDefault();
5298
- window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
5299
- }
5300
- });
5301
- })();
5340
+ document.addEventListener('keydown', (e) => {
5341
+ if (!window.__HUB__?.enabled) return;
5342
+ if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
5343
+ e.preventDefault();
5344
+ window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
5345
+ }
5346
+ if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
5347
+ e.preventDefault();
5348
+ window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
5349
+ }
5350
+ });
5302
5351
 
5303
5352
  window.hubNavigate = function hubNavigate(app, url) {
5304
5353
  if (!window.__HUB__?.enabled) return;
package/public/index.html CHANGED
@@ -46,7 +46,7 @@
46
46
  <header class="sidebar-header">
47
47
  <div class="logo">
48
48
  <div class="logo-mark">
49
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
49
+ <svg viewBox="4 6 16 12" fill="none" stroke="currentColor" stroke-width="2.5">
50
50
  <path d="M5 13l4 4L19 7"/>
51
51
  </svg>
52
52
  </div>
@@ -183,7 +183,7 @@
183
183
  <circle cx="12" cy="17" r="0.5" fill="currentColor"/>
184
184
  </svg>
185
185
  </button>
186
- <a href="https://github.com/NikiforovAll/claude-task-viewer" target="_blank" class="icon-btn" title="View on GitHub" aria-label="View on GitHub">
186
+ <a href="https://github.com/NikiforovAll/claude-code-kanban" target="_blank" class="icon-btn" title="View on GitHub" aria-label="View on GitHub">
187
187
  <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
188
188
  <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
189
189
  </svg>
@@ -396,6 +396,18 @@
396
396
  <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">N</kbd></td>
397
397
  <td style="padding: 4px 0; color: var(--text-primary);">Toggle scratchpad</td>
398
398
  </tr>
399
+ <tr>
400
+ <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">T</kbd></td>
401
+ <td style="padding: 4px 0; color: var(--text-primary);">Toggle theme</td>
402
+ </tr>
403
+ <tr>
404
+ <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">$</kbd></td>
405
+ <td style="padding: 4px 0; color: var(--text-primary);">Jump to cost</td>
406
+ </tr>
407
+ <tr>
408
+ <td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">M</kbd></td>
409
+ <td style="padding: 4px 0; color: var(--text-primary);">Jump to marketplace</td>
410
+ </tr>
399
411
  <tr>
400
412
  <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+S</kbd></td>
401
413
  <td style="padding: 4px 0; color: var(--text-primary);">Storage manager</td>
@@ -487,6 +499,9 @@
487
499
  <button id="session-info-sticky-btn" class="icon-btn" style="display:none" title="Sticky pin — always show at top" onclick="toggleSessionSticky(_infoModalSessionId); updateStickyBtnState()">
488
500
  <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
501
  </button>
502
+ <button id="session-info-cost-btn" class="icon-btn" style="display:none" title="Open in Cost" onclick="openCost(_infoModalSessionId)">
503
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
504
+ </button>
490
505
  <button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
491
506
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
492
507
  <path d="M18 6L6 18M6 6l12 12"/>
package/server.js CHANGED
@@ -48,17 +48,18 @@ 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'));
51
+ function getArgUrl(argName, envName) {
52
+ const idx = process.argv.findIndex(arg => arg.startsWith(`--${argName}`));
53
53
  if (idx !== -1) {
54
54
  const arg = process.argv[idx];
55
55
  if (arg.includes('=')) return arg.split('=').slice(1).join('=');
56
56
  if (process.argv[idx + 1]) return process.argv[idx + 1];
57
57
  }
58
- return process.env.MARKETPLACE_URL || null;
58
+ return process.env[envName] || null;
59
59
  }
60
60
 
61
- const MARKETPLACE_URL = getMarketplaceUrl();
61
+ const MARKETPLACE_URL = getArgUrl('marketplace-url', 'MARKETPLACE_URL');
62
+ const COST_URL = getArgUrl('cost-url', 'COST_URL');
62
63
  const CLAUDE_DIR = getClaudeDir();
63
64
  const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
64
65
  const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
@@ -1276,7 +1277,7 @@ app.get('/api/version', (req, res) => {
1276
1277
  });
1277
1278
 
1278
1279
  app.get('/api/config', (req, res) => {
1279
- res.json({ marketplaceUrl: MARKETPLACE_URL });
1280
+ res.json({ marketplaceUrl: MARKETPLACE_URL, costUrl: COST_URL });
1280
1281
  });
1281
1282
 
1282
1283
  // API: Get all tasks across all sessions