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
@@ -6,6 +6,7 @@ import { escapeHtml, showToast } from "./utils.js";
6
6
  import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
8
  import { VENDOR_NAMES } from './app-rendering.js';
9
+ import { reportPaneContext } from './pane-bridge.js';
9
10
 
10
11
  // --- Module-owned state (not in store) ---
11
12
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
@@ -150,12 +151,30 @@ function modeDisplayName(value) {
150
151
  return value;
151
152
  }
152
153
 
153
- function effortDisplayName(value) {
154
+ export function effortDisplayName(value) {
154
155
  if (!value) return "";
155
156
  if (value === "xhigh") return "X-High";
156
157
  return value.charAt(0).toUpperCase() + value.slice(1);
157
158
  }
158
159
 
160
+ // Effort levels for an arbitrary vendor/model pair (pure; the pair dialog
161
+ // uses this for sessions other than the active one). Falls back from the
162
+ // model's own supportedEffortLevels to the vendor table.
163
+ export function effortLevelsFor(vendor, models, modelValue) {
164
+ var defaults = EFFORT_LEVELS_BY_VENDOR[vendor] || EFFORT_LEVELS;
165
+ var list = models || [];
166
+ for (var i = 0; i < list.length; i++) {
167
+ var value = typeof list[i] === "string" ? list[i] : (list[i] && (list[i].value || list[i].id)) || "";
168
+ if (value === modelValue) {
169
+ if (list[i].supportedEffortLevels && list[i].supportedEffortLevels.length > 0) {
170
+ return list[i].supportedEffortLevels;
171
+ }
172
+ return defaults;
173
+ }
174
+ }
175
+ return defaults;
176
+ }
177
+
159
178
  function thinkingDisplayName(value) {
160
179
  if (value === "disabled") return "Off";
161
180
  if (value === "adaptive") return "Adaptive";
@@ -179,22 +198,25 @@ function hasBeta(name) {
179
198
 
180
199
  function rebuildModelList() {
181
200
  if (!configModelList) return;
182
- // Picker visibility by vendor+mode:
183
- // Claude TUI -> shown, free (Claude TUI accepts mid-thread model swaps).
184
- // GUI (Claude SDK + Codex) -> shown, but locked once the session has
185
- // history. Both bind the model at session creation and
186
- // changing mid-thread breaks tool schemas / cache reuse,
187
- // so we allow the pick only before the first message.
188
- // sdk-bridge setModel stores into sm.currentModel when
189
- // there's no active queryInstance, so a pick made before
190
- // the first message takes effect on session start.
201
+ // Picker lock policy is capability-based (yoke midSessionModelSwitch):
202
+ // Claude -> free. The SDK supports mid-thread swaps on a live query
203
+ // (query.setModel: "Change the model used for subsequent
204
+ // responses"), and sdk-bridge already routes set_model to
205
+ // queryInstance.setModel. Cache re-priming on switch is a
206
+ // cost, not a breakage.
207
+ // Codex / Kiro -> locked once the session has history: their threads
208
+ // bind the model at creation and a mid-thread change would be
209
+ // silently ignored. Picks made before the first message still
210
+ // work everywhere (sdk-bridge stores into sm.currentModel when
211
+ // there's no active queryInstance).
191
212
  var modelSection = configModelList.parentElement;
192
213
  var s = store.snap();
193
214
  var vendor = s.currentVendor || "claude";
194
215
  if (modelSection) modelSection.style.display = "";
195
216
  configModelList.innerHTML = "";
196
217
 
197
- var lockedForGui = s.activeSessionMode === "gui" && !!s.sessionHasHistory;
218
+ var caps = s.vendorCapabilities || {};
219
+ var lockedForGui = s.activeSessionMode === "gui" && !!s.sessionHasHistory && !caps.midSessionModelSwitch;
198
220
 
199
221
  var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
200
222
  for (var i = 0; i < list.length; i++) {
@@ -210,7 +232,7 @@ function rebuildModelList() {
210
232
  btn.textContent = label;
211
233
  if (lockedForGui) {
212
234
  btn.disabled = true;
213
- btn.title = "Model is locked after the first message in a GUI session. Start a new session to change it.";
235
+ btn.title = "This vendor binds the model when the session starts. Start a new session to change it.";
214
236
  } else {
215
237
  btn.addEventListener("click", function () {
216
238
  var model = this.dataset.model;
@@ -228,7 +250,7 @@ function rebuildModelList() {
228
250
  if (lockedForGui) {
229
251
  var hint = document.createElement("div");
230
252
  hint.className = "config-model-hint";
231
- hint.textContent = "Locked after first message — start a new session to change.";
253
+ hint.textContent = "Locked after first message. Start a new session to change models.";
232
254
  configModelList.appendChild(hint);
233
255
  }
234
256
  }
@@ -471,6 +493,9 @@ export function initPanels() {
471
493
  }
472
494
  updateContextPanel();
473
495
  }
496
+ // Split opened/closed -> show or hide the title-bar context gauge
497
+ if (state.splitPanes !== prev.splitPanes) updateContextPanel();
498
+
474
499
  // Vendor changed -> switch model list and current model to match
475
500
  if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
476
501
  var ws = getWs();
@@ -670,18 +695,7 @@ export function getModelSupportsEffort() {
670
695
 
671
696
  export function getModelEffortLevels() {
672
697
  var s = store.snap();
673
- var vendor = s.currentVendor || "claude";
674
- var defaultLevels = EFFORT_LEVELS_BY_VENDOR[vendor] || EFFORT_LEVELS;
675
- if (!s.currentModels || s.currentModels.length === 0) return defaultLevels;
676
- for (var i = 0; i < s.currentModels.length; i++) {
677
- if (s.currentModels[i].value === s.currentModel) {
678
- if (s.currentModels[i].supportedEffortLevels && s.currentModels[i].supportedEffortLevels.length > 0) {
679
- return s.currentModels[i].supportedEffortLevels;
680
- }
681
- return defaultLevels;
682
- }
683
- }
684
- return defaultLevels;
698
+ return effortLevelsFor(s.currentVendor || "claude", s.currentModels || [], s.currentModel);
685
699
  }
686
700
 
687
701
  // --- Usage panel ---
@@ -807,6 +821,7 @@ export function updateContextPanel() {
807
821
  var win = contextData.contextWindow;
808
822
  var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
809
823
  var cls = contextPctClass(pct);
824
+ reportPaneContext({ pct: pct, used: used, win: win, cls: cls, model: contextData.model, cost: contextData.cost });
810
825
  // Panel bar
811
826
  contextBarFill.style.width = pct.toFixed(1) + "%";
812
827
  contextBarFill.className = "context-bar-fill" + cls;
@@ -819,8 +834,15 @@ export function updateContextPanel() {
819
834
  if (contextMiniLabel) {
820
835
  contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
821
836
  }
822
- // Header bar
823
- if (pct > 0) {
837
+ // Header bar. A split group has no meaningful group-level context usage:
838
+ // the two members have separate windows, and the parent shell's own numbers
839
+ // are whatever it last viewed natively. Each pane header carries its own
840
+ // chip instead, so hide the title-bar gauge while a split is open.
841
+ var splitOpen = !!store.get('splitPanes');
842
+ var headerCtxEl = store.get('headerContextEl');
843
+ if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen);
844
+ if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen);
845
+ if (pct > 0 && !splitOpen) {
824
846
  var statusArea = document.querySelector(".title-bar-content .status");
825
847
  var hCtxEl = store.get('headerContextEl');
826
848
  if (statusArea && !hCtxEl) {
@@ -32,6 +32,7 @@ import { resetRateLimitState } from './app-rate-limit.js';
32
32
  import { closeSessionInfoPopover } from './app-header.js';
33
33
  import { resetDebateState } from './debate.js';
34
34
  import { removeDebateBottomBar } from './app-debate-ui.js';
35
+ import { familyOf as deriveFamily, displayProject } from './worktree-family.js';
35
36
 
36
37
  // --- Module-owned state ---
37
38
  var cachedProjects = [];
@@ -201,6 +202,32 @@ export function setCachedProjectCount(v) { cachedProjectCount = v; }
201
202
  export function getCachedRemovedProjects() { return cachedRemovedProjects; }
202
203
  export function setCachedRemovedProjects(v) { cachedRemovedProjects = v; }
203
204
 
205
+ export function familyOf(slug) { return deriveFamily(cachedProjects, slug); }
206
+ export function currentFamily() { return familyOf(store.get('currentSlug')); }
207
+
208
+ export function updateTitleBarProjectIdentity(fallbackName, fallbackIcon) {
209
+ if (store.get('dmMode') || store.get('mateProjectSlug')) {
210
+ var dmNameEl = document.getElementById("title-bar-project-name");
211
+ if (dmNameEl && fallbackName) dmNameEl.textContent = fallbackName;
212
+ return;
213
+ }
214
+ var identity = displayProject(cachedProjects, store.get('currentSlug'));
215
+ var name = identity ? (identity.title || identity.project || identity.slug) : fallbackName;
216
+ var icon = identity ? (identity.icon || null) : (fallbackIcon || null);
217
+ var nameEl = document.getElementById("title-bar-project-name");
218
+ if (nameEl && name) nameEl.textContent = name;
219
+ var iconEl = document.getElementById("title-bar-project-icon");
220
+ if (!iconEl) return;
221
+ if (icon) {
222
+ iconEl.textContent = icon;
223
+ parseEmojis(iconEl);
224
+ iconEl.classList.add("has-icon");
225
+ } else {
226
+ iconEl.textContent = "";
227
+ iconEl.classList.remove("has-icon");
228
+ }
229
+ }
230
+
204
231
  // --- Functions ---
205
232
 
206
233
  export function updateProjectList(msg) {
@@ -215,6 +242,7 @@ export function updateProjectList(msg) {
215
242
  _lastProjectsJson = projectsJson;
216
243
  }
217
244
  cachedProjects = msg.projects;
245
+ store.set({ projects: cachedProjects });
218
246
  }
219
247
  if (msg.removedProjects) cachedRemovedProjects = msg.removedProjects;
220
248
  else if (msg.removedProjects === undefined) { /* keep cached */ }
@@ -326,31 +354,12 @@ export function renderProjectList() {
326
354
  });
327
355
  var st = store.snap();
328
356
  var iconStripActiveSlug = (st.mateProjectSlug && st.savedMainSlug) ? st.savedMainSlug : st.currentSlug;
357
+ var activeFamily = deriveFamily(cachedProjects, iconStripActiveSlug);
358
+ if (activeFamily.parent) iconStripActiveSlug = activeFamily.parent.slug;
329
359
  renderIconStrip(iconStripProjects, iconStripActiveSlug);
330
360
  // Update title bar project name and icon if it changed
331
361
  if (!st.mateProjectSlug) {
332
- for (var pi = 0; pi < cachedProjects.length; pi++) {
333
- if (cachedProjects[pi].slug === st.currentSlug) {
334
- var updatedName = cachedProjects[pi].title || cachedProjects[pi].project;
335
- var tbName = document.getElementById("title-bar-project-name");
336
- if (tbName && updatedName) tbName.textContent = updatedName;
337
- var tbIcon = document.getElementById("title-bar-project-icon");
338
- if (tbIcon) {
339
- var pIcon = cachedProjects[pi].icon || null;
340
- if (pIcon) {
341
- tbIcon.textContent = pIcon;
342
- parseEmojis(tbIcon);
343
- tbIcon.classList.add("has-icon");
344
- try { localStorage.setItem("clay-project-icon-" + (st.currentSlug || "default"), pIcon); } catch (e) {}
345
- } else {
346
- tbIcon.textContent = "";
347
- tbIcon.classList.remove("has-icon");
348
- try { localStorage.removeItem("clay-project-icon-" + (st.currentSlug || "default")); } catch (e) {}
349
- }
350
- }
351
- break;
352
- }
353
- }
362
+ updateTitleBarProjectIdentity(store.get('projectName'), null);
354
363
  }
355
364
  // Re-apply current socket status to the active icon's dot
356
365
  var dot = getStatusDot();
@@ -557,9 +566,13 @@ function showRemoveProjectTaskDialog(slug, name, taskCount) {
557
566
  export function handleRemoveProjectResult(msg) {
558
567
  if (msg.ok) {
559
568
  var currentSlug = store.get('currentSlug');
569
+ var removedProj = null;
570
+ for (var ri = 0; ri < cachedProjects.length; ri++) {
571
+ if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
572
+ }
573
+ var isWorktree = removedProj ? !!removedProj.isWorktree : msg.slug.indexOf("--") !== -1;
560
574
  if (msg.slug === currentSlug) {
561
- var isWorktree = msg.slug.indexOf("--") !== -1;
562
- var parentSlug = isWorktree ? msg.slug.split("--")[0] : null;
575
+ var parentSlug = isWorktree ? ((removedProj && removedProj.parentSlug) || msg.slug.split("--")[0]) : null;
563
576
 
564
577
  showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
565
578
 
@@ -570,10 +583,6 @@ export function handleRemoveProjectResult(msg) {
570
583
  store.set({ connected: false });
571
584
  document.getElementById("connect-overlay").classList.add("hidden");
572
585
  if (!isWorktree) {
573
- var removedProj = null;
574
- for (var ri = 0; ri < cachedProjects.length; ri++) {
575
- if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
576
- }
577
586
  if (removedProj) {
578
587
  cachedRemovedProjects.push({
579
588
  path: removedProj.path || "",
@@ -585,7 +594,7 @@ export function handleRemoveProjectResult(msg) {
585
594
  }
586
595
  cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
587
596
  cachedProjectCount = cachedProjects.length;
588
- store.set({ currentSlug: null });
597
+ store.set({ currentSlug: null, projects: cachedProjects });
589
598
  renderProjectList();
590
599
  resetClientState();
591
600
 
@@ -595,7 +604,11 @@ export function handleRemoveProjectResult(msg) {
595
604
  showHomeHub();
596
605
  }
597
606
  } else {
598
- showToast(msg.slug.indexOf("--") !== -1 ? "Worktree removed" : "Project removed", "success");
607
+ showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
608
+ cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
609
+ cachedProjectCount = cachedProjects.length;
610
+ store.set({ projects: cachedProjects });
611
+ renderProjectList();
599
612
  }
600
613
  } else {
601
614
  showToast(msg.error || "Failed to remove project", "error");
@@ -416,12 +416,14 @@ export function finalizeAssistantBlock() {
416
416
  store.set({ currentMsgEl: null, currentFullText: "" });
417
417
  }
418
418
 
419
- export function addUserMessage(text, images, pastes, fromUserId, fromUserName) {
419
+ export function addUserMessage(text, images, pastes, fromUserId, fromUserName, delegatedMeta) {
420
420
  if (!text && (!images || images.length === 0) && (!pastes || pastes.length === 0)) return;
421
421
  var myUserId = store.get('myUserId');
422
- var isOtherUser = fromUserId && fromUserId !== myUserId;
422
+ var isDelegated = !!delegatedMeta;
423
+ var isOtherUser = isDelegated || (fromUserId && fromUserId !== myUserId);
423
424
  var div = document.createElement("div");
424
425
  div.className = "msg-user" + (isOtherUser ? " msg-user-other" : "");
426
+ if (isDelegated) div.classList.add("msg-user-delegated");
425
427
  div.dataset.turn = ++turnCounter;
426
428
  if (shouldGroupMessage("msg-user")) div.classList.add("grouped");
427
429
  var bubble = document.createElement("div");
@@ -482,7 +484,10 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName) {
482
484
  var cachedAllUsers = store.get('cachedAllUsers');
483
485
  var _targetUser;
484
486
  var _displayName;
485
- if (isOtherUser) {
487
+ if (isDelegated) {
488
+ _targetUser = null;
489
+ _displayName = "from " + (delegatedMeta.delegatedByTitle || "Driver");
490
+ } else if (isOtherUser) {
486
491
  _targetUser = cachedAllUsers.find(function (u) { return u.id === fromUserId; });
487
492
  _displayName = fromUserName || (_targetUser && (_targetUser.displayName || _targetUser.username)) || "User";
488
493
  } else {
@@ -498,7 +503,9 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName) {
498
503
 
499
504
  var avi = document.createElement("img");
500
505
  avi.className = "dm-bubble-avatar" + (isOtherUser ? " dm-bubble-avatar-other" : " dm-bubble-avatar-me");
501
- avi.src = isOtherUser
506
+ avi.src = isDelegated
507
+ ? (VENDOR_AVATARS[delegatedMeta.delegatedByVendor] || VENDOR_AVATARS.claude)
508
+ : isOtherUser
502
509
  ? userAvatarUrl(_targetUser || { id: fromUserId }, 36)
503
510
  : (document.body.dataset.myAvatarUrl || userAvatarUrl(_targetUser || { id: myUserId }, 36));
504
511
  div.appendChild(avi);
@@ -0,0 +1,178 @@
1
+ // Title-bar branch switcher for a project and its worktrees.
2
+
3
+ import { store } from './store.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { familyOf } from './worktree-family.js';
7
+ import { switchProject, confirmRemoveProject } from './app-projects.js';
8
+ import { showWorktreeModal } from './sidebar-projects.js';
9
+
10
+ var chip = null;
11
+ var label = null;
12
+ var menu = null;
13
+ var menuOpen = false;
14
+ var defaultBranches = {};
15
+ // slug -> true/false once /api/branches answers; undefined while unknown.
16
+ // Non-git projects hide the chip (no worktrees to manage there).
17
+ var gitRepoBySlug = {};
18
+
19
+ function projectName(project) {
20
+ return project ? (project.title || project.project || project.name || project.slug) : "";
21
+ }
22
+
23
+ function currentProject(projects) {
24
+ var slug = store.get('currentSlug');
25
+ for (var i = 0; i < projects.length; i++) {
26
+ if (projects[i].slug === slug) return projects[i];
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function defaultBranch(parent) {
32
+ return defaultBranches[parent.slug] || "default";
33
+ }
34
+
35
+ function requestDefaultBranch(parent) {
36
+ if (!parent || Object.prototype.hasOwnProperty.call(defaultBranches, parent.slug)) return;
37
+ defaultBranches[parent.slug] = null;
38
+ fetch("/p/" + encodeURIComponent(parent.slug) + "/api/branches")
39
+ .then(function (response) { return response.json(); })
40
+ .then(function (data) {
41
+ defaultBranches[parent.slug] = data.defaultBranch || "default";
42
+ gitRepoBySlug[parent.slug] = data.isGitRepo !== false;
43
+ renderBranchSwitcher();
44
+ })
45
+ .catch(function () {
46
+ defaultBranches[parent.slug] = "default";
47
+ renderBranchSwitcher();
48
+ });
49
+ }
50
+
51
+ function closeMenu() {
52
+ menuOpen = false;
53
+ if (menu) menu.classList.add("hidden");
54
+ if (chip) {
55
+ chip.classList.remove("open");
56
+ chip.setAttribute("aria-expanded", "false");
57
+ }
58
+ }
59
+
60
+ function positionMenu() {
61
+ if (!chip || !menu || !menuOpen) return;
62
+ var rect = chip.getBoundingClientRect();
63
+ var left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 8);
64
+ var top = rect.bottom + 6;
65
+ if (top + menu.offsetHeight > window.innerHeight - 8) top = rect.top - menu.offsetHeight - 6;
66
+ menu.style.left = Math.max(8, left) + "px";
67
+ menu.style.top = Math.max(8, top) + "px";
68
+ }
69
+
70
+ function branchLabel(project, parent) {
71
+ return project.isWorktree ? (project.branch || projectName(project)) : defaultBranch(parent);
72
+ }
73
+
74
+ function appendProjectRow(container, project, parent, isCurrent) {
75
+ var row = document.createElement("div");
76
+ row.className = "branch-chip-row" + (isCurrent ? " current" : "");
77
+ var inaccessible = project.isWorktree && project.worktreeAccessible === false;
78
+ var main = document.createElement("button");
79
+ main.className = "branch-chip-row-main";
80
+ main.disabled = inaccessible;
81
+ if (inaccessible) main.title = "Outside project path";
82
+ main.innerHTML = iconHtml("git-branch") + '<span class="branch-chip-row-label">' +
83
+ escapeHtml(branchLabel(project, parent)) + "</span>";
84
+ if (project.isProcessing) {
85
+ var dot = document.createElement("span");
86
+ dot.className = "branch-chip-processing";
87
+ main.appendChild(dot);
88
+ }
89
+ if (project.pendingPermissions > 0) {
90
+ var badge = document.createElement("span");
91
+ badge.className = "branch-chip-pending";
92
+ badge.textContent = project.pendingPermissions > 99 ? "99+" : String(project.pendingPermissions);
93
+ main.appendChild(badge);
94
+ }
95
+ if (isCurrent) main.insertAdjacentHTML("beforeend", iconHtml("check"));
96
+ if (!inaccessible) {
97
+ main.addEventListener("click", function () { closeMenu(); switchProject(project.slug); });
98
+ }
99
+ row.appendChild(main);
100
+
101
+ if (project.isWorktree && (!store.get('permissions') || store.get('permissions').deleteProject !== false)) {
102
+ var remove = document.createElement("button");
103
+ remove.className = "branch-chip-row-action";
104
+ remove.title = "Remove worktree";
105
+ remove.innerHTML = iconHtml("trash-2");
106
+ remove.addEventListener("click", function (event) {
107
+ event.stopPropagation();
108
+ closeMenu();
109
+ confirmRemoveProject(project.slug, projectName(project));
110
+ });
111
+ row.appendChild(remove);
112
+ }
113
+ container.appendChild(row);
114
+ }
115
+
116
+ function renderMenu(family, current) {
117
+ menu.innerHTML = "";
118
+ if (family.parent) appendProjectRow(menu, family.parent, family.parent, current.slug === family.parent.slug);
119
+ for (var i = 0; i < family.worktrees.length; i++) {
120
+ appendProjectRow(menu, family.worktrees[i], family.parent || family.worktrees[i], current.slug === family.worktrees[i].slug);
121
+ }
122
+ if (family.parent) {
123
+ var footer = document.createElement("button");
124
+ footer.className = "branch-chip-new";
125
+ footer.innerHTML = iconHtml("plus") + " <span>New worktree...</span>";
126
+ footer.addEventListener("click", function () {
127
+ closeMenu();
128
+ showWorktreeModal(family.parent.slug, projectName(family.parent));
129
+ });
130
+ menu.appendChild(footer);
131
+ }
132
+ refreshIcons();
133
+ positionMenu();
134
+ }
135
+
136
+ export function renderBranchSwitcher() {
137
+ if (!chip) return;
138
+ var projects = store.get('projects') || [];
139
+ var current = currentProject(projects);
140
+ var family = current ? familyOf(projects, current.slug) : { parent: null, worktrees: [] };
141
+ // The chip is the project's worktree management surface, so it shows even
142
+ // with zero worktrees (the menu then offers the default branch and "New
143
+ // worktree"). Hidden where there is no project context to manage, and for
144
+ // non-git projects (optimistically visible until /api/branches answers).
145
+ var gateSlug = family.parent ? family.parent.slug : (current ? current.slug : null);
146
+ var hidden = !current || store.get('dmMode') || store.get('mateProjectSlug') ||
147
+ store.get('homeHubVisible') || (gateSlug && gitRepoBySlug[gateSlug] === false);
148
+ chip.classList.toggle("hidden", hidden);
149
+ if (hidden) { closeMenu(); return; }
150
+ if (family.parent) requestDefaultBranch(family.parent);
151
+ label.textContent = branchLabel(current, family.parent || current);
152
+ if (menuOpen) renderMenu(family, current);
153
+ }
154
+
155
+ export function initBranchSwitcher() {
156
+ chip = document.getElementById("branch-chip");
157
+ label = document.getElementById("branch-chip-label");
158
+ menu = document.getElementById("branch-chip-menu");
159
+ if (!chip || !label || !menu) return;
160
+ chip.addEventListener("click", function (event) {
161
+ event.stopPropagation();
162
+ menuOpen = !menuOpen;
163
+ chip.classList.toggle("open", menuOpen);
164
+ chip.setAttribute("aria-expanded", menuOpen ? "true" : "false");
165
+ menu.classList.toggle("hidden", !menuOpen);
166
+ renderBranchSwitcher();
167
+ });
168
+ document.addEventListener("click", function (event) {
169
+ if (menuOpen && !menu.contains(event.target) && !chip.contains(event.target)) closeMenu();
170
+ });
171
+ window.addEventListener("resize", positionMenu);
172
+ store.subscribe(function (state, prev) {
173
+ if (state.projects !== prev.projects || state.currentSlug !== prev.currentSlug ||
174
+ state.dmMode !== prev.dmMode || state.mateProjectSlug !== prev.mateProjectSlug ||
175
+ state.homeHubVisible !== prev.homeHubVisible) renderBranchSwitcher();
176
+ });
177
+ renderBranchSwitcher();
178
+ }
@@ -16,6 +16,5 @@ export function getSessionListEl() { return ref("session-list"); }
16
16
 
17
17
  export function getStatusDot() {
18
18
  return document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status") ||
19
- document.querySelector("#icon-strip-projects .icon-strip-wt-item.active .icon-strip-status") ||
20
19
  document.querySelector("#icon-strip-users .icon-strip-mate.active .icon-strip-status");
21
20
  }
@@ -0,0 +1,41 @@
1
+ // Pane-side postMessage bridge for the split-view shell. A chrome-less pane
2
+ // iframe reports its context usage up to the parent (which renders a chip in
3
+ // the pane header) and honors panel-toggle requests coming back down.
4
+ // Same-origin messages only, both directions.
5
+
6
+ import { store } from './store.js';
7
+ import { applyContextView, getContextView } from './app-panels.js';
8
+
9
+ var panelOpen = false;
10
+
11
+ export function reportPaneContext(data) {
12
+ if (!store.get('paneMode') || window.parent === window) return;
13
+ window.parent.postMessage({
14
+ type: "clay-pane-context",
15
+ sessionId: store.get('activeSessionId'),
16
+ pct: data.pct,
17
+ used: data.used,
18
+ win: data.win,
19
+ cls: data.cls,
20
+ model: data.model,
21
+ cost: data.cost,
22
+ }, window.location.origin);
23
+ }
24
+
25
+ // Toggle without setContextView: panes share localStorage with the main app,
26
+ // so persisting here would silently rewrite the user's main-view preference.
27
+ function togglePaneContextPanel() {
28
+ panelOpen = !panelOpen;
29
+ applyContextView(panelOpen ? "panel" : "off");
30
+ }
31
+
32
+ export function initPaneBridge() {
33
+ if (!store.get('paneMode')) return;
34
+ panelOpen = getContextView() === "panel";
35
+ window.addEventListener("message", function (event) {
36
+ if (event.origin !== window.location.origin) return;
37
+ var msg = event.data;
38
+ if (!msg || msg.type !== "clay-pane-toggle-context") return;
39
+ togglePaneContextPanel();
40
+ });
41
+ }
@@ -0,0 +1,18 @@
1
+ // Pure resolution for the one-shot session pin used by pane-mode clients.
2
+
3
+ export function resolvePaneSession(paneMode, pinPending, pendingSessionId, sessions) {
4
+ if (!paneMode || !pinPending || !pendingSessionId) return { consumed: false, sessionId: null };
5
+ var list = sessions || [];
6
+ for (var i = 0; i < list.length; i++) {
7
+ if (list[i].id === pendingSessionId) {
8
+ return { consumed: true, sessionId: pendingSessionId };
9
+ }
10
+ }
11
+ return { consumed: true, sessionId: null };
12
+ }
13
+
14
+ // session_switched is authoritative even when the previous session left the
15
+ // vendor selector locked. The lock only applies to late model metadata.
16
+ export function resolveSwitchedVendor(currentVendor, sessionVendor) {
17
+ return sessionVendor || currentVendor || "claude";
18
+ }
@@ -19,6 +19,7 @@ import { getCachedProjects, switchProject } from './app-projects.js';
19
19
  import { openDm } from './app-dm.js';
20
20
  import { mateAvatarUrl } from './avatar.js';
21
21
  import { parseEmojis } from './markdown.js';
22
+ import { switcherProjectName } from './worktree-family.js';
22
23
 
23
24
  var _projectMru = []; // project slugs, most recent first
24
25
  var _mateMru = []; // mate ids (mate_XXX), most recent first
@@ -263,7 +264,7 @@ function buildProjectEntries() {
263
264
  var unreachable = p.isWorktree && p.worktreeAccessible === false;
264
265
  return {
265
266
  key: p.slug,
266
- title: p.title || p.project || p.slug,
267
+ title: switcherProjectName(projects, p),
267
268
  icon: p.icon || null,
268
269
  fallbackIcon: 'folder',
269
270
  isCurrent: p.slug === store.get('currentSlug'),
@@ -38,6 +38,7 @@ import { showHomeHub } from './app-home-hub.js';
38
38
  import { openTerminal } from './terminal.js';
39
39
  import { requestKnowledgeList } from './mate-knowledge.js';
40
40
  import { loadRootDirectory } from './filebrowser.js';
41
+ import { aggregateFamily, familyOf, parentProjects } from './worktree-family.js';
41
42
 
42
43
  // --- Mobile state ---
43
44
  var mobileChatSheetOpen = false;
@@ -147,10 +148,22 @@ function closeMobileSheet() {
147
148
  }
148
149
 
149
150
  function renderSheetProjects(listEl) {
150
- for (var i = 0; i < getCachedProjectList().length; i++) {
151
+ var projects = getCachedProjectList();
152
+ var parents = parentProjects(projects);
153
+ var activeFamily = familyOf(projects, getCachedCurrentSlug());
154
+ var activeSlug = activeFamily.parent ? activeFamily.parent.slug : getCachedCurrentSlug();
155
+
156
+ for (var i = 0; i < parents.length; i++) {
151
157
  (function (p) {
158
+ var family = familyOf(projects, p.slug);
159
+ var displayProject = aggregateFamily(p, family.worktrees);
160
+ var hasWorktreeUnread = family.worktrees.some(function (worktree) { return worktree.unread > 0; });
161
+ var hasWorktreePending = family.worktrees.some(function (worktree) { return worktree.pendingPermissions > 0; });
152
162
  var el = document.createElement("button");
153
- el.className = "mobile-project-item" + (p.slug === getCachedCurrentSlug() ? " active" : "");
163
+ el.className = "mobile-project-item" + (p.slug === activeSlug ? " active" : "");
164
+ if ((displayProject.pendingPermissions > 0 && p.slug !== activeSlug) || hasWorktreePending) {
165
+ el.classList.add("has-pending-perm");
166
+ }
154
167
 
155
168
  var abbrev = document.createElement("span");
156
169
  abbrev.className = "mobile-project-abbrev";
@@ -167,16 +180,16 @@ function renderSheetProjects(listEl) {
167
180
  name.textContent = p.name;
168
181
  el.appendChild(name);
169
182
 
170
- if (p.isProcessing) {
183
+ if (displayProject.isProcessing) {
171
184
  var dot = document.createElement("span");
172
185
  dot.className = "mobile-project-processing";
173
186
  el.appendChild(dot);
174
187
  }
175
188
 
176
- if (p.unread > 0 && p.slug !== getCachedCurrentSlug()) {
189
+ if (displayProject.unread > 0 && (p.slug !== activeSlug || hasWorktreeUnread)) {
177
190
  var mBadge = document.createElement("span");
178
191
  mBadge.className = "mobile-project-unread";
179
- mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
192
+ mBadge.textContent = displayProject.unread > 99 ? "99+" : String(displayProject.unread);
180
193
  el.appendChild(mBadge);
181
194
  }
182
195
 
@@ -186,7 +199,7 @@ function renderSheetProjects(listEl) {
186
199
  });
187
200
 
188
201
  listEl.appendChild(el);
189
- })(getCachedProjectList()[i]);
202
+ })(parents[i]);
190
203
  }
191
204
  }
192
205