clay-server 5.3.0-beta.6 → 5.3.0-beta.7

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 (111) hide show
  1. package/lib/project-admin-overview.js +22 -11
  2. package/lib/project-background-tasks.js +76 -0
  3. package/lib/project-connection.js +1 -1
  4. package/lib/project-sessions.js +12 -5
  5. package/lib/project-split-views.js +12 -3
  6. package/lib/project-view-store.js +7 -5
  7. package/lib/project-worker-conversation.js +5 -0
  8. package/lib/public/app.js +7 -0
  9. package/lib/public/css/contextual-clay.css +2 -1
  10. package/lib/public/css/mate-conversation.css +1 -1
  11. package/lib/public/css/pending-message-queue.css +24 -19
  12. package/lib/public/css/project-admin-overview.css +42 -13
  13. package/lib/public/css/shared-browser.css +31 -8
  14. package/lib/public/css/split-view-layout.css +92 -7
  15. package/lib/public/css/tool-pane.css +4 -7
  16. package/lib/public/index.html +5 -5
  17. package/lib/public/modules/agent-message-bubbles.js +96 -0
  18. package/lib/public/modules/app-header.js +6 -0
  19. package/lib/public/modules/app-messages.js +26 -9
  20. package/lib/public/modules/background-task-previews.js +8 -0
  21. package/lib/public/modules/background-task-viewer.js +97 -0
  22. package/lib/public/modules/background-tasks-ui.js +34 -125
  23. package/lib/public/modules/chat-bubble-renderer.js +11 -5
  24. package/lib/public/modules/contextual-clay-geometry.js +7 -3
  25. package/lib/public/modules/pane-activation.js +5 -3
  26. package/lib/public/modules/pane-header-tooltips.js +32 -0
  27. package/lib/public/modules/pending-message-queue-drag.js +69 -0
  28. package/lib/public/modules/pending-message-queue.js +3 -33
  29. package/lib/public/modules/permission-control.js +6 -5
  30. package/lib/public/modules/project-admin-overview.js +125 -10
  31. package/lib/public/modules/project-grouping.js +9 -0
  32. package/lib/public/modules/server-settings-controls.js +33 -4
  33. package/lib/public/modules/session-folder-toolbar.js +1 -1
  34. package/lib/public/modules/session-tool-item.js +86 -0
  35. package/lib/public/modules/settings-select.js +2 -1
  36. package/lib/public/modules/shared-browser-tabs.js +37 -21
  37. package/lib/public/modules/sidebar-projects.js +1 -10
  38. package/lib/public/modules/split-session-empty-pane.js +47 -0
  39. package/lib/public/modules/split-view-create-menu.js +74 -65
  40. package/lib/public/modules/split-view-layout-scope.js +4 -0
  41. package/lib/public/modules/split-view-picker-actions.js +71 -0
  42. package/lib/public/modules/split-view-picker-render.js +81 -0
  43. package/lib/public/modules/split-view-presentation-renderer.js +8 -8
  44. package/lib/public/modules/split-view-worker-activity.js +2 -2
  45. package/lib/public/modules/split-view-worker-transcript.js +70 -33
  46. package/lib/public/modules/split-view-workers.js +6 -4
  47. package/lib/public/modules/terminal-viewport.js +14 -0
  48. package/lib/public/modules/terminal.js +3 -4
  49. package/lib/public/modules/theme-palette.js +177 -0
  50. package/lib/public/modules/theme.js +1 -174
  51. package/lib/public/modules/tool-pane-chrome.js +7 -1
  52. package/lib/public/modules/tools.js +5 -89
  53. package/lib/public/modules/tooltip.js +32 -6
  54. package/lib/public/modules/wireframe-workbench.js +2 -2
  55. package/lib/public/modules/worker-message-previews.js +39 -0
  56. package/lib/public/pane-assets/app-5W6KJXK5.js +1 -0
  57. package/lib/public/pane-assets/chunk-2IJUTAB6.js +2 -0
  58. package/lib/public/pane-assets/{chunk-BMHTBRMO.js → chunk-7OVNC5HT.js} +1 -1
  59. package/lib/public/pane-assets/chunk-BQN7B62H.js +3 -0
  60. package/lib/public/pane-assets/{chunk-2AIWCI6Y.js → chunk-EMLN2Y7L.js} +1 -1
  61. package/lib/public/pane-assets/{chunk-VKYBAVWT.js → chunk-ENPAHXLB.js} +1 -1
  62. package/lib/public/pane-assets/chunk-EPBGGZRT.js +1 -0
  63. package/lib/public/pane-assets/chunk-EYTE56IA.js +1 -0
  64. package/lib/public/pane-assets/{chunk-6NYCNWVF.js → chunk-FQTNR4ZS.js} +1 -1
  65. package/lib/public/pane-assets/chunk-L6LSA5WI.js +1 -0
  66. package/lib/public/pane-assets/chunk-M57UIGZV.js +1 -0
  67. package/lib/public/pane-assets/chunk-MGTFTEJP.js +1 -0
  68. package/lib/public/pane-assets/chunk-MMG6MYCN.js +1 -0
  69. package/lib/public/pane-assets/chunk-MZLMEXMW.js +1 -0
  70. package/lib/public/pane-assets/chunk-OG3E5SVA.js +166 -0
  71. package/lib/public/pane-assets/chunk-RGAFZ3T2.js +1 -0
  72. package/lib/public/pane-assets/chunk-RNDRURMF.js +1 -0
  73. package/lib/public/pane-assets/{chunk-PV4PCH2V.js → chunk-SJOT74VW.js} +1 -1
  74. package/lib/public/pane-assets/{chunk-LDIZLKB2.js → chunk-SJTGHIN5.js} +1 -1
  75. package/lib/public/pane-assets/chunk-SLMAFKI2.js +1 -0
  76. package/lib/public/pane-assets/chunk-U3I5KY3B.js +1 -0
  77. package/lib/public/pane-assets/{chunk-VNAV23CM.js → chunk-UPXU2IPC.js} +1 -1
  78. package/lib/public/pane-assets/{chunk-TMGSDFJI.js → chunk-VEBYD7TL.js} +1 -1
  79. package/lib/public/pane-assets/chunk-XBHYNXXT.js +1 -0
  80. package/lib/public/pane-assets/manifest.json +426 -414
  81. package/lib/public/pane-assets/style-BSKKK54Q.css +1 -0
  82. package/lib/sdk-message-processor.js +5 -2
  83. package/lib/server-storage.js +27 -3
  84. package/lib/server.js +4 -2
  85. package/lib/sessions.js +1 -1
  86. package/lib/storage-usage.js +29 -7
  87. package/lib/terminal-manager.js +2 -2
  88. package/lib/worker-message-previews.js +50 -0
  89. package/lib/ws-schema.js +1 -1
  90. package/lib/yoke/adapters/claude-worker.js +8 -0
  91. package/lib/yoke/adapters/claude.js +17 -2
  92. package/lib/yoke/claude-task-reader.js +94 -0
  93. package/lib/yoke/interface.js +1 -0
  94. package/package.json +1 -1
  95. package/lib/public/pane-assets/app-7UZ727NA.js +0 -1
  96. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +0 -1
  97. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +0 -1
  98. package/lib/public/pane-assets/chunk-6XK2OATZ.js +0 -1
  99. package/lib/public/pane-assets/chunk-BKIDR2ME.js +0 -1
  100. package/lib/public/pane-assets/chunk-GEFCFZEN.js +0 -1
  101. package/lib/public/pane-assets/chunk-GN2NQEVC.js +0 -2
  102. package/lib/public/pane-assets/chunk-IMLMBPT4.js +0 -166
  103. package/lib/public/pane-assets/chunk-K5S6AILM.js +0 -1
  104. package/lib/public/pane-assets/chunk-OWNNLZ36.js +0 -1
  105. package/lib/public/pane-assets/chunk-P2C3LOWL.js +0 -3
  106. package/lib/public/pane-assets/chunk-R4LMPTKU.js +0 -1
  107. package/lib/public/pane-assets/chunk-VWTHIQCE.js +0 -1
  108. package/lib/public/pane-assets/chunk-WM3K5AFH.js +0 -1
  109. package/lib/public/pane-assets/chunk-XOB3O327.js +0 -1
  110. package/lib/public/pane-assets/chunk-XOOUCFYT.js +0 -1
  111. package/lib/public/pane-assets/style-ZM7OOU3M.css +0 -1
