clay-server 4.0.0-beta.8 → 4.0.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 (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -0,0 +1,176 @@
1
+ // Driver-operated Split Worker surface.
2
+ //
3
+ // A configured Split Worker executes work its Driver delegated, so its pane is
4
+ // an execution surface rather than a chat the person types into. This module
5
+ // takes the composer's footprint away and replaces it with one compact,
6
+ // non-interactive status line.
7
+ //
8
+ // What stays, deliberately: the transcript, streaming status, tool and
9
+ // permission progress, scrolling, and the stop control — the human keeps their
10
+ // emergency brake. What goes: the text input, attachments, send, the composer
11
+ // context row, and any Worker-local permission-mode affordance.
12
+ //
13
+ // The role is read from pair state only (`splitGroups` + the displayed
14
+ // session), which is server-broadcast and therefore correct across reconnect,
15
+ // project switch, session switch, dissolve, replacement and role change. There
16
+ // is no second copy of the role and nothing in localStorage. A plain ad-hoc
17
+ // side-by-side split has no pair roles, so it is never locked and keeps its
18
+ // composer.
19
+ //
20
+ // Accessibility: locked controls are removed from the tab order with `disabled`
21
+ // and `tabindex="-1"`, and the composer region is `aria-hidden` while the
22
+ // status line carries the explanation, so a screen reader is told the pane is
23
+ // Driver-controlled instead of finding focusable inputs that do nothing.
24
+
25
+ import { store } from './store.js';
26
+
27
+ var STATUS_ID = "worker-pane-status";
28
+ var LOCK_CLASS = "worker-pane-locked";
29
+
30
+ // Every interactive composer affordance. Queried lazily: a pane iframe and the
31
+ // main window share this markup, and some ids are mobile-only.
32
+ var LOCKED_CONTROL_IDS = [
33
+ "input",
34
+ "send-btn",
35
+ "attach-file-btn",
36
+ "attach-image-btn",
37
+ "input-more-btn",
38
+ "shell-command-btn",
39
+ "stt-btn",
40
+ "schedule-btn",
41
+ "ask-mate-btn",
42
+ "context-sources-add",
43
+ "composer-add-worker-btn",
44
+ "composer-handoff-btn",
45
+ ];
46
+
47
+ function lockedControls() {
48
+ var found = [];
49
+ for (var i = 0; i < LOCKED_CONTROL_IDS.length; i++) {
50
+ var el = document.getElementById(LOCKED_CONTROL_IDS[i]);
51
+ if (el) found.push(el);
52
+ }
53
+ return found;
54
+ }
55
+
56
+ // The configured Worker of a live pair, for the session actually on screen.
57
+ // Returns false for an ad-hoc split, for the Driver, and for any ordinary
58
+ // session, so the composer is only ever taken away in the one real case.
59
+ export function isDriverOperatedView(state) {
60
+ var snapshot = state || store.snap();
61
+ var sessionId = snapshot.paneMode && snapshot.paneSessionId
62
+ ? snapshot.paneSessionId
63
+ : snapshot.activeSessionId;
64
+ if (!sessionId) return false;
65
+ var groups = snapshot.splitGroups || [];
66
+ for (var i = 0; i < groups.length; i++) {
67
+ var pair = groups[i] && groups[i].pair;
68
+ if (!pair) continue;
69
+ if (pair.workerId === sessionId) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ function ensureStatusLine(inputArea) {
75
+ var existing = document.getElementById(STATUS_ID);
76
+ if (existing) return existing;
77
+ var line = document.createElement("div");
78
+ line.id = STATUS_ID;
79
+ line.className = "worker-pane-status";
80
+ // A status region, not a live region: it states a standing condition rather
81
+ // than announcing a change, so it should not interrupt.
82
+ line.setAttribute("role", "status");
83
+ var mark = document.createElement("span");
84
+ mark.className = "worker-pane-status-mark";
85
+ mark.setAttribute("aria-hidden", "true");
86
+ var label = document.createElement("span");
87
+ label.className = "worker-pane-status-label";
88
+ label.textContent = "Controlled by Driver";
89
+ line.appendChild(mark);
90
+ line.appendChild(label);
91
+ inputArea.appendChild(line);
92
+ return line;
93
+ }
94
+
95
+ function applyLock(locked) {
96
+ var inputArea = document.getElementById("input-area");
97
+ var inputWrapper = document.getElementById("input-wrapper");
98
+ if (!inputArea) return;
99
+
100
+ document.body.classList.toggle(LOCK_CLASS, locked);
101
+
102
+ if (inputWrapper) {
103
+ // Hidden from assistive tech as well as sight; the status line speaks for
104
+ // the region instead.
105
+ if (locked) inputWrapper.setAttribute("aria-hidden", "true");
106
+ else inputWrapper.removeAttribute("aria-hidden");
107
+ }
108
+
109
+ var controls = lockedControls();
110
+ for (var i = 0; i < controls.length; i++) {
111
+ var el = controls[i];
112
+ if (locked) {
113
+ if (el.dataset.workerLockPrev === undefined) {
114
+ el.dataset.workerLockPrev = el.disabled ? "1" : "0";
115
+ }
116
+ el.disabled = true;
117
+ el.setAttribute("tabindex", "-1");
118
+ } else if (el.dataset.workerLockPrev !== undefined) {
119
+ // Restore exactly what was there before the lock, so an independently
120
+ // disabled send button stays disabled when the pair dissolves.
121
+ el.disabled = el.dataset.workerLockPrev === "1";
122
+ el.removeAttribute("tabindex");
123
+ delete el.dataset.workerLockPrev;
124
+ }
125
+ }
126
+
127
+ var status = document.getElementById(STATUS_ID);
128
+ if (locked) {
129
+ status = ensureStatusLine(inputArea);
130
+ status.classList.add("visible");
131
+ } else if (status) {
132
+ status.classList.remove("visible");
133
+ }
134
+ }
135
+
136
+ // Skip permissions and any other Worker-local permission-mode affordance are
137
+ // the Driver's to set, so they are removed from this pane only. The Driver pane
138
+ // and ordinary sessions are untouched.
139
+ function applyPermissionChrome(locked) {
140
+ var ids = ["skip-perms-pill", "config-chip-wrap"];
141
+ for (var i = 0; i < ids.length; i++) {
142
+ var el = document.getElementById(ids[i]);
143
+ if (!el) continue;
144
+ if (locked) {
145
+ if (el.dataset.workerLockHidden === undefined) {
146
+ el.dataset.workerLockHidden = el.classList.contains("hidden") ? "1" : "0";
147
+ }
148
+ el.classList.add("hidden");
149
+ el.setAttribute("aria-hidden", "true");
150
+ } else if (el.dataset.workerLockHidden !== undefined) {
151
+ if (el.dataset.workerLockHidden === "0") el.classList.remove("hidden");
152
+ el.removeAttribute("aria-hidden");
153
+ delete el.dataset.workerLockHidden;
154
+ }
155
+ }
156
+ }
157
+
158
+ export function syncWorkerPaneLock(state) {
159
+ var locked = isDriverOperatedView(state);
160
+ applyLock(locked);
161
+ applyPermissionChrome(locked);
162
+ return locked;
163
+ }
164
+
165
+ export function initWorkerPaneLock() {
166
+ syncWorkerPaneLock();
167
+ // Pair state is the source of truth, so every input that could change the
168
+ // role re-runs the same resolution: a dissolve, a replacement, a role swap,
169
+ // switching to another session, or a reconnect that reloads the groups.
170
+ store.subscribe(function (state, prev) {
171
+ if (state.splitGroups === prev.splitGroups
172
+ && state.activeSessionId === prev.activeSessionId
173
+ && state.paneSessionId === prev.paneSessionId) return;
174
+ syncWorkerPaneLock(state);
175
+ });
176
+ }
@@ -0,0 +1,16 @@
1
+ export function workerProposalSelection(msg) {
2
+ var hasSelection = typeof msg.selectedVendor === "string" ||
3
+ typeof msg.selectedModel === "string" || typeof msg.selectedEffort === "string";
4
+ return {
5
+ selected: hasSelection,
6
+ vendor: hasSelection ? (msg.selectedVendor || "") : (msg.recommendedVendor || ""),
7
+ model: hasSelection ? (msg.selectedModel || "") : (msg.recommendedModel || ""),
8
+ effort: hasSelection ? (msg.selectedEffort || "") : (msg.recommendedEffort || "medium"),
9
+ };
10
+ }
11
+
12
+ export function syncWorkerProposalSelection(msg, sync) {
13
+ var selection = workerProposalSelection(msg);
14
+ if (selection.selected && typeof sync === "function") sync(selection);
15
+ return selection;
16
+ }
@@ -2,6 +2,7 @@ import { getWs } from './ws-ref.js';
2
2
  import { addToMessages, scrollToBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
3
3
  import { effortLevelsFor, effortDisplayName } from './app-panels.js';
4
4
  import { iconHtml, refreshIcons } from './icons.js';
5
+ import { workerProposalSelection, syncWorkerProposalSelection } from './worker-proposal-state.js';
5
6
 
6
7
  function optionValue(entry) {
7
8
  if (typeof entry === "string") return entry;
@@ -27,7 +28,7 @@ function planSteps(plan) {
27
28
  }).filter(Boolean).slice(0, 8);
28
29
  }
29
30
 
30
- function buildVendorSelect(msg) {
31
+ function buildVendorSelect(msg, preferred) {
31
32
  var select = document.createElement("select");
32
33
  select.className = "worker-proposal-select worker-proposal-vendor";
33
34
  var installed = (msg.options && msg.options.installedVendors) || [];
@@ -37,7 +38,7 @@ function buildVendorSelect(msg) {
37
38
  option.textContent = VENDOR_NAMES[installed[i]] || installed[i];
38
39
  select.appendChild(option);
39
40
  }
40
- if (installed.indexOf(msg.recommendedVendor) !== -1) select.value = msg.recommendedVendor;
41
+ if (installed.indexOf(preferred) !== -1) select.value = preferred;
41
42
  return select;
42
43
  }
43
44
 
@@ -98,29 +99,38 @@ function statusLabel(status) {
98
99
  if (status === "interrupted") return "Interrupted";
99
100
  if (status === "declined") return "Continuing here";
100
101
  if (status === "error") return "Needs attention";
101
- return "Suggested by Fable";
102
+ return "Awaiting your choice";
103
+ }
104
+
105
+ function decisionLabel(status, autoAccepted) {
106
+ if (autoAccepted) return "Driver recommendation auto-accepted under Full auto";
107
+ if (status === "pending") return "Runtime choice remains with you";
108
+ if (status === "declined") return "Runtime proposal declined by user";
109
+ return "Runtime configuration accepted by user";
102
110
  }
103
111
 
104
112
  function applyState(card, msg) {
105
113
  var status = msg.status || "pending";
106
114
  card.dataset.status = status;
107
- if (msg.autoApproved) card.dataset.autoApproved = "true";
108
- var autoApproved = card.dataset.autoApproved === "true";
115
+ if (msg.autoAccepted || msg.autoApproved) card.dataset.autoAccepted = "true";
116
+ if (msg.autoAccepted === false || msg.autoApproved === false) card.dataset.autoAccepted = "false";
117
+ var autoAccepted = card.dataset.autoAccepted === "true";
118
+ syncWorkerProposalSelection(msg, card._syncWorkerProposalSelection);
109
119
  var badge = card.querySelector(".worker-proposal-status");
110
- if (badge) badge.textContent = statusLabel(status) + (autoApproved ? " · auto-approved" : "");
120
+ if (badge) badge.textContent = statusLabel(status) + (autoAccepted ? " · auto-accepted" : "");
121
+ var decision = card.querySelector(".worker-proposal-decision");
122
+ if (decision) {
123
+ decision.textContent = decisionLabel(status, autoAccepted);
124
+ decision.classList.toggle("automatic", autoAccepted);
125
+ }
111
126
  var error = card.querySelector(".worker-proposal-error");
112
127
  if (error) {
113
128
  error.textContent = msg.error || "";
114
129
  error.classList.toggle("hidden", !msg.error);
115
130
  }
116
- var preview = card.querySelector(".worker-proposal-result");
117
- if (preview && msg.resultPreview) {
118
- preview.textContent = msg.resultPreview;
119
- preview.classList.remove("hidden");
120
- }
121
131
  setControlsDisabled(card, status !== "pending");
122
132
  var actions = card.querySelector(".worker-proposal-actions");
123
- if (actions) actions.classList.toggle("hidden", autoApproved);
133
+ if (actions) actions.classList.toggle("hidden", autoAccepted);
124
134
  }
125
135
 
126
136
  function sendDecision(card, accepted) {
@@ -150,10 +160,12 @@ export function renderWorkerProposal(msg) {
150
160
  card.className = "worker-proposal-card";
151
161
  card.dataset.proposalId = msg.proposalId;
152
162
  card.dataset.status = msg.status || "pending";
163
+ var runtimeSelection = workerProposalSelection(msg);
153
164
 
154
165
  var header = document.createElement("div");
155
166
  header.className = "worker-proposal-header";
156
- header.innerHTML = '<div class="worker-proposal-mark">' + iconHtml("git-branch") + '</div><div class="worker-proposal-heading"><span class="worker-proposal-kicker">DRIVER / WORKER</span><strong>Let a Worker handle the execution</strong></div><span class="worker-proposal-status"></span>';
167
+ var replacing = msg.action === "replace";
168
+ header.innerHTML = '<div class="worker-proposal-mark">' + iconHtml("git-branch") + '</div><div class="worker-proposal-heading"><span class="worker-proposal-kicker">DRIVER / SPLIT WORKER</span><strong>' + (replacing ? "Configure a replacement Split Worker" : "Configure a Split Worker") + '</strong></div><span class="worker-proposal-status"></span>';
157
169
  card.appendChild(header);
158
170
 
159
171
  var summary = document.createElement("p");
@@ -173,12 +185,24 @@ export function renderWorkerProposal(msg) {
173
185
  card.appendChild(plan);
174
186
  }
175
187
 
188
+ if (msg.recommendationRationale) {
189
+ var rationale = document.createElement("div");
190
+ rationale.className = "worker-proposal-rationale";
191
+ var rationaleLabel = document.createElement("span");
192
+ rationaleLabel.textContent = "Driver's recommendation rationale";
193
+ var rationaleText = document.createElement("p");
194
+ rationaleText.textContent = msg.recommendationRationale;
195
+ rationale.appendChild(rationaleLabel);
196
+ rationale.appendChild(rationaleText);
197
+ card.appendChild(rationale);
198
+ }
199
+
176
200
  var config = document.createElement("div");
177
201
  config.className = "worker-proposal-config";
178
202
  var vendorField = document.createElement("label");
179
203
  vendorField.className = "worker-proposal-field worker-proposal-vendor-field";
180
204
  vendorField.innerHTML = '<span>Worker</span><span class="worker-proposal-vendor-control"><img alt=""></span>';
181
- var vendorSelect = buildVendorSelect(msg);
205
+ var vendorSelect = buildVendorSelect(msg, runtimeSelection.vendor);
182
206
  vendorField.querySelector(".worker-proposal-vendor-control").appendChild(vendorSelect);
183
207
  config.appendChild(vendorField);
184
208
  var modelField = document.createElement("label");
@@ -194,6 +218,10 @@ export function renderWorkerProposal(msg) {
194
218
  config.appendChild(effortField);
195
219
  card.appendChild(config);
196
220
 
221
+ var decision = document.createElement("div");
222
+ decision.className = "worker-proposal-decision";
223
+ card.appendChild(decision);
224
+
197
225
  function syncVendor(preferredModel, preferredEffort) {
198
226
  var vendor = vendorSelect.value;
199
227
  var avatar = vendorField.querySelector("img");
@@ -201,31 +229,32 @@ export function renderWorkerProposal(msg) {
201
229
  fillModels(modelSelect, vendor, msg, preferredModel);
202
230
  fillEffort(card, vendor, modelSelect.value, msg, preferredEffort);
203
231
  }
232
+ card._syncWorkerProposalSelection = function (selection) {
233
+ if (selection.vendor && vendorSelect.value !== selection.vendor) vendorSelect.value = selection.vendor;
234
+ syncVendor(selection.model, selection.effort);
235
+ };
204
236
  vendorSelect.addEventListener("change", function () { syncVendor("", "medium"); });
205
237
  modelSelect.addEventListener("change", function () {
206
238
  var active = card.querySelector(".worker-proposal-effort-btn.active");
207
239
  fillEffort(card, vendorSelect.value, modelSelect.value, msg, active ? active.dataset.effort : "medium");
208
240
  });
209
- syncVendor(msg.recommendedModel || "", msg.recommendedEffort || "medium");
241
+ syncVendor(runtimeSelection.model, runtimeSelection.effort);
210
242
 
211
243
  var error = document.createElement("div");
212
244
  error.className = "worker-proposal-error hidden";
213
245
  card.appendChild(error);
214
- var result = document.createElement("div");
215
- result.className = "worker-proposal-result hidden";
216
- card.appendChild(result);
217
246
 
218
247
  var actions = document.createElement("div");
219
248
  actions.className = "worker-proposal-actions";
220
249
  var decline = document.createElement("button");
221
250
  decline.type = "button";
222
251
  decline.className = "worker-proposal-action secondary";
223
- decline.textContent = "Continue here";
252
+ decline.textContent = replacing ? "Keep current Worker" : "Continue here";
224
253
  decline.addEventListener("click", function () { sendDecision(card, false); });
225
254
  var accept = document.createElement("button");
226
255
  accept.type = "button";
227
256
  accept.className = "worker-proposal-action primary";
228
- accept.innerHTML = iconHtml("panel-right-open") + "<span>Run with Split Worker</span>";
257
+ accept.innerHTML = iconHtml("panel-right-open") + "<span>" + (replacing ? "Replace Split Worker" : "Run with Split Worker") + "</span>";
229
258
  accept.addEventListener("click", function () { sendDecision(card, true); });
230
259
  actions.appendChild(decline);
231
260
  actions.appendChild(accept);
@@ -14,6 +14,7 @@
14
14
  @import url("css/input.css?v=20260828-composer-depth1");
15
15
  @import url("css/filebrowser.css?v=20260829-terminal-canvas1");
16
16
  @import url("css/git-panel.css");
17
+ @import url("css/git-placard.css");
17
18
  @import url("css/diff.css");
18
19
  @import url("css/highlight.css");
19
20
  @import url("css/server-settings.css");
@@ -22,6 +23,7 @@
22
23
  @import url("css/mobile-nav.css?v=20260407");
23
24
  @import url("css/loop.css");
24
25
  @import url("css/scheduler.css");
26
+ @import url("css/project-logs.css");
25
27
  @import url("css/scheduler-modal.css");
26
28
  @import url("css/home-hub.css");
27
29
  @import url("css/home-sidebar.css");
@@ -39,7 +41,7 @@
39
41
  @import url("css/session-search.css");
40
42
  @import url("css/tooltip.css");
41
43
  @import url("css/mates.css?v=20260407");
42
- @import url("css/command-palette.css?v=20260901-clay-chat2");
44
+ @import url("css/command-palette.css?v=20260908-log-links2");
43
45
  @import url("css/mention.css");
44
46
  @import url("css/debate.css");
45
47
  @import url("css/home-debate-planning.css");
package/lib/sdk-bridge.js CHANGED
@@ -12,6 +12,7 @@ var { createMessageQueue } = require("./sdk-message-queue");
12
12
  var { attachMessageProcessor } = require("./sdk-message-processor");
13
13
  var homeDebateToolPolicy = require("./home-debate-tool-policy");
14
14
  var yoke = require("./yoke");
15
+ var sessionTitlePolicy = require("./session-title-policy");
15
16
 
16
17
  // Extract serializable tool descriptors from MCP server instances.
17
18
  // Used for IPC to worker processes (McpSdkServerConfigWithInstance is not serializable).
@@ -128,6 +129,9 @@ function createSDKBridge(opts) {
128
129
  var clayTls = opts.clayTls || false;
129
130
  var clayAuthToken = opts.clayAuthToken || null;
130
131
  var getSessionToolDefs = opts.getSessionToolDefs || function () { return []; };
132
+ // Split Worker permission routing. A getter because the pair module is
133
+ // constructed before this bridge exists.
134
+ var getWorkerPermissionRouter = opts.getWorkerPermissionRouter || function () { return null; };
131
135
  var onProcessingChanged = opts.onProcessingChanged || function () {};
132
136
  var _cachedFreshAuthState = null;
133
137
  var _cachedFreshAuthAt = 0;
@@ -601,27 +605,77 @@ function createSDKBridge(opts) {
601
605
  return { behavior: "allow", updatedInput: input };
602
606
  }
603
607
 
604
- // Auto-approve split-pair partner tools. The pair itself is
605
- // user-constructed (the user created the split and its roles and
606
- // watches both panes live), the target is structurally confined to the
607
- // split partner, and a prompt on every delegation hop defeats the
608
- // driver/worker workflow. spawn_sessions is NOT here: it creates new
609
- // sessions and keeps its prompt.
610
- var pairPartnerTools = { send_to_partner: true, read_partner: true, propose_worker: true };
611
- if (toolName.indexOf("mcp__clay-sessions__") === 0) {
612
- var sessionsToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
613
- if (pairPartnerTools[sessionsToolName]) {
608
+ // Auto-approve the non-mutating configuration proposal and the visible
609
+ // Driver/Split Worker pair tools, including the Driver's answer to a Worker
610
+ // permission request. Creation and replacement still require a separate
611
+ // human decision in the proposal card; skip-permissions is never treated as
612
+ // that product choice. Every handler is confined to the caller's exact
613
+ // session/pair and re-checks eligibility, ownership and live identity.
614
+ //
615
+ // spawn_sessions is deliberately absent: it creates arbitrary sessions.
616
+ //
617
+ // Only the two name forms the bridge can genuinely emit are accepted:
618
+ //
619
+ // mcp__clay-sessions__<tool> every MCP-mounted vendor. The server
620
+ // segment is matched exactly, so a tool of
621
+ // the same name on another MCP server is
622
+ // never auto-approved.
623
+ // <tool> a Codex dynamic tool. Codex canonicalizes
624
+ // send_to_partner and read_partner to the MCP
625
+ // form itself, so only the remaining names
626
+ // can legitimately arrive bare.
627
+ var PAIR_MCP_PREFIX = "mcp__clay-sessions__";
628
+ var pairMcpTools = {
629
+ propose_worker: true,
630
+ send_to_partner: true,
631
+ read_partner: true,
632
+ partner_status: true,
633
+ replace_partner: true,
634
+ interrupt_partner: true,
635
+ close_partner: true,
636
+ record_partner_evaluation: true,
637
+ respond_to_worker_permission: true,
638
+ };
639
+ var pairDynamicTools = {
640
+ propose_worker: true,
641
+ partner_status: true,
642
+ replace_partner: true,
643
+ interrupt_partner: true,
644
+ close_partner: true,
645
+ record_partner_evaluation: true,
646
+ respond_to_worker_permission: true,
647
+ };
648
+ if (toolName.indexOf("mcp__") === 0) {
649
+ if (toolName.indexOf(PAIR_MCP_PREFIX) === 0
650
+ && pairMcpTools[toolName.slice(PAIR_MCP_PREFIX.length)]) {
614
651
  return { behavior: "allow", updatedInput: input };
615
652
  }
653
+ } else if (pairDynamicTools[toolName]) {
654
+ return { behavior: "allow", updatedInput: input };
655
+ }
656
+
657
+ // Sticky-note operations are immediately visible and reversible on the
658
+ // shared board. The exact session-bound handlers still enforce ownership,
659
+ // lifecycle rules, and input validation, so a provider prompt adds no
660
+ // authority. Keep this confined to the exact Clay Notes namespace.
661
+ var NOTES_MCP_PREFIX = "mcp__clay-notes__";
662
+ var notesTools = {
663
+ list_notes: true,
664
+ write_note: true,
665
+ close_note: true,
666
+ reopen_note: true,
667
+ remove_note: true,
668
+ };
669
+ if (toolName.indexOf(NOTES_MCP_PREFIX) === 0
670
+ && notesTools[toolName.slice(NOTES_MCP_PREFIX.length)]) {
671
+ return { behavior: "allow", updatedInput: input };
616
672
  }
617
673
 
618
- // Sticky-note reads and writes are immediately visible on the shared
619
- // board. Deletion stays outside the whitelist and therefore prompts;
620
- // its session ownership is enforced again inside the server handler.
621
- var notesToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
622
- var isClayNotesTool = toolName.indexOf("mcp__clay-notes__") === 0
623
- || toolName.indexOf("clay-notes__") !== -1;
624
- if (isClayNotesTool && (notesToolName === "list_notes" || notesToolName === "write_note")) {
674
+ // Project Logs are exact-session, project-bound tools whose authority is
675
+ // enforced again by the server binding. Canonical changes are append-only
676
+ // revisions, and the surface exposes no destructive delete operation, so
677
+ // a second provider permission prompt adds no additional safety.
678
+ if (toolName.indexOf("mcp__clay-logs__") === 0) {
625
679
  return { behavior: "allow", updatedInput: input };
626
680
  }
627
681
 
@@ -720,6 +774,19 @@ function createSDKBridge(opts) {
720
774
  // Full-auto mode: auto-approve everything except AskUserQuestion
721
775
  // (which still needs to go through the user interaction flow).
722
776
  var clayPermissionMode = session.permissionMode || sm.currentPermissionMode;
777
+ // A configured Split Worker is Driver-operated, so it runs under its
778
+ // Driver's permission mode, re-resolved from the live pair on every single
779
+ // decision. Nothing is copied at creation and the Worker's own persisted
780
+ // setting is never written, so toggling skip permissions on the Driver
781
+ // applies to the Worker's next call in either direction, mid-turn included.
782
+ var _wpMode = getWorkerPermissionRouter();
783
+ if (_wpMode && typeof _wpMode.inheritedPermissionMode === "function") {
784
+ var inherited = _wpMode.inheritedPermissionMode(session);
785
+ if (inherited) clayPermissionMode = inherited;
786
+ }
787
+ // AskUserQuestion is genuine user input, not an authorization, so it is
788
+ // excluded here exactly as it always was: a Driver in skip mode still
789
+ // cannot silently answer a question addressed to the person.
723
790
  if (clayPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
724
791
  return Promise.resolve({ behavior: "allow", updatedInput: input });
725
792
  }
@@ -747,6 +814,22 @@ function createSDKBridge(opts) {
747
814
  return Promise.resolve({ behavior: "allow", updatedInput: input });
748
815
  }
749
816
 
817
+ // A visible Split Worker's remaining prompts belong to its paired Driver,
818
+ // not to the Worker pane. This sits below every auto-decision above, so
819
+ // skip permissions, the whitelist, allowedTools and the loop/debate
820
+ // policies all still resolve first and never wait on a Driver.
821
+ var _wp = getWorkerPermissionRouter();
822
+ if (_wp) {
823
+ var routed = _wp.routeIfWorker(session, {
824
+ toolName: toolName,
825
+ input: input,
826
+ toolUseId: opts.toolUseID,
827
+ decisionReason: opts.decisionReason || "",
828
+ signal: opts.signal,
829
+ });
830
+ if (routed) return routed;
831
+ }
832
+
750
833
  // Regular tool permission request: send to client and wait
751
834
  return new Promise(function(resolve) {
752
835
  var requestId = crypto.randomUUID();
@@ -1540,25 +1623,33 @@ function createSDKBridge(opts) {
1540
1623
  if (selectedUserInputMode === "fallback") {
1541
1624
  sessionToolDefs = sessionToolDefs.concat(yoke.userInput.fallbackToolDefs(sessionUserInputHandler));
1542
1625
  }
1626
+ function findSessionToolDef(toolName) {
1627
+ // Codex restores a thread's original dynamic-tool catalog on resume, but
1628
+ // pair state can change after this query starts. Resolve live session
1629
+ // handlers first so the dispatcher cannot lag behind that catalog. Keep
1630
+ // the query snapshot as a fallback for query-bound tools such as the
1631
+ // structured-input fallback, whose handler exists only for this turn.
1632
+ var liveToolDefs = getSessionToolDefs(session) || [];
1633
+ for (var liveIndex = 0; liveIndex < liveToolDefs.length; liveIndex++) {
1634
+ if (liveToolDefs[liveIndex].name === toolName) return liveToolDefs[liveIndex];
1635
+ }
1636
+ for (var snapshotIndex = 0; snapshotIndex < sessionToolDefs.length; snapshotIndex++) {
1637
+ if (sessionToolDefs[snapshotIndex].name === toolName) return sessionToolDefs[snapshotIndex];
1638
+ }
1639
+ return null;
1640
+ }
1543
1641
 
1544
- // Derive an explicit session title for fresh queries so the SDK records
1545
- // it at session creation and skips its own auto-generation. This also
1546
- // lets us short-circuit autoGenerateTitle below for the common case.
1642
+ // Give fresh queries an immediate provisional label. The visible user
1643
+ // message in history wins over `text`, which may contain an internal
1644
+ // provider wrapper rather than what the person typed.
1547
1645
  // Only applied to NEW sessions (no cliSessionId yet) — when resuming,
1548
1646
  // the SDK ignores Options.title in favor of the persisted title.
1549
1647
  var initialTitle = null;
1648
+ var initialTitleProvisional = false;
1550
1649
  if (!session.cliSessionId && !session.titleManuallySet && !session.titleAutoGenerated) {
1551
- if (session.title) {
1552
- // Loop / scheduled / mate-seeded sessions arrive with a title already set.
1553
- initialTitle = session.title;
1554
- } else if (typeof text === "string") {
1555
- // Derive a quick first-line snippet from the user's first message.
1556
- // Skip if too short to be meaningful — fall back to autoGenerateTitle.
1557
- var firstLine = text.replace(/\s+/g, " ").trim();
1558
- if (firstLine.length >= 10) {
1559
- initialTitle = firstLine.length > 60 ? firstLine.substring(0, 60) : firstLine;
1560
- }
1561
- }
1650
+ var initialTitleChoice = sessionTitlePolicy.selectInitialTitle(session, text);
1651
+ initialTitle = initialTitleChoice.title;
1652
+ initialTitleProvisional = initialTitleChoice.provisional;
1562
1653
  }
1563
1654
 
1564
1655
  var queryOpts = {
@@ -1587,10 +1678,9 @@ function createSDKBridge(opts) {
1587
1678
  toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
1588
1679
  dynamicTools: sessionToolDefs,
1589
1680
  callDynamicTool: function (toolName, args) {
1590
- for (var dti = 0; dti < sessionToolDefs.length; dti++) {
1591
- if (sessionToolDefs[dti].name === toolName && typeof sessionToolDefs[dti].handler === "function") {
1592
- return sessionToolDefs[dti].handler(args || {});
1593
- }
1681
+ var toolDef = findSessionToolDef(toolName);
1682
+ if (toolDef && typeof toolDef.handler === "function") {
1683
+ return toolDef.handler(args || {});
1594
1684
  }
1595
1685
  return Promise.reject(new Error("Session tool not found: " + toolName));
1596
1686
  },
@@ -1598,12 +1688,8 @@ function createSDKBridge(opts) {
1598
1688
  abortController: linuxUser ? undefined : session.abortController,
1599
1689
  canUseTool: function(toolName, input, toolOpts) {
1600
1690
  var permissionName = toolName;
1601
- for (var pti = 0; pti < sessionToolDefs.length; pti++) {
1602
- if (sessionToolDefs[pti].name === toolName && sessionToolDefs[pti].permissionName) {
1603
- permissionName = sessionToolDefs[pti].permissionName;
1604
- break;
1605
- }
1606
- }
1691
+ var permissionToolDef = findSessionToolDef(toolName);
1692
+ if (permissionToolDef && permissionToolDef.permissionName) permissionName = permissionToolDef.permissionName;
1607
1693
  return handleCanUseTool(session, permissionName, input, toolOpts);
1608
1694
  },
1609
1695
  onUserInputRequest: function(request, respond) {
@@ -1653,16 +1739,16 @@ function createSDKBridge(opts) {
1653
1739
  try {
1654
1740
  handle = await sessionAdapter.createQuery(queryOpts);
1655
1741
  console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
1656
- // SDK accepted the explicit title adopt it locally so the session
1657
- // list reflects it immediately and autoGenerateTitle skips this
1658
- // session (titleAutoGenerated gates re-trigger).
1742
+ // SDK accepted the explicit title. Provisional labels remain eligible
1743
+ // for semantic generation after the first Home exchange (or the normal
1744
+ // project threshold); seeded titles remain fixed.
1659
1745
  if (initialTitle && !session.title) {
1660
1746
  session.title = initialTitle;
1661
- session.titleAutoGenerated = true;
1747
+ session.titleAutoGenerated = !initialTitleProvisional;
1662
1748
  sm.saveSessionFile(session);
1663
1749
  sm.broadcastSessionList();
1664
1750
  } else if (initialTitle && session.title === initialTitle) {
1665
- session.titleAutoGenerated = true;
1751
+ session.titleAutoGenerated = !initialTitleProvisional;
1666
1752
  sm.saveSessionFile(session);
1667
1753
  }
1668
1754
  } catch (e) {
@@ -2035,6 +2121,7 @@ function createSDKBridge(opts) {
2035
2121
  // No active query — just store the model for next startQuery
2036
2122
  session.model = model;
2037
2123
  sm.saveSessionFile(session);
2124
+ sm.broadcastSessionList();
2038
2125
  sm.defaultModelByVendor = sm.defaultModelByVendor || {};
2039
2126
  // Confirm against the session vendor so a non-default vendor does not
2040
2127
  // receive model metadata labeled as the project's default adapter.
@@ -2048,6 +2135,7 @@ function createSDKBridge(opts) {
2048
2135
  await session.queryInstance.setModel(model);
2049
2136
  session.model = model;
2050
2137
  sm.saveSessionFile(session);
2138
+ sm.broadcastSessionList();
2051
2139
  sm.defaultModelByVendor = sm.defaultModelByVendor || {};
2052
2140
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
2053
2141
  sm.defaultModelByVendor[sessionVendor] = model;
@@ -2070,6 +2158,7 @@ function createSDKBridge(opts) {
2070
2158
  sm.currentEffortByVendor[effortVendor] = clamped;
2071
2159
  session.effort = clamped;
2072
2160
  sm.saveSessionFile(session);
2161
+ sm.broadcastSessionList();
2073
2162
  sm.currentEffort = clamped;
2074
2163
  if (!session.queryInstance) {
2075
2164
  return clamped;