clay-server 4.1.0 → 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/daemon-project-access.js +14 -0
- package/lib/daemon.js +7 -0
- package/lib/project-connection.js +5 -2
- package/lib/project-full-access.js +69 -56
- package/lib/project-issue-launch.js +3 -1
- package/lib/project-sessions.js +158 -32
- package/lib/project-user-message.js +5 -2
- package/lib/project.js +5 -0
- package/lib/public/app.js +7 -0
- package/lib/public/css/command-palette.css +51 -33
- package/lib/public/css/filebrowser.css +20 -0
- package/lib/public/css/issues.css +4 -4
- 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 +18 -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 +24 -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-issue-links.js +1 -0
- package/lib/public/modules/clay-log-links.js +5 -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/project-session-visibility-settings.js +138 -0
- package/lib/public/modules/project-settings.js +5 -0
- package/lib/public/modules/sidebar-projects.js +14 -3
- package/lib/public/modules/split-view.js +29 -29
- package/lib/sdk-bridge.js +95 -40
- package/lib/server.js +8 -1
- package/lib/session-permission-mode.js +42 -0
- package/lib/session-visibility.js +62 -1
- package/lib/sessions.js +16 -1
- package/lib/ws-schema.js +4 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
|
|
5
|
+
var requestSequence = 0;
|
|
6
|
+
var initialized = false;
|
|
7
|
+
|
|
8
|
+
function selectElement() {
|
|
9
|
+
return document.getElementById("ps-session-visibility-default");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pendingState() {
|
|
13
|
+
return (store.get('projectSessionVisibilityState') || {}).pending || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function activeState() {
|
|
17
|
+
return (store.get('projectSessionVisibilityState') || {}).active || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function setVisibilityState(active, pending) {
|
|
21
|
+
store.set({ projectSessionVisibilityState: { active: active || null, pending: pending || null } });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setSelectState(select, value, disabled) {
|
|
25
|
+
if (!select) return;
|
|
26
|
+
select.value = value;
|
|
27
|
+
select.disabled = !!disabled;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isCurrentProject(active) {
|
|
31
|
+
return !!(active && active.slug && active.slug === store.get('currentSlug'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isReadOnly(active) {
|
|
35
|
+
return !active || !active.authorized || active.isWorktree || !isCurrentProject(active);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderActiveState() {
|
|
39
|
+
var active = activeState();
|
|
40
|
+
var pending = pendingState();
|
|
41
|
+
var select = selectElement();
|
|
42
|
+
var inheritedHint = document.getElementById("ps-session-visibility-default-inherited-hint");
|
|
43
|
+
var nonCurrentHint = document.getElementById("ps-session-visibility-default-non-current-hint");
|
|
44
|
+
if (inheritedHint) inheritedHint.classList.toggle("hidden", !(active && active.isWorktree));
|
|
45
|
+
if (nonCurrentHint) nonCurrentHint.classList.toggle("hidden", !(active && active.authorized && !active.isWorktree && !isCurrentProject(active)));
|
|
46
|
+
if (!select || !active) return;
|
|
47
|
+
setSelectState(select, active.effectiveValue, isReadOnly(active) || !!(pending && pending.slug === active.slug));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isAuthorized(project) {
|
|
51
|
+
var users = store.get('cachedAllUsers') || [];
|
|
52
|
+
var userId = store.get('myUserId');
|
|
53
|
+
var currentUser = null;
|
|
54
|
+
for (var i = 0; i < users.length; i++) {
|
|
55
|
+
if (users[i].id === userId) { currentUser = users[i]; break; }
|
|
56
|
+
}
|
|
57
|
+
return !!(store.get('isMultiUserMode') && project &&
|
|
58
|
+
((project.projectOwnerId && project.projectOwnerId === userId) || (currentUser && currentUser.role === "admin")) &&
|
|
59
|
+
(!store.get('permissions') || store.get('permissions').projectSettings !== false));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function initProjectSessionVisibilitySettings() {
|
|
63
|
+
var select = selectElement();
|
|
64
|
+
if (!select || initialized) return;
|
|
65
|
+
initialized = true;
|
|
66
|
+
select.addEventListener("change", function () {
|
|
67
|
+
var ws = getWs();
|
|
68
|
+
var active = activeState();
|
|
69
|
+
var slug = active && active.slug || "";
|
|
70
|
+
var prior = active && active.effectiveValue || "private";
|
|
71
|
+
if (!slug || !ws || ws.readyState !== 1 || pendingState() || isReadOnly(active)) {
|
|
72
|
+
setSelectState(select, prior, isReadOnly(active) || !!pendingState());
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
requestSequence += 1;
|
|
76
|
+
var requestId = "project-session-visibility-" + Date.now() + "-" + requestSequence;
|
|
77
|
+
setVisibilityState(active, { requestId: requestId, slug: slug, requested: select.value, previous: prior });
|
|
78
|
+
setSelectState(select, select.value, true);
|
|
79
|
+
ws.send(JSON.stringify({ type: "set_project_session_visibility_default", slug: slug, visibility: select.value, requestId: requestId }));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
store.subscribe(function (state, previous) {
|
|
83
|
+
if (state.currentSlug !== previous.currentSlug) renderActiveState();
|
|
84
|
+
if (state.connected || previous.connected !== true) return;
|
|
85
|
+
var pending = pendingState();
|
|
86
|
+
if (!pending) return;
|
|
87
|
+
var current = selectElement();
|
|
88
|
+
var active = activeState();
|
|
89
|
+
if (current && active && active.slug === pending.slug) setSelectState(current, pending.previous, isReadOnly(active));
|
|
90
|
+
setVisibilityState(active, null);
|
|
91
|
+
renderActiveState();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function populateProjectSessionVisibilitySettings(slug, project) {
|
|
96
|
+
var field = document.getElementById("ps-session-visibility-default-field");
|
|
97
|
+
var select = selectElement();
|
|
98
|
+
if (field) field.style.display = isAuthorized(project) ? "" : "none";
|
|
99
|
+
if (!select) return;
|
|
100
|
+
var value = project && project.sessionVisibilityDefault === "shared" ? "shared" : "private";
|
|
101
|
+
var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized: isAuthorized(project) };
|
|
102
|
+
var pending = pendingState();
|
|
103
|
+
setVisibilityState(active, pending);
|
|
104
|
+
renderActiveState();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function refreshProjectSessionVisibilitySettings(projects) {
|
|
108
|
+
var select = selectElement();
|
|
109
|
+
if (!select) return;
|
|
110
|
+
var active = activeState();
|
|
111
|
+
var slug = active && active.slug || "";
|
|
112
|
+
var source = Array.isArray(projects) ? projects : [];
|
|
113
|
+
for (var i = 0; i < source.length; i++) {
|
|
114
|
+
if (source[i] && source[i].slug === slug) {
|
|
115
|
+
populateProjectSessionVisibilitySettings(slug, source[i]);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function handleProjectSessionVisibilityDefault(msg) {
|
|
122
|
+
var pending = pendingState();
|
|
123
|
+
if (!pending || msg.requestId !== pending.requestId || msg.slug !== pending.slug) return;
|
|
124
|
+
var active = activeState();
|
|
125
|
+
setVisibilityState(active, null);
|
|
126
|
+
var select = selectElement();
|
|
127
|
+
if (!select || !active || active.slug !== msg.slug) return;
|
|
128
|
+
if (!msg.ok) {
|
|
129
|
+
active = Object.assign({}, active, { effectiveValue: pending.previous });
|
|
130
|
+
setVisibilityState(active, null);
|
|
131
|
+
setSelectState(select, pending.previous, isReadOnly(active));
|
|
132
|
+
showToast(msg.error || "Failed to update new session visibility", "error");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
active = Object.assign({}, active, { effectiveValue: msg.visibility === "shared" ? "shared" : "private" });
|
|
136
|
+
setVisibilityState(active, null);
|
|
137
|
+
setSelectState(select, active.effectiveValue, isReadOnly(active));
|
|
138
|
+
}
|
|
@@ -4,6 +4,7 @@ import { showToast } from './utils.js';
|
|
|
4
4
|
import { parseEmojis } from './markdown.js';
|
|
5
5
|
import { closeFileViewer } from './filebrowser.js';
|
|
6
6
|
import { renderModelList, renderModeList, renderEffortBar, renderThinkingBar, renderBetaCard } from './settings-defaults.js';
|
|
7
|
+
import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
|
|
7
8
|
|
|
8
9
|
var ctx = null;
|
|
9
10
|
var panelEl = null;
|
|
@@ -162,6 +163,8 @@ export function initProjectSettings(appCtx, emojiCategories) {
|
|
|
162
163
|
hideTransferForm();
|
|
163
164
|
});
|
|
164
165
|
}
|
|
166
|
+
|
|
167
|
+
initProjectSessionVisibilitySettings();
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
// ===== Open / Close =====
|
|
@@ -246,6 +249,8 @@ function populateProfile() {
|
|
|
246
249
|
// Icon
|
|
247
250
|
updateIconPreview(currentProject ? currentProject.icon : null);
|
|
248
251
|
|
|
252
|
+
populateProjectSessionVisibilitySettings(currentSlug, currentProject);
|
|
253
|
+
|
|
249
254
|
// Owner (only in multi-user mode)
|
|
250
255
|
var ownerField = document.getElementById("ps-owner-field");
|
|
251
256
|
if (ownerField) {
|