claude-code-kanban 3.5.1 → 3.5.3
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/cli.js
CHANGED
|
@@ -42,6 +42,16 @@ const COMMANDS = {
|
|
|
42
42
|
},
|
|
43
43
|
run: runSessionViewCli,
|
|
44
44
|
},
|
|
45
|
+
pin: {
|
|
46
|
+
summary: 'Pin (or unpin) a session in the sidebar of connected browser tabs',
|
|
47
|
+
usage: 'claude-code-kanban session pin <id> [--sticky] [--unpin]',
|
|
48
|
+
flags: {
|
|
49
|
+
'<id>': 'Full session id, or a unique prefix',
|
|
50
|
+
'--sticky': 'Set sticky state (always shown, top of list)',
|
|
51
|
+
'--unpin': 'Clear pin/sticky state',
|
|
52
|
+
},
|
|
53
|
+
run: runSessionPinCli,
|
|
54
|
+
},
|
|
45
55
|
peek: {
|
|
46
56
|
summary: 'Show the last N messages from a session',
|
|
47
57
|
usage: 'claude-code-kanban session peek <id> [--limit <n>] [--json]',
|
|
@@ -361,6 +371,31 @@ async function runSessionOpenCli(args) {
|
|
|
361
371
|
} catch (e) { reportCliError(e); return 1; }
|
|
362
372
|
}
|
|
363
373
|
|
|
374
|
+
async function runSessionPinCli(args) {
|
|
375
|
+
const idArg = args.find(a => !a.startsWith('--'));
|
|
376
|
+
if (!idArg) {
|
|
377
|
+
printLeafHelp('session pin', COMMANDS.session.verbs.pin);
|
|
378
|
+
return 1;
|
|
379
|
+
}
|
|
380
|
+
const state = args.includes('--unpin') ? 'none' : args.includes('--sticky') ? 'sticky' : 'pinned';
|
|
381
|
+
const resolved = await resolveSessionByIdOrPrefix(idArg);
|
|
382
|
+
if (!resolved) return 1;
|
|
383
|
+
try {
|
|
384
|
+
const res = await cliFetch('/api/session/pin', {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: { 'Content-Type': 'application/json' },
|
|
387
|
+
body: JSON.stringify({ id: resolved.id, state })
|
|
388
|
+
});
|
|
389
|
+
if (!res.ok) {
|
|
390
|
+
console.error(`Pin failed (${res.status}): ${await res.text()}`);
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
const label = state === 'none' ? 'unpinned' : state;
|
|
394
|
+
console.log(`Session ${label}: ${resolved.id}${resolved.customTitle ? ` (${resolved.customTitle})` : ''}`);
|
|
395
|
+
return 0;
|
|
396
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
397
|
+
}
|
|
398
|
+
|
|
364
399
|
async function runSessionViewCli(args) {
|
|
365
400
|
const idArg = args.find(a => !a.startsWith('--'));
|
|
366
401
|
if (!idArg) {
|
package/lib/parsers.js
CHANGED
|
@@ -182,10 +182,22 @@ function scrapeScalarFromBlob(blob, re) {
|
|
|
182
182
|
try { return JSON.parse(`"${m[1]}"`); } catch (e) { return null; }
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
const sessionInfoCache = new Map();
|
|
186
|
+
const SESSION_INFO_CACHE_MAX = 2000;
|
|
187
|
+
|
|
185
188
|
function readSessionInfoFromJsonl(jsonlPath) {
|
|
186
189
|
const result = { slug: null, projectPath: null, cwd: null, gitBranch: null, customTitle: null };
|
|
187
190
|
let stat;
|
|
188
191
|
let fd;
|
|
192
|
+
try {
|
|
193
|
+
stat = statSync(jsonlPath);
|
|
194
|
+
const cached = sessionInfoCache.get(jsonlPath);
|
|
195
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
196
|
+
return { ...cached.result, customTitle: readCustomTitle(jsonlPath, stat) };
|
|
197
|
+
}
|
|
198
|
+
} catch (_) {
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
189
201
|
// State shared across head-chunk parse, leftover flush, and tail parse.
|
|
190
202
|
let lastCwdFromHead = null;
|
|
191
203
|
const applyLine = (line) => {
|
|
@@ -200,7 +212,6 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
200
212
|
} catch (e) {}
|
|
201
213
|
};
|
|
202
214
|
try {
|
|
203
|
-
stat = statSync(jsonlPath);
|
|
204
215
|
fd = fs.openSync(jsonlPath, 'r');
|
|
205
216
|
const CHUNK_SIZE = 16384;
|
|
206
217
|
const TAIL_SIZE = 16384;
|
|
@@ -264,6 +275,14 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
264
275
|
if (fd !== undefined) { try { fs.closeSync(fd); } catch (e) {} }
|
|
265
276
|
}
|
|
266
277
|
|
|
278
|
+
if (stat) {
|
|
279
|
+
const { customTitle: _ct, ...rest } = result;
|
|
280
|
+
sessionInfoCache.set(jsonlPath, { mtimeMs: stat.mtimeMs, size: stat.size, result: rest });
|
|
281
|
+
if (sessionInfoCache.size > SESSION_INFO_CACHE_MAX) {
|
|
282
|
+
const firstKey = sessionInfoCache.keys().next().value;
|
|
283
|
+
sessionInfoCache.delete(firstKey);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
267
286
|
result.customTitle = readCustomTitle(jsonlPath, stat);
|
|
268
287
|
return result;
|
|
269
288
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kanban
|
|
3
|
-
description: Drive the claude-code-kanban browser dashboard from this Claude session. Use this skill when the user mentions "kanban" together with "session" — e.g. "open this session in kanban", "show kanban", "focus current session in kanban", "preview this file in kanban", or asks to peek/view a kanban session.
|
|
3
|
+
description: Drive the claude-code-kanban browser dashboard from this Claude session. Use this skill when the user mentions "kanban" together with "session" — e.g. "open this session in kanban", "show kanban", "focus current session in kanban", "pin/unpin a session in kanban", "preview this file in kanban", or asks to peek/view a kanban session.
|
|
4
4
|
compatibility: Requires the `claude-code-kanban` CLI on PATH and the server running locally (default port 3456).
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -22,6 +22,20 @@ claude-code-kanban session open ${CLAUDE_SESSION_ID}
|
|
|
22
22
|
|
|
23
23
|
Trigger phrases: "show this session in kanban", "focus current session", "open in kanban".
|
|
24
24
|
|
|
25
|
+
## Pin the current session in kanban
|
|
26
|
+
|
|
27
|
+
Pins the active Claude session in the sidebar so it stays visible regardless of filters. Three states: `pinned` (default), `sticky` (always at the top), or cleared via `--unpin`.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude-code-kanban session pin ${CLAUDE_SESSION_ID} # pin
|
|
31
|
+
claude-code-kanban session pin ${CLAUDE_SESSION_ID} --sticky # sticky at top
|
|
32
|
+
claude-code-kanban session pin ${CLAUDE_SESSION_ID} --unpin # clear
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
State applies to every connected browser tab (broadcast via SSE) and persists in each tab's localStorage. With no tabs open the command is a no-op.
|
|
36
|
+
|
|
37
|
+
Trigger phrases: "pin this session", "pin in kanban", "make this session sticky", "unpin session".
|
|
38
|
+
|
|
25
39
|
## Preview a file in kanban
|
|
26
40
|
|
|
27
41
|
Opens a markdown file in the preview modal:
|
package/public/app.js
CHANGED
|
@@ -1335,6 +1335,19 @@ function toggleSessionSticky(sessionId) {
|
|
|
1335
1335
|
renderSessions();
|
|
1336
1336
|
}
|
|
1337
1337
|
|
|
1338
|
+
function handleSessionPinEvent({ id, state }) {
|
|
1339
|
+
if (!id) return;
|
|
1340
|
+
pinnedSessionIds.delete(id);
|
|
1341
|
+
stickySessionIds.delete(id);
|
|
1342
|
+
if (state === 'pinned') pinnedSessionIds.add(id);
|
|
1343
|
+
if (state === 'sticky') {
|
|
1344
|
+
pinnedSessionIds.add(id);
|
|
1345
|
+
stickySessionIds.add(id);
|
|
1346
|
+
}
|
|
1347
|
+
savePinnedSessions();
|
|
1348
|
+
renderSessions();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1338
1351
|
function getSessionPinState(sessionId) {
|
|
1339
1352
|
if (stickySessionIds.has(sessionId)) return 'sticky';
|
|
1340
1353
|
if (pinnedSessionIds.has(sessionId)) return 'pinned';
|
|
@@ -4153,6 +4166,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
4153
4166
|
lastTasksHash = '';
|
|
4154
4167
|
const refreshes = [fetchSessions()];
|
|
4155
4168
|
if (currentSessionId) refreshes.push(fetchTasks(currentSessionId));
|
|
4169
|
+
refreshRateLimits();
|
|
4156
4170
|
Promise.all(refreshes)
|
|
4157
4171
|
.then(() => showToast('Data refreshed', 'success'))
|
|
4158
4172
|
.finally(() => {
|
|
@@ -4515,6 +4529,10 @@ function setupEventSource() {
|
|
|
4515
4529
|
handleSessionOpenEvent(data);
|
|
4516
4530
|
}
|
|
4517
4531
|
|
|
4532
|
+
if (data.type === 'session:pin') {
|
|
4533
|
+
handleSessionPinEvent(data);
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4518
4536
|
if (data.type === 'team-update') {
|
|
4519
4537
|
const teamSession = sessions.find((s) => s.isTeam && s.teamName === data.teamName);
|
|
4520
4538
|
if (teamSession) {
|
|
@@ -5725,7 +5743,11 @@ function refreshRateLimits() {
|
|
|
5725
5743
|
fetch('/api/context-status')
|
|
5726
5744
|
.then((r) => r.json())
|
|
5727
5745
|
.then((all) => {
|
|
5728
|
-
|
|
5746
|
+
let freshest = null;
|
|
5747
|
+
for (const e of Object.values(all || {})) {
|
|
5748
|
+
if (e?.rate_limits && (!freshest || (e._updatedAt || 0) > (freshest._updatedAt || 0))) freshest = e;
|
|
5749
|
+
}
|
|
5750
|
+
const rl = freshest?.rate_limits || null;
|
|
5729
5751
|
const fh = rl?.five_hour?.used_percentage ?? null;
|
|
5730
5752
|
const sd = rl?.seven_day?.used_percentage ?? null;
|
|
5731
5753
|
const key = `${fh}|${sd}`;
|
package/server.js
CHANGED
|
@@ -1531,6 +1531,21 @@ app.post('/api/session/open', async (req, res) => {
|
|
|
1531
1531
|
}
|
|
1532
1532
|
});
|
|
1533
1533
|
|
|
1534
|
+
app.post('/api/session/pin', async (req, res) => {
|
|
1535
|
+
try {
|
|
1536
|
+
const { id, state } = req.body || {};
|
|
1537
|
+
if (!id || typeof id !== 'string') return res.status(400).json({ error: 'id is required' });
|
|
1538
|
+
if (!['none', 'pinned', 'sticky'].includes(state)) {
|
|
1539
|
+
return res.status(400).json({ error: 'state must be none|pinned|sticky' });
|
|
1540
|
+
}
|
|
1541
|
+
broadcast({ type: 'session:pin', id, state });
|
|
1542
|
+
res.json({ success: true, id, state });
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
console.error('Error in /api/session/pin:', error);
|
|
1545
|
+
res.status(500).json({ error: error.message || 'Failed' });
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1534
1549
|
app.get('/api/preview', async (req, res) => {
|
|
1535
1550
|
try {
|
|
1536
1551
|
const abs = resolvePreviewPath(req.query.path);
|
|
@@ -1746,6 +1761,7 @@ contextStatusWatcher.on('all', (event, filePath) => {
|
|
|
1746
1761
|
if (event === 'add' || event === 'change') {
|
|
1747
1762
|
try {
|
|
1748
1763
|
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1764
|
+
try { data._updatedAt = statSync(filePath).mtimeMs; } catch (_) { data._updatedAt = Date.now(); }
|
|
1749
1765
|
contextStatusCache.set(sessionId, data);
|
|
1750
1766
|
evictStaleCache(contextStatusCache);
|
|
1751
1767
|
} catch (e) { /* ignore malformed */ }
|