@ccpocket/bridge 1.25.0 → 1.27.0
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/README.md +4 -0
- package/dist/codex-process.d.ts +30 -0
- package/dist/codex-process.js +750 -83
- package/dist/codex-process.js.map +1 -1
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +15 -0
- package/dist/parser.js.map +1 -1
- package/dist/push-i18n.d.ts +1 -1
- package/dist/push-i18n.js +18 -1
- package/dist/push-i18n.js.map +1 -1
- package/dist/session.d.ts +4 -0
- package/dist/session.js +147 -69
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +2 -0
- package/dist/sessions-index.js +10 -0
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +7 -0
- package/dist/websocket.js +404 -106
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
7
7
|
import { SessionManager } from "./session.js";
|
|
8
8
|
import { SdkProcess } from "./sdk-process.js";
|
|
9
|
+
import { CodexProcess } from "./codex-process.js";
|
|
9
10
|
import { parseClientMessage } from "./parser.js";
|
|
10
11
|
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession } from "./sessions-index.js";
|
|
11
12
|
import { ArchiveStore } from "./archive-store.js";
|
|
@@ -37,14 +38,81 @@ const CODEX_MODELS = [
|
|
|
37
38
|
function permissionModeToApprovalPolicy(mode) {
|
|
38
39
|
return mode === "bypassPermissions" ? "never" : "on-request";
|
|
39
40
|
}
|
|
41
|
+
function deriveExecutionMode(params) {
|
|
42
|
+
if (params.executionMode === "default" ||
|
|
43
|
+
params.executionMode === "acceptEdits" ||
|
|
44
|
+
params.executionMode === "fullAccess") {
|
|
45
|
+
return params.executionMode;
|
|
46
|
+
}
|
|
47
|
+
if (params.permissionMode === "bypassPermissions" ||
|
|
48
|
+
params.approvalPolicy === "never") {
|
|
49
|
+
return "fullAccess";
|
|
50
|
+
}
|
|
51
|
+
if (params.permissionMode === "acceptEdits") {
|
|
52
|
+
return params.provider === "codex" ? "default" : "acceptEdits";
|
|
53
|
+
}
|
|
54
|
+
return "default";
|
|
55
|
+
}
|
|
56
|
+
function derivePlanMode(params) {
|
|
57
|
+
return params.planMode ??
|
|
58
|
+
((params.permissionMode === "plan") ||
|
|
59
|
+
(params.collaborationMode === "plan"));
|
|
60
|
+
}
|
|
61
|
+
function modesToLegacyPermissionMode(provider, executionMode, planMode) {
|
|
62
|
+
if (planMode)
|
|
63
|
+
return "plan";
|
|
64
|
+
switch (executionMode) {
|
|
65
|
+
case "fullAccess":
|
|
66
|
+
return "bypassPermissions";
|
|
67
|
+
case "acceptEdits":
|
|
68
|
+
return "acceptEdits";
|
|
69
|
+
case "default":
|
|
70
|
+
default:
|
|
71
|
+
return provider === "codex" ? "acceptEdits" : "default";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
40
74
|
/** Map simplified SandboxMode (on/off) to Codex internal sandbox mode. */
|
|
41
75
|
function sandboxModeToInternal(mode) {
|
|
42
|
-
|
|
76
|
+
switch (mode) {
|
|
77
|
+
case "danger-full-access":
|
|
78
|
+
case "workspace-write":
|
|
79
|
+
case "read-only":
|
|
80
|
+
return mode;
|
|
81
|
+
case "off":
|
|
82
|
+
return "danger-full-access";
|
|
83
|
+
default:
|
|
84
|
+
return "workspace-write";
|
|
85
|
+
}
|
|
43
86
|
}
|
|
44
87
|
/** Map Codex internal sandbox mode back to simplified on/off for clients. */
|
|
45
88
|
function sandboxModeToExternal(mode) {
|
|
46
89
|
return mode === "danger-full-access" ? "off" : "on";
|
|
47
90
|
}
|
|
91
|
+
function threadTimestampToIso(value) {
|
|
92
|
+
return value > 0 ? new Date(value * 1000).toISOString() : "";
|
|
93
|
+
}
|
|
94
|
+
function envFlagEnabled(name) {
|
|
95
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
96
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
97
|
+
}
|
|
98
|
+
function codexThreadToRecentSession(thread, indexed) {
|
|
99
|
+
return {
|
|
100
|
+
sessionId: thread.id,
|
|
101
|
+
provider: "codex",
|
|
102
|
+
...(thread.name ? { name: thread.name } : {}),
|
|
103
|
+
...(thread.agentNickname ? { agentNickname: thread.agentNickname } : {}),
|
|
104
|
+
...(thread.agentRole ? { agentRole: thread.agentRole } : {}),
|
|
105
|
+
summary: thread.preview || undefined,
|
|
106
|
+
firstPrompt: thread.preview || "",
|
|
107
|
+
created: threadTimestampToIso(thread.createdAt),
|
|
108
|
+
modified: threadTimestampToIso(thread.updatedAt),
|
|
109
|
+
gitBranch: thread.gitBranch ?? "",
|
|
110
|
+
projectPath: thread.cwd,
|
|
111
|
+
...(indexed?.resumeCwd ? { resumeCwd: indexed.resumeCwd } : {}),
|
|
112
|
+
isSidechain: false,
|
|
113
|
+
...(indexed?.codexSettings ? { codexSettings: indexed.codexSettings } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
48
116
|
export class BridgeWebSocketServer {
|
|
49
117
|
static MAX_DEBUG_EVENTS = 800;
|
|
50
118
|
static MAX_HISTORY_SUMMARY_ITEMS = 300;
|
|
@@ -67,6 +135,8 @@ export class BridgeWebSocketServer {
|
|
|
67
135
|
/** FCM token → push notification locale */
|
|
68
136
|
tokenLocales = new Map();
|
|
69
137
|
tokenPrivacyMode = new Map();
|
|
138
|
+
failSetPermissionMode = envFlagEnabled("BRIDGE_FAIL_SET_PERMISSION_MODE");
|
|
139
|
+
failSetSandboxMode = envFlagEnabled("BRIDGE_FAIL_SET_SANDBOX_MODE");
|
|
70
140
|
constructor(options) {
|
|
71
141
|
const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup } = options;
|
|
72
142
|
this.apiKey = apiKey ?? null;
|
|
@@ -145,6 +215,91 @@ export class BridgeWebSocketServer {
|
|
|
145
215
|
errorCode: "path_not_allowed",
|
|
146
216
|
};
|
|
147
217
|
}
|
|
218
|
+
buildSessionCreatedMessage(params) {
|
|
219
|
+
const { sessionId, provider, projectPath, session, permissionMode, executionMode, planMode, sandboxMode, slashCommands, skills, skillMetadata, sourceSessionId, } = params;
|
|
220
|
+
const msg = {
|
|
221
|
+
type: "system",
|
|
222
|
+
subtype: "session_created",
|
|
223
|
+
sessionId,
|
|
224
|
+
provider,
|
|
225
|
+
projectPath,
|
|
226
|
+
...(permissionMode ? { permissionMode: permissionMode } : {}),
|
|
227
|
+
...((executionMode ?? (session?.process instanceof SdkProcess
|
|
228
|
+
? session.process.permissionMode === "bypassPermissions"
|
|
229
|
+
? "fullAccess"
|
|
230
|
+
: session.process.permissionMode === "acceptEdits"
|
|
231
|
+
? "acceptEdits"
|
|
232
|
+
: "default"
|
|
233
|
+
: session?.process instanceof CodexProcess
|
|
234
|
+
? session.process.approvalPolicy === "never"
|
|
235
|
+
? "fullAccess"
|
|
236
|
+
: "default"
|
|
237
|
+
: undefined))
|
|
238
|
+
? {
|
|
239
|
+
executionMode: (executionMode ?? (session?.process instanceof SdkProcess
|
|
240
|
+
? session.process.permissionMode === "bypassPermissions"
|
|
241
|
+
? "fullAccess"
|
|
242
|
+
: session.process.permissionMode === "acceptEdits"
|
|
243
|
+
? "acceptEdits"
|
|
244
|
+
: "default"
|
|
245
|
+
: session?.process instanceof CodexProcess
|
|
246
|
+
? session.process.approvalPolicy === "never"
|
|
247
|
+
? "fullAccess"
|
|
248
|
+
: "default"
|
|
249
|
+
: undefined)),
|
|
250
|
+
}
|
|
251
|
+
: {}),
|
|
252
|
+
...((planMode ??
|
|
253
|
+
(session?.process instanceof SdkProcess
|
|
254
|
+
? session.process.permissionMode === "plan"
|
|
255
|
+
: session?.process instanceof CodexProcess
|
|
256
|
+
? session.process.collaborationMode === "plan"
|
|
257
|
+
: undefined)) !=
|
|
258
|
+
null
|
|
259
|
+
? {
|
|
260
|
+
planMode: planMode ??
|
|
261
|
+
(session?.process instanceof SdkProcess
|
|
262
|
+
? session.process.permissionMode === "plan"
|
|
263
|
+
: session?.process instanceof CodexProcess
|
|
264
|
+
? session.process.collaborationMode === "plan"
|
|
265
|
+
: false),
|
|
266
|
+
}
|
|
267
|
+
: {}),
|
|
268
|
+
...(sandboxMode ? { sandboxMode } : {}),
|
|
269
|
+
...(slashCommands ? { slashCommands } : {}),
|
|
270
|
+
...(skills ? { skills } : {}),
|
|
271
|
+
...(skillMetadata
|
|
272
|
+
? {
|
|
273
|
+
skillMetadata: skillMetadata,
|
|
274
|
+
}
|
|
275
|
+
: {}),
|
|
276
|
+
...(session?.worktreePath
|
|
277
|
+
? {
|
|
278
|
+
worktreePath: session.worktreePath,
|
|
279
|
+
worktreeBranch: session.worktreeBranch,
|
|
280
|
+
}
|
|
281
|
+
: {}),
|
|
282
|
+
...(sourceSessionId ? { sourceSessionId } : {}),
|
|
283
|
+
};
|
|
284
|
+
if (provider === "codex" && session?.codexSettings) {
|
|
285
|
+
if (session.codexSettings.model !== undefined) {
|
|
286
|
+
msg.model = session.codexSettings.model;
|
|
287
|
+
}
|
|
288
|
+
if (session.codexSettings.approvalPolicy !== undefined) {
|
|
289
|
+
msg.approvalPolicy = session.codexSettings.approvalPolicy;
|
|
290
|
+
}
|
|
291
|
+
if (session.codexSettings.modelReasoningEffort !== undefined) {
|
|
292
|
+
msg.modelReasoningEffort = session.codexSettings.modelReasoningEffort;
|
|
293
|
+
}
|
|
294
|
+
if (session.codexSettings.networkAccessEnabled !== undefined) {
|
|
295
|
+
msg.networkAccessEnabled = session.codexSettings.networkAccessEnabled;
|
|
296
|
+
}
|
|
297
|
+
if (session.codexSettings.webSearchMode !== undefined) {
|
|
298
|
+
msg.webSearchMode = session.codexSettings.webSearchMode;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return msg;
|
|
302
|
+
}
|
|
148
303
|
close() {
|
|
149
304
|
console.log("[ws] Shutting down...");
|
|
150
305
|
this.sessionManager.destroyAll();
|
|
@@ -213,14 +368,24 @@ export class BridgeWebSocketServer {
|
|
|
213
368
|
}
|
|
214
369
|
try {
|
|
215
370
|
const provider = msg.provider ?? "claude";
|
|
371
|
+
const executionMode = deriveExecutionMode({
|
|
372
|
+
provider,
|
|
373
|
+
permissionMode: msg.permissionMode,
|
|
374
|
+
executionMode: msg.executionMode,
|
|
375
|
+
});
|
|
376
|
+
const planMode = derivePlanMode({
|
|
377
|
+
permissionMode: msg.permissionMode,
|
|
378
|
+
planMode: msg.planMode,
|
|
379
|
+
});
|
|
380
|
+
const legacyPermissionMode = modesToLegacyPermissionMode(provider, executionMode, planMode);
|
|
216
381
|
if (provider === "codex") {
|
|
217
|
-
console.log(`[ws] start(codex):
|
|
382
|
+
console.log(`[ws] start(codex): execution=${executionMode} plan=${planMode}`);
|
|
218
383
|
}
|
|
219
384
|
const cached = provider === "claude" ? this.sessionManager.getCachedCommands(msg.projectPath) : undefined;
|
|
220
385
|
const sessionId = this.sessionManager.create(msg.projectPath, {
|
|
221
386
|
sessionId: msg.sessionId,
|
|
222
387
|
continueMode: msg.continue,
|
|
223
|
-
permissionMode:
|
|
388
|
+
permissionMode: legacyPermissionMode,
|
|
224
389
|
model: msg.model,
|
|
225
390
|
effort: msg.effort,
|
|
226
391
|
maxTurns: msg.maxTurns,
|
|
@@ -238,33 +403,38 @@ export class BridgeWebSocketServer {
|
|
|
238
403
|
existingWorktreePath: msg.existingWorktreePath,
|
|
239
404
|
}, provider, provider === "codex"
|
|
240
405
|
? {
|
|
241
|
-
approvalPolicy:
|
|
406
|
+
approvalPolicy: executionMode === "fullAccess" ? "never" : "on-request",
|
|
242
407
|
sandboxMode: sandboxModeToInternal(msg.sandboxMode),
|
|
243
408
|
model: msg.model,
|
|
244
409
|
modelReasoningEffort: msg.modelReasoningEffort ?? undefined,
|
|
245
410
|
networkAccessEnabled: msg.networkAccessEnabled,
|
|
246
411
|
webSearchMode: msg.webSearchMode ?? undefined,
|
|
247
412
|
threadId: msg.sessionId,
|
|
248
|
-
collaborationMode:
|
|
413
|
+
collaborationMode: planMode ? "plan" : "default",
|
|
249
414
|
}
|
|
250
415
|
: undefined);
|
|
251
416
|
const createdSession = this.sessionManager.get(sessionId);
|
|
252
417
|
// Load saved session name from CLI storage (for resumed sessions)
|
|
253
418
|
void this.loadAndSetSessionName(createdSession, provider, msg.projectPath, msg.sessionId).then(() => {
|
|
254
|
-
this.send(ws, {
|
|
255
|
-
type: "system",
|
|
256
|
-
subtype: "session_created",
|
|
419
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
257
420
|
sessionId,
|
|
258
421
|
provider,
|
|
259
422
|
projectPath: msg.projectPath,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
423
|
+
session: createdSession,
|
|
424
|
+
permissionMode: legacyPermissionMode,
|
|
425
|
+
executionMode,
|
|
426
|
+
planMode,
|
|
427
|
+
sandboxMode: msg.sandboxMode,
|
|
428
|
+
...(cached
|
|
429
|
+
? {
|
|
430
|
+
slashCommands: cached.slashCommands,
|
|
431
|
+
skills: cached.skills,
|
|
432
|
+
...(cached.skillMetadata
|
|
433
|
+
? { skillMetadata: cached.skillMetadata }
|
|
434
|
+
: {}),
|
|
435
|
+
}
|
|
436
|
+
: {}),
|
|
437
|
+
}));
|
|
268
438
|
this.broadcastSessionList();
|
|
269
439
|
// Send a gentle tip when the project is not a git repository
|
|
270
440
|
if (createdSession && !createdSession.gitBranch) {
|
|
@@ -479,6 +649,14 @@ export class BridgeWebSocketServer {
|
|
|
479
649
|
break;
|
|
480
650
|
}
|
|
481
651
|
case "set_permission_mode": {
|
|
652
|
+
if (this.failSetPermissionMode) {
|
|
653
|
+
this.send(ws, {
|
|
654
|
+
type: "error",
|
|
655
|
+
message: "Failed to set permission mode: forced test failure",
|
|
656
|
+
errorCode: "set_permission_mode_rejected",
|
|
657
|
+
});
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
482
660
|
const session = this.resolveSession(msg.sessionId);
|
|
483
661
|
if (!session) {
|
|
484
662
|
this.send(ws, { type: "error", message: "No active session." });
|
|
@@ -488,14 +666,52 @@ export class BridgeWebSocketServer {
|
|
|
488
666
|
// Permission mode for Codex requires a session restart (like sandbox mode).
|
|
489
667
|
// approvalPolicy and collaborationMode are thread-level settings that
|
|
490
668
|
// only take effect reliably at thread/start or thread/resume time.
|
|
491
|
-
const
|
|
492
|
-
|
|
669
|
+
const executionMode = deriveExecutionMode({
|
|
670
|
+
provider: "codex",
|
|
671
|
+
permissionMode: msg.mode,
|
|
672
|
+
executionMode: msg.executionMode,
|
|
673
|
+
});
|
|
674
|
+
const planMode = derivePlanMode({
|
|
675
|
+
permissionMode: msg.mode,
|
|
676
|
+
planMode: msg.planMode,
|
|
677
|
+
});
|
|
678
|
+
const legacyPermissionMode = modesToLegacyPermissionMode("codex", executionMode, planMode);
|
|
679
|
+
const newApproval = executionMode === "fullAccess" ? "never" : "on-request";
|
|
680
|
+
const newCollaboration = planMode ? "plan" : "default";
|
|
493
681
|
const currentApproval = session.process.approvalPolicy;
|
|
494
682
|
const currentCollaboration = session.process.collaborationMode;
|
|
495
683
|
if (newApproval === currentApproval && newCollaboration === currentCollaboration) {
|
|
496
684
|
break; // No change needed
|
|
497
685
|
}
|
|
498
|
-
|
|
686
|
+
const canApplyModeInPlace = session.status === "idle";
|
|
687
|
+
if (canApplyModeInPlace) {
|
|
688
|
+
const process = session.process;
|
|
689
|
+
if (newApproval !== currentApproval) {
|
|
690
|
+
process.setApprovalPolicy(newApproval);
|
|
691
|
+
}
|
|
692
|
+
if (newCollaboration !== currentCollaboration) {
|
|
693
|
+
process.setCollaborationMode(newCollaboration);
|
|
694
|
+
}
|
|
695
|
+
session.lastActivityAt = new Date();
|
|
696
|
+
this.broadcast({
|
|
697
|
+
type: "system",
|
|
698
|
+
subtype: "set_permission_mode",
|
|
699
|
+
sessionId: session.id,
|
|
700
|
+
permissionMode: legacyPermissionMode,
|
|
701
|
+
executionMode,
|
|
702
|
+
planMode,
|
|
703
|
+
});
|
|
704
|
+
this.broadcastSessionList();
|
|
705
|
+
this.recordDebugEvent(session.id, {
|
|
706
|
+
direction: "internal",
|
|
707
|
+
channel: "bridge",
|
|
708
|
+
type: "permission_mode_changed",
|
|
709
|
+
detail: `mode=${msg.mode} approval=${newApproval} collaboration=${newCollaboration} applied=in-place`,
|
|
710
|
+
});
|
|
711
|
+
console.log(`[ws] set_permission_mode(codex): execution=${executionMode} plan=${planMode} → approval=${newApproval}, collaboration=${newCollaboration} (in-place)`);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
console.log(`[ws] set_permission_mode(codex): execution=${executionMode} plan=${planMode} → approval=${newApproval}, collaboration=${newCollaboration} (restart)`);
|
|
499
715
|
const oldSessionId = session.id;
|
|
500
716
|
const threadId = session.claudeSessionId;
|
|
501
717
|
const projectPath = session.projectPath;
|
|
@@ -519,17 +735,19 @@ export class BridgeWebSocketServer {
|
|
|
519
735
|
const newSession = this.sessionManager.get(newId);
|
|
520
736
|
if (newSession && sessionName)
|
|
521
737
|
newSession.name = sessionName;
|
|
522
|
-
this.broadcast({
|
|
523
|
-
type: "system",
|
|
524
|
-
subtype: "session_created",
|
|
738
|
+
this.broadcast(this.buildSessionCreatedMessage({
|
|
525
739
|
sessionId: newId,
|
|
526
740
|
provider: "codex",
|
|
527
741
|
projectPath,
|
|
528
|
-
|
|
529
|
-
|
|
742
|
+
session: newSession,
|
|
743
|
+
permissionMode: legacyPermissionMode,
|
|
744
|
+
executionMode,
|
|
745
|
+
planMode,
|
|
746
|
+
sandboxMode: oldSettings.sandboxMode
|
|
747
|
+
? sandboxModeToExternal(oldSettings.sandboxMode)
|
|
748
|
+
: undefined,
|
|
530
749
|
sourceSessionId: oldSessionId,
|
|
531
|
-
|
|
532
|
-
});
|
|
750
|
+
}));
|
|
533
751
|
this.broadcastSessionList();
|
|
534
752
|
console.log(`[ws] Permission mode change (no thread): created new session ${newId} (mode=${msg.mode})`);
|
|
535
753
|
break;
|
|
@@ -565,20 +783,19 @@ export class BridgeWebSocketServer {
|
|
|
565
783
|
newSession.name = sessionName;
|
|
566
784
|
}
|
|
567
785
|
void this.loadAndSetSessionName(newSession, "codex", effectiveProjectPath, threadId).then(() => {
|
|
568
|
-
this.broadcast({
|
|
569
|
-
type: "system",
|
|
570
|
-
subtype: "session_created",
|
|
786
|
+
this.broadcast(this.buildSessionCreatedMessage({
|
|
571
787
|
sessionId: newId,
|
|
572
788
|
provider: "codex",
|
|
573
789
|
projectPath: effectiveProjectPath,
|
|
574
|
-
|
|
575
|
-
|
|
790
|
+
session: newSession,
|
|
791
|
+
permissionMode: legacyPermissionMode,
|
|
792
|
+
executionMode,
|
|
793
|
+
planMode,
|
|
794
|
+
sandboxMode: oldSettings.sandboxMode
|
|
795
|
+
? sandboxModeToExternal(oldSettings.sandboxMode)
|
|
796
|
+
: undefined,
|
|
576
797
|
sourceSessionId: oldSessionId,
|
|
577
|
-
|
|
578
|
-
worktreePath: newSession.worktreePath,
|
|
579
|
-
worktreeBranch: newSession.worktreeBranch,
|
|
580
|
-
} : {}),
|
|
581
|
-
});
|
|
798
|
+
}));
|
|
582
799
|
this.broadcastSessionList();
|
|
583
800
|
});
|
|
584
801
|
this.debugEvents.set(newId, []);
|
|
@@ -603,6 +820,14 @@ export class BridgeWebSocketServer {
|
|
|
603
820
|
break;
|
|
604
821
|
}
|
|
605
822
|
case "set_sandbox_mode": {
|
|
823
|
+
if (this.failSetSandboxMode) {
|
|
824
|
+
this.send(ws, {
|
|
825
|
+
type: "error",
|
|
826
|
+
message: "Failed to set sandbox mode: forced test failure",
|
|
827
|
+
errorCode: "set_sandbox_mode_rejected",
|
|
828
|
+
});
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
606
831
|
const session = this.resolveSession(msg.sessionId);
|
|
607
832
|
if (!session) {
|
|
608
833
|
this.send(ws, { type: "error", message: "No active session." });
|
|
@@ -639,19 +864,14 @@ export class BridgeWebSocketServer {
|
|
|
639
864
|
if (newSession && sessionName)
|
|
640
865
|
newSession.name = sessionName;
|
|
641
866
|
void this.loadAndSetSessionName(newSession, "claude", projectPath, claudeSessionId).then(() => {
|
|
642
|
-
this.broadcast({
|
|
643
|
-
type: "system",
|
|
644
|
-
subtype: "session_created",
|
|
867
|
+
this.broadcast(this.buildSessionCreatedMessage({
|
|
645
868
|
sessionId: newId,
|
|
646
869
|
provider: "claude",
|
|
647
870
|
projectPath,
|
|
871
|
+
session: newSession,
|
|
648
872
|
sandboxMode: msg.sandboxMode,
|
|
649
873
|
sourceSessionId: oldSessionId,
|
|
650
|
-
|
|
651
|
-
worktreePath: newSession.worktreePath,
|
|
652
|
-
worktreeBranch: newSession.worktreeBranch,
|
|
653
|
-
} : {}),
|
|
654
|
-
});
|
|
874
|
+
}));
|
|
655
875
|
this.broadcastSessionList();
|
|
656
876
|
});
|
|
657
877
|
this.debugEvents.set(newId, []);
|
|
@@ -682,6 +902,9 @@ export class BridgeWebSocketServer {
|
|
|
682
902
|
const worktreeBranch = session.worktreeBranch;
|
|
683
903
|
const sessionName = session.name;
|
|
684
904
|
const collaborationMode = session.process.collaborationMode;
|
|
905
|
+
const executionMode = oldSettings.approvalPolicy === "never" ? "fullAccess" : "default";
|
|
906
|
+
const planMode = collaborationMode === "plan";
|
|
907
|
+
const legacyPermissionMode = modesToLegacyPermissionMode("codex", executionMode, planMode);
|
|
685
908
|
this.sessionManager.destroy(oldSessionId);
|
|
686
909
|
console.log(`[ws] Sandbox mode change: destroyed session ${oldSessionId}`);
|
|
687
910
|
// Check if the user actually exchanged messages in this session.
|
|
@@ -706,16 +929,17 @@ export class BridgeWebSocketServer {
|
|
|
706
929
|
const newSession = this.sessionManager.get(newId);
|
|
707
930
|
if (newSession && sessionName)
|
|
708
931
|
newSession.name = sessionName;
|
|
709
|
-
this.broadcast({
|
|
710
|
-
type: "system",
|
|
711
|
-
subtype: "session_created",
|
|
932
|
+
this.broadcast(this.buildSessionCreatedMessage({
|
|
712
933
|
sessionId: newId,
|
|
713
934
|
provider: "codex",
|
|
714
935
|
projectPath,
|
|
936
|
+
session: newSession,
|
|
937
|
+
permissionMode: legacyPermissionMode,
|
|
938
|
+
executionMode,
|
|
939
|
+
planMode,
|
|
715
940
|
sandboxMode: sandboxModeToExternal(newSandboxMode),
|
|
716
941
|
sourceSessionId: oldSessionId,
|
|
717
|
-
|
|
718
|
-
});
|
|
942
|
+
}));
|
|
719
943
|
this.broadcastSessionList();
|
|
720
944
|
console.log(`[ws] Sandbox mode change (no thread): created new session ${newId} (sandbox=${newSandboxMode})`);
|
|
721
945
|
break;
|
|
@@ -752,19 +976,17 @@ export class BridgeWebSocketServer {
|
|
|
752
976
|
newSession.name = sessionName;
|
|
753
977
|
}
|
|
754
978
|
void this.loadAndSetSessionName(newSession, "codex", effectiveProjectPath, threadId).then(() => {
|
|
755
|
-
this.broadcast({
|
|
756
|
-
type: "system",
|
|
757
|
-
subtype: "session_created",
|
|
979
|
+
this.broadcast(this.buildSessionCreatedMessage({
|
|
758
980
|
sessionId: newId,
|
|
759
981
|
provider: "codex",
|
|
760
982
|
projectPath: effectiveProjectPath,
|
|
983
|
+
session: newSession,
|
|
984
|
+
permissionMode: legacyPermissionMode,
|
|
985
|
+
executionMode,
|
|
986
|
+
planMode,
|
|
761
987
|
sandboxMode: sandboxModeToExternal(newSandboxMode),
|
|
762
988
|
sourceSessionId: oldSessionId,
|
|
763
|
-
|
|
764
|
-
worktreePath: newSession.worktreePath,
|
|
765
|
-
worktreeBranch: newSession.worktreeBranch,
|
|
766
|
-
} : {}),
|
|
767
|
-
});
|
|
989
|
+
}));
|
|
768
990
|
this.broadcastSessionList();
|
|
769
991
|
});
|
|
770
992
|
this.debugEvents.set(newId, []);
|
|
@@ -825,16 +1047,15 @@ export class BridgeWebSocketServer {
|
|
|
825
1047
|
console.log(`[ws] Clear context: created new session ${newId} (CLI session: ${claudeSessionId ?? "new"})`);
|
|
826
1048
|
// Notify all clients. Broadcast is used so reconnecting clients also receive it.
|
|
827
1049
|
const newSession = this.sessionManager.get(newId);
|
|
828
|
-
this.
|
|
829
|
-
type: "system",
|
|
830
|
-
subtype: "session_created",
|
|
1050
|
+
const createdMsg = this.buildSessionCreatedMessage({
|
|
831
1051
|
sessionId: newId,
|
|
832
1052
|
provider: newSession?.provider ?? "claude",
|
|
833
1053
|
projectPath,
|
|
834
|
-
|
|
835
|
-
|
|
1054
|
+
session: newSession,
|
|
1055
|
+
permissionMode,
|
|
836
1056
|
sourceSessionId: sessionId,
|
|
837
1057
|
});
|
|
1058
|
+
this.broadcast({ ...createdMsg, clearContext: true });
|
|
838
1059
|
this.broadcastSessionList();
|
|
839
1060
|
}
|
|
840
1061
|
else {
|
|
@@ -1027,15 +1248,7 @@ export class BridgeWebSocketServer {
|
|
|
1027
1248
|
}
|
|
1028
1249
|
case "list_recent_sessions": {
|
|
1029
1250
|
const requestId = ++this.recentSessionsRequestId;
|
|
1030
|
-
|
|
1031
|
-
limit: msg.limit,
|
|
1032
|
-
offset: msg.offset,
|
|
1033
|
-
projectPath: msg.projectPath,
|
|
1034
|
-
provider: msg.provider,
|
|
1035
|
-
namedOnly: msg.namedOnly,
|
|
1036
|
-
searchQuery: msg.searchQuery,
|
|
1037
|
-
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
1038
|
-
}).then(({ sessions, hasMore }) => {
|
|
1251
|
+
this.listRecentSessions(msg).then(({ sessions, hasMore }) => {
|
|
1039
1252
|
// Drop stale responses when rapid filter switches cause out-of-order completion
|
|
1040
1253
|
if (requestId !== this.recentSessionsRequestId)
|
|
1041
1254
|
return;
|
|
@@ -1086,6 +1299,16 @@ export class BridgeWebSocketServer {
|
|
|
1086
1299
|
break;
|
|
1087
1300
|
}
|
|
1088
1301
|
const provider = msg.provider ?? "claude";
|
|
1302
|
+
const executionMode = deriveExecutionMode({
|
|
1303
|
+
provider,
|
|
1304
|
+
permissionMode: msg.permissionMode,
|
|
1305
|
+
executionMode: msg.executionMode,
|
|
1306
|
+
});
|
|
1307
|
+
const planMode = derivePlanMode({
|
|
1308
|
+
permissionMode: msg.permissionMode,
|
|
1309
|
+
planMode: msg.planMode,
|
|
1310
|
+
});
|
|
1311
|
+
const legacyPermissionMode = modesToLegacyPermissionMode(provider, executionMode, planMode);
|
|
1089
1312
|
const sessionRefId = msg.sessionId;
|
|
1090
1313
|
// Resume flow: keep past history in SessionInfo and deliver it only
|
|
1091
1314
|
// via get_history(sessionId) to avoid duplicate/missed replay races.
|
|
@@ -1110,29 +1333,28 @@ export class BridgeWebSocketServer {
|
|
|
1110
1333
|
getCodexSessionHistory(sessionRefId).then((pastMessages) => {
|
|
1111
1334
|
const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
1112
1335
|
threadId: sessionRefId,
|
|
1113
|
-
approvalPolicy:
|
|
1336
|
+
approvalPolicy: executionMode === "fullAccess" ? "never" : "on-request",
|
|
1114
1337
|
sandboxMode: sandboxModeToInternal(msg.sandboxMode),
|
|
1115
1338
|
model: msg.model,
|
|
1116
1339
|
modelReasoningEffort: msg.modelReasoningEffort ?? undefined,
|
|
1117
1340
|
networkAccessEnabled: msg.networkAccessEnabled,
|
|
1118
1341
|
webSearchMode: msg.webSearchMode ?? undefined,
|
|
1119
|
-
collaborationMode:
|
|
1342
|
+
collaborationMode: planMode ? "plan" : "default",
|
|
1120
1343
|
});
|
|
1121
1344
|
const createdSession = this.sessionManager.get(sessionId);
|
|
1122
1345
|
void this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId).then(() => {
|
|
1123
|
-
this.send(ws, {
|
|
1124
|
-
type: "system",
|
|
1125
|
-
subtype: "session_created",
|
|
1346
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
1126
1347
|
sessionId,
|
|
1127
1348
|
provider: "codex",
|
|
1128
1349
|
projectPath: effectiveProjectPath,
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1350
|
+
session: createdSession,
|
|
1351
|
+
sandboxMode: createdSession?.codexSettings?.sandboxMode
|
|
1352
|
+
? sandboxModeToExternal(createdSession.codexSettings.sandboxMode)
|
|
1353
|
+
: undefined,
|
|
1354
|
+
permissionMode: legacyPermissionMode,
|
|
1355
|
+
executionMode,
|
|
1356
|
+
planMode,
|
|
1357
|
+
}));
|
|
1136
1358
|
this.broadcastSessionList();
|
|
1137
1359
|
});
|
|
1138
1360
|
this.debugEvents.set(sessionId, []);
|
|
@@ -1169,7 +1391,7 @@ export class BridgeWebSocketServer {
|
|
|
1169
1391
|
getSessionHistory(claudeSessionId).then((pastMessages) => {
|
|
1170
1392
|
const sessionId = this.sessionManager.create(msg.projectPath, {
|
|
1171
1393
|
sessionId: claudeSessionId,
|
|
1172
|
-
permissionMode:
|
|
1394
|
+
permissionMode: legacyPermissionMode,
|
|
1173
1395
|
model: msg.model,
|
|
1174
1396
|
effort: msg.effort,
|
|
1175
1397
|
maxTurns: msg.maxTurns,
|
|
@@ -1182,19 +1404,26 @@ export class BridgeWebSocketServer {
|
|
|
1182
1404
|
const createdSession = this.sessionManager.get(sessionId);
|
|
1183
1405
|
void this.loadAndSetSessionName(createdSession, "claude", msg.projectPath, claudeSessionId).then(() => {
|
|
1184
1406
|
this.send(ws, {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1407
|
+
...this.buildSessionCreatedMessage({
|
|
1408
|
+
sessionId,
|
|
1409
|
+
provider: "claude",
|
|
1410
|
+
projectPath: msg.projectPath,
|
|
1411
|
+
session: createdSession,
|
|
1412
|
+
permissionMode: legacyPermissionMode,
|
|
1413
|
+
executionMode,
|
|
1414
|
+
planMode,
|
|
1415
|
+
sandboxMode: msg.sandboxMode,
|
|
1416
|
+
...(cached
|
|
1417
|
+
? {
|
|
1418
|
+
slashCommands: cached.slashCommands,
|
|
1419
|
+
skills: cached.skills,
|
|
1420
|
+
...(cached.skillMetadata
|
|
1421
|
+
? { skillMetadata: cached.skillMetadata }
|
|
1422
|
+
: {}),
|
|
1423
|
+
}
|
|
1424
|
+
: {}),
|
|
1425
|
+
}),
|
|
1188
1426
|
claudeSessionId,
|
|
1189
|
-
provider: "claude",
|
|
1190
|
-
projectPath: msg.projectPath,
|
|
1191
|
-
...(msg.permissionMode ? { permissionMode: msg.permissionMode } : {}),
|
|
1192
|
-
...(msg.sandboxMode ? { sandboxMode: msg.sandboxMode } : {}),
|
|
1193
|
-
...(cached ? { slashCommands: cached.slashCommands, skills: cached.skills, ...(cached.skillMetadata ? { skillMetadata: cached.skillMetadata } : {}) } : {}),
|
|
1194
|
-
...(createdSession?.worktreePath ? {
|
|
1195
|
-
worktreePath: createdSession.worktreePath,
|
|
1196
|
-
worktreeBranch: createdSession.worktreeBranch,
|
|
1197
|
-
} : {}),
|
|
1198
1427
|
});
|
|
1199
1428
|
this.broadcastSessionList();
|
|
1200
1429
|
});
|
|
@@ -1504,15 +1733,14 @@ export class BridgeWebSocketServer {
|
|
|
1504
1733
|
// Notify the new session ID
|
|
1505
1734
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1506
1735
|
const rewindPermMode = newSession?.process instanceof SdkProcess ? newSession.process.permissionMode : undefined;
|
|
1507
|
-
this.send(ws, {
|
|
1508
|
-
type: "system",
|
|
1509
|
-
subtype: "session_created",
|
|
1736
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
1510
1737
|
sessionId: newSessionId,
|
|
1511
1738
|
provider: newSession?.provider ?? "claude",
|
|
1512
1739
|
projectPath: newSession?.projectPath ?? "",
|
|
1513
|
-
|
|
1740
|
+
session: newSession,
|
|
1741
|
+
permissionMode: rewindPermMode,
|
|
1514
1742
|
sourceSessionId: msg.sessionId,
|
|
1515
|
-
});
|
|
1743
|
+
}));
|
|
1516
1744
|
this.sendSessionList(ws);
|
|
1517
1745
|
});
|
|
1518
1746
|
}
|
|
@@ -1532,15 +1760,14 @@ export class BridgeWebSocketServer {
|
|
|
1532
1760
|
this.send(ws, { type: "rewind_result", success: true, mode: "both" });
|
|
1533
1761
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1534
1762
|
const rewindPermMode2 = newSession?.process instanceof SdkProcess ? newSession.process.permissionMode : undefined;
|
|
1535
|
-
this.send(ws, {
|
|
1536
|
-
type: "system",
|
|
1537
|
-
subtype: "session_created",
|
|
1763
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
1538
1764
|
sessionId: newSessionId,
|
|
1539
1765
|
provider: newSession?.provider ?? "claude",
|
|
1540
1766
|
projectPath: newSession?.projectPath ?? "",
|
|
1541
|
-
|
|
1767
|
+
session: newSession,
|
|
1768
|
+
permissionMode: rewindPermMode2,
|
|
1542
1769
|
sourceSessionId: msg.sessionId,
|
|
1543
|
-
});
|
|
1770
|
+
}));
|
|
1544
1771
|
this.sendSessionList(ws);
|
|
1545
1772
|
});
|
|
1546
1773
|
}
|
|
@@ -1794,6 +2021,77 @@ export class BridgeWebSocketServer {
|
|
|
1794
2021
|
}
|
|
1795
2022
|
}
|
|
1796
2023
|
}
|
|
2024
|
+
async listRecentSessions(msg) {
|
|
2025
|
+
if (msg.provider === "codex") {
|
|
2026
|
+
try {
|
|
2027
|
+
return await this.listRecentCodexThreads(msg);
|
|
2028
|
+
}
|
|
2029
|
+
catch (err) {
|
|
2030
|
+
console.warn(`[ws] Codex thread/list failed, falling back to rollout scan: ${err}`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return getAllRecentSessions({
|
|
2034
|
+
limit: msg.limit,
|
|
2035
|
+
offset: msg.offset,
|
|
2036
|
+
projectPath: msg.projectPath,
|
|
2037
|
+
provider: msg.provider,
|
|
2038
|
+
namedOnly: msg.namedOnly,
|
|
2039
|
+
searchQuery: msg.searchQuery,
|
|
2040
|
+
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
getActiveCodexProcess() {
|
|
2044
|
+
const summary = this.sessionManager.list().find((session) => session.provider === "codex");
|
|
2045
|
+
if (!summary)
|
|
2046
|
+
return null;
|
|
2047
|
+
const session = this.sessionManager.get(summary.id);
|
|
2048
|
+
return session?.provider === "codex" ? session.process : null;
|
|
2049
|
+
}
|
|
2050
|
+
async listRecentCodexThreads(msg) {
|
|
2051
|
+
const limit = msg.limit ?? 20;
|
|
2052
|
+
const offset = msg.offset ?? 0;
|
|
2053
|
+
const process = this.getActiveCodexProcess() ?? await this.createStandaloneCodexProcess(msg.projectPath);
|
|
2054
|
+
const isStandalone = process !== this.getActiveCodexProcess();
|
|
2055
|
+
try {
|
|
2056
|
+
const result = await process.listThreads({
|
|
2057
|
+
limit: limit + offset,
|
|
2058
|
+
cwd: msg.projectPath,
|
|
2059
|
+
searchTerm: msg.searchQuery,
|
|
2060
|
+
});
|
|
2061
|
+
const archivedIds = this.archiveStore.archivedIds();
|
|
2062
|
+
const indexedSessions = await getAllRecentSessions({
|
|
2063
|
+
provider: "codex",
|
|
2064
|
+
projectPath: msg.projectPath,
|
|
2065
|
+
archivedSessionIds: archivedIds,
|
|
2066
|
+
});
|
|
2067
|
+
const indexedById = new Map(indexedSessions.sessions.map((session) => [
|
|
2068
|
+
session.sessionId,
|
|
2069
|
+
{
|
|
2070
|
+
codexSettings: session.codexSettings,
|
|
2071
|
+
resumeCwd: session.resumeCwd,
|
|
2072
|
+
},
|
|
2073
|
+
]));
|
|
2074
|
+
const sessions = result.data
|
|
2075
|
+
.filter((thread) => !archivedIds.has(thread.id))
|
|
2076
|
+
.filter((thread) => !msg.namedOnly || !!thread.name)
|
|
2077
|
+
.slice(offset, offset + limit)
|
|
2078
|
+
.map((thread) => codexThreadToRecentSession(thread, indexedById.get(thread.id)));
|
|
2079
|
+
return {
|
|
2080
|
+
sessions,
|
|
2081
|
+
hasMore: result.nextCursor != null,
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
finally {
|
|
2085
|
+
if (isStandalone) {
|
|
2086
|
+
process.stop();
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
async createStandaloneCodexProcess(projectPath) {
|
|
2091
|
+
const proc = new CodexProcess();
|
|
2092
|
+
await proc.initializeOnly(projectPath ?? process.cwd());
|
|
2093
|
+
return proc;
|
|
2094
|
+
}
|
|
1797
2095
|
/** Extract a short project label from the full projectPath (last directory name). */
|
|
1798
2096
|
projectLabel(sessionId) {
|
|
1799
2097
|
const session = this.sessionManager.get(sessionId);
|