clay-server 4.2.0-beta.1 → 4.2.0-beta.3

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 (36) hide show
  1. package/lib/project-connection.js +5 -2
  2. package/lib/project-full-access.js +69 -56
  3. package/lib/project-sessions.js +126 -26
  4. package/lib/project-user-message.js +5 -2
  5. package/lib/project.js +1 -0
  6. package/lib/public/app.js +6 -0
  7. package/lib/public/css/filebrowser.css +20 -0
  8. package/lib/public/css/messages.css +27 -0
  9. package/lib/public/css/overlays.css +60 -0
  10. package/lib/public/css/pane.css +21 -4
  11. package/lib/public/css/pending-message-queue.css +5 -0
  12. package/lib/public/index.html +8 -3
  13. package/lib/public/modules/app-connection.js +4 -0
  14. package/lib/public/modules/app-header.js +18 -25
  15. package/lib/public/modules/app-messages.js +17 -7
  16. package/lib/public/modules/app-panels.js +22 -11
  17. package/lib/public/modules/clay-file-links.js +7 -0
  18. package/lib/public/modules/clay-log-links.js +4 -0
  19. package/lib/public/modules/github-links.js +115 -0
  20. package/lib/public/modules/markdown.js +9 -2
  21. package/lib/public/modules/mcp-ui.js +154 -17
  22. package/lib/public/modules/pane-bridge.js +2 -0
  23. package/lib/public/modules/pane-file-bridge.js +69 -0
  24. package/lib/public/modules/pending-message-queue-model.js +6 -0
  25. package/lib/public/modules/pending-message-queue-recovery.js +74 -0
  26. package/lib/public/modules/pending-message-queue.js +70 -67
  27. package/lib/public/modules/permission-control.js +187 -0
  28. package/lib/public/modules/split-view.js +29 -29
  29. package/lib/sdk-bridge.js +95 -40
  30. package/lib/session-permission-mode.js +42 -0
  31. package/lib/sessions.js +14 -1
  32. package/lib/ws-schema.js +2 -0
  33. package/lib/yoke/adapters/claude-worker-policy.js +67 -0
  34. package/lib/yoke/adapters/claude-worker.js +47 -7
  35. package/lib/yoke/adapters/claude.js +114 -6
  36. package/package.json +1 -1
@@ -42,6 +42,8 @@ export function forwardPaneIssueReference(ref, projectSlug) {
42
42
  return true;
43
43
  }
44
44
 
45
+ export { forwardPaneFileReference } from './pane-file-bridge.js';
46
+
45
47
  // Toggle without setContextView: panes use a transient override and must not
46
48
  // rewrite the user's server-persisted main-view preference.
47
49
  function togglePaneContextPanel() {
@@ -0,0 +1,69 @@
1
+ import { store } from './store.js';
2
+
3
+ function paneFrame(host, source) {
4
+ if (!host || !source) return null;
5
+ var frames = host.querySelectorAll(".split-pane-frame");
6
+ for (var i = 0; i < frames.length; i++) {
7
+ if (frames[i].contentWindow === source) return frames[i];
8
+ }
9
+ return null;
10
+ }
11
+
12
+ function matchingPane(frame, split) {
13
+ if (!frame || !split || !split.panes) return null;
14
+ for (var i = 0; i < split.panes.length; i++) {
15
+ var pane = split.panes[i];
16
+ if (String(pane.slug) === String(frame.dataset.projectSlug) && String(pane.sessionId) === String(frame.dataset.sessionId)) return pane;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function hasLivePaneSource(frame, pane) {
22
+ if (!frame || !pane || typeof frame.src !== "string") return false;
23
+ var expected = "/p/" + encodeURIComponent(pane.slug) + "/?pane=1&session=" + encodeURIComponent(pane.sessionId);
24
+ try { return new URL(expected, window.location.href).href === frame.src; } catch (e) { return false; }
25
+ }
26
+
27
+ function validLocation(value) {
28
+ return value == null || (Number.isSafeInteger(value) && value > 0);
29
+ }
30
+
31
+ export function revealFilesPanel() {
32
+ var filesPanel = document.getElementById("sidebar-panel-files");
33
+ if (!filesPanel || !filesPanel.classList.contains("hidden")) return false;
34
+ var filesButton = document.getElementById("file-browser-btn");
35
+ if (!filesButton) return false;
36
+ filesButton.click();
37
+ return true;
38
+ }
39
+
40
+ export function forwardPaneFileReference(path, opts) {
41
+ if (!store.get('paneMode') || window.parent === window || typeof path !== "string" || !path) return false;
42
+ window.parent.postMessage({
43
+ type: "clay-pane-open-file",
44
+ path: path,
45
+ line: opts && opts.line != null ? opts.line : null,
46
+ column: opts && opts.column != null ? opts.column : null,
47
+ projectSlug: opts && opts.projectSlug ? opts.projectSlug : store.get('currentSlug'),
48
+ sessionId: opts && opts.sessionId != null ? String(opts.sessionId) : (store.get('activeSessionId') != null ? String(store.get('activeSessionId')) : null),
49
+ }, window.location.origin);
50
+ return true;
51
+ }
52
+
53
+ export function handlePaneFileMessage(event, host, openFile, openFiles) {
54
+ if (!event || event.origin !== window.location.origin || !host) return false;
55
+ var msg = event.data;
56
+ if (!msg || msg.type !== "clay-pane-open-file") return false;
57
+ var frame = paneFrame(host, event.source);
58
+ var state = store.snap();
59
+ var pane = matchingPane(frame, state.splitPanes);
60
+ if (!pane || !hasLivePaneSource(frame, pane) || String(pane.slug) !== String(state.currentSlug) || msg.projectSlug !== frame.dataset.projectSlug || String(msg.sessionId) !== String(frame.dataset.sessionId) || !validLocation(msg.line) || !validLocation(msg.column)) return true;
61
+ if (typeof msg.path !== "string" || !msg.path) return true;
62
+ if (!state.connected || typeof openFile !== "function") return true;
63
+ // The frame/session association was validated above. The host browser owns
64
+ // the read, so do not apply the pane session as a host-session restriction;
65
+ // the normal authenticated project filesystem checks remain authoritative.
66
+ var result = openFile(msg.path, { line: msg.line, column: msg.column, projectSlug: msg.projectSlug });
67
+ if (result !== false && typeof openFiles === "function") openFiles();
68
+ return true;
69
+ }
@@ -2,6 +2,12 @@ export function activeQueueItems(items) {
2
2
  return (items || []).filter(function (item) { return item.state === "pending" || item.state === "claimed"; });
3
3
  }
4
4
 
5
+ export function hasFutureScheduledPending(items) {
6
+ items = items || [];
7
+ for (var i = 0; i < items.length; i++) if (items[i].state === "pending" && items[i].schedule && items[i].schedule.ready !== true) return true;
8
+ return false;
9
+ }
10
+
5
11
  export function moveQueueId(ids, id, direction, targetId) {
6
12
  var next = (ids || []).slice();
7
13
  var from = next.indexOf(id);
@@ -0,0 +1,74 @@
1
+ // Queue recovery keeps an authorized queue scope distinct from WebSocket transport.
2
+
3
+ function sameId(a, b) { return String(a) === String(b); }
4
+
5
+ export function emptyQueue() { return { projectSlug: null, sessionId: null, revision: 0, paused: false, items: [], loading: false, error: "", errorSource: null }; }
6
+
7
+ export function queueContext(state) {
8
+ state = state || {};
9
+ var available = !!state.currentSlug && !!state.activeSessionId && state.activeSessionMode !== "tui" && !state.dmMode && !state.mateProjectSlug;
10
+ return {
11
+ connected: !!state.connected,
12
+ projectSlug: state.currentSlug || null,
13
+ sessionId: state.activeSessionId || null,
14
+ accountId: state.myUserId || "default",
15
+ available: available,
16
+ enabled: available && !!state.connected,
17
+ };
18
+ }
19
+
20
+ export function sameQueueContext(a, b) {
21
+ return !!a && !!b && a.projectSlug === b.projectSlug && sameId(a.sessionId, b.sessionId) && a.accountId === b.accountId;
22
+ }
23
+
24
+ export function queueScopeChanged(state, previous) {
25
+ var next = queueContext(state);
26
+ var prior = queueContext(previous);
27
+ return !sameQueueContext(next, prior) || next.available !== prior.available || !!state.isMultiUserMode !== !!previous.isMultiUserMode;
28
+ }
29
+
30
+ export function queueReconnected(state, previous) {
31
+ return !!state.connected && !previous.connected && !queueScopeChanged(state, previous);
32
+ }
33
+
34
+ export function queueCanRender(queue, target) {
35
+ return !!target && target.available && !!queue && queue.projectSlug === target.projectSlug && sameId(queue.sessionId, target.sessionId);
36
+ }
37
+
38
+ export function createQueueHydrationRecovery() {
39
+ var timer = null;
40
+ var requestId = null;
41
+ function clear(id) {
42
+ if (id && requestId !== id) return;
43
+ if (timer) clearTimeout(timer);
44
+ timer = null;
45
+ requestId = null;
46
+ }
47
+ return {
48
+ schedule: function (id, retry) {
49
+ clear();
50
+ requestId = id;
51
+ timer = setTimeout(function () {
52
+ timer = null;
53
+ requestId = null;
54
+ retry();
55
+ }, 10000);
56
+ if (timer && timer.unref) timer.unref();
57
+ },
58
+ clear: clear,
59
+ };
60
+ }
61
+
62
+ export function canonicalQueue(queue, result) {
63
+ if (queue && queue.projectSlug === result.projectSlug && sameId(queue.sessionId, result.sessionId) && Number(result.revision) < Number(queue.revision)) return null;
64
+ return {
65
+ projectSlug: result.projectSlug,
66
+ sessionId: result.sessionId,
67
+ revision: Number(result.revision) || 0,
68
+ paused: result.paused === true,
69
+ items: Array.isArray(result.items) ? result.items : queue.items || [],
70
+ loading: false,
71
+ error: "",
72
+ errorSource: null,
73
+ };
74
+ }
@@ -2,77 +2,50 @@ import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
3
  import { escapeHtml } from './utils.js';
4
4
  import { refreshIcons } from './icons.js';
5
- import { activeQueueItems, moveQueueId, matchesQueueContext, canApplyQueueRevision } from './pending-message-queue-model.js';
5
+ import { activeQueueItems, hasFutureScheduledPending, moveQueueId, matchesQueueContext, canApplyQueueRevision } from './pending-message-queue-model.js';
6
+ import { emptyQueue, queueContext, sameQueueContext, queueScopeChanged, queueReconnected, queueCanRender, createQueueHydrationRecovery, canonicalQueue } from './pending-message-queue-recovery.js';
6
7
  import { imageSrc, imageDraft, openImagePreview, chooseImages } from './pending-message-image-editor.js';
7
8
  import { setScheduleBtnDisabled } from './scheduled-message-state.js';
8
9
 
10
+ export { hasFutureScheduledPending };
9
11
  var queueEl = null;
10
12
  var initialized = false;
11
13
  var requestCounter = 0;
12
14
  var editGenerationCounter = 0;
13
-
14
- function emptyQueue() {
15
- return { projectSlug: null, sessionId: null, revision: 0, paused: false, items: [], loading: false, error: "" };
16
- }
17
-
18
- function context() {
19
- var state = store.snap();
20
- return {
21
- connected: !!state.connected,
22
- projectSlug: state.currentSlug || null,
23
- sessionId: state.activeSessionId || null,
24
- accountId: state.myUserId || "default",
25
- enabled: !!state.connected && !!state.currentSlug && !!state.activeSessionId && state.activeSessionMode !== "tui" && !state.dmMode && !state.mateProjectSlug,
26
- };
27
- }
28
-
15
+ var hydrationRecovery = createQueueHydrationRecovery();
16
+ function context() { return queueContext(store.snap()); }
29
17
  function sameId(a, b) { return String(a) === String(b); }
30
- function sameContext(a, b) { return !!a && !!b && a.projectSlug === b.projectSlug && sameId(a.sessionId, b.sessionId) && a.accountId === b.accountId; }
18
+ function sameContext(a, b) { return sameQueueContext(a, b); }
31
19
  function currentQueue() { return store.get('pendingMessageQueue') || emptyQueue(); }
32
-
33
20
  function isOwn(item) {
34
21
  if (!store.get('isMultiUserMode')) return true;
35
22
  return !!item && String(item.actorId) === String(store.get('myUserId'));
36
23
  }
37
-
38
- export function hasFutureScheduledPending(items) {
39
- items = items || [];
40
- for (var i = 0; i < items.length; i++) {
41
- if (items[i].state === "pending" && items[i].schedule && items[i].schedule.ready !== true) return true;
42
- }
43
- return false;
44
- }
45
-
46
24
  function syncScheduleButton() {
47
25
  var target = context();
48
26
  var queue = currentQueue();
49
27
  var current = target.enabled && queue.projectSlug === target.projectSlug && sameId(queue.sessionId, target.sessionId);
50
28
  setScheduleBtnDisabled(current && hasFutureScheduledPending(queue.items));
51
29
  }
52
-
53
30
  function isEditableItem(id) {
54
31
  var items = currentQueue().items || [];
55
32
  for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].state === "pending" && isOwn(items[i]);
56
33
  return false;
57
34
  }
58
-
59
35
  function send(message) {
60
36
  var socket = getWs();
61
37
  if (!socket || socket.readyState !== 1) return false;
62
- socket.send(JSON.stringify(message));
63
- return true;
38
+ try { socket.send(JSON.stringify(message)); return true; }
39
+ catch (e) { return false; }
64
40
  }
65
-
66
41
  function nextRequestId(kind) {
67
42
  requestCounter++;
68
43
  return "pending-" + kind + "-" + Date.now().toString(36) + "-" + requestCounter;
69
44
  }
70
-
71
45
  function nextEditGeneration() {
72
46
  editGenerationCounter++;
73
47
  return editGenerationCounter;
74
48
  }
75
-
76
49
  function updateEdit(update) {
77
50
  var edit = store.get('pendingMessageQueueEdit');
78
51
  if (!edit) return;
@@ -81,7 +54,6 @@ function updateEdit(update) {
81
54
  next.generation = nextEditGeneration();
82
55
  store.set({ pendingMessageQueueEdit: next });
83
56
  }
84
-
85
57
  function rememberRequest(kind, extra) {
86
58
  var requestId = nextRequestId(kind);
87
59
  var requests = Object.assign({}, store.get('pendingMessageQueueRequests') || {});
@@ -89,7 +61,6 @@ function rememberRequest(kind, extra) {
89
61
  store.set({ pendingMessageQueueRequests: requests });
90
62
  return requestId;
91
63
  }
92
-
93
64
  function forgetRequest(requestId) {
94
65
  var requests = Object.assign({}, store.get('pendingMessageQueueRequests') || {});
95
66
  var request = requests[requestId] || null;
@@ -97,15 +68,33 @@ function forgetRequest(requestId) {
97
68
  store.set({ pendingMessageQueueRequests: requests });
98
69
  return request;
99
70
  }
71
+ function scheduleHydrationRetry(requestId) {
72
+ hydrationRecovery.schedule(requestId, function () {
73
+ var requests = store.get('pendingMessageQueueRequests') || {};
74
+ if (!requests[requestId] || !sameContext(requests[requestId].context, context()) || !context().enabled) return;
75
+ forgetRequest(requestId);
76
+ store.set({ pendingMessageQueue: Object.assign({}, currentQueue(), { loading: false, error: "Queue refresh timed out. Retrying…", errorSource: "hydration" }) });
77
+ requestPendingMessages(true);
78
+ });
79
+ }
100
80
 
81
+ function retryHydration() {
82
+ hydrationRecovery.schedule("retry", function () { if (context().enabled) requestPendingMessages(true); });
83
+ }
101
84
  export function requestPendingMessages(preserveError) {
102
85
  var target = context();
103
86
  if (!target.enabled) return false;
104
87
  var requestId = rememberRequest("get");
105
- store.set({ pendingMessageQueue: Object.assign({}, currentQueue(), { projectSlug: target.projectSlug, sessionId: target.sessionId, loading: true, error: preserveError ? currentQueue().error : "" }) });
106
- return send({ type: "pending_message_get", projectSlug: target.projectSlug, sessionId: target.sessionId, requestId: requestId });
88
+ store.set({ pendingMessageQueue: Object.assign({}, currentQueue(), { projectSlug: target.projectSlug, sessionId: target.sessionId, loading: true, error: preserveError ? currentQueue().error : "", errorSource: preserveError ? currentQueue().errorSource : null }), pendingMessageQueueRecovery: true });
89
+ if (!send({ type: "pending_message_get", projectSlug: target.projectSlug, sessionId: target.sessionId, requestId: requestId })) {
90
+ forgetRequest(requestId);
91
+ store.set({ pendingMessageQueue: Object.assign({}, currentQueue(), { loading: false, error: "Queue connection unavailable", errorSource: "hydration" }) });
92
+ retryHydration();
93
+ return false;
94
+ }
95
+ scheduleHydrationRetry(requestId);
96
+ return true;
107
97
  }
108
-
109
98
  function sendMutation(kind, fields) {
110
99
  var target = context();
111
100
  var queue = currentQueue();
@@ -131,7 +120,6 @@ function sendMutation(kind, fields) {
131
120
  }
132
121
  return true;
133
122
  }
134
-
135
123
  function sendScheduledNow(id) {
136
124
  var target = context();
137
125
  var queue = currentQueue();
@@ -153,19 +141,12 @@ function scheduledTimeLabel(notBefore) {
153
141
 
154
142
  function applyCanonical(result) {
155
143
  var target = context();
156
- if (!result || result.projectSlug !== target.projectSlug || !sameId(result.sessionId, target.sessionId)) return false;
144
+ if (!target.available || !result || result.projectSlug !== target.projectSlug || !sameId(result.sessionId, target.sessionId)) return false;
157
145
  var queue = currentQueue();
158
- if (queue.projectSlug === result.projectSlug && sameId(queue.sessionId, result.sessionId) && !canApplyQueueRevision(queue.revision, result.revision)) return false;
146
+ var nextQueue = canonicalQueue(queue, result);
147
+ if (!nextQueue) return false;
159
148
  var nextEdit = editAfterQueueTransition(Array.isArray(result.items) ? result.items : queue.items || []);
160
- var update = { pendingMessageQueue: {
161
- projectSlug: result.projectSlug,
162
- sessionId: result.sessionId,
163
- revision: Number(result.revision) || 0,
164
- paused: result.paused === true,
165
- items: Array.isArray(result.items) ? result.items : queue.items || [],
166
- loading: false,
167
- error: "",
168
- } };
149
+ var update = { pendingMessageQueueRecovery: false, pendingMessageQueue: nextQueue };
169
150
  if (nextEdit) update.pendingMessageQueueEdit = nextEdit;
170
151
  store.set(update);
171
152
  return true;
@@ -182,7 +163,7 @@ function editAfterQueueTransition(items) {
182
163
 
183
164
  function conflict(request, error) {
184
165
  var edit = store.get('pendingMessageQueueEdit');
185
- var update = { pendingMessageQueue: Object.assign({}, currentQueue(), { loading: false, error: error || "Queue changed. Refreshed the latest order." }) };
166
+ var update = { pendingMessageQueue: Object.assign({}, currentQueue(), { loading: false, error: error || "Queue changed. Refreshed the latest order.", errorSource: "mutation" }) };
186
167
  if (request && request.kind === "edit" && edit && edit.id === request.itemId) update.pendingMessageQueueEdit = Object.assign({}, edit, { error: error || "Queue changed. Review your edit and save again." });
187
168
  store.set(update);
188
169
  requestPendingMessages(true);
@@ -192,20 +173,26 @@ export function handlePendingMessageQueueMessage(msg) {
192
173
  var types = ["pending_message_result", "message_queued", "pending_message_queued", "pending_message_state", "pending_message_claimed", "pending_message_consumed"];
193
174
  if (!msg || types.indexOf(msg.type) < 0) return false;
194
175
  var target = context();
176
+ if (!target.available) return true;
195
177
  if (msg.type === "pending_message_result") {
196
178
  var request = forgetRequest(msg.requestId);
179
+ if (request && request.kind === "get") hydrationRecovery.clear(msg.requestId);
197
180
  if (!request || !sameContext(request.context, target)) return true;
198
181
  var result = msg.result || {};
199
- if (!matchesQueueContext(result, target.projectSlug, target.sessionId)) return true;
182
+ if (!matchesQueueContext(result, target.projectSlug, target.sessionId)) {
183
+ if (request.kind === "get") retryHydration();
184
+ return true;
185
+ }
200
186
  if (!result.ok) {
201
187
  if (request.kind === "get") {
202
- store.set({ pendingMessageQueue: Object.assign({}, currentQueue(), { loading: false, error: result.error || "Queue update failed" }) });
188
+ store.set({ pendingMessageQueue: Object.assign({}, currentQueue(), { loading: false, error: result.error || "Queue update failed", errorSource: "hydration" }) });
189
+ retryHydration();
203
190
  return true;
204
191
  }
205
192
  conflict(request, result.error || "Queue update failed");
206
193
  return true;
207
194
  }
208
- applyCanonical(result);
195
+ if (!applyCanonical(result) && request.kind === "get") retryHydration();
209
196
  if (request.kind === "edit") {
210
197
  var currentEdit = store.get('pendingMessageQueueEdit');
211
198
  if (currentEdit && currentEdit.id === request.itemId && currentEdit.text === request.editText && (!Object.prototype.hasOwnProperty.call(request, "editImages") || JSON.stringify(currentEdit.images || []) === request.editImages) && currentEdit.generation === request.editGeneration) {
@@ -237,7 +224,7 @@ export function handlePendingMessageQueueMessage(msg) {
237
224
  });
238
225
  }
239
226
  var nextEdit = editAfterQueueTransition(items);
240
- var update = { pendingMessageQueue: Object.assign({}, queue, { projectSlug: msg.projectSlug, sessionId: msg.sessionId, revision: Number(msg.revision) || queue.revision, paused: msg.paused === true, items: items, loading: false, error: "" }) };
227
+ var update = { pendingMessageQueue: Object.assign({}, queue, { projectSlug: msg.projectSlug, sessionId: msg.sessionId, revision: Number(msg.revision) || queue.revision, paused: msg.paused === true, items: items, loading: false, error: "", errorSource: null }) };
241
228
  if (nextEdit) update.pendingMessageQueueEdit = nextEdit;
242
229
  store.set(update);
243
230
  return true;
@@ -339,10 +326,12 @@ function renderItem(item, index) {
339
326
  var attachment = attachmentLabel(item);
340
327
  var scheduled = item.schedule && item.schedule.jobId;
341
328
  var futureScheduled = scheduled && item.schedule.ready !== true;
329
+ var controlsDisabled = !context().enabled;
330
+ var disabled = controlsDisabled ? ' disabled aria-disabled="true"' : '';
342
331
  var text = (item.message && item.message.text || "").trim() || "Attachment-only message";
343
332
  var html = '<div class="pending-message-chip' + (item.state === "claimed" ? ' is-claimed' : '') + (scheduled ? ' is-scheduled' : '') + (futureScheduled ? ' is-future' : '') + '" data-pending-id="' + escapeHtml(item.id) + '">';
344
333
  html += '<span class="pending-message-number" aria-hidden="true">' + (index + 1) + '</span>';
345
- if (own && item.state === "pending") html += '<button class="pending-message-drag" type="button" data-action="drag" aria-label="Reorder queued message ' + (index + 1) + '" title="Drag, or use arrow keys to reorder">' + '<i data-lucide="grip-vertical"></i></button>';
334
+ if (own && item.state === "pending") html += '<button class="pending-message-drag" type="button" data-action="drag" aria-label="Reorder queued message ' + (index + 1) + '" title="Drag, or use arrow keys to reorder"' + disabled + '>' + '<i data-lucide="grip-vertical"></i></button>';
346
335
  else html += '<span class="pending-message-drag is-static"><i data-lucide="' + (item.state === "claimed" ? "loader-circle" : "lock-keyhole") + '"></i></span>';
347
336
  html += '<div class="pending-message-content">';
348
337
  if (isEditing) {
@@ -356,7 +345,7 @@ function renderItem(item, index) {
356
345
  html += '<button type="button" class="pending-message-add-image" data-action="add-image"' + (editing.pendingReads ? ' disabled' : '') + '>+ Add images</button></div>';
357
346
  if (editing.pendingReads) html += '<div class="pending-message-image-reading">Reading image…</div>';
358
347
  if (editing.error) html += '<div class="pending-message-error" role="alert">' + escapeHtml(editing.error) + '</div>';
359
- html += '<div class="pending-message-edit-actions"><button type="button" data-action="edit-cancel">Cancel</button><button type="button" class="is-primary" data-action="edit-save"' + (editing.pendingReads ? ' disabled' : '') + '>Save</button></div>';
348
+ html += '<div class="pending-message-edit-actions"><button type="button" data-action="edit-cancel">Cancel</button><button type="button" class="is-primary" data-action="edit-save"' + (editing.pendingReads || !context().enabled ? ' disabled' : '') + '>Save</button></div>';
360
349
  } else {
361
350
  html += '<div class="pending-message-preview" title="' + escapeHtml(text) + '">' + escapeHtml(text) + '</div>';
362
351
  if (item.message && item.message.images && item.message.images.length) {
@@ -371,9 +360,9 @@ function renderItem(item, index) {
371
360
  if (scheduled) html += '<div class="pending-message-schedule-meta"><i data-lucide="clock-3"></i><span>' + escapeHtml(futureScheduled ? scheduledTimeLabel(item.schedule.notBefore) : "Ready to send") + '</span></div>';
372
361
  }
373
362
  html += '</div><div class="pending-message-actions">';
374
- if (!isEditing && own && item.state === "pending") html += '<button type="button" data-action="edit" aria-label="Edit queued message" title="Edit"><i data-lucide="pencil"></i></button>';
375
- if (!isEditing && own && item.state === "pending" && futureScheduled) html += '<button type="button" data-action="send-now" aria-label="Send scheduled message now" title="Send now"><i data-lucide="send"></i></button>';
376
- if (!isEditing && own) html += '<button type="button" data-action="cancel" aria-label="Cancel queued message" title="Cancel"><i data-lucide="x"></i></button>';
363
+ if (!isEditing && own && item.state === "pending") html += '<button type="button" data-action="edit" aria-label="Edit queued message" title="Edit"' + disabled + '><i data-lucide="pencil"></i></button>';
364
+ if (!isEditing && own && item.state === "pending" && futureScheduled) html += '<button type="button" data-action="send-now" aria-label="Send scheduled message now" title="Send now"' + disabled + '><i data-lucide="send"></i></button>';
365
+ if (!isEditing && own) html += '<button type="button" data-action="cancel" aria-label="Cancel queued message" title="Cancel"' + disabled + '><i data-lucide="x"></i></button>';
377
366
  html += '</div></div>';
378
367
  return html;
379
368
  }
@@ -386,12 +375,15 @@ function render() {
386
375
  var queue = currentQueue();
387
376
  var items = activeQueueItems(queue.items);
388
377
  var focus = captureFocus();
389
- var visible = target.enabled && queue.projectSlug === target.projectSlug && sameId(queue.sessionId, target.sessionId) && items.length > 0;
378
+ var recovering = !!store.get('pendingMessageQueueRecovery');
379
+ var visible = queueCanRender(queue, target) && (items.length > 0 || queue.errorSource === "mutation");
390
380
  queueEl.classList.toggle("hidden", !visible);
391
381
  if (!visible) { queueEl.innerHTML = ""; return; }
392
382
  var html = '<div class="pending-message-head"><div><span class="pending-message-eyebrow">Next up</span><span class="pending-message-count">' + items.length + '</span></div>';
393
- if (queue.paused) html += '<button type="button" class="pending-message-resume" data-action="resume"><i data-lucide="play"></i><span>Resume queue</span></button>';
383
+ if (queue.paused) html += '<button type="button" class="pending-message-resume" data-action="resume"' + (target.enabled ? '' : ' disabled aria-disabled="true"') + '><i data-lucide="play"></i><span>Resume queue</span></button>';
394
384
  html += '</div>';
385
+ if (!target.connected) html += '<div class="pending-message-restoring" role="status">Connection lost. Queue changes are unavailable.</div>';
386
+ else if (queue.loading || recovering) html += '<div class="pending-message-restoring" role="status">Restoring queued messages…</div>';
395
387
  if (queue.error) html += '<div class="pending-message-banner-error" role="alert">' + escapeHtml(queue.error) + '</div>';
396
388
  html += '<div class="pending-message-list">';
397
389
  for (var i = 0; i < items.length; i++) html += renderItem(items[i], i);
@@ -474,18 +466,29 @@ export function initPendingMessageQueue() {
474
466
  queueEl = document.getElementById("pending-message-queue");
475
467
  if (!queueEl) return;
476
468
  bindEvents();
469
+ var initialTarget = context();
470
+ if (initialTarget.available && !queueCanRender(currentQueue(), initialTarget)) store.set({ pendingMessageQueue: Object.assign({}, emptyQueue(), { projectSlug: initialTarget.projectSlug, sessionId: initialTarget.sessionId }), pendingMessageQueueRecovery: true });
477
471
  store.subscribe(function (state, previous) {
478
- var contextChanged = state.connected !== previous.connected || state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.activeSessionMode !== previous.activeSessionMode || state.dmMode !== previous.dmMode || state.mateProjectSlug !== previous.mateProjectSlug || state.myUserId !== previous.myUserId || state.isMultiUserMode !== previous.isMultiUserMode;
479
- if (contextChanged) {
480
- store.set({ pendingMessageQueue: emptyQueue(), pendingMessageQueueRequests: {}, pendingMessageQueueEdit: null, pendingMessageQueueDrag: null, pendingMessageQueueFocusReturn: null });
472
+ if (queueScopeChanged(state, previous)) {
473
+ hydrationRecovery.clear();
474
+ store.set({ pendingMessageQueue: emptyQueue(), pendingMessageQueueRecovery: false, pendingMessageQueueRequests: {}, pendingMessageQueueEdit: null, pendingMessageQueueDrag: null, pendingMessageQueueFocusReturn: null });
481
475
  setScheduleBtnDisabled(false);
482
476
  if (context().enabled) requestPendingMessages();
483
477
  return;
484
478
  }
479
+ if (queueReconnected(state, previous)) {
480
+ hydrationRecovery.clear();
481
+ store.set({ pendingMessageQueueRequests: {} });
482
+ requestPendingMessages();
483
+ render();
484
+ return;
485
+ }
486
+ if (state.connected !== previous.connected) { render(); return; }
485
487
  var edit = state.pendingMessageQueueEdit;
486
488
  var previousEdit = previous.pendingMessageQueueEdit;
487
489
  var editNeedsRender = edit !== previousEdit && (!edit || !previousEdit || edit.id !== previousEdit.id || edit.error !== previousEdit.error || edit.images !== previousEdit.images || edit.pendingReads !== previousEdit.pendingReads);
488
490
  if (state.pendingMessageQueue !== previous.pendingMessageQueue || editNeedsRender) render();
489
491
  });
490
492
  render();
493
+ if (context().enabled) requestPendingMessages();
491
494
  }