claude-code-kanban 3.0.0 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "3.0.0",
3
+ "version": "3.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": {
@@ -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) {
@@ -4043,7 +4049,7 @@ function setupEventSource() {
4043
4049
  if (isMetadata) {
4044
4050
  clearTimeout(metadataRefreshTimer);
4045
4051
  metadataRefreshTimer = setTimeout(async () => {
4046
- fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
4052
+ fetchSessions(false).catch((err) => console.error('[SSE] fetchSessions failed:', err));
4047
4053
  if (currentSessionId) {
4048
4054
  await fetchAgents(currentSessionId);
4049
4055
  if (!agentLogMode) fetchMessages(currentSessionId);
@@ -4084,7 +4090,7 @@ function setupEventSource() {
4084
4090
  pendingAgentSessionIds.add(data.sessionId);
4085
4091
  clearTimeout(agentRefreshTimer);
4086
4092
  agentRefreshTimer = setTimeout(() => {
4087
- fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
4093
+ fetchSessions(false).catch((err) => console.error('[SSE] fetchSessions failed:', err));
4088
4094
  if (viewMode === 'project' && currentProjectSessionIds.some((id) => pendingAgentSessionIds.has(id))) {
4089
4095
  refreshProjectAgents();
4090
4096
  } else if (currentSessionId && pendingAgentSessionIds.has(currentSessionId)) {
@@ -4109,8 +4115,22 @@ function setupEventSource() {
4109
4115
  };
4110
4116
  }
4111
4117
 
4112
- // Fallback poll every 30s in case SSE silently drops
4118
+ // When the tab becomes visible after being hidden, catch up immediately
4119
+ let _pollMissed = false;
4120
+ document.addEventListener('visibilitychange', () => {
4121
+ if (!document.hidden && _pollMissed) {
4122
+ _pollMissed = false;
4123
+ fetchSessions().catch(() => {});
4124
+ if (currentSessionId) fetchTasks(currentSessionId).catch(() => {});
4125
+ }
4126
+ });
4127
+
4128
+ // Fallback poll every 30s in case SSE silently drops; skip when tab is hidden
4113
4129
  setInterval(() => {
4130
+ if (document.hidden) {
4131
+ _pollMissed = true;
4132
+ return;
4133
+ }
4114
4134
  fetchSessions().catch(() => {});
4115
4135
  }, 30000);
4116
4136
 
@@ -4941,6 +4961,8 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
4941
4961
  _infoModalSessionId = session.id;
4942
4962
  updateStickyBtnState();
4943
4963
  updateDismissBtnState();
4964
+ const costBtn = document.getElementById('session-info-cost-btn');
4965
+ if (costBtn) costBtn.style.display = window.__HUB__?.enabled || appConfig.costUrl ? '' : 'none';
4944
4966
  modal.classList.add('visible');
4945
4967
 
4946
4968
  const keyHandler = (e) => {
@@ -5043,6 +5065,15 @@ function openFolderInEditor(folder, file) {
5043
5065
  postAndToast('/api/open-folder', body, 'folder');
5044
5066
  }
5045
5067
 
5068
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML
5069
+ function openCost(sessionId) {
5070
+ if (window.__HUB__?.enabled) {
5071
+ hubNavigate('cost', `?view=detail&session=${encodeURIComponent(sessionId)}`);
5072
+ } else if (appConfig.costUrl) {
5073
+ window.open(`${appConfig.costUrl}?view=detail&session=${encodeURIComponent(sessionId)}`, '_blank');
5074
+ }
5075
+ }
5076
+
5046
5077
  // biome-ignore lint/correctness/noUnusedVariables: used in HTML
5047
5078
  function openMarketplace(projectPath) {
5048
5079
  const params = new URLSearchParams({ project: projectPath });
@@ -5233,33 +5264,42 @@ if (urlState.search) {
5233
5264
  document.getElementById('search-clear-btn').classList.add('visible');
5234
5265
  }
5235
5266
 
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 (_) {
5267
+ Promise.all([
5268
+ fetch('/hub-config')
5269
+ .then((r) => r.json())
5270
+ .then((cfg) => {
5271
+ if (!cfg.enabled) return;
5272
+ window.__HUB__ = cfg;
5273
+ })
5274
+ .catch(() => {}),
5275
+ fetch('/api/config')
5276
+ .then((r) => r.json())
5277
+ .then((c) => {
5278
+ appConfig = c;
5279
+ })
5280
+ .catch(() => {}),
5281
+ ])
5282
+ .then(() => fetchSessions())
5283
+ .then(async () => {
5284
+ if (urlState.projectView) {
5285
+ try {
5286
+ await fetchProjectView(atob(urlState.projectView));
5287
+ } catch (_) {
5288
+ showAllTasks();
5289
+ }
5290
+ } else if (urlState.session) {
5291
+ await fetchTasks(urlState.session);
5292
+ } else {
5248
5293
  showAllTasks();
5249
5294
  }
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
- });
5295
+ if (urlState.messages && currentSessionId) {
5296
+ toggleMessagePanel();
5297
+ // Re-render after panel layout settles so scroll dimensions are correct
5298
+ requestAnimationFrame(() => {
5299
+ if (currentMessages.length) renderMessages(currentMessages);
5300
+ });
5301
+ }
5302
+ });
5263
5303
 
5264
5304
  window.addEventListener('popstate', () => {
5265
5305
  const s = getUrlState();
@@ -5282,23 +5322,17 @@ window.addEventListener('popstate', () => {
5282
5322
  //#endregion
5283
5323
 
5284
5324
  // #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
- })();
5325
+ document.addEventListener('keydown', (e) => {
5326
+ if (!window.__HUB__?.enabled) return;
5327
+ if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
5328
+ e.preventDefault();
5329
+ window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
5330
+ }
5331
+ if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
5332
+ e.preventDefault();
5333
+ window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
5334
+ }
5335
+ });
5302
5336
 
5303
5337
  window.hubNavigate = function hubNavigate(app, url) {
5304
5338
  if (!window.__HUB__?.enabled) return;
package/public/index.html CHANGED
@@ -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>
@@ -487,6 +487,9 @@
487
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
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
489
  </button>
490
+ <button id="session-info-cost-btn" class="icon-btn" style="display:none" title="Open in Cost" onclick="openCost(_infoModalSessionId)">
491
+ <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>
492
+ </button>
490
493
  <button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
491
494
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
492
495
  <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