clay-server 2.47.0-beta.6 → 3.0.0-beta.2

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.
Files changed (46) hide show
  1. package/lib/daemon.js +6 -27
  2. package/lib/project-connection.js +21 -8
  3. package/lib/project-session-pair.js +312 -0
  4. package/lib/project-session-spawn.js +8 -4
  5. package/lib/project-sessions.js +108 -10
  6. package/lib/project-user-message.js +18 -8
  7. package/lib/project.js +33 -0
  8. package/lib/public/app.js +20 -0
  9. package/lib/public/css/menus.css +83 -1
  10. package/lib/public/css/messages.css +14 -0
  11. package/lib/public/css/pane.css +420 -0
  12. package/lib/public/css/sidebar.css +40 -0
  13. package/lib/public/css/sticky-notes.css +2 -0
  14. package/lib/public/index.html +4 -0
  15. package/lib/public/modules/app-connection.js +6 -0
  16. package/lib/public/modules/app-favicon.js +5 -0
  17. package/lib/public/modules/app-header.js +101 -6
  18. package/lib/public/modules/app-messages.js +59 -6
  19. package/lib/public/modules/app-panels.js +49 -27
  20. package/lib/public/modules/app-rendering.js +11 -4
  21. package/lib/public/modules/pane-bridge.js +41 -0
  22. package/lib/public/modules/pane-session.js +18 -0
  23. package/lib/public/modules/sidebar-sessions.js +175 -1
  24. package/lib/public/modules/sidebar.js +2 -0
  25. package/lib/public/modules/split-group-helpers.js +18 -0
  26. package/lib/public/modules/split-pair-ui.js +324 -0
  27. package/lib/public/modules/split-view.js +499 -0
  28. package/lib/public/modules/sticky-notes.js +32 -91
  29. package/lib/public/style.css +1 -0
  30. package/lib/sdk-bridge.js +111 -20
  31. package/lib/sdk-message-processor.js +2 -2
  32. package/lib/server.js +24 -4
  33. package/lib/session-hygiene.js +100 -0
  34. package/lib/session-pair-mcp-server.js +28 -0
  35. package/lib/session-split-groups.js +298 -0
  36. package/lib/sessions.js +163 -34
  37. package/lib/ws-request.js +14 -0
  38. package/lib/ws-schema.js +13 -0
  39. package/lib/yoke/adapters/claude.js +32 -0
  40. package/lib/yoke/adapters/codex.js +100 -77
  41. package/lib/yoke/adapters/kiro.js +34 -35
  42. package/lib/yoke/codex-app-server.js +56 -6
  43. package/lib/yoke/index.js +1 -0
  44. package/lib/yoke/skill-discovery.js +196 -0
  45. package/lib/yoke/vendor-registry.js +34 -0
  46. package/package.json +2 -2
