claude-code-kanban 7.1.1 → 7.2.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/public/index.html CHANGED
@@ -293,6 +293,9 @@
293
293
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
294
294
  </button>
295
295
  <div class="pane-tools">
296
+ <button id="pane-split-btn" class="icon-btn pane-split-btn" title="Split with the tab before it (|)" aria-label="Split panes" aria-pressed="false">
297
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="12" y1="4" x2="12" y2="20"/></svg>
298
+ </button>
296
299
  <button id="pane-reload" class="icon-btn pane-tool" title="Reload pane" aria-label="Reload pane">
297
300
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><polyline points="21 3 21 8 16 8"/></svg>
298
301
  </button>
@@ -302,6 +305,8 @@
302
305
  </div>
303
306
  </div>
304
307
 
308
+ <div id="pane-split" class="pane-split">
309
+ <div id="board-side" class="board-side">
305
310
  <div class="kanban" id="main-content">
306
311
  <div id="owner-filter-bar" class="owner-filter-bar">
307
312
  <select id="owner-filter" class="filter-dropdown" onchange="filterByOwner(this.value)" aria-label="Filter by team member">
@@ -335,6 +340,18 @@
335
340
  <div id="completed-tasks" class="column-tasks" role="list" data-status="completed" ondragover="onColumnDragOver(event)" ondragleave="onColumnDragLeave(event)" ondrop="onColumnDrop(event)"></div>
336
341
  </div>
337
342
  </div>
343
+ <div id="agent-footer" class="agent-footer">
344
+ <div class="agent-footer-header" onclick="toggleAgentFooter()">
345
+ <span id="agent-footer-label">Agents</span>
346
+ <button class="agent-footer-toggle" id="agent-footer-toggle" aria-label="Toggle agent panel">&#x25BE;</button>
347
+ </div>
348
+ <div class="agent-footer-content" id="agent-footer-content"></div>
349
+ </div>
350
+ </div>
351
+ <div id="pane-divider" class="pane-divider" title="Drag to resize · drag to an edge to unsplit · double-click to reset"></div>
352
+ <div id="pane-views" class="pane-views"></div>
353
+ <div id="pane-drop" class="pane-drop"><div data-side="l"></div><div data-side="r"></div></div>
354
+ </div>
338
355
  <div id="terminal-pane" class="terminal-pane">
339
356
  <div class="terminal-bar">
340
357
  <span id="terminal-status" class="terminal-status"></span>
@@ -353,14 +370,6 @@
353
370
  <div id="terminal-host" class="terminal-host"></div>
354
371
  <textarea id="terminal-key-proxy" class="terminal-key-proxy" tabindex="-1" aria-hidden="true"></textarea>
355
372
  </div>
356
- <div id="pane-views" class="pane-views"></div>
357
- <div id="agent-footer" class="agent-footer">
358
- <div class="agent-footer-header" onclick="toggleAgentFooter()">
359
- <span id="agent-footer-label">Agents</span>
360
- <button class="agent-footer-toggle" id="agent-footer-toggle" aria-label="Toggle agent panel">&#x25BE;</button>
361
- </div>
362
- <div class="agent-footer-content" id="agent-footer-content"></div>
363
- </div>
364
373
  </div>
365
374
  </main>
366
375
 
package/public/style.css CHANGED
@@ -368,7 +368,9 @@ body::before {
368
368
  transition: background 0.15s;
369
369
  }
370
370
  .panel-resize-handle:hover,