@@ -1,28 +1,17 @@
1
- // background-tasks-ui.js - Background task status line above the composer.
2
- //
3
- // Collapsed it reads as a quiet status line, not a button demanding attention:
4
- // a muted activity indicator, a count, and a chevron. Expanding lists each task
5
- // with its elapsed time and a Stop control.
6
- //
7
- // Elapsed time comes from `started_at` (epoch ms) which the server stamps in
8
- // lib/background-task-timing.js. The ticker only runs while the list is
9
- // expanded, so a collapsed bar costs nothing.
1
+ import { bindAgentMessageBubble, rememberAgentMessage } from './agent-message-bubbles.js';
2
+ import { backgroundPreviewKey } from './background-task-previews.js';
3
+ // Native background work uses the same compact conversation chips as Workers.
10
4
 
5
+ import { openBackgroundTask } from './background-task-viewer.js';
11
6
  import { store } from './store.js';
12
7
  import { getWs } from './ws-ref.js';
13
- import { escapeHtml } from './utils.js';
14
8
  import { iconHtml, refreshIcons } from './icons.js';
15
9
 
16
- var expanded = false;
17
- var elapsedTimer = null;
18
-
19
- // Lucide icon per vendor-neutral task type (see normalizeBackgroundTasks in
20
- // yoke/adapters/claude.js for where these types are assigned).
21
- var TASK_TYPE_ICONS = { shell: "terminal", agent: "sparkles", other: "circle-dot" };
22
-
23
10
  export function initBackgroundTasksUi() {
24
11
  store.subscribe(function (state, prev) {
25
- if (state.activeBackgroundTasks !== prev.activeBackgroundTasks) renderBackgroundTasks();
12
+ if (state.sessionOriginId !== prev.sessionOriginId || state.currentSlug !== prev.currentSlug) { store.set({ activeBackgroundTasks: [], backgroundTaskCatalog: [] }); return; }
13
+ if (state.backgroundTaskViewer !== prev.backgroundTaskViewer) document.querySelectorAll('[data-background-task]').forEach(function (chip) { var view = state.backgroundTaskViewer; chip.setAttribute('aria-expanded', String(!!view && chip.dataset.backgroundTask === view.taskId)); });
14
+ if (state.activeBackgroundTasks !== prev.activeBackgroundTasks || state.backgroundTaskCatalog !== prev.backgroundTaskCatalog) renderBackgroundTasks();
26
15
  });
27
16
  renderBackgroundTasks();
28
17
  }