@@ -0,0 +1,420 @@
1
+ /* Pane-mode app instances and the parent split shell. */
2
+
3
+ body.pane-mode #top-bar,
4
+ body.pane-mode #icon-strip,
5
+ body.pane-mode #sidebar-column,
6
+ body.pane-mode #mate-sidebar-column,
7
+ body.pane-mode #sidebar-resize-handle,
8
+ body.pane-mode #mate-sidebar-resize-handle,
9
+ body.pane-mode #sidebar-overlay,
10
+ body.pane-mode #home-hub,
11
+ body.pane-mode #whats-new-article,
12
+ body.pane-mode .title-bar-content,
13
+ body.pane-mode .dm-header-bar,
14
+ body.pane-mode #mobile-tab-bar,
15
+ body.pane-mode #mobile-sheet,
16
+ body.pane-mode #user-island {
17
+ display: none !important;
18
+ }
19
+
20
+ /* Sticky notes are project-wide. The parent split shell renders one shared
21
+ canvas across the full group instead of duplicating it in each iframe. */
22
+ body.pane-mode #sticky-notes-container {
23
+ display: none !important;
24
+ }
25
+
26
+ body.pane-mode #main-area {
27
+ border: 0;
28
+ border-radius: 0;
29
+ }
30
+
31
+ body.pane-mode #main-column,
32
+ body.pane-mode #main-panels,
33
+ body.pane-mode #app {
34
+ width: 100%;
35
+ height: 100%;
36
+ }
37
+
38
+ /* Narrow panes cross the mobile breakpoint, but pane mode hides the mobile
39
+ tab bar. Keep the normal composer inset instead of reserving its 56px. */
40
+ body.pane-mode #input-area {
41
+ padding-bottom: calc(var(--safe-bottom) + 8px);
42
+ }
43
+
44
+ #main-panels {
45
+ position: relative;
46
+ }
47
+
48
+ #app.split-native-hidden {
49
+ display: none !important;
50
+ }
51
+
52
+ #split-host {
53
+ display: none;
54
+ flex: 1;
55
+ min-width: 0;
56
+ min-height: 0;
57
+ background: var(--bg);
58
+ }
59
+
60
+ #split-host.visible {
61
+ display: flex;
62
+ }
63
+
64
+ .split-pane {
65
+ flex: 1;
66
+ display: flex;
67
+ flex-direction: column;
68
+ min-width: 0;
69
+ min-height: 0;
70
+ border-left: 1px solid var(--border-subtle);
71
+ background: var(--bg);
72
+ }
73
+
74
+ .split-pane:first-child {
75
+ border-left: 0;
76
+ }
77
+
78
+ .split-pane-header {
79
+ height: 24px;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 6px;
83
+ padding: 0 5px 0 9px;
84
+ border-bottom: 1px solid var(--border-subtle);
85
+ background: var(--sidebar-bg);
86
+ color: var(--text-secondary);
87
+ flex-shrink: 0;
88
+ }
89
+
90
+ .split-pane-vendor {
91
+ width: 14px;
92
+ height: 14px;
93
+ border-radius: 4px;
94
+ flex-shrink: 0;
95
+ }
96
+
97
+ .split-pane-title {
98
+ flex: 1;
99
+ min-width: 0;
100
+ overflow: hidden;
101
+ text-overflow: ellipsis;
102
+ white-space: nowrap;
103
+ font-size: 11px;
104
+ font-weight: 600;
105
+ cursor: text;
106
+ border-radius: 4px;
107
+ padding: 1px 4px;
108
+ margin-left: -4px;
109
+ }
110
+
111
+ .split-pane-title:hover {
112
+ background: var(--bg-alt);
113
+ }
114
+
115
+ .split-pair-role,
116
+ .split-pair-live {
117
+ flex-shrink: 0;
118
+ border-radius: 999px;
119
+ padding: 1px 6px;
120
+ font-size: 8px;
121
+ font-weight: 700;
122
+ letter-spacing: 0.05em;
123
+ text-transform: uppercase;
124
+ }
125
+
126
+ .split-pair-role-driver { color: var(--accent); background: color-mix(in srgb, var(--accent) 13%, transparent); }
127
+ .split-pair-role-worker { color: var(--text-secondary); background: var(--bg-alt); }
128
+
129
+ /* Role badges are buttons: Driver click clears roles, Worker click takes over. */
130
+ button.split-pair-role {
131
+ border: 0;
132
+ cursor: pointer;
133
+ font-family: inherit;
134
+ }
135
+ button.split-pair-role:hover { filter: brightness(1.25); }
136
+
137
+ /* Ad-hoc splits: role assignment in each pane header. ALWAYS fully
138
+ visible; hover/opacity gating made the control undiscoverable. */
139
+ .split-pair-set-driver {
140
+ flex-shrink: 0;
141
+ display: inline-flex;
142
+ align-items: center;
143
+ border: 1px solid var(--border);
144
+ border-radius: 999px;
145
+ padding: 2px 9px;
146
+ font-size: 9px;
147
+ font-weight: 700;
148
+ letter-spacing: 0.05em;
149
+ text-transform: uppercase;
150
+ background: var(--bg-alt);
151
+ color: var(--text-secondary);
152
+ cursor: pointer;
153
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
154
+ font-family: inherit;
155
+ }
156
+ .split-pair-set-driver:hover {
157
+ color: var(--accent);
158
+ border-color: var(--accent);
159
+ background: color-mix(in srgb, var(--accent) 10%, var(--bg));
160
+ }
161
+ .split-pair-live { color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); animation: pair-status-pulse 1.5s ease-in-out infinite; }
162
+
163
+ #split-host.split-delegating::after {
164
+ content: "→";
165
+ position: absolute;
166
+ z-index: 4;
167
+ top: calc(50% - 10px);
168
+ left: calc(50% - 17px);
169
+ width: 34px;
170
+ height: 20px;
171
+ display: grid;
172
+ place-items: center;
173
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
174
+ border-radius: 999px;
175
+ pointer-events: none;
176
+ background: var(--sidebar-bg);
177
+ color: var(--accent);
178
+ font-size: 14px;
179
+ font-weight: 800;
180
+ animation: pair-divider-flow 1.1s ease-in-out infinite;
181
+ filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent) 65%, transparent));
182
+ }
183
+
184
+ #split-host.split-direction-left::after { content: "←"; animation-name: pair-divider-flow-left; }
185
+
186
+ @keyframes pair-divider-flow { 0%, 100% { transform: translateX(-3px); opacity: 0.65; } 50% { transform: translateX(3px); opacity: 1; } }
187
+ @keyframes pair-divider-flow-left { 0%, 100% { transform: translateX(3px); opacity: 0.65; } 50% { transform: translateX(-3px); opacity: 1; } }
188
+ @keyframes pair-status-pulse { 50% { opacity: 0.55; } }
189
+
190
+ /* Pair dialog rides the standard wt-modal pattern; these classes only add
191
+ the two-column role layout on top of it. */
192
+ .pair-modal { min-width: 480px; max-width: 560px; }
193
+ .pair-modal-desc { font-size: 12px; color: var(--text-dimmer); margin: -10px 0 14px; }
194
+ .pair-modal-roles { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
195
+ .pair-modal-role { min-width: 0; }
196
+ .pair-modal-role-heading { display: flex; align-items: center; gap: 9px; margin-bottom: 10px; }
197
+ .pair-modal-role-heading div { display: grid; gap: 1px; }
198
+ .pair-modal-role-heading strong { color: var(--text); font-size: 12px; }
199
+ .pair-modal-role-heading span { color: var(--text-dimmer); font-size: 10px; }
200
+ @media (max-width: 560px) {
201
+ .pair-modal { min-width: 0; width: calc(100vw - 32px); }
202
+ .pair-modal-roles { grid-template-columns: 1fr; }
203
+ }
204
+ .pair-modal-current {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 8px;
208
+ padding: 8px 10px;
209
+ border: 1px solid var(--border-subtle);
210
+ border-radius: 8px;
211
+ background: var(--bg-alt);
212
+ font-size: 13px;
213
+ font-weight: 600;
214
+ overflow: hidden;
215
+ }
216
+ .pair-modal-current span {
217
+ overflow: hidden;
218
+ text-overflow: ellipsis;
219
+ white-space: nowrap;
220
+ }
221
+ .pair-role-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--text-dimmer); box-shadow: 0 0 0 4px color-mix(in srgb, var(--text-dimmer) 12%, transparent); flex-shrink: 0; }
222
+ .pair-role-driver { background: var(--accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent); }
223
+
224
+ .split-pane-rename-input {
225
+ flex: 1;
226
+ min-width: 0;
227
+ border: 1px solid var(--accent);
228
+ border-radius: 4px;
229
+ background: var(--bg);
230
+ color: var(--text);
231
+ font-size: 11px;
232
+ font-weight: 600;
233
+ padding: 1px 4px;
234
+ margin-left: -4px;
235
+ outline: none;
236
+ }
237
+
238
+ .split-pane-context {
239
+ display: none;
240
+ align-items: center;
241
+ gap: 4px;
242
+ border: 0;
243
+ padding: 2px 5px;
244
+ border-radius: 4px;
245
+ background: transparent;
246
+ color: var(--text-dimmer);
247
+ cursor: pointer;
248
+ flex-shrink: 0;
249
+ }
250
+
251
+ .split-pane-context.has-data {
252
+ display: inline-flex;
253
+ }
254
+
255
+ .split-pane-context:hover {
256
+ background: var(--bg-alt);
257
+ color: var(--text);
258
+ }
259
+
260
+ .split-pane-context-bar {
261
+ width: 36px;
262
+ height: 3px;
263
+ border-radius: 2px;
264
+ background: var(--border);
265
+ overflow: hidden;
266
+ }
267
+
268
+ .split-pane-context-fill {
269
+ display: block;
270
+ height: 100%;
271
+ width: 0%;
272
+ border-radius: 2px;
273
+ background: var(--accent);
274
+ transition: width 0.3s ease, background 0.3s ease;
275
+ }
276
+
277
+ .split-pane-context-fill.warn { background: var(--warning); }
278
+ .split-pane-context-fill.danger { background: var(--error); }
279
+
280
+ .split-pane-context-label {
281
+ font-family: "Roboto Mono", monospace;
282
+ font-size: 10px;
283
+ line-height: 1;
284
+ }
285
+
286
+ /* Header-scoped (two-class) selectors: pane.css loads BEFORE menus.css in
287
+ style.css, so the base .session-full-access rules would win a specificity
288
+ tie and keep the pill at its 22px title-bar size inside the 24px pane
289
+ header. Scope under .split-pane-header to outrank them. */
290
+ .split-pane-header .split-pane-full-access {
291
+ height: 15px;
292
+ align-self: center;
293
+ margin-left: 0;
294
+ padding: 0 3px 0 7px;
295
+ gap: 5px;
296
+ font-size: 8px;
297
+ letter-spacing: 0.02em;
298
+ }
299
+
300
+ .split-pane-header .session-full-access-track {
301
+ width: 16px;
302
+ height: 9px;
303
+ }
304
+
305
+ .split-pane-header .session-full-access-track > span {
306
+ top: 1.5px;
307
+ left: 1.5px;
308
+ width: 6px;
309
+ height: 6px;
310
+ }
311
+
312
+ .split-pane-header .split-pane-full-access.active .session-full-access-track > span { transform: translateX(7px); }
313
+
314
+ @media (max-width: 900px) {
315
+ .split-pane-header .session-full-access-label { display: none; }
316
+ .split-pane-header .split-pane-full-access { padding-left: 3px; }
317
+ }
318
+
319
+ .split-pane-close {
320
+ width: 20px;
321
+ height: 20px;
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ border: 0;
326
+ border-radius: 4px;
327
+ background: transparent;
328
+ color: var(--text-dimmer);
329
+ cursor: pointer;
330
+ }
331
+
332
+ .split-pane-close:hover {
333
+ background: var(--bg-alt);
334
+ color: var(--text);
335
+ }
336
+
337
+ .split-pane-close svg {
338
+ width: 12px;
339
+ height: 12px;
340
+ }
341
+
342
+ .split-pane-frame {
343
+ flex: 1;
344
+ width: 100%;
345
+ min-height: 0;
346
+ border: 0;
347
+ background: var(--bg);
348
+ }
349
+
350
+ /* Prevent an iframe from taking the pointer midway through a note gesture. */
351
+ body.sticky-note-interacting .split-pane-frame {
352
+ pointer-events: none;
353
+ }
354
+
355
+ /* Arc-style drop preview: while a session is dragged over the app, hovering
356
+ one half folds the live app into the other half and a ghost pane marks
357
+ exactly where the dragged session will land. Drop keeps that layout, so
358
+ the preview and the result match 1:1. */
359
+
360
+ .split-drop-overlay {
361
+ position: absolute;
362
+ inset: 0;
363
+ z-index: 10050;
364
+ display: none;
365
+ background: rgba(var(--overlay-rgb), 0.05);
366
+ pointer-events: none;
367
+ }
368
+
369
+ .split-drop-overlay.visible {
370
+ display: block;
371
+ pointer-events: auto;
372
+ }
373
+
374
+ #main-panels.split-drag-active #app {
375
+ transition: margin 0.18s ease;
376
+ }
377
+
378
+ #main-panels.split-preview-left #app {
379
+ margin-left: 50%;
380
+ }
381
+
382
+ #main-panels.split-preview-right #app {
383
+ margin-right: 50%;
384
+ }
385
+
386
+ .split-drop-ghost {
387
+ position: absolute;
388
+ top: 10px;
389
+ bottom: 10px;
390
+ left: 10px;
391
+ width: calc(50% - 16px);
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ border: 2px dashed var(--accent);
396
+ border-radius: 10px;
397
+ background: color-mix(in srgb, var(--accent) 10%, var(--bg));
398
+ color: var(--text);
399
+ opacity: 0;
400
+ pointer-events: none;
401
+ transition: left 0.18s ease, opacity 0.12s ease;
402
+ }
403
+
404
+ #main-panels.split-preview-left .split-drop-ghost {
405
+ opacity: 1;
406
+ }
407
+
408
+ #main-panels.split-preview-right .split-drop-ghost {
409
+ left: calc(50% + 6px);
410
+ opacity: 1;
411
+ }
412
+
413
+ .split-drop-ghost-title {
414
+ max-width: 80%;
415
+ overflow: hidden;
416
+ text-overflow: ellipsis;
417
+ white-space: nowrap;
418
+ font-size: 13px;
419
+ font-weight: 700;
420
+ }
@@ -873,6 +873,46 @@
873
873
  font-weight: 600;