371
- .panel-resize-handle.dragging {
371
+ .panel-resize-handle.dragging,
372
+ .pane-divider:hover,
373
+ .pane-divider.dragging {
372
374
  background: var(--accent-dim);
373
375
  }
374
376
 
@@ -3461,8 +3463,7 @@ code.language-tree .hljs-title {
3461
3463
  display: none;
3462
3464
  }
3463
3465
 
3464
- .session-view.pane-mode .kanban,
3465
- .session-view.pane-mode .agent-footer {
3466
+ .session-view.pane-mode .board-side {
3466
3467
  display: none;
3467
3468
  }
3468
3469
 
@@ -3526,9 +3527,55 @@ code.language-tree .hljs-title {
3526
3527
  .pane-tab.on {
3527
3528
  background: var(--bg-deep);
3528
3529
  border-color: var(--border);
3530
+ border-top-color: var(--accent);
3529
3531
  color: var(--text-primary);
3530
3532
  }
3531
3533
 
3534
+ .pane-tab.shown {
3535
+ background: var(--bg-deep);
3536
+ border-color: var(--border);
3537
+ color: var(--text-secondary);
3538
+ }
3539
+
3540
+ .pane-tab.dragging {
3541
+ opacity: 0.5;
3542
+ }
3543
+
3544
+ .pane-pair {
3545
+ display: flex;
3546
+ flex: 0 1 auto;
3547
+ align-items: stretch;
3548
+ min-width: 0;
3549
+ border: 1px solid var(--border);
3550
+ border-bottom: 0;
3551
+ border-radius: 7px 7px 0 0;
3552
+ background: var(--bg-elevated);
3553
+ }
3554
+
3555
+ .pane-pair.on {
3556
+ border-color: var(--accent-dim);
3557
+ }
3558
+
3559
+ .pane-pair .pane-tab {
3560
+ min-width: 80px;
3561
+ }
3562
+
3563
+ .pane-pair .pane-tab.on,
3564
+ .pane-pair .pane-tab.shown {
3565
+ border-color: transparent;
3566
+ }
3567
+
3568
+ .pane-pair-sep {
3569
+ flex: none;
3570
+ width: 1px;
3571
+ margin: 5px 0;
3572
+ background: var(--border);
3573
+ }
3574
+
3575
+ .pane-strip.drop-hot {
3576
+ background: var(--accent-dim);
3577
+ }
3578
+
3532
3579
  .pane-tab.pinned {
3533
3580
  flex: none;
3534
3581
  min-width: 0;
@@ -3604,13 +3651,19 @@ code.language-tree .hljs-title {
3604
3651
  }
3605
3652
 
3606
3653
  .pane-add,
3607
- .pane-tool {
3654
+ .pane-tool,
3655
+ .pane-split-btn {
3608
3656
  width: 26px;
3609
3657
  height: 26px;
3610
3658
  border-color: transparent;
3611
3659
  }
3612
3660
 
3661
+ .pane-split-btn.on {
3662
+ color: var(--accent);
3663
+ }
3664
+
3613
3665
  .pane-add svg,
3666
+ .pane-split-btn svg,
3614
3667
  .pane-tool svg {
3615
3668
  width: 14px;
3616
3669
  height: 14px;
@@ -3628,7 +3681,8 @@ code.language-tree .hljs-title {
3628
3681
  margin: 0 0 3px auto;
3629
3682
  }
3630
3683
 
3631
- .pane-tool:disabled {
3684
+ .pane-tool:disabled,
3685
+ .pane-split-btn:disabled {
3632
3686
  opacity: 0.35;
3633
3687
  cursor: default;
3634
3688
  background: none;
@@ -3657,6 +3711,178 @@ code.language-tree .hljs-title {
3657
3711
  display: flex;
3658
3712
  }
3659
3713
 
3714
+ .pane-split {
3715
+ --pane-split: 50%;
3716
+ position: relative;
3717
+ flex: 1;
3718
+ display: flex;
3719
+ min-height: 0;
3720
+ }
3721
+
3722
+ .board-side {
3723
+ flex: 1;
3724
+ display: flex;
3725
+ flex-direction: column;
3726
+ min-width: 0;
3727
+ container: board / inline-size;
3728
+ }
3729
+
3730
+ .board-side > .kanban {
3731
+ min-height: 0;
3732
+ }
3733
+
3734
+ /* 936px is where the full layout stops fitting: 3 × 280px columns, 2 × 24px gaps, 2 × 24px padding. */
3735
+ @container board (max-width: 936px) {
3736
+ .kanban {
3737
+ gap: 12px;
3738
+ padding: 16px;
3739
+ }
3740
+
3741
+ .kanban-column {
3742
+ min-width: 140px;
3743
+ }
3744
+
3745
+ .column-header {
3746
+ gap: 6px;
3747
+ padding-bottom: 12px;
3748
+ margin-bottom: 12px;
3749
+ }
3750
+
3751
+ .column-title {
3752
+ font-size: 12px;
3753
+ letter-spacing: 0.03em;
3754
+ white-space: nowrap;
3755
+ overflow: hidden;
3756
+ text-overflow: ellipsis;
3757
+ }
3758
+
3759
+ .column-tasks {
3760
+ padding-right: 0;
3761
+ }
3762
+ }
3763
+
3764
+ .session-view.terminal-mode .pane-split {
3765
+ display: none;
3766
+ }
3767
+
3768
+ .pane-divider,
3769
+ .pane-drop {
3770
+ display: none;
3771
+ }
3772
+
3773
+ .pane-divider {
3774
+ background: linear-gradient(to right, transparent 2px, var(--border) 2px 4px, transparent 4px);
3775
+ transition: background 0.15s;
3776
+ }
3777
+
3778
+ .session-view.split-mode:not(.terminal-mode) .pane-views {
3779
+ display: contents;
3780
+ }
3781
+
3782
+ .session-view.split-mode .pane-split > .board-side.side-l,
3783
+ .session-view.split-mode .pane-split > .board-side.side-r,
3784
+ .session-view.split-mode .pane-view {
3785
+ position: absolute;
3786
+ top: 0;
3787
+ bottom: 0;
3788
+ background: var(--bg-deep);
3789
+ }
3790
+
3791
+ .session-view.split-mode .side-l {
3792
+ left: 0;
3793
+ right: calc(100% - var(--pane-split) + 3px);
3794
+ }
3795
+
3796
+ .session-view.split-mode .side-r {
3797
+ left: calc(var(--pane-split) + 3px);
3798
+ right: 0;
3799
+ }
3800
+
3801
+ .pane-split > .message-panel.log-l,
3802
+ .pane-split > .message-panel.log-r {
3803
+ position: absolute;
3804
+ top: 0;
3805
+ bottom: 0;
3806
+ left: auto;
3807
+ height: auto;
3808
+ z-index: 3;
3809
+ }
3810
+
3811
+ .pane-split > .message-panel.log-l {
3812
+ right: calc(100% - var(--pane-split) + 3px);
3813
+ width: min(var(--message-panel-width, 540px), calc(var(--pane-split) - 3px));
3814
+ }
3815
+
3816
+ .pane-split > .message-panel.log-r {
3817
+ right: 0;
3818
+ width: min(var(--message-panel-width, 540px), calc(100% - var(--pane-split) - 3px));
3819
+ }
3820
+
3821
+ .pane-split > .message-panel.full-width.log-l {
3822
+ left: 0;
3823
+ width: auto;
3824
+ }
3825
+
3826
+ .pane-split > .message-panel.full-width.log-r {
3827
+ left: calc(var(--pane-split) + 3px);
3828
+ width: auto;
3829
+ }
3830
+
3831
+ .session-view.split-mode .pane-focus::after {
3832
+ content: '';
3833
+ position: absolute;
3834
+ top: 0;
3835
+ left: 0;
3836
+ right: 0;
3837
+ height: 2px;
3838
+ background: var(--accent);
3839
+ pointer-events: none;
3840
+ z-index: 3;
3841
+ }
3842
+
3843
+ .session-view.split-mode:not(.terminal-mode) .pane-divider {
3844
+ display: block;
3845
+ position: absolute;
3846
+ top: 0;
3847
+ bottom: 0;
3848
+ left: calc(var(--pane-split) - 3px);
3849
+ width: 6px;
3850
+ cursor: col-resize;
3851
+ z-index: 4;
3852
+ }
3853
+
3854
+ .pane-split.resizing {
3855
+ cursor: col-resize;
3856
+ }
3857
+
3858
+ .pane-split.resizing iframe {
3859
+ pointer-events: none;
3860
+ }
3861
+
3862
+ .pane-split.collapse-l .side-l,
3863
+ .pane-split.collapse-r .side-r {
3864
+ opacity: 0.35;
3865
+ }
3866
+
3867
+ .pane-split.dropping .pane-drop {
3868
+ display: flex;
3869
+ position: absolute;
3870
+ inset: 0;
3871
+ z-index: 5;
3872
+ }
3873
+
3874
+ .pane-drop > div {
3875
+ flex: 1;
3876
+ margin: 8px;
3877
+ border-radius: 8px;
3878
+ }
3879
+
3880
+ .pane-drop > div.hot {
3881
+ background: var(--accent-dim);
3882
+ outline: 2px dashed var(--accent);
3883
+ outline-offset: -2px;
3884
+ }
3885
+
3660
3886
  .pane-view:has(> .review-panel) {
3661
3887
  flex-direction: row;
3662
3888
  }
@@ -5269,6 +5495,7 @@ code.language-tree .hljs-title {
5269
5495
 
5270
5496
  .linked-doc-list,
5271
5497
  .scratch-file-list,
5498
+ .zen-bookmark-list,
5272
5499
  .artifact-list {
5273
5500
  list-style: none;
5274
5501
  padding: 0;
@@ -5280,6 +5507,7 @@ code.language-tree .hljs-title {
5280
5507
 
5281
5508
  .linked-doc-item,
5282
5509
  .scratch-file-item,
5510
+ .zen-bookmark-item,
5283
5511
  .artifact-item {
5284
5512
  display: flex;
5285
5513
  align-items: baseline;
@@ -5289,6 +5517,7 @@ code.language-tree .hljs-title {
5289
5517
 
5290
5518
  .linked-doc-link,
5291
5519
  .scratch-file-link,
5520
+ .zen-bookmark-link,
5292
5521
  .artifact-link {
5293
5522
  color: var(--accent-text);
5294
5523
  text-decoration: underline;
@@ -5346,6 +5575,12 @@ code.language-tree .hljs-title {
5346
5575
  font-size: 11px;
5347
5576
  }
5348
5577
 
5578
+ .zen-bookmark-who {
5579
+ color: var(--text-muted);
5580
+ flex-shrink: 0;
5581
+ }
5582
+
5583
+ .zen-bookmark-link,
5349
5584
  .scratch-file-link {
5350
5585
  background: none;
5351
5586
  border: none;
package/server.js CHANGED
@@ -4015,6 +4015,12 @@ const panesFileStamp = () => {
4015
4015
  }
4016
4016
  };
4017
4017
  let panesStamp;
4018
+ // File panes render the file once; this watch tells the boards when one changes. An editor
4019
+ // or an agent often saves several times in a row, so each path waits for a quiet 200 ms.
4020
+ const paneFileWatcher = chokidar.watch([], { ignoreInitial: true });
4021
+ const paneFileTimers = new Map();
4022
+ let paneFilesWatched = new Set();
4023
+ let paneFileWatchQueued = false;
4018
4024
  const panes = createPaneStore({
4019
4025
  load: () => {
4020
4026
  const stamp = panesFileStamp();
@@ -4026,7 +4032,37 @@ const panes = createPaneStore({
4026
4032
  writeJsonAtomic(PANES_FILE, data);
4027
4033
  panesStamp = panesFileStamp();
4028
4034
  },
4029
- onChange: (sessionId, layout) => broadcast({ type: 'pane:changed', sessionId, layout }),
4035
+ onChange: (sessionId, layout) => {
4036
+ broadcast({ type: 'pane:changed', sessionId, layout });
4037
+ queuePaneFileWatch();
4038
+ },
4039
+ });
4040
+
4041
+ // Deferred: the store's first load reports changes before `panes` is assigned, and a reload
4042
+ // reports each changed session, which one sync covers.
4043
+ function queuePaneFileWatch() {
4044
+ if (paneFileWatchQueued) return;
4045
+ paneFileWatchQueued = true;
4046
+ setImmediate(() => {
4047
+ paneFileWatchQueued = false;
4048
+ const next = panes.fileTargets();
4049
+ const added = [...next].filter((p) => !paneFilesWatched.has(p));
4050
+ const gone = [...paneFilesWatched].filter((p) => !next.has(p));
4051
+ if (added.length) paneFileWatcher.add(added);
4052
+ if (gone.length) paneFileWatcher.unwatch(gone);
4053
+ paneFilesWatched = next;
4054
+ });
4055
+ }
4056
+ paneFileWatcher.on('all', (event, filePath) => {
4057
+ if (event !== 'add' && event !== 'change' && event !== 'unlink') return;
4058
+ clearTimeout(paneFileTimers.get(filePath));
4059
+ paneFileTimers.set(
4060
+ filePath,
4061
+ setTimeout(() => {
4062
+ paneFileTimers.delete(filePath);
4063
+ broadcast({ type: 'pane:file-changed', path: filePath });
4064
+ }, 200),
4065
+ );
4030
4066
  });
4031
4067
  chokidar.watch(PANES_FILE, { ignoreInitial: true }).on('all', (event) => {
4032
4068
  if (event === 'add' || event === 'change') panes.reload();
@@ -4143,7 +4179,8 @@ function parseReviewBody(body) {
4143
4179
  selector: code(c?.selector, 500),
4144
4180
  }));
4145
4181
  if (items.some((c) => !c.comment)) throw previewError(400, 'every comment needs text');
4146
- return { kind: source.kind, label, path: source.kind === 'file' ? source.path : null, items };
4182
+ const locate = oneLine(source.locate, 600) || null;
4183
+ return { kind: source.kind, label, path: source.kind === 'file' ? source.path : null, locate, items };
4147
4184
  }
4148
4185
 
4149
4186
  function formatReviewMarkdown(src, items) {
@@ -4152,6 +4189,7 @@ function formatReviewMarkdown(src, items) {
4152
4189
  '',
4153
4190
  `Each comment is the user's instruction about the quoted text.${src.path ? ' A quote missing from the source means the source changed after the review: say so.' : ''}`,
4154
4191
  ];
4192
+ if (src.locate) parts.push('', src.locate);
4155
4193
  items.forEach((c, i) => {
4156
4194
  const where = [c.line && `line ${c.line}`, c.heading && `under "${c.heading}"`].filter(Boolean).join(', ');
4157
4195
  parts.push('', `## ${i + 1}${where ? ` (${where})` : ''}`, '');
@@ -4491,6 +4529,7 @@ async function runRetention() {
4491
4529
  const opts = { known: scan?.ids, maxAgeMs: retentionMs(CLAUDE_DIR) };
4492
4530
  const markers = dispatched.prune(opts);
4493
4531
  const layouts = panes.prune(opts);
4532
+ if (layouts) queuePaneFileWatch();
4494
4533
  const reviews = await pruneSessionDirs(REVIEW_DIR, opts);
4495
4534
  const contexts = await pruneContextStatus(CONTEXT_STATUS_DIR, opts);
4496
4535
  const wts = worktrees.prune(scan?.dirs);