claude-code-kanban 4.6.0 → 4.7.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/README.md CHANGED
@@ -34,7 +34,7 @@ All changes are non-destructive — existing settings are preserved.
34
34
  npx claude-code-kanban --open
35
35
  ```
36
36
 
37
- Open http://localhost:3456 (or use `--open` to auto-launch the browser).
37
+ Open http://localhost:3541 (or use `--open` to auto-launch the browser).
38
38
 
39
39
  ### 3. Use Claude Code as usual
40
40
 
@@ -64,7 +64,7 @@ npx claude-code-kanban --open # Auto-open browser
64
64
  npx claude-code-kanban --dir=~/.claude-work # Custom Claude config dir
65
65
  ```
66
66
 
67
- If port 3456 is in use, the server falls back to a random available port.
67
+ If port 3541 is in use, the server falls back to a random available port.
68
68
 
69
69
  ### Global install
70
70
 
package/cli.js CHANGED
@@ -145,7 +145,7 @@ function printTopHelp() {
145
145
  console.log(' --help, -h Show help (top-level, noun-level, or leaf-level)');
146
146
  console.log(' --version, -v Print version and exit');
147
147
  console.log('\nServer mode (no subcommand):');
148
- console.log(' --port <n> Port to listen on (default 3456)');
148
+ console.log(' --port <n> Port to listen on (default 3541)');
149
149
  console.log(' --dir <path> Override Claude config dir (default ~/.claude)');
150
150
  console.log(' --open Open browser on start');
151
151
  console.log(' --install, --uninstall Install or remove the agent-spy hook');
@@ -187,7 +187,7 @@ function getArgValue(args, name) {
187
187
  return args[idx + 1] && !args[idx + 1].startsWith('--') ? args[idx + 1] : null;
188
188
  }
189
189
 
190
- function cliPort() { return process.env.PORT || 3456; }
190
+ function cliPort() { return process.env.PORT || 3541; }
191
191
  function unreachable() { return `Cannot reach cck server on port ${cliPort()}. Start it first with "claude-code-kanban".`; }
192
192
 
193
193
  class CliUnreachable extends Error { constructor() { super(unreachable()); this.code = 'unreachable'; } }
package/lib/parsers.js CHANGED
@@ -442,13 +442,20 @@ function getSystemMessageLabel(text) {
442
442
  const statusMatch = text.match(/<status>([^<]+)<\/status>/);
443
443
  return statusMatch ? `Background task ${statusMatch[1]}` : 'Background task notification';
444
444
  }
445
- if (text.includes('<local-command-stdout>') && text.includes('Compacted')) return 'Compacted';
445
+ // The "Compacted (ctrl+o…)" stdout echo is redundant with the isCompactSummary
446
+ // chip (modern Claude Code stores the summary inline). Skipping it lets the one
447
+ // summary-bearing chip stand alone instead of trailing a bare marker that the
448
+ // resume prompt sorts away from, preventing collapse.
449
+ if (text.includes('<local-command-stdout>') && text.includes('Compacted')) return '__skip__';
446
450
  if (text.includes('<local-command-stdout>')) return 'Command output';
447
451
  if (text.includes('<local-command-caveat>')) return 'System notification';
448
452
  if (text.includes('.output completed') && text.includes('Background command')) return 'Background task completed';
449
453
  if (text.startsWith('This session is being continued from a previous conversation')) return '__skip__';
450
454
  if (text.includes('<command-name>/clear</command-name>')) return '__skip__';
451
- if (text.includes('<command-name>/compact</command-name>')) return 'Compacted';
455
+ // The /compact trigger record is redundant: the boundary stdout + isCompactSummary
456
+ // record already render as one expandable "Compacted" chip. Skip it so the chip
457
+ // collapses cleanly instead of leaving a separate marker where the user typed it.
458
+ if (text.includes('<command-name>/compact</command-name>')) return '__skip__';
452
459
  return null;
453
460
  }
454
461
 
@@ -641,6 +648,26 @@ function readRecentMessages(jsonlPath, limit = 10) {
641
648
  }
