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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/project-connection.js +5 -2
  2. package/lib/project-full-access.js +69 -56
  3. package/lib/project-sessions.js +126 -26
  4. package/lib/project-user-message.js +5 -2
  5. package/lib/project.js +1 -0
  6. package/lib/public/app.js +6 -0
  7. package/lib/public/css/filebrowser.css +20 -0
  8. package/lib/public/css/messages.css +27 -0
  9. package/lib/public/css/overlays.css +60 -0
  10. package/lib/public/css/pane.css +21 -4
  11. package/lib/public/css/pending-message-queue.css +5 -0
  12. package/lib/public/index.html +8 -3
  13. package/lib/public/modules/app-connection.js +4 -0
  14. package/lib/public/modules/app-header.js +18 -25
  15. package/lib/public/modules/app-messages.js +17 -7
  16. package/lib/public/modules/app-panels.js +22 -11
  17. package/lib/public/modules/clay-file-links.js +7 -0
  18. package/lib/public/modules/clay-log-links.js +4 -0
  19. package/lib/public/modules/github-links.js +115 -0
  20. package/lib/public/modules/markdown.js +9 -2
  21. package/lib/public/modules/mcp-ui.js +154 -17
  22. package/lib/public/modules/pane-bridge.js +2 -0
  23. package/lib/public/modules/pane-file-bridge.js +69 -0
  24. package/lib/public/modules/pending-message-queue-model.js +6 -0
  25. package/lib/public/modules/pending-message-queue-recovery.js +74 -0
  26. package/lib/public/modules/pending-message-queue.js +70 -67
  27. package/lib/public/modules/permission-control.js +187 -0
  28. package/lib/public/modules/split-view.js +29 -29
  29. package/lib/sdk-bridge.js +95 -40
  30. package/lib/session-permission-mode.js +42 -0
  31. package/lib/sessions.js +14 -1
  32. package/lib/ws-schema.js +2 -0
  33. package/lib/yoke/adapters/claude-worker-policy.js +67 -0
  34. package/lib/yoke/adapters/claude-worker.js +47 -7
  35. package/lib/yoke/adapters/claude.js +114 -6
  36. package/package.json +1 -1
@@ -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
- sendTo(ws, { type: "config_state", model: initialModelInfo.model, vendor: initialVendor, sessionId: restoredActive ? restoredActive.localId : null, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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
- 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: active.permissionMode || null });
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
- // Shared, transactional control for Clay-managed per-session Skip Permissions.
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
- permissionMode: session.permissionMode || null,
19
- permissionModeBeforeFullAccess: session.permissionModeBeforeFullAccess || null,
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
- function notify(session) {
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
- // Clay's common permission callback remains the approval authority.
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.enabled === !!enabled) return previous;
38
- if (enabled) {
39
- session.permissionModeBeforeFullAccess = session.permissionMode || sm.currentPermissionMode || "default";
40
- session.permissionMode = "bypassPermissions";
41
- } else {
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 applyNativeMode(session, !!enabled);
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 () { return applyEnabled(session, enabled); });
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
- return serialize(session, function () { return applyRestore(session, prior); });
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 };
@@ -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" && msg.serverName) {
1185
- var session = getSessionForWs(ws);
1186
- if (session && sdk.setMcpPermissionModeOverride) {
1187
- // mode: "default" | "auto" | null (null clears the override)
1188
- var mcpMode = (msg.mode === "auto" || msg.mode === "default") ? msg.mode : null;
1189
- sdk.setMcpPermissionModeOverride(session, msg.serverName, mcpMode);
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" && msg.mode) {
1250
- sm.currentPermissionMode = msg.mode;
1251
- var session = getSessionForWs(ws);
1252
- if (session) {
1253
- session.permissionMode = msg.mode;
1254
- sm.saveSessionFile(session);
1255
- sm.broadcastSessionList();
1256
- sdk.setPermissionMode(session, msg.mode);
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
- 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 });
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, { visibility: "public" })) {
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
- sdk.setPermissionMode(session, "acceptEdits");
1607
- sm.currentPermissionMode = "acceptEdits";
1608
- 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 });
1609
- pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1610
- sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
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
- if (msg.projectSlug !== slug) queueResult = { ok: false, error: "Project changed", sessionId: queueSession.localId, projectSlug: slug };
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 --- */
@@ -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: 15px;
373
+ height: auto;
374
374
  align-self: center;
375
375
  margin-left: 0;
376
- padding: 0 3px 0 7px;
377
- gap: 5px;
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,