@ccpocket/bridge 1.80.2 → 1.81.1
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/dist/auto-rename.js +1 -0
- package/dist/auto-rename.js.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +47 -0
- package/dist/parser.js +58 -1
- package/dist/parser.js.map +1 -1
- package/dist/prompt-history-store.d.ts +8 -1
- package/dist/prompt-history-store.js +18 -6
- package/dist/prompt-history-store.js.map +1 -1
- package/dist/recording-store.d.ts +2 -0
- package/dist/recording-store.js.map +1 -1
- package/dist/sdk-process.d.ts +2 -0
- package/dist/sdk-process.js +3 -0
- package/dist/sdk-process.js.map +1 -1
- package/dist/sessions-index.d.ts +6 -0
- package/dist/sessions-index.js +3 -0
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +19 -1
- package/dist/websocket.js +513 -19
- package/dist/websocket.js.map +1 -1
- package/dist/workspace-store.d.ts +49 -0
- package/dist/workspace-store.js +212 -0
- package/dist/workspace-store.js.map +1 -0
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -104,6 +104,7 @@ const OPT_IN_SERVER_MESSAGES = new Set([
|
|
|
104
104
|
"goal_state",
|
|
105
105
|
"guardian_approval",
|
|
106
106
|
"prompt_history_status",
|
|
107
|
+
"projects",
|
|
107
108
|
"push_registration_result",
|
|
108
109
|
]);
|
|
109
110
|
function isRecord(value) {
|
|
@@ -517,6 +518,8 @@ export class BridgeWebSocketServer {
|
|
|
517
518
|
uploadStore;
|
|
518
519
|
galleryStore;
|
|
519
520
|
projectHistory;
|
|
521
|
+
workspaceStore;
|
|
522
|
+
pendingSessionWorkspaces = new Map();
|
|
520
523
|
debugTraceStore;
|
|
521
524
|
recordingStore;
|
|
522
525
|
worktreeStore;
|
|
@@ -570,7 +573,7 @@ export class BridgeWebSocketServer {
|
|
|
570
573
|
pendingClaudeResumeInputs = new WeakMap();
|
|
571
574
|
resumeOperations = new Map();
|
|
572
575
|
constructor(options) {
|
|
573
|
-
const { server, apiKey, allowedDirs, imageStore, mediaStore, uploadStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, fileUploadMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
|
|
576
|
+
const { server, apiKey, allowedDirs, imageStore, mediaStore, uploadStore, galleryStore, projectHistory, workspaceStore, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, fileUploadMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
|
|
574
577
|
this.apiKey = apiKey ?? null;
|
|
575
578
|
this.allowedDirs = allowedDirs ?? [];
|
|
576
579
|
this.imageStore = imageStore ?? null;
|
|
@@ -578,6 +581,7 @@ export class BridgeWebSocketServer {
|
|
|
578
581
|
this.uploadStore = uploadStore ?? null;
|
|
579
582
|
this.galleryStore = galleryStore ?? null;
|
|
580
583
|
this.projectHistory = projectHistory ?? null;
|
|
584
|
+
this.workspaceStore = workspaceStore ?? null;
|
|
581
585
|
this.debugTraceStore = debugTraceStore ?? new DebugTraceStore();
|
|
582
586
|
this.recordingStore = recordingStore ?? null;
|
|
583
587
|
this.worktreeStore = new WorktreeStore();
|
|
@@ -619,7 +623,10 @@ export class BridgeWebSocketServer {
|
|
|
619
623
|
const info = this.galleryStore.metaToInfo(meta);
|
|
620
624
|
this.broadcast({ type: "gallery_new_image", image: info });
|
|
621
625
|
}
|
|
622
|
-
}, this.worktreeStore, () =>
|
|
626
|
+
}, this.worktreeStore, (sessionId) => {
|
|
627
|
+
void this.persistPendingSessionWorkspace(sessionId);
|
|
628
|
+
this.broadcastSessionList();
|
|
629
|
+
});
|
|
623
630
|
this.wss.on("connection", (ws, req) => {
|
|
624
631
|
// API key authentication
|
|
625
632
|
if (this.apiKey) {
|
|
@@ -856,12 +863,16 @@ export class BridgeWebSocketServer {
|
|
|
856
863
|
const derivedCodexSettings = provider === "codex"
|
|
857
864
|
? withDerivedCodexPermissionsMode(session?.codexSettings)
|
|
858
865
|
: session?.codexSettings;
|
|
866
|
+
const workspace = session
|
|
867
|
+
? this.workspaceForRuntimeSession(session)
|
|
868
|
+
: undefined;
|
|
859
869
|
const msg = {
|
|
860
870
|
type: "system",
|
|
861
871
|
subtype: "session_created",
|
|
862
872
|
sessionId,
|
|
863
873
|
provider,
|
|
864
874
|
projectPath,
|
|
875
|
+
...(workspace ? { workspace } : {}),
|
|
865
876
|
...(permissionMode
|
|
866
877
|
? {
|
|
867
878
|
permissionMode: permissionMode,
|
|
@@ -1069,6 +1080,7 @@ export class BridgeWebSocketServer {
|
|
|
1069
1080
|
}
|
|
1070
1081
|
const projectPath = session.projectPath;
|
|
1071
1082
|
const codexSettings = session.codexSettings;
|
|
1083
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
1072
1084
|
const worktreeOpts = session.worktreePath
|
|
1073
1085
|
? {
|
|
1074
1086
|
existingWorktreePath: session.worktreePath,
|
|
@@ -1086,6 +1098,7 @@ export class BridgeWebSocketServer {
|
|
|
1086
1098
|
...(codexSettings ?? {}),
|
|
1087
1099
|
threadId,
|
|
1088
1100
|
}));
|
|
1101
|
+
this.attachWorkspaceToRuntimeSession(newSessionId, workspace);
|
|
1089
1102
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1090
1103
|
this.send(ws, {
|
|
1091
1104
|
type: "rewind_result",
|
|
@@ -1152,6 +1165,7 @@ export class BridgeWebSocketServer {
|
|
|
1152
1165
|
}
|
|
1153
1166
|
const projectPath = session.projectPath;
|
|
1154
1167
|
const codexSettings = session.codexSettings;
|
|
1168
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
1155
1169
|
const worktreeOpts = session.worktreePath
|
|
1156
1170
|
? {
|
|
1157
1171
|
existingWorktreePath: session.worktreePath,
|
|
@@ -1174,6 +1188,7 @@ export class BridgeWebSocketServer {
|
|
|
1174
1188
|
...(codexSettings ?? {}),
|
|
1175
1189
|
threadId: forkedThreadId,
|
|
1176
1190
|
}));
|
|
1191
|
+
this.attachWorkspaceToRuntimeSession(newSessionId, workspace);
|
|
1177
1192
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1178
1193
|
this.send(ws, this.buildSessionCreatedMessage({
|
|
1179
1194
|
sessionId: newSessionId,
|
|
@@ -1852,7 +1867,7 @@ export class BridgeWebSocketServer {
|
|
|
1852
1867
|
// Send session list and project history on connect
|
|
1853
1868
|
this.refreshConnectionMetadata();
|
|
1854
1869
|
this.sendSessionList(ws);
|
|
1855
|
-
const projects = this.
|
|
1870
|
+
const projects = this.legacyProjectHistoryProjects();
|
|
1856
1871
|
this.send(ws, { type: "project_history", projects });
|
|
1857
1872
|
ws.on("message", (data) => {
|
|
1858
1873
|
if (this.rejectedProtocolClients.has(ws))
|
|
@@ -1954,7 +1969,32 @@ export class BridgeWebSocketServer {
|
|
|
1954
1969
|
}
|
|
1955
1970
|
switch (msg.type) {
|
|
1956
1971
|
case "start": {
|
|
1957
|
-
|
|
1972
|
+
let projectPath = resolvePlatformPath(msg.projectPath, this.platform);
|
|
1973
|
+
let resolvedWorkspace;
|
|
1974
|
+
if (msg.projectId) {
|
|
1975
|
+
const project = this.workspaceStore?.getProject(msg.projectId);
|
|
1976
|
+
if (!project) {
|
|
1977
|
+
this.send(ws, {
|
|
1978
|
+
type: "error",
|
|
1979
|
+
requestId: msg.requestId,
|
|
1980
|
+
errorCode: "project_not_found",
|
|
1981
|
+
message: `Project not found: ${msg.projectId}`,
|
|
1982
|
+
});
|
|
1983
|
+
break;
|
|
1984
|
+
}
|
|
1985
|
+
const normalized = this.normalizeWorkspaceRoots(project.rootPaths);
|
|
1986
|
+
if (normalized.deniedRoot || !normalized.roots) {
|
|
1987
|
+
this.send(ws, this.buildPathNotAllowedError(normalized.deniedRoot ?? project.rootPaths[0]));
|
|
1988
|
+
break;
|
|
1989
|
+
}
|
|
1990
|
+
projectPath = normalized.roots[0];
|
|
1991
|
+
resolvedWorkspace = {
|
|
1992
|
+
kind: "project",
|
|
1993
|
+
projectId: project.id,
|
|
1994
|
+
projectName: project.name,
|
|
1995
|
+
rootPaths: normalized.roots,
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1958
1998
|
if (!this.isPathAllowed(projectPath)) {
|
|
1959
1999
|
this.send(ws, {
|
|
1960
2000
|
...this.buildPathNotAllowedError(msg.projectPath),
|
|
@@ -2012,9 +2052,9 @@ export class BridgeWebSocketServer {
|
|
|
2012
2052
|
break;
|
|
2013
2053
|
}
|
|
2014
2054
|
}
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2055
|
+
const requestedAdditionalRoots = resolvedWorkspace?.rootPaths.slice(1) ??
|
|
2056
|
+
msg.additionalWritableRoots;
|
|
2057
|
+
const additionalWritableRoots = this.normalizeAdditionalWritableRoots(requestedAdditionalRoots, projectPath);
|
|
2018
2058
|
if (additionalWritableRoots.deniedRoot) {
|
|
2019
2059
|
this.send(ws, {
|
|
2020
2060
|
...this.buildPathNotAllowedError(additionalWritableRoots.deniedRoot),
|
|
@@ -2041,6 +2081,7 @@ export class BridgeWebSocketServer {
|
|
|
2041
2081
|
...(msg.sandboxMode
|
|
2042
2082
|
? { sandboxEnabled: msg.sandboxMode === "on" }
|
|
2043
2083
|
: {}),
|
|
2084
|
+
additionalDirectories: additionalWritableRoots.roots,
|
|
2044
2085
|
},
|
|
2045
2086
|
worktreeOptions: {
|
|
2046
2087
|
useWorktree: msg.useWorktree,
|
|
@@ -2086,6 +2127,10 @@ export class BridgeWebSocketServer {
|
|
|
2086
2127
|
usedFallback: false,
|
|
2087
2128
|
};
|
|
2088
2129
|
const createdSession = this.sessionManager.get(sessionId);
|
|
2130
|
+
if (resolvedWorkspace) {
|
|
2131
|
+
this.pendingSessionWorkspaces.set(sessionId, resolvedWorkspace);
|
|
2132
|
+
void this.persistPendingSessionWorkspace(sessionId);
|
|
2133
|
+
}
|
|
2089
2134
|
const cached = this.sessionManager.getCachedCommands(provider, createdSession?.worktreePath ?? projectPath);
|
|
2090
2135
|
// Load saved session name from CLI storage (for resumed sessions)
|
|
2091
2136
|
void this.loadAndSetSessionName(createdSession, provider, projectPath, msg.sessionId).then(() => {
|
|
@@ -2150,9 +2195,17 @@ export class BridgeWebSocketServer {
|
|
|
2150
2195
|
this.recordingStore?.saveMeta(sessionId, {
|
|
2151
2196
|
bridgeSessionId: sessionId,
|
|
2152
2197
|
projectPath,
|
|
2198
|
+
...(resolvedWorkspace?.projectId
|
|
2199
|
+
? { projectId: resolvedWorkspace.projectId }
|
|
2200
|
+
: {}),
|
|
2201
|
+
...(resolvedWorkspace?.projectName
|
|
2202
|
+
? { projectName: resolvedWorkspace.projectName }
|
|
2203
|
+
: {}),
|
|
2153
2204
|
createdAt: new Date().toISOString(),
|
|
2154
2205
|
});
|
|
2155
|
-
|
|
2206
|
+
if (!resolvedWorkspace) {
|
|
2207
|
+
this.projectHistory?.addProject(projectPath);
|
|
2208
|
+
}
|
|
2156
2209
|
}
|
|
2157
2210
|
catch (err) {
|
|
2158
2211
|
console.error(`[ws] Failed to start session:`, err);
|
|
@@ -2805,6 +2858,7 @@ export class BridgeWebSocketServer {
|
|
|
2805
2858
|
const worktreePath = session.worktreePath;
|
|
2806
2859
|
const worktreeBranch = session.worktreeBranch;
|
|
2807
2860
|
const sessionName = session.name;
|
|
2861
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
2808
2862
|
this.destroySession(oldSessionId);
|
|
2809
2863
|
console.log(`[ws] Permission mode change: destroyed session ${oldSessionId}`);
|
|
2810
2864
|
const hasUserMessages = session.history?.some((m) => m.type === "user_input" || m.type === "assistant") ||
|
|
@@ -2822,8 +2876,10 @@ export class BridgeWebSocketServer {
|
|
|
2822
2876
|
serviceTier: oldSettings.serviceTier,
|
|
2823
2877
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
2824
2878
|
webSearchMode: oldSettings.webSearchMode,
|
|
2879
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
2825
2880
|
collaborationMode: newCollaboration,
|
|
2826
2881
|
}));
|
|
2882
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
2827
2883
|
const newSession = this.sessionManager.get(newId);
|
|
2828
2884
|
if (newSession && sessionName)
|
|
2829
2885
|
newSession.name = sessionName;
|
|
@@ -2883,8 +2939,10 @@ export class BridgeWebSocketServer {
|
|
|
2883
2939
|
serviceTier: oldSettings.serviceTier,
|
|
2884
2940
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
2885
2941
|
webSearchMode: oldSettings.webSearchMode,
|
|
2942
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
2886
2943
|
collaborationMode: newCollaboration,
|
|
2887
2944
|
}));
|
|
2945
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
2888
2946
|
const newSession = this.sessionManager.get(newId);
|
|
2889
2947
|
if (newSession && sessionName) {
|
|
2890
2948
|
newSession.name = sessionName;
|
|
@@ -3174,6 +3232,7 @@ export class BridgeWebSocketServer {
|
|
|
3174
3232
|
const sessionName = session.name;
|
|
3175
3233
|
const permissionMode = session.process.permissionMode;
|
|
3176
3234
|
const model = session.process.model;
|
|
3235
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
3177
3236
|
this.destroySession(oldSessionId);
|
|
3178
3237
|
console.log(`[ws] Claude sandbox change: destroyed session ${oldSessionId}`);
|
|
3179
3238
|
const newId = this.sessionManager.create(projectPath, {
|
|
@@ -3181,9 +3240,11 @@ export class BridgeWebSocketServer {
|
|
|
3181
3240
|
permissionMode,
|
|
3182
3241
|
model,
|
|
3183
3242
|
sandboxEnabled: newEnabled,
|
|
3243
|
+
additionalDirectories: workspace?.rootPaths.slice(1),
|
|
3184
3244
|
}, undefined, worktreePath
|
|
3185
3245
|
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
3186
3246
|
: undefined, "claude");
|
|
3247
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3187
3248
|
const newSession = this.sessionManager.get(newId);
|
|
3188
3249
|
if (newSession && sessionName)
|
|
3189
3250
|
newSession.name = sessionName;
|
|
@@ -3227,6 +3288,7 @@ export class BridgeWebSocketServer {
|
|
|
3227
3288
|
const sessionName = session.name;
|
|
3228
3289
|
const collaborationMode = session.process
|
|
3229
3290
|
.collaborationMode;
|
|
3291
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
3230
3292
|
const executionMode = oldSettings.approvalPolicy === "never" ? "fullAccess" : "default";
|
|
3231
3293
|
const planMode = collaborationMode === "plan";
|
|
3232
3294
|
const legacyPermissionMode = modesToLegacyPermissionMode("codex", executionMode, planMode);
|
|
@@ -3255,8 +3317,10 @@ export class BridgeWebSocketServer {
|
|
|
3255
3317
|
serviceTier: oldSettings.serviceTier,
|
|
3256
3318
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
3257
3319
|
webSearchMode: oldSettings.webSearchMode,
|
|
3320
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
3258
3321
|
collaborationMode,
|
|
3259
3322
|
}));
|
|
3323
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3260
3324
|
const newSession = this.sessionManager.get(newId);
|
|
3261
3325
|
if (newSession && sessionName)
|
|
3262
3326
|
newSession.name = sessionName;
|
|
@@ -3309,8 +3373,10 @@ export class BridgeWebSocketServer {
|
|
|
3309
3373
|
serviceTier: oldSettings.serviceTier,
|
|
3310
3374
|
networkAccessEnabled: oldSettings.networkAccessEnabled,
|
|
3311
3375
|
webSearchMode: oldSettings.webSearchMode,
|
|
3376
|
+
additionalWritableRoots: oldSettings.additionalWritableRoots,
|
|
3312
3377
|
collaborationMode,
|
|
3313
3378
|
}));
|
|
3379
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3314
3380
|
// Restore session name
|
|
3315
3381
|
const newSession = this.sessionManager.get(newId);
|
|
3316
3382
|
if (newSession && sessionName) {
|
|
@@ -3377,6 +3443,7 @@ export class BridgeWebSocketServer {
|
|
|
3377
3443
|
const permissionMode = sdkProc.permissionMode;
|
|
3378
3444
|
const worktreePath = session.worktreePath;
|
|
3379
3445
|
const worktreeBranch = session.worktreeBranch;
|
|
3446
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
3380
3447
|
this.destroySession(sessionId);
|
|
3381
3448
|
console.log(`[ws] Clear context: destroyed session ${sessionId}`);
|
|
3382
3449
|
const newId = this.sessionManager.create(projectPath, {
|
|
@@ -3388,9 +3455,11 @@ export class BridgeWebSocketServer {
|
|
|
3388
3455
|
: {}),
|
|
3389
3456
|
permissionMode,
|
|
3390
3457
|
initialInput: planText || undefined,
|
|
3458
|
+
additionalDirectories: workspace?.rootPaths.slice(1),
|
|
3391
3459
|
}, undefined, worktreePath
|
|
3392
3460
|
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
3393
3461
|
: undefined);
|
|
3462
|
+
this.attachWorkspaceToRuntimeSession(newId, workspace);
|
|
3394
3463
|
console.log(`[ws] Clear context: created new session ${newId} (CLI session: ${claudeSessionId ?? "new"})`);
|
|
3395
3464
|
// Notify all clients. Broadcast is used so reconnecting clients also receive it.
|
|
3396
3465
|
const newSession = this.sessionManager.get(newId);
|
|
@@ -3576,7 +3645,7 @@ export class BridgeWebSocketServer {
|
|
|
3576
3645
|
this.send(ws, {
|
|
3577
3646
|
type: "session_context",
|
|
3578
3647
|
sessionId: msg.sessionId,
|
|
3579
|
-
context: { ...context },
|
|
3648
|
+
context: { ...this.runtimeSessionWithWorkspace(context) },
|
|
3580
3649
|
});
|
|
3581
3650
|
}
|
|
3582
3651
|
else {
|
|
@@ -3830,6 +3899,8 @@ export class BridgeWebSocketServer {
|
|
|
3830
3899
|
limit: msg.limit,
|
|
3831
3900
|
offset: msg.offset,
|
|
3832
3901
|
projectPath: msg.projectPath,
|
|
3902
|
+
projectId: msg.projectId,
|
|
3903
|
+
workspaceKind: msg.workspaceKind,
|
|
3833
3904
|
requestScope: msg.requestScope,
|
|
3834
3905
|
requestId: msg.requestId,
|
|
3835
3906
|
});
|
|
@@ -3844,6 +3915,8 @@ export class BridgeWebSocketServer {
|
|
|
3844
3915
|
message: `Failed to list recent sessions: ${err}`,
|
|
3845
3916
|
errorCode: "recent_sessions_failed",
|
|
3846
3917
|
path: msg.projectPath,
|
|
3918
|
+
projectId: msg.projectId,
|
|
3919
|
+
workspaceKind: msg.workspaceKind,
|
|
3847
3920
|
requestId: msg.requestId,
|
|
3848
3921
|
requestScope: msg.requestScope,
|
|
3849
3922
|
offset: msg.offset,
|
|
@@ -3904,9 +3977,58 @@ export class BridgeWebSocketServer {
|
|
|
3904
3977
|
}
|
|
3905
3978
|
case "resume_session": {
|
|
3906
3979
|
const resumeStartedAt = Date.now();
|
|
3907
|
-
console.log(`[ws] resume_session: sessionId=${msg.sessionId} projectPath=${msg.projectPath} provider=${msg.provider ?? "claude"}`);
|
|
3908
|
-
const resumeProjectPath = resolvePlatformPath(msg.projectPath, this.platform);
|
|
3909
3980
|
const provider = msg.provider ?? "claude";
|
|
3981
|
+
const storedAssignment = this.workspaceStore?.getAssignment(provider, msg.sessionId);
|
|
3982
|
+
const requestedProject = !storedAssignment && msg.projectId
|
|
3983
|
+
? this.workspaceStore?.getProject(msg.projectId)
|
|
3984
|
+
: undefined;
|
|
3985
|
+
if (!storedAssignment && msg.projectId && !requestedProject) {
|
|
3986
|
+
this.sendResumeFailed(ws, {
|
|
3987
|
+
provider,
|
|
3988
|
+
sourceSessionId: msg.sessionId,
|
|
3989
|
+
projectPath: msg.projectPath,
|
|
3990
|
+
resumeRequestId: msg.resumeRequestId,
|
|
3991
|
+
});
|
|
3992
|
+
this.send(ws, {
|
|
3993
|
+
type: "error",
|
|
3994
|
+
sessionId: msg.sessionId,
|
|
3995
|
+
requestId: msg.resumeRequestId,
|
|
3996
|
+
errorCode: "project_not_found",
|
|
3997
|
+
message: `Project not found: ${msg.projectId}`,
|
|
3998
|
+
});
|
|
3999
|
+
break;
|
|
4000
|
+
}
|
|
4001
|
+
const assignedProject = storedAssignment?.projectId
|
|
4002
|
+
? this.workspaceStore?.getProject(storedAssignment.projectId)
|
|
4003
|
+
: undefined;
|
|
4004
|
+
const resolvedResumeProjectName = assignedProject?.name ?? storedAssignment?.projectName;
|
|
4005
|
+
const resumeRoots = storedAssignment?.rootPaths ?? requestedProject?.rootPaths;
|
|
4006
|
+
console.log(`[ws] resume_session: sessionId=${msg.sessionId} projectPath=${msg.projectPath} provider=${msg.provider ?? "claude"}`);
|
|
4007
|
+
// A stored assignment supplies the root snapshot and secondary roots,
|
|
4008
|
+
// while the recent-session path may be a worktree cwd that must win.
|
|
4009
|
+
const resumeProjectPath = resolvePlatformPath(storedAssignment
|
|
4010
|
+
? msg.projectPath
|
|
4011
|
+
: (resumeRoots?.[0] ?? msg.projectPath), this.platform);
|
|
4012
|
+
const resumeAdditionalRoots = resumeRoots?.slice(1) ?? msg.additionalWritableRoots;
|
|
4013
|
+
const resolvedResumeWorkspace = storedAssignment
|
|
4014
|
+
? {
|
|
4015
|
+
kind: storedAssignment.kind,
|
|
4016
|
+
...(storedAssignment.projectId
|
|
4017
|
+
? { projectId: storedAssignment.projectId }
|
|
4018
|
+
: {}),
|
|
4019
|
+
...(resolvedResumeProjectName
|
|
4020
|
+
? { projectName: resolvedResumeProjectName }
|
|
4021
|
+
: {}),
|
|
4022
|
+
rootPaths: storedAssignment.rootPaths,
|
|
4023
|
+
}
|
|
4024
|
+
: requestedProject
|
|
4025
|
+
? {
|
|
4026
|
+
kind: "project",
|
|
4027
|
+
projectId: requestedProject.id,
|
|
4028
|
+
projectName: requestedProject.name,
|
|
4029
|
+
rootPaths: requestedProject.rootPaths,
|
|
4030
|
+
}
|
|
4031
|
+
: undefined;
|
|
3910
4032
|
if (!this.isPathAllowed(resumeProjectPath)) {
|
|
3911
4033
|
this.sendResumeFailed(ws, {
|
|
3912
4034
|
provider,
|
|
@@ -3917,6 +4039,13 @@ export class BridgeWebSocketServer {
|
|
|
3917
4039
|
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
3918
4040
|
break;
|
|
3919
4041
|
}
|
|
4042
|
+
if (resolvedResumeWorkspace &&
|
|
4043
|
+
!storedAssignment &&
|
|
4044
|
+
this.workspaceStore) {
|
|
4045
|
+
void this.workspaceStore.assignSession(provider, msg.sessionId, resolvedResumeWorkspace).catch((error) => {
|
|
4046
|
+
console.error("[workspace] Failed to persist resume assignment:", error);
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
3920
4049
|
const normalizedCodexPermissionsMode = provider === "codex"
|
|
3921
4050
|
? normalizeCodexPermissionsMode(msg.codexPermissionsMode)
|
|
3922
4051
|
: undefined;
|
|
@@ -3960,7 +4089,7 @@ export class BridgeWebSocketServer {
|
|
|
3960
4089
|
const effectiveProfile = msg.profile
|
|
3961
4090
|
? await this.resolveCodexResumeProfile(msg.profile, sessionRefId, effectiveProjectPath)
|
|
3962
4091
|
: undefined;
|
|
3963
|
-
const additionalWritableRoots = this.normalizeAdditionalWritableRoots(
|
|
4092
|
+
const additionalWritableRoots = this.normalizeAdditionalWritableRoots(resumeAdditionalRoots, effectiveProjectPath);
|
|
3964
4093
|
if (additionalWritableRoots.deniedRoot) {
|
|
3965
4094
|
this.sendResumeFailed(ws, {
|
|
3966
4095
|
provider,
|
|
@@ -4098,7 +4227,9 @@ export class BridgeWebSocketServer {
|
|
|
4098
4227
|
type: "session_resumed",
|
|
4099
4228
|
detail: `provider=codex thread=${sessionRefId}`,
|
|
4100
4229
|
});
|
|
4101
|
-
|
|
4230
|
+
if (!resolvedResumeWorkspace) {
|
|
4231
|
+
this.projectHistory?.addProject(effectiveProjectPath);
|
|
4232
|
+
}
|
|
4102
4233
|
console.info(formatResumePerformanceLog({
|
|
4103
4234
|
provider: "codex",
|
|
4104
4235
|
sourceSessionId: sessionRefId,
|
|
@@ -4136,6 +4267,17 @@ export class BridgeWebSocketServer {
|
|
|
4136
4267
|
break;
|
|
4137
4268
|
}
|
|
4138
4269
|
const claudeSessionId = sessionRefId;
|
|
4270
|
+
const additionalDirectories = this.normalizeAdditionalWritableRoots(resumeAdditionalRoots, resumeProjectPath);
|
|
4271
|
+
if (additionalDirectories.deniedRoot) {
|
|
4272
|
+
this.sendResumeFailed(ws, {
|
|
4273
|
+
provider,
|
|
4274
|
+
sourceSessionId: sessionRefId,
|
|
4275
|
+
projectPath: resumeProjectPath,
|
|
4276
|
+
resumeRequestId: msg.resumeRequestId,
|
|
4277
|
+
});
|
|
4278
|
+
this.send(ws, this.buildPathNotAllowedError(additionalDirectories.deniedRoot));
|
|
4279
|
+
break;
|
|
4280
|
+
}
|
|
4139
4281
|
// Look up worktree mapping for this Claude session
|
|
4140
4282
|
const wtMapping = this.worktreeStore.get(claudeSessionId);
|
|
4141
4283
|
let worktreeOpts;
|
|
@@ -4190,6 +4332,7 @@ export class BridgeWebSocketServer {
|
|
|
4190
4332
|
...(msg.sandboxMode
|
|
4191
4333
|
? { sandboxEnabled: msg.sandboxMode === "on" }
|
|
4192
4334
|
: {}),
|
|
4335
|
+
additionalDirectories: additionalDirectories.roots,
|
|
4193
4336
|
},
|
|
4194
4337
|
pastMessages,
|
|
4195
4338
|
worktreeOptions: worktreeOpts,
|
|
@@ -4260,7 +4403,9 @@ export class BridgeWebSocketServer {
|
|
|
4260
4403
|
type: "session_resumed",
|
|
4261
4404
|
detail: `provider=claude session=${claudeSessionId}`,
|
|
4262
4405
|
});
|
|
4263
|
-
|
|
4406
|
+
if (!resolvedResumeWorkspace) {
|
|
4407
|
+
this.projectHistory?.addProject(resumeProjectPath);
|
|
4408
|
+
}
|
|
4264
4409
|
})
|
|
4265
4410
|
.catch((err) => {
|
|
4266
4411
|
if (!historyLoaded) {
|
|
@@ -4347,16 +4492,104 @@ export class BridgeWebSocketServer {
|
|
|
4347
4492
|
break;
|
|
4348
4493
|
}
|
|
4349
4494
|
case "list_project_history": {
|
|
4350
|
-
const projects = this.
|
|
4495
|
+
const projects = this.legacyProjectHistoryProjects();
|
|
4351
4496
|
this.send(ws, { type: "project_history", projects });
|
|
4352
4497
|
break;
|
|
4353
4498
|
}
|
|
4354
4499
|
case "remove_project_history": {
|
|
4355
4500
|
this.projectHistory?.removeProject(msg.projectPath);
|
|
4356
|
-
const projects = this.
|
|
4501
|
+
const projects = this.legacyProjectHistoryProjects();
|
|
4357
4502
|
this.send(ws, { type: "project_history", projects });
|
|
4358
4503
|
break;
|
|
4359
4504
|
}
|
|
4505
|
+
case "list_projects": {
|
|
4506
|
+
this.send(ws, this.projectsMessage(msg.requestId));
|
|
4507
|
+
break;
|
|
4508
|
+
}
|
|
4509
|
+
case "create_project": {
|
|
4510
|
+
if (!this.workspaceStore) {
|
|
4511
|
+
this.send(ws, {
|
|
4512
|
+
type: "error",
|
|
4513
|
+
requestId: msg.requestId,
|
|
4514
|
+
errorCode: "projects_unavailable",
|
|
4515
|
+
message: "Project storage is unavailable",
|
|
4516
|
+
});
|
|
4517
|
+
break;
|
|
4518
|
+
}
|
|
4519
|
+
const normalized = this.normalizeWorkspaceRoots(msg.rootPaths);
|
|
4520
|
+
if (normalized.deniedRoot || !normalized.roots) {
|
|
4521
|
+
this.send(ws, this.buildPathNotAllowedError(normalized.deniedRoot ?? msg.rootPaths[0]));
|
|
4522
|
+
break;
|
|
4523
|
+
}
|
|
4524
|
+
try {
|
|
4525
|
+
await this.workspaceStore.createProject(msg.name, normalized.roots);
|
|
4526
|
+
this.broadcast(this.projectsMessage(msg.requestId));
|
|
4527
|
+
}
|
|
4528
|
+
catch (error) {
|
|
4529
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "create Project", error);
|
|
4530
|
+
}
|
|
4531
|
+
break;
|
|
4532
|
+
}
|
|
4533
|
+
case "update_project": {
|
|
4534
|
+
if (!this.workspaceStore) {
|
|
4535
|
+
this.send(ws, {
|
|
4536
|
+
type: "error",
|
|
4537
|
+
requestId: msg.requestId,
|
|
4538
|
+
errorCode: "projects_unavailable",
|
|
4539
|
+
message: "Project storage is unavailable",
|
|
4540
|
+
});
|
|
4541
|
+
break;
|
|
4542
|
+
}
|
|
4543
|
+
const normalized = this.normalizeWorkspaceRoots(msg.rootPaths);
|
|
4544
|
+
if (normalized.deniedRoot || !normalized.roots) {
|
|
4545
|
+
this.send(ws, this.buildPathNotAllowedError(normalized.deniedRoot ?? msg.rootPaths[0]));
|
|
4546
|
+
break;
|
|
4547
|
+
}
|
|
4548
|
+
let updated;
|
|
4549
|
+
try {
|
|
4550
|
+
updated = await this.workspaceStore.updateProject(msg.projectId, msg.name, normalized.roots);
|
|
4551
|
+
}
|
|
4552
|
+
catch (error) {
|
|
4553
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "update Project", error);
|
|
4554
|
+
break;
|
|
4555
|
+
}
|
|
4556
|
+
if (!updated) {
|
|
4557
|
+
this.send(ws, {
|
|
4558
|
+
type: "error",
|
|
4559
|
+
requestId: msg.requestId,
|
|
4560
|
+
errorCode: "project_not_found",
|
|
4561
|
+
message: `Project not found: ${msg.projectId}`,
|
|
4562
|
+
});
|
|
4563
|
+
break;
|
|
4564
|
+
}
|
|
4565
|
+
this.broadcast(this.projectsMessage(msg.requestId));
|
|
4566
|
+
break;
|
|
4567
|
+
}
|
|
4568
|
+
case "remove_project": {
|
|
4569
|
+
if (!this.workspaceStore) {
|
|
4570
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "remove Project", new Error("Project storage is unavailable"));
|
|
4571
|
+
break;
|
|
4572
|
+
}
|
|
4573
|
+
let removed;
|
|
4574
|
+
try {
|
|
4575
|
+
removed = await this.workspaceStore.removeProject(msg.projectId);
|
|
4576
|
+
}
|
|
4577
|
+
catch (error) {
|
|
4578
|
+
this.sendWorkspaceMutationError(ws, msg.requestId, "remove Project", error);
|
|
4579
|
+
break;
|
|
4580
|
+
}
|
|
4581
|
+
if (!removed) {
|
|
4582
|
+
this.send(ws, {
|
|
4583
|
+
type: "error",
|
|
4584
|
+
requestId: msg.requestId,
|
|
4585
|
+
errorCode: "project_not_found",
|
|
4586
|
+
message: `Project not found: ${msg.projectId}`,
|
|
4587
|
+
});
|
|
4588
|
+
break;
|
|
4589
|
+
}
|
|
4590
|
+
this.broadcast(this.projectsMessage(msg.requestId));
|
|
4591
|
+
break;
|
|
4592
|
+
}
|
|
4360
4593
|
case "list_directory": {
|
|
4361
4594
|
try {
|
|
4362
4595
|
const listing = await listAllowedDirectories(msg.path, this.allowedDirs, this.platform, msg.includeHidden ?? false);
|
|
@@ -4700,6 +4933,20 @@ export class BridgeWebSocketServer {
|
|
|
4700
4933
|
}
|
|
4701
4934
|
}
|
|
4702
4935
|
}
|
|
4936
|
+
// A recording keeps its name snapshot after Project deletion, but a
|
|
4937
|
+
// live Project rename should be reflected when the list is read.
|
|
4938
|
+
for (const recording of recordings) {
|
|
4939
|
+
const projectId = recording.meta?.projectId;
|
|
4940
|
+
if (!projectId)
|
|
4941
|
+
continue;
|
|
4942
|
+
const project = this.workspaceStore?.getProject(projectId);
|
|
4943
|
+
if (project && recording.meta) {
|
|
4944
|
+
recording.meta = {
|
|
4945
|
+
...recording.meta,
|
|
4946
|
+
projectName: project.name,
|
|
4947
|
+
};
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4703
4950
|
this.send(ws, { type: "recording_list", recordings });
|
|
4704
4951
|
});
|
|
4705
4952
|
break;
|
|
@@ -5758,9 +6005,17 @@ export class BridgeWebSocketServer {
|
|
|
5758
6005
|
break;
|
|
5759
6006
|
}
|
|
5760
6007
|
try {
|
|
6008
|
+
const runtimeSession = msg.sessionId
|
|
6009
|
+
? this.resolveSession(msg.sessionId)
|
|
6010
|
+
: undefined;
|
|
6011
|
+
const workspace = runtimeSession
|
|
6012
|
+
? this.workspaceForRuntimeSession(runtimeSession)
|
|
6013
|
+
: undefined;
|
|
5761
6014
|
const entry = await this.promptHistoryStore.record({
|
|
5762
6015
|
text: msg.text,
|
|
5763
6016
|
projectPath: msg.projectPath,
|
|
6017
|
+
projectId: workspace?.projectId ?? msg.projectId,
|
|
6018
|
+
projectName: workspace?.projectName ?? msg.projectName,
|
|
5764
6019
|
clientId: msg.clientId,
|
|
5765
6020
|
clientName: msg.clientName,
|
|
5766
6021
|
sessionId: msg.sessionId,
|
|
@@ -5831,6 +6086,7 @@ export class BridgeWebSocketServer {
|
|
|
5831
6086
|
id: msg.id,
|
|
5832
6087
|
text: msg.text,
|
|
5833
6088
|
projectPath: msg.projectPath,
|
|
6089
|
+
projectId: msg.projectId,
|
|
5834
6090
|
action: msg.action,
|
|
5835
6091
|
isFavorite: msg.isFavorite,
|
|
5836
6092
|
updatedAt: msg.updatedAt,
|
|
@@ -6204,7 +6460,7 @@ export class BridgeWebSocketServer {
|
|
|
6204
6460
|
}
|
|
6205
6461
|
sendSessionList(ws) {
|
|
6206
6462
|
this.pruneDebugEvents();
|
|
6207
|
-
const sessions = this.
|
|
6463
|
+
const sessions = this.runtimeSessionsWithWorkspaces();
|
|
6208
6464
|
this.send(ws, {
|
|
6209
6465
|
type: "session_list",
|
|
6210
6466
|
sessions,
|
|
@@ -6242,7 +6498,7 @@ export class BridgeWebSocketServer {
|
|
|
6242
6498
|
/** Broadcast session list to all connected clients. */
|
|
6243
6499
|
broadcastSessionList() {
|
|
6244
6500
|
this.pruneDebugEvents();
|
|
6245
|
-
const sessions = this.
|
|
6501
|
+
const sessions = this.runtimeSessionsWithWorkspaces();
|
|
6246
6502
|
this.broadcast({
|
|
6247
6503
|
type: "session_list",
|
|
6248
6504
|
sessions,
|
|
@@ -6264,6 +6520,18 @@ export class BridgeWebSocketServer {
|
|
|
6264
6520
|
],
|
|
6265
6521
|
});
|
|
6266
6522
|
}
|
|
6523
|
+
runtimeSessionsWithWorkspaces() {
|
|
6524
|
+
return this.sessionManager
|
|
6525
|
+
.list()
|
|
6526
|
+
.map((summary) => this.runtimeSessionWithWorkspace(summary));
|
|
6527
|
+
}
|
|
6528
|
+
runtimeSessionWithWorkspace(summary) {
|
|
6529
|
+
const session = this.sessionManager.get(summary.id);
|
|
6530
|
+
const workspace = session
|
|
6531
|
+
? this.workspaceForRuntimeSession(session)
|
|
6532
|
+
: undefined;
|
|
6533
|
+
return workspace ? { ...summary, workspace } : summary;
|
|
6534
|
+
}
|
|
6267
6535
|
broadcastPromptHistoryStatus() {
|
|
6268
6536
|
if (!this.promptHistoryStore)
|
|
6269
6537
|
return;
|
|
@@ -6390,6 +6658,7 @@ export class BridgeWebSocketServer {
|
|
|
6390
6658
|
}
|
|
6391
6659
|
destroySession(sessionId) {
|
|
6392
6660
|
this.flushSessionDeltaBatches(sessionId);
|
|
6661
|
+
this.pendingSessionWorkspaces.delete(sessionId);
|
|
6393
6662
|
this.sessionManager.destroy(sessionId);
|
|
6394
6663
|
}
|
|
6395
6664
|
trackSessionMessage(sessionId, msg) {
|
|
@@ -6407,10 +6676,15 @@ export class BridgeWebSocketServer {
|
|
|
6407
6676
|
msg.sessionId) {
|
|
6408
6677
|
const session = this.sessionManager.get(sessionId);
|
|
6409
6678
|
if (session) {
|
|
6679
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
6410
6680
|
this.recordingStore?.saveMeta(sessionId, {
|
|
6411
6681
|
bridgeSessionId: sessionId,
|
|
6412
6682
|
claudeSessionId: msg.sessionId,
|
|
6413
6683
|
projectPath: session.projectPath,
|
|
6684
|
+
...(workspace?.projectId ? { projectId: workspace.projectId } : {}),
|
|
6685
|
+
...(workspace?.projectName
|
|
6686
|
+
? { projectName: workspace.projectName }
|
|
6687
|
+
: {}),
|
|
6414
6688
|
createdAt: session.createdAt.toISOString(),
|
|
6415
6689
|
});
|
|
6416
6690
|
}
|
|
@@ -6433,6 +6707,128 @@ export class BridgeWebSocketServer {
|
|
|
6433
6707
|
}
|
|
6434
6708
|
}
|
|
6435
6709
|
async listRecentSessions(msg) {
|
|
6710
|
+
const primaryProjectIds = new Set(msg.projectPath === undefined
|
|
6711
|
+
? []
|
|
6712
|
+
: (this.workspaceStore?.listProjects() ?? [])
|
|
6713
|
+
.filter((project) => project.rootPaths[0] === msg.projectPath)
|
|
6714
|
+
.map((project) => project.id));
|
|
6715
|
+
const workspaceFilterRequested = msg.projectId !== undefined ||
|
|
6716
|
+
msg.workspaceKind !== undefined ||
|
|
6717
|
+
primaryProjectIds.size > 0;
|
|
6718
|
+
if (!workspaceFilterRequested) {
|
|
6719
|
+
const rawResult = await this.listRecentSessionsRaw(msg);
|
|
6720
|
+
const enriched = rawResult.sessions.map((session) => this.enrichRecentSessionWorkspace(session));
|
|
6721
|
+
return { sessions: enriched, hasMore: rawResult.hasMore };
|
|
6722
|
+
}
|
|
6723
|
+
const matchesWorkspace = (session) => {
|
|
6724
|
+
const value = session;
|
|
6725
|
+
const workspace = value.workspace;
|
|
6726
|
+
if (msg.projectId && workspace?.projectId !== msg.projectId)
|
|
6727
|
+
return false;
|
|
6728
|
+
if (msg.workspaceKind && workspace?.kind !== msg.workspaceKind)
|
|
6729
|
+
return false;
|
|
6730
|
+
if (msg.projectPath !== undefined) {
|
|
6731
|
+
const matchesPrimaryProject = typeof workspace?.projectId === "string" &&
|
|
6732
|
+
primaryProjectIds.has(workspace.projectId);
|
|
6733
|
+
if (value.projectPath !== msg.projectPath &&
|
|
6734
|
+
!matchesPrimaryProject) {
|
|
6735
|
+
return false;
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
return true;
|
|
6739
|
+
};
|
|
6740
|
+
const offset = msg.offset ?? 0;
|
|
6741
|
+
const limit = msg.limit ?? 20;
|
|
6742
|
+
const requiredMatches = offset + limit + 1;
|
|
6743
|
+
let filtered;
|
|
6744
|
+
if (msg.provider === "codex") {
|
|
6745
|
+
filtered = await this.listWorkspaceFilteredCodexSessions(msg, matchesWorkspace, requiredMatches);
|
|
6746
|
+
}
|
|
6747
|
+
else if (msg.provider === "claude") {
|
|
6748
|
+
filtered = await this.listWorkspaceFilteredIndexedSessions(msg, matchesWorkspace, requiredMatches);
|
|
6749
|
+
}
|
|
6750
|
+
else {
|
|
6751
|
+
// The filesystem index includes both providers and is also the fallback
|
|
6752
|
+
// for Codex rollouts not returned by app-server. Merge it with one
|
|
6753
|
+
// cursor-preserving app-server scan for the freshest Codex metadata.
|
|
6754
|
+
const [indexed, codex] = await Promise.all([
|
|
6755
|
+
this.listWorkspaceFilteredIndexedSessions(msg, matchesWorkspace, requiredMatches),
|
|
6756
|
+
this.listWorkspaceFilteredCodexSessions({ ...msg, provider: "codex" }, matchesWorkspace, requiredMatches),
|
|
6757
|
+
]);
|
|
6758
|
+
filtered = mergeRecentSessionPages([...codex, ...indexed]);
|
|
6759
|
+
}
|
|
6760
|
+
return {
|
|
6761
|
+
sessions: filtered.slice(offset, offset + limit),
|
|
6762
|
+
hasMore: filtered.length > offset + limit,
|
|
6763
|
+
};
|
|
6764
|
+
}
|
|
6765
|
+
async listWorkspaceFilteredIndexedSessions(msg, matchesWorkspace, limit) {
|
|
6766
|
+
const result = await getAllRecentSessions({
|
|
6767
|
+
limit,
|
|
6768
|
+
offset: 0,
|
|
6769
|
+
// Project identity is authoritative. Do not pre-filter by the current
|
|
6770
|
+
// primary root: assignments may retain an older snapshot or worktree cwd.
|
|
6771
|
+
provider: msg.provider,
|
|
6772
|
+
namedOnly: msg.namedOnly,
|
|
6773
|
+
searchQuery: msg.searchQuery,
|
|
6774
|
+
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
6775
|
+
sessionFilter: (session) => matchesWorkspace(this.enrichRecentSessionWorkspace(session)),
|
|
6776
|
+
});
|
|
6777
|
+
return result.sessions.map((session) => this.enrichRecentSessionWorkspace(session));
|
|
6778
|
+
}
|
|
6779
|
+
async listWorkspaceFilteredCodexSessions(msg, matchesWorkspace, limit) {
|
|
6780
|
+
try {
|
|
6781
|
+
return await this.listWorkspaceFilteredCodexThreads(msg, matchesWorkspace, limit);
|
|
6782
|
+
}
|
|
6783
|
+
catch (err) {
|
|
6784
|
+
console.warn(`[ws] Codex thread/list failed, falling back to rollout scan: ${err}`);
|
|
6785
|
+
return this.listWorkspaceFilteredIndexedSessions({ ...msg, provider: "codex" }, matchesWorkspace, limit);
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
async listWorkspaceFilteredCodexThreads(msg, matchesWorkspace, limit) {
|
|
6789
|
+
const activeProcess = this.getActiveCodexProcess();
|
|
6790
|
+
const process = activeProcess ?? (await this.createStandaloneCodexProcess(undefined));
|
|
6791
|
+
const isStandalone = activeProcess === null;
|
|
6792
|
+
try {
|
|
6793
|
+
const archivedIds = this.archiveStore.archivedIds();
|
|
6794
|
+
const matchingThreads = [];
|
|
6795
|
+
let cursor;
|
|
6796
|
+
do {
|
|
6797
|
+
const request = {
|
|
6798
|
+
limit: 500,
|
|
6799
|
+
searchTerm: msg.searchQuery,
|
|
6800
|
+
sourceKinds: CODEX_RECENT_THREAD_SOURCE_KINDS,
|
|
6801
|
+
};
|
|
6802
|
+
if (cursor != null)
|
|
6803
|
+
request.cursor = cursor;
|
|
6804
|
+
const result = await process.listThreads(request);
|
|
6805
|
+
for (const thread of result.data) {
|
|
6806
|
+
if (archivedIds.has(thread.id))
|
|
6807
|
+
continue;
|
|
6808
|
+
if (msg.namedOnly && !thread.name)
|
|
6809
|
+
continue;
|
|
6810
|
+
const workspaceProbe = this.enrichRecentSessionWorkspace({
|
|
6811
|
+
provider: "codex",
|
|
6812
|
+
sessionId: thread.id,
|
|
6813
|
+
projectPath: thread.cwd,
|
|
6814
|
+
});
|
|
6815
|
+
if (!matchesWorkspace(workspaceProbe))
|
|
6816
|
+
continue;
|
|
6817
|
+
matchingThreads.push(thread);
|
|
6818
|
+
if (matchingThreads.length >= limit)
|
|
6819
|
+
break;
|
|
6820
|
+
}
|
|
6821
|
+
cursor = result.nextCursor;
|
|
6822
|
+
} while (matchingThreads.length < limit && cursor != null);
|
|
6823
|
+
const indexedById = await getCodexSessionIndexMetadata(matchingThreads.map((thread) => thread.id));
|
|
6824
|
+
return matchingThreads.map((thread) => this.enrichRecentSessionWorkspace(codexThreadToRecentSession(thread, indexedById.get(thread.id))));
|
|
6825
|
+
}
|
|
6826
|
+
finally {
|
|
6827
|
+
if (isStandalone)
|
|
6828
|
+
process.stop();
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
async listRecentSessionsRaw(msg) {
|
|
6436
6832
|
if (msg.provider === "codex") {
|
|
6437
6833
|
return this.listRecentCodexSessions(msg);
|
|
6438
6834
|
}
|
|
@@ -6449,11 +6845,106 @@ export class BridgeWebSocketServer {
|
|
|
6449
6845
|
archivedSessionIds: this.archiveStore.archivedIds(),
|
|
6450
6846
|
});
|
|
6451
6847
|
}
|
|
6848
|
+
enrichRecentSessionWorkspace(session) {
|
|
6849
|
+
if (!session || typeof session !== "object")
|
|
6850
|
+
return session;
|
|
6851
|
+
const value = session;
|
|
6852
|
+
const provider = value.provider;
|
|
6853
|
+
const providerSessionId = value.sessionId;
|
|
6854
|
+
const projectPath = value.projectPath;
|
|
6855
|
+
if ((provider !== "claude" && provider !== "codex") ||
|
|
6856
|
+
typeof providerSessionId !== "string" ||
|
|
6857
|
+
typeof projectPath !== "string") {
|
|
6858
|
+
return session;
|
|
6859
|
+
}
|
|
6860
|
+
const workspace = this.workspaceStore?.resolveRecentWorkspace(provider, providerSessionId);
|
|
6861
|
+
return {
|
|
6862
|
+
...value,
|
|
6863
|
+
workspace: workspace ?? {
|
|
6864
|
+
kind: "unassigned",
|
|
6865
|
+
rootPaths: projectPath ? [projectPath] : [],
|
|
6866
|
+
},
|
|
6867
|
+
};
|
|
6868
|
+
}
|
|
6869
|
+
normalizeWorkspaceRoots(rootPaths) {
|
|
6870
|
+
const roots = [];
|
|
6871
|
+
const seen = new Set();
|
|
6872
|
+
for (const rawPath of rootPaths) {
|
|
6873
|
+
const path = resolvePlatformPath(rawPath, this.platform);
|
|
6874
|
+
if (!this.isPathAllowed(path))
|
|
6875
|
+
return { deniedRoot: rawPath };
|
|
6876
|
+
if (!seen.has(path)) {
|
|
6877
|
+
seen.add(path);
|
|
6878
|
+
roots.push(path);
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
return roots.length > 0 ? { roots } : {};
|
|
6882
|
+
}
|
|
6883
|
+
async persistPendingSessionWorkspace(sessionId) {
|
|
6884
|
+
const workspace = this.pendingSessionWorkspaces.get(sessionId);
|
|
6885
|
+
const session = this.sessionManager.get(sessionId);
|
|
6886
|
+
if (!workspace || !session?.claudeSessionId || !this.workspaceStore)
|
|
6887
|
+
return;
|
|
6888
|
+
try {
|
|
6889
|
+
await this.workspaceStore.assignSession(session.provider, session.claudeSessionId, workspace);
|
|
6890
|
+
this.pendingSessionWorkspaces.delete(sessionId);
|
|
6891
|
+
}
|
|
6892
|
+
catch (error) {
|
|
6893
|
+
console.error("[workspace] Failed to persist session assignment:", error);
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6896
|
+
workspaceForRuntimeSession(session) {
|
|
6897
|
+
const pending = this.pendingSessionWorkspaces.get(session.id);
|
|
6898
|
+
if (pending) {
|
|
6899
|
+
return { ...pending, rootPaths: [...pending.rootPaths] };
|
|
6900
|
+
}
|
|
6901
|
+
if (!session.claudeSessionId || !this.workspaceStore)
|
|
6902
|
+
return undefined;
|
|
6903
|
+
const assignment = this.workspaceStore.getAssignment(session.provider, session.claudeSessionId);
|
|
6904
|
+
if (!assignment)
|
|
6905
|
+
return undefined;
|
|
6906
|
+
const project = assignment.projectId
|
|
6907
|
+
? this.workspaceStore.getProject(assignment.projectId)
|
|
6908
|
+
: undefined;
|
|
6909
|
+
const projectName = project?.name ?? assignment.projectName;
|
|
6910
|
+
return {
|
|
6911
|
+
kind: assignment.kind,
|
|
6912
|
+
...(assignment.projectId ? { projectId: assignment.projectId } : {}),
|
|
6913
|
+
...(projectName ? { projectName } : {}),
|
|
6914
|
+
rootPaths: [...assignment.rootPaths],
|
|
6915
|
+
};
|
|
6916
|
+
}
|
|
6917
|
+
attachWorkspaceToRuntimeSession(sessionId, workspace) {
|
|
6918
|
+
if (!workspace)
|
|
6919
|
+
return;
|
|
6920
|
+
this.pendingSessionWorkspaces.set(sessionId, workspace);
|
|
6921
|
+
void this.persistPendingSessionWorkspace(sessionId);
|
|
6922
|
+
}
|
|
6923
|
+
projectsMessage(requestId) {
|
|
6924
|
+
return {
|
|
6925
|
+
type: "projects",
|
|
6926
|
+
projects: this.workspaceStore?.listProjects() ?? [],
|
|
6927
|
+
...(requestId ? { requestId } : {}),
|
|
6928
|
+
};
|
|
6929
|
+
}
|
|
6930
|
+
sendWorkspaceMutationError(ws, requestId, action, error) {
|
|
6931
|
+
this.send(ws, {
|
|
6932
|
+
type: "error",
|
|
6933
|
+
requestId,
|
|
6934
|
+
errorCode: "workspace_write_failed",
|
|
6935
|
+
message: `Failed to ${action}: ${error instanceof Error ? error.message : String(error)}`,
|
|
6936
|
+
});
|
|
6937
|
+
}
|
|
6938
|
+
legacyProjectHistoryProjects() {
|
|
6939
|
+
return this.projectHistory?.getProjects() ?? [];
|
|
6940
|
+
}
|
|
6452
6941
|
listRecentSessionsCoalesced(msg) {
|
|
6453
6942
|
const key = JSON.stringify({
|
|
6454
6943
|
limit: msg.limit ?? null,
|
|
6455
6944
|
offset: msg.offset ?? null,
|
|
6456
6945
|
projectPath: msg.projectPath ?? null,
|
|
6946
|
+
projectId: msg.projectId ?? null,
|
|
6947
|
+
workspaceKind: msg.workspaceKind ?? null,
|
|
6457
6948
|
provider: msg.provider ?? null,
|
|
6458
6949
|
namedOnly: msg.namedOnly ?? null,
|
|
6459
6950
|
searchQuery: msg.searchQuery ?? null,
|
|
@@ -6810,11 +7301,14 @@ export class BridgeWebSocketServer {
|
|
|
6810
7301
|
throw err;
|
|
6811
7302
|
}
|
|
6812
7303
|
}
|
|
6813
|
-
/**
|
|
7304
|
+
/** Resolve a user-facing Project label, falling back to the cwd basename. */
|
|
6814
7305
|
projectLabel(sessionId) {
|
|
6815
7306
|
const session = this.sessionManager.get(sessionId);
|
|
6816
7307
|
if (!session?.projectPath)
|
|
6817
7308
|
return "";
|
|
7309
|
+
const workspace = this.workspaceForRuntimeSession(session);
|
|
7310
|
+
if (workspace?.projectName)
|
|
7311
|
+
return workspace.projectName;
|
|
6818
7312
|
const parts = session.projectPath.replace(/\/+$/, "").split("/");
|
|
6819
7313
|
return parts[parts.length - 1] || "";
|
|
6820
7314
|
}
|