clay-server 5.2.0-beta.8 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/pane-pending-interactions.js +20 -0
  9. package/lib/pane-session-activation.js +114 -0
  10. package/lib/pane-session-transport.js +26 -0
  11. package/lib/project-session-notes.js +33 -8
  12. package/lib/project-sessions.js +78 -29
  13. package/lib/project-start-protocol.js +29 -0
  14. package/lib/project-vendor-login.js +2 -0
  15. package/lib/project.js +16 -4
  16. package/lib/public/app.js +41 -18
  17. package/lib/public/css/contextual-clay.css +2 -2
  18. package/lib/public/css/filebrowser.css +1 -1
  19. package/lib/public/css/input.css +6 -0
  20. package/lib/public/css/mate-loading-scene.css +21 -0
  21. package/lib/public/css/overlays.css +64 -256
  22. package/lib/public/css/project-mate-creation.css +5 -6
  23. package/lib/public/css/project-start.css +108 -0
  24. package/lib/public/css/projects-hub.css +53 -14
  25. package/lib/public/css/settings-shell.css +103 -0
  26. package/lib/public/css/sidebar.css +7 -0
  27. package/lib/public/css/sticky-notes.css +37 -11
  28. package/lib/public/css/title-bar.css +83 -0
  29. package/lib/public/index.html +50 -63
  30. package/lib/public/modules/account-transport-settings.js +8 -2
  31. package/lib/public/modules/admin-settings-email.js +7 -2
  32. package/lib/public/modules/admin-settings-invitations.js +2 -2
  33. package/lib/public/modules/admin-settings-projects.js +2 -2
  34. package/lib/public/modules/admin-settings-ui.js +20 -4
  35. package/lib/public/modules/admin-settings-users.js +4 -4
  36. package/lib/public/modules/app-connection.js +3 -0
  37. package/lib/public/modules/app-home-hub.js +2 -1
  38. package/lib/public/modules/app-message-router.js +13 -0
  39. package/lib/public/modules/app-messages.js +35 -7
  40. package/lib/public/modules/app-misc.js +3 -1
  41. package/lib/public/modules/app-projects.js +6 -407
  42. package/lib/public/modules/app-rate-limit.js +12 -1
  43. package/lib/public/modules/clay-owner-navigation.js +0 -3
  44. package/lib/public/modules/clay-studio-render.js +3 -1
  45. package/lib/public/modules/clay-tool-presentation.js +1 -0
  46. package/lib/public/modules/command-palette.js +52 -14
  47. package/lib/public/modules/contextual-clay-workspace.js +14 -2
  48. package/lib/public/modules/contextual-clay.js +19 -4
  49. package/lib/public/modules/contextual-mate-context.js +6 -4
  50. package/lib/public/modules/elicitation-form.js +5 -0
  51. package/lib/public/modules/github-star.js +66 -0
  52. package/lib/public/modules/history-replay-batch.js +8 -0
  53. package/lib/public/modules/home-surface-boot.js +0 -1
  54. package/lib/public/modules/input.js +55 -11
  55. package/lib/public/modules/mate-loading-scene.js +17 -0
  56. package/lib/public/modules/message-delivery.js +10 -2
  57. package/lib/public/modules/model-picker.js +2 -0
  58. package/lib/public/modules/pane-activation.js +214 -0
  59. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  60. package/lib/public/modules/pane-binding.js +82 -0
  61. package/lib/public/modules/pane-bridge.js +5 -2
  62. package/lib/public/modules/pane-browser-bridge.js +5 -3
  63. package/lib/public/modules/pane-file-bridge.js +4 -0
  64. package/lib/public/modules/pane-issue-bridge.js +4 -1
  65. package/lib/public/modules/profile.js +11 -3
  66. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  67. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  68. package/lib/public/modules/project-settings-environment.js +12 -1
  69. package/lib/public/modules/project-settings.js +119 -129
  70. package/lib/public/modules/project-start-dialog.js +130 -0
  71. package/lib/public/modules/project-start-folder-state.js +78 -0
  72. package/lib/public/modules/project-start-folder-view.js +190 -0
  73. package/lib/public/modules/project-start-forms.js +66 -0
  74. package/lib/public/modules/project-start-path.js +60 -0
  75. package/lib/public/modules/project-start-state.js +54 -0
  76. package/lib/public/modules/project-start-submit.js +67 -0
  77. package/lib/public/modules/projects-hub-view.js +32 -44
  78. package/lib/public/modules/projects-hub.js +39 -28
  79. package/lib/public/modules/rewind.js +5 -0
  80. package/lib/public/modules/server-settings.js +36 -91
  81. package/lib/public/modules/session-actions.js +5 -2
  82. package/lib/public/modules/session-context-menu.js +5 -0
  83. package/lib/public/modules/session-rich-drafts.js +21 -0
  84. package/lib/public/modules/session-switch-defer.js +67 -0
  85. package/lib/public/modules/settings-project-target.js +56 -0
  86. package/lib/public/modules/settings-select.js +1 -1
  87. package/lib/public/modules/settings-shell-registry.js +88 -0
  88. package/lib/public/modules/settings-shell-view.js +104 -0
  89. package/lib/public/modules/settings-shell.js +342 -0
  90. package/lib/public/modules/sidebar-mobile.js +8 -30
  91. package/lib/public/modules/sidebar-projects.js +2 -1
  92. package/lib/public/modules/split-group-restore.js +68 -0
  93. package/lib/public/modules/split-pair-ui.js +1 -0
  94. package/lib/public/modules/split-pane-reconciler.js +15 -0
  95. package/lib/public/modules/split-pane-renderer.js +87 -6
  96. package/lib/public/modules/split-session-boundary.js +1 -1
  97. package/lib/public/modules/split-view.js +51 -52
  98. package/lib/public/modules/sticky-notes-checklist.js +51 -6
  99. package/lib/public/modules/stt-controller.js +4 -1
  100. package/lib/public/modules/stt-recording.js +1 -1
  101. package/lib/public/modules/theme.js +31 -3
  102. package/lib/public/modules/tools.js +18 -2
  103. package/lib/public/modules/user-settings-integrations.js +2 -1
  104. package/lib/public/modules/user-settings-layout.js +12 -34
  105. package/lib/public/modules/user-settings-profile.js +1 -1
  106. package/lib/public/modules/user-settings.js +18 -85
  107. package/lib/public/modules/vendor-usage-format.js +93 -0
  108. package/lib/public/modules/vendor-usage-list.js +45 -0
  109. package/lib/public/modules/vendor-usage-model.js +215 -0
  110. package/lib/public/modules/vendor-usage-overview.js +323 -0
  111. package/lib/public/style.css +10 -7
  112. package/lib/public/welcome-art/conversation-desk.png +0 -0
  113. package/lib/public/welcome-art/mate-clay.png +0 -0
  114. package/lib/public/welcome-art/project-clay.png +0 -0
  115. package/lib/sdk-bridge.js +6 -1
  116. package/lib/sdk-message-processor.js +9 -1
  117. package/lib/sdk-quota-snapshot.js +20 -2
  118. package/lib/server-github-stars.js +39 -0
  119. package/lib/server-global-ws.js +21 -17
  120. package/lib/server.js +11 -0
  121. package/lib/session-notes-mcp-server.js +26 -4
  122. package/lib/session-notes-writes.js +88 -0
  123. package/lib/session-visibility.js +20 -8
  124. package/lib/sessions.js +27 -10
  125. package/lib/vendor-usage-acp-readers.js +97 -0
  126. package/lib/vendor-usage-capabilities.js +88 -0
  127. package/lib/vendor-usage-http-readers.js +214 -0
  128. package/lib/vendor-usage-metrics.js +47 -0
  129. package/lib/vendor-usage-native-readers.js +106 -0
  130. package/lib/vendor-usage-overview.js +406 -0
  131. package/lib/vendor-usage-service.js +281 -0
  132. package/lib/ws-schema.js +4 -2
  133. package/lib/yoke/adapters/claude.js +12 -0
  134. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  135. package/lib/yoke/adapters/codex.js +45 -12
  136. package/lib/yoke/claude-hook-installer.js +1 -0
  137. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  138. package/lib/yoke/codex-model-catalog.js +37 -3
  139. package/lib/yoke/muse-services.js +16 -3
  140. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  141. package/lib/yoke/vendor-registry.js +6 -2
  142. package/package.json +5 -3
  143. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -10,7 +10,8 @@ import { store } from './store.js';