874
874
  }
875
875
 
876
+ .split-group-text {
877
+ display: flex;
878
+ align-items: center;
879
+ }
880
+
881
+ .split-group-glyph {
882
+ width: 14px;
883
+ margin-right: 4px;
884
+ display: inline-flex;
885
+ align-items: center;
886
+ color: var(--text-dimmer);
887
+ flex-shrink: 0;
888
+ }
889
+
890
+ .split-group-glyph svg {
891
+ width: 13px;
892
+ height: 13px;
893
+ }
894
+
895
+ .split-group-vendors {
896
+ display: inline-flex;
897
+ align-items: center;
898
+ margin-right: 6px;
899
+ flex-shrink: 0;
900
+ }
901
+
902
+ .split-group-vendors .session-vendor-icon {
903
+ margin-right: 0;
904
+ }
905
+
906
+ .split-group-vendors .session-vendor-icon + .session-vendor-icon {
907
+ margin-left: -3px;
908
+ }
909
+
910
+ .split-group-name {
911
+ overflow: hidden;
912
+ text-overflow: ellipsis;
913
+ white-space: nowrap;
914
+ }
915
+
876
916
  .session-item.drag-over-above::before,
877
917
  .session-item.drag-over-below::after {
878
918
  content: "";
@@ -122,6 +122,7 @@
122
122
  white-space: nowrap;
123
123
  color: transparent;
124
124
  pointer-events: auto;
125
+ touch-action: none;
125
126
  }
