clay-server 4.0.0-beta.2 → 4.0.0-beta.21

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 (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -0,0 +1,169 @@
1
+ // Snooze control for the update-available banner.
2
+ //
3
+ // Lives outside app-notifications.js so the banner module does not grow
4
+ // further, and because this is a self-contained concern: one compact menu, one
5
+ // message out, one message in.
6
+ //
7
+ // The client decides nothing. It sends an allowlisted duration key and, as a
8
+ // hint only, the browser's UTC offset so "Tomorrow" lands on the user's
9
+ // morning rather than the server's. The deadline, the target version and the
10
+ // user identity are all resolved server-side, so a tampered payload can only
11
+ // ever produce a refusal or a shorter snooze.
12
+
13
+ import { getWs } from './ws-ref.js';
14
+ import { iconHtml, refreshIcons } from './icons.js';
15
+
16
+ // Must match SNOOZE_OPTIONS in lib/update-snooze.js. The server re-validates
17
+ // every key, so this list is presentation, not policy.
18
+ var SNOOZE_CHOICES = [
19
+ { id: "3h", label: "3 hours" },
20
+ { id: "8h", label: "8 hours" },
21
+ { id: "tomorrow", label: "Tomorrow" },
22
+ ];
23
+
24
+ var openMenu = null;
25
+ var bannerRemover = null;
26
+
27
+ // app-notifications.js owns banner teardown (it runs the exit animation and
28
+ // keeps the clear-all button in sync). Registering the remover here keeps the
29
+ // dependency one-way, so the two modules never form a cycle.
30
+ export function setSnoozeBannerRemover(fn) {
31
+ bannerRemover = typeof fn === "function" ? fn : null;
32
+ }
33
+
34
+ export function isSnoozeChoice(id) {
35
+ for (var i = 0; i < SNOOZE_CHOICES.length; i++) {
36
+ if (SNOOZE_CHOICES[i].id === id) return true;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function closeMenu() {
42
+ if (!openMenu) return;
43
+ var trigger = openMenu.trigger;
44
+ if (openMenu.menu && openMenu.menu.parentNode) {
45
+ openMenu.menu.parentNode.removeChild(openMenu.menu);
46
+ }
47
+ openMenu = null;
48
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
49
+ }
50
+
51
+ function sendSnooze(duration) {
52
+ if (!isSnoozeChoice(duration)) return;
53
+ var ws = getWs();
54
+ if (!ws || ws.readyState !== 1) return;
55
+ // A hint, not an instruction: the server validates the range, computes the
56
+ // instant itself, and clamps the result to its own maximum.
57
+ var offset = -new Date().getTimezoneOffset();
58
+ ws.send(JSON.stringify({ type: "update_snooze", duration: duration, tzOffsetMinutes: offset }));
59
+ }
60
+
61
+ function buildMenu(trigger) {
62
+ var menu = document.createElement("div");
63
+ menu.className = "notif-banner-snooze-menu";
64
+ menu.setAttribute("role", "menu");
65
+ menu.setAttribute("aria-label", "Snooze this update");
66
+
67
+ for (var i = 0; i < SNOOZE_CHOICES.length; i++) {
68
+ (function (choice) {
69
+ var item = document.createElement("button");
70
+ item.type = "button";
71
+ item.className = "notif-banner-snooze-item";
72
+ item.setAttribute("role", "menuitem");
73
+ item.textContent = choice.label;
74
+ item.addEventListener("click", function (e) {
75
+ e.stopPropagation();
76
+ closeMenu();
77
+ sendSnooze(choice.id);
78
+ });
79
+ menu.appendChild(item);
80
+ })(SNOOZE_CHOICES[i]);
81
+ }
82
+
83
+ // Roving focus with the arrow keys, Escape back to the trigger. Enough for a
84
+ // three-item menu to be usable without a pointer.
85
+ menu.addEventListener("keydown", function (e) {
86
+ var items = menu.querySelectorAll(".notif-banner-snooze-item");
87
+ var index = -1;
88
+ for (var i = 0; i < items.length; i++) {
89
+ if (items[i] === document.activeElement) { index = i; break; }
90
+ }
91
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
92
+ e.preventDefault();
93
+ var next = e.key === "ArrowDown" ? index + 1 : index - 1;
94
+ if (next < 0) next = items.length - 1;
95
+ if (next >= items.length) next = 0;
96
+ if (items[next]) items[next].focus();
97
+ return;
98
+ }
99
+ if (e.key === "Escape") {
100
+ e.preventDefault();
101
+ e.stopPropagation();
102
+ closeMenu();
103
+ if (trigger) trigger.focus();
104
+ }
105
+ });
106
+
107
+ return menu;
108
+ }
109
+
110
+ // Append a compact Snooze control to an existing update banner's action row.
111
+ // Reuses the banner's own button shape rather than introducing a new one.
112
+ export function attachSnoozeControl(actionsEl) {
113
+ if (!actionsEl) return null;
114
+
115
+ var wrap = document.createElement("div");
116
+ wrap.className = "notif-banner-snooze";
117
+
118
+ var trigger = document.createElement("button");
119
+ trigger.type = "button";
120
+ trigger.className = "notif-banner-snooze-btn";
121
+ trigger.setAttribute("aria-haspopup", "menu");
122
+ trigger.setAttribute("aria-expanded", "false");
123
+ trigger.setAttribute("aria-label", "Snooze this update notification");
124
+ trigger.innerHTML = iconHtml("clock") + "<span>Snooze</span>";
125
+
126
+ trigger.addEventListener("click", function (e) {
127
+ e.stopPropagation();
128
+ if (openMenu && openMenu.trigger === trigger) {
129
+ closeMenu();
130
+ return;
131
+ }
132
+ closeMenu();
133
+ var menu = buildMenu(trigger);
134
+ wrap.appendChild(menu);
135
+ openMenu = { menu: menu, trigger: trigger };
136
+ trigger.setAttribute("aria-expanded", "true");
137
+ var first = menu.querySelector(".notif-banner-snooze-item");
138
+ if (first) first.focus();
139
+ });
140
+
141
+ wrap.appendChild(trigger);
142
+ actionsEl.appendChild(wrap);
143
+ refreshIcons();
144
+ return wrap;
145
+ }
146
+
147
+ // The server confirms a snooze to every device of that user, so each one drops
148
+ // the banner rather than only the device the user acted on.
149
+ export function handleUpdateSnoozed(msg) {
150
+ closeMenu();
151
+ if (!msg || !msg.ok) return;
152
+ var banner = document.querySelector('.notif-banner[data-notif-id="_update"]');
153
+ if (!banner) return;
154
+ if (msg.version && banner.getAttribute("data-update-version") !== msg.version) return;
155
+ if (bannerRemover) bannerRemover(banner);
156
+ else if (banner.parentNode) banner.parentNode.removeChild(banner);
157
+ }
158
+
159
+ // Dismiss on click-away, blur, or a viewport change so the menu never lingers
160
+ // invisibly. Registered defensively because importing this module must stay
161
+ // side-effect-safe: several suites load the client graph against a partial
162
+ // document/window stub, and a dismiss convenience must not be what breaks them.
163
+ function onGlobal(target, name, fn) {
164
+ if (target && typeof target.addEventListener === "function") target.addEventListener(name, fn);
165
+ }
166
+
167
+ onGlobal(typeof document !== "undefined" ? document : null, "click", function () { closeMenu(); });
168
+ onGlobal(typeof window !== "undefined" ? window : null, "blur", function () { closeMenu(); });
169
+ onGlobal(typeof window !== "undefined" ? window : null, "resize", function () { closeMenu(); });
@@ -10,6 +10,7 @@ import { setSTTLang } from './stt.js';
10
10
  import { userAvatarUrl } from './avatar.js';
11
11
  import { store } from './store.js';
12
12
  import { getWs } from './ws-ref.js';
13
+ import { initCapsulePreference, syncCapsulePreference } from './capsule-preference.js';
13
14
 
14
15
  var ctx = null;
15
16
  var settingsEl = null;
@@ -34,6 +35,8 @@ export function initUserSettings(appCtx) {
34
35
 
35
36
  if (!settingsEl || !openBtn) return;
36
37
 
38
+ initCapsulePreference();
39
+
37
40
  navItems = settingsEl.querySelectorAll('.us-nav-item');
38
41
  sections = settingsEl.querySelectorAll('.us-section');
39
42
  pinForm = document.getElementById('us-pin-form');
@@ -466,6 +469,7 @@ function populateAccount() {
466
469
  if (matesToggle) matesToggle.checked = matesOn;
467
470
  store.set({ projectMateDmsEnabled: matesOn });
468
471
  applyProjectMateDmsClass(matesOn);
472
+ syncCapsulePreference(data.capsulesEnabled === true);
469
473
  if (data.chatLayout) {
470
474
  setChatLayout(data.chatLayout); // update local cache + CSS
471
475
  }
@@ -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,16 +99,30 @@ 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 || "";
@@ -120,7 +135,7 @@ function applyState(card, msg) {
120
135
  }
121
136
  setControlsDisabled(card, status !== "pending");
122
137
  var actions = card.querySelector(".worker-proposal-actions");
123
- if (actions) actions.classList.toggle("hidden", autoApproved);
138
+ if (actions) actions.classList.toggle("hidden", autoAccepted);
124
139
  }
125
140
 
126
141
  function sendDecision(card, accepted) {
@@ -150,10 +165,12 @@ export function renderWorkerProposal(msg) {
150
165
  card.className = "worker-proposal-card";
151
166
  card.dataset.proposalId = msg.proposalId;
152
167
  card.dataset.status = msg.status || "pending";
168
+ var runtimeSelection = workerProposalSelection(msg);
153
169
 
154
170
  var header = document.createElement("div");
155
171
  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>';
172
+ var replacing = msg.action === "replace";
173
+ 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
174
  card.appendChild(header);
158
175
 
159
176
  var summary = document.createElement("p");
@@ -173,12 +190,24 @@ export function renderWorkerProposal(msg) {
173
190
  card.appendChild(plan);
174
191
  }
175
192
 
193
+ if (msg.recommendationRationale) {
194
+ var rationale = document.createElement("div");
195
+ rationale.className = "worker-proposal-rationale";
196
+ var rationaleLabel = document.createElement("span");
197
+ rationaleLabel.textContent = "Driver's recommendation rationale";
198
+ var rationaleText = document.createElement("p");
199
+ rationaleText.textContent = msg.recommendationRationale;
200
+ rationale.appendChild(rationaleLabel);
201
+ rationale.appendChild(rationaleText);
202
+ card.appendChild(rationale);
203
+ }
204
+
176
205
  var config = document.createElement("div");
177
206
  config.className = "worker-proposal-config";
178
207
  var vendorField = document.createElement("label");
179
208
  vendorField.className = "worker-proposal-field worker-proposal-vendor-field";
180
209
  vendorField.innerHTML = '<span>Worker</span><span class="worker-proposal-vendor-control"><img alt=""></span>';
181
- var vendorSelect = buildVendorSelect(msg);
210
+ var vendorSelect = buildVendorSelect(msg, runtimeSelection.vendor);
182
211
  vendorField.querySelector(".worker-proposal-vendor-control").appendChild(vendorSelect);
183
212
  config.appendChild(vendorField);
184
213
  var modelField = document.createElement("label");
@@ -194,6 +223,10 @@ export function renderWorkerProposal(msg) {
194
223
  config.appendChild(effortField);
195
224
  card.appendChild(config);
196
225
 
226
+ var decision = document.createElement("div");
227
+ decision.className = "worker-proposal-decision";
228
+ card.appendChild(decision);
229
+
197
230
  function syncVendor(preferredModel, preferredEffort) {
198
231
  var vendor = vendorSelect.value;
199
232
  var avatar = vendorField.querySelector("img");
@@ -201,12 +234,16 @@ export function renderWorkerProposal(msg) {
201
234
  fillModels(modelSelect, vendor, msg, preferredModel);
202
235
  fillEffort(card, vendor, modelSelect.value, msg, preferredEffort);
203
236
  }
237
+ card._syncWorkerProposalSelection = function (selection) {
238
+ if (selection.vendor && vendorSelect.value !== selection.vendor) vendorSelect.value = selection.vendor;
239
+ syncVendor(selection.model, selection.effort);
240
+ };
204
241
  vendorSelect.addEventListener("change", function () { syncVendor("", "medium"); });
205
242
  modelSelect.addEventListener("change", function () {
206
243
  var active = card.querySelector(".worker-proposal-effort-btn.active");
207
244
  fillEffort(card, vendorSelect.value, modelSelect.value, msg, active ? active.dataset.effort : "medium");
208
245
  });
209
- syncVendor(msg.recommendedModel || "", msg.recommendedEffort || "medium");
246
+ syncVendor(runtimeSelection.model, runtimeSelection.effort);
210
247
 
211
248
  var error = document.createElement("div");
212
249
  error.className = "worker-proposal-error hidden";
@@ -220,12 +257,12 @@ export function renderWorkerProposal(msg) {
220
257
  var decline = document.createElement("button");
221
258
  decline.type = "button";
222
259
  decline.className = "worker-proposal-action secondary";
223
- decline.textContent = "Continue here";
260
+ decline.textContent = replacing ? "Keep current Worker" : "Continue here";
224
261
  decline.addEventListener("click", function () { sendDecision(card, false); });
225
262
  var accept = document.createElement("button");
226
263
  accept.type = "button";
227
264
  accept.className = "worker-proposal-action primary";
228
- accept.innerHTML = iconHtml("panel-right-open") + "<span>Run with Split Worker</span>";
265
+ accept.innerHTML = iconHtml("panel-right-open") + "<span>" + (replacing ? "Replace Split Worker" : "Run with Split Worker") + "</span>";
229
266
  accept.addEventListener("click", function () { sendDecision(card, true); });
230
267
  actions.appendChild(decline);
231
268
  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");