@ccpocket/bridge 1.51.0 → 1.52.2
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/codex-process.d.ts +1 -0
- package/dist/codex-process.js +9 -0
- package/dist/codex-process.js.map +1 -1
- package/dist/parser.d.ts +4 -0
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +1 -0
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +1 -0
- package/dist/sessions-index.js +43 -5
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +212 -4
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export declare class BridgeWebSocketServer {
|
|
|
61
61
|
private buildPathNotAllowedError;
|
|
62
62
|
private normalizeAdditionalWritableRoots;
|
|
63
63
|
private buildSessionCreatedMessage;
|
|
64
|
+
private rewindCodexConversation;
|
|
64
65
|
private sendTip;
|
|
65
66
|
private splitPastHistoryMessages;
|
|
66
67
|
private createClaudeSessionWithFallback;
|
|
@@ -117,6 +118,7 @@ export declare class BridgeWebSocketServer {
|
|
|
117
118
|
broadcastGalleryNewImage(image: import("./gallery-store.js").GalleryImageInfo): void;
|
|
118
119
|
private collectGitDiff;
|
|
119
120
|
private static readonly IMAGE_EXTENSIONS;
|
|
121
|
+
private static readonly FILE_PEEK_IMAGE_EXTENSIONS;
|
|
120
122
|
private static readonly AUTO_DISPLAY_THRESHOLD;
|
|
121
123
|
private static readonly MAX_IMAGE_SIZE;
|
|
122
124
|
private static mimeTypeForExt;
|
package/dist/websocket.js
CHANGED
|
@@ -7,9 +7,9 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { WebSocketServer, WebSocket } from "ws";
|
|
8
8
|
import { SessionManager, } from "./session.js";
|
|
9
9
|
import { SdkProcess } from "./sdk-process.js";
|
|
10
|
-
import { CodexProcess } from "./codex-process.js";
|
|
10
|
+
import { CodexProcess, } from "./codex-process.js";
|
|
11
11
|
import { parseClientMessage, } from "./parser.js";
|
|
12
|
-
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
|
|
12
|
+
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, codexUserTurnUuid, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
|
|
13
13
|
import { ArchiveStore } from "./archive-store.js";
|
|
14
14
|
import { WorktreeStore } from "./worktree-store.js";
|
|
15
15
|
import { listWorktrees, removeWorktree, worktreeExists, getMainBranch, } from "./worktree.js";
|
|
@@ -38,10 +38,50 @@ const CODEX_MODELS = [
|
|
|
38
38
|
"gpt-5.3-codex",
|
|
39
39
|
"gpt-5.3-codex-spark",
|
|
40
40
|
];
|
|
41
|
+
const CODEX_USER_TURN_UUID_RE = /^codex:user-turn:(\d+)$/;
|
|
41
42
|
const OPT_IN_SERVER_MESSAGES = new Set([
|
|
42
43
|
"conversation_queue",
|
|
43
44
|
"prompt_history_status",
|
|
44
45
|
]);
|
|
46
|
+
function parseCodexUserTurnOrdinal(uuid) {
|
|
47
|
+
if (!uuid)
|
|
48
|
+
return null;
|
|
49
|
+
const match = uuid.match(CODEX_USER_TURN_UUID_RE);
|
|
50
|
+
if (!match)
|
|
51
|
+
return null;
|
|
52
|
+
const ordinal = Number(match[1]);
|
|
53
|
+
return Number.isInteger(ordinal) && ordinal > 0 ? ordinal : null;
|
|
54
|
+
}
|
|
55
|
+
function countCodexUserTurnsInSession(session) {
|
|
56
|
+
let count = 0;
|
|
57
|
+
let maxOrdinal = 0;
|
|
58
|
+
const observe = (uuid) => {
|
|
59
|
+
count += 1;
|
|
60
|
+
const ordinal = parseCodexUserTurnOrdinal(uuid);
|
|
61
|
+
if (ordinal !== null) {
|
|
62
|
+
maxOrdinal = Math.max(maxOrdinal, ordinal);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
if (Array.isArray(session.pastMessages)) {
|
|
66
|
+
for (const message of session.pastMessages) {
|
|
67
|
+
if (!message || typeof message !== "object")
|
|
68
|
+
continue;
|
|
69
|
+
const item = message;
|
|
70
|
+
if (item.role === "user" && item.isMeta !== true) {
|
|
71
|
+
observe(typeof item.uuid === "string" ? item.uuid : undefined);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const message of session.history) {
|
|
76
|
+
if (message.type === "user_input") {
|
|
77
|
+
observe(message.userMessageUuid);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Math.max(count, maxOrdinal);
|
|
81
|
+
}
|
|
82
|
+
function nextCodexUserTurnUuid(session) {
|
|
83
|
+
return codexUserTurnUuid(countCodexUserTurnsInSession(session) + 1);
|
|
84
|
+
}
|
|
45
85
|
// ---- Codex mode mapping helpers ----
|
|
46
86
|
/** Map unified PermissionMode to Codex approval_policy.
|
|
47
87
|
* Only "bypassPermissions" maps to "never"; all others use "on-request". */
|
|
@@ -394,6 +434,118 @@ export class BridgeWebSocketServer {
|
|
|
394
434
|
}
|
|
395
435
|
return msg;
|
|
396
436
|
}
|
|
437
|
+
async rewindCodexConversation(ws, sessionId, targetUuid, mode) {
|
|
438
|
+
if (mode !== "conversation") {
|
|
439
|
+
this.send(ws, {
|
|
440
|
+
type: "rewind_result",
|
|
441
|
+
success: false,
|
|
442
|
+
mode,
|
|
443
|
+
error: "Codex only supports conversation rewind",
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const session = this.sessionManager.get(sessionId);
|
|
448
|
+
if (!session) {
|
|
449
|
+
this.send(ws, {
|
|
450
|
+
type: "rewind_result",
|
|
451
|
+
success: false,
|
|
452
|
+
mode,
|
|
453
|
+
error: `Session ${sessionId} not found`,
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const codexProcess = session.process;
|
|
458
|
+
if (session.provider !== "codex" ||
|
|
459
|
+
typeof codexProcess.rollbackThread !== "function") {
|
|
460
|
+
this.send(ws, {
|
|
461
|
+
type: "rewind_result",
|
|
462
|
+
success: false,
|
|
463
|
+
mode,
|
|
464
|
+
error: "Session is not a Codex session",
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (session.status !== "idle" || (codexProcess.status ?? session.status) !== "idle") {
|
|
469
|
+
this.send(ws, {
|
|
470
|
+
type: "rewind_result",
|
|
471
|
+
success: false,
|
|
472
|
+
mode,
|
|
473
|
+
error: "Cannot rewind while Codex is running",
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (session.codexQueuedInput) {
|
|
478
|
+
this.send(ws, {
|
|
479
|
+
type: "rewind_result",
|
|
480
|
+
success: false,
|
|
481
|
+
mode,
|
|
482
|
+
error: "Cannot rewind while Codex has queued input",
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const targetOrdinal = parseCodexUserTurnOrdinal(targetUuid);
|
|
487
|
+
const totalUserTurns = countCodexUserTurnsInSession(session);
|
|
488
|
+
if (targetOrdinal === null || targetOrdinal > totalUserTurns) {
|
|
489
|
+
this.send(ws, {
|
|
490
|
+
type: "rewind_result",
|
|
491
|
+
success: false,
|
|
492
|
+
mode,
|
|
493
|
+
error: "Invalid Codex rewind target",
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const numTurns = totalUserTurns - targetOrdinal;
|
|
498
|
+
if (numTurns <= 0) {
|
|
499
|
+
this.send(ws, {
|
|
500
|
+
type: "rewind_result",
|
|
501
|
+
success: false,
|
|
502
|
+
mode,
|
|
503
|
+
error: "No newer turns to rewind",
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const threadId = codexProcess.sessionId ?? session.claudeSessionId;
|
|
508
|
+
if (!threadId) {
|
|
509
|
+
this.send(ws, {
|
|
510
|
+
type: "rewind_result",
|
|
511
|
+
success: false,
|
|
512
|
+
mode,
|
|
513
|
+
error: "No Codex thread ID available for rewind",
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const projectPath = session.projectPath;
|
|
518
|
+
const codexSettings = session.codexSettings;
|
|
519
|
+
const worktreeOpts = session.worktreePath
|
|
520
|
+
? {
|
|
521
|
+
existingWorktreePath: session.worktreePath,
|
|
522
|
+
worktreeBranch: session.worktreeBranch,
|
|
523
|
+
}
|
|
524
|
+
: undefined;
|
|
525
|
+
await codexProcess.rollbackThread(numTurns);
|
|
526
|
+
const pastMessages = await getCodexSessionHistory(threadId);
|
|
527
|
+
this.sessionManager.destroy(sessionId);
|
|
528
|
+
const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
529
|
+
...(codexSettings ?? {}),
|
|
530
|
+
threadId,
|
|
531
|
+
});
|
|
532
|
+
const newSession = this.sessionManager.get(newSessionId);
|
|
533
|
+
this.send(ws, {
|
|
534
|
+
type: "rewind_result",
|
|
535
|
+
success: true,
|
|
536
|
+
mode,
|
|
537
|
+
});
|
|
538
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
539
|
+
sessionId: newSessionId,
|
|
540
|
+
provider: "codex",
|
|
541
|
+
projectPath,
|
|
542
|
+
session: newSession,
|
|
543
|
+
approvalsReviewer: codexSettings?.approvalsReviewer,
|
|
544
|
+
sandboxMode: codexSettings?.sandboxMode,
|
|
545
|
+
sourceSessionId: sessionId,
|
|
546
|
+
}));
|
|
547
|
+
this.sendSessionList(ws);
|
|
548
|
+
}
|
|
397
549
|
sendTip(ws, sessionId, tipCode, session) {
|
|
398
550
|
const tipMsg = {
|
|
399
551
|
type: "system",
|
|
@@ -810,6 +962,7 @@ export class BridgeWebSocketServer {
|
|
|
810
962
|
itemId: randomUUID(),
|
|
811
963
|
text,
|
|
812
964
|
createdAt: new Date().toISOString(),
|
|
965
|
+
userMessageUuid: nextCodexUserTurnUuid(session),
|
|
813
966
|
...(images.length > 0 ? { imageCount: images.length, images } : {}),
|
|
814
967
|
...(imageRefs ? { imageRefs } : {}),
|
|
815
968
|
...(codexSkills.length > 0 ? { skills: codexSkills } : {}),
|
|
@@ -846,6 +999,9 @@ export class BridgeWebSocketServer {
|
|
|
846
999
|
const userEntry = this.sessionManager.appendHistory(session.id, {
|
|
847
1000
|
type: "user_input",
|
|
848
1001
|
text,
|
|
1002
|
+
...(session.provider === "codex"
|
|
1003
|
+
? { userMessageUuid: nextCodexUserTurnUuid(session) }
|
|
1004
|
+
: {}),
|
|
849
1005
|
...(clientMessageId ? { clientMessageId } : {}),
|
|
850
1006
|
timestamp: new Date().toISOString(),
|
|
851
1007
|
...(images.length > 0 ? { imageCount: images.length } : {}),
|
|
@@ -2304,11 +2460,41 @@ export class BridgeWebSocketServer {
|
|
|
2304
2460
|
});
|
|
2305
2461
|
return;
|
|
2306
2462
|
}
|
|
2463
|
+
const resolvedFileStat = fileStat.isSymbolicLink()
|
|
2464
|
+
? await stat(absPath)
|
|
2465
|
+
: fileStat;
|
|
2466
|
+
const ext = extname(absPath).toLowerCase();
|
|
2467
|
+
if (BridgeWebSocketServer.FILE_PEEK_IMAGE_EXTENSIONS.has(ext)) {
|
|
2468
|
+
const mimeType = BridgeWebSocketServer.mimeTypeForExt(ext);
|
|
2469
|
+
if (resolvedFileStat.size > BridgeWebSocketServer.MAX_IMAGE_SIZE) {
|
|
2470
|
+
this.send(ws, {
|
|
2471
|
+
type: "file_content",
|
|
2472
|
+
filePath: msg.filePath,
|
|
2473
|
+
kind: "image",
|
|
2474
|
+
content: "",
|
|
2475
|
+
mimeType,
|
|
2476
|
+
sizeBytes: resolvedFileStat.size,
|
|
2477
|
+
error: "Image too large to preview. Maximum size is 5 MB.",
|
|
2478
|
+
});
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
const buf = await readFile(absPath);
|
|
2482
|
+
this.send(ws, {
|
|
2483
|
+
type: "file_content",
|
|
2484
|
+
filePath: msg.filePath,
|
|
2485
|
+
kind: "image",
|
|
2486
|
+
content: "",
|
|
2487
|
+
base64: buf.toString("base64"),
|
|
2488
|
+
mimeType,
|
|
2489
|
+
sizeBytes: buf.length,
|
|
2490
|
+
});
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2307
2493
|
const maxLines = typeof msg.maxLines === "number" && msg.maxLines > 0
|
|
2308
2494
|
? msg.maxLines
|
|
2309
2495
|
: 5000;
|
|
2310
2496
|
const raw = await readFile(absPath, "utf-8");
|
|
2311
|
-
const
|
|
2497
|
+
const textExt = ext.replace(/^\./, "").toLowerCase();
|
|
2312
2498
|
const languageMap = {
|
|
2313
2499
|
ts: "typescript",
|
|
2314
2500
|
tsx: "typescript",
|
|
@@ -2343,7 +2529,7 @@ export class BridgeWebSocketServer {
|
|
|
2343
2529
|
makefile: "makefile",
|
|
2344
2530
|
gradle: "groovy",
|
|
2345
2531
|
};
|
|
2346
|
-
const language = languageMap[
|
|
2532
|
+
const language = languageMap[textExt] ?? (textExt || undefined);
|
|
2347
2533
|
const lines = raw.split("\n");
|
|
2348
2534
|
const truncated = lines.length > maxLines;
|
|
2349
2535
|
const content = truncated
|
|
@@ -2352,6 +2538,7 @@ export class BridgeWebSocketServer {
|
|
|
2352
2538
|
this.send(ws, {
|
|
2353
2539
|
type: "file_content",
|
|
2354
2540
|
filePath: msg.filePath,
|
|
2541
|
+
kind: "text",
|
|
2355
2542
|
content,
|
|
2356
2543
|
language,
|
|
2357
2544
|
totalLines: lines.length,
|
|
@@ -2972,6 +3159,14 @@ export class BridgeWebSocketServer {
|
|
|
2972
3159
|
});
|
|
2973
3160
|
return;
|
|
2974
3161
|
}
|
|
3162
|
+
if (session.provider === "codex") {
|
|
3163
|
+
this.send(ws, {
|
|
3164
|
+
type: "rewind_preview",
|
|
3165
|
+
canRewind: false,
|
|
3166
|
+
error: "Codex rewind does not restore files",
|
|
3167
|
+
});
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
2975
3170
|
this.sessionManager
|
|
2976
3171
|
.rewindFiles(msg.sessionId, msg.targetUuid, true)
|
|
2977
3172
|
.then((result) => {
|
|
@@ -3013,6 +3208,11 @@ export class BridgeWebSocketServer {
|
|
|
3013
3208
|
error: errMsg,
|
|
3014
3209
|
});
|
|
3015
3210
|
};
|
|
3211
|
+
if (session.provider === "codex") {
|
|
3212
|
+
this.rewindCodexConversation(ws, msg.sessionId, msg.targetUuid, msg.mode)
|
|
3213
|
+
.catch(handleError);
|
|
3214
|
+
break;
|
|
3215
|
+
}
|
|
3016
3216
|
if (msg.mode === "code") {
|
|
3017
3217
|
// Code-only rewind: rewind files without restarting the conversation
|
|
3018
3218
|
this.sessionManager
|
|
@@ -4072,6 +4272,14 @@ export class BridgeWebSocketServer {
|
|
|
4072
4272
|
".bmp",
|
|
4073
4273
|
".svg",
|
|
4074
4274
|
]);
|
|
4275
|
+
static FILE_PEEK_IMAGE_EXTENSIONS = new Set([
|
|
4276
|
+
".png",
|
|
4277
|
+
".jpg",
|
|
4278
|
+
".jpeg",
|
|
4279
|
+
".gif",
|
|
4280
|
+
".webp",
|
|
4281
|
+
".svg",
|
|
4282
|
+
]);
|
|
4075
4283
|
// Image diff thresholds (configurable via environment variables)
|
|
4076
4284
|
// - Auto-display: images ≤ threshold are sent inline as base64
|
|
4077
4285
|
// - Max size: images ≤ max are available for on-demand loading
|