clay-server 4.0.0-beta.12 → 4.0.0-beta.14

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.
@@ -1,4 +1,4 @@
1
- /* Project Logs — restrained, factual project ledger. */
1
+ /* Project Logs — a restrained editorial ledger, designed for close reading. */
2
2
 
3
3
  /* Wide screens: bounded workbench window beside #app, matching the geometry of
4
4
  the document viewer and the terminal. */
@@ -94,90 +94,32 @@
94
94
  .project-logs-icon-btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
95
95
  .project-logs-icon-btn:disabled { opacity: 0.4; cursor: default; }
96
96
  .project-logs-window-actions { display: inline-flex; align-items: center; gap: 6px; }
97
- /* --- Ambient edge handle ---------------------------------------------
98
- A slim notch on the right workbench boundary, offered only where a real
99
- pointer can hover. Touch keeps the explicit toolbar route. */
100
- @media (min-width: 1024px) {
101
- .project-logs-handle {
102
- position: absolute;
103
- top: 50%;
104
- right: 0;
105
- transform: translateY(-50%);
106
- width: 14px;
107
- height: 88px;
108
- padding: 0;
109
- display: flex;
110
- align-items: center;
111
- justify-content: center;
112
- border: 1px solid var(--border);
113
- border-right: 0;
114
- border-radius: 8px 0 0 8px;
115
- background: var(--bg-alt);
116
- cursor: pointer;
117
- z-index: 40;
118
- transition: width 0.16s ease, background 0.16s ease;
119
- }
120
- .project-logs-handle:hover,
121
- .project-logs-handle:focus-visible { width: 18px; background: var(--bg); }
122
- .project-logs-handle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
123
- .project-logs-handle.hidden { display: none; }
124
- .project-logs-handle-notch {
125
- width: 3px;
126
- height: 34px;
127
- border-radius: 999px;
128
- background: var(--border);
129
- transition: background 0.16s ease, height 0.16s ease;
130
- }
131
- .project-logs-handle:hover .project-logs-handle-notch { background: var(--text-dimmer); }
132
- /* Unread: a brighter notch, no motion of its own. */
133
- .project-logs-handle.has-unread .project-logs-handle-notch { background: var(--accent); height: 46px; }
134
- }
135
-
136
- /* Touch and narrow viewports never depend on hover. */
137
- @media (max-width: 1023px) {
138
- .project-logs-handle { display: none; }
139
- }
140
-
141
- /* A preview is the same pane, revealed but uncommitted. */
142
- #project-logs-panel.project-logs-previewing { box-shadow: 0 0 0 1px var(--accent), 4px 8px 24px rgba(var(--shadow-rgb), 0.2); }
143
-
144
- /* --- Unread cues --- */
97
+ /* The explicit tool button is the only discovery affordance. */
145
98
  #project-logs-btn.project-logs-unread { color: var(--accent); }
146
99
  #project-logs-count { background: var(--accent); color: #fff; }
147
100
 
148
- @keyframes project-logs-pulse {
149
- 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
150
- 70% { box-shadow: 0 0 0 7px transparent; }
151
- 100% { box-shadow: 0 0 0 0 transparent; }
152
- }
153
- .project-logs-pulse { animation: project-logs-pulse 1.1s ease-out 2; }
154
-
155
- /* Reduced motion keeps the static unread marker and drops every flourish. */
156
- @media (prefers-reduced-motion: reduce) {
157
- .project-logs-pulse { animation: none; }
158
- .project-logs-handle { transition: none; }
159
- .project-logs-handle-notch { transition: none; }
160
- }
161
-
162
101
  /* --- Ledger toolbar --- */
163
- .project-logs-toolbar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
102
+ .project-logs-toolbar { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; background: color-mix(in srgb, var(--bg-alt) 72%, transparent); }
164
103
  .project-logs-toolbar.hidden { display: none; }
165
- .project-logs-search { flex: 1; min-width: 0; height: 32px; display: flex; align-items: center; gap: 7px; padding: 0 9px; border: 1px solid var(--border); border-radius: 7px; background: var(--bg-alt); color: var(--text-dimmer); }
104
+ .project-logs-search { flex: 1; min-width: 0; height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-dimmer); }
166
105
  .project-logs-search .lucide { width: 14px; flex: 0 0 auto; }
167
106
  .project-logs-search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--text); font: inherit; font-size: 12px; }