10
10
  import { getWs } from './ws-ref.js';
11
11
  import { closeSidebar } from './sidebar.js';
12
12
  import { showIconTooltip, hideIconTooltip, bindIconTooltip, closeUserCtxMenu, getCurrentDmUserId } from './sidebar-mates.js';
13
- import { switchProject, openAddProjectModal, getCachedProjects } from './app-projects.js';
13
+ import { switchProject, getCachedProjects } from './app-projects.js';
14
+ import { openAddProjectModal } from './project-start-dialog.js';
14
15
  import { showHomeHub } from './app-home-hub.js';
15
16
  import { getPendingTuiAttention } from './app-notifications.js';
16
17
  import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
@@ -0,0 +1,68 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { getCachedSessions } from './sidebar-sessions.js';
4
+ import { isCurrentProjectSessionReady } from './split-session-boundary.js';
5
+ import { splitPanesMatchProject, reconcileRestoredSplit } from './project-activation.js';
6
+ import { splitGroupMemberIds } from './split-group-helpers.js';
7
+ import { splitPaneForSession } from './split-pane-reconciler.js';
8
+ import { clearProjectSplitState } from './split-session-boundary.js';
9
+
10
+ export function reconcileSplitIdentity(state, previous, host) {
11
+ var ownerChanged = (previous.authIdentityLoaded === true || state.splitIdentityHydrated) && state.myUserId !== previous.myUserId;
12
+ if (state.authIdentityLoaded && !state.splitIdentityHydrated) store.set({ splitIdentityHydrated: true });
13
+ if (state.currentSlug === previous.currentSlug && state.myUserId === previous.myUserId) return false;
14
+ Array.from(host.querySelectorAll('.split-pane-frame')).forEach(function (frame) {
15
+ if (frame.clayPane) frame.clayPane.suspend();
16
+ });
17
+ host.innerHTML = '';
18
+ if (ownerChanged) {
19
+ // This socket was authenticated as the previous owner. Fresh metadata on
20
+ // that connection cannot establish the replacement owner's pane boundary.
21
+ store.set({ splitIdentityBlockedSocket: getWs(), paneBindingSessions: [] });
22
+ clearProjectSplitState();
23
+ } else if (state.splitPanes) store.set({ splitPanes: null });
24
+ return true;
25
+ }
26
+
27
+ // The initial owner response can arrive after history and split metadata.
28
+ // Wait for it so identity cleanup cannot discard panes we just restored.
29
+ export function restoreSplitGroup(openGroup) {
30
+ var state = store.snap();
31
+ if (state.authIdentityLoaded === false || state.isMultiUserMode && !state.myUserId) return;
32
+ if (!isCurrentProjectSessionReady(state, getWs())) return;
33
+ var split = state.splitPanes;
34
+ if (split && !splitPanesMatchProject(split, state.currentSlug)) {
35
+ store.set({ splitPanes: null });
36
+ return;
37
+ }
38
+ if (state.paneMode) return;
39
+ if (split) {
40
+ var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
41
+ if (reconciliation.action === "clear") {
42
+ store.set({ splitPanes: null });
43
+ return;
44
+ }
45
+ if (reconciliation.action === "rebuild") {
46
+ var rebuilt = reconciliation.group;
47
+ if (rebuilt.members.some(function (id) { return !getCachedSessions().some(function (session) { return session.id === id; }); })) {
48
+ store.set({ splitPanes: null });
49
+ return;
50
+ }
51
+ store.set({ splitPanes: {
52
+ groupId: rebuilt.id,
53
+ panes: splitGroupMemberIds(rebuilt).map(function (id) { return splitPaneForSession(id, state.currentSlug, getCachedSessions()); }),
54
+ } });
55
+ }
56
+ return;
57
+ }
58
+ var activeId = store.get('activeSessionId');
59
+ if (!activeId) return;
60
+ var groups = store.get('splitGroups') || [];
61
+ for (var i = 0; i < groups.length; i++) {
62
+ var members = groups[i].members || [];
63
+ if (members.indexOf(activeId) !== -1) {
64
+ openGroup(groups[i]);
65
+ return;
66
+ }
67
+ }
68
+ }
@@ -236,6 +236,7 @@ function sendSetPair(groupId, driverId) {
236
236
 
237
237
  export function syncPairChrome(host, split) {
238
238
  if (!host || !split || !split.groupId) return;
239
+ if (Array.from(host.querySelectorAll('.split-pane-frame')).some(function (frame) { return !!frame.dataset.requestedSessionId; })) return;
239
240
  var groups = store.get('splitGroups') || [];
240
241
  var group = null;
241
242
  for (var i = 0; i < groups.length; i++) {
@@ -2,6 +2,11 @@
2
2
  // (and their iframe browsing contexts) are reused by stable project/session
3
3
  // identity while metadata and visual order are refreshed.
4
4
 
5
+ export function splitPaneForSession(id, slug, sessions) {
6
+ var session = (sessions || []).find(function (item) { return item.id === id; });
7
+ return { slug: slug, sessionId: id, title: session && session.title || ('Session ' + id) };
8
+ }
9
+
5
10
  export function splitPaneIdentity(pane) {
6
11
  return String(pane && pane.slug || "") + "#" + String(pane && pane.sessionId || "");
7
12
  }
@@ -16,9 +21,19 @@ export function reconcileSplitPanes(host, panes, createPane, updatePane) {
16
21
  var pane = desired[pi];
17
22
  var key = splitPaneIdentity(pane);
18
23
  var paneEl = byKey.get(key);
24
+ if (!paneEl) {
25
+ for (var ci = 0; ci < existing.length; ci++) {
26
+ var candidate = existing[ci], frame = candidate.querySelector('.split-pane-frame');
27
+ if (!used.has(candidate) && frame && frame.dataset.projectSlug === pane.slug && frame.clayPane &&
28
+ !desired.some(function (item) { return splitPaneIdentity(item) === candidate.dataset.splitPaneKey; })) {
29
+ paneEl = candidate; break;
30
+ }
31
+ }
32
+ }
19
33
  if (!paneEl) paneEl = createPane(pane, pi);
20
34
  paneEl.dataset.splitPaneKey = key;
21
35
  updatePane(paneEl, pane, pi);
36
+ if (paneEl.style) paneEl.style.order = String(pi);
22
37
  used.add(paneEl);
23
38
  if (!paneEl.parentNode) host.appendChild(paneEl);
24
39
  }
@@ -6,6 +6,8 @@ import { formatTokens } from './app-panels.js';
6
6
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
7
7
  import { createPermissionControl, bindPermissionControl, renderPermissionControl } from './permission-control.js';
8
8
  import { splitGroupRoles } from './split-group-helpers.js';
9
+ import { paneTarget, livePaneBinding, samePaneTarget } from './pane-binding.js';
10
+ import { syncPairChrome } from './split-pair-ui.js';
9
11
 
10
12
  function sessionById(sessionId) {
11
13
  var sessions = getCachedSessions() || [];
@@ -17,17 +19,31 @@ function paneUrl(pane) {
17
19
  return "/p/" + encodeURIComponent(pane.slug) + "/?pane=1&session=" + encodeURIComponent(pane.sessionId);
18
20
  }
19
21
 
22
+ function headerBindingStamp(frame) {
23
+ var live = frame.clayPane && frame.clayPane.identity();
24
+ return JSON.stringify([frame.dataset.sessionId, frame.dataset.sessionOriginId,
25
+ live && live.binding && live.binding.requestId, live && live.binding && live.binding.generation]);
26
+ }
27
+
20
28
  function startPaneRename(header, titleEl, pane) {
21
29
  if (header.querySelector(".split-pane-rename-input")) return;
22
30
  var input = document.createElement("input");
23
31
  input.type = "text"; input.className = "split-pane-rename-input"; input.value = pane.title;
24
32
  titleEl.style.display = "none"; header.insertBefore(input, titleEl); input.focus(); input.select();
25
33
  var done = false;
34
+ var section = header.parentNode, frame = section.querySelector('.split-pane-frame');
35
+ var origin = frame.dataset.sessionOriginId, binding = frame.clayPane && frame.clayPane.identity().binding, socket = getWs();
36
+ function current() {
37
+ var live = frame.clayPane && frame.clayPane.identity();
38
+ return header.isConnected && !header.inert && socket === getWs() && store.get('connected') &&
39
+ frame.dataset.sessionId === String(pane.sessionId) && frame.dataset.sessionOriginId === origin && !frame.dataset.requestedSessionId &&
40
+ (!frame.clayPane || livePaneBinding(frame) && live.binding === binding);
41
+ }
26
42
  function finish(commit) {
27
43
  if (done) return;
28
44
  done = true;
29
45
  var newTitle = input.value.trim(); input.remove(); titleEl.style.display = "";
30
- if (!commit || !newTitle || newTitle === pane.title) return;
46
+ if (!commit || !current() || !newTitle || newTitle === pane.title) return;
31
47
  pane.title = newTitle; titleEl.textContent = newTitle;
32
48
  var ws = getWs();
33
49
  if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "rename_session", id: pane.sessionId, title: newTitle }));
@@ -37,6 +53,7 @@ function startPaneRename(header, titleEl, pane) {
37
53
  if (event.key === "Escape") { event.preventDefault(); finish(false); }
38
54
  });
39
55
  input.addEventListener("blur", function () { finish(true); });
56
+ header.addEventListener('clay-pane-retarget', function cancel() { finish(false); header.removeEventListener('clay-pane-retarget', cancel); });
40
57
  input.addEventListener("click", function (event) { event.stopPropagation(); });
41
58
  }
42
59
 
@@ -73,33 +90,97 @@ export function createSplitPane(pane, closePane) {
73
90
  var vendorIcon = document.createElement("img"); vendorIcon.className = "split-pane-vendor";
74
91
  vendorIcon.src = VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude; vendorIcon.alt = ""; vendorIcon.title = VENDOR_NAMES[vendor] || vendor; header.appendChild(vendorIcon);
75
92
  var title = document.createElement("span"); title.className = "split-pane-title"; title.textContent = pane.title; title.title = "Rename session";
76
- title.addEventListener("click", function () { startPaneRename(header, title, pane); }); header.appendChild(title);
93
+ title.addEventListener("click", function () { var current = sessionById(Number(paneEl.dataset.sessionId)); if (current) startPaneRename(header, title, { sessionId: current.id, title: current.title }); }); header.appendChild(title);
77
94
  var frame = document.createElement("iframe"); frame.className = "split-pane-frame"; frame.dataset.projectSlug = pane.slug;
78
95
  frame.dataset.sessionId = String(pane.sessionId); frame.src = paneUrl(pane); frame.title = pane.title;
96
+ frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
97
+ header.addEventListener('pointerdown', function () { header.dataset.pointerSessionId = frame.dataset.sessionId; header.dataset.pointerBinding = headerBindingStamp(frame); });
98
+ header.addEventListener('click', function (event) {
99
+ if (frame.clayPane && (!livePaneBinding(frame) || event.detail && header.dataset.pointerBinding !== headerBindingStamp(frame))) {
100
+ event.preventDefault(); event.stopImmediatePropagation();
101
+ }
102
+ }, true);
103
+ ['keydown', 'keyup'].forEach(function (name) {
104
+ header.addEventListener(name, function (event) {
105
+ var stamp = headerBindingStamp(frame);
106
+ if (name === 'keydown' && !event.repeat) header.dataset.keyBinding = stamp;
107
+ if (header.inert || header.dataset.keyBinding !== stamp) { event.preventDefault(); event.stopImmediatePropagation(); }
108
+ }, true);
109
+ });
79
110
  var ctxChip = document.createElement("button"); ctxChip.type = "button"; ctxChip.className = "split-pane-context"; ctxChip.title = "Context usage";
80
111
  ctxChip.innerHTML = '<span class="split-pane-context-bar"><span class="split-pane-context-fill"></span></span><span class="split-pane-context-label"></span>';
81
- ctxChip.addEventListener("click", function () { if (frame.contentWindow) frame.contentWindow.postMessage({ type: "clay-pane-toggle-context" }, window.location.origin); }); header.appendChild(ctxChip);
112
+ ctxChip.addEventListener("click", function () { if (frame.contentWindow && (!frame.clayPane || livePaneBinding(frame))) frame.contentWindow.postMessage({ type: "clay-pane-toggle-context", paneBinding: frame.clayPane && frame.clayPane.identity().binding }, window.location.origin); }); header.appendChild(ctxChip);
82
113
  var fullAccess = createPermissionControl("split-pane-full-access hidden", "Driver session permission mode");
83
- updatePaneFullAccessButton(fullAccess, session); bindPermissionControl(fullAccess, function () { return permissionState(sessionById(pane.sessionId)); }); header.appendChild(fullAccess); header.insertBefore(fullAccess, ctxChip);
114
+ updatePaneFullAccessButton(fullAccess, session); bindPermissionControl(fullAccess, function () { return permissionState(sessionById(Number(paneEl.dataset.sessionId))); }); header.appendChild(fullAccess); header.insertBefore(fullAccess, ctxChip);
84
115
  var close = document.createElement("button"); close.type = "button"; close.className = "split-pane-close"; close.title = "Close pane"; close.setAttribute("aria-label", "Close pane"); close.innerHTML = iconHtml("x");
85
116
  close.dataset.sessionId = String(pane.sessionId); close.addEventListener("click", function () { closePane(Number(this.dataset.sessionId)); }); header.appendChild(close);
117
+ frame.addEventListener('clay-pane-ready', function () {
118
+ var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId), current = sessionById(id);
119
+ var live = frame.clayPane && frame.clayPane.identity(), target = paneTarget(store.snap(), getCachedSessions(), id);
120
+ if (!current || !live || !live.ready || !samePaneTarget(target, live.binding) || live.sessionId !== id) return;
121
+ frame.dataset.sessionId = String(id); delete frame.dataset.requestedSessionId;
122
+ frame.dataset.sessionOriginId = live.binding.sessionOriginId;
123
+ updateSplitPaneMetadata(paneEl, { slug: frame.dataset.projectSlug, sessionId: id, title: current.title });
124
+ syncPairChrome(paneEl.parentNode, store.get('splitPanes'));
125
+ });
126
+ frame.addEventListener('clay-pane-activation-failed', function () {
127
+ var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId);
128
+ var fallbackTarget = paneTarget(store.snap(), getCachedSessions(), id);
129
+ if (!fallbackTarget) { header.inert = true; return; }
130
+ frame.dataset.sessionOriginId = fallbackTarget.sessionOriginId;
131
+ frame.src = paneUrl({ slug: frame.dataset.projectSlug, sessionId: id });
132
+ frame.dataset.sessionId = String(id); delete frame.dataset.requestedSessionId;
133
+ });
86
134
  paneEl.appendChild(header); paneEl.appendChild(frame); return paneEl;
