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
|
@@ -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
|
+
}
|
|
@@ -8,13 +8,15 @@ import { detachTuiView } from './session-tui-view.js';
|
|
|
8
8
|
import { formatTokens } from './app-panels.js';
|
|
9
9
|
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
10
10
|
import { groupedSessionIds, findSplitGroup, isConfiguredWorker } from './split-group-helpers.js';
|
|
11
|
-
import { showConfirm } from './app-misc.js';
|
|
12
11
|
import { syncPairChrome } from './split-pair-ui.js';
|
|
13
12
|
import { presentMarkdownEdit } from './filebrowser.js';
|
|
14
13
|
import { autoStartLoginIfNeeded } from './vendor-login.js';
|
|
15
14
|
import { isCurrentProjectSessionReady } from './split-session-boundary.js';
|
|
16
15
|
import { splitPanesMatchProject, hasCurrentSplitGroup, reconcileRestoredSplit } from './project-activation.js';
|
|
17
16
|
import { handlePaneIssueMessage } from './pane-issue-bridge.js';
|
|
17
|
+
import { handlePaneFileMessage, revealFilesPanel } from './pane-file-bridge.js';
|
|
18
|
+
import { openFile } from './filebrowser.js';
|
|
19
|
+
import { bindPermissionControl, createPermissionControl, renderPermissionControl } from './permission-control.js';
|
|
18
20
|
|
|
19
21
|
var host = null;
|
|
20
22
|
var nativeApp = null;
|
|
@@ -161,25 +163,28 @@ export function syncPaneTitles() {
|
|
|
161
163
|
syncPairChrome(host, split);
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
function
|
|
165
|
-
if (!button) return;
|
|
166
|
+
function panePermissionControlState(session) {
|
|
166
167
|
var mode = session && (session.runtimeMode || session.mode || "gui");
|
|
167
168
|
var worker = !!session && isConfiguredWorker(store.get('splitGroups'), session.id);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
return {
|
|
170
|
+
projectSlug: store.get('currentSlug'),
|
|
171
|
+
sessionId: session && session.id,
|
|
172
|
+
vendor: session && session.vendor || store.get('currentVendor') || "claude",
|
|
173
|
+
permissionMode: session && session.permissionMode || "default",
|
|
174
|
+
effectivePermissionMode: session && session.effectivePermissionMode || null,
|
|
175
|
+
permissionCapabilities: session && session.permissionCapabilities || { auto: false, mcpOverride: false },
|
|
176
|
+
connected: store.get('connected'),
|
|
177
|
+
globalPermissionModeForced: store.get('skipPermsEnabled') === true,
|
|
178
|
+
visible: !!session && !worker && mode === "gui",
|
|
179
|
+
locked: worker,
|
|
180
|
+
};
|
|
176
181
|
}
|
|
177
182
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
function updatePaneFullAccessButton(button, session) {
|
|
184
|
+
if (!button) return;
|
|
185
|
+
// Permission policy belongs to the Driver. A configured Worker inherits it
|
|
186
|
+
// server-side and must not present a second, misleading control.
|
|
187
|
+
renderPermissionControl(button, panePermissionControlState(session));
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
function createPane(pane, index) {
|
|
@@ -208,6 +213,8 @@ function createPane(pane, index) {
|
|
|
208
213
|
|
|
209
214
|
var frame = document.createElement("iframe");
|
|
210
215
|
frame.className = "split-pane-frame";
|
|
216
|
+
frame.dataset.projectSlug = pane.slug;
|
|
217
|
+
frame.dataset.sessionId = String(pane.sessionId);
|
|
211
218
|
frame.src = paneUrl(pane);
|
|
212
219
|
frame.title = pane.title;
|
|
213
220
|
|
|
@@ -225,21 +232,11 @@ function createPane(pane, index) {
|
|
|
225
232
|
});
|
|
226
233
|
header.appendChild(ctxChip);
|
|
227
234
|
|
|
228
|
-
var fullAccess =
|
|
229
|
-
fullAccess.type = "button";
|
|
230
|
-
fullAccess.className = "session-full-access split-pane-full-access hidden";
|
|
231
|
-
fullAccess.setAttribute("role", "switch");
|
|
232
|
-
fullAccess.innerHTML = '<span class="session-full-access-label">Skip Permissions</span><span class="session-full-access-track" aria-hidden="true"><span></span></span>';
|
|
235
|
+
var fullAccess = createPermissionControl("split-pane-full-access hidden", "Driver session permission mode");
|
|
233
236
|
updatePaneFullAccessButton(fullAccess, session);
|
|
234
|
-
fullAccess
|
|
237
|
+
bindPermissionControl(fullAccess, function() {
|
|
235
238
|
var current = sessionById(pane.sessionId);
|
|
236
|
-
|
|
237
|
-
setPaneFullAccess(pane.sessionId, false);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
showConfirm("Skip permission prompts for this session? Tool requests will be approved automatically.", function () {
|
|
241
|
-
setPaneFullAccess(pane.sessionId, true);
|
|
242
|
-
}, "Skip Permissions", true);
|
|
239
|
+
return panePermissionControlState(current);
|
|
243
240
|
});
|
|
244
241
|
header.appendChild(fullAccess);
|
|
245
242
|
// Permission state belongs to the session identity on the left. Keep it
|
|
@@ -293,6 +290,7 @@ function handlePaneMessage(event) {
|
|
|
293
290
|
return;
|
|
294
291
|
}
|
|
295
292
|
if (handlePaneIssueMessage(event, host)) return;
|
|
293
|
+
if (handlePaneFileMessage(event, host, openFile, revealFilesPanel)) return;
|
|
296
294
|
if (msg.type !== "clay-pane-context") return;
|
|
297
295
|
var frames = host.querySelectorAll(".split-pane-frame");
|
|
298
296
|
for (var i = 0; i < frames.length; i++) {
|
|
@@ -527,6 +525,8 @@ export function initSplitView() {
|
|
|
527
525
|
document.addEventListener("click", handleSidebarSessionClick, true);
|
|
528
526
|
store.subscribe(function (state, prev) {
|
|
529
527
|
if (state.splitPanes !== prev.splitPanes) renderSplit(state.splitPanes);
|
|
528
|
+
if (state.permissionModePendingByTarget !== prev.permissionModePendingByTarget || state.connected !== prev.connected ||
|
|
529
|
+
state.skipPermsEnabled !== prev.skipPermsEnabled) syncPaneTitles();
|
|
530
530
|
// Switching to a session outside the open split (new_session, palette,
|
|
531
531
|
// notification click) closes the split UI; the group itself persists.
|
|
532
532
|
if (state.activeSessionId !== prev.activeSessionId && state.splitPanes && state.splitPanes.panes) {
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -788,61 +788,61 @@ function createSDKBridge(opts) {
|
|
|
788
788
|
return null; // Not whitelisted
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
-
function
|
|
791
|
+
function resolveSessionPermissionMode(session) {
|
|
792
|
+
var mode = session.permissionMode || sm.currentPermissionMode;
|
|
793
|
+
var router = getWorkerPermissionRouter();
|
|
794
|
+
if (router && typeof router.inheritedPermissionMode === "function") {
|
|
795
|
+
var inherited = router.inheritedPermissionMode(session);
|
|
796
|
+
if (inherited) mode = inherited;
|
|
797
|
+
}
|
|
798
|
+
return mode;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function handlePreToolUsePolicy(session, toolName, input) {
|
|
792
802
|
var debateDecision = homeDebateToolPolicy.initialToolDecision(session, toolName, input);
|
|
793
|
-
if (debateDecision) return
|
|
803
|
+
if (debateDecision) return debateDecision;
|
|
794
804
|
// The dynamic proposal tool is bound to this exact query session. It only
|
|
795
805
|
// posts the inline proposal; starting the debate still requires the user.
|
|
796
806
|
if (session.homeDebatePlanning === true && toolName === "propose_debate") {
|
|
797
|
-
return
|
|
807
|
+
return { behavior: "allow", updatedInput: input };
|
|
798
808
|
}
|
|
799
809
|
// Mate creation proposals only present the exact-session approval card;
|
|
800
810
|
// creating the Mate still requires the user's explicit action there.
|
|
801
811
|
if (session.mateCreationMode === true && toolName === "propose_mate") {
|
|
802
|
-
return
|
|
803
|
-
}
|
|
804
|
-
// Full-auto mode: auto-approve everything except AskUserQuestion
|
|
805
|
-
// (which still needs to go through the user interaction flow).
|
|
806
|
-
var clayPermissionMode = session.permissionMode || sm.currentPermissionMode;
|
|
807
|
-
// A configured Split Worker is Driver-operated, so it runs under its
|
|
808
|
-
// Driver's permission mode, re-resolved from the live pair on every single
|
|
809
|
-
// decision. Nothing is copied at creation and the Worker's own persisted
|
|
810
|
-
// setting is never written, so toggling skip permissions on the Driver
|
|
811
|
-
// applies to the Worker's next call in either direction, mid-turn included.
|
|
812
|
-
var _wpMode = getWorkerPermissionRouter();
|
|
813
|
-
if (_wpMode && typeof _wpMode.inheritedPermissionMode === "function") {
|
|
814
|
-
var inherited = _wpMode.inheritedPermissionMode(session);
|
|
815
|
-
if (inherited) clayPermissionMode = inherited;
|
|
816
|
-
}
|
|
817
|
-
// AskUserQuestion is genuine user input, not an authorization, so it is
|
|
818
|
-
// excluded here exactly as it always was: a Driver in skip mode still
|
|
819
|
-
// cannot silently answer a question addressed to the person.
|
|
820
|
-
if (clayPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
|
|
821
|
-
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
812
|
+
return { behavior: "allow", updatedInput: input };
|
|
822
813
|
}
|
|
823
|
-
|
|
824
814
|
// Ralph Loop execution: auto-approve all tools, deny interactive ones.
|
|
825
815
|
// Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
|
|
826
816
|
if (session.loop && session.loop.active && session.loop.role !== "crafting") {
|
|
827
817
|
if (toolName === "AskUserQuestion") {
|
|
828
|
-
return
|
|
818
|
+
return { behavior: "deny", message: "Autonomous mode. Make your own decision." };
|
|
829
819
|
}
|
|
830
820
|
if (toolName === "EnterPlanMode") {
|
|
831
|
-
return
|
|
821
|
+
return { behavior: "deny", message: "Do not enter plan mode. Execute directly." };
|
|
832
822
|
}
|
|
833
|
-
return
|
|
823
|
+
return { behavior: "allow", updatedInput: input };
|
|
834
824
|
}
|
|
835
825
|
|
|
836
826
|
// Check shared whitelist (read-only tools, safe browser tools, safe bash commands)
|
|
837
827
|
var whitelisted = checkToolWhitelist(toolName, input);
|
|
838
828
|
if (whitelisted) {
|
|
839
|
-
return
|
|
829
|
+
return whitelisted;
|
|
840
830
|
}
|
|
841
831
|
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function handleCanUseTool(session, toolName, input, opts) {
|
|
836
|
+
var initialDecision = handlePreToolUsePolicy(session, toolName, input);
|
|
837
|
+
if (initialDecision) return Promise.resolve(initialDecision);
|
|
838
|
+
// Skip permissions removes only the remaining Clay prompt. The SDK's
|
|
839
|
+
// deny/ask rules and Auto classifier have already run before this callback.
|
|
840
|
+
var skipRemainingPermissions = resolveSessionPermissionMode(session) === "bypassPermissions" && toolName !== "AskUserQuestion";
|
|
841
|
+
|
|
842
|
+
if (skipRemainingPermissions) return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
843
|
+
|
|
842
844
|
// Auto-approve if tool was previously allowed for session
|
|
843
|
-
if (session.allowedTools && session.allowedTools[toolName]) {
|
|
844
|
-
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
845
|
-
}
|
|
845
|
+
if (session.allowedTools && session.allowedTools[toolName]) return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
846
846
|
|
|
847
847
|
// A visible Split Worker's remaining prompts belong to its paired Driver,
|
|
848
848
|
// not to the Worker pane. This sits below every auto-decision above, so
|
|
@@ -988,12 +988,32 @@ function createSDKBridge(opts) {
|
|
|
988
988
|
}
|
|
989
989
|
}
|
|
990
990
|
|
|
991
|
+
function ownsQueryRuntime(session, queryInstance, queryGeneration) {
|
|
992
|
+
return session.queryInstance === queryInstance
|
|
993
|
+
&& Number(session._sdkQueryGeneration || 0) === Number(queryGeneration)
|
|
994
|
+
&& (!sm.sessions || typeof sm.sessions.get !== "function" || sm.sessions.get(session.localId) === session);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function recordRuntimeEffectivePermissionMode(session, queryInstance, queryGeneration, message) {
|
|
998
|
+
if (!message || (message.yokeType !== "init" && message.yokeType !== "status") || !message.permissionMode) return false;
|
|
999
|
+
if (!ownsQueryRuntime(session, queryInstance, queryGeneration)) return false;
|
|
1000
|
+
session.effectivePermissionMode = message.permissionMode;
|
|
1001
|
+
session.permissionModeFallbackReason = null;
|
|
1002
|
+
sm.saveSessionFile(session);
|
|
1003
|
+
sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: session.vendor || "claude",
|
|
1004
|
+
mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: message.permissionMode,
|
|
1005
|
+
permissionCapabilities: session.permissionCapabilities || { auto: false, mcpOverride: false },
|
|
1006
|
+
mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {} });
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
991
1010
|
async function processQueryStream(session, codexCatalogCandidate) {
|
|
992
1011
|
// Capture references at start so we only clean up OUR resources in finally,
|
|
993
1012
|
// not resources from a newer query that may have been created after an abort.
|
|
994
1013
|
var myQueryInstance = session.queryInstance;
|
|
995
1014
|
var myAbortController = session.abortController;
|
|
996
1015
|
var myMessageQueue = session.messageQueue;
|
|
1016
|
+
var myQueryGeneration = Number(session._sdkQueryGeneration || 0);
|
|
997
1017
|
console.log("[sdk-bridge] processQueryStream: starting for-await loop, vendor=" + (session.vendor || adapter.vendor));
|
|
998
1018
|
try {
|
|
999
1019
|
for await (var msg of myQueryInstance) {
|
|
@@ -1023,8 +1043,10 @@ function createSDKBridge(opts) {
|
|
|
1023
1043
|
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
|
|
1024
1044
|
break;
|
|
1025
1045
|
case "permission_mode_changed":
|
|
1026
|
-
|
|
1027
|
-
|
|
1046
|
+
// Setter notifications are acknowledgements, not a new project
|
|
1047
|
+
// default and not proof of the runtime-effective mode. Only an
|
|
1048
|
+
// init/status event below may confirm effectivePermissionMode.
|
|
1049
|
+
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: session.effectivePermissionMode || null, permissionCapabilities: session.permissionCapabilities || { auto: false, mcpOverride: false }, mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {}, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1028
1050
|
break;
|
|
1029
1051
|
case "worker_error":
|
|
1030
1052
|
send({ type: "error", text: metaData.error });
|
|
@@ -1033,6 +1055,10 @@ function createSDKBridge(opts) {
|
|
|
1033
1055
|
continue;
|
|
1034
1056
|
}
|
|
1035
1057
|
processSDKMessage(session, msg);
|
|
1058
|
+
// SDK init/status events are the only authority for runtime-effective
|
|
1059
|
+
// permission state. Setter acknowledgement deliberately does not
|
|
1060
|
+
// change this field.
|
|
1061
|
+
recordRuntimeEffectivePermissionMode(session, myQueryInstance, myQueryGeneration, msg);
|
|
1036
1062
|
if (codexCatalogCandidate && msg && msg.yokeType === "session_started" && msg.sessionId
|
|
1037
1063
|
&& session.queryInstance === myQueryInstance
|
|
1038
1064
|
&& Number(session._sdkQueryGeneration || 0) === codexCatalogCandidate.generation
|
|
@@ -1843,12 +1869,20 @@ function createSDKBridge(opts) {
|
|
|
1843
1869
|
abortController: linuxUser ? undefined : session.abortController,
|
|
1844
1870
|
canUseTool: function(toolName, input, toolOpts) {
|
|
1845
1871
|
var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
|
|
1846
|
-
if (nativeQuestionError) return
|
|
1872
|
+
if (nativeQuestionError) return { behavior: "deny", message: nativeQuestionError };
|
|
1847
1873
|
var permissionName = toolName;
|
|
1848
1874
|
var permissionToolDef = findSessionToolDef(toolName);
|
|
1849
1875
|
if (permissionToolDef && permissionToolDef.permissionName) permissionName = permissionToolDef.permissionName;
|
|
1850
1876
|
return handleCanUseTool(session, permissionName, input, toolOpts);
|
|
1851
1877
|
},
|
|
1878
|
+
preToolUsePolicy: function(toolName, input) {
|
|
1879
|
+
var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
|
|
1880
|
+
if (nativeQuestionError) return Promise.resolve({ behavior: "deny", message: nativeQuestionError });
|
|
1881
|
+
var permissionName = toolName;
|
|
1882
|
+
var permissionToolDef = findSessionToolDef(toolName);
|
|
1883
|
+
if (permissionToolDef && permissionToolDef.permissionName) permissionName = permissionToolDef.permissionName;
|
|
1884
|
+
return handlePreToolUsePolicy(session, permissionName, input);
|
|
1885
|
+
},
|
|
1852
1886
|
onUserInputRequest: function(request, respond) {
|
|
1853
1887
|
return sessionUserInputHandler(request, respond);
|
|
1854
1888
|
},
|
|
@@ -1948,6 +1982,24 @@ function createSDKBridge(opts) {
|
|
|
1948
1982
|
|
|
1949
1983
|
// Store QueryHandle on session for iteration and control.
|
|
1950
1984
|
session.queryInstance = handle;
|
|
1985
|
+
// Capabilities come from the installed query handle, never a model-name
|
|
1986
|
+
// table. `acceptEdits` and MCP overrides are Claude SDK semantics; a
|
|
1987
|
+
// vendor may expose the generic setter without implementing either.
|
|
1988
|
+
session.permissionCapabilities = require("./session-permission-mode").capabilities(session, sm.defaultVendor);
|
|
1989
|
+
session.permissionCapabilities.mcpOverride = sessionAdapter.vendor === "claude" && typeof handle.setMcpPermissionModeOverride === "function";
|
|
1990
|
+
session.effectivePermissionMode = null;
|
|
1991
|
+
sm.saveSessionFile(session);
|
|
1992
|
+
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: null, permissionCapabilities: session.permissionCapabilities, mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {}, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1993
|
+
|
|
1994
|
+
if (session.permissionMode === "auto" && session.permissionCapabilities.mcpOverride) {
|
|
1995
|
+
var overrideNames = Object.keys(session.mcpPermissionModeOverrides || {});
|
|
1996
|
+
for (var oi = 0; oi < overrideNames.length; oi++) {
|
|
1997
|
+
try { await handle.setMcpPermissionModeOverride(overrideNames[oi], session.mcpPermissionModeOverrides[overrideNames[oi]]); }
|
|
1998
|
+
catch (overrideError) {
|
|
1999
|
+
sendToSession(session, { type: "error", text: "Failed to restore the AI review policy for " + overrideNames[oi] + ": " + (overrideError.message || overrideError) });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
1951
2003
|
|
|
1952
2004
|
if (typeof beforePush === "function" && beforePush() !== true) {
|
|
1953
2005
|
try { handle.close(); } catch (e) {}
|
|
@@ -2358,17 +2410,15 @@ function createSDKBridge(opts) {
|
|
|
2358
2410
|
async function setPermissionMode(session, mode) {
|
|
2359
2411
|
if (!session.queryInstance) {
|
|
2360
2412
|
// No active query — just store the mode for next startQuery
|
|
2361
|
-
|
|
2362
|
-
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
2363
|
-
return;
|
|
2413
|
+
return { ok: true, effectiveMode: mode };
|
|
2364
2414
|
}
|
|
2365
2415
|
try {
|
|
2366
2416
|
// Route through QueryHandle (works for both in-process and worker paths)
|
|
2367
2417
|
await session.queryInstance.setPermissionMode(mode);
|
|
2368
|
-
|
|
2369
|
-
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
2418
|
+
return { ok: true, effectiveMode: mode };
|
|
2370
2419
|
} catch (e) {
|
|
2371
2420
|
sendToSession(session, { type: "error", text: "Failed to set permission mode: " + (e.message || e) });
|
|
2421
|
+
return { ok: false, error: e.message || String(e) };
|
|
2372
2422
|
}
|
|
2373
2423
|
}
|
|
2374
2424
|
|
|
@@ -2404,12 +2454,14 @@ function createSDKBridge(opts) {
|
|
|
2404
2454
|
|
|
2405
2455
|
// Override the permission mode for one MCP server on the active query.
|
|
2406
2456
|
async function setMcpPermissionModeOverride(session, serverName, mode) {
|
|
2407
|
-
if (!session || !session.queryInstance) return;
|
|
2457
|
+
if (!session || !session.queryInstance || typeof session.queryInstance.setMcpPermissionModeOverride !== "function") return { ok: false, error: "MCP permission controls are unavailable for this session." };
|
|
2408
2458
|
try {
|
|
2409
2459
|
// Route through QueryHandle (works for both in-process and worker paths)
|
|
2410
2460
|
await session.queryInstance.setMcpPermissionModeOverride(serverName, mode);
|
|
2461
|
+
return { ok: true };
|
|
2411
2462
|
} catch (e) {
|
|
2412
2463
|
send({ type: "error", text: "Failed to set MCP permission mode: " + (e.message || e) });
|
|
2464
|
+
return { ok: false, error: e.message || String(e) };
|
|
2413
2465
|
}
|
|
2414
2466
|
}
|
|
2415
2467
|
|
|
@@ -2583,6 +2635,9 @@ function createSDKBridge(opts) {
|
|
|
2583
2635
|
createMessageQueue: createMessageQueue,
|
|
2584
2636
|
processSDKMessage: processSDKMessage,
|
|
2585
2637
|
checkToolWhitelist: checkToolWhitelist,
|
|
2638
|
+
handlePreToolUsePolicy: handlePreToolUsePolicy,
|
|
2639
|
+
ownsQueryRuntime: ownsQueryRuntime,
|
|
2640
|
+
recordRuntimeEffectivePermissionMode: recordRuntimeEffectivePermissionMode,
|
|
2586
2641
|
handleCanUseTool: handleCanUseTool,
|
|
2587
2642
|
handleElicitation: handleElicitation,
|
|
2588
2643
|
processQueryStream: processQueryStream,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Canonical, server-owned permission selection validation. The requested mode
|
|
2
|
+
// is persisted separately from the SDK mode because Clay keeps the final
|
|
3
|
+
// canUseTool decision for GUI sessions.
|
|
4
|
+
|
|
5
|
+
function normalize(mode) {
|
|
6
|
+
if (mode === "default" || mode === "auto" || mode === "plan" || mode === "acceptEdits" || mode === "bypassPermissions") return mode;
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function capabilities(session, defaultVendor) {
|
|
11
|
+
var vendor = session && session.vendor || defaultVendor || "claude";
|
|
12
|
+
return {
|
|
13
|
+
auto: vendor === "claude",
|
|
14
|
+
mcpOverride: vendor === "claude" && !!(session && session.queryInstance &&
|
|
15
|
+
typeof session.queryInstance.setMcpPermissionModeOverride === "function"),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function supportsAuto(session, defaultVendor) {
|
|
20
|
+
return capabilities(session, defaultVendor).auto;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validate(session, mode, defaultVendor) {
|
|
24
|
+
var requested = normalize(mode);
|
|
25
|
+
if (!requested) return { ok: false, error: "Unknown permission mode." };
|
|
26
|
+
if (requested === "auto" && !supportsAuto(session, defaultVendor)) {
|
|
27
|
+
return { ok: false, error: "Auto permissions are not available for this session." };
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, mode: requested };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clientState(session, fallback, defaultVendor) {
|
|
33
|
+
var requested = session && session.permissionMode || fallback || "default";
|
|
34
|
+
return {
|
|
35
|
+
requestedPermissionMode: requested,
|
|
36
|
+
effectivePermissionMode: session && session.effectivePermissionMode || null,
|
|
37
|
+
permissionCapabilities: capabilities(session, defaultVendor),
|
|
38
|
+
mcpPermissionModeOverrides: session && session.mcpPermissionModeOverrides || {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { validate: validate, clientState: clientState, supportsAuto: supportsAuto, capabilities: capabilities };
|
package/lib/sessions.js
CHANGED
|
@@ -177,6 +177,7 @@ function createSessionManager(opts) {
|
|
|
177
177
|
// so lazy-resume (`claude --resume`) re-spawns with the same flag.
|
|
178
178
|
if (session.dangerouslySkipPermissions) metaObj.dangerouslySkipPermissions = true;
|
|
179
179
|
if (session.permissionMode) metaObj.permissionMode = session.permissionMode;
|
|
180
|
+
if (session.mcpPermissionModeOverrides) metaObj.mcpPermissionModeOverrides = session.mcpPermissionModeOverrides;
|
|
180
181
|
if (session.permissionModeBeforeFullAccess) metaObj.permissionModeBeforeFullAccess = session.permissionModeBeforeFullAccess;
|
|
181
182
|
if (session.autonomousRun) metaObj.autonomousRun = session.autonomousRun;
|
|
182
183
|
if (session.loopInterviewBrief) metaObj.loopInterviewBrief = session.loopInterviewBrief;
|
|
@@ -348,6 +349,10 @@ function createSessionManager(opts) {
|
|
|
348
349
|
// Clay's own per-session permission mode.
|
|
349
350
|
session.dangerouslySkipPermissions = session.mode === "tui" && !!m.dangerouslySkipPermissions;
|
|
350
351
|
session.permissionMode = m.permissionMode || (session.mode === "gui" && m.dangerouslySkipPermissions ? "bypassPermissions" : null);
|
|
352
|
+
// Runtime-effective state is observed from the live SDK only. A daemon
|
|
353
|
+
// restore has no active runtime to confirm the old value.
|
|
354
|
+
session.effectivePermissionMode = null;
|
|
355
|
+
session.mcpPermissionModeOverrides = m.mcpPermissionModeOverrides && typeof m.mcpPermissionModeOverrides === "object" ? m.mcpPermissionModeOverrides : {};
|
|
351
356
|
session.permissionModeBeforeFullAccess = m.permissionModeBeforeFullAccess || null;
|
|
352
357
|
session.autonomousRun = restoreStoredRun(m.autonomousRun);
|
|
353
358
|
session.loopInterviewBrief = m.loopInterviewBrief && typeof m.loopInterviewBrief === "object" ? m.loopInterviewBrief : null;
|
|
@@ -587,6 +592,9 @@ function createSessionManager(opts) {
|
|
|
587
592
|
runtimeTerminalId: typeof s.runtimeTerminalId === "number" ? s.runtimeTerminalId : null,
|
|
588
593
|
dangerouslySkipPermissions: !!s.dangerouslySkipPermissions,
|
|
589
594
|
permissionMode: s.permissionMode || null,
|
|
595
|
+
effectivePermissionMode: s.effectivePermissionMode || null,
|
|
596
|
+
permissionCapabilities: require("./session-permission-mode").capabilities(s, defaultVendor),
|
|
597
|
+
mcpPermissionModeOverrides: s.mcpPermissionModeOverrides || {},
|
|
590
598
|
autonomousRun: s.autonomousRun ? {
|
|
591
599
|
id: s.autonomousRun.id,
|
|
592
600
|
state: s.autonomousRun.state,
|
|
@@ -676,6 +684,8 @@ function createSessionManager(opts) {
|
|
|
676
684
|
mode: (sessionOpts && sessionOpts.mode === "tui") ? "tui" : "gui",
|
|
677
685
|
dangerouslySkipPermissions: !!(sessionOpts && sessionOpts.mode === "tui" && sessionOpts.dangerouslySkipPermissions),
|
|
678
686
|
permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
|
|
687
|
+
effectivePermissionMode: null,
|
|
688
|
+
mcpPermissionModeOverrides: {},
|
|
679
689
|
permissionModeBeforeFullAccess: null,
|
|
680
690
|
autonomousRun: null,
|
|
681
691
|
loopInterviewBrief: null,
|
|
@@ -725,6 +735,8 @@ function createSessionManager(opts) {
|
|
|
725
735
|
mode: (sessionOpts && sessionOpts.mode === "tui") ? "tui" : "gui",
|
|
726
736
|
dangerouslySkipPermissions: !!(sessionOpts && sessionOpts.mode === "tui" && sessionOpts.dangerouslySkipPermissions),
|
|
727
737
|
permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
|
|
738
|
+
effectivePermissionMode: null,
|
|
739
|
+
mcpPermissionModeOverrides: {},
|
|
728
740
|
permissionModeBeforeFullAccess: null,
|
|
729
741
|
autonomousRun: null,
|
|
730
742
|
loopInterviewBrief: null,
|
|
@@ -826,7 +838,8 @@ function createSessionManager(opts) {
|
|
|
826
838
|
var _capsByVendor = capabilitiesByVendor || {};
|
|
827
839
|
var _sessionVendor = session.vendor || defaultVendor || "claude";
|
|
828
840
|
var _vendorCaps = _capsByVendor[_sessionVendor] || {};
|
|
829
|
-
|
|
841
|
+
var _permissionState = require("./session-permission-mode").clientState(session, "default", defaultVendor);
|
|
842
|
+
_send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: _permissionState.requestedPermissionMode, effectivePermissionMode: _permissionState.effectivePermissionMode, permissionCapabilities: _permissionState.permissionCapabilities, mcpPermissionModeOverrides: _permissionState.mcpPermissionModeOverrides, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
|
|
830
843
|
// Send vendor-specific slash commands
|
|
831
844
|
var _vendorCmds = (session.slashCommandsByVendor && session.slashCommandsByVendor[_sessionVendor])
|
|
832
845
|
|| session.slashCommands
|