@@ -45,37 +34,6 @@ function pad(value) {
45
34
  return value < 10 ? "0" + value : String(value);
46
35
  }
47
36
 
48
- function stopElapsedTimer() {
49
- if (!elapsedTimer) return;
50
- clearInterval(elapsedTimer);
51
- elapsedTimer = null;
52
- }
53
-
54
- // Ticks at most once a second and only touches the time cells, so expanding
55
- // the list never re-renders (and never steals focus from) the rows.
56
- function startElapsedTimer() {
57
- stopElapsedTimer();
58
- elapsedTimer = setInterval(updateElapsedCells, 1000);
59
- }
60
-
61
- function updateElapsedCells() {
62
- var bar = document.getElementById('background-tasks-bar');
63
- if (!bar || !expanded) {
64
- stopElapsedTimer();
65
- return;
66
- }
67
- var now = Date.now();
68
- var cells = bar.querySelectorAll('.background-task-elapsed');
69
- for (var i = 0; i < cells.length; i++) {
70
- var startedAt = Number(cells[i].dataset.startedAt);
71
- cells[i].textContent = formatElapsed(startedAt, now);
72
- }
73
- }
74
-
75
- function activityDotsHtml() {
76
- return '<span class="background-tasks-activity" aria-hidden="true"><i></i><i></i><i></i></span>';
77
- }
78
-
79
37
  // Ambient tasks are the CLI's own housekeeping. The SDK asks hosts to keep
80
38
  // them out of activity indicators; Clay still lists them so the user can see
81
39
  // what runs on their behalf. Absent means false (Codex has no ambient concept,
@@ -96,83 +54,34 @@ export function splitTasks(tasks) {
96
54
  return { user: user, ambient: ambient };
97
55
  }
98
56
 