87
135
  }
88
136
 
89
137
  export function updateSplitPaneMetadata(paneEl, pane) {
90
138
  var session = sessionById(pane.sessionId); var header = paneEl.querySelector(".split-pane-header");
91
139
  var title = paneEl.querySelector(".split-pane-title"); var frame = paneEl.querySelector(".split-pane-frame"); var vendor = paneEl.querySelector(".split-pane-vendor");
140
+ var live = frame && frame.clayPane && frame.clayPane.identity();
141
+ var retarget = frame && (Number(frame.dataset.sessionId) !== pane.sessionId || frame.dataset.sessionOriginId !== (session && session.sessionOriginId || '') || frame.clayPane && (!live || !live.ready));
142
+ if (retarget && header) {
143
+ header.dispatchEvent(new Event('clay-pane-retarget'));
144
+ header.inert = true;
145
+ header.title = 'Loading conversation';
146
+ if (title) { title.style.display = ''; title.textContent = 'Loading conversation'; }
147
+ if (vendor) vendor.hidden = true;
148
+ var permission = paneEl.querySelector('.split-pane-full-access');
149
+ if (permission) permission.classList.add('hidden');
150
+ resetPaneContextChip(paneEl);
151
+ }
152
+ if (frame && frame.clayPane && retarget) {
153
+ var target = paneTarget(store.snap(), getCachedSessions(), pane.sessionId);
154
+ if (target && frame.clayPane.source === frame.contentWindow && frame.clayPane.document === frame.contentDocument && frame.clayPane.activate(pane.sessionId, false, target)) {
155
+ frame.dataset.requestedSessionId = String(pane.sessionId);
156
+ if (header) header.inert = true;
157
+ return;
158
+ }
159
+ frame.src = paneUrl(pane);
160
+ frame.dataset.sessionId = String(pane.sessionId); frame.dataset.projectSlug = pane.slug;
161
+ frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
162
+ }
163
+ if (header) header.inert = false;
92
164
  paneEl.dataset.sessionId = String(pane.sessionId); paneEl.dataset.projectSlug = pane.slug;
93
165
  if (header) header.title = pane.title; if (title && title.style.display !== "none") title.textContent = pane.title; if (frame) frame.title = pane.title;
94
- if (vendor) { var vendorName = session && session.vendor || "claude"; vendor.src = VENDOR_AVATARS[vendorName] || VENDOR_AVATARS.claude; vendor.title = VENDOR_NAMES[vendorName] || vendorName; }
166
+ if (vendor) { vendor.hidden = false; var vendorName = session && session.vendor || "claude"; vendor.src = VENDOR_AVATARS[vendorName] || VENDOR_AVATARS.claude; vendor.title = VENDOR_NAMES[vendorName] || vendorName; }
167
+ updatePaneFullAccessButton(paneEl.querySelector('.split-pane-full-access'), session);
95
168
  var close = paneEl.querySelector(".split-pane-close");
96
169
  if (close) {
97
170
  close.dataset.sessionId = String(pane.sessionId); close.disabled = false; close.title = "Close pane"; close.setAttribute("aria-label", close.title);
98
171
  }
99
172
  }
