@ccpocket/bridge 1.51.0 → 1.53.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/dist/codex-process.d.ts +7 -0
- package/dist/codex-process.js +44 -3
- package/dist/codex-process.js.map +1 -1
- package/dist/parser.d.ts +8 -0
- package/dist/parser.js +5 -0
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts +8 -8
- package/dist/session.js +31 -38
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +3 -0
- package/dist/sessions-index.js +253 -5
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +6 -0
- package/dist/websocket.js +463 -28
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
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, codexThreadToSessionHistory, 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,139 @@ 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
|
+
}
|
|
85
|
+
function normalizeHistoryContent(content) {
|
|
86
|
+
if (typeof content === "string")
|
|
87
|
+
return content;
|
|
88
|
+
if (!Array.isArray(content))
|
|
89
|
+
return [];
|
|
90
|
+
const normalized = [];
|
|
91
|
+
for (const item of content) {
|
|
92
|
+
if (!item || typeof item !== "object")
|
|
93
|
+
continue;
|
|
94
|
+
const value = item;
|
|
95
|
+
if (typeof value.type !== "string")
|
|
96
|
+
continue;
|
|
97
|
+
if (value.type === "text") {
|
|
98
|
+
normalized.push({
|
|
99
|
+
type: "text",
|
|
100
|
+
text: typeof value.text === "string" ? value.text : "",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else if (value.type === "tool_use") {
|
|
104
|
+
normalized.push({
|
|
105
|
+
type: "tool_use",
|
|
106
|
+
id: typeof value.id === "string" ? value.id : undefined,
|
|
107
|
+
name: typeof value.name === "string" ? value.name : undefined,
|
|
108
|
+
input: value.input && typeof value.input === "object" && !Array.isArray(value.input)
|
|
109
|
+
? value.input
|
|
110
|
+
: undefined,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
function buildCodexHistoryPrefix(session, targetOrdinal) {
|
|
117
|
+
const messages = [];
|
|
118
|
+
let userOrdinal = 0;
|
|
119
|
+
let reachedEnd = false;
|
|
120
|
+
const appendPastMessage = (message) => {
|
|
121
|
+
if (reachedEnd || !message || typeof message !== "object")
|
|
122
|
+
return;
|
|
123
|
+
const item = message;
|
|
124
|
+
if (item.role === "user" && item.isMeta !== true) {
|
|
125
|
+
userOrdinal += 1;
|
|
126
|
+
if (userOrdinal > targetOrdinal) {
|
|
127
|
+
reachedEnd = true;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
messages.push({ ...item });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (item.role === "assistant" && userOrdinal > 0 && userOrdinal <= targetOrdinal) {
|
|
134
|
+
messages.push({ ...item });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
for (const message of session.pastMessages ?? []) {
|
|
138
|
+
appendPastMessage(message);
|
|
139
|
+
if (reachedEnd)
|
|
140
|
+
return messages;
|
|
141
|
+
}
|
|
142
|
+
for (const message of session.history) {
|
|
143
|
+
if (message.type === "user_input") {
|
|
144
|
+
if (message.isMeta === true)
|
|
145
|
+
continue;
|
|
146
|
+
const userInput = message;
|
|
147
|
+
userOrdinal += 1;
|
|
148
|
+
if (userOrdinal > targetOrdinal)
|
|
149
|
+
break;
|
|
150
|
+
messages.push({
|
|
151
|
+
role: "user",
|
|
152
|
+
uuid: userInput.userMessageUuid,
|
|
153
|
+
timestamp: userInput.timestamp,
|
|
154
|
+
imageCount: userInput.imageCount,
|
|
155
|
+
content: [{ type: "text", text: userInput.text }],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else if (message.type === "assistant" &&
|
|
159
|
+
userOrdinal > 0 &&
|
|
160
|
+
userOrdinal <= targetOrdinal) {
|
|
161
|
+
messages.push({
|
|
162
|
+
role: "assistant",
|
|
163
|
+
uuid: message.messageUuid,
|
|
164
|
+
content: normalizeHistoryContent(message.message.content),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return messages;
|
|
169
|
+
}
|
|
170
|
+
function countCodexHistoryUserTurns(messages) {
|
|
171
|
+
return messages.filter((message) => message.role === "user" && !message.isMeta)
|
|
172
|
+
.length;
|
|
173
|
+
}
|
|
45
174
|
// ---- Codex mode mapping helpers ----
|
|
46
175
|
/** Map unified PermissionMode to Codex approval_policy.
|
|
47
176
|
* Only "bypassPermissions" maps to "never"; all others use "on-request". */
|
|
@@ -394,6 +523,204 @@ export class BridgeWebSocketServer {
|
|
|
394
523
|
}
|
|
395
524
|
return msg;
|
|
396
525
|
}
|
|
526
|
+
async rewindCodexConversation(ws, sessionId, targetUuid, mode) {
|
|
527
|
+
if (mode !== "conversation") {
|
|
528
|
+
this.send(ws, {
|
|
529
|
+
type: "rewind_result",
|
|
530
|
+
success: false,
|
|
531
|
+
mode,
|
|
532
|
+
error: "Codex only supports conversation rewind",
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const session = this.sessionManager.get(sessionId);
|
|
537
|
+
if (!session) {
|
|
538
|
+
this.send(ws, {
|
|
539
|
+
type: "rewind_result",
|
|
540
|
+
success: false,
|
|
541
|
+
mode,
|
|
542
|
+
error: `Session ${sessionId} not found`,
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const codexProcess = session.process;
|
|
547
|
+
if (session.provider !== "codex" ||
|
|
548
|
+
typeof codexProcess.rollbackThread !== "function") {
|
|
549
|
+
this.send(ws, {
|
|
550
|
+
type: "rewind_result",
|
|
551
|
+
success: false,
|
|
552
|
+
mode,
|
|
553
|
+
error: "Session is not a Codex session",
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (session.status !== "idle" || (codexProcess.status ?? session.status) !== "idle") {
|
|
558
|
+
this.send(ws, {
|
|
559
|
+
type: "rewind_result",
|
|
560
|
+
success: false,
|
|
561
|
+
mode,
|
|
562
|
+
error: "Cannot rewind while Codex is running",
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (session.codexQueuedInput) {
|
|
567
|
+
this.send(ws, {
|
|
568
|
+
type: "rewind_result",
|
|
569
|
+
success: false,
|
|
570
|
+
mode,
|
|
571
|
+
error: "Cannot rewind while Codex has queued input",
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const targetOrdinal = parseCodexUserTurnOrdinal(targetUuid);
|
|
576
|
+
const totalUserTurns = countCodexUserTurnsInSession(session);
|
|
577
|
+
if (targetOrdinal === null || targetOrdinal > totalUserTurns) {
|
|
578
|
+
this.send(ws, {
|
|
579
|
+
type: "rewind_result",
|
|
580
|
+
success: false,
|
|
581
|
+
mode,
|
|
582
|
+
error: "Invalid Codex rewind target",
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const numTurns = totalUserTurns - targetOrdinal + 1;
|
|
587
|
+
if (numTurns <= 0) {
|
|
588
|
+
this.send(ws, {
|
|
589
|
+
type: "rewind_result",
|
|
590
|
+
success: false,
|
|
591
|
+
mode,
|
|
592
|
+
error: "Invalid Codex rewind target",
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const threadId = codexProcess.sessionId ?? session.claudeSessionId;
|
|
597
|
+
if (!threadId) {
|
|
598
|
+
this.send(ws, {
|
|
599
|
+
type: "rewind_result",
|
|
600
|
+
success: false,
|
|
601
|
+
mode,
|
|
602
|
+
error: "No Codex thread ID available for rewind",
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const projectPath = session.projectPath;
|
|
607
|
+
const codexSettings = session.codexSettings;
|
|
608
|
+
const worktreeOpts = session.worktreePath
|
|
609
|
+
? {
|
|
610
|
+
existingWorktreePath: session.worktreePath,
|
|
611
|
+
worktreeBranch: session.worktreeBranch,
|
|
612
|
+
}
|
|
613
|
+
: undefined;
|
|
614
|
+
const rolledBackThread = await codexProcess.rollbackThread(numTurns);
|
|
615
|
+
const pastMessages = this.codexHistoryFromThreadOrFallback({
|
|
616
|
+
thread: rolledBackThread,
|
|
617
|
+
expectedUserTurns: targetOrdinal - 1,
|
|
618
|
+
fallback: buildCodexHistoryPrefix(session, targetOrdinal - 1),
|
|
619
|
+
});
|
|
620
|
+
this.sessionManager.destroy(sessionId);
|
|
621
|
+
const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
622
|
+
...(codexSettings ?? {}),
|
|
623
|
+
threadId,
|
|
624
|
+
});
|
|
625
|
+
const newSession = this.sessionManager.get(newSessionId);
|
|
626
|
+
this.send(ws, {
|
|
627
|
+
type: "rewind_result",
|
|
628
|
+
success: true,
|
|
629
|
+
mode,
|
|
630
|
+
});
|
|
631
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
632
|
+
sessionId: newSessionId,
|
|
633
|
+
provider: "codex",
|
|
634
|
+
projectPath,
|
|
635
|
+
session: newSession,
|
|
636
|
+
approvalsReviewer: codexSettings?.approvalsReviewer,
|
|
637
|
+
sandboxMode: codexSettings?.sandboxMode,
|
|
638
|
+
sourceSessionId: sessionId,
|
|
639
|
+
}));
|
|
640
|
+
this.sendSessionList(ws);
|
|
641
|
+
}
|
|
642
|
+
async forkCodexSession(ws, sessionId, targetUuid) {
|
|
643
|
+
const session = this.sessionManager.get(sessionId);
|
|
644
|
+
if (!session) {
|
|
645
|
+
this.send(ws, {
|
|
646
|
+
type: "error",
|
|
647
|
+
message: `Session ${sessionId} not found`,
|
|
648
|
+
errorCode: "fork_failed",
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const codexProcess = session.process;
|
|
653
|
+
if (session.provider !== "codex" ||
|
|
654
|
+
typeof codexProcess.forkThread !== "function") {
|
|
655
|
+
this.send(ws, {
|
|
656
|
+
type: "error",
|
|
657
|
+
message: "Fork is only supported for Codex sessions",
|
|
658
|
+
errorCode: "fork_failed",
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (session.status !== "idle" || (codexProcess.status ?? session.status) !== "idle") {
|
|
663
|
+
this.send(ws, {
|
|
664
|
+
type: "error",
|
|
665
|
+
message: "Cannot fork while Codex is running",
|
|
666
|
+
errorCode: "fork_failed",
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (session.codexQueuedInput) {
|
|
671
|
+
this.send(ws, {
|
|
672
|
+
type: "error",
|
|
673
|
+
message: "Cannot fork while Codex has queued input",
|
|
674
|
+
errorCode: "fork_failed",
|
|
675
|
+
});
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const targetOrdinal = parseCodexUserTurnOrdinal(targetUuid);
|
|
679
|
+
const totalUserTurns = countCodexUserTurnsInSession(session);
|
|
680
|
+
if (targetOrdinal === null || targetOrdinal > totalUserTurns) {
|
|
681
|
+
this.send(ws, {
|
|
682
|
+
type: "error",
|
|
683
|
+
message: "Invalid Codex fork target",
|
|
684
|
+
errorCode: "fork_failed",
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const projectPath = session.projectPath;
|
|
689
|
+
const codexSettings = session.codexSettings;
|
|
690
|
+
const worktreeOpts = session.worktreePath
|
|
691
|
+
? {
|
|
692
|
+
existingWorktreePath: session.worktreePath,
|
|
693
|
+
worktreeBranch: session.worktreeBranch,
|
|
694
|
+
}
|
|
695
|
+
: undefined;
|
|
696
|
+
const forked = await codexProcess.forkThread();
|
|
697
|
+
const forkedThreadId = forked.threadId;
|
|
698
|
+
const turnsToDrop = totalUserTurns - targetOrdinal;
|
|
699
|
+
let forkedThread = forked.thread;
|
|
700
|
+
if (turnsToDrop > 0) {
|
|
701
|
+
forkedThread = await codexProcess.rollbackThreadById(forkedThreadId, turnsToDrop);
|
|
702
|
+
}
|
|
703
|
+
const pastMessages = this.codexHistoryFromThreadOrFallback({
|
|
704
|
+
thread: forkedThread,
|
|
705
|
+
expectedUserTurns: targetOrdinal,
|
|
706
|
+
fallback: buildCodexHistoryPrefix(session, targetOrdinal),
|
|
707
|
+
});
|
|
708
|
+
const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
709
|
+
...(codexSettings ?? {}),
|
|
710
|
+
threadId: forkedThreadId,
|
|
711
|
+
});
|
|
712
|
+
const newSession = this.sessionManager.get(newSessionId);
|
|
713
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
714
|
+
sessionId: newSessionId,
|
|
715
|
+
provider: "codex",
|
|
716
|
+
projectPath,
|
|
717
|
+
session: newSession,
|
|
718
|
+
approvalsReviewer: codexSettings?.approvalsReviewer,
|
|
719
|
+
sandboxMode: codexSettings?.sandboxMode,
|
|
720
|
+
sourceSessionId: sessionId,
|
|
721
|
+
}));
|
|
722
|
+
this.sendSessionList(ws);
|
|
723
|
+
}
|
|
397
724
|
sendTip(ws, sessionId, tipCode, session) {
|
|
398
725
|
const tipMsg = {
|
|
399
726
|
type: "system",
|
|
@@ -461,6 +788,39 @@ export class BridgeWebSocketServer {
|
|
|
461
788
|
}
|
|
462
789
|
return { pastMessages, historyMessages };
|
|
463
790
|
}
|
|
791
|
+
async getCodexThreadHistoryFromRpc(threadId, projectPath) {
|
|
792
|
+
const activeProcess = this.getActiveCodexProcess();
|
|
793
|
+
const process = activeProcess ?? (await this.createStandaloneCodexProcess(projectPath));
|
|
794
|
+
const isStandalone = process !== activeProcess;
|
|
795
|
+
try {
|
|
796
|
+
const thread = await process.readThread(threadId, true);
|
|
797
|
+
return codexThreadToSessionHistory(thread);
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
if (isStandalone) {
|
|
801
|
+
process.stop();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
async getCodexThreadHistory(threadId, projectPath) {
|
|
806
|
+
if (!this.getActiveCodexProcess() && process.env.NODE_ENV === "test") {
|
|
807
|
+
return getCodexSessionHistory(threadId);
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
return await this.getCodexThreadHistoryFromRpc(threadId, projectPath);
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
console.warn(`[ws] thread/read failed for ${threadId}; falling back to JSONL: ${err instanceof Error ? err.message : String(err)}`);
|
|
814
|
+
return getCodexSessionHistory(threadId);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
codexHistoryFromThreadOrFallback(params) {
|
|
818
|
+
const messages = codexThreadToSessionHistory(params.thread);
|
|
819
|
+
if (countCodexHistoryUserTurns(messages) >= params.expectedUserTurns) {
|
|
820
|
+
return messages;
|
|
821
|
+
}
|
|
822
|
+
return params.fallback;
|
|
823
|
+
}
|
|
464
824
|
createClaudeSessionWithFallback(params) {
|
|
465
825
|
const initialMode = params.options?.permissionMode ?? "default";
|
|
466
826
|
try {
|
|
@@ -769,7 +1129,9 @@ export class BridgeWebSocketServer {
|
|
|
769
1129
|
// The SDK stream does NOT emit user messages, so session.history would
|
|
770
1130
|
// otherwise lack them. This ensures get_history responses include user
|
|
771
1131
|
// messages and replaceEntries on the client side preserves them.
|
|
772
|
-
//
|
|
1132
|
+
// Flutter already shows the user bubble optimistically. For Codex we
|
|
1133
|
+
// echo the accepted user_input back with its synthetic UUID so the live
|
|
1134
|
+
// bubble becomes rewindable/forkable without requiring a stop+resume.
|
|
773
1135
|
//
|
|
774
1136
|
// Register images in the image store so they can be served via HTTP
|
|
775
1137
|
// when the client re-enters the session and loads history.
|
|
@@ -810,6 +1172,7 @@ export class BridgeWebSocketServer {
|
|
|
810
1172
|
itemId: randomUUID(),
|
|
811
1173
|
text,
|
|
812
1174
|
createdAt: new Date().toISOString(),
|
|
1175
|
+
userMessageUuid: nextCodexUserTurnUuid(session),
|
|
813
1176
|
...(images.length > 0 ? { imageCount: images.length, images } : {}),
|
|
814
1177
|
...(imageRefs ? { imageRefs } : {}),
|
|
815
1178
|
...(codexSkills.length > 0 ? { skills: codexSkills } : {}),
|
|
@@ -846,12 +1209,22 @@ export class BridgeWebSocketServer {
|
|
|
846
1209
|
const userEntry = this.sessionManager.appendHistory(session.id, {
|
|
847
1210
|
type: "user_input",
|
|
848
1211
|
text,
|
|
1212
|
+
...(session.provider === "codex"
|
|
1213
|
+
? { userMessageUuid: nextCodexUserTurnUuid(session) }
|
|
1214
|
+
: {}),
|
|
849
1215
|
...(clientMessageId ? { clientMessageId } : {}),
|
|
850
1216
|
timestamp: new Date().toISOString(),
|
|
851
1217
|
...(images.length > 0 ? { imageCount: images.length } : {}),
|
|
852
1218
|
...(imageRefs ? { images: imageRefs } : {}),
|
|
853
1219
|
});
|
|
854
1220
|
const acceptedSeq = userEntry?.seq ?? session.historyRevision;
|
|
1221
|
+
if (session.provider === "codex" && userEntry) {
|
|
1222
|
+
this.send(ws, {
|
|
1223
|
+
...userEntry.message,
|
|
1224
|
+
sessionId: session.id,
|
|
1225
|
+
historySeq: acceptedSeq,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
855
1228
|
// Persist images to Gallery Store asynchronously (fire-and-forget)
|
|
856
1229
|
if (images.length > 0 && this.galleryStore && session.projectPath) {
|
|
857
1230
|
for (const img of images) {
|
|
@@ -1266,7 +1639,7 @@ export class BridgeWebSocketServer {
|
|
|
1266
1639
|
worktreeBranch,
|
|
1267
1640
|
};
|
|
1268
1641
|
}
|
|
1269
|
-
|
|
1642
|
+
this.getCodexThreadHistory(threadId, effectiveProjectPath)
|
|
1270
1643
|
.then((pastMessages) => {
|
|
1271
1644
|
const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
1272
1645
|
threadId,
|
|
@@ -1491,7 +1864,7 @@ export class BridgeWebSocketServer {
|
|
|
1491
1864
|
else if (worktreePath) {
|
|
1492
1865
|
worktreeOpts = { existingWorktreePath: worktreePath, worktreeBranch };
|
|
1493
1866
|
}
|
|
1494
|
-
|
|
1867
|
+
this.getCodexThreadHistory(threadId, effectiveProjectPath)
|
|
1495
1868
|
.then((pastMessages) => {
|
|
1496
1869
|
const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
1497
1870
|
threadId,
|
|
@@ -2028,8 +2401,8 @@ export class BridgeWebSocketServer {
|
|
|
2028
2401
|
};
|
|
2029
2402
|
}
|
|
2030
2403
|
}
|
|
2031
|
-
|
|
2032
|
-
.
|
|
2404
|
+
try {
|
|
2405
|
+
const pastMessages = await this.getCodexThreadHistory(sessionRefId, effectiveProjectPath);
|
|
2033
2406
|
const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
2034
2407
|
threadId: sessionRefId,
|
|
2035
2408
|
profile: effectiveProfile,
|
|
@@ -2048,22 +2421,21 @@ export class BridgeWebSocketServer {
|
|
|
2048
2421
|
: "default",
|
|
2049
2422
|
});
|
|
2050
2423
|
const createdSession = this.sessionManager.get(sessionId);
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
});
|
|
2424
|
+
await this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId);
|
|
2425
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
2426
|
+
sessionId,
|
|
2427
|
+
provider: "codex",
|
|
2428
|
+
projectPath: effectiveProjectPath,
|
|
2429
|
+
session: createdSession,
|
|
2430
|
+
sandboxMode: createdSession?.codexSettings?.sandboxMode
|
|
2431
|
+
? sandboxModeToExternal(createdSession.codexSettings.sandboxMode)
|
|
2432
|
+
: undefined,
|
|
2433
|
+
approvalsReviewer: createdSession?.codexSettings?.approvalsReviewer,
|
|
2434
|
+
permissionMode: legacyPermissionMode,
|
|
2435
|
+
executionMode,
|
|
2436
|
+
planMode,
|
|
2437
|
+
}));
|
|
2438
|
+
this.broadcastSessionList();
|
|
2067
2439
|
this.debugEvents.set(sessionId, []);
|
|
2068
2440
|
this.recordDebugEvent(sessionId, {
|
|
2069
2441
|
direction: "internal",
|
|
@@ -2072,13 +2444,13 @@ export class BridgeWebSocketServer {
|
|
|
2072
2444
|
detail: `provider=codex thread=${sessionRefId}`,
|
|
2073
2445
|
});
|
|
2074
2446
|
this.projectHistory?.addProject(effectiveProjectPath);
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2447
|
+
}
|
|
2448
|
+
catch (err) {
|
|
2077
2449
|
this.send(ws, {
|
|
2078
2450
|
type: "error",
|
|
2079
2451
|
message: `Failed to load Codex session history: ${err}`,
|
|
2080
2452
|
});
|
|
2081
|
-
}
|
|
2453
|
+
}
|
|
2082
2454
|
break;
|
|
2083
2455
|
}
|
|
2084
2456
|
const claudeSessionId = sessionRefId;
|
|
@@ -2304,11 +2676,41 @@ export class BridgeWebSocketServer {
|
|
|
2304
2676
|
});
|
|
2305
2677
|
return;
|
|
2306
2678
|
}
|
|
2679
|
+
const resolvedFileStat = fileStat.isSymbolicLink()
|
|
2680
|
+
? await stat(absPath)
|
|
2681
|
+
: fileStat;
|
|
2682
|
+
const ext = extname(absPath).toLowerCase();
|
|
2683
|
+
if (BridgeWebSocketServer.FILE_PEEK_IMAGE_EXTENSIONS.has(ext)) {
|
|
2684
|
+
const mimeType = BridgeWebSocketServer.mimeTypeForExt(ext);
|
|
2685
|
+
if (resolvedFileStat.size > BridgeWebSocketServer.MAX_IMAGE_SIZE) {
|
|
2686
|
+
this.send(ws, {
|
|
2687
|
+
type: "file_content",
|
|
2688
|
+
filePath: msg.filePath,
|
|
2689
|
+
kind: "image",
|
|
2690
|
+
content: "",
|
|
2691
|
+
mimeType,
|
|
2692
|
+
sizeBytes: resolvedFileStat.size,
|
|
2693
|
+
error: "Image too large to preview. Maximum size is 5 MB.",
|
|
2694
|
+
});
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const buf = await readFile(absPath);
|
|
2698
|
+
this.send(ws, {
|
|
2699
|
+
type: "file_content",
|
|
2700
|
+
filePath: msg.filePath,
|
|
2701
|
+
kind: "image",
|
|
2702
|
+
content: "",
|
|
2703
|
+
base64: buf.toString("base64"),
|
|
2704
|
+
mimeType,
|
|
2705
|
+
sizeBytes: buf.length,
|
|
2706
|
+
});
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2307
2709
|
const maxLines = typeof msg.maxLines === "number" && msg.maxLines > 0
|
|
2308
2710
|
? msg.maxLines
|
|
2309
2711
|
: 5000;
|
|
2310
2712
|
const raw = await readFile(absPath, "utf-8");
|
|
2311
|
-
const
|
|
2713
|
+
const textExt = ext.replace(/^\./, "").toLowerCase();
|
|
2312
2714
|
const languageMap = {
|
|
2313
2715
|
ts: "typescript",
|
|
2314
2716
|
tsx: "typescript",
|
|
@@ -2343,7 +2745,7 @@ export class BridgeWebSocketServer {
|
|
|
2343
2745
|
makefile: "makefile",
|
|
2344
2746
|
gradle: "groovy",
|
|
2345
2747
|
};
|
|
2346
|
-
const language = languageMap[
|
|
2748
|
+
const language = languageMap[textExt] ?? (textExt || undefined);
|
|
2347
2749
|
const lines = raw.split("\n");
|
|
2348
2750
|
const truncated = lines.length > maxLines;
|
|
2349
2751
|
const content = truncated
|
|
@@ -2352,6 +2754,7 @@ export class BridgeWebSocketServer {
|
|
|
2352
2754
|
this.send(ws, {
|
|
2353
2755
|
type: "file_content",
|
|
2354
2756
|
filePath: msg.filePath,
|
|
2757
|
+
kind: "text",
|
|
2355
2758
|
content,
|
|
2356
2759
|
language,
|
|
2357
2760
|
totalLines: lines.length,
|
|
@@ -2972,6 +3375,14 @@ export class BridgeWebSocketServer {
|
|
|
2972
3375
|
});
|
|
2973
3376
|
return;
|
|
2974
3377
|
}
|
|
3378
|
+
if (session.provider === "codex") {
|
|
3379
|
+
this.send(ws, {
|
|
3380
|
+
type: "rewind_preview",
|
|
3381
|
+
canRewind: false,
|
|
3382
|
+
error: "Codex rewind does not restore files",
|
|
3383
|
+
});
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
2975
3386
|
this.sessionManager
|
|
2976
3387
|
.rewindFiles(msg.sessionId, msg.targetUuid, true)
|
|
2977
3388
|
.then((result) => {
|
|
@@ -3013,6 +3424,11 @@ export class BridgeWebSocketServer {
|
|
|
3013
3424
|
error: errMsg,
|
|
3014
3425
|
});
|
|
3015
3426
|
};
|
|
3427
|
+
if (session.provider === "codex") {
|
|
3428
|
+
this.rewindCodexConversation(ws, msg.sessionId, msg.targetUuid, msg.mode)
|
|
3429
|
+
.catch(handleError);
|
|
3430
|
+
break;
|
|
3431
|
+
}
|
|
3016
3432
|
if (msg.mode === "code") {
|
|
3017
3433
|
// Code-only rewind: rewind files without restarting the conversation
|
|
3018
3434
|
this.sessionManager
|
|
@@ -3109,6 +3525,17 @@ export class BridgeWebSocketServer {
|
|
|
3109
3525
|
}
|
|
3110
3526
|
break;
|
|
3111
3527
|
}
|
|
3528
|
+
case "fork": {
|
|
3529
|
+
this.forkCodexSession(ws, msg.sessionId, msg.targetUuid).catch((err) => {
|
|
3530
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3531
|
+
this.send(ws, {
|
|
3532
|
+
type: "error",
|
|
3533
|
+
message: errMsg,
|
|
3534
|
+
errorCode: "fork_failed",
|
|
3535
|
+
});
|
|
3536
|
+
});
|
|
3537
|
+
break;
|
|
3538
|
+
}
|
|
3112
3539
|
case "list_windows": {
|
|
3113
3540
|
listWindows()
|
|
3114
3541
|
.then((windows) => {
|
|
@@ -4072,6 +4499,14 @@ export class BridgeWebSocketServer {
|
|
|
4072
4499
|
".bmp",
|
|
4073
4500
|
".svg",
|
|
4074
4501
|
]);
|
|
4502
|
+
static FILE_PEEK_IMAGE_EXTENSIONS = new Set([
|
|
4503
|
+
".png",
|
|
4504
|
+
".jpg",
|
|
4505
|
+
".jpeg",
|
|
4506
|
+
".gif",
|
|
4507
|
+
".webp",
|
|
4508
|
+
".svg",
|
|
4509
|
+
]);
|
|
4075
4510
|
// Image diff thresholds (configurable via environment variables)
|
|
4076
4511
|
// - Auto-display: images ≤ threshold are sent inline as base64
|
|
4077
4512
|
// - Max size: images ≤ max are available for on-demand loading
|