99
- function taskRowHtml(task, now) {
100
- var taskType = task.task_type || 'other';
101
- var icon = TASK_TYPE_ICONS[taskType] || TASK_TYPE_ICONS.other;
102
- var description = task.description || 'Background task';
103
- var startedAt = typeof task.started_at === 'number' ? task.started_at : 0;
104
- var elapsed = formatElapsed(startedAt, now);
105
- return '<div class="background-task-row' + (isAmbientTask(task) ? ' ambient' : '') + '">' +
106
- '<span class="background-task-icon" title="' + escapeHtml(taskType) + '">' + iconHtml(icon) + '</span>' +
107
- '<span class="background-task-description" title="' + escapeHtml(description) + '">' +
108
- escapeHtml(description) + '</span>' +
109
- '<span class="background-task-meta">' +
110
- '<span class="background-task-type">' + escapeHtml(taskType) + '</span>' +
111
- '<span class="background-task-elapsed" data-started-at="' + startedAt + '">' + escapeHtml(elapsed) + '</span>' +
112
- '</span>' +
113
- '<button type="button" class="background-task-stop" data-task-id="' + escapeHtml(task.task_id || '') + '" ' +
114
- 'aria-label="Stop ' + escapeHtml(description) + '">Stop</button>' +
115
- '</div>';
116
- }
117
-
118
57
  function renderBackgroundTasks() {
119
- var tasks = store.get('activeBackgroundTasks') || [];
120
- var split = splitTasks(tasks);
121
- var existing = document.getElementById('background-tasks-bar');
122
- // Ambient-only means nothing the user asked for is running, so the composer
123
- // stays quiet: no bar, no dots, no count.
124
- if (split.user.length === 0) {
125
- if (existing) existing.remove();
126
- expanded = false;
127
- stopElapsedTimer();
128
- return;
129
- }
130
- var inputArea = document.getElementById('input-area');
131
- if (!inputArea || !inputArea.parentNode) return;
132
-
133
- var bar = existing || document.createElement('div');
134
- bar.id = 'background-tasks-bar';
135
- bar.className = 'background-tasks-bar' + (expanded ? ' expanded' : '');
136
-
137
- var taskLabel = split.user.length === 1 ? 'background task' : 'background tasks';
138
- var summary = split.user.length + ' ' + taskLabel;
139
- var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '"' +
140
- ' aria-controls="background-tasks-list" aria-label="' + summary + ', ' +
141
- (expanded ? 'collapse' : 'expand') + ' details">' +
142
- activityDotsHtml() +
143
- '<span class="background-tasks-summary">' + summary + '</span>' +
144
- '<span class="background-tasks-chevron" aria-hidden="true">' + iconHtml('chevron-right') + '</span>' +
145
- '</button>';
146
-
147
- if (expanded) {
148
- var now = Date.now();
149
- html += '<div class="background-tasks-list" id="background-tasks-list">';
150
- for (var i = 0; i < split.user.length; i++) html += taskRowHtml(split.user[i], now);
151
- // Housekeeping is shown, not hidden: the user can see what Clay runs on
152
- // its own and stop a runaway one. Kept visually subordinate to real work.
153
- if (split.ambient.length > 0) {
154
- html += '<div class="background-tasks-section-label">Housekeeping</div>';
155
- for (var a = 0; a < split.ambient.length; a++) html += taskRowHtml(split.ambient[a], now);
58
+ var existing = document.getElementById('background-tasks-bar'); if (existing) existing.remove();
59
+ var split = splitTasks(store.get('backgroundTaskCatalog') || store.get('activeBackgroundTasks') || []);
60
+ var tasks = split.user.length ? split.user.concat(split.ambient) : [];
61
+ var root = document.getElementById('background-task-tabs');
62
+ if (!tasks.length) { if (root) root.remove(); return; }
63
+ var messages = document.getElementById('messages'); if (!messages) return;
64
+ if (!root) { root = document.createElement('div'); root.id = 'background-task-tabs'; root.className = 'split-view-workers background-task-tabs'; root.setAttribute('aria-label', 'Background tasks'); messages.parentNode.insertBefore(root, messages); }
65
+ var signature = JSON.stringify(tasks.map(function (task) { return [task.task_id, task.run_id, task.active, task.description]; }));
66
+ if (root.dataset.signature === signature) return;
67
+ root.dataset.signature = signature; root.textContent = '';
68
+ tasks.forEach(function (task) {
69
+ var pill = document.createElement('span'); pill.className = 'split-view-worker-pill' + (task.ambient ? ' background-task-ambient' : '');
70
+ var button = document.createElement('button'); button.type = 'button'; button.className = 'split-view-worker-chip'; button.dataset.backgroundTask = task.task_id;
71
+ bindAgentMessageBubble(button, backgroundPreviewKey(task));
72
+ if (task.preview) rememberAgentMessage(backgroundPreviewKey(task), task.preview, false);
73
+ button.title = (task.ambient ? 'Housekeeping · ' : '') + (task.description || 'Background task') + (task.started_at ? ' · ' + formatElapsed(task.started_at) : '') + (task.active === false ? ' · Finished' : ' · Running'); button.setAttribute('aria-haspopup', 'dialog');
74
+ var view = store.get('backgroundTaskViewer'); button.setAttribute('aria-expanded', String(!!view && view.taskId === task.task_id));
75
+ var dot = document.createElement('span'); dot.className = 'split-view-worker-dot'; dot.dataset.state = task.active === false ? 'ready' : 'working';
76
+ var label = document.createElement('span'); label.className = 'split-view-worker-name'; label.textContent = task.description || 'Background task';
77
+ dot.setAttribute('aria-label', task.active === false ? 'Finished' : 'Running');
78
+ button.append(dot, label); button.addEventListener('click', function () { openBackgroundTask(task); }); pill.appendChild(button);
79
+ if (task.active !== false) {
80
+ var stop = document.createElement('button'); stop.type = 'button'; stop.className = 'split-view-worker-remove'; stop.setAttribute('aria-label', 'Stop ' + (task.description || 'background task')); stop.title = 'Stop task'; stop.innerHTML = iconHtml('square');
81
+ stop.addEventListener('click', function () { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'background_task_stop', requestId: crypto.randomUUID(), taskId: task.task_id, runId: task.run_id || null, sessionOriginId: store.get('sessionOriginId') })); });
82
+ pill.appendChild(stop);
156
83
  }
157
- html += '</div>';
158
- }
159
- bar.innerHTML = html;
160
- if (!existing) inputArea.parentNode.insertBefore(bar, inputArea);
161
- refreshIcons();
162
-
163
- bar.querySelector('.background-tasks-toggle').addEventListener('click', function () {
164
- expanded = !expanded;
165
- renderBackgroundTasks();
84
+ root.appendChild(pill);
166
85
  });
167
-
168
- var stopButtons = bar.querySelectorAll('.background-task-stop');
169
- for (var j = 0; j < stopButtons.length; j++) {
170
- stopButtons[j].addEventListener('click', function () {
171
- var ws = getWs();
172
- if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'stop_task', taskId: this.dataset.taskId }));
173
- });
174
- }
175
-
176
- if (expanded) startElapsedTimer();
177
- else stopElapsedTimer();
86
+ refreshIcons();
178
87
  }