126
127
 
127
128
  .sticky-note-spacer:active { cursor: grabbing; }
@@ -335,6 +336,7 @@
335
336
  cursor: nwse-resize;
336
337
  opacity: 0;
337
338
  transition: opacity 0.15s;
339
+ touch-action: none;
338
340
  }
339
341
 
340
342
  .sticky-note:hover .sticky-note-resize { opacity: 0.3; }
@@ -370,7 +370,11 @@
370
370
  <button id="hamburger-btn" aria-label="Toggle sidebar" title="Toggle sidebar"><i data-lucide="menu"></i></button>
371
371
  <span class="header-title" id="header-title">Connecting...</span>
372
372
  <button id="header-info-btn" type="button" title="Session info"><i data-lucide="info"></i></button>
373
+ <button id="header-full-access-btn" type="button" class="session-full-access hidden" role="switch" aria-checked="false" title="Skip Permissions is off — permission prompts are on">
374
+ <span class="session-full-access-label">Skip Permissions</span><span class="session-full-access-track" aria-hidden="true"><span></span></span>
375
+ </button>
373
376
  <button id="header-rename-btn" type="button" title="Rename session"><i data-lucide="pencil"></i></button>
377
+ <button id="header-add-worker-btn" type="button" title="Add Worker — this session becomes the Driver of a pair"><i data-lucide="user-plus"></i></button>
374
378
  <button type="button" id="header-tui-close-btn" class="header-tui-close-btn hidden" title="Close terminal (keeps history, resume anytime)" aria-label="Close terminal"><i data-lucide="power"></i></button>