168
- .project-logs-filter { height: 32px; max-width: 42%; padding: 0 8px; border: 1px solid var(--border); border-radius: 7px; background: var(--bg-alt); color: var(--text-secondary); font: inherit; font-size: 12px; cursor: pointer; }
107
+ .project-logs-search:focus-within, .project-logs-filter:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent); border-color: var(--accent); }
108
+ .project-logs-filter { height: 36px; max-width: 42%; padding: 0 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-secondary); font: inherit; font-size: 12px; cursor: pointer; }
169
109
 
170
110
  /* --- Ledger list: full pane, chronological, commit-history feel --- */
171
- .project-logs-list { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 0; }
111
+ .project-logs-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 0 18px; }
172
112
  .project-logs-list.hidden { display: none; }
173
- .project-log-row { width: 100%; display: block; padding: 12px 14px; border: 0; border-bottom: 1px solid var(--border-subtle); background: transparent; color: inherit; text-align: left; cursor: pointer; }
174
- .project-log-row:hover { background: rgba(var(--overlay-rgb), 0.05); }
113
+ .project-log-row { width: 100%; display: block; padding: 15px 18px 14px; border: 0; border-bottom: 1px solid var(--border-subtle); border-left: 2px solid transparent; background: transparent; color: inherit; text-align: left; cursor: pointer; transition: background 0.14s ease, border-color 0.14s ease; }
114
+ .project-log-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); border-left-color: var(--accent); }
115
+ .project-log-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
175
116
  .project-log-row:last-child { border-bottom: 0; }
176
117
  .project-log-row-head { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
177
- .project-log-row-title { flex: 1; min-width: 0; font-size: 13px; font-weight: 650; color: var(--text); }
178
- .project-log-row-summary { margin: 5px 0 0; color: var(--text-secondary); font-size: 12px; line-height: 1.5; }
179
- .project-log-row-meta { margin-top: 6px; color: var(--text-dimmer); font-size: 10.5px; font-family: var(--font-mono); }
180
- .project-logs-list-empty { padding: 32px 20px; color: var(--text-dimmer); font-size: 12px; text-align: center; line-height: 1.6; }
118
+ .project-log-row-title { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
119
+ .project-log-row-summary { margin: 7px 0 0; color: var(--text-secondary); font-size: 12.5px; line-height: 1.55; }
120
+ .project-log-row-meta { margin-top: 8px; color: var(--text-dimmer); font-size: 10.5px; font-family: var(--font-mono); }
121
+ .project-logs-list-empty { max-width: 320px; margin: 54px auto; padding: 20px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); color: var(--text-dimmer); font-size: 12px; text-align: center; line-height: 1.7; }
122
+ .project-logs-list[aria-busy="true"] .project-logs-list-empty { color: var(--text-secondary); font-family: var(--font-mono); }
181
123
 
182
124
  /* Category chip and priority signal. Categories are project-defined, so the
183
125
  chip is deliberately neutral: no per-category selectors and no exhaustive
@@ -191,13 +133,13 @@
191
133
  /* --- Entry detail: full pane, replaces the list --- */
192
134
  .project-logs-detail { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; }
193
135
  .project-logs-detail.hidden { display: none; }
194
- .project-log-document { max-width: 820px; margin: 0 auto; padding: 22px 22px 40px; }
136
+ .project-log-document { max-width: 760px; margin: 0 auto; padding: 32px 28px 52px; }
195
137
  #project-logs-panel.panel-fullscreen .project-log-document { padding: clamp(28px, 5vw, 64px); }
196
138
  .project-log-doc-chips { display: flex; align-items: center; gap: 7px; margin-bottom: 10px; }
197
- .project-log-document h1 { margin: 0 0 8px; color: var(--text); font-family: var(--font-display); font-size: clamp(20px, 2.4vw, 28px); line-height: 1.2; }
198
- .project-log-doc-summary { margin: 0 0 10px; color: var(--text-secondary); font-size: 13px; line-height: 1.55; }
199
- .project-log-byline { margin: 0 0 18px; padding-bottom: 16px; border-bottom: 1px solid var(--border-subtle); color: var(--text-dimmer); font-size: 11px; font-family: var(--font-mono); }
200
- .project-log-markdown { line-height: 1.65; }
139
+ .project-log-document h1 { margin: 0 0 10px; color: var(--text); font-family: var(--font-display); font-size: clamp(24px, 2.8vw, 34px); letter-spacing: -0.025em; line-height: 1.12; }
140
+ .project-log-doc-summary { margin: 0 0 13px; color: var(--text-secondary); font-size: 14px; line-height: 1.6; }
141
+ .project-log-byline { margin: 0 0 25px; padding-bottom: 18px; border-bottom: 2px solid var(--border-subtle); color: var(--text-dimmer); font-size: 11px; font-family: var(--font-mono); }
142
+ .project-log-markdown { font-size: 14px; line-height: 1.75; }
201
143
 
202
144
  /* --- Discussion --- */
203
145
  .project-log-discussion { margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--border-subtle); }
@@ -230,5 +172,9 @@
230
172
 
231
173
  @media (max-width: 768px) {
232
174
  .project-logs-subtitle { display: none; }
233
- .project-log-document { padding: 18px 16px 32px; }
175
+ .project-logs-topbar { gap: 8px; padding: 0 12px; }
176
+ .project-logs-toolbar { padding: 10px 12px; }
177
+ .project-logs-filter { max-width: 44%; }
178
+ .project-log-row { padding: 14px 14px 13px; }
179
+ .project-log-document { padding: 24px 18px 38px; }
234
180
  }
@@ -159,12 +159,12 @@ function renderRow(entry, onOpen) {
159
159
  return row;
160
160
  }
161
161
 
162
- export function renderList(listEl, entries, onOpen) {
162
+ export function renderList(listEl, entries, onOpen, emptyState) {
163
163
  listEl.innerHTML = "";
164
164
  if (!entries.length) {
165
165
  var empty = document.createElement("div");
166
166
  empty.className = "project-logs-list-empty";
167
- empty.textContent = "No logs yet. This project's agent sessions record decisions and work here.";
167
+ empty.textContent = emptyState || "No logs yet. This project's agent sessions record decisions and work here.";
168
168
  listEl.appendChild(empty);
169
169
  return;
170
170
  }
@@ -15,15 +15,10 @@ import { getWs } from './ws-ref.js';
15
15
  import { refreshIcons } from './icons.js';
16
16
  import { showToast } from './utils.js';
17
17
  import { closeScheduler } from './scheduler.js';
18
- import { hideNotes } from './sticky-notes.js';
19
18
  import { closeNotesBrowser } from './sticky-notes-browser.js';
20
19
  import { closeFileViewer } from './filebrowser.js';
21
20
  import { closeTerminal } from './terminal.js';
22
21
  import { renderFilter, renderList, renderDetail } from './project-logs-render.js';
23
- import {
24
- initProjectLogsAmbient, bindPanelHover, noteCanonicalUpdate,
25
- acknowledgeUpdates, resetAmbient, syncAmbient, hidePreview, pinFromPreview
26
- } from './project-logs-ambient.js';
27
22
 
28
23
  var panel = null;
29
24
  var listEl = null;
@@ -34,6 +29,37 @@ var backBtn = null;
34
29
  var searchTimer = null;
35
30
  var requestCounter = 0;
36
31
 
32
+ function syncUnreadBadge() {
33
+ var button = document.getElementById("project-logs-btn");
34
+ if (!button) return;
35
+ var unread = store.get('projectLogsUnread') || 0;
36
+ button.classList.toggle("project-logs-unread", unread > 0);
37
+ var badge = document.getElementById("project-logs-count");
38
+ if (!badge) return;
39
+ badge.textContent = unread > 0 ? String(unread) : "";
40
+ badge.classList.toggle("hidden", unread < 1);
41
+ }
42
+
43
+ function acknowledgeUpdates() {
44
+ if (!store.get('projectLogsUnread')) return;
45
+ store.set({ projectLogsUnread: 0 });
46
+ syncUnreadBadge();
47
+ }
48
+
49
+ function noteCanonicalUpdate(msg) {
50
+ if (!msg || !msg.ref || !msg.revision) return false;
51
+ var seen = store.get('projectLogsSeenRevisions') || {};
52
+ if (seen[msg.ref] >= msg.revision) return false;
53
+ var next = Object.assign({}, seen);
54
+ next[msg.ref] = msg.revision;
55
+ store.set({
56
+ projectLogsSeenRevisions: next,
57
+ projectLogsUnread: store.get('projectLogsOpen') ? 0 : (store.get('projectLogsUnread') || 0) + 1,
58
+ });
59
+ syncUnreadBadge();
60
+ return true;
61
+ }
62
+
37
63
  function nextRequestId(prefix) {
38
64
  requestCounter += 1;
39
65
  return prefix + "-" + Date.now() + "-" + requestCounter;
@@ -80,11 +106,7 @@ function ensurePanel() {
80
106
  backBtn = panel.querySelector("#project-logs-back");
81
107
 
82
108
  panel.querySelector("#project-logs-close").addEventListener("click", closeProjectLogs);
83
- backBtn.addEventListener("click", function () { pinOnInteraction(); showList(); });
84
- // Any meaningful interaction inside a preview commits to it.
85
- searchEl.addEventListener("focus", pinOnInteraction);
86
- panel.addEventListener("pointerdown", pinOnInteraction);
87
- bindPanelHover(panel);
109
+ backBtn.addEventListener("click", showList);
88
110
  panel.querySelector("#project-logs-wide").addEventListener("click", function () {
89
111
  applyWindowState(!store.get('projectLogsWide'), store.get('projectLogsFullscreen'));
90
112
  });
@@ -98,24 +120,6 @@ function ensurePanel() {
98
120
  refreshIcons();
99
121
  }
100
122
 
101
- // A preview reveals the pane without committing to it: no acknowledgement, no
102
- // open state, and no list refresh beyond what is already on screen.
103
- function revealPreview() {
104
- ensurePanel();
105
- if (!panel) return;
106
- panel.classList.remove("hidden");
107
- panel.classList.add("project-logs-previewing");
108
- applyWindowState(store.get('projectLogsWide'), false);
109
- if (!store.get('projectLogsEntries') || !store.get('projectLogsEntries').length) requestList();
110
- }
111
-
112
- function hideRevealedPreview() {
113
- if (!panel) return;
114
- panel.classList.remove("project-logs-previewing");
115
- if (store.get('projectLogsOpen')) return;
116
- panel.classList.add("hidden");
117
- }
118
-
119
123
  // Width and fullscreen are transient view state, so they live in the store like
120
124
  // every other mutable UI value. `panel-fullscreen` is the shared class the
121
125
  // document viewer and terminal already use.
@@ -147,13 +151,6 @@ function showList() {
147
151
  listEl.scrollTop = store.get('projectLogsListScroll') || 0;
148
152
  }
149
153
 
150
- // A preview becomes a real open the moment the user does something with it.
151
- function pinOnInteraction() {
152
- if (store.get('projectLogsOpen')) return;
153
- if (!store.get('projectLogsPreview')) return;
154
- pinFromPreview();
155
- }
156
-
157
154
  function showDetail(entry) {
158
155
  if (!panel || !entry) return;
159
156
  store.set({ projectLogsListScroll: listEl.scrollTop, projectLogsView: "detail", projectLogsSelectedRef: entry.ref });
@@ -170,12 +167,21 @@ function showDetail(entry) {
170
167
  function requestList() {
171
168
  var requestId = nextRequestId("list-logs");
172
169
  store.set({ projectLogsListRequestId: requestId });
173
- send({
170
+ if (listEl && !store.get('projectLogsEntries').length) {
171
+ listEl.setAttribute("aria-busy", "true");
172
+ renderList(listEl, [], requestEntry, "Loading the project ledger...");
173
+ }
174
+ if (!send({
174
175
  type: "project_logs_list",
175
176
  requestId: requestId,
176
177
  query: searchEl ? searchEl.value.trim() : "",
177
178
  category: store.get('projectLogsCategory') || "",
178
- });
179
+ })) {
180
+ if (listEl) {
181
+ listEl.removeAttribute("aria-busy");
182
+ renderList(listEl, [], requestEntry, "Logs are unavailable while disconnected.");
183
+ }
184
+ }
179
185
  }
180
186
 
181
187
  function requestEntry(ref) {
@@ -185,7 +191,6 @@ function requestEntry(ref) {
185
191
  }
186
192
 
187
193
  function submitComment(ref, body, statusEl, inputEl) {
188
- pinOnInteraction();
189
194
  var requestId = nextRequestId("comment-log");
190
195
  store.set({ projectLogsCommentRequestId: requestId, projectLogsCommentInput: inputEl || null });
191
196
  if (!send({ type: "project_log_comment", requestId: requestId, ref: ref, body: body })) {
@@ -197,7 +202,6 @@ function submitComment(ref, body, statusEl, inputEl) {
197
202
  }
198
203
 
199
204
  function applyFilter(category) {
200
- pinOnInteraction();
201
205
  store.set({ projectLogsCategory: category || "" });
202
206
  requestList();
203
207
  }
@@ -211,18 +215,13 @@ export function initProjectLogs() {
211
215
  if (store.get('projectLogsOpen')) closeProjectLogs();
212
216
  else openProjectLogs();
213
217
  });
214
- initProjectLogsAmbient({
215
- reveal: revealPreview,
216
- hide: hideRevealedPreview,
217
- pin: openProjectLogs,
218
- });
218
+ syncUnreadBadge();
219
219
  store.subscribe(function (state, previous) {
220
220
  if (state.currentSlug === previous.currentSlug) return;
221
221
  closeProjectLogs();
222
222
  applyWindowState(false, false);
223
223
  // A different project must never inherit the previous project's unread
224
- // state or animate its updates.
225
- resetAmbient();
224
+ // state.
226
225
  store.set({
227
226
  projectLogsEntries: [],
228
227
  projectLogsSelectedRef: null,
@@ -231,8 +230,11 @@ export function initProjectLogs() {
231
230
  projectLogsListScroll: 0,
232
231
  projectLogsListRequestId: null,
233
232
  projectLogsReadRequestId: null,
234
- projectLogsCommentRequestId: null
233
+ projectLogsCommentRequestId: null,
234
+ projectLogsUnread: 0,
235
+ projectLogsSeenRevisions: {}
235
236
  });
237
+ syncUnreadBadge();
236
238
  });
237
239
  }
238
240
 
@@ -243,37 +245,31 @@ export function openProjectLogs() {
243
245
  if (!panel) return;
244
246
  closeScheduler();
245
247
  closeNotesBrowser();
246
- hideNotes();
247
248
  // Claim the single right workbench slot.
248
249
  try { closeFileViewer(); } catch (e) {}
249
250
  try { closeTerminal(); } catch (e) {}
250
251
  panel.classList.remove("hidden");
251
- panel.classList.remove("project-logs-previewing");
252
252
  applyWindowState(store.get('projectLogsWide'), false);
253
253
  var button = document.getElementById("project-logs-btn");
254
254
  if (button) button.classList.add("active");
255
- store.set({ projectLogsOpen: true, projectLogsPinned: true, projectLogsPreview: false });
256
- // Opening the ledger is the acknowledgement; the ambient cues clear here and
257
- // nowhere else.
255
+ store.set({ projectLogsOpen: true });
256
+ // Opening through the explicit tool button acknowledges the live badge.
258
257
  acknowledgeUpdates();
259
258
  showList();
260
259
  requestList();
261
260
  }
262
261
 
263
262
  export function closeProjectLogs() {
264
- hidePreview();
265
263
  if (!store.get('projectLogsOpen')) return;
266
264
  if (panel) {
267
265
  panel.classList.add("hidden");
268
- panel.classList.remove("project-logs-previewing");
269
266
  }
270
267
  // Fullscreen is always dropped on close so the next open is a bounded pane
271
268
  // and never silently hides the conversation.
272
269
  applyWindowState(store.get('projectLogsWide'), false);
273
270
  var button = document.getElementById("project-logs-btn");
274
271
  if (button) button.classList.remove("active");
275
- store.set({ projectLogsOpen: false, projectLogsPinned: false });
276
- syncAmbient();
272
+ store.set({ projectLogsOpen: false });
277
273
  }
278
274
 
279
275
  // --- Server messages -----------------------------------------------------
@@ -282,10 +278,13 @@ export function handleProjectLogsState(msg) {
282
278
  if (msg.requestId && msg.requestId !== store.get('projectLogsListRequestId')) return;
283
279
  store.set({ projectLogsEntries: msg.entries || [] });
284
280
  if (!listEl) return;
281
+ listEl.removeAttribute("aria-busy");
285
282
  if (filterEl && Array.isArray(msg.categories)) {
286
283
  renderFilter(filterEl, msg.categories, store.get('projectLogsCategory'), applyFilter);
287
284
  }
288
- renderList(listEl, msg.entries || [], requestEntry);
285
+ var isFiltered = !!((searchEl && searchEl.value.trim()) || store.get('projectLogsCategory'));
286
+ renderList(listEl, msg.entries || [], requestEntry,
287
+ isFiltered ? "No entries match this search or category." : "No logs yet. This project's agent sessions record decisions and work here.");
289
288
  if (store.get('projectLogsView') !== "detail") listEl.scrollTop = store.get('projectLogsListScroll') || 0;
290
289
  }
291
290
 
@@ -309,8 +308,8 @@ export function handleProjectLogCommented(msg) {
309
308
  requestList();
310
309
  }
311
310
 
312
- // A canonical revision landed. This marks the ambient cues and never opens,
313
- // focuses, or scrolls anything.
311
+ // A canonical revision landed. This marks the explicit button only; it never
312
+ // opens, reveals, focuses, or scrolls the pane.
314
313
  export function handleProjectLogUpdated(msg) {
315
314
  noteCanonicalUpdate(msg);
316
315
  // Refresh the ledger only when it is already on screen, so the row moves to
@@ -325,5 +324,9 @@ export function handleProjectLogsError(msg) {
325
324
  store.get('projectLogsCommentRequestId')
326
325
  ];
327
326
  if (msg.requestId && pending.indexOf(msg.requestId) === -1) return;
327
+ if (msg.requestId && msg.requestId === store.get('projectLogsListRequestId') && listEl) {
328
+ listEl.removeAttribute("aria-busy");
329
+ renderList(listEl, [], requestEntry, "The ledger could not be loaded. Try opening Logs again.");
330
+ }
328
331
  showToast(msg.message || "Project Logs could not complete the request.", "error");
329
332
  }
@@ -16,3 +16,11 @@ export function findSplitGroup(groups, memberIds) {
16
16
  }
17
17
  return null;
18
18
  }
19
+
20
+ export function isConfiguredWorker(groups, sessionId) {
21
+ var list = groups || [];
22
+ for (var i = 0; i < list.length; i++) {
23
+ if (list[i].pair && list[i].pair.workerId === sessionId) return true;
24
+ }
25
+ return false;
26
+ }
@@ -7,7 +7,7 @@ import { iconHtml, refreshIcons } from './icons.js';
7
7
  import { detachTuiView } from './session-tui-view.js';
8
8
  import { formatTokens } from './app-panels.js';
9
9
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
10
- import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
10
+ import { groupedSessionIds, findSplitGroup, isConfiguredWorker } from './split-group-helpers.js';
11
11
  import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
13
  import { presentMarkdownEdit } from './filebrowser.js';
@@ -159,7 +159,10 @@ export function syncPaneTitles() {
159
159
  function updatePaneFullAccessButton(button, session) {
160
160
  if (!button) return;
161
161
  var mode = session && (session.runtimeMode || session.mode || "gui");
162
- var visible = !!session && !store.get('skipPermsEnabled') && mode === "gui";
162
+ var worker = !!session && isConfiguredWorker(store.get('splitGroups'), session.id);
163
+ // Permission policy belongs to the Driver. A configured Worker inherits it
164
+ // server-side and must not present a second, misleading control.
165
+ var visible = !!session && !worker && !store.get('skipPermsEnabled') && mode === "gui";
163
166
  var enabled = visible && session.permissionMode === "bypassPermissions";
164
167
  button.classList.toggle("hidden", !visible);
165
168
  button.classList.toggle("active", enabled);
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).
@@ -663,6 +664,14 @@ function createSDKBridge(opts) {
663
664
  return { behavior: "allow", updatedInput: input };
664
665
  }
665
666
 
667
+ // Project Logs are exact-session, project-bound tools whose authority is
668
+ // enforced again by the server binding. Canonical changes are append-only
669
+ // revisions, and the surface exposes no destructive delete operation, so
670
+ // a second provider permission prompt adds no additional safety.
671
+ if (toolName.indexOf("mcp__clay-logs__") === 0) {
672
+ return { behavior: "allow", updatedInput: input };
673
+ }
674
+
666
675
  // This tool only prepares a read-only UI projection for the document the
667
676
  // agent is about to edit. The subsequent Edit/Write keeps its own normal
668
677
  // permission behavior.
@@ -1608,24 +1617,17 @@ function createSDKBridge(opts) {
1608
1617
  sessionToolDefs = sessionToolDefs.concat(yoke.userInput.fallbackToolDefs(sessionUserInputHandler));
1609
1618
  }
1610
1619
 
1611
- // Derive an explicit session title for fresh queries so the SDK records
1612
- // it at session creation and skips its own auto-generation. This also
1613
- // lets us short-circuit autoGenerateTitle below for the common case.
1620
+ // Give fresh queries an immediate provisional label. The visible user
1621
+ // message in history wins over `text`, which may contain an internal
1622
+ // provider wrapper rather than what the person typed.
1614
1623
  // Only applied to NEW sessions (no cliSessionId yet) — when resuming,
1615
1624
  // the SDK ignores Options.title in favor of the persisted title.
1616
1625
  var initialTitle = null;
1626
+ var initialTitleProvisional = false;
1617
1627
  if (!session.cliSessionId && !session.titleManuallySet && !session.titleAutoGenerated) {
1618
- if (session.title) {
1619
- // Loop / scheduled / mate-seeded sessions arrive with a title already set.
1620
- initialTitle = session.title;
1621
- } else if (typeof text === "string") {
1622
- // Derive a quick first-line snippet from the user's first message.
1623
- // Skip if too short to be meaningful — fall back to autoGenerateTitle.
1624
- var firstLine = text.replace(/\s+/g, " ").trim();
1625
- if (firstLine.length >= 10) {
1626
- initialTitle = firstLine.length > 60 ? firstLine.substring(0, 60) : firstLine;
1627
- }
1628
- }
1628
+ var initialTitleChoice = sessionTitlePolicy.selectInitialTitle(session, text);
1629
+ initialTitle = initialTitleChoice.title;
1630
+ initialTitleProvisional = initialTitleChoice.provisional;
1629
1631
  }
1630
1632
 
1631
1633
  var queryOpts = {
@@ -1720,16 +1722,16 @@ function createSDKBridge(opts) {
1720
1722
  try {
1721
1723
  handle = await sessionAdapter.createQuery(queryOpts);
1722
1724
  console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
1723
- // SDK accepted the explicit title — adopt it locally so the session
1724
- // list reflects it immediately and autoGenerateTitle skips this
1725
- // session (titleAutoGenerated gates re-trigger).
1725
+ // SDK accepted the explicit title. Provisional labels remain eligible
1726
+ // for semantic generation after the first Home exchange (or the normal
1727
+ // project threshold); seeded titles remain fixed.
1726
1728
  if (initialTitle && !session.title) {
1727
1729
  session.title = initialTitle;
1728
- session.titleAutoGenerated = true;
1730
+ session.titleAutoGenerated = !initialTitleProvisional;
1729
1731
  sm.saveSessionFile(session);
1730
1732
  sm.broadcastSessionList();
1731
1733
  } else if (initialTitle && session.title === initialTitle) {
1732
- session.titleAutoGenerated = true;
1734
+ session.titleAutoGenerated = !initialTitleProvisional;
1733
1735
  sm.saveSessionFile(session);
1734
1736
  }
1735
1737
  } catch (e) {
@@ -1,6 +1,9 @@
1
1
  var usersModule = require("./users");
2
2
  var yoke = require("./yoke");
3
3
  var backgroundTaskTiming = require("./background-task-timing");
4
+ var sessionTitlePolicy = require("./session-title-policy");
5
+
6
+ var OVERLOAD_RETRY_LIMIT = 3;
4
7
 
5
8
  function attachMessageProcessor(ctx) {
6
9
  var sm = ctx.sm;
@@ -23,8 +26,6 @@ function attachMessageProcessor(ctx) {
23
26
  var saveImageFile = ctx.saveImageFile;
24
27
  var getSessionLinuxUser = ctx.getSessionLinuxUser || function () { return null; };
25
28
 
26
- var AUTO_TITLE_TURN_THRESHOLD = 2;
27
-
28
29
  function getMateIdForNotification() {
29
30
  if (!isMate) return null;
30
31
  if (typeof slug === "string" && slug.indexOf("mate-") === 0) {
@@ -488,6 +489,8 @@ function attachMessageProcessor(ctx) {
488
489
  }
489
490
 
490
491
  } else if (parsed.yokeType === "result") {
492
+ var overloadAbortRequested = !!session._overloadAbortRequested;
493
+ session._overloadAbortRequested = false;
491
494
  session.blocks = {};
492
495
  session.sentToolResults = {};
493
496
  session.pendingPermissions = {};
@@ -533,7 +536,9 @@ function attachMessageProcessor(ctx) {
533
536
  session._awaitingTurnResult = false;
534
537
  session._queuedTurnCount = 0;
535
538
  onProcessingChanged();
536
- sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
539
+ if (!overloadAbortRequested) {
540
+ sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
541
+ }
537
542
  sendAndRecord(session, { type: "done", code: 1 });
538
543
  sm.broadcastSessionList();
539
544
  return;
@@ -625,7 +630,7 @@ function attachMessageProcessor(ctx) {
625
630
  sm.broadcastSessionList();
626
631
 
627
632
  // Auto-generate title after N turns (skip if loop or already auto-generated)
628
- if (session.turnCount === AUTO_TITLE_TURN_THRESHOLD
633
+ if (session.turnCount === sessionTitlePolicy.generationTurn(isMate)
629
634
  && !session.titleAutoGenerated
630
635
  && !session.titleManuallySet
631
636
  && !session.loop
@@ -820,9 +825,33 @@ function attachMessageProcessor(ctx) {
820
825
  }
821
826
 
822
827
  } else if (parsed.yokeType === "api_retry") {
823
- // Transient retry notification, show in UI but don't persist in history
824
- var retryText = parsed.message || parsed.error || "Retrying API request...";
825
- sendToSession(session, { type: "system_info", text: retryText });
828
+ // The Claude SDK may otherwise paint one persistent red error for every
829
+ // retry and remain busy for many minutes. Show one transient status, and
830
+ // stop a repeatedly overloaded turn after a small bounded number of
831
+ // attempts so the user regains control and can retry or change models.
832
+ var apiRetryAttempt = parsed.attempt || 1;
833
+ if (parsed.error === "overloaded") {
834
+ if (apiRetryAttempt === 1) {
835
+ session._overloadAbortRequested = false;
836
+ sendToSession(session, {
837
+ type: "system_info",
838
+ text: "The model is overloaded. Retrying briefly...",
839
+ });
840
+ }
841
+ if (apiRetryAttempt >= OVERLOAD_RETRY_LIMIT && !session._overloadAbortRequested) {
842
+ session._overloadAbortRequested = true;
843
+ sendAndRecord(session, {
844
+ type: "error",
845
+ text: "The model remained overloaded after " + apiRetryAttempt +
846
+ " attempts, so this turn was stopped. Try again or choose another model.",
847
+ });
848
+ if (session.abortController && typeof session.abortController.abort === "function") {
849
+ try { session.abortController.abort(); } catch (e) {}
850
+ }
851
+ }
852
+ } else if (apiRetryAttempt === 1) {
853
+ sendToSession(session, { type: "system_info", text: "The model request failed temporarily. Retrying..." });
854
+ }
826
855
 
827
856
  } else if (parsed.yokeType === "commands_changed") {
828
857
  // Mid-session command list change: rebuild the union with skills (same as
@@ -9,6 +9,7 @@ var attachHomeMateCreation = require("./server-home-mate-creation").attachHomeMa
9
9
  var attachHomeDebates = require("./server-home-debates").attachHomeDebates, attachHomeClayEntry = require("./server-home-clay-entry").attachHomeClayEntry;
10
10
  var homeChatEvents = require("./server-home-chat-events");
11
11
  var homeCapsuleCreation = require("./server-home-capsule-creation");
12
+ var sessionTitlePolicy = require("./session-title-policy");
12
13
  var historyToHomeChat = homeChatEvents.historyToHomeChat;
13
14
  var transformEvent = homeChatEvents.transformEvent;
14
15
  function attachHomeChat(deps) {
@@ -84,6 +85,10 @@ function attachHomeChat(deps) {
84
85
  var result = [];
85
86
  sessionManager.sessions.forEach(function (session) {
86
87
  if (!ownsSession(session, userId)) return;
88
+ if (sessionTitlePolicy.repairLegacySearchTitle(session)
89
+ && typeof sessionManager.saveSessionFile === "function") {
90
+ sessionManager.saveSessionFile(session);
91
+ }
87
92
  var cliSessionId = sessionString(session.cliSessionId, 512);
88
93
  var createdAt = sessionTimestamp(session.createdAt);
89
94
  var lastActivity = sessionTimestamp(session.lastActivity) || createdAt;