clay-server 2.47.0-beta.5 → 3.0.0-beta.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.
Files changed (58) hide show
  1. package/lib/daemon.js +6 -27
  2. package/lib/project-connection.js +21 -8
  3. package/lib/project-http.js +5 -2
  4. package/lib/project-session-pair.js +312 -0
  5. package/lib/project-session-spawn.js +8 -4
  6. package/lib/project-sessions.js +108 -10
  7. package/lib/project-user-message.js +18 -8
  8. package/lib/project.js +33 -0
  9. package/lib/public/app.js +24 -1
  10. package/lib/public/css/icon-strip.css +0 -190
  11. package/lib/public/css/menus.css +83 -1
  12. package/lib/public/css/messages.css +14 -0
  13. package/lib/public/css/mobile-nav.css +4 -0
  14. package/lib/public/css/pane.css +420 -0
  15. package/lib/public/css/sidebar.css +40 -0
  16. package/lib/public/css/sticky-notes.css +2 -0
  17. package/lib/public/css/title-bar.css +169 -0
  18. package/lib/public/index.html +10 -0
  19. package/lib/public/modules/app-connection.js +6 -0
  20. package/lib/public/modules/app-favicon.js +6 -10
  21. package/lib/public/modules/app-header.js +101 -6
  22. package/lib/public/modules/app-home-hub.js +2 -0
  23. package/lib/public/modules/app-messages.js +63 -13
  24. package/lib/public/modules/app-panels.js +49 -27
  25. package/lib/public/modules/app-projects.js +43 -30
  26. package/lib/public/modules/app-rendering.js +11 -4
  27. package/lib/public/modules/branch-switcher.js +178 -0
  28. package/lib/public/modules/dom-refs.js +0 -1
  29. package/lib/public/modules/pane-bridge.js +41 -0
  30. package/lib/public/modules/pane-session.js +18 -0
  31. package/lib/public/modules/project-switcher.js +2 -1
  32. package/lib/public/modules/sidebar-mobile.js +19 -6
  33. package/lib/public/modules/sidebar-projects.js +63 -236
  34. package/lib/public/modules/sidebar-sessions.js +175 -1
  35. package/lib/public/modules/sidebar.js +2 -0
  36. package/lib/public/modules/split-group-helpers.js +18 -0
  37. package/lib/public/modules/split-pair-ui.js +324 -0
  38. package/lib/public/modules/split-view.js +499 -0
  39. package/lib/public/modules/sticky-notes.js +32 -91
  40. package/lib/public/modules/worktree-family.js +60 -0
  41. package/lib/public/style.css +1 -0
  42. package/lib/sdk-bridge.js +111 -20
  43. package/lib/sdk-message-processor.js +2 -2
  44. package/lib/server.js +24 -4
  45. package/lib/session-hygiene.js +100 -0
  46. package/lib/session-pair-mcp-server.js +28 -0
  47. package/lib/session-split-groups.js +298 -0
  48. package/lib/sessions.js +163 -34
  49. package/lib/ws-request.js +14 -0
  50. package/lib/ws-schema.js +13 -0
  51. package/lib/yoke/adapters/claude.js +32 -0
  52. package/lib/yoke/adapters/codex.js +100 -77
  53. package/lib/yoke/adapters/kiro.js +34 -35
  54. package/lib/yoke/codex-app-server.js +56 -6
  55. package/lib/yoke/index.js +1 -0
  56. package/lib/yoke/skill-discovery.js +196 -0
  57. package/lib/yoke/vendor-registry.js +34 -0
  58. package/package.json +1 -1
@@ -272,6 +272,175 @@
272
272
  transform: rotate(180deg);
273
273
  }
274
274
 