375
379
  <div id="mate-mobile-title" class="mate-mobile-title hidden">
376
380
  <img id="mate-mobile-avatar" class="mate-mobile-avatar" alt="">
@@ -144,6 +144,12 @@ export function connect() {
144
144
  reconnectDelay = 1000;
145
145
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
146
146
 
147
+ // A pane pin is one-shot per WebSocket, not per page lifetime. The server
148
+ // intentionally does not restore pane presence after a daemon restart.
149
+ if (store.get('paneMode') && store.get('paneSessionId')) {
150
+ store.set({ panePinPending: true });
151
+ }
152
+
147
153
  // Wrap ws.send to blink LED on outgoing traffic
148
154
  var currentWs = getWs();
149
155
  var _origSend = currentWs.send.bind(currentWs);
@@ -18,6 +18,7 @@ var sessionIoTimers = {};
18
18
  var crossProjectBlinkTimer = null;
19
19
 
20
20
  export function initFavicon() {
21
+ if (store.get('paneMode')) return;
21
22
  faviconLink = document.querySelector('link[rel="icon"]');
22
23
  faviconCanvas = document.createElement("canvas");
23
24
  faviconCanvas.width = 32;
@@ -38,6 +39,7 @@ export function initFavicon() {
38
39
  }
39
40
 
40
41
  export function updateFavicon(bgColor) {
42
+ if (store.get('paneMode')) return;
41
43
  if (!faviconLink) return;
42
44
  if (!bgColor) {
43
45
  if (faviconOrigHref) { faviconLink.href = faviconOrigHref; faviconOrigHref = null; }
@@ -59,6 +61,7 @@ export function updateFavicon(bgColor) {
59
61
  }
60
62
 
61
63
  export function drawFaviconAnimFrame() {
64
+ if (store.get('paneMode')) return;
62
65
  if (!faviconImgReady) return;
63
66
  var S = 32;
64
67
  var bands = BAND_COLORS.length;
@@ -160,6 +163,7 @@ export function updateCrossProjectBlink() {
160
163
  }
161
164
 
162
165
  export function startUrgentBlink() {
166
+ if (store.get('paneMode')) return;
163
167
  if (urgentBlinkTimer) return;
164
168
  savedTitle = document.title;
165
169
  if (!faviconOrigHref && faviconLink) faviconOrigHref = faviconLink.href;
@@ -175,6 +179,7 @@ export function startUrgentBlink() {
175
179
  }
176
180
 
177
181
  export function stopUrgentBlink() {
182
+ if (store.get('paneMode')) return;
178
183
  if (!urgentBlinkTimer) return;
179
184
  clearInterval(urgentBlinkTimer);
180
185
  clearInterval(urgentTitleTimer);
@@ -11,6 +11,9 @@ import { processMessage } from './app-messages.js';
11
11
  import { saveToolState, resetToolState, restoreToolState } from './tools.js';
12
12
  import { getSessionUsage, setSessionUsage, getContextData, setContextData, updateContextPanel, updateUsagePanel } from './app-panels.js';
13
13
  import { onHistoryPrepended as onSessionSearchHistoryPrepended } from './session-search.js';
14
+ import { getCachedSessions } from './sidebar-sessions.js';
15
+ import { openPairDialog } from './split-pair-ui.js';
16
+ import { showConfirm } from './app-misc.js';
14
17
 
15
18
  // --- Module-owned state ---
16
19
  var sessionInfoPopover = null;
@@ -20,6 +23,66 @@ export function initHeader() {
20
23
  var headerRenameBtn = document.getElementById("header-rename-btn");
21
24
  var headerTitleEl = document.getElementById("header-title");
22
25
  var headerInfoBtn = document.getElementById("header-info-btn");
26
+ var headerAddWorkerBtn = document.getElementById("header-add-worker-btn");
27
+
28
+ // --- Add Worker: the current session becomes the Driver of a new pair ---
29
+ if (headerAddWorkerBtn) {
30
+ headerAddWorkerBtn.addEventListener("click", function () {
31
+ var sessionId = store.get('activeSessionId');
32
+ if (!sessionId || store.get('splitPanes') || store.get('dmMode')) return;
33
+ openPairDialog({
34
+ sessionId: sessionId,
35
+ title: headerTitleEl ? headerTitleEl.textContent : "",
36
+ vendor: store.get('currentVendor') || "claude",
37
+ });
38
+ });
39
+ // A split shows the GROUP in the title bar; adding a worker there makes
40
+ // no sense (the group already has two members), so hide the button.
41
+ store.subscribe(function (state, prev) {
42
+ if (state.splitPanes !== prev.splitPanes) {
43
+ headerAddWorkerBtn.classList.toggle("hidden", !!state.splitPanes);
44
+ }
45
+ });
46
+ }
47
+ var headerFullAccessBtn = document.getElementById("header-full-access-btn");
48
+
49
+ function updateFullAccessButton() {
50
+ if (!headerFullAccessBtn) return;
51
+ var s = store.snap();
52
+ var visible = !s.dmMode && !s.splitPanes && !s.skipPermsEnabled && s.activeSessionMode === "gui" && !!s.activeSessionId;
53
+ var enabled = !!s.sessionFullAccess;
54
+ headerFullAccessBtn.classList.toggle("hidden", !visible);
55
+ headerFullAccessBtn.classList.toggle("active", enabled);
56
+ headerFullAccessBtn.setAttribute("aria-checked", enabled ? "true" : "false");
57
+ headerFullAccessBtn.title = enabled ? "Skip Permissions is on — click to restore permission prompts" : "Skip Permissions is off — permission prompts are on";
58
+ }
59
+
60
+ function sendFullAccess(enabled) {
61
+ var ws = getWs();
62
+ var id = store.get('activeSessionId');
63
+ if (ws && ws.readyState === 1 && id) {
64
+ ws.send(JSON.stringify({ type: "set_session_full_access", id: id, enabled: enabled }));
65
+ }
66
+ }
67
+
68
+ if (headerFullAccessBtn) {
69
+ headerFullAccessBtn.addEventListener("click", function () {
70
+ if (store.get('sessionFullAccess')) {
71
+ sendFullAccess(false);
72
+ return;
73
+ }
74
+ showConfirm("Skip permission prompts for this session? Tool requests will be approved automatically.", function () {
75
+ sendFullAccess(true);
76
+ }, "Skip Permissions", true);
77
+ });
78
+ store.subscribe(function (state, prev) {
79
+ if (state.sessionFullAccess !== prev.sessionFullAccess || state.activeSessionId !== prev.activeSessionId ||
80
+ state.activeSessionMode !== prev.activeSessionMode ||
81
+ state.dmMode !== prev.dmMode || state.splitPanes !== prev.splitPanes ||
82
+ state.skipPermsEnabled !== prev.skipPermsEnabled) updateFullAccessButton();
83
+ });
84
+ updateFullAccessButton();
85
+ }
23
86
 
24
87
  // --- Header session rename ---
25
88
  if (headerRenameBtn) {
@@ -40,8 +103,18 @@ export function initHeader() {
40
103
  var newTitle = input.value.trim();
41
104
  var ws = getWs();
42
105
  if (newTitle && newTitle !== currentText && ws && ws.readyState === 1) {
43
- ws.send(JSON.stringify({ type: "rename_session", id: store.get('activeSessionId'), title: newTitle }));
44
- headerTitleEl.textContent = newTitle;
106
+ // With a split group open the header shows the GROUP name, so the
107
+ // pencil must rename the group, not the parent's last active session.
108
+ var split = store.get('splitPanes');
109
+ if (split) {
110
+ if (split.groupId) {
111
+ ws.send(JSON.stringify({ type: "split_group_rename", id: split.groupId, name: newTitle }));
112
+ headerTitleEl.textContent = newTitle;
113
+ }
114
+ } else {
115
+ ws.send(JSON.stringify({ type: "rename_session", id: store.get('activeSessionId'), title: newTitle }));
116
+ headerTitleEl.textContent = newTitle;
117
+ }
45
118
  }
46
119
  input.remove();
47
120
  headerTitleEl.style.display = "";
@@ -89,10 +162,32 @@ export function initHeader() {
89
162
  pop.appendChild(row);
90
163
  }
91
164
 
92
- var s = store.snap();
93
- if (s.cliSessionId) addRow("Session ID", s.cliSessionId);
94
- if (s.activeSessionId) addRow("Local ID", s.activeSessionId);
95
- if (s.cliSessionId) addRow("Resume", "claude --resume " + s.cliSessionId);
165
+ var split = store.get('splitPanes');
166
+ if (split && split.panes) {
167
+ // A split group is open: single-session info makes no sense here.
168
+ // Show one section per member instead.
169
+ var cached = getCachedSessions() || [];
170
+ for (var pi = 0; pi < split.panes.length; pi++) {
171
+ var member = null;
172
+ for (var ci = 0; ci < cached.length; ci++) {
173
+ if (cached[ci].id === split.panes[pi].sessionId) { member = cached[ci]; break; }
174
+ }
175
+ var heading = document.createElement("div");
176
+ heading.className = "info-section-title";
177
+ heading.textContent = (member && member.title) || split.panes[pi].title || ("Session " + split.panes[pi].sessionId);
178
+ pop.appendChild(heading);
179
+ addRow("Local ID", split.panes[pi].sessionId);
180
+ if (member && member.cliSessionId) {
181
+ addRow("Session ID", member.cliSessionId);
182
+ addRow("Resume", "claude --resume " + member.cliSessionId);
183
+ }
184
+ }
185
+ } else {
186
+ var s = store.snap();
187
+ if (s.cliSessionId) addRow("Session ID", s.cliSessionId);
188
+ if (s.activeSessionId) addRow("Local ID", s.activeSessionId);
189
+ if (s.cliSessionId) addRow("Resume", "claude --resume " + s.cliSessionId);
190
+ }
96
191
 
97
192
  document.body.appendChild(pop);
98
193
  sessionInfoPopover = pop;