claude-code-kanban 7.1.2 → 7.3.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
 
@@ -840,6 +849,7 @@
840
849
  </div>
841
850
  <div class="modal-footer ns-footer">
842
851
  <label class="ns-check ns-stay" title="Start in the background and keep the current view"><input type="checkbox" id="ns-stay"> Stay here</label>
852
+ <label class="ns-check ns-edit" title="Open a shell in the folder with the claude command typed in. Press Enter to run it."><input type="checkbox" id="ns-edit"> Edit before run</label>
843
853
  <button type="button" class="btn btn-secondary" onclick="closeNewSession()">Cancel</button>
844
854
  <button type="button" id="ns-start" class="btn btn-primary" onclick="startNewSession()" disabled>Start</button>
845
855
  </div>
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
 
@@ -3530,6 +3531,51 @@ code.language-tree .hljs-title {
3530
3531
  color: var(--text-primary);
3531
3532
  }
3532
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
+
3533
3579
  .pane-tab.pinned {
3534
3580
  flex: none;
3535
3581
  min-width: 0;
@@ -3605,13 +3651,19 @@ code.language-tree .hljs-title {
3605
3651
  }
3606
3652
 
3607
3653
  .pane-add,
3608
- .pane-tool {
3654
+ .pane-tool,
3655
+ .pane-split-btn {
3609
3656
  width: 26px;
3610
3657
  height: 26px;
3611
3658
  border-color: transparent;
3612
3659
  }
3613
3660
 
3661
+ .pane-split-btn.on {
3662
+ color: var(--accent);
3663
+ }
3664
+
3614
3665
  .pane-add svg,
3666
+ .pane-split-btn svg,
3615
3667
  .pane-tool svg {
3616
3668
  width: 14px;
3617
3669
  height: 14px;
@@ -3629,7 +3681,8 @@ code.language-tree .hljs-title {
3629
3681
  margin: 0 0 3px auto;
3630
3682
  }
3631
3683
 
3632
- .pane-tool:disabled {
3684
+ .pane-tool:disabled,
3685
+ .pane-split-btn:disabled {
3633
3686
  opacity: 0.35;
3634
3687
  cursor: default;
3635
3688
  background: none;
@@ -3658,6 +3711,178 @@ code.language-tree .hljs-title {
3658
3711
  display: flex;
3659
3712
  }
3660
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
+
3661
3886
  .pane-view:has(> .review-panel) {
3662
3887
  flex-direction: row;
3663
3888
  }
@@ -3684,6 +3909,49 @@ code.language-tree .hljs-title {
3684
3909
  background: #fff;
3685
3910
  }
3686
3911
 
3912
+ .pane-url-bar {
3913
+ position: absolute;
3914
+ right: 12px;
3915
+ bottom: 12px;
3916
+ display: flex;
3917
+ align-items: center;
3918
+ gap: 2px;
3919
+ padding: 3px;
3920
+ border: 1px solid var(--border);
3921
+ border-radius: 8px;
3922
+ background: var(--bg-elevated);
3923
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
3924
+ font-family: var(--font-mono);
3925
+ font-size: 12px;
3926
+ opacity: 0;
3927
+ pointer-events: none;
3928
+ transition: opacity 0.15s;
3929
+ }
3930
+
3931
+ .pane-view:hover > .pane-url-bar,
3932
+ .pane-view.frame-focus > .pane-url-bar {
3933
+ opacity: 1;
3934
+ pointer-events: auto;
3935
+ }
3936
+
3937
+ .pane-url-bar .icon-btn {
3938
+ width: auto;
3939
+ min-width: 26px;
3940
+ height: 24px;
3941
+ padding: 0 6px;
3942
+ border-color: transparent;
3943
+ font: inherit;
3944
+ }
3945
+
3946
+ .pane-url-bar .pane-keys {
3947
+ display: none;
3948
+ color: var(--accent);
3949
+ }
3950
+
3951
+ .pane-view.frame-focus .pane-keys {
3952
+ display: flex;
3953
+ }
3954
+
3687
3955
  .pane-doc {
3688
3956
  flex: 1;
3689
3957
  min-height: 0;
@@ -5270,6 +5538,7 @@ code.language-tree .hljs-title {
5270
5538
 
5271
5539
  .linked-doc-list,
5272
5540
  .scratch-file-list,
5541
+ .zen-bookmark-list,
5273
5542
  .artifact-list {
5274
5543
  list-style: none;
5275
5544
  padding: 0;
@@ -5281,6 +5550,7 @@ code.language-tree .hljs-title {
5281
5550
 
5282
5551
  .linked-doc-item,
5283
5552
  .scratch-file-item,
5553
+ .zen-bookmark-item,
5284
5554
  .artifact-item {
5285
5555
  display: flex;
5286
5556
  align-items: baseline;
@@ -5290,6 +5560,7 @@ code.language-tree .hljs-title {
5290
5560
 
5291
5561
  .linked-doc-link,
5292
5562
  .scratch-file-link,
5563
+ .zen-bookmark-link,
5293
5564
  .artifact-link {
5294
5565
  color: var(--accent-text);
5295
5566
  text-decoration: underline;
@@ -5347,6 +5618,12 @@ code.language-tree .hljs-title {
5347
5618
  font-size: 11px;
5348
5619
  }
5349
5620
 
5621
+ .zen-bookmark-who {
5622
+ color: var(--text-muted);
5623
+ flex-shrink: 0;
5624
+ }
5625
+
5626
+ .zen-bookmark-link,
5350
5627
  .scratch-file-link {
5351
5628
  background: none;
5352
5629
  border: none;
@@ -7099,7 +7376,7 @@ select.form-input option:checked {
7099
7376
 
7100
7377
  /* #region NEW_SESSION */
7101
7378
  .modal.new-session-modal {
7102
- width: 520px;
7379
+ width: 640px;
7103
7380
  max-width: calc(100vw - 32px);
7104
7381
  overflow: visible;
7105
7382
  }
@@ -7228,7 +7505,8 @@ select.form-input option:checked {
7228
7505
  accent-color: var(--accent);
7229
7506
  }
7230
7507
 
7231
- .ns-inline .form-input:disabled {
7508
+ .ns-inline .form-input:disabled,
7509
+ .ns-check:has(input:disabled) {
7232
7510
  opacity: 0.5;
7233
7511
  cursor: not-allowed;
7234
7512
  }
@@ -7311,15 +7589,27 @@ select.form-input option:checked {
7311
7589
  border-top: 1px solid var(--border);
7312
7590
  }
7313
7591
 
7314
- .ns-stay {
7315
- margin-right: auto;
7592
+ .ns-stay,
7593
+ .ns-edit {
7316
7594
  color: var(--text-tertiary);
7317
7595
  }
7318
7596
 
7319
- .new-session-modal.ns-resume .ns-stay {
7597
+ .ns-edit {
7598
+ margin-right: auto;
7599
+ }
7600
+
7601
+ .new-session-modal.ns-resume .ns-stay,
7602
+ .new-session-modal.ns-resume .ns-edit {
7320
7603
  visibility: hidden;
7321
7604
  }
7322
7605
 
7606
+ .ns-footer .btn-primary {
7607
+ min-width: 0;
7608
+ overflow: hidden;
7609
+ text-overflow: ellipsis;
7610
+ white-space: nowrap;
7611
+ }
7612
+
7323
7613
  .ns-footer .btn-primary:disabled {
7324
7614
  opacity: 0.5;
7325
7615
  cursor: not-allowed;
@@ -11,12 +11,14 @@
11
11
  const modsOf = (e) =>
12
12
  [e.ctrlKey && 'ctrl', e.altKey && 'alt', e.shiftKey && 'shift', e.metaKey && 'meta'].filter(Boolean).join('+');
13
13
 
14
- // A claim names a key by its physical code, or by its character when the layout moves it.
14
+ // A claim names a key by its physical code, or by its character when the layout moves it. A press
15
+ // that AltGr turns into a character (Polish AltGr+S is 'ś') is matched by its character only.
15
16
  function keySigs(e) {
16
17
  const mods = modsOf(e);
17
- return [e.code && `${mods}|c:${e.code}`, typeof e.key === 'string' && `${mods}|k:${e.key.toLowerCase()}`].filter(
18
- Boolean,
19
- );
18
+ return [
19
+ e.code && !e.getModifierState?.('AltGraph') && `${mods}|c:${e.code}`,
20
+ typeof e.key === 'string' && `${mods}|k:${e.key.toLowerCase()}`,
21
+ ].filter(Boolean);
20
22
  }
21
23
 
22
24
  const claimSig = (c) => keySigs(c)[0];
@@ -1,18 +1,51 @@
1
- // claude-hub-sdk 1.2.0 (sha256 62c7d9f477b2). Copied by npm run sdk:sync in claude-code-hub. Do not edit.
2
- // Claude Code Hub SDK stub: what an app serves when no hub runs it. Under a hub, the hub hands the
3
- // app its real SDK (client.js) in place of this file. The API must match client.js; a test checks it.
4
- ((root) => {
1
+ // claude-hub-sdk 1.4.0 (sha256 7d559a8888ce). Copied by npm run sdk:sync in claude-code-hub. Do not edit.
2
+ // Claude Code Hub SDK: the one rule that names a key press, shared by the hub page, client.js and
3
+ // stub.js. The hub serves it to its page as /sdk/keys.js; mount() and sdk:sync put it in front of
4
+ // client.js and stub.js, so an app still loads one file.
5
+ // var, not const: the hub page and an app page each load it as a classic script, and a second
6
+ // const declaration in the same page would throw.
7
+ var ClaudeHubKeys = (() => {
5
8
  'use strict';
6
9
 
7
- // A copy of client.js comboOf(); a test checks they match.
10
+ // Modifiers in ctrl, alt, shift, meta order, joined by '+' to the key. macOS composes
11
+ // Option+<key> into a character (Option+1 is '¡', Option+P is 'π') and holding Control does not
12
+ // undo it, so e.key alone cannot name these presses there. e.code is the physical key, which is
13
+ // wrong for non-US layouts, hence only as a fallback. No fallback under AltGraph: Chromium sets it
14
+ // when AltGr types a character, and that press is text. Takes a real KeyboardEvent or a forwarded
15
+ // {key, code} payload; a payload without code degrades to key. A payload needs no AltGraph: the
16
+ // SDK forwards only a press whose key already names a bound combo.
17
+ // Self-contained on purpose: cck puts its source text into a sandboxed frame.
8
18
  function comboOf(e) {
9
19
  const lower = typeof e.key === 'string' ? e.key.toLowerCase() : '';
10
- const m = /^(?:Key|Digit)([A-Z1-9])$/.exec(e.code || '');
20
+ const m = !e.getModifierState?.('AltGraph') && /^(?:Key|Digit)([A-Z1-9])$/.exec(e.code || '');
11
21
  const key = /^[a-z1-9]$/.test(lower) ? lower : m ? m[1].toLowerCase() : e.key;
12
22
  const mods = [e.ctrlKey && 'ctrl', e.altKey && 'alt', e.shiftKey && 'shift', e.metaKey && 'meta'];
13
23
  return [...mods, key].filter(Boolean).join('+');
14
24
  }
15
25
 
26
+ // The keys of a combo as a help row shows them: ['Ctrl', 'Alt', 'P'], or ['⌃', '⌥', 'P'] on macOS.
27
+ // The {n} of a numbered combo reads 1…9.
28
+ function keyParts(combo, mac) {
29
+ const mods = mac
30
+ ? { ctrl: '⌃', alt: '⌥', shift: '⇧', meta: '⌘' }
31
+ : { ctrl: 'Ctrl', alt: 'Alt', shift: 'Shift', meta: 'Win' };
32
+ const named = { '{n}': '1…9', ArrowLeft: '←', ArrowRight: '→', ArrowUp: '↑', ArrowDown: '↓' };
33
+ return combo.split('+').map((k) => mods[k] ?? named[k] ?? (k.length === 1 ? k.toUpperCase() : k));
34
+ }
35
+
36
+ return { comboOf, keyParts };
37
+ })();
38
+
39
+ if (typeof module === 'object' && module.exports) module.exports = ClaudeHubKeys;
40
+
41
+ // Claude Code Hub SDK stub: what an app serves when no hub runs it. Under a hub, the hub hands the
42
+ // app its real SDK (client.js) in place of this file. The API must match client.js; a test checks it.
43
+ ((root) => {
44
+ 'use strict';
45
+
46
+ // keys.js comes first in the synced file. Required as a module, stub.js loads it itself.
47
+ const { comboOf } = typeof ClaudeHubKeys === 'object' ? ClaudeHubKeys : require('./keys');
48
+
16
49
  function createClaudeHub(win) {
17
50
  let hub = null;
18
51
  const off = () => {};
@@ -43,6 +76,7 @@
43
76
  can: (action) => !!standaloneFn(action),
44
77
  forwards: () => false,
45
78
  forwardCombos: () => [],
79
+ keyLabel: () => null,
46
80
  closeGuard() {},
47
81
  openExternal(url) {
48
82
  win.open(url, '_blank', 'noopener');
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);