275
+ .branch-chip {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ gap: 4px;
279
+ min-width: 0;
280
+ max-width: 132px;
281
+ /* Optical alignment: the pill reads slightly high next to the 17px/800
282
+ project name, so nudge it down within the centered flex row. */
283
+ margin-top: 2px;
284
+ padding: 4px 7px;
285
+ border: 1px solid var(--border);
286
+ border-radius: 999px;
287
+ background: var(--bg-alt);
288
+ color: var(--text-secondary);
289
+ font: 600 11px/1.2 "Pretendard", system-ui, sans-serif;
290
+ cursor: pointer;
291
+ flex-shrink: 1;
292
+ }
293
+
294
+ .branch-chip.hidden,
295
+ .branch-chip-menu.hidden {
296
+ display: none;
297
+ }
298
+
299
+ .branch-chip:hover,
300
+ .branch-chip.open {
301
+ border-color: var(--text-dimmer);
302
+ color: var(--text);
303
+ }
304
+
305
+ .branch-chip > svg {
306
+ width: 12px;
307
+ height: 12px;
308
+ flex-shrink: 0;
309
+ }
310
+
311
+ #branch-chip-label {
312
+ overflow: hidden;
313
+ text-overflow: ellipsis;
314
+ white-space: nowrap;
315
+ }
316
+
317
+ .branch-chip-chevron {
318
+ transition: transform 0.15s;
319
+ }
320
+
321
+ .branch-chip.open .branch-chip-chevron {
322
+ transform: rotate(180deg);
323
+ }
324
+
325
+ .branch-chip-menu {
326
+ position: fixed;
327
+ z-index: 10020;
328
+ width: 250px;
329
+ padding: 6px;
330
+ border: 1px solid var(--border);
331
+ border-radius: 10px;
332
+ background: var(--bg);
333
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
334
+ }
335
+
336
+ .branch-chip-row {
337
+ display: flex;
338
+ align-items: center;
339
+ border-radius: 7px;
340
+ }
341
+
342
+ .branch-chip-row:hover {
343
+ background: var(--bg-alt);
344
+ }
345
+
346
+ .branch-chip-row-main,
347
+ .branch-chip-row-action,
348
+ .branch-chip-new {
349
+ border: 0;
350
+ background: transparent;
351
+ color: var(--text-secondary);
352
+ font-family: inherit;
353
+ cursor: pointer;
354
+ }
355
+
356
+ .branch-chip-row-main {
357
+ min-width: 0;
358
+ flex: 1;
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 7px;
362
+ padding: 8px;
363
+ text-align: left;
364
+ }
365
+
366
+ .branch-chip-row-main:disabled {
367
+ opacity: 0.38;
368
+ cursor: not-allowed;
369
+ }
370
+
371
+ .branch-chip-row-main svg,
372
+ .branch-chip-row-action svg,
373
+ .branch-chip-new svg {
374
+ width: 14px;
375
+ height: 14px;
376
+ flex-shrink: 0;
377
+ }
378
+
379
+ .branch-chip-row-label {
380
+ overflow: hidden;
381
+ text-overflow: ellipsis;
382
+ white-space: nowrap;
383
+ }
384
+
385
+ .branch-chip-row.current .branch-chip-row-label {
386
+ color: var(--text);
387
+ font-weight: 700;
388
+ }
389
+
390
+ .branch-chip-row-main > .lucide-check {
391
+ margin-left: auto;
392
+ color: var(--accent);
393
+ }
394
+
395
+ .branch-chip-processing {
396
+ width: 7px;
397
+ height: 7px;
398
+ border-radius: 50%;
399
+ background: var(--success, #23a55a);
400
+ flex-shrink: 0;
401
+ }
402
+
403
+ .branch-chip-pending {
404
+ min-width: 17px;
405
+ padding: 1px 4px;
406
+ border-radius: 8px;
407
+ background: var(--danger, #e74c3c);
408
+ color: #fff;
409
+ font-size: 9px;
410
+ text-align: center;
411
+ }
412
+
413
+ .branch-chip-row-action {
414
+ display: flex;
415
+ padding: 7px;
416
+ opacity: 0;
417
+ }
418
+
419
+ .branch-chip-row:hover .branch-chip-row-action,
420
+ .branch-chip-row-action:focus-visible {
421
+ opacity: 0.7;
422
+ }
423
+
424
+ .branch-chip-row-action:hover {
425
+ color: var(--danger, #e74c3c);
426
+ opacity: 1;
427
+ }
428
+
429
+ .branch-chip-new {
430
+ width: 100%;
431
+ display: flex;
432
+ align-items: center;
433
+ gap: 7px;
434
+ margin-top: 5px;
435
+ padding: 9px 8px 7px;
436
+ border-top: 1px solid var(--border-subtle);
437
+ text-align: left;
438
+ }
439
+
440
+ .branch-chip-new:hover {
441
+ color: var(--text);
442
+ }
443
+
275
444
  .project-rename-input {
276
445
  width: 100%;
277
446
  font-family: "Pretendard", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
@@ -210,6 +210,12 @@
210
210
  <span class="title-bar-project-name" id="title-bar-project-name"></span>
211
211
  <i data-lucide="chevron-down" class="title-bar-chevron"></i>
212
212
  </button>
213
+ <button id="branch-chip" class="branch-chip hidden" type="button" aria-label="Switch branch" aria-expanded="false" aria-haspopup="menu">
214
+ <i data-lucide="git-branch"></i>
215
+ <span id="branch-chip-label"></span>
216
+ <i data-lucide="chevron-down" class="branch-chip-chevron"></i>
217
+ </button>
218
+ <div id="branch-chip-menu" class="branch-chip-menu hidden"></div>
213
219
  <span id="sidebar-presence" class="sidebar-presence"></span>
214
220
  <button id="sidebar-toggle-btn" class="sidebar-collapse-btn" title="Collapse sidebar"><i data-lucide="panel-left-close"></i></button>
215
221
  </div>
@@ -364,7 +370,11 @@
364
370
  <button id="hamburger-btn" aria-label="Toggle sidebar" title="Toggle sidebar"><i data-lucide="menu"></i></button>
365
371
  <span class="header-title" id="header-title">Connecting...</span>
366
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>
367
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>
368
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>
369
379
  <div id="mate-mobile-title" class="mate-mobile-title hidden">
370
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;
@@ -106,14 +109,6 @@ export function blinkIO() {
106
109
  var sessionDot = document.querySelector(".session-item.active .session-processing") ||
107
110
  document.querySelector(".mate-session-item.active .session-processing");
108
111
  if (sessionDot) sessionDot.classList.add("io");
109
- // If active project is a worktree, also blink the parent project dot
110
- var activeWt = document.querySelector("#icon-strip-projects .icon-strip-wt-item.active");
111
- var parentDot = null;
112
- if (activeWt) {
113
- var group = activeWt.closest(".icon-strip-group");
114
- if (group) parentDot = group.querySelector(".folder-header .icon-strip-status");
115
- if (parentDot) parentDot.classList.add("io");
116
- }
117
112
  // Mobile chat chip dot + mobile session dot
118
113
  var mobileChipDot = null;
119
114
  var _s = store.snap();
@@ -132,7 +127,6 @@ export function blinkIO() {
132
127
  var sd = document.querySelector(".session-item.active .session-processing.io") ||
133
128
  document.querySelector(".mate-session-item.active .session-processing.io");
134
129
  if (sd) sd.classList.remove("io");
135
- if (parentDot) parentDot.classList.remove("io");
136
130
  if (mobileChipDot) mobileChipDot.classList.remove("io");
137
131
  if (mobileSessionDot) mobileSessionDot.classList.remove("io");
138
132
  }, 80);
@@ -152,7 +146,7 @@ export function blinkSessionDot(sessionId) {
152
146
  export function updateCrossProjectBlink() {
153
147
  if (crossProjectBlinkTimer) { clearTimeout(crossProjectBlinkTimer); crossProjectBlinkTimer = null; }
154
148
  function doBlink() {
155
- var dots = document.querySelectorAll("#icon-strip-projects .icon-strip-item:not(.active) .icon-strip-status.processing, #icon-strip-projects .icon-strip-wt-item:not(.active) .icon-strip-status.processing, #icon-strip-users .icon-strip-mate:not(.active) .icon-strip-status.processing");
149
+ var dots = document.querySelectorAll("#icon-strip-projects .icon-strip-item:not(.active) .icon-strip-status.processing, #icon-strip-users .icon-strip-mate:not(.active) .icon-strip-status.processing");
156
150
  // Also blink mobile chat chip dots (same icon-strip-status class inside chips)
157
151
  var mobileDots = document.querySelectorAll(".mobile-chat-chip .icon-strip-status.processing");
158
152
  var allDots = [];
@@ -169,6 +163,7 @@ export function updateCrossProjectBlink() {
169
163
  }
170
164
 
171
165
  export function startUrgentBlink() {
166
+ if (store.get('paneMode')) return;
172
167
  if (urgentBlinkTimer) return;
173
168
  savedTitle = document.title;
174
169
  if (!faviconOrigHref && faviconLink) faviconOrigHref = faviconLink.href;
@@ -184,6 +179,7 @@ export function startUrgentBlink() {
184
179
  }
185
180
 
186
181
  export function stopUrgentBlink() {
182
+ if (store.get('paneMode')) return;
187
183
  if (!urgentBlinkTimer) return;
188
184
  clearInterval(urgentBlinkTimer);
189
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;
@@ -619,6 +619,7 @@ export function showHomeHub() {
619
619
  // anywhere via the persistent FAB (#clay-fab), not embedded here.
620
620
  if (store.get('dmMode')) exitDmMode();
621
621
  homeHubVisible = true;
622
+ store.set({ homeHubVisible: true });
622
623
  homeHub.classList.remove("hidden");
623
624
  // Show close button only if there's a project to return to
624
625
  if (hubCloseBtn) {
@@ -652,6 +653,7 @@ export function showHomeHub() {
652
653
  export function hideHomeHub() {
653
654
  if (!homeHubVisible) return;
654
655
  homeHubVisible = false;
656
+ store.set({ homeHubVisible: false });
655
657
  homeHub.classList.add("hidden");
656
658
  stopTipRotation();
657
659
  var mobileHome = document.getElementById("mobile-home-btn");
@@ -19,6 +19,8 @@ import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js
19
19
  import { renderMemoryList } from './mate-memory.js';
20
20
  import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
21
21
  import { handleFindInSessionResults } from './session-search.js';
22
+ import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
23
+ import { showPairDialog, handlePairCreated, handleSplitDelegation } from './split-pair-ui.js';
22
24
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
23
25
  import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
24
26
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
@@ -46,8 +48,9 @@ import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, update
46
48
  import { setStatus } from './app-connection.js';
47
49
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
48
50
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
51
+ import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
49
52
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
50
- import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
53
+ import { updateProjectList, updateTitleBarProjectIdentity, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
51
54
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
52
55
  import { hideHomeHub, handleHubSchedules } from './app-home-hub.js';
53
56
  import { openDm, enterDmMode, exitDmMode, handleMateCreatedInApp, updateMateIconStatus, appendDmMessage, showDmTypingIndicator, buildMateInterviewPrompt } from './app-dm.js';
@@ -82,8 +85,7 @@ export function processMessage(msg) {
82
85
  // Override title bar with mate name and re-apply color
83
86
  var _mdn = (store.get('dmTargetUser').displayName || "New Mate");
84
87
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
85
- var _tbpn = document.getElementById("title-bar-project-name");
86
- if (_tbpn) _tbpn.textContent = _mdn;
88
+ updateTitleBarProjectIdentity(_mdn, null);
87
89
  var _mc2 = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
88
90
  var _tbc2 = document.querySelector(".title-bar-content");
89
91
  if (_tbc2) { _tbc2.style.background = _mc2; _tbc2.classList.add("mate-dm-active"); }
@@ -296,8 +298,7 @@ export function processMessage(msg) {
296
298
  if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
297
299
  var _mateDN = store.get('dmTargetUser').displayName || "New Mate";
298
300
  headerTitleEl.textContent = _mateDN;
299
- var tbProjectName = document.getElementById("title-bar-project-name");
300
- if (tbProjectName) tbProjectName.textContent = _mateDN;
301
+ updateTitleBarProjectIdentity(_mateDN, null);
301
302
  // Re-apply mate title bar styling (may be lost during project switch)
302
303
  var _mc = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
303
304
  var _tbc = document.querySelector(".title-bar-content");
@@ -305,8 +306,7 @@ export function processMessage(msg) {
305
306
  document.body.classList.add("mate-dm-active");
306
307
  } else {
307
308
  headerTitleEl.textContent = store.get('projectName');
308
- var tbProjectName = document.getElementById("title-bar-project-name");
309
- if (tbProjectName) tbProjectName.textContent = msg.title || store.get('projectName');
309
+ updateTitleBarProjectIdentity(msg.title || store.get('projectName'), null);
310
310
  }
311
311
  updatePageTitle();
312
312
  if (msg.version) {
@@ -552,9 +552,42 @@ export function processMessage(msg) {
552
552
  case "session_list":
553
553
  renderMateSessionList(msg.sessions || []);
554
554
  renderSessionList(msg.sessions || []);
555
+ syncPaneTitles();
556
+ var panePin = resolvePaneSession(store.get('paneMode'), store.get('panePinPending'), store.get('paneSessionId'), msg.sessions);
557
+ if (panePin.consumed) {
558
+ store.set({ panePinPending: false });
559
+ if (panePin.sessionId && getWs() && getWs().readyState === 1) {
560
+ getWs().send(JSON.stringify({ type: "switch_session", id: panePin.sessionId }));
561
+ }
562
+ }
555
563
  handlePaletteSessionSwitch();
556
564
  break;
557
565
 
566
+ case "split_groups":
567
+ store.set({ splitGroups: msg.groups || [] });
568
+ renderSessionList(null);
569
+ maybeRestoreSplitGroup();
570
+ syncPaneTitles();
571
+ break;
572
+
573
+ case "split_group_result":
574
+ if (!msg.ok && msg.error) showToast(msg.error, "error");
575
+ break;
576
+
577
+ case "pair_session_options":
578
+ showPairDialog(msg);
579
+ break;
580
+
581
+ case "pair_session_created":
582
+ var createdPairGroup = handlePairCreated(msg);
583
+ if (createdPairGroup) openGroup(createdPairGroup);
584
+ break;
585
+
586
+ case "split_delegation":
587
+ handleSplitDelegation(msg);
588
+ syncPaneTitles();
589
+ break;
590
+
558
591
  case "session_presence":
559
592
  updateSessionPresence(msg.presence || {});
560
593
  break;
@@ -614,11 +647,14 @@ export function processMessage(msg) {
614
647
  var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
615
648
  ? msg.runtimeTerminalId
616
649
  : (typeof msg.terminalId === "number" ? msg.terminalId : null);
617
- store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory });
650
+ store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || store.get('currentModel'), currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
618
651
  // TUI sessions swap the chat UI for an embedded xterm running
619
652
  // `claude` inside a real PTY. Mount or tear down before the rest of
620
653
  // the chat-side bookkeeping runs so we don't waste work on hidden DOM.
621
- if (_effectiveMode === "tui" && typeof _effectiveTerminalId === "number") {
654
+ // With a split open the native app is hidden behind the pane iframes,
655
+ // but the TUI host is position:fixed on body -- attaching it here
656
+ // would overlay the split. Members render inside their own panes.
657
+ if (_effectiveMode === "tui" && typeof _effectiveTerminalId === "number" && !store.get('splitPanes')) {
622
658
  attachTuiView(_effectiveTerminalId, msg.id, msg.vendor || null);
623
659
  } else {
624
660
  detachTuiView();
@@ -627,9 +663,10 @@ export function processMessage(msg) {
627
663
  // (the composer is hidden; clicking Resume spawns claude --resume).
628
664
  setTuiSuspendedView(!!msg.tuiSuspended, msg.id);
629
665
  if (msg.vendor) {
630
- if (!store.get('vendorSelectionLocked') || msg.hasHistory) {
631
- store.set({ currentVendor: msg.vendor });
632
- }
666
+ // session_switched is authoritative. The lock rejects stale
667
+ // model_info responses; it must not block an intentional switch to
668
+ // a blank session owned by another vendor.
669
+ store.set({ currentVendor: resolveSwitchedVendor(store.get('currentVendor'), msg.vendor) });
633
670
  // Sessions with history have their vendor structurally bound to
634
671
  // the session: lock so a late-arriving default-adapter model_info
635
672
  // can't flip the UI back. Previously this branch unlocked, which
@@ -706,6 +743,19 @@ export function processMessage(msg) {
706
743
  if (!("ontouchstart" in window)) {
707
744
  inputEl.focus();
708
745
  }
746
+ // Reload survival: a restored active session that is a split-group
747
+ // member reopens its group (no-op when a split is already open).
748
+ maybeRestoreSplitGroup();
749
+ break;
750
+
751
+ case "session_full_access_changed":
752
+ if (msg.id === store.get('activeSessionId')) {
753
+ store.set({ sessionFullAccess: !!msg.enabled, currentMode: msg.permissionMode || "default" });
754
+ }
755
+ break;
756
+
757
+ case "session_full_access_result":
758
+ if (!msg.ok && msg.error) showToast(msg.error, "error");
709
759
  break;
710
760
 
711
761
  case "session_id":
@@ -736,7 +786,7 @@ export function processMessage(msg) {
736
786
  renderPlanCard(msg.planContent);
737
787
  addUserMessage("Execute the following plan. Do NOT re-enter plan mode — just implement it step by step.", msg.images || null, msg.pastes || null, msg.from, msg.fromName);
738
788
  } else {
739
- addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName);
789
+ addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName, msg.delegated ? msg : null);
740
790
  }
741
791
  break;
742
792