100
173
 
174
+ function resetPaneContextChip(paneEl) {
175
+ var chip = paneEl.querySelector('.split-pane-context'); if (!chip) return;
176
+ chip.classList.remove('has-data'); chip.title = 'Context usage';
177
+ var fill = chip.querySelector('.split-pane-context-fill'), label = chip.querySelector('.split-pane-context-label');
178
+ if (fill) { fill.style.width = '0%'; fill.className = 'split-pane-context-fill'; }
179
+ if (label) label.textContent = '';
180
+ }
181
+
101
182
  export function updatePaneContextChip(paneEl, msg) {
102
- if (!paneEl) return; var chip = paneEl.querySelector(".split-pane-context"); if (!chip) return; var pct = msg.pct || 0; if (pct <= 0) return;
183
+ if (!paneEl) return; var chip = paneEl.querySelector(".split-pane-context"); if (!chip) return; var pct = msg.pct || 0; if (pct <= 0) { resetPaneContextChip(paneEl); return; }
103
184
  chip.classList.add("has-data"); var fill = chip.querySelector(".split-pane-context-fill"); var label = chip.querySelector(".split-pane-context-label");
104
185
  fill.style.width = Math.min(100, pct).toFixed(1) + "%"; fill.className = "split-pane-context-fill" + (msg.cls || ""); label.textContent = pct.toFixed(0) + "%";
105
186
  var tip = "Context " + pct.toFixed(0) + "% (" + formatTokens(msg.used || 0) + " / " + formatTokens(msg.win || 0) + " tokens)";
@@ -2,7 +2,7 @@ import { store } from './store.js';
2
2
  import { isProjectSessionReady } from './project-activation.js';
3
3
 
4
4
  export function isCurrentProjectSessionReady(state, socket) {
5
- return isProjectSessionReady(state, socket);
5
+ return socket !== state.splitIdentityBlockedSocket && isProjectSessionReady(state, socket);
6
6
  }
7
7
 
8
8
  export function clearProjectSplitState() {
@@ -6,6 +6,9 @@ import { openWireframe } from './wireframe-workbench.js';
6
6
 
7
7
  import { store } from './store.js';
8
8
  import { getWs } from './ws-ref.js';
9
+ import { livePaneBinding } from './pane-binding.js';
10
+ import { showToast } from './utils.js';
11
+ import { checkComposerDeparture } from './session-rich-drafts.js';
9
12
  import { getCachedSessions } from './sidebar-sessions.js';
10
13
  import { refreshIcons } from './icons.js';
11
14
  import { detachTuiView } from './session-tui-view.js';
@@ -14,11 +17,12 @@ import { syncPairChrome } from './split-pair-ui.js';
14
17
  import { presentMarkdownEdit } from './filebrowser.js';
15
18
  import { autoStartLoginIfNeeded } from './vendor-login.js';
16
19
  import { isCurrentProjectSessionReady } from './split-session-boundary.js';
17
- import { splitPanesMatchProject, reconcileRestoredSplit } from './project-activation.js';
20
+ import { splitPanesMatchProject } from './project-activation.js';
18
21
  import { handlePaneIssueMessage } from './pane-issue-bridge.js';
19
22
  import { handlePaneFileMessage, revealFilesPanel } from './pane-file-bridge.js';
20
23
  import { openFile } from './filebrowser.js';
21
- import { reconcileSplitPanes } from './split-pane-reconciler.js';
24
+ import { restoreSplitGroup, reconcileSplitIdentity } from './split-group-restore.js';
25
+ import { reconcileSplitPanes, splitPaneForSession } from './split-pane-reconciler.js';
22
26
  import { createSplitPane, updateSplitPaneMetadata, updatePaneContextChip, updatePaneFullAccessButton } from './split-pane-renderer.js';
23
27
 
24
28
  var host = null;
@@ -54,12 +58,7 @@ function sessionById(sessionId) {
54
58
  }
55
59
 
56
60
  function paneForSession(sessionId) {
57
- var session = sessionById(sessionId);
58
- return {
59
- slug: store.get('currentSlug'),
60
- sessionId: sessionId,
61
- title: (session && session.title) || ("Session " + sessionId),
62
- };
61
+ return splitPaneForSession(sessionId, store.get('currentSlug'), getCachedSessions());
63
62
  }
64
63
 
65
64
  // Arc-style drop preview: hovering one half folds the live app into the
@@ -90,6 +89,7 @@ function showDropOverlay() {
90
89
  }
91
90
 
92
91
  function switchNativeSession(sessionId, expectedSlug) {
92
+ if (!canLeavePanes()) return;
93
93
  store.set({ splitPanes: null });
94
94
  var ws = getWs();
95
95
  var state = store.snap();
@@ -128,16 +128,16 @@ export function syncPaneTitles() {
128
128
  var split = store.get('splitPanes');
129
129
  if (!split || !split.panes || !host) return;
130
130
  var titleEls = host.querySelectorAll(".split-pane-title");
131
+ var paneEls = host.querySelectorAll('.split-pane');
131
132
  for (var i = 0; i < split.panes.length && i < titleEls.length; i++) {
132
- var session = sessionById(split.panes[i].sessionId);
133
+ var session = sessionById(paneEls[i] && Number(paneEls[i].dataset.sessionId) || split.panes[i].sessionId);
133
134
  if (!session || !session.title) continue;
134
- split.panes[i].title = session.title;
135
135
  if (titleEls[i].parentNode) titleEls[i].parentNode.title = session.title;
136
136
  if (titleEls[i].style.display !== "none") titleEls[i].textContent = session.title;
137
137
  }
138
138
  var accessBtns = host.querySelectorAll(".split-pane-full-access");
139
139
  for (var ai = 0; ai < split.panes.length && ai < accessBtns.length; ai++) {
140
- updatePaneFullAccessButton(accessBtns[ai], sessionById(split.panes[ai].sessionId));
140
+ updatePaneFullAccessButton(accessBtns[ai], sessionById(paneEls[ai] && Number(paneEls[ai].dataset.sessionId) || split.panes[ai].sessionId));
141
141
  }
142
142
  syncPairChrome(host, split);
143
143
  }
@@ -146,6 +146,8 @@ function handlePaneMessage(event) {
146
146
  if (event.origin !== window.location.origin) return;
147
147
  var msg = event.data;
148
148
  if (!msg || !host) return;
149
+ var reusable = Array.from(host.querySelectorAll('.split-pane-frame')).find(function (frame) { return frame.contentWindow === event.source && frame.clayPane; });
150
+ if (reusable && !livePaneBinding(reusable, msg)) return;
149
151
  if (msg.type === "clay-pane-present-wireframe" || msg.type === "clay-pane-present-markdown") {
150
152
  if (msg.message && !msg.message.controlPresentation && trustedStudioFrame(event, msg)) {
151
153
  if (msg.type === "clay-pane-present-wireframe") openWireframe(msg.message);
@@ -177,7 +179,10 @@ function renderSplit(split) {
177
179
  var panes = split && split.panes;
178
180
  if (!panes || panes.length < 2 || panes.length > 3) {
179
181
  placeStickyNotesOverlay(false);
180
- host.innerHTML = "";
182
+ var frames = host.querySelectorAll('.split-pane-frame');
183
+ var retain = frames.length > 0 && frames.length <= 3 && Array.from(frames).every(function (frame) { return frame.dataset.projectSlug === store.get('currentSlug') && frame.clayPane; });
184
+ if (retain) Array.from(frames).forEach(function (frame) { frame.clayPane.suspend(); });
185
+ else host.innerHTML = "";
181
186
  host.classList.remove("visible");
182
187
  nativeApp.classList.remove("split-native-hidden");
183
188
  return;
@@ -217,6 +222,7 @@ function openSplit(side, draggedId) {
217
222
  }
218
223
 
219
224
  export function openGroup(group) {
225
+ if (!canLeavePanes()) return false;
220
226
  if (!group || !Array.isArray(group.members) || group.members.length < 2 || group.members.length > 3) return false;
221
227
  var memberIds = splitGroupMemberIds(group);
222
228
  if (memberIds.length !== group.members.length) return false;
@@ -265,47 +271,9 @@ export function openWorkerWithDriver(worker) {
265
271
  return true;
266
272
  }
267
273
 
268
- // Reload survival: when the restored active session turns out to be a split
269
- // group member, reopen that group. Safe to call often -- no-ops unless a
270
- // group member is active natively with no split open.
274
+ // Keep the public entry shared by connection, sidebar and identity hydration.
271
275
  export function maybeRestoreSplitGroup() {
272
- var state = store.snap();
273
- if (!isCurrentProjectSessionReady(state, getWs())) return;
274
- var split = state.splitPanes;
275
- if (split && !splitPanesMatchProject(split, state.currentSlug)) {
276
- store.set({ splitPanes: null });
277
- return;
278
- }
279
- if (state.paneMode) return;
280
- if (split) {
281
- var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
282
- if (reconciliation.action === "clear") {
283
- store.set({ splitPanes: null });
284
- return;
285
- }
286
- if (reconciliation.action === "rebuild") {
287
- var rebuilt = reconciliation.group;
288
- if (rebuilt.members.some(function (id) { return !sessionById(id); })) {
289
- store.set({ splitPanes: null });
290
- return;
291
- }
292
- store.set({ splitPanes: {
293
- groupId: rebuilt.id,
294
- panes: splitGroupMemberIds(rebuilt).map(function (id) { return paneForSession(id); }),
295
- } });
296
- }
297
- return;
298
- }
299
- var activeId = store.get('activeSessionId');
300
- if (!activeId) return;
301
- var groups = store.get('splitGroups') || [];
302
- for (var i = 0; i < groups.length; i++) {
303
- var members = groups[i].members || [];
304
- if (members.indexOf(activeId) !== -1) {
305
- openGroup(groups[i]);
306
- return;
307
- }
308
- }
276
+ restoreSplitGroup(openGroup);
309
277
  }
310
278
 
311
279
  export function separateGroup(group) {
@@ -369,6 +337,24 @@ function handleSessionDragStart(event) {
369
337
  if (draggedSessionId) showDropOverlay();
370
338
  }
371
339
 
340
+ function openDriverGroup(driver) {
341
+ var state = store.snap();
342
+ if (!driver || driver.sessionRole !== "driver" || !isCurrentProjectSessionReady(state, getWs())) return false;
343
+ var groups = state.splitGroups || [];
344
+ for (var i = 0; i < groups.length; i++) {
345
+ var group = groups[i];
346
+ var roles = splitGroupRoles(group);
347
+ if (!roles || roles.driverId !== driver.id || typeof group.id !== "string" || !group.id) continue;
348
+ var available = roles.workerIds.every(function (id) {
349
+ var worker = sessionById(id);
350
+ return worker && worker.sessionRole === "worker" && worker.parentAvailable === true && worker.parentSessionId === driver.id;
351
+ });
352
+ // Keep mounted conversations when selecting their Driver again.
353
+ if (available) return openGroup(group);
354
+ }
355
+ return false;
356
+ }
357
+
372
358
  function handleSidebarSessionClick(event) {
373
359
  if (!store.get('splitPanes') || event.button !== 0) return;
374
360
  // This document capture listener runs before a row's bubble handlers. Leave
@@ -381,11 +367,22 @@ function handleSidebarSessionClick(event) {
381
367
  if (!sessionId) return;
382
368
  event.preventDefault();
383
369
  event.stopImmediatePropagation();
370
+ if (!canLeavePanes()) return;
384
371
  var session = sessionById(sessionId);
385
372
  if (session && session.sessionRole === "worker" && openWorkerWithDriver(session)) return;
373
+ if (openDriverGroup(session)) return;
386
374
  switchNativeSession(sessionId);
387
375
  }
388
376
 
377
+ function canLeavePanes() {
378
+ if (!host) return true;
379
+ try {
380
+ if (!store.get('splitPanes')) checkComposerDeparture();
381
+ Array.from(host.querySelectorAll('.split-pane-frame')).forEach(function (frame) { if (frame.clayPane) frame.clayPane.checkDraft(); });
382
+ return true;
383
+ } catch (error) { showToast(error.message, 'warn'); return false; }
384
+ }
385
+
389
386
  export function initSplitView() {
390
387
  if (store.get('paneMode')) return;
391
388
  var mainPanels = document.getElementById("main-panels");
@@ -411,6 +408,8 @@ export function initSplitView() {
411
408
  });
412
409
  document.addEventListener("click", handleSidebarSessionClick, true);
413
410
  store.subscribe(function (state, prev) {
411
+ if (state.authIdentityLoaded && !prev.authIdentityLoaded) queueMicrotask(maybeRestoreSplitGroup);
412
+ if (reconcileSplitIdentity(state, prev, host)) { renderSplit(null); return; }
414
413
  if (state.splitPanes !== prev.splitPanes) renderSplit(state.splitPanes);
415
414
  if (state.permissionModePendingByTarget !== prev.permissionModePendingByTarget || state.connected !== prev.connected ||
416
415
  state.skipPermsEnabled !== prev.skipPermsEnabled) syncPaneTitles();
@@ -27,27 +27,55 @@ export function renderTaskControls(root, id) {
27
27
  var ui = getNoteUiState(id);
28
28
  var fields = ui.checklistInputs || {};
29
29
  var busy = ui.pending.length > 0;
30
+ var active = document.activeElement;
31
+ var focusField = active && root.contains(active) ? active.dataset.taskField : undefined;
32
+ var selection = focusField !== undefined ? [active.selectionStart, active.selectionEnd] : null;
30
33
  root.replaceChildren();
31
34
  var form = document.createElement('form'); form.className = 'sn-task-form';
35
+ form.setAttribute('aria-label', 'Checklist');
36
+ form.setAttribute('aria-busy', busy ? 'true' : 'false');
37
+ var feedback = document.createElement('span'); feedback.className = 'sn-task-feedback';
38
+ feedback.setAttribute('role', 'status');
39
+ var saveButton = document.createElement('button');
40
+ function updateFeedback() {
41
+ var current = getNoteUiState(id);
42
+ var dirty = checklistDraftText(current) !== current.text;
43
+ feedback.textContent = busy ? 'Saving…' : current.error ? (dirty ? 'Draft kept. ' : 'Not saved. ') + current.error : dirty ? 'Unsaved changes' : 'All changes saved';
44
+ saveButton.disabled = busy || !dirty;
45
+ }
32
46
  function input(label, value, key) {
33
- var control = document.createElement('input'); control.type = 'text'; control.value = fields[key] !== undefined ? fields[key] : value;
47
+ var control = document.createElement('textarea'); control.rows = 1; control.value = fields[key] !== undefined ? fields[key] : value;
34
48
  control.setAttribute('aria-label', label); control.placeholder = label; control.maxLength = 400;
35
- control.disabled = busy;
49
+ control.readOnly = busy;
50
+ control.dataset.taskField = key;
51
+ control.onkeydown = function (event) {
52
+ if (event.key === 'Enter' && !event.isComposing) { event.preventDefault(); form.requestSubmit(); }
53
+ };
36
54
  control.oninput = function () {
37
55
  var current = getNoteUiState(id);
56
+ // A task remains one persisted line; visual wrapping does not split tasks.
57
+ control.value = control.value.replace(/[\r\n]+/g, ' ');
38
58
  var draft = Object.assign({}, current.checklistInputs || {}); draft[key] = control.value;
39
59
  setNoteUiState(id, { checklistInputs: draft });
60
+ updateFeedback();
40
61
  };
41
62
  return control;
42
63
  }
43
64
  var title = input('Checklist title', (ui.text || '').split('\n')[0], 'title'); title.className = 'sn-task-title'; form.appendChild(title);
44
- getChecklistItems(ui.text).forEach(function (item, position) {
65
+ var items = getChecklistItems(ui.text);
66
+ var completed = 0;
67
+ var summary = document.createElement('div'); summary.className = 'sn-task-summary';
68
+ form.appendChild(summary);
69
+ items.forEach(function (item, position) {
45
70
  var row = document.createElement('div'); row.className = 'sn-task-row';
46
71
  var marker = item.text.match(/^\s*(?:[-*+]\s+)?\[([ xX])\]\s*(.*)$/);
47
72
  var check = document.createElement('button'); check.type = 'button'; check.className = 'sn-check';
48
73
  var checked = ui.checklistState[item.key];
49
74
  if (checked === undefined) checked = marker && marker[1].toLowerCase() === 'x';
50
- check.classList.toggle('checked', !!checked); check.textContent = checked ? '✓' : '☐';
75
+ check.classList.toggle('checked', !!checked);
76
+ check.innerHTML = '<span class="sn-task-box" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none"><path d="m3.5 8 3 3 6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span>';
77
+ row.classList.toggle('is-complete', !!checked);
78
+ if (checked) completed++;
51
79
  check.setAttribute('role', 'checkbox'); check.setAttribute('aria-checked', checked ? 'true' : 'false'); check.setAttribute('aria-label', 'Complete task ' + (position + 1));
52
80
  check.dataset.checklistIndex = item.index; check.dataset.checklistKey = encodeURIComponent(item.key);
53
81
  check.disabled = busy;
@@ -59,8 +87,15 @@ export function renderTaskControls(root, id) {
59
87
  };
60
88
  row.append(check, label, remove); form.appendChild(row);
61
89
  });
62
- var entry = input('New task', '', 'entry'); entry.className = 'sn-task-entry'; form.appendChild(entry);
63
- var saveButton = document.createElement('button'); saveButton.type = 'submit'; saveButton.className = 'sn-task-save'; saveButton.textContent = 'Save tasks'; saveButton.disabled = busy; form.appendChild(saveButton);
90
+ summary.textContent = items.length ? completed + ' of ' + items.length + ' complete' : 'A little space for what’s next';
91
+ var addRow = document.createElement('div'); addRow.className = 'sn-task-add';
92
+ var plus = document.createElement('span'); plus.className = 'sn-task-plus'; plus.innerHTML = iconHtml('plus'); plus.setAttribute('aria-hidden', 'true');
93
+ var entry = input('New task', '', 'entry'); entry.className = 'sn-task-entry'; entry.placeholder = 'Add a task…';
94
+ addRow.append(plus, entry); form.appendChild(addRow);
95
+ var footer = document.createElement('div'); footer.className = 'sn-task-footer';
96
+ saveButton.type = 'submit'; saveButton.className = 'sn-task-save'; saveButton.textContent = 'Save tasks';
97
+ footer.append(feedback, saveButton); form.appendChild(footer);
98
+ updateFeedback();
64
99
  function save(text) {
65
100
  var current = getNoteUiState(id);
66
101
  if (current.pending.length) return;
@@ -68,4 +103,14 @@ export function renderTaskControls(root, id) {
68
103
  }
69
104
  form.onsubmit = function (event) { event.preventDefault(); save(checklistDraftText(getNoteUiState(id))); };
70
105
  root.appendChild(form); refreshIcons(root);
106
+ if (focusField !== undefined) {
107
+ var controls = form.querySelectorAll('[data-task-field]');
108
+ for (var i = 0; i < controls.length; i++) {
109
+ if (controls[i].dataset.taskField === focusField) {
110
+ controls[i].focus({ preventScroll: true });
111
+ controls[i].setSelectionRange(selection[0], selection[1]);
112
+ break;
113
+ }
114
+ }
115
+ }
71
116
  }
@@ -12,7 +12,8 @@ export function speechContext() {
12
12
  var s = store.snap();
13
13
  return JSON.stringify([s.currentSlug, s.activeSessionId, s.dmMode, s.dmKey,
14
14
  s.dmTargetUser && s.dmTargetUser.id, s.homePrimarySurface, s.homeSubSurface,
15
- s.homeChatMateId, s.homeChatSessionId]);
15
+ s.homeChatMateId, s.homeChatSessionId, s.sessionOriginId, s.myUserId, s.sessionActionEpoch || 0,
16
+ s.paneCommittedBinding && s.paneCommittedBinding.generation]);
16
17
  }
17
18
 
18
19
  export function setSpeechStatus(id, message, isError) {
@@ -76,6 +77,7 @@ export function claimSpeech(id, input, recognition) {
76
77
  var active = { id: id, input: input, recognition: recognition, context: speechContext(),
77
78
  before: input.value, lastValue: input.value, writing: false, started: false, timeout: null };
78
79
  active.traceId = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7);
80
+ active.paneBinding = store.get('paneCommittedBinding') || null;
79
81
  active.traceStarted = Date.now();
80
82
  store.set({ speechActive: active, speechRetry: null });
81
83
  return active;
@@ -113,6 +115,7 @@ export function startSTT(id, input) {
113
115
  var active = { id: id, input: input, recognition: recognition, context: speechContext(),
114
116
  before: input.value, lastValue: input.value, writing: false, started: false, timeout: null };
115
117
  recognition.lang = store.get('speechLang') || 'en-US';
118
+ active.paneBinding = store.get('paneCommittedBinding') || null;
116
119
  recognition.continuous = true;
117
120
  recognition.interimResults = true;
118
121
  recognition.onstart = function () {
@@ -26,7 +26,7 @@ async function upload(active, blob) {
26
26
  model: active.model, language: active.language, blob: blob };
27
27
  failSTT(error.name === 'AbortError' ? 'Transcription timed out. Please try again.' : error.message);
28
28
  store.set({ speechRetry: retry });
29
- setTimeout(function () { if (store.get('speechRetry') === retry) store.set({ speechRetry: null }); }, 300000);
29
+ setTimeout(function () { if (store.get('speechRetry') === retry && !store.get('deferredSessionSwitch')) store.set({ speechRetry: null }); }, 300000);
30
30
  return false;
31
31
  }
32
32
  }