642
649
  }
643
650
  } else if (obj.type === 'user' && obj.message?.role === 'user' && !obj.isMeta) {
651
+ if (obj.isCompactSummary) {
652
+ // Surface the compaction summary as a single expandable "Compacted"
653
+ // chip (compactSummary), never a raw bubble. The boundary marker plus
654
+ // the /compact command/stdout records collapse into this one entry below.
655
+ const raw = typeof obj.message.content === 'string'
656
+ ? obj.message.content
657
+ : Array.isArray(obj.message.content)
658
+ ? obj.message.content.filter((b) => b.type === 'text').map((b) => b.text).join('\n')
659
+ : '';
660
+ const summary = raw
661
+ .replace(/^This session is being continued[^\n]*\n+(The summary below[^\n]*\n+)?/i, '')
662
+ .trim();
663
+ messages.push({
664
+ type: 'user',
665
+ systemLabel: 'Compacted',
666
+ compactSummary: summary || null,
667
+ timestamp: obj.timestamp
668
+ });
669
+ continue;
670
+ }
644
671
  if (typeof obj.message.content === 'string') {
645
672
  const t = obj.message.content;
646
673
  const tmMatch = t.match(/<teammate-message\s+([^>]*)>([\s\S]*?)<\/teammate-message>/);
@@ -801,6 +828,12 @@ function readRecentMessages(jsonlPath, limit = 10) {
801
828
  messages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
802
829
  for (let i = messages.length - 1; i > 0; i--) {
803
830
  if (messages[i].systemLabel === 'Compacted' && messages[i - 1].systemLabel === 'Compacted') {
831
+ // Carry the summary body onto the surviving chip so collapsing the
832
+ // boundary + /compact command + stdout + isCompactSummary records into
833
+ // one entry never drops the expandable summary.
834
+ if (messages[i].compactSummary && !messages[i - 1].compactSummary) {
835
+ messages[i - 1].compactSummary = messages[i].compactSummary;
836
+ }
804
837
  messages.splice(i, 1);
805
838
  }
806
839
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "4.6.0",
3
+ "version": "4.7.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": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Agent activity tracking for claude-code-kanban dashboard"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kanban
3
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
- compatibility: Requires the `claude-code-kanban` CLI on PATH and the server running locally (default port 3456).
4
+ compatibility: Requires the `claude-code-kanban` CLI on PATH and the server running locally (default port 3541).
5
5
  ---
6
6
 
7
7
  # Kanban Skill
@@ -69,6 +69,6 @@ Add `--json` to any list-style verb for machine-readable output.
69
69
 
70
70
  ## Troubleshooting
71
71
 
72
- - **"Cannot reach cck server on port 3456"** → ask the user to start it: `claude-code-kanban` (or `npm start` in the cck repo).
72
+ - **"Cannot reach cck server on port 3541"** → ask the user to start it: `claude-code-kanban` (or `npm start` in the cck repo).
73
73
  - **Different port** → set `PORT=<n>` env var when invoking the CLI.
74
74
  - **Ambiguous session prefix (HTTP 409)** → use the full id. With `${CLAUDE_SESSION_ID}` this won't happen.
package/public/app.js CHANGED
@@ -511,6 +511,8 @@ function setActivityFilter(kind) {
511
511
  // waiting is a sub-state of active — couple them so one click covers all running sessions
512
512
  toggleActivityKind('active');
513
513
  toggleActivityKind('waiting');
514
+ // Expand sections that have active sessions so they're visible on click
515
+ if (activityFilter.has('active')) expandActiveGroups();
514
516
  } else {
515
517
  toggleActivityKind(kind);
516
518
  }
@@ -2829,6 +2831,8 @@ function renderSessions() {
2829
2831
 
2830
2832
  // Group active sessions by project
2831
2833
  if (sessionFilter === 'active') {
2834
+ // Auto-expand a collapsed section when newly-active work lands in it (live refresh).
2835
+ expandActiveGroups({ onlyNew: true });
2832
2836
  const groups = new Map();
2833
2837
  const ungrouped = [];
2834
2838
  for (const session of filteredSessions) {
@@ -3212,8 +3216,7 @@ async function onColumnDrop(e) {
3212
3216
 
3213
3217
  //#region KEYBOARD_NAV
3214
3218
  function selectTask(taskId, sessionId) {
3215
- const prev = document.querySelector('.task-card.selected');
3216
- if (prev) prev.classList.remove('selected');
3219
+ clearTaskSelection();
3217
3220
  selectedTaskId = taskId;
3218
3221
  selectedSessionId = sessionId;
3219
3222
  taskHighlightDimmed = false;
@@ -3330,6 +3333,11 @@ function clearKbSelection() {
3330
3333
  if (prev) prev.classList.remove('kb-selected');
3331
3334
  }
3332
3335
 
3336
+ function clearTaskSelection() {
3337
+ const prev = document.querySelector('.task-card.selected');
3338
+ if (prev) prev.classList.remove('selected');
3339
+ }
3340
+
3333
3341
  function selectSessionByIndex(idx, items) {
3334
3342
  items = items || getNavigableItems();
3335
3343
  if (items.length === 0) return;
@@ -3368,11 +3376,37 @@ function setGroupCollapsed(header, collapsed) {
3368
3376
  header.classList.toggle('collapsed', collapsed);
3369
3377
  const container = getGroupSessionsContainer(header);
3370
3378
  if (container) container.classList.toggle('collapsed', collapsed);
3379
+ persistCollapsedGroups();
3380
+ }
3381
+
3382
+ function persistCollapsedGroups() {
3371
3383
  try {
3372
3384
  localStorage.setItem('collapsedGroups', JSON.stringify([...collapsedProjectGroups]));
3373
3385
  } catch (_) {}
3374
3386
  }
3375
3387
 
3388
+ let prevActiveSessionIds = null; // null until primed by the first onlyNew pass
3389
+
3390
+ // Expand collapsed project sections that contain active sessions; leave the rest collapsed.
3391
+ // onlyNew: expand only for sessions that became active since the last call, so a section the
3392
+ // user manually collapsed stays collapsed until NEW active work lands in it (live refresh path).
3393
+ // The first onlyNew pass only primes the tracking set — it respects saved collapse state on load.
3394
+ // onlyNew=false force-expands every section with active work (explicit chip click).
3395
+ function expandActiveGroups({ onlyNew = false } = {}) {
3396
+ const primed = prevActiveSessionIds !== null;
3397
+ const activeIds = new Set();
3398
+ let changed = false;
3399
+ for (const s of sessions) {
3400
+ if (!isSessionActive(s)) continue;
3401
+ activeIds.add(s.id);
3402
+ if (onlyNew && (!primed || prevActiveSessionIds.has(s.id))) continue;
3403
+ if (collapsedProjectGroups.delete(s.project || '__ungrouped__')) changed = true;
3404
+ }
3405
+ prevActiveSessionIds = activeIds;
3406
+ if (changed) persistCollapsedGroups();
3407
+ // renderSessions() re-renders headers/containers from the updated set
3408
+ }
3409
+
3376
3410
  function isGroupHeader(el) {
3377
3411
  return el.classList.contains('project-group-header') || el.classList.contains('pinned-sub-header');
3378
3412
  }
@@ -3434,8 +3468,7 @@ function setFocusZone(zone) {
3434
3468
  // Clear all zone visuals
3435
3469
  sidebar.classList.remove('sidebar-focused');
3436
3470
  clearKbSelection();
3437
- const selCard = document.querySelector('.task-card.selected');
3438
- if (selCard) selCard.classList.remove('selected');
3471
+ clearTaskSelection();
3439
3472
 
3440
3473
  focusZone = zone;
3441
3474
  if (zone === 'sidebar') {
@@ -3761,10 +3794,8 @@ async function addNote(event, taskId, sessionId) {
3761
3794
  function closeDetailPanel() {
3762
3795
  detailPanel.classList.remove('visible');
3763
3796
  document.getElementById('delete-task-btn').style.display = 'none';
3764
- // Keep the task selected for keyboard nav, but drop its white highlight.
3765
- taskHighlightDimmed = true;
3766
- const sel = document.querySelector('.task-card.selected');
3767
- if (sel) sel.classList.remove('selected');
3797
+ // Keep the task selected AND highlighted after closing (no dim-off).
3798
+ taskHighlightDimmed = false;
3768
3799
  }
3769
3800
 
3770
3801
  let deleteTaskId = null;
@@ -4618,7 +4649,10 @@ document.addEventListener('keydown', (e) => {
4618
4649
  return;
4619
4650
  }
4620
4651
  if (e.key === 'Escape') {
4621
- setFocusZone('board');
4652
+ // Plain unfocus — drop the keyboard cursor without jumping into the board
4653
+ document.querySelector('.sidebar').classList.remove('sidebar-focused');
4654
+ clearKbSelection();
4655
+ focusZone = 'board';
4622
4656
  return;
4623
4657
  }
4624
4658
  }
@@ -4669,6 +4703,11 @@ document.addEventListener('keydown', (e) => {
4669
4703
  if (detailPanel.classList.contains('visible')) closeDetailPanel();
4670
4704
  else if (agentLogMode) exitAgentLogMode();
4671
4705
  else if (messagePanelOpen) toggleMessagePanel();
4706
+ else {
4707
+ // Nothing open — plain unfocus: drop the task-card selection highlight
4708
+ clearTaskSelection();
4709
+ selectedTaskId = null;
4710
+ }
4672
4711
  return;
4673
4712
  }
4674
4713
 
package/public/style.css CHANGED
@@ -454,22 +454,15 @@ body::before {
454
454
  padding: 12px 14px;
455
455
  margin-bottom: 4px;
456
456
  background: transparent;
457
- border: 1px solid transparent;
457
+ border: 1px solid var(--border);
458
458
  border-radius: 8px;
459
459
  text-align: left;
460
460
  cursor: pointer;
461
461
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
462
- box-shadow:
463
- 0 1px 3px rgba(0, 0, 0, 0.12),
464
- 0 1px 2px rgba(0, 0, 0, 0.08);
465
- }
466
-
467
- .session-item:hover {
468
- background: var(--bg-hover);
469
462
  }
470
463
 
471
464
  .session-item.active {
472
- background: var(--bg-elevated);
465
+ background: var(--bg-deep);
473
466
  border-color: var(--accent);
474
467
  }
475
468
 
@@ -1173,7 +1166,7 @@ body::before {
1173
1166
  height: 100vh;
1174
1167
  background: var(--bg-surface);
1175
1168
  border-left: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
1176
- box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
1169
+ box-shadow: -1px 0 12px rgba(0, 0, 0, 0.04);
1177
1170
  display: none;
1178
1171
  flex-direction: column;
1179
1172
  z-index: 100;
@@ -2011,7 +2004,7 @@ body::before {
2011
2004
  height: 100vh;
2012
2005
  background: var(--bg-deep);
2013
2006
  border-left: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
2014
- box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
2007
+ box-shadow: -1px 0 12px rgba(0, 0, 0, 0.04);
2015
2008
  display: none;
2016
2009
  flex-direction: column;
2017
2010
  z-index: 99;
@@ -4228,11 +4221,6 @@ pre.mermaid svg {
4228
4221
  width: 0;
4229
4222
  }
4230
4223
 
4231
- .sidebar-focused .session-item.active {
4232
- background: transparent;
4233
- border-color: var(--accent);
4234
- }
4235
-
4236
4224
  .sidebar-focused .session-item.active::before {
4237
4225
  width: 0;
4238
4226
  }
package/server.js CHANGED
@@ -38,7 +38,7 @@ if (require("./cli").runCli(process.argv)) return;
38
38
 
39
39
 
40
40
  const app = express();
41
- const PORT = process.env.PORT || 3456;
41
+ const PORT = process.env.PORT || 3541;
42
42
 
43
43
  // Parse --dir flag for custom Claude directory
44
44
  function getClaudeDir() {