clay-server 4.2.0-beta.1 → 4.2.0-beta.2
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 +73 -0
- package/lib/public/modules/pending-message-queue.js +67 -64
- 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,73 @@
|
|
|
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: "" }; }
|
|
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
|
+
};
|
|
73
|
+
}
|
|
@@ -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…" }) });
|
|
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 : "" }), 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" }) });
|
|
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;
|
|
@@ -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
188
|
store.set({ pendingMessageQueue: Object.assign({}, currentQueue(), { loading: false, error: result.error || "Queue update failed" }) });
|
|
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) {
|
|
@@ -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.loading || recovering || !!queue.error);
|
|
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
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Shared session permission selector for standalone and split Driver headers.
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { getWs } from './ws-ref.js';
|
|
5
|
+
import { showToast } from './utils.js';
|
|
6
|
+
|
|
7
|
+
var REQUEST_TIMEOUT_MS = 10000;
|
|
8
|
+
var requestTimers = {};
|
|
9
|
+
|
|
10
|
+
function targetKey(projectSlug, sessionId) {
|
|
11
|
+
return String(projectSlug || "") + ":" + String(sessionId || "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function pendingMap() {
|
|
15
|
+
return store.get('permissionModePendingByTarget') || {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function permissionPendingFor(projectSlug, sessionId) {
|
|
19
|
+
return pendingMap()[targetKey(projectSlug, sessionId)] || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function replacePending(key, value) {
|
|
23
|
+
var next = Object.assign({}, pendingMap());
|
|
24
|
+
if (value) next[key] = value;
|
|
25
|
+
else delete next[key];
|
|
26
|
+
store.set({ permissionModePendingByTarget: next });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearRequest(requestId, key) {
|
|
30
|
+
if (requestTimers[requestId]) clearTimeout(requestTimers[requestId]);
|
|
31
|
+
delete requestTimers[requestId];
|
|
32
|
+
var pending = pendingMap()[key];
|
|
33
|
+
if (pending && pending.requestId === requestId) replacePending(key, null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function clearPermissionModePending() {
|
|
37
|
+
var ids = Object.keys(requestTimers);
|
|
38
|
+
for (var i = 0; i < ids.length; i++) clearTimeout(requestTimers[ids[i]]);
|
|
39
|
+
requestTimers = {};
|
|
40
|
+
store.set({ permissionModePendingByTarget: {} });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createPermissionControl(className, ariaLabel) {
|
|
44
|
+
var control = document.createElement("div");
|
|
45
|
+
control.className = "session-permission-control " + (className || "");
|
|
46
|
+
control.setAttribute("role", "group");
|
|
47
|
+
control.setAttribute("aria-label", ariaLabel || "Session permission mode");
|
|
48
|
+
control.innerHTML = '<div class="session-permission-segmented">' +
|
|
49
|
+
'<button type="button" data-permission-mode="default">Ask</button>' +
|
|
50
|
+
'<button type="button" data-permission-mode="auto">Auto</button>' +
|
|
51
|
+
'<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions"><span class="permission-label-long">Skip permissions</span><span class="permission-label-short" aria-hidden="true">Skip</span></button>' +
|
|
52
|
+
'</div><span class="session-permission-status" aria-live="polite"></span>';
|
|
53
|
+
return control;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function modeLabel(mode) {
|
|
57
|
+
var labels = { default: "Ask", plan: "Plan", acceptEdits: "Auto-accept edits", dontAsk: "Don't ask",
|
|
58
|
+
bypassPermissions: "Skip permissions", auto: "Auto" };
|
|
59
|
+
return labels[mode] || mode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function permissionStatusText(state, pending, autoSupported, autoVisible) {
|
|
63
|
+
if (state.globalPermissionModeForced) return "Skip permissions forced by the server setting";
|
|
64
|
+
if (pending) return "Applying…";
|
|
65
|
+
if (state.permissionMode === "plan") return "Plan workflow";
|
|
66
|
+
if (state.permissionMode === "acceptEdits") return "Auto-accept edits workflow";
|
|
67
|
+
if (state.permissionMode === "dontAsk") return "Don't ask workflow";
|
|
68
|
+
if (autoVisible && !autoSupported) return "Auto unavailable for this Claude session";
|
|
69
|
+
if (state.permissionMode === "auto" && !state.effectivePermissionMode) return "Auto selected · waiting for runtime";
|
|
70
|
+
if (state.permissionMode === "auto" && state.effectivePermissionMode !== "auto") {
|
|
71
|
+
return "Auto unavailable at runtime · using " + modeLabel(state.effectivePermissionMode);
|
|
72
|
+
}
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function compactPermissionStatusText(state, pending, autoSupported, autoVisible, fullText) {
|
|
77
|
+
if (state.globalPermissionModeForced) return "Forced Skip";
|
|
78
|
+
if (pending) return "Applying…";
|
|
79
|
+
if (state.permissionMode === "plan") return "Plan";
|
|
80
|
+
if (state.permissionMode === "acceptEdits") return "Auto-accept";
|
|
81
|
+
if (state.permissionMode === "dontAsk") return "Don't ask";
|
|
82
|
+
if (autoVisible && !autoSupported) return "Auto unavailable";
|
|
83
|
+
if (state.permissionMode === "auto" && !state.effectivePermissionMode) return "Waiting for Auto";
|
|
84
|
+
if (state.permissionMode === "auto" && state.effectivePermissionMode !== "auto") return "Using " + modeLabel(state.effectivePermissionMode);
|
|
85
|
+
return fullText;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function renderPermissionControl(control, state) {
|
|
89
|
+
if (!control) return;
|
|
90
|
+
var projectSlug = state.projectSlug || "";
|
|
91
|
+
var sessionId = state.sessionId || null;
|
|
92
|
+
var pending = permissionPendingFor(projectSlug, sessionId);
|
|
93
|
+
var autoVisible = state.vendor === "claude";
|
|
94
|
+
var autoSupported = !!(state.permissionCapabilities && state.permissionCapabilities.auto === true);
|
|
95
|
+
var globallyForced = state.globalPermissionModeForced === true;
|
|
96
|
+
var connected = state.connected !== false && !!getWs() && getWs().readyState === 1;
|
|
97
|
+
control.classList.toggle("hidden", state.visible === false);
|
|
98
|
+
control.classList.toggle("permission-pending", !!pending);
|
|
99
|
+
control.classList.toggle("permission-auto-unsupported", autoVisible && !autoSupported);
|
|
100
|
+
control.classList.toggle("permission-globally-forced", globallyForced);
|
|
101
|
+
control.classList.toggle("permission-runtime-fallback", state.permissionMode === "auto" && !!state.effectivePermissionMode && state.effectivePermissionMode !== "auto");
|
|
102
|
+
var buttons = control.querySelectorAll("[data-permission-mode]");
|
|
103
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
104
|
+
var mode = buttons[i].dataset.permissionMode;
|
|
105
|
+
var unsupported = mode === "auto" && autoVisible && !autoSupported;
|
|
106
|
+
buttons[i].hidden = mode === "auto" && !autoVisible;
|
|
107
|
+
var selected = globallyForced ? mode === "bypassPermissions" : mode === state.permissionMode;
|
|
108
|
+
buttons[i].disabled = globallyForced || unsupported || !!pending || !connected || state.locked === true;
|
|
109
|
+
buttons[i].classList.toggle("active", selected);
|
|
110
|
+
buttons[i].setAttribute("aria-pressed", selected ? "true" : "false");
|
|
111
|
+
if (unsupported) buttons[i].title = "Auto permissions are unavailable for this Claude session.";
|
|
112
|
+
else buttons[i].removeAttribute("title");
|
|
113
|
+
}
|
|
114
|
+
var status = control.querySelector(".session-permission-status");
|
|
115
|
+
if (status) {
|
|
116
|
+
var fullStatus = permissionStatusText(state, pending, autoSupported, autoVisible);
|
|
117
|
+
var compact = control.classList.contains("split-pane-full-access");
|
|
118
|
+
status.textContent = compact ? compactPermissionStatusText(state, pending, autoSupported, autoVisible, fullStatus) : fullStatus;
|
|
119
|
+
status.classList.toggle("hidden", !status.textContent);
|
|
120
|
+
if (fullStatus) status.title = fullStatus;
|
|
121
|
+
else status.removeAttribute("title");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function bindPermissionControl(control, getState) {
|
|
126
|
+
if (!control || control.dataset.permissionBound === "true") return;
|
|
127
|
+
control.dataset.permissionBound = "true";
|
|
128
|
+
control.addEventListener("click", function(event) {
|
|
129
|
+
var button = event.target.closest("[data-permission-mode]");
|
|
130
|
+
if (!button || button.disabled) return;
|
|
131
|
+
var state = getState() || {};
|
|
132
|
+
var mode = button.dataset.permissionMode;
|
|
133
|
+
var autoSupported = !!(state.permissionCapabilities && state.permissionCapabilities.auto === true);
|
|
134
|
+
if (state.visible === false || state.locked === true || state.globalPermissionModeForced === true || state.connected === false || store.get('connected') === false) return;
|
|
135
|
+
if (!state.projectSlug || state.projectSlug !== store.get('currentSlug') || !state.sessionId) return;
|
|
136
|
+
if (mode === "auto" && (state.vendor !== "claude" || !autoSupported)) return;
|
|
137
|
+
sendPermissionMode(state.projectSlug, state.sessionId, mode);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sendPermissionMode(projectSlug, sessionId, mode) {
|
|
142
|
+
var ws = getWs();
|
|
143
|
+
if (store.get('skipPermsEnabled') === true) {
|
|
144
|
+
showToast("Skip permissions is forced by the server setting.", "error");
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (!projectSlug || !sessionId || !ws || ws.readyState !== 1) {
|
|
148
|
+
showToast("Permission mode could not be changed while disconnected.", "error");
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
var key = targetKey(projectSlug, sessionId);
|
|
152
|
+
if (permissionPendingFor(projectSlug, sessionId)) return false;
|
|
153
|
+
var requestId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + ":" + String(sessionId);
|
|
154
|
+
replacePending(key, { requestId: requestId, projectSlug: projectSlug, sessionId: sessionId, mode: mode });
|
|
155
|
+
requestTimers[requestId] = setTimeout(function() {
|
|
156
|
+
clearRequest(requestId, key);
|
|
157
|
+
showToast("Permission mode change timed out. The previous mode is still shown.", "error");
|
|
158
|
+
}, REQUEST_TIMEOUT_MS);
|
|
159
|
+
try {
|
|
160
|
+
var sendResult = ws.send(JSON.stringify({ type: "set_permission_mode", projectSlug: projectSlug, sessionId: sessionId, mode: mode, requestId: requestId }));
|
|
161
|
+
if (sendResult === false) throw new Error("The permission transport declined the request.");
|
|
162
|
+
} catch (error) {
|
|
163
|
+
clearRequest(requestId, key);
|
|
164
|
+
showToast("Permission mode could not be sent: " + (error.message || error), "error");
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function handlePermissionModeResult(msg) {
|
|
171
|
+
var key = targetKey(msg.projectSlug, msg.sessionId);
|
|
172
|
+
var pending = pendingMap()[key];
|
|
173
|
+
if (!pending || pending.requestId !== msg.requestId) return;
|
|
174
|
+
clearRequest(msg.requestId, key);
|
|
175
|
+
if (!msg.ok) {
|
|
176
|
+
showToast(msg.error || "Permission mode could not be changed.", "error");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (msg.projectSlug === store.get('currentSlug') && msg.sessionId === store.get('activeSessionId')) {
|
|
180
|
+
store.set({
|
|
181
|
+
currentMode: msg.mode || "default",
|
|
182
|
+
effectivePermissionMode: Object.prototype.hasOwnProperty.call(msg, 'effectivePermissionMode') ? msg.effectivePermissionMode : null,
|
|
183
|
+
permissionCapabilities: msg.permissionCapabilities || store.get('permissionCapabilities'),
|
|
184
|
+
mcpPermissionModeOverrides: msg.mcpPermissionModeOverrides || {},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|