@@ -39,7 +39,7 @@ function appendIdentity(row, options, avatarClass) {
39
39
 
40
40
  export function createAssistantBubble(options) {
41
41
  options = options || {};
42
- var row = document.createElement("div");
42
+ var row = document.createElement(options.tagName || "div");
43
43
  row.className = "msg-assistant";
44
44
  if (options.turn !== undefined && options.turn !== null) row.dataset.turn = options.turn;
45
45
  var contentWrap = appendIdentity(row, options, "dm-bubble-avatar-mate");
@@ -52,7 +52,7 @@ export function createAssistantBubble(options) {
52
52
 
53
53
  export function createUserBubble(options) {
54
54
  options = options || {};
55
- var row = document.createElement("div");
55
+ var row = document.createElement(options.tagName || "div");
56
56
  row.className = "msg-user";
57
57
  if (options.turn !== undefined && options.turn !== null) row.dataset.turn = options.turn;
58
58
  var contentWrap = appendIdentity(row, options, "dm-bubble-avatar-me");
@@ -70,11 +70,17 @@ function assistantContent(row) {
70
70
  return row ? row.querySelector(".md-content") : null;
71
71
  }
72
72
 
73
- export function renderAssistantBubbleText(row, text, highlight) {
73
+ export function renderAssistantBubbleText(row, text, highlight, options) {
74
74
  var content = assistantContent(row);
75
75
  if (!content) return null;
76
- content.innerHTML = renderMarkdown(text || "");
77
- enhanceClaySessionLinks(content);
76
+ var html = renderMarkdown(text || "");
77
+ if (options && options.readOnly) html = DOMPurify.sanitize(html, {
78
+ ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'del', 's', 'blockquote', 'ul', 'ol', 'li', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'a', 'wbr'],
79
+ ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'start', 'class'], ALLOW_DATA_ATTR: false
80
+ });
81
+ content.innerHTML = html;
82
+ if (options && options.readOnly) content.querySelectorAll('[class]').forEach(function (element) { if (element.tagName !== 'CODE' && element.tagName !== 'PRE') element.removeAttribute('class'); });
83
+ if (!options || !options.readOnly) enhanceClaySessionLinks(content);
78
84
  if (highlight) highlightCodeBlocks(content);
79
85
  return content;
80
86
  }
@@ -8,8 +8,12 @@ function state() { return store.get('contextualClayGeometry') || {}; }
8
8
  function surface() { return document.getElementById('contextual-clay'); }
9
9
  function narrow() { return window.matchMedia('(max-width: 768px)').matches; }
10
10
  function rectValue(rect) { return { left: rect.left, top: rect.top, width: rect.width, height: rect.height }; }
11
+ function maximumWidth() {
12
+ var style = getComputedStyle(surface());
13
+ return (parseFloat(style.getPropertyValue('--mate-content-width')) || 860) + (parseFloat(style.getPropertyValue('--contextual-chat-gutter')) || 80);
14
+ }
11
15
  function bounded(value) {
12
- return { left: Math.round(Math.max(0, Math.min(16384, value.left))), top: Math.round(Math.max(0, Math.min(16384, value.top))), width: Math.round(Math.max(340, Math.min(2560, value.width))), height: Math.round(Math.max(360, Math.min(2160, value.height))) };
16
+ return { left: Math.round(Math.max(0, Math.min(16384, value.left))), top: Math.round(Math.max(0, Math.min(16384, value.top))), width: Math.round(Math.max(340, Math.min(maximumWidth(), value.width))), height: Math.round(Math.max(360, Math.min(2160, value.height))) };
13
17
  }
14
18
  function setState(patch) { store.set({ contextualClayGeometry: Object.assign({}, state(), patch) }); }
15
19
  function defaultGeometry() {
@@ -22,7 +26,7 @@ export function positionContextualClay() {
22
26
  var hint = renderContextualMateHint();
23
27
  if (!element || narrow() || (store.get('contextualClay') || {}).mode === 'opening') return;
24
28
  var desired = state().preview || state().desired || defaultGeometry();
25
- var width = Math.min(desired.width, window.innerWidth - 16);
29
+ var width = Math.min(desired.width, maximumWidth(), window.innerWidth - 16);
26
30
  var height = Math.min(desired.height, window.innerHeight - 16);
27
31
  var left = Math.max(8, Math.min(desired.left, window.innerWidth - width - 8));
28
32
  var top = Math.max(8, Math.min(desired.top, window.innerHeight - height - 8));
@@ -78,7 +82,7 @@ function move(event) {
78
82
  if (!state().preview && Math.hypot(dx, dy) < 3) return;
79
83
  var value = Object.assign({}, drag.start);
80
84
  if (drag.edge) {
81
- if (drag.edge.indexOf('e') >= 0) value.width = Math.max(340, Math.min(2560, window.innerWidth - value.left - 8, value.width + dx));
85
+ if (drag.edge.indexOf('e') >= 0) value.width = Math.max(340, Math.min(maximumWidth(), window.innerWidth - value.left - 8, value.width + dx));
82
86
  if (drag.edge.indexOf('s') >= 0) value.height = Math.max(360, Math.min(2160, window.innerHeight - value.top - 8, value.height + dy));
83
87
  } else {
84
88
  value.left = Math.max(8, Math.min(window.innerWidth - value.width - 8, value.left + dx));
@@ -174,7 +174,7 @@ export function paneOutgoing(data) {
174
174
  if (message.paneBinding && !samePaneBinding(binding, message.paneBinding)) return null;
175
175
  var gesture = (store.get('paneActionEvents') || []).find(function (entry) { return entry.event.isTrusted && entry.event.eventPhase && samePaneBinding(entry.binding, binding); });
176
176
  var action = store.get('paneCallbackBinding') || gesture && gesture.binding;
177
- var read = /^(get_|list_)|(_get|_list|_options)$/.test(message.type) || message.type === 'load_more_history';
177
+ var read = /^(get_|list_)|(_get|_list|_options)$/.test(message.type) || message.type === 'load_more_history' || message.type === 'background_task_read';
178
178
  if (!message.paneBinding && !read && !samePaneBinding(action, binding)) return null;
179
179
  return JSON.stringify(Object.assign({}, message, { paneBinding: message.paneBinding || action || binding }));
180
180
  }
@@ -221,7 +221,7 @@ export function initPaneActivation() {
221
221
  event.preventDefault(); event.stopImmediatePropagation();
222
222
  }
223
223
  }, true);
224
- ['click', 'input', 'change', 'keydown', 'keyup', 'submit', 'blur'].forEach(function (name) {
224
+ ['click', 'input', 'change', 'keydown', 'keyup', 'submit', 'blur', 'pointerup'].forEach(function (name) {
225
225
  document.addEventListener(name, function (event) {
226
226
  var keys = store.get('paneHeldKeys') || {};
227
227
  var binding = store.get('paneCommittedBinding');
@@ -229,7 +229,9 @@ export function initPaneActivation() {
229
229
  var staleKey = (name === 'keyup' || name === 'keydown' && event.repeat) && previousKey && !samePaneBinding(previousKey, binding);
230
230
  if (name === 'keydown' && !event.repeat) keys[event.code || event.key] = binding;
231
231
  if (name === 'keyup') delete keys[event.code || event.key];
232
- if (staleKey || store.get('panePendingBinding') || store.get('paneSuspended') || !store.get('paneReady')) {
232
+ // A release from an earlier conversation must not authorize a drag mutation.
233
+ var stalePointer = name === 'pointerup' && !samePaneBinding(store.get('panePointerBinding'), binding);
234
+ if (staleKey || stalePointer || store.get('panePendingBinding') || store.get('paneSuspended') || !store.get('paneReady')) {
233
235
  event.preventDefault(); event.stopImmediatePropagation(); return;
234
236
  }
235
237
  var events = (store.get('paneActionEvents') || []).filter(function (entry) { return entry.event.eventPhase; });
@@ -0,0 +1,32 @@
1
+ // Pane chrome uses the existing JS tooltip service, including lazy workbenches.
2
+ import { registerTooltip, pruneTooltip } from './tooltip.js';
3
+
4
+ var HEADERS = '.split-pane-header, .tool-pane-header, .file-viewer-tabbar, .terminal-header, .shared-browser-header, .split-view-workers, #top-bar .top-bar-actions';
5
+ function sync(header) {
6
+ header.querySelectorAll('[title], button, [role="button"], a[aria-label]').forEach(function (element) {
7
+ var label = element.getAttribute('title') || element.getAttribute('aria-label') || element.getAttribute('data-tip');
8
+ if (!label || !label.trim()) return;
9
+ if (element.matches('button, [role="button"], a') && (!element.hasAttribute('aria-label') || element.dataset.tooltipGeneratedLabel === 'true' && element.hasAttribute('title'))) {
10
+ element.dataset.tooltipGeneratedLabel = 'true';
11
+ if (element.getAttribute('aria-label') !== label) element.setAttribute('aria-label', label);
12
+ }
13
+ if (element.getAttribute('data-tip') !== label || element.hasAttribute('title')) registerTooltip(element, label);
14
+ });
15
+ }
16
+ export function initPaneHeaderTooltips() {
17
+ document.querySelectorAll(HEADERS).forEach(sync);
18
+ var observer = new MutationObserver(function (records) {
19
+ pruneTooltip();
20
+ var headers = new Set();
21
+ records.forEach(function (record) {
22
+ var parent = record.target.nodeType === 1 && record.target.closest(HEADERS); if (parent) headers.add(parent);
23
+ (record.addedNodes || []).forEach(function (node) {
24
+ if (node.nodeType !== 1) return;
25
+ if (node.matches(HEADERS)) headers.add(node);
26
+ node.querySelectorAll(HEADERS).forEach(function (header) { headers.add(header); });
27
+ });
28
+ });
29
+ headers.forEach(function (header) { if (header.isConnected) sync(header); });
30
+ });
31
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title', 'aria-label'] });
32
+ }
@@ -0,0 +1,69 @@
1
+ import { store } from './store.js';
2
+ import { queueContext, sameQueueContext } from './pending-message-queue-recovery.js';
3
+
4
+ function validDrag(drag) {
5
+ var queue = store.get('pendingMessageQueue') || {};
6
+ return drag && queueContext(store.snap()).enabled && sameQueueContext(drag.context, queueContext(store.snap())) && queue.revision === drag.revision;
7
+ }
8
+
9
+ export function paintPendingQueueDrag(element) {
10
+ var drag = store.get('pendingMessageQueueDrag');
11
+ element.classList.toggle('is-reordering', !!drag && drag.moved);
12
+ element.querySelectorAll('[data-pending-id]').forEach(function (row) {
13
+ row.classList.toggle('is-dragging', !!drag && drag.moved && row.dataset.pendingId === drag.id);
14
+ row.classList.toggle('is-drop-target', !!drag && drag.moved && row.dataset.pendingId === drag.targetId);
15
+ row.classList.toggle('is-drop-after', !!drag && drag.after && row.dataset.pendingId === drag.targetId);
16
+ });
17
+ }
18
+
19
+ export function bindPendingQueueDrag(element, reorder) {
20
+ function cleanupDrag(event) {
21
+ var drag = store.get('pendingMessageQueueDrag');
22
+ if (!drag || (event && drag.pointerId !== event.pointerId)) return null;
23
+ store.set({ pendingMessageQueueDrag: null });
24
+ if (element.hasPointerCapture(drag.pointerId)) element.releasePointerCapture(drag.pointerId);
25
+ paintPendingQueueDrag(element);
26
+ return drag;
27
+ }
28
+ element.addEventListener('pointerdown', function (event) {
29
+ var row = event.target.closest('[data-pending-id]');
30
+ var handle = row && row.querySelector('[data-action="drag"]');
31
+ if (event.target.closest('textarea, input, a, [contenteditable="true"]') || (event.target.closest('button') && !event.target.closest('[data-action="drag"]'))) return;
32
+ if (!handle || handle.disabled || !event.isPrimary || event.button !== 0) return;
33
+ if (store.get('pendingMessageQueueEdit') && store.get('pendingMessageQueueEdit').id === row.dataset.pendingId) return;
34
+ event.preventDefault();
35
+ handle.focus({ preventScroll: true });
36
+ // Rows can be replaced by a live queue refresh during this gesture.
37
+ element.setPointerCapture(event.pointerId);
38
+ store.set({ pendingMessageQueueDrag: { id: row.dataset.pendingId, pointerId: event.pointerId, targetId: null, context: queueContext(store.snap()), revision: (store.get('pendingMessageQueue') || {}).revision, startX: event.clientX, startY: event.clientY, moved: false } });
39
+ });
40
+ element.addEventListener('pointermove', function (event) {
41
+ var drag = store.get('pendingMessageQueueDrag');
42
+ if (!drag || drag.pointerId !== event.pointerId) return;
43
+ if (!validDrag(drag)) { cleanupDrag(event); return; }
44
+ var moved = drag.moved || Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) > 4;
45
+ var hit = document.elementFromPoint(event.clientX, event.clientY);
46
+ var row = hit && hit.closest('[data-pending-id]');
47
+ var targetId = row && element.contains(row) && row.querySelector('button[data-action="drag"]:not(:disabled)') && row.dataset.pendingId !== drag.id ? row.dataset.pendingId : null;
48
+ var rows = Array.from(element.querySelectorAll('[data-pending-id]'));
49
+ var source = rows.findIndex(function (candidate) { return candidate.dataset.pendingId === drag.id; });
50
+ store.set({ pendingMessageQueueDrag: Object.assign({}, drag, { moved: moved, targetId: targetId, after: targetId && rows.indexOf(row) > source }) });
51
+ paintPendingQueueDrag(element);
52
+ });
53
+ element.addEventListener('pointerup', function (event) {
54
+ var drag = cleanupDrag(event);
55
+ if (validDrag(drag) && drag.moved && drag.targetId) reorder(drag.id, 0, drag.targetId);
56
+ });
57
+ element.addEventListener('pointercancel', cleanupDrag);
58
+ element.addEventListener('lostpointercapture', cleanupDrag);
59
+ element.addEventListener('keydown', function (event) { if (event.key === 'Escape') cleanupDrag(); });
60
+ window.addEventListener('blur', function () { cleanupDrag(); });
61
+ store.subscribe(function (state, previous) {
62
+ var drag = state.pendingMessageQueueDrag;
63
+ if (drag && !validDrag(drag)) cleanupDrag();
64
+ else if (!drag && previous.pendingMessageQueueDrag) {
65
+ if (element.hasPointerCapture(previous.pendingMessageQueueDrag.pointerId)) element.releasePointerCapture(previous.pendingMessageQueueDrag.pointerId);
66
+ paintPendingQueueDrag(element);
67
+ }
68
+ });
69
+ }
@@ -5,6 +5,7 @@ import { refreshIcons } from './icons.js';
5
5
  import { activeQueueItems, hasFutureScheduledPending, moveQueueId, matchesQueueContext, canApplyQueueRevision } from './pending-message-queue-model.js';
6
6
  import { emptyQueue, queueContext, sameQueueContext, queueScopeChanged, queueReconnected, queueCanRender, createQueueHydrationRecovery, canonicalQueue } from './pending-message-queue-recovery.js';
7
7
  import { imageSrc, imageDraft, openImagePreview, chooseImages } from './pending-message-image-editor.js';
8
+ import { bindPendingQueueDrag, paintPendingQueueDrag } from './pending-message-queue-drag.js';
8
9
  import { setScheduleBtnDisabled } from './scheduled-message-state.js';
9
10
 
10
11
  export { hasFutureScheduledPending };
@@ -391,6 +392,7 @@ function render() {
391
392
  html += '</div>';
392
393
  queueEl.innerHTML = html;
393
394
  refreshIcons();
395
+ paintPendingQueueDrag(queueEl);
394
396
  restoreFocus(focus);
395
397
  }
396
398
 
@@ -426,39 +428,7 @@ function bindEvents() {
426
428
  var row = handle.closest("[data-pending-id]");
427
429
  reorder(row.dataset.pendingId, event.key === "ArrowUp" ? -1 : 1, null);
428
430
  });
429
- queueEl.addEventListener("pointerdown", function (event) {
430
- var handle = event.target.closest('[data-action="drag"]');
431
- if (!handle) return;
432
- var row = handle.closest("[data-pending-id]");
433
- handle.setPointerCapture(event.pointerId);
434
- store.set({ pendingMessageQueueDrag: { id: row.dataset.pendingId, pointerId: event.pointerId, targetId: null } });
435
- row.classList.add("is-dragging");
436
- });
437
- queueEl.addEventListener("pointermove", function (event) {
438
- var drag = store.get('pendingMessageQueueDrag');
439
- if (!drag || drag.pointerId !== event.pointerId) return;
440
- var target = document.elementFromPoint(event.clientX, event.clientY);
441
- var row = target && target.closest("[data-pending-id]");
442
- var targetId = row && row.dataset.pendingId !== drag.id ? row.dataset.pendingId : null;
443
- queueEl.querySelectorAll(".is-drop-target").forEach(function (node) { node.classList.remove("is-drop-target"); });
444
- if (targetId && ownedPendingIds().indexOf(targetId) >= 0) row.classList.add("is-drop-target");
445
- store.set({ pendingMessageQueueDrag: Object.assign({}, drag, { targetId: targetId }) });
446
- });
447
- function cleanupDrag(event) {
448
- var drag = store.get('pendingMessageQueueDrag');
449
- if (!drag || drag.pointerId !== event.pointerId) return null;
450
- store.set({ pendingMessageQueueDrag: null });
451
- queueEl.querySelectorAll(".is-dragging, .is-drop-target").forEach(function (node) { node.classList.remove("is-dragging", "is-drop-target"); });
452
- return drag;
453
- }
454
- function finishDrag(event) {
455
- var drag = cleanupDrag(event);
456
- if (!drag) return;
457
- if (drag.targetId && ownedPendingIds().indexOf(drag.targetId) >= 0) reorder(drag.id, 0, drag.targetId);
458
- }
459
- queueEl.addEventListener("pointerup", finishDrag);
460
- queueEl.addEventListener("pointercancel", cleanupDrag);
461
- queueEl.addEventListener("lostpointercapture", cleanupDrag);
431
+ bindPendingQueueDrag(queueEl, reorder);
462
432
  }
463
433
 
464
434
  export function initPendingMessageQueue() {
@@ -46,9 +46,9 @@ export function createPermissionControl(className, ariaLabel) {
46
46
  control.setAttribute("role", "group");
47
47
  control.setAttribute("aria-label", ariaLabel || "Session permission mode");
48
48
  control.innerHTML = '<div class="session-permission-segmented">' +
49
- '<button type="button" data-permission-mode="default" aria-label="Ask permissions" title="Ask permissions"><span class="permission-label">Ask</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
50
- '<button type="button" data-permission-mode="auto" aria-label="Auto permissions" title="Auto permissions"><span class="permission-label">Auto</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
51
- '<button type="button" data-permission-mode="bypassPermissions" aria-label="YOLO permissions" title="YOLO permissions"><span class="permission-label">YOLO</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
49
+ '<button type="button" data-permission-mode="default" aria-label="Ask permissions" data-tip="Ask before tool actions that need permission. Previously allowed actions can run without asking again."><span class="permission-label">Ask</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
50
+ '<button type="button" data-permission-mode="auto" aria-label="Auto permissions" data-tip="Let the runtime assess tool actions and approve them automatically when allowed. Some actions may still need your permission."><span class="permission-label">Auto</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
51
+ '<button type="button" data-permission-mode="bypassPermissions" aria-label="YOLO permissions" data-tip="Run tools, file changes and commands without asking for permission. Clay access restrictions still apply."><span class="permission-label">YOLO</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
52
52
  '</div><span class="session-permission-status" aria-live="polite"></span>';
53
53
  control.querySelector(".session-permission-status").setAttribute("aria-atomic", "true");
54
54
  return control;
@@ -102,7 +102,7 @@ export function renderPermissionControl(control, state) {
102
102
  control.classList.toggle("permission-globally-forced", globallyForced);
103
103
  control.classList.toggle("permission-runtime-fallback", state.permissionMode === "auto" && !!state.effectivePermissionMode && state.effectivePermissionMode !== "auto");
104
104
  control.setAttribute("aria-busy", pending ? "true" : "false");
105
- var buttonTitles = { default: "Ask permissions", auto: "Auto permissions", bypassPermissions: "YOLO permissions" };
105
+ var buttonDescriptions = {"default": "Ask before tool actions that need permission. Previously allowed actions can run without asking again.", "auto": "Let the runtime assess tool actions and approve them automatically when allowed. Some actions may still need your permission.", "bypassPermissions": "Run tools, file changes and commands without asking for permission. Clay access restrictions still apply."};
106
106
  var buttons = control.querySelectorAll("[data-permission-mode]");
107
107
  for (var i = 0; i < buttons.length; i++) {
108
108
  var mode = buttons[i].dataset.permissionMode;
@@ -115,7 +115,8 @@ export function renderPermissionControl(control, state) {
115
115
  buttons[i].classList.toggle("pending", requested);
116
116
  buttons[i].setAttribute("aria-pressed", selected ? "true" : "false");
117
117
  buttons[i].setAttribute("aria-busy", requested ? "true" : "false");
118
- buttons[i].title = unsupported ? "Auto permissions are unavailable for this session." : buttonTitles[mode];
118
+ buttons[i].setAttribute("data-tip", (unsupported ? "Auto is unavailable for this session. " : "") + buttonDescriptions[mode]);
119
+ buttons[i].removeAttribute("title");
119
120
  }
120
121
  var status = control.querySelector(".session-permission-status");
121
122
  if (status) {