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 +6 -6
- package/public/app.js +90 -56
- package/public/index.html +4 -1
- package/server.js +6 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "3.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-
|
|
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-
|
|
34
|
+
"url": "https://github.com/NikiforovAll/claude-code-kanban/issues"
|
|
35
35
|
},
|
|
36
|
-
"homepage": "https://github.com/NikiforovAll/claude-
|
|
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": "^
|
|
40
|
+
"open": "^11.0.0"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
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
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
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
|
-
(
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
.
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
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-
|
|
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
|
|
52
|
-
const idx = process.argv.findIndex(arg => arg.startsWith(
|
|
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
|
|
58
|
+
return process.env[envName] || null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const MARKETPLACE_URL =
|
|
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
|