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.
- package/lib/project-connection.js +5 -2
- package/lib/project-full-access.js +69 -56
- package/lib/project-sessions.js +126 -26
- package/lib/project-user-message.js +5 -2
- package/lib/project.js +1 -0
- package/lib/public/app.js +6 -0
- package/lib/public/css/filebrowser.css +20 -0
- package/lib/public/css/messages.css +27 -0
- package/lib/public/css/overlays.css +60 -0
- package/lib/public/css/pane.css +21 -4
- package/lib/public/css/pending-message-queue.css +5 -0
- package/lib/public/index.html +8 -3
- package/lib/public/modules/app-connection.js +4 -0
- package/lib/public/modules/app-header.js +18 -25
- package/lib/public/modules/app-messages.js +17 -7
- package/lib/public/modules/app-panels.js +22 -11
- package/lib/public/modules/clay-file-links.js +7 -0
- package/lib/public/modules/clay-log-links.js +4 -0
- package/lib/public/modules/github-links.js +115 -0
- package/lib/public/modules/markdown.js +9 -2
- package/lib/public/modules/mcp-ui.js +154 -17
- package/lib/public/modules/pane-bridge.js +2 -0
- package/lib/public/modules/pane-file-bridge.js +69 -0
- package/lib/public/modules/pending-message-queue-model.js +6 -0
- package/lib/public/modules/pending-message-queue-recovery.js +74 -0
- package/lib/public/modules/pending-message-queue.js +70 -67
- package/lib/public/modules/permission-control.js +187 -0
- package/lib/public/modules/split-view.js +29 -29
- package/lib/sdk-bridge.js +95 -40
- package/lib/session-permission-mode.js +42 -0
- package/lib/sessions.js +14 -1
- package/lib/ws-schema.js +2 -0
- package/lib/yoke/adapters/claude-worker-policy.js +67 -0
- package/lib/yoke/adapters/claude-worker.js +47 -7
- package/lib/yoke/adapters/claude.js +114 -6
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
-
|
|
479
|
-
|
|
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
|
}
|