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
|
@@ -5,6 +5,7 @@ var userPresence = require("./user-presence");
|
|
|
5
5
|
var emailAccounts = require("./email-accounts");
|
|
6
6
|
var { getCodexConfig } = require("./codex-defaults");
|
|
7
7
|
var yoke = require("./yoke");
|
|
8
|
+
var permissionModes = require("./session-permission-mode");
|
|
8
9
|
|
|
9
10
|
function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
|
|
10
11
|
try {
|
|
@@ -195,7 +196,8 @@ function attachConnection(ctx) {
|
|
|
195
196
|
// model. Always send it so reconnects cannot fall back to "Not installed."
|
|
196
197
|
var initialModelInfo = buildInitialModelInfo(sm, initialVendor, initialModel, initialModels, restoredActive ? restoredActive.localId : null);
|
|
197
198
|
sendTo(ws, initialModelInfo);
|
|
198
|
-
|
|
199
|
+
var initialPermissionState = permissionModes.clientState(restoredActive, sm.currentPermissionMode, sm.defaultVendor);
|
|
200
|
+
sendTo(ws, { type: "config_state", model: initialModelInfo.model, vendor: initialVendor, sessionId: restoredActive ? restoredActive.localId : null, mode: initialPermissionState.requestedPermissionMode, effectivePermissionMode: initialPermissionState.effectivePermissionMode, permissionCapabilities: initialPermissionState.permissionCapabilities, mcpPermissionModeOverrides: initialPermissionState.mcpPermissionModeOverrides, effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
199
201
|
sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
|
|
200
202
|
sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
|
|
201
203
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
@@ -270,7 +272,8 @@ function attachConnection(ctx) {
|
|
|
270
272
|
try { resolveSessionForView(active, ws); } catch (e) {}
|
|
271
273
|
}
|
|
272
274
|
var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
|
|
273
|
-
|
|
275
|
+
var activePermissionState = permissionModes.clientState(active, sm.currentPermissionMode, sm.defaultVendor);
|
|
276
|
+
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: activePermissionState.requestedPermissionMode, effectivePermissionMode: activePermissionState.effectivePermissionMode, permissionCapabilities: activePermissionState.permissionCapabilities, mcpPermissionModeOverrides: activePermissionState.mcpPermissionModeOverrides });
|
|
274
277
|
if (ctx.autonomousRun) sendTo(ws, { type: "autonomous_run_state", sessionId: active.localId, run: ctx.autonomousRun.project(active.autonomousRun) });
|
|
275
278
|
// Send per-session context sources
|
|
276
279
|
var sessionSources = loadContextSources(slug, active.localId);
|
|
@@ -1,86 +1,99 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Serialized, transactional owner for every per-session permission selection.
|
|
2
|
+
// Skip is Clay-managed and deliberately maps the SDK to default.
|
|
2
3
|
|
|
3
4
|
function createFullAccessService(ctx) {
|
|
4
5
|
var sm = ctx.sm;
|
|
5
6
|
var transitions = new Map();
|
|
6
|
-
|
|
7
7
|
function serialize(session, action) {
|
|
8
8
|
var prior = transitions.get(session.localId) || Promise.resolve();
|
|
9
9
|
var next = prior.catch(function () {}).then(action);
|
|
10
10
|
transitions.set(session.localId, next);
|
|
11
|
-
return next.finally(function () {
|
|
12
|
-
if (transitions.get(session.localId) === next) transitions.delete(session.localId);
|
|
13
|
-
});
|
|
11
|
+
return next.finally(function () { if (transitions.get(session.localId) === next) transitions.delete(session.localId); });
|
|
14
12
|
}
|
|
15
|
-
|
|
16
13
|
function snapshot(session) {
|
|
17
|
-
return {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
enabled: session.permissionMode === "bypassPermissions",
|
|
21
|
-
};
|
|
14
|
+
return { permissionMode: session.permissionMode || null, permissionModeBeforeFullAccess: session.permissionModeBeforeFullAccess || null,
|
|
15
|
+
effectivePermissionMode: session.effectivePermissionMode || null, permissionModeFallbackReason: session.permissionModeFallbackReason || null,
|
|
16
|
+
enabled: session.permissionMode === "bypassPermissions" };
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
function persist(session) {
|
|
19
|
+
sm.saveSessionFile(session);
|
|
20
|
+
sm.broadcastSessionList();
|
|
25
21
|
if (typeof sm.sendToSession === "function") sm.sendToSession(session, { type: "session_full_access_changed", id: session.localId,
|
|
26
22
|
enabled: session.permissionMode === "bypassPermissions", permissionMode: session.permissionMode });
|
|
27
23
|
}
|
|
28
|
-
|
|
29
|
-
async function applyNativeMode(session, enabled) {
|
|
24
|
+
async function setNative(session, mode) {
|
|
30
25
|
if (!session.queryInstance || typeof session.queryInstance.setPermissionMode !== "function") return;
|
|
31
|
-
|
|
32
|
-
await session.queryInstance.setPermissionMode(enabled ? "default" : session.permissionMode);
|
|
26
|
+
await session.queryInstance.setPermissionMode(mode === "bypassPermissions" ? "default" : mode);
|
|
33
27
|
}
|
|
34
|
-
|
|
35
|
-
async function applyEnabled(session, enabled) {
|
|
28
|
+
async function applyMode(session, mode) {
|
|
36
29
|
var previous = snapshot(session);
|
|
37
|
-
if (previous.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
session.permissionMode = session.permissionModeBeforeFullAccess || "default";
|
|
43
|
-
session.permissionModeBeforeFullAccess = null;
|
|
44
|
-
}
|
|
30
|
+
if (previous.permissionMode === mode) return previous;
|
|
31
|
+
// Clear stale confirmation before the setter. An init/status event may
|
|
32
|
+
// arrive while it is pending; do not clear that newer observation later.
|
|
33
|
+
session.effectivePermissionMode = null;
|
|
34
|
+
session.permissionModeFallbackReason = null;
|
|
45
35
|
try {
|
|
46
|
-
await
|
|
47
|
-
sm.saveSessionFile(session);
|
|
48
|
-
sm.broadcastSessionList();
|
|
49
|
-
notify(session);
|
|
50
|
-
return previous;
|
|
36
|
+
await setNative(session, mode);
|
|
51
37
|
} catch (error) {
|
|
38
|
+
session.effectivePermissionMode = previous.effectivePermissionMode;
|
|
39
|
+
session.permissionModeFallbackReason = previous.permissionModeFallbackReason;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
session.permissionMode = mode;
|
|
43
|
+
session.permissionModeBeforeFullAccess = mode === "bypassPermissions" ? (previous.permissionMode || sm.currentPermissionMode || "default") : null;
|
|
44
|
+
try { persist(session); return previous; }
|
|
45
|
+
catch (error) {
|
|
52
46
|
session.permissionMode = previous.permissionMode;
|
|
53
47
|
session.permissionModeBeforeFullAccess = previous.permissionModeBeforeFullAccess;
|
|
48
|
+
session.effectivePermissionMode = previous.effectivePermissionMode;
|
|
49
|
+
session.permissionModeFallbackReason = previous.permissionModeFallbackReason;
|
|
50
|
+
try { await setNative(session, previous.permissionMode || "default"); }
|
|
51
|
+
catch (rollbackError) {
|
|
52
|
+
session.effectivePermissionMode = null;
|
|
53
|
+
session.permissionModeFallbackReason = "Permission rollback failed: " + (rollbackError.message || rollbackError);
|
|
54
|
+
}
|
|
54
55
|
throw error;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
function setMode(session, mode) { return serialize(session, function () { return applyMode(session, mode); }); }
|
|
58
59
|
function setEnabled(session, enabled) {
|
|
59
|
-
return serialize(session, function () {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
async function applyRestore(session, prior) {
|
|
63
|
-
if (!prior) return;
|
|
64
|
-
var current = snapshot(session);
|
|
65
|
-
session.permissionMode = prior.permissionMode;
|
|
66
|
-
session.permissionModeBeforeFullAccess = prior.permissionModeBeforeFullAccess;
|
|
67
|
-
try {
|
|
68
|
-
await applyNativeMode(session, prior.enabled);
|
|
69
|
-
sm.saveSessionFile(session);
|
|
70
|
-
sm.broadcastSessionList();
|
|
71
|
-
notify(session);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
session.permissionMode = current.permissionMode;
|
|
74
|
-
session.permissionModeBeforeFullAccess = current.permissionModeBeforeFullAccess;
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
60
|
+
return serialize(session, function () {
|
|
61
|
+
return applyMode(session, enabled ? "bypassPermissions" : (session.permissionModeBeforeFullAccess || "default"));
|
|
62
|
+
});
|
|
77
63
|
}
|
|
78
|
-
|
|
79
64
|
function restore(session, prior) {
|
|
80
|
-
|
|
65
|
+
if (!prior) return Promise.resolve();
|
|
66
|
+
return serialize(session, async function () {
|
|
67
|
+
var current = snapshot(session);
|
|
68
|
+
// Runtime-effective state is an observation, not part of the selection
|
|
69
|
+
// snapshot. Clear it before the native setter and preserve any newer
|
|
70
|
+
// init/status event that arrives while the acknowledgement is pending.
|
|
71
|
+
session.effectivePermissionMode = null;
|
|
72
|
+
session.permissionModeFallbackReason = null;
|
|
73
|
+
try {
|
|
74
|
+
await setNative(session, prior.permissionMode || "default");
|
|
75
|
+
} catch (error) {
|
|
76
|
+
session.effectivePermissionMode = current.effectivePermissionMode;
|
|
77
|
+
session.permissionModeFallbackReason = current.permissionModeFallbackReason;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
session.permissionMode = prior.permissionMode;
|
|
81
|
+
session.permissionModeBeforeFullAccess = prior.permissionModeBeforeFullAccess;
|
|
82
|
+
try { persist(session); }
|
|
83
|
+
catch (error) {
|
|
84
|
+
session.permissionMode = current.permissionMode;
|
|
85
|
+
session.permissionModeBeforeFullAccess = current.permissionModeBeforeFullAccess;
|
|
86
|
+
session.effectivePermissionMode = null;
|
|
87
|
+
session.permissionModeFallbackReason = null;
|
|
88
|
+
try { await setNative(session, current.permissionMode || "default"); }
|
|
89
|
+
catch (rollbackError) {
|
|
90
|
+
session.effectivePermissionMode = null;
|
|
91
|
+
session.permissionModeFallbackReason = "Permission rollback failed: " + (rollbackError.message || rollbackError);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
81
96
|
}
|
|
82
|
-
|
|
83
|
-
return { restore: restore, setEnabled: setEnabled, snapshot: snapshot };
|
|
97
|
+
return { restore: restore, setEnabled: setEnabled, setMode: setMode, snapshot: snapshot };
|
|
84
98
|
}
|
|
85
|
-
|
|
86
99
|
module.exports = { createFullAccessService: createFullAccessService };
|
package/lib/project-sessions.js
CHANGED
|
@@ -88,6 +88,7 @@ function attachSessions(ctx) {
|
|
|
88
88
|
var slug = ctx.slug;
|
|
89
89
|
var isMate = ctx.isMate;
|
|
90
90
|
var osUsers = ctx.osUsers;
|
|
91
|
+
var dangerouslySkipPermissions = ctx.dangerouslySkipPermissions === true;
|
|
91
92
|
var currentVersion = ctx.currentVersion;
|
|
92
93
|
var sm = ctx.sm;
|
|
93
94
|
var sdk = ctx.sdk;
|
|
@@ -98,6 +99,7 @@ function attachSessions(ctx) {
|
|
|
98
99
|
var sendToAdmins = ctx.sendToAdmins;
|
|
99
100
|
var broadcastUpdateAvailable = ctx.broadcastUpdateAvailable || function () {};
|
|
100
101
|
var broadcastUpdateStarted = ctx.broadcastUpdateStarted || function () {};
|
|
102
|
+
var isDriverOperatedSession = ctx.isDriverOperatedSession || function () { return false; };
|
|
101
103
|
var sendToUpdateUser = ctx.sendToUpdateUser || function () {};
|
|
102
104
|
var sendToSession = ctx.sendToSession;
|
|
103
105
|
var sendToSessionOthers = ctx.sendToSessionOthers;
|
|
@@ -1181,13 +1183,62 @@ function attachSessions(ctx) {
|
|
|
1181
1183
|
return true;
|
|
1182
1184
|
}
|
|
1183
1185
|
|
|
1184
|
-
if (msg.type === "set_mcp_permission_mode_override"
|
|
1185
|
-
var
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1186
|
+
if (msg.type === "set_mcp_permission_mode_override") {
|
|
1187
|
+
var mcpRequestId = typeof msg.requestId === "string" && msg.requestId.length <= 128 ? msg.requestId : null;
|
|
1188
|
+
var mcpProjectSlug = typeof msg.projectSlug === "string" && msg.projectSlug.length <= 256 ? msg.projectSlug : null;
|
|
1189
|
+
var mcpSessionId = Number(msg.sessionId);
|
|
1190
|
+
var mcpServerName = typeof msg.serverName === "string" && msg.serverName.length > 0 && msg.serverName.length <= 256 ? msg.serverName : null;
|
|
1191
|
+
var mcpSession = Number.isSafeInteger(mcpSessionId) ? sm.sessions.get(mcpSessionId) : null;
|
|
1192
|
+
function mcpPermissionResult(ok, error, overrides) {
|
|
1193
|
+
sendTo(ws, {
|
|
1194
|
+
type: "mcp_permission_mode_result",
|
|
1195
|
+
ok: ok === true,
|
|
1196
|
+
requestId: mcpRequestId,
|
|
1197
|
+
projectSlug: mcpProjectSlug,
|
|
1198
|
+
sessionId: Number.isSafeInteger(mcpSessionId) ? mcpSessionId : null,
|
|
1199
|
+
serverName: mcpServerName,
|
|
1200
|
+
mode: ok ? mcpMode : null,
|
|
1201
|
+
mcpPermissionModeOverrides: ok ? overrides || {} : {},
|
|
1202
|
+
error: error || null,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
var mcpMode = msg.mode === "auto" || msg.mode === "default" ? msg.mode : null;
|
|
1206
|
+
var mcpModeValid = msg.mode == null || msg.mode === "" || msg.mode === "auto" || msg.mode === "default";
|
|
1207
|
+
if (mcpProjectSlug !== slug) {
|
|
1208
|
+
mcpPermissionResult(false, "The AI review request belongs to a different project.");
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
if (!mcpServerName || !mcpModeValid) {
|
|
1212
|
+
mcpPermissionResult(false, "The AI review request is invalid.");
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
var mcpRuntimeMode = mcpSession && (mcpSession.runtimeMode || mcpSession.mode || "gui");
|
|
1216
|
+
if (!mcpSession || !ownsSearchSession(ws, mcpSession) || mcpRuntimeMode !== "gui" || isDriverOperatedSession(mcpSession)) {
|
|
1217
|
+
mcpPermissionResult(false, "AI review controls are only available on an accessible Driver GUI session.");
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
var mcpApplicable = (mcpSession.effectivePermissionMode === "auto" || mcpSession.effectivePermissionMode === "bypassPermissions") &&
|
|
1221
|
+
mcpSession.permissionCapabilities && mcpSession.permissionCapabilities.mcpOverride === true && typeof sdk.setMcpPermissionModeOverride === "function";
|
|
1222
|
+
if (!mcpApplicable) {
|
|
1223
|
+
mcpPermissionResult(false, "AI review controls are not available for this session.");
|
|
1224
|
+
return true;
|
|
1190
1225
|
}
|
|
1226
|
+
(async function () {
|
|
1227
|
+
try {
|
|
1228
|
+
var result = await sdk.setMcpPermissionModeOverride(mcpSession, mcpServerName, mcpMode);
|
|
1229
|
+
if (!result || result.ok === false) {
|
|
1230
|
+
mcpPermissionResult(false, result && result.error || "Failed to change AI review policy.");
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
mcpSession.mcpPermissionModeOverrides = mcpSession.mcpPermissionModeOverrides || {};
|
|
1234
|
+
if (mcpMode) mcpSession.mcpPermissionModeOverrides[mcpServerName] = mcpMode;
|
|
1235
|
+
else delete mcpSession.mcpPermissionModeOverrides[mcpServerName];
|
|
1236
|
+
if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(mcpSession);
|
|
1237
|
+
mcpPermissionResult(true, null, mcpSession.mcpPermissionModeOverrides);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
mcpPermissionResult(false, "Failed to change AI review policy: " + (error.message || error));
|
|
1240
|
+
}
|
|
1241
|
+
})();
|
|
1191
1242
|
return true;
|
|
1192
1243
|
}
|
|
1193
1244
|
|
|
@@ -1204,6 +1255,10 @@ function attachSessions(ctx) {
|
|
|
1204
1255
|
" is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
|
|
1205
1256
|
} else {
|
|
1206
1257
|
vendorSession.vendor = msg.vendor;
|
|
1258
|
+
if (vendorSession.permissionMode === "auto" && msg.vendor !== "claude") {
|
|
1259
|
+
vendorSession.permissionMode = "default";
|
|
1260
|
+
vendorSession.effectivePermissionMode = null;
|
|
1261
|
+
}
|
|
1207
1262
|
if (vendorSession.vendor !== msg.vendor) vendorSession.model = "";
|
|
1208
1263
|
sm.saveSessionFile(vendorSession);
|
|
1209
1264
|
sm.broadcastSessionList();
|
|
@@ -1246,16 +1301,62 @@ function attachSessions(ctx) {
|
|
|
1246
1301
|
return true;
|
|
1247
1302
|
}
|
|
1248
1303
|
|
|
1249
|
-
if (msg.type === "set_permission_mode"
|
|
1250
|
-
|
|
1251
|
-
var
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1304
|
+
if (msg.type === "set_permission_mode") {
|
|
1305
|
+
var permissionModes = require("./session-permission-mode");
|
|
1306
|
+
var requestId = typeof msg.requestId === "string" && msg.requestId.length <= 128 ? msg.requestId : null;
|
|
1307
|
+
var requestedProjectSlug = typeof msg.projectSlug === "string" && msg.projectSlug.length <= 256 ? msg.projectSlug : null;
|
|
1308
|
+
var requestedSessionId = Number(msg.sessionId);
|
|
1309
|
+
var session = Number.isSafeInteger(requestedSessionId) ? sm.sessions.get(requestedSessionId) : null;
|
|
1310
|
+
function permissionResult(ok, error, includeState) {
|
|
1311
|
+
var state = includeState && session ? permissionModes.clientState(session, sm.currentPermissionMode, sm.defaultVendor) : null;
|
|
1312
|
+
sendTo(ws, {
|
|
1313
|
+
type: "permission_mode_result",
|
|
1314
|
+
ok: ok === true,
|
|
1315
|
+
requestId: requestId,
|
|
1316
|
+
projectSlug: requestedProjectSlug,
|
|
1317
|
+
sessionId: Number.isSafeInteger(requestedSessionId) ? requestedSessionId : null,
|
|
1318
|
+
mode: state ? state.requestedPermissionMode : null,
|
|
1319
|
+
effectivePermissionMode: state ? state.effectivePermissionMode : null,
|
|
1320
|
+
permissionCapabilities: state ? state.permissionCapabilities : null,
|
|
1321
|
+
mcpPermissionModeOverrides: state ? state.mcpPermissionModeOverrides : {},
|
|
1322
|
+
error: error || null,
|
|
1323
|
+
});
|
|
1257
1324
|
}
|
|
1258
|
-
|
|
1325
|
+
if (requestedProjectSlug !== slug) {
|
|
1326
|
+
permissionResult(false, "The permission request belongs to a different project.", false);
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
if (!session || !ownsSearchSession(ws, session) || isDriverOperatedSession(session)) {
|
|
1330
|
+
permissionResult(false, "Permissions can only be changed on an accessible Driver session.", false);
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
var permissionRuntimeMode = session.runtimeMode || session.mode || "gui";
|
|
1334
|
+
if (permissionRuntimeMode !== "gui") {
|
|
1335
|
+
permissionResult(false, "Permissions can only be changed on a GUI session.", true);
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
if (dangerouslySkipPermissions) {
|
|
1339
|
+
permissionResult(false, "Skip permissions is forced by the server setting.", true);
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
var valid = permissionModes.validate(session, msg.mode, sm.defaultVendor);
|
|
1343
|
+
if (!valid.ok) { permissionResult(false, valid.error, true); return true; }
|
|
1344
|
+
(async function() {
|
|
1345
|
+
try {
|
|
1346
|
+
if (session.permissionMode === "bypassPermissions" && valid.mode !== "bypassPermissions" && autonomousRun) {
|
|
1347
|
+
await autonomousRun.stopForPermissionChange(session);
|
|
1348
|
+
}
|
|
1349
|
+
await fullAccessService.setMode(session, valid.mode);
|
|
1350
|
+
var state = permissionModes.clientState(session, sm.currentPermissionMode, sm.defaultVendor);
|
|
1351
|
+
sm.sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: session.vendor || sm.defaultVendor || "claude", mode: state.requestedPermissionMode,
|
|
1352
|
+
effectivePermissionMode: state.effectivePermissionMode, permissionRequestId: requestId,
|
|
1353
|
+
permissionCapabilities: state.permissionCapabilities, mcpPermissionModeOverrides: state.mcpPermissionModeOverrides,
|
|
1354
|
+
effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1355
|
+
permissionResult(true, null, true);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
permissionResult(false, "Failed to change permission mode: " + (error.message || error), true);
|
|
1358
|
+
}
|
|
1359
|
+
})();
|
|
1259
1360
|
return true;
|
|
1260
1361
|
}
|
|
1261
1362
|
|
|
@@ -1263,12 +1364,12 @@ function attachSessions(ctx) {
|
|
|
1263
1364
|
var fullAccessSession = msg.id ? sm.sessions.get(Number(msg.id)) : getSessionForWs(ws);
|
|
1264
1365
|
var fullAccessEnabled = msg.enabled === true;
|
|
1265
1366
|
var fullAccessMode = fullAccessSession && (fullAccessSession.runtimeMode || fullAccessSession.mode || "gui");
|
|
1266
|
-
if (!fullAccessSession || fullAccessMode !== "gui") {
|
|
1367
|
+
if (!fullAccessSession || fullAccessMode !== "gui" || isDriverOperatedSession(fullAccessSession)) {
|
|
1267
1368
|
sendTo(ws, { type: "session_full_access_result", ok: false, id: msg.id || null, error: "Skip Permissions is only available for GUI sessions." });
|
|
1268
1369
|
return true;
|
|
1269
1370
|
}
|
|
1270
1371
|
if (usersModule.isMultiUser() && ws._clayUser &&
|
|
1271
|
-
!usersModule.canAccessSession(ws._clayUser.id, fullAccessSession,
|
|
1372
|
+
!usersModule.canAccessSession(ws._clayUser.id, fullAccessSession, getProjectAccess())) {
|
|
1272
1373
|
sendTo(ws, { type: "session_full_access_result", ok: false, id: fullAccessSession.localId, error: "You do not have access to this session." });
|
|
1273
1374
|
return true;
|
|
1274
1375
|
}
|
|
@@ -1603,11 +1704,13 @@ function attachSessions(ctx) {
|
|
|
1603
1704
|
|
|
1604
1705
|
// --- Plan approval: "allow_accept_edits" -- approve + switch to acceptEdits mode ---
|
|
1605
1706
|
if (decision === "allow_accept_edits") {
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1707
|
+
fullAccessService.setMode(session, "acceptEdits").then(function() {
|
|
1708
|
+
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
1709
|
+
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
1710
|
+
}).catch(function(error) {
|
|
1711
|
+
pending.resolve({ behavior: "deny", message: "Could not enter the accepted-edits workflow." });
|
|
1712
|
+
sendTo(ws, { type: "error", text: "Failed to change permission mode: " + (error.message || error) });
|
|
1713
|
+
});
|
|
1611
1714
|
return true;
|
|
1612
1715
|
}
|
|
1613
1716
|
|
|
@@ -1632,10 +1735,6 @@ function attachSessions(ctx) {
|
|
|
1632
1735
|
}
|
|
1633
1736
|
});
|
|
1634
1737
|
|
|
1635
|
-
// Update permission mode for the new session
|
|
1636
|
-
sm.currentPermissionMode = "acceptEdits";
|
|
1637
|
-
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1638
|
-
|
|
1639
1738
|
// Build prompt from plan content (sent from client) or plan file path
|
|
1640
1739
|
var clientPlanContent = msg.planContent || "";
|
|
1641
1740
|
var planPrompt;
|
|
@@ -1654,6 +1753,7 @@ function attachSessions(ctx) {
|
|
|
1654
1753
|
]).then(function () {
|
|
1655
1754
|
try {
|
|
1656
1755
|
var newSession = sm.createSession(null, ws);
|
|
1756
|
+
newSession.permissionMode = "acceptEdits";
|
|
1657
1757
|
// Send the plan as the first user message (with planContent for UI rendering)
|
|
1658
1758
|
var userMsg = { type: "user_message", text: planPrompt, planContent: clientPlanContent || null };
|
|
1659
1759
|
newSession.history.push(userMsg);
|
|
@@ -122,10 +122,13 @@ function attachUserMessage(ctx) {
|
|
|
122
122
|
function handleUserMessage(ws, msg, forcedSession, internal) {
|
|
123
123
|
if (msg.type === "pending_message_get" || msg.type === "pending_message_edit" || msg.type === "pending_message_cancel" || msg.type === "pending_message_reorder" || msg.type === "pending_message_resume") {
|
|
124
124
|
var queueSession = forcedSession || getSessionForWs(ws);
|
|
125
|
-
if (!queueSession || !pendingMessageQueue) return true;
|
|
126
125
|
var queueActor = ws && ws._clayUser;
|
|
127
126
|
var queueResult;
|
|
128
|
-
|
|
127
|
+
var requestedQueue = { ok: false, projectSlug: msg.projectSlug || slug, sessionId: msg.sessionId == null ? null : msg.sessionId, revision: 0, paused: false, items: [] };
|
|
128
|
+
if (!pendingMessageQueue) queueResult = Object.assign({}, requestedQueue, { error: "The message queue is unavailable." });
|
|
129
|
+
else if (!queueSession) queueResult = Object.assign({}, requestedQueue, { error: "The session is no longer available." });
|
|
130
|
+
else if (msg.projectSlug !== slug) queueResult = Object.assign({}, requestedQueue, { error: "Project changed" });
|
|
131
|
+
else if (msg.sessionId == null || String(msg.sessionId) !== String(queueSession.localId)) queueResult = Object.assign({}, requestedQueue, { error: "Session changed" });
|
|
129
132
|
else if (msg.type === "pending_message_resume") {
|
|
130
133
|
queueResult = pendingMessageQueue.resume(queueSession, queueActor, msg.revision, msg.sessionId);
|
|
131
134
|
if (queueResult.ok && !queueResult.paused) {
|
package/lib/project.js
CHANGED
|
@@ -1945,6 +1945,7 @@ function createProjectContext(opts) {
|
|
|
1945
1945
|
adapter: adapter,
|
|
1946
1946
|
fullAccessService: _fullAccess,
|
|
1947
1947
|
autonomousRun: _autonomousRun,
|
|
1948
|
+
isDriverOperatedSession: function (session) { return !isMate && _sessionPair.workerPermission.isDriverOperated(session); },
|
|
1948
1949
|
getProjectList: getProjectList,
|
|
1949
1950
|
getProjectCount: getProjectCount,
|
|
1950
1951
|
getScheduleCount: getScheduleCount,
|
package/lib/public/app.js
CHANGED
|
@@ -333,6 +333,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
333
333
|
cursorSharingHydrated: false,
|
|
334
334
|
pendingOutboundMessages: [],
|
|
335
335
|
pendingMessageQueue: { projectSlug: null, sessionId: null, revision: 0, paused: false, items: [], loading: false, error: "" },
|
|
336
|
+
pendingMessageQueueRecovery: false,
|
|
336
337
|
pendingMessageQueueRequests: {},
|
|
337
338
|
pendingMessageQueueEdit: null,
|
|
338
339
|
pendingMessageQueueDrag: null,
|
|
@@ -493,6 +494,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
493
494
|
// new-session menu doesn't flash the TUI-only entry.
|
|
494
495
|
claudeOpenMode: "gui",
|
|
495
496
|
currentMode: "default",
|
|
497
|
+
effectivePermissionMode: null,
|
|
498
|
+
permissionCapabilities: { auto: false, mcpOverride: false },
|
|
499
|
+
permissionModePendingByTarget: {},
|
|
500
|
+
mcpPermissionModePendingByTarget: {},
|
|
501
|
+
mcpPermissionModeOverrides: {},
|
|
496
502
|
currentEffort: "medium",
|
|
497
503
|
currentBetas: [],
|
|
498
504
|
currentThinking: "adaptive",
|
|
@@ -686,6 +686,26 @@
|
|
|
686
686
|
font-size: 12px;
|
|
687
687
|
color: var(--text-muted);
|
|
688
688
|
}
|
|
689
|
+
.mcp-server-perm-mode {
|
|
690
|
+
max-width: 118px;
|
|
691
|
+
padding: 5px 24px 5px 8px;
|
|
692
|
+
border: 1px solid var(--border);
|
|
693
|
+
border-radius: 6px;
|
|
694
|
+
background: var(--bg);
|
|
695
|
+
color: var(--text);
|
|
696
|
+
font-size: 12px;
|
|
697
|
+
}
|
|
698
|
+
.mcp-server-perm-mode:disabled {
|
|
699
|
+
cursor: wait;
|
|
700
|
+
opacity: 0.55;
|
|
701
|
+
}
|
|
702
|
+
.mcp-server-perm-reason {
|
|
703
|
+
max-width: 180px;
|
|
704
|
+
color: var(--text-muted);
|
|
705
|
+
font-size: 11px;
|
|
706
|
+
line-height: 1.3;
|
|
707
|
+
text-align: right;
|
|
708
|
+
}
|
|
689
709
|
|
|
690
710
|
/* Beta toggle row inside settings card */
|
|
691
711
|
.ps-beta-toggle-row,
|
|
@@ -508,6 +508,33 @@ pre:hover .code-copy-btn { opacity: 1; }
|
|
|
508
508
|
.clay-file-link-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
509
509
|
.clay-file-link-notice { margin-top: 5px; color: var(--text-secondary); font-size: 12px; }
|
|
510
510
|
|
|
511
|
+
.hosted-link-chip, .github-link-chip {
|
|
512
|
+
display: inline-flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
gap: 5px;
|
|
515
|
+
max-width: 100%;
|
|
516
|
+
padding: 3px 8px;
|
|
517
|
+
border: 1px solid color-mix(in srgb, var(--text) 16%, var(--border));
|
|
518
|
+
border-radius: 999px;
|
|
519
|
+
background: color-mix(in srgb, var(--text) 8%, var(--bg));
|
|
520
|
+
color: var(--text);
|
|
521
|
+
font-size: 0.92em;
|
|
522
|
+
line-height: 1.35;
|
|
523
|
+
vertical-align: baseline;
|
|
524
|
+
text-decoration: none !important;
|
|
525
|
+
transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
|
|
526
|
+
}
|
|
527
|
+
.hosted-link-chip { color: var(--text); }
|
|
528
|
+
.md-content .github-link-chip { color: var(--text); }
|
|
529
|
+
.md-content .gitlab-link-chip { color: var(--text); }
|
|
530
|
+
.hosted-link-chip:hover, .github-link-chip:hover { background: color-mix(in srgb, var(--text) 14%, var(--bg-alt)); border-color: color-mix(in srgb, var(--text) 30%, var(--border)); }
|
|
531
|
+
.hosted-link-chip:active, .github-link-chip:active { transform: translateY(1px); }
|
|
532
|
+
.hosted-link-chip:focus-visible, .github-link-chip:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
|
|
533
|
+
.github-link-icon { display: inline-flex; width: 15px; height: 15px; flex: 0 0 15px; }
|
|
534
|
+
.github-link-icon .lucide { width: 15px; height: 15px; }
|
|
535
|
+
.github-link-main { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
536
|
+
.github-link-kind { min-width: 0; max-width: 65%; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); font-size: 0.88em; }
|
|
537
|
+
|
|
511
538
|
.md-content ul, .md-content ol {
|
|
512
539
|
padding-left: 24px;
|
|
513
540
|
margin: 10px 0;
|
|
@@ -519,6 +519,66 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
|
|
|
519
519
|
.popover-copy.copied { color: var(--success); border-color: var(--success); }
|
|
520
520
|
|
|
521
521
|
/* Skip permissions pill */
|
|
522
|
+
.session-permission-control {
|
|
523
|
+
display: inline-flex;
|
|
524
|
+
align-items: center;
|
|
525
|
+
flex: 0 1 auto;
|
|
526
|
+
flex-wrap: wrap;
|
|
527
|
+
gap: 7px;
|
|
528
|
+
max-width: 100%;
|
|
529
|
+
min-width: 0;
|
|
530
|
+
}
|
|
531
|
+
.session-permission-control.hidden { display: none; }
|
|
532
|
+
.session-permission-segmented {
|
|
533
|
+
display: inline-flex;
|
|
534
|
+
align-items: center;
|
|
535
|
+
flex: 0 0 auto;
|
|
536
|
+
gap: 2px;
|
|
537
|
+
padding: 2px;
|
|
538
|
+
border: 1px solid var(--border, rgba(255,255,255,.18));
|
|
539
|
+
border-radius: 999px;
|
|
540
|
+
background: var(--surface-raised, rgba(0,0,0,.18));
|
|
541
|
+
}
|
|
542
|
+
.session-permission-segmented button {
|
|
543
|
+
flex: 0 0 auto;
|
|
544
|
+
border: 0;
|
|
545
|
+
border-radius: 999px;
|
|
546
|
+
padding: 3px 8px;
|
|
547
|
+
color: var(--text-muted, inherit);
|
|
548
|
+
background: transparent;
|
|
549
|
+
font: inherit;
|
|
550
|
+
font-size: 11px;
|
|
551
|
+
white-space: nowrap;
|
|
552
|
+
cursor: pointer;
|
|
553
|
+
}
|
|
554
|
+
.session-permission-segmented button.active { background: var(--accent, #6f8cff); color: #fff; }
|
|
555
|
+
.session-permission-segmented button:disabled { cursor: not-allowed; opacity: .5; }
|
|
556
|
+
.session-permission-control.permission-pending .session-permission-segmented button:disabled { cursor: wait; }
|
|
557
|
+
.session-permission-status {
|
|
558
|
+
flex: 1 1 118px;
|
|
559
|
+
min-width: 0;
|
|
560
|
+
max-width: 190px;
|
|
561
|
+
overflow-wrap: anywhere;
|
|
562
|
+
white-space: normal;
|
|
563
|
+
color: var(--text-dimmer, var(--text-muted));
|
|
564
|
+
font-size: 10px;
|
|
565
|
+
line-height: 1.2;
|
|
566
|
+
}
|
|
567
|
+
.session-permission-status.hidden { display: none; }
|
|
568
|
+
.permission-runtime-fallback .session-permission-status { color: var(--warning, #d49a55); }
|
|
569
|
+
.permission-globally-forced .session-permission-status { color: var(--warning, #d49a55); }
|
|
570
|
+
.permission-label-short { display: none; }
|
|
571
|
+
@media (max-width: 760px) {
|
|
572
|
+
.permission-label-long { display: none; }
|
|
573
|
+
.permission-label-short { display: inline; }
|
|
574
|
+
.session-permission-control { column-gap: 5px; row-gap: 2px; }
|
|
575
|
+
.session-permission-segmented button { padding-inline: 7px; }
|
|
576
|
+
.session-permission-status {
|
|
577
|
+
flex-basis: 96px;
|
|
578
|
+
max-width: 170px;
|
|
579
|
+
font-size: 9px;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
522
582
|
#skip-perms-pill .lucide { width: 12px; height: 12px; flex-shrink: 0; }
|
|
523
583
|
|
|
524
584
|
/* --- Confirm modal --- */
|
package/lib/public/css/pane.css
CHANGED
|
@@ -370,13 +370,31 @@ body.pane-mode #input-area {
|
|
|
370
370
|
tie and keep the pill at its 22px title-bar size inside the 24px pane
|
|
371
371
|
header. Scope under .split-pane-header to outrank them. */
|
|
372
372
|
.split-pane-header .split-pane-full-access {
|
|
373
|
-
height:
|
|
373
|
+
height: auto;
|
|
374
374
|
align-self: center;
|
|
375
375
|
margin-left: 0;
|
|
376
|
-
padding: 0
|
|
377
|
-
gap:
|
|
376
|
+
padding: 0;
|
|
377
|
+
gap: 4px;
|
|
378
378
|
font-size: 8px;
|
|
379
379
|
letter-spacing: 0.02em;
|
|
380
|
+
flex-wrap: nowrap;
|
|
381
|
+
flex: 0 0 auto;
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.split-pane-header .session-permission-segmented button {
|
|
386
|
+
padding: 1px 5px;
|
|
387
|
+
font-size: 8px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.split-pane-header .session-permission-status {
|
|
391
|
+
flex: 0 0 auto;
|
|
392
|
+
min-width: 40px;
|
|
393
|
+
max-width: 72px;
|
|
394
|
+
overflow: hidden;
|
|
395
|
+
text-overflow: ellipsis;
|
|
396
|
+
white-space: nowrap;
|
|
397
|
+
font-size: 8px;
|
|
380
398
|
}
|
|
381
399
|
|
|
382
400
|
.split-pane-header .session-full-access-track {
|
|
@@ -395,7 +413,6 @@ body.pane-mode #input-area {
|
|
|
395
413
|
|
|
396
414
|
@media (max-width: 900px) {
|
|
397
415
|
.split-pane-header .session-full-access-label { display: none; }
|
|
398
|
-
.split-pane-header .split-pane-full-access { padding-left: 3px; }
|
|
399
416
|
}
|
|
400
417
|
|
|
401
418
|
.split-pane-close {
|
|
@@ -167,6 +167,11 @@ button.pending-message-drag:active { cursor: grabbing; }
|
|
|
167
167
|
.pending-message-banner-error { color: var(--error); font-size: 10px; line-height: 1.35; }
|
|
168
168
|
.pending-message-error { margin-top: 4px; }
|
|
169
169
|
.pending-message-banner-error { padding: 0 8px 6px; }
|
|
170
|
+
.pending-message-restoring { padding: 0 8px 6px; color: var(--text-dimmer); font-size: 10px; line-height: 1.35; }
|
|
171
|
+
|
|
172
|
+
.pending-message-drag:disabled,
|
|
173
|
+
.pending-message-actions button:disabled,
|
|
174
|
+
.pending-message-resume:disabled { cursor: not-allowed; opacity: .48; }
|
|
170
175
|
|
|
171
176
|
.pending-message-drag:focus-visible,
|
|
172
177
|
.pending-message-actions button:focus-visible,
|