clay-server 5.2.0 → 5.2.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/public/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { initStudioBridge } from './modules/clay-studio-bridge.js';
2
2
  import { initSharedBrowser } from './modules/shared-browser.js';
3
+ import { initTopbarWorkbenchControls } from './modules/topbar-workbench-controls.js';
3
4
  import { initGenerationStop } from './modules/generation-stop.js';
4
5
  import { initIssueLinks } from './modules/clay-issue-links.js';
5
6
  import { initIssues, closeIssues } from './modules/issues.js';
@@ -332,6 +333,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
332
333
  splitDelegations: {},
333
334
  pairResultStatus: [],
334
335
  notesTemporarilyHidden: false,
336
+ pairResultTrayOpen: false,
335
337
  dmMode: false,
336
338
  historyFrom: 0,
337
339
  loadingMore: false,
@@ -1288,6 +1290,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1288
1290
  fileViewerEl: $("file-viewer"),
1289
1291
  });
1290
1292
  initGitPanel();
1293
+ initTopbarWorkbenchControls();
1291
1294
  initSharedBrowser();
1292
1295
  initStudioBridge();
1293
1296
  initWorkerPaneLock();
@@ -5,7 +5,6 @@
5
5
  /* --- Top bar search bar trigger (Slack-style) --- */
6
6
  .cmd-palette-searchbar {
7
7
  position: absolute;
8
- left: 50%;
9
8
  transform: translateX(-50%);
10
9
  display: flex;
11
10
  align-items: center;
@@ -20,8 +19,19 @@
20
19
  font-size: 12px;
21
20
  color: var(--text-dimmer);
22
21
  transition: background 0.15s, border-color 0.15s;
23
- width: min(520px, 40%);
22
+ /* topbar-workbench-controls.js publishes the width the free gap allows. */
23
+ width: var(--top-bar-search-width, min(520px, 40%));
24
+ left: var(--top-bar-search-left, 50%);
25
+ }
26
+
27
+ /* Not enough gap beside the actions: same trigger, icon only. */
28
+ #top-bar.search-icon .cmd-palette-searchbar {
29
+ width: 24px;
30
+ padding: 0;
31
+ justify-content: center;
24
32
  }
33
+ #top-bar.search-icon .cmd-palette-searchbar-text,
34
+ #top-bar.search-icon .cmd-palette-searchbar-kbd { display: none; }
25
35
 
26
36
  .cmd-palette-searchbar:hover {
27
37
  background: rgba(var(--overlay-rgb), 0.08);
@@ -390,7 +390,8 @@
390
390
 
391
391
  /* --- Panel fullscreen --- */
392
392
  @media (min-width: 1024px) {
393
- #main-column:has(.panel-fullscreen:not(.hidden):not(#shared-browser-panel)) > .title-bar-content { display: none; }
393
+ /* Browser, Terminal and Files expand below the session title; see right-workbench.css. */
394
+ #main-column:has(.panel-fullscreen:not(.hidden):not(#shared-browser-panel, #terminal-container, #file-viewer)) > .title-bar-content { display: none; }
394
395
  #main-panels:has(.panel-fullscreen:not(.hidden)) > #app { display: none; }
395
396
  }
396
397
  @media (max-width: 1023px) {
@@ -1060,15 +1061,13 @@
1060
1061
  width: 70%;
1061
1062
  max-width: 1200px;
1062
1063
  }
1064
+ /* Same in-flow region as Browser and Terminal (right-workbench.css). */
1063
1065
  #file-viewer.panel-fullscreen {
1064
- position: fixed;
1065
- inset: 0;
1066
1066
  width: 100%;
1067
- height: 100dvh;
1067
+ height: 100%;
1068
1068
  max-width: none;
1069
1069
  min-width: 0;
1070
1070
  margin: 0;
1071
- z-index: 10000;
1072
1071
  border: 0;
1073
1072
  border-radius: 0;
1074
1073
  background: var(--bg);
@@ -1278,23 +1278,42 @@ pre.mermaid-error {
1278
1278
  .plan-banner.plan-exit { color: var(--success); }
1279
1279
  .plan-banner.plan-exit .plan-banner-icon { color: var(--success); }
1280
1280
 
1281
- /* Recovery notices use the same quiet surface as session work references. */
1282
- .pair-result-status { flex: 0 0 auto; margin: 8px 16px 0; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-alt); color: var(--text-muted); font-size: 12px; line-height: 1.5; max-height: 35vh; overflow-y: auto; }
1283
- .pair-result-status-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--text-secondary); }
1284
- .pair-result-status button { font: inherit; color: var(--text-secondary); background: transparent; border: 1px solid var(--border); border-radius: 5px; cursor: pointer; }
1285
- .pair-result-status .pair-result-status-dismiss { display: grid; place-items: center; flex: 0 0 28px; width: 28px; height: 28px; padding: 0; border: 0; font-size: 20px; line-height: 1; }
1286
- .pair-result-status button:hover { background: var(--input-bg); color: var(--text); }
1287
- .pair-result-status button:focus-visible, .pair-result-status summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1288
- .pair-result-status-details > summary { cursor: pointer; width: fit-content; }
1289
- .pair-result-status-card { padding: 10px 0; border-top: 1px solid var(--border); margin-top: 8px; }
1281
+ /* Worker recovery notice: one compact, content-width disclosure aligned to the conversation inset. */
1282
+ .pair-result-status { position: relative; flex: 0 0 auto; align-self: flex-start; box-sizing: border-box; width: fit-content; min-width: 0; max-width: calc(100% - 32px); margin: 8px 16px 0; border: 1px solid var(--border-subtle); border-radius: 8px; background: rgba(var(--overlay-rgb), 0.02); color: var(--text-muted); font-size: 12px; line-height: 1.5; }
1283
+ .pair-result-status.is-open { width: min(640px, calc(100% - 32px)); }
1284
+ .pair-result-status-bar { display: flex; align-items: center; gap: 2px; min-height: 30px; padding: 0 2px 0 0; }
1285
+ .pair-result-status button { font: inherit; color: var(--text-secondary); background: transparent; border: 0; border-radius: 6px; cursor: pointer; }
1286
+ .pair-result-status-toggle { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 0 8px; margin: 1px 0 1px 1px; }
1287
+ .pair-result-status-icon { display: inline-flex; color: var(--text-dimmer); }
1288
+ .pair-result-status-tone-blocked .pair-result-status-icon, .pair-result-status-tone-uncertain .pair-result-status-icon { color: var(--warning); }
1289
+ .pair-result-status .lucide { width: 14px; height: 14px; }
1290
+ .pair-result-status-label { color: var(--text-secondary); font-weight: 500; white-space: nowrap; }
1291
+ .pair-result-status-review { color: var(--text-muted); white-space: nowrap; }
1292
+ .pair-result-status-chevron { display: inline-flex; color: var(--text-dimmer); transition: transform 0.15s; }
1293
+ .pair-result-status.is-open .pair-result-status-chevron { transform: rotate(180deg); }
1294
+ .pair-result-status-state, .pair-result-status-live { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1295
+ .pair-result-status .pair-result-status-dismiss { display: grid; place-items: center; flex: 0 0 28px; width: 28px; height: 28px; padding: 0; color: var(--text-dimmer); }
1296
+ .pair-result-status button:hover { background: rgba(var(--overlay-rgb), 0.06); color: var(--text); }
1297
+ .pair-result-status button:focus-visible, .pair-result-status summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
1298
+ .pair-result-status-tray { border-radius: 0 0 7px 7px; max-height: min(40vh, 360px); overflow-y: auto; padding: 0 10px 8px; border-top: 1px solid var(--border-subtle); overflow-wrap: anywhere; }
1299
+ .pair-result-status-tray[hidden] { display: none; }
1300
+ .pair-result-status-card { padding: 8px 0; }
1301
+ .pair-result-status-card + .pair-result-status-card { border-top: 1px solid var(--border-subtle); }
1290
1302
  .pair-result-status-card > strong, .pair-result-status-card > span { display: block; }
1291
1303
  .pair-result-status-card > strong { color: var(--text-secondary); font-weight: 500; margin-bottom: 3px; }
1292
1304
  .pair-result-status-card details { margin-top: 7px; }
1293
1305
  .pair-result-status-card summary { cursor: pointer; }
1294
1306
  .pair-result-status-card pre { max-height: 180px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
1295
- .pair-result-status-retry { margin-top: 8px; padding: 4px 8px; }
1307
+ .pair-result-status .pair-result-status-retry { margin-top: 8px; padding: 4px 8px; border: 1px solid var(--border); border-radius: 5px; }
1296
1308
  .pair-result-status-retry:disabled { cursor: not-allowed; opacity: 0.55; }
1297
- @media (max-width: 600px) { .pair-result-status { margin: 6px 8px 0; } .pair-result-status .pair-result-status-dismiss { min-width: 36px; height: 36px; } }
1309
+ @media (prefers-reduced-motion: reduce) { .pair-result-status-chevron { transition: none; } }
1310
+ @media (max-width: 600px) {
1311
+ .pair-result-status { margin: 6px 8px 0; max-width: calc(100% - 16px); }
1312
+ .pair-result-status.is-open { width: calc(100% - 16px); }
1313
+ .pair-result-status-bar { min-height: 36px; }
1314
+ .pair-result-status-toggle { min-height: 34px; }
1315
+ .pair-result-status .pair-result-status-dismiss { flex-basis: 36px; width: 36px; height: 36px; }
1316
+ }
1298
1317
 
1299
1318
 
1300
1319
  /* --- Plan card --- */
@@ -66,6 +66,9 @@
66
66
  /* Vendor usage overview: one compact chip per tracked vendor, details in a popover */
67
67
  .vendor-usage-overview { display: inline-flex; align-items: center; gap: 4px; margin-right: 4px; }
68
68
  .vendor-usage-item { display: inline-flex; position: relative; }
69
+ /* Crowded top bar: per-vendor chips fold into the always-last all-vendors chip, then the session usage link. */
70
+ #top-bar.usage-compact .vendor-usage-item:not(:last-child),
71
+ #top-bar.usage-compact-link #rate-limit-usage-link { display: none; }
69
72
  .top-bar-actions button.vendor-usage-chip {
70
73
  display: inline-flex;
71
74
  padding: 2px 8px;
@@ -20,6 +20,34 @@
20
20
  animation: none;
21
21
  }
22
22
 
23
+ /* Desktop expansion of Browser, Terminal and Files shares one region: the
24
+ area below the session title (the title and its right tools stay visible).
25
+ Chat (#app) is hidden by filebrowser.css; the split host is a sibling whose
26
+ pane headers overflow its collapsed width, so lift it out of flow and make it
27
+ unpaintable and unclickable while expanded, including descendants that force visibility. Its frames keep their size and
28
+ state, and modals keep their own stacking above this region. */
29
+ @media (min-width: 1024px) {
30
+ :is(#file-viewer, #terminal-container, #shared-browser-panel).panel-fullscreen {
31
+ align-self: stretch;
32
+ width: 100%;
33
+ max-width: none;
34
+ min-width: 0;
35
+ height: 100%;
36
+ margin: 0;
37
+ border: 0;
38
+ border-radius: 0;
39
+ }
40
+ #main-panels:has(> :is(#file-viewer, #terminal-container, #shared-browser-panel).panel-fullscreen:not(.hidden)) > #split-host {
41
+ position: absolute;
42
+ inset: 0;
43
+ visibility: hidden;
44
+ pointer-events: none;
45
+ /* Some descendants set visibility:visible themselves (for example the
46
+ pending permission spinner); the clip keeps all of them unpainted. */
47
+ clip-path: inset(100%);
48
+ }
49
+ }
50
+
23
51
  @media (prefers-reduced-motion: reduce) {
24
52
  :is(#file-viewer, #terminal-container, #project-logs-panel, #you-panel, #issues-panel, #linear-panel, #notes-browser, #scheduled-tasks-panel, #debates-workbench, #shared-browser-panel, #mcp-skills-workbench, #mate-knowledge-workbench):not(.panel-fullscreen) {
25
53
  animation: none;
@@ -120,6 +120,22 @@
120
120
 
121
121
  #server-settings-btn.active { color: var(--text); }
122
122
 
123
+ /* Browser, Terminal and sticky-note visibility live beside Notifications and
124
+ Settings on desktop. Match those compact top-bar buttons; the same nodes keep
125
+ their own IDs, state and handlers (see topbar-workbench-controls.js). */
126
+ .top-bar-actions :is(#shared-browser-toggle-btn, #terminal-toggle-btn, #sticky-notes-toggle-btn) {
127
+ position: relative;
128
+ padding: 3px;
129
+ border: 0;
130
+ border-radius: 6px;
131
+ }
132
+ .top-bar-actions :is(#shared-browser-toggle-btn, #terminal-toggle-btn, #sticky-notes-toggle-btn) .lucide { width: 14px; height: 14px; }
133
+ .top-bar-actions :is(#shared-browser-toggle-btn, #terminal-toggle-btn, #sticky-notes-toggle-btn):hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb), 0.06); border: 0; }
134
+ .top-bar-actions #shared-browser-toggle-btn:disabled:hover { background: none; }
135
+ /* These controls belong to a session: keep them out of the project-less shell and while the title bar is replaced by another view. */
136
+ body.projects-shell .top-bar-actions :is(#shared-browser-toggle-btn, #terminal-toggle-btn, #sticky-notes-toggle-btn),
137
+ #top-bar.title-bar-hidden .top-bar-actions :is(#shared-browser-toggle-btn, #terminal-toggle-btn, #sticky-notes-toggle-btn) { display: none; }
138
+
123
139
  /* --- Theme toggle pill switch (light/dark) --- */
124
140
  .theme-toggle {
125
141
  display: flex;
@@ -77,6 +77,10 @@ body { background: transparent; color: inherit; }
77
77
  <!-- Pill badges -->
78
78
  <div id="skip-perms-pill" class="top-bar-pill pill-error hidden"><i data-lucide="shield-off"></i> <span>Skip perms</span></div>
79
79
  <div id="client-count" class="top-bar-pill pill-accent hidden"><i data-lucide="users"></i> <span id="client-count-text"></span></div>
80
+ <!-- Right workbench controls: same nodes and handlers as before; app JS returns them to the title bar on mobile. -->
81
+ <button type="button" id="shared-browser-toggle-btn" class="hidden" title="Browser" aria-label="Browser" aria-controls="shared-browser-panel" aria-expanded="false" disabled><i data-lucide="globe"></i></button>
82
+ <button id="terminal-toggle-btn" title="Terminal"><i data-lucide="square-terminal"></i><span id="terminal-count" class="hidden"></span></button>
83
+ <button type="button" id="sticky-notes-toggle-btn" aria-label="Hide sticky notes" aria-pressed="false" title="Hide sticky notes"><i data-lucide="eye-off"></i></button>
80
84
  <button id="notif-center-btn" title="Notifications"><i data-lucide="bell"></i><span id="notif-center-badge" class="notif-badge hidden">0</span></button>
81
85
  <button id="server-settings-btn" title="Settings" aria-label="Settings" aria-expanded="false"><i data-lucide="settings"></i></button>
82
86
  </div>
@@ -324,9 +328,6 @@ body { background: transparent; color: inherit; }
324
328
  </button>
325
329
  <button id="debate-pdf-btn" class="hidden" title="Export debate as PDF"><i data-lucide="download"></i></button>
326
330
  <button id="find-in-session-btn" title="Search in session (Ctrl+F)"><i data-lucide="search"></i></button>
327
- <button type="button" id="sticky-notes-toggle-btn" aria-label="Hide sticky notes" aria-pressed="false" title="Hide sticky notes"><i data-lucide="eye-off"></i></button>
328
- <button type="button" id="shared-browser-toggle-btn" class="hidden" title="Browser" aria-label="Browser" aria-controls="shared-browser-panel" aria-expanded="false" disabled><i data-lucide="globe"></i></button>
329
- <button id="terminal-toggle-btn" title="Terminal"><i data-lucide="square-terminal"></i><span id="terminal-count" class="hidden"></span></button>
330
331
  </div>
331
332
  </div>
332
333
  <div id="main-panels">
@@ -1,8 +1,13 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
3
  import { showConfirm } from './app-misc.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
4
5
 
5
6
  var statusGeneration = 0;
7
+ // Whether the review tray is open lives in the store (pairResultTrayOpen), so it
8
+ // survives server re-renders and resets on clear or session switch.
9
+ function isTrayOpen() { return !!store.get("pairResultTrayOpen"); }
10
+ function setTrayOpen(open) { if (isTrayOpen() !== open) store.set({ pairResultTrayOpen: open }); }
6
11
 
7
12
  function retrySource(item) {
8
13
  return { item: item, generation: statusGeneration, projectSlug: store.get("currentSlug") || store.get("activeProjectSlug") || null,
@@ -43,56 +48,100 @@ function dismiss(items) {
43
48
  });
44
49
  }
45
50
 
51
+ function worstState(items) {
52
+ if (items.some(function (item) { return item.state === "blocked"; })) return "blocked";
53
+ if (items.some(function (item) { return item.state === "uncertain"; })) return "uncertain";
54
+ return "pending";
55
+ }
56
+
57
+ function stateWords(state) {
58
+ return state === "blocked" ? "blocked" : (state === "uncertain" ? "delivery uncertain" : "pending");
59
+ }
60
+
61
+ function button(className, label, icon) {
62
+ var node = document.createElement("button");
63
+ node.type = "button"; node.className = className;
64
+ if (label) node.setAttribute("aria-label", label);
65
+ if (icon) node.innerHTML = iconHtml(icon);
66
+ return node;
67
+ }
68
+
69
+ function renderCard(item) {
70
+ var card = document.createElement("div");
71
+ card.className = "pair-result-status-card pair-result-status-" + item.state;
72
+ var title = document.createElement("strong");
73
+ title.textContent = item.state === "blocked" ? "Split Worker result blocked" :
74
+ (item.state === "uncertain" ? (item.hasCompletedResult === false ? "Split Worker recovery uncertain" : "Split Worker result delivery uncertain") : "Split Worker result pending");
75
+ var message = document.createElement("span");
76
+ message.textContent = item.message || "The completed result is retained.";
77
+ card.appendChild(title); card.appendChild(message);
78
+ if (item.hasCompletedResult !== false) {
79
+ var details = document.createElement("details"), summary = document.createElement("summary"), body = document.createElement("pre");
80
+ summary.textContent = "View completed result";
81
+ body.textContent = item.error ? "Error: " + item.error : (item.response || "No text response was recorded.");
82
+ details.appendChild(summary); details.appendChild(body); card.appendChild(details);
83
+ }
84
+ if (item.canRetry) {
85
+ var retryButton = button("pair-result-status-retry", null, null);
86
+ retryButton.textContent = "Retry delivery";
87
+ retryButton.addEventListener("click", retry.bind(null, item));
88
+ card.appendChild(retryButton);
89
+ }
90
+ return card;
91
+ }
92
+
46
93
  function renderPairResultStatus() {
47
94
  var existing = document.getElementById("pair-result-status");
95
+ var focusedToggle = !!(existing && existing.ownerDocument.activeElement && existing.ownerDocument.activeElement.className === "pair-result-status-toggle");
48
96
  if (existing) existing.remove();
49
97
  var items = store.get("pairResultStatus") || [];
50
- if (!items.length) return;
98
+ if (!items.length) { setTrayOpen(false); return; }
51
99
  var messages = document.getElementById("messages");
52
100
  if (!messages || !messages.parentElement) return;
101
+ var trayOpen = isTrayOpen();
102
+ var state = worstState(items);
103
+ var label = items.length === 1 ? "Worker update" : items.length + " Worker updates";
53
104
  var section = document.createElement("section");
54
105
  section.id = "pair-result-status";
55
- section.className = "pair-result-status";
56
- section.setAttribute("role", "status");
57
- var header = document.createElement("div");
58
- header.className = "pair-result-status-header";
59
- var heading = document.createElement("span");
60
- heading.textContent = items.length === 1 ? "Worker update needs attention" : items.length + " Worker updates need attention";
61
- var close = document.createElement("button");
62
- close.type = "button"; close.className = "pair-result-status-dismiss";
63
- close.textContent = "×";
64
- close.setAttribute("aria-label", "Dismiss Worker notices");
106
+ section.className = "pair-result-status pair-result-status-tone-" + state + (trayOpen ? " is-open" : "");
107
+ var bar = document.createElement("div");
108
+ bar.className = "pair-result-status-bar";
109
+ var toggle = button("pair-result-status-toggle", null, null);
110
+ toggle.setAttribute("aria-expanded", trayOpen ? "true" : "false");
111
+ toggle.setAttribute("aria-controls", "pair-result-status-tray");
112
+ toggle.innerHTML = '<span class="pair-result-status-icon" aria-hidden="true">' + iconHtml(state === "pending" ? "clock" : "triangle-alert") + '</span>' +
113
+ '<span class="pair-result-status-label">' + label + '</span>' +
114
+ '<span class="pair-result-status-state">, ' + stateWords(state) + '.</span>' +
115
+ '<span class="pair-result-status-review">Review</span>' +
116
+ '<span class="pair-result-status-chevron" aria-hidden="true">' + iconHtml("chevron-down") + '</span>';
117
+ // One short polite announcement; the details stay in the tray.
118
+ var live = document.createElement("span");
119
+ live.className = "pair-result-status-live"; live.setAttribute("role", "status"); live.textContent = label;
120
+ bar.appendChild(live);
121
+ var close = button("pair-result-status-dismiss", "Dismiss Worker notices", "x");
65
122
  close.title = "Dismiss notices. Retained tasks and results are kept.";
66
123
  close.addEventListener("click", dismiss.bind(null, items));
67
- header.appendChild(heading); header.appendChild(close); section.appendChild(header);
68
- var content = document.createElement("details"), toggle = document.createElement("summary");
69
- content.className = "pair-result-status-details";
70
- toggle.textContent = "Review updates";
71
- content.appendChild(toggle); section.appendChild(content);
72
- for (var i = 0; i < items.length; i++) {
73
- var item = items[i], card = document.createElement("div");
74
- card.className = "pair-result-status-card pair-result-status-" + item.state;
75
- var title = document.createElement("strong");
76
- title.textContent = item.state === "blocked" ? "Split Worker result blocked" :
77
- (item.state === "uncertain" ? (item.hasCompletedResult === false ? "Split Worker recovery uncertain" : "Split Worker result delivery uncertain") : "Split Worker result pending");
78
- var message = document.createElement("span");
79
- message.textContent = item.message || "The completed result is retained.";
80
- card.appendChild(title); card.appendChild(message);
81
- if (item.hasCompletedResult !== false) {
82
- var details = document.createElement("details"), summary = document.createElement("summary"), body = document.createElement("pre");
83
- summary.textContent = "View completed result";
84
- body.textContent = item.error ? "Error: " + item.error : (item.response || "No text response was recorded.");
85
- details.appendChild(summary); details.appendChild(body); card.appendChild(details);
86
- }
87
- if (item.canRetry) {
88
- var button = document.createElement("button");
89
- button.type = "button"; button.className = "pair-result-status-retry"; button.textContent = "Retry delivery";
90
- button.addEventListener("click", retry.bind(null, item));
91
- card.appendChild(button);
92
- }
93
- content.appendChild(card);
94
- }
124
+ bar.appendChild(toggle); bar.appendChild(close); section.appendChild(bar);
125
+ var tray = document.createElement("div");
126
+ tray.id = "pair-result-status-tray"; tray.className = "pair-result-status-tray";
127
+ tray.hidden = !trayOpen;
128
+ for (var i = 0; i < items.length; i++) tray.appendChild(renderCard(items[i]));
129
+ section.appendChild(tray);
130
+ toggle.addEventListener("click", function () {
131
+ trayOpen = !isTrayOpen(); setTrayOpen(trayOpen);
132
+ toggle.setAttribute("aria-expanded", trayOpen ? "true" : "false");
133
+ tray.hidden = !trayOpen;
134
+ section.className = section.className.replace(/ is-open/g, "") + (trayOpen ? " is-open" : "");
135
+ });
136
+ section.addEventListener("keydown", function (event) {
137
+ if (event.key !== "Escape" || !isTrayOpen()) return;
138
+ trayOpen = false; setTrayOpen(false); toggle.setAttribute("aria-expanded", "false"); tray.hidden = true;
139
+ section.className = section.className.replace(/ is-open/g, "");
140
+ if (toggle.focus) toggle.focus();
141
+ });
95
142
  messages.parentElement.insertBefore(section, messages);
143
+ refreshIcons();
144
+ if (focusedToggle && toggle.focus) toggle.focus();
96
145
  }
97
146
 
98
147
  function handlePairResultStatus(msg) {
@@ -105,6 +154,7 @@ function handlePairResultStatus(msg) {
105
154
 
106
155
  function clearPairResultStatus() {
107
156
  statusGeneration++;
157
+ setTrayOpen(false);
108
158
  store.set({ pairResultStatus: [] });
109
159
  renderPairResultStatus();
110
160
  }
@@ -0,0 +1,102 @@
1
+ // Placement of the Browser, Terminal and sticky-note visibility buttons.
2
+ // Desktop keeps them in the top bar beside Notifications and Settings; where the
3
+ // top bar is hidden (mobile) the very same nodes return to the title bar status
4
+ // area. Nodes, IDs, handlers, state and permissions are never recreated.
5
+ var MOBILE = '(max-width: 768px)';
6
+ var IDS = ['sticky-notes-toggle-btn', 'shared-browser-toggle-btn', 'terminal-toggle-btn'];
7
+ var DESKTOP_ORDER = ['shared-browser-toggle-btn', 'terminal-toggle-btn', 'sticky-notes-toggle-btn'];
8
+
9
+ function place(mobile) {
10
+ var status = document.querySelector('#main-column > .title-bar-content .status');
11
+ var actions = document.querySelector('#top-bar .top-bar-actions');
12
+ var anchor = document.getElementById('notif-center-btn');
13
+ if (!status || !actions) return;
14
+ var order = mobile ? IDS : DESKTOP_ORDER;
15
+ for (var i = 0; i < order.length; i++) {
16
+ var node = document.getElementById(order[i]);
17
+ if (!node) continue;
18
+ if (mobile) status.appendChild(node);
19
+ else actions.insertBefore(node, anchor);
20
+ }
21
+ }
22
+
23
+ // The search trigger is centred in the bar. It keeps that place and width when
24
+ // the free gap between the left group and the actions allows, otherwise it
25
+ // collapses to an icon-only trigger (same node, ID, handler and label) inside
26
+ // the gap. If even the icon cannot fit, the per-vendor usage chips fold into the
27
+ // existing all-vendors chip, then the active-session usage link folds, before
28
+ // anything else is touched. Action buttons are never hidden or clipped.
29
+ var MIN_FULL = 150, ICON = 24, MARGIN = 12;
30
+
31
+ // Sizes seen right after the last applied layout. Observer wake-ups that find the
32
+ // same sizes (including the ones our own class changes cause) do no work at all.
33
+ var applied = null;
34
+
35
+ function sizes(bar, left, actions) {
36
+ return Math.round(bar.offsetWidth) + '|' + Math.round(left.offsetWidth) + '|' + Math.round(actions.offsetWidth);
37
+ }
38
+
39
+ function layoutTopbar(force) {
40
+ var bar = document.getElementById('top-bar');
41
+ var left = bar && bar.querySelector('.top-bar-left-pills');
42
+ var actions = bar && bar.querySelector('.top-bar-actions');
43
+ var search = document.getElementById('cmd-palette-btn');
44
+ if (!bar || !left || !actions || !search) return;
45
+ if (!bar.offsetWidth) return;
46
+ if (!force && applied === sizes(bar, left, actions)) return;
47
+ if (getComputedStyle(search).display === 'none') return;
48
+ var levels = [[], ['usage-compact'], ['usage-compact', 'usage-compact-link']];
49
+ var result = null;
50
+ for (var i = 0; i < levels.length && !result; i++) {
51
+ bar.classList.remove('usage-compact', 'usage-compact-link', 'search-icon');
52
+ levels[i].forEach(function (name) { bar.classList.add(name); });
53
+ bar.style.removeProperty('--top-bar-search-width'); bar.style.removeProperty('--top-bar-search-left');
54
+ var width = bar.getBoundingClientRect().width, center = width / 2;
55
+ var barLeft = bar.getBoundingClientRect().left;
56
+ var leftEdge = left.getBoundingClientRect().right - barLeft, actionsEdge = actions.getBoundingClientRect().left - barLeft;
57
+ var half = Math.min(center - leftEdge, actionsEdge - center) - MARGIN;
58
+ var target = Math.min(520, width * 0.4);
59
+ if (2 * half >= MIN_FULL) result = { mode: 'full', width: Math.min(target, 2 * half), left: null, level: i };
60
+ else if (actionsEdge - leftEdge >= ICON + 2 * MARGIN) {
61
+ // Stay centred in the bar when that is inside the gap.
62
+ var lo = leftEdge + MARGIN + ICON / 2, hi = actionsEdge - MARGIN - ICON / 2;
63
+ result = { mode: 'icon', width: ICON, left: Math.min(Math.max(center, lo), hi), level: i };
64
+ }
65
+ }
66
+ if (!result) result = { mode: 'icon', width: ICON, left: Math.max(left.getBoundingClientRect().right - bar.getBoundingClientRect().left + MARGIN + ICON / 2, 0), level: levels.length - 1 };
67
+ bar.classList.toggle('search-icon', result.mode === 'icon');
68
+ if (result.mode === 'full') bar.style.setProperty('--top-bar-search-width', Math.round(result.width) + 'px');
69
+ else bar.style.setProperty('--top-bar-search-left', Math.round(result.left) + 'px');
70
+ applied = sizes(bar, left, actions);
71
+ }
72
+
73
+ function observeTopbarLayout() {
74
+ var bar = document.getElementById('top-bar');
75
+ if (!bar || typeof ResizeObserver === 'undefined') return;
76
+ var queued = false;
77
+ var run = function (force) { if (queued) return; queued = true; requestAnimationFrame(function () { queued = false; layoutTopbar(force === true); }); };
78
+ var observer = new ResizeObserver(function () { run(false); });
79
+ observer.observe(bar);
80
+ ['.top-bar-left-pills', '.top-bar-actions'].forEach(function (selector) { var node = bar.querySelector(selector); if (node) observer.observe(node); });
81
+ // Content swaps that leave the applied size unchanged (a folded usage chip changing)
82
+ // still need a fresh decision; plain class toggles are caught by the size observers.
83
+ new MutationObserver(function () { run(true); }).observe(bar.querySelector('.top-bar-actions'), { childList: true, subtree: true, characterData: true });
84
+ layoutTopbar(true);
85
+ }
86
+
87
+ // Mirror "title bar replaced by another view" on the top bar so CSS needs no body-level :has().
88
+ function mirrorTitleBarHidden() {
89
+ var title = document.querySelector('#main-column > .title-bar-content'), bar = document.getElementById('top-bar');
90
+ if (!title || !bar) return;
91
+ var sync = function () { bar.classList.toggle('title-bar-hidden', title.classList.contains('hidden')); };
92
+ sync();
93
+ new MutationObserver(sync).observe(title, { attributes: true, attributeFilter: ['class'] });
94
+ }
95
+
96
+ export function initTopbarWorkbenchControls() {
97
+ var query = window.matchMedia(MOBILE);
98
+ place(query.matches);
99
+ query.addEventListener('change', function () { place(query.matches); });
100
+ mirrorTitleBarHidden();
101
+ observeTopbarLayout();
102
+ }
@@ -6,7 +6,7 @@
6
6
  @import url("css/linear.css");
7
7
  @import url("css/base.css?v=20260828-type1");
8
8
  @import url("css/icon-strip.css?v=20260828-surfaces4");
9
- @import url("css/title-bar.css?v=20261018-actions-layer");
9
+ @import url("css/title-bar.css?v=20261018-workbench-controls2");
10
10
  @import url("css/clay-workspace.css?v=20261010-tint");
11
11
  @import url("css/pane.css");
12
12
  @import url("css/worker-proposal.css");
@@ -14,15 +14,15 @@
14
14
  @import url("css/session-actions.css?v=20260828-contrast1");
15
15
  @import url("css/sidebar.css?v=20261018-quick-appearance");
16
16
  @import url("css/session-folders.css");
17
- @import url("css/overlays.css?v=20261017-usage2");
17
+ @import url("css/overlays.css?v=20261018-topbar-gap");
18
18
  @import url("css/menus.css");
19
- @import url("css/messages.css?v=20260828-delegated-muted1");
19
+ @import url("css/messages.css?v=20261018-worker-notice");
20
20
  @import url("css/generated-images.css?v=20260829-save-details1");
21
21
  @import url("css/rewind.css");
22
22
  @import url("css/elicitation-form.css");
23
23
  @import url("css/input.css?v=20261002-stop-inline1");
24
24
  @import url("css/pending-message-queue.css?v=20260914-queue1");
25
- @import url("css/filebrowser.css?v=20261018-browser-header1");
25
+ @import url("css/filebrowser.css?v=20261018-workbench-region");
26
26
  @import url("css/git-panel.css");
27
27
  @import url("css/git-placard.css");
28
28
  @import url("css/diff.css");
@@ -53,7 +53,7 @@
53
53
  @import url("css/session-search.css");
54
54
  @import url("css/tooltip.css");
55
55
  @import url("css/mates.css?v=20260407");
56
- @import url("css/command-palette.css?v=20260910-ask-clay-controls1");
56
+ @import url("css/command-palette.css?v=20261018-topbar-gap");
57
57
  @import url("css/mention.css");
58
58
  @import url("css/debate.css");
59
59
  @import url("css/home-debate-planning.css");
@@ -76,7 +76,7 @@
76
76
  @import url("./css/mcp-skills-workbench.css");
77
77
  @import url("css/skills-workbench.css");
78
78
 
79
- @import url("css/right-workbench.css");
79
+ @import url("css/right-workbench.css?v=20261018-workbench-region");
80
80
 
81
81
  @import url("css/mate-conversation.css");
82
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "5.2.0",
3
+ "version": "5.2.1-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"