codex-web-ui 0.1.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 +372 -0
- package/bin/codex-web-ui +4 -0
- package/launch_codex_webui_unpacked.sh +1278 -0
- package/package.json +15 -0
- package/webui-bridge.js +202 -0
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-web-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Launch Codex Desktop Web UI from CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codex-web-ui": "bin/codex-web-ui"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"launch_codex_webui_unpacked.sh",
|
|
12
|
+
"webui-bridge.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
]
|
|
15
|
+
}
|
package/webui-bridge.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (typeof window === "undefined") return;
|
|
3
|
+
if (window.electronBridge?.sendMessageFromView) return;
|
|
4
|
+
|
|
5
|
+
const runtimeConfig = window.__CODEX_WEBUI_CONFIG__ ?? {};
|
|
6
|
+
const workerSubscribers = new Map();
|
|
7
|
+
const outboundQueue = [];
|
|
8
|
+
|
|
9
|
+
const reconnectBaseMs = 500;
|
|
10
|
+
const reconnectMaxMs = 5000;
|
|
11
|
+
let reconnectAttempt = 0;
|
|
12
|
+
let reconnectTimer = null;
|
|
13
|
+
let socket = null;
|
|
14
|
+
let isOpen = false;
|
|
15
|
+
let activeSocketToken = 0;
|
|
16
|
+
|
|
17
|
+
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
18
|
+
const wsPath =
|
|
19
|
+
typeof runtimeConfig.wsPath === "string" && runtimeConfig.wsPath.length > 0
|
|
20
|
+
? runtimeConfig.wsPath
|
|
21
|
+
: "/ws";
|
|
22
|
+
const wsUrl = new URL(wsPath, `${wsProtocol}//${window.location.host}`);
|
|
23
|
+
|
|
24
|
+
const flushQueue = () => {
|
|
25
|
+
if (!isOpen || !socket || socket.readyState !== WebSocket.OPEN) return;
|
|
26
|
+
while (outboundQueue.length > 0) {
|
|
27
|
+
const payload = outboundQueue.shift();
|
|
28
|
+
if (typeof payload === "string") socket.send(payload);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const sendPacket = (packet) => {
|
|
33
|
+
const serialized = JSON.stringify(packet);
|
|
34
|
+
if (isOpen && socket && socket.readyState === WebSocket.OPEN) {
|
|
35
|
+
socket.send(serialized);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
outboundQueue.push(serialized);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const emitWorkerMessage = (workerId, payload) => {
|
|
42
|
+
const subscribers = workerSubscribers.get(workerId);
|
|
43
|
+
if (!subscribers) return;
|
|
44
|
+
subscribers.forEach((subscriber) => {
|
|
45
|
+
try {
|
|
46
|
+
subscriber(payload);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.warn("Worker subscription handler failed", error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleInboundPacket = (packet) => {
|
|
54
|
+
if (!packet || typeof packet !== "object") return;
|
|
55
|
+
if (packet.kind === "message-for-view") {
|
|
56
|
+
window.dispatchEvent(
|
|
57
|
+
new MessageEvent("message", {
|
|
58
|
+
data: packet.payload,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (packet.kind === "worker-message-for-view") {
|
|
64
|
+
if (typeof packet.workerId === "string") {
|
|
65
|
+
emitWorkerMessage(packet.workerId, packet.payload);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (packet.kind === "bridge-error") {
|
|
70
|
+
console.warn("Codex WebUI bridge error", packet.message ?? "unknown");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (packet.kind === "open-new-instance") {
|
|
74
|
+
if (typeof packet.url === "string" && packet.url.length > 0) {
|
|
75
|
+
try {
|
|
76
|
+
const target = new URL(packet.url, window.location.href);
|
|
77
|
+
target.pathname = window.location.pathname;
|
|
78
|
+
target.search = window.location.search;
|
|
79
|
+
target.hash = window.location.hash;
|
|
80
|
+
if (target.toString() !== window.location.toString()) {
|
|
81
|
+
window.location.replace(target.toString());
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn("Codex WebUI open-new-instance redirect failed", error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const scheduleReconnect = () => {
|
|
91
|
+
if (socket && socket.readyState === WebSocket.OPEN) return;
|
|
92
|
+
if (reconnectTimer != null) return;
|
|
93
|
+
const delay = Math.min(
|
|
94
|
+
reconnectMaxMs,
|
|
95
|
+
reconnectBaseMs * 2 ** reconnectAttempt,
|
|
96
|
+
);
|
|
97
|
+
reconnectAttempt += 1;
|
|
98
|
+
reconnectTimer = window.setTimeout(() => {
|
|
99
|
+
reconnectTimer = null;
|
|
100
|
+
connect();
|
|
101
|
+
}, delay);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const connect = () => {
|
|
105
|
+
if (
|
|
106
|
+
socket &&
|
|
107
|
+
(socket.readyState === WebSocket.CONNECTING ||
|
|
108
|
+
socket.readyState === WebSocket.OPEN)
|
|
109
|
+
) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const currentToken = ++activeSocketToken;
|
|
113
|
+
const nextSocket = new WebSocket(wsUrl.toString());
|
|
114
|
+
socket = nextSocket;
|
|
115
|
+
nextSocket.addEventListener("open", () => {
|
|
116
|
+
if (currentToken !== activeSocketToken) return;
|
|
117
|
+
isOpen = true;
|
|
118
|
+
reconnectAttempt = 0;
|
|
119
|
+
flushQueue();
|
|
120
|
+
window.dispatchEvent(
|
|
121
|
+
new MessageEvent("message", {
|
|
122
|
+
data: {
|
|
123
|
+
type: "ipc-broadcast",
|
|
124
|
+
method: "client-status-changed",
|
|
125
|
+
sourceClientId: null,
|
|
126
|
+
version: 1,
|
|
127
|
+
params: { status: "connected" },
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
nextSocket.addEventListener("message", (event) => {
|
|
133
|
+
if (currentToken !== activeSocketToken) return;
|
|
134
|
+
let packet;
|
|
135
|
+
try {
|
|
136
|
+
packet = JSON.parse(String(event.data));
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
handleInboundPacket(packet);
|
|
141
|
+
});
|
|
142
|
+
nextSocket.addEventListener("close", () => {
|
|
143
|
+
if (currentToken !== activeSocketToken) return;
|
|
144
|
+
socket = null;
|
|
145
|
+
isOpen = false;
|
|
146
|
+
scheduleReconnect();
|
|
147
|
+
});
|
|
148
|
+
nextSocket.addEventListener("error", () => {
|
|
149
|
+
if (currentToken !== activeSocketToken) return;
|
|
150
|
+
isOpen = false;
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
connect();
|
|
155
|
+
|
|
156
|
+
window.codexWindowType = "web";
|
|
157
|
+
window.electronBridge = {
|
|
158
|
+
windowType: "web",
|
|
159
|
+
sendMessageFromView: async (message) => {
|
|
160
|
+
sendPacket({
|
|
161
|
+
kind: "message-from-view",
|
|
162
|
+
payload: message,
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
getPathForFile: () => null,
|
|
166
|
+
sendWorkerMessageFromView: async (workerId, message) => {
|
|
167
|
+
sendPacket({
|
|
168
|
+
kind: "worker-message-from-view",
|
|
169
|
+
workerId,
|
|
170
|
+
payload: message,
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
subscribeToWorkerMessages: (workerId, callback) => {
|
|
174
|
+
let subscribers = workerSubscribers.get(workerId);
|
|
175
|
+
if (!subscribers) {
|
|
176
|
+
subscribers = new Set();
|
|
177
|
+
workerSubscribers.set(workerId, subscribers);
|
|
178
|
+
}
|
|
179
|
+
subscribers.add(callback);
|
|
180
|
+
return () => {
|
|
181
|
+
const activeSubscribers = workerSubscribers.get(workerId);
|
|
182
|
+
if (!activeSubscribers) return;
|
|
183
|
+
activeSubscribers.delete(callback);
|
|
184
|
+
if (activeSubscribers.size === 0) {
|
|
185
|
+
workerSubscribers.delete(workerId);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
showContextMenu: async () => null,
|
|
190
|
+
triggerSentryTestError: async () => {
|
|
191
|
+
sendPacket({
|
|
192
|
+
kind: "trigger-sentry-test",
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
getSentryInitOptions: () => runtimeConfig.sentryInitOptions ?? null,
|
|
196
|
+
getAppSessionId: () =>
|
|
197
|
+
runtimeConfig.appSessionId ??
|
|
198
|
+
runtimeConfig.sentryInitOptions?.codexAppSessionId ??
|
|
199
|
+
null,
|
|
200
|
+
getBuildFlavor: () => runtimeConfig.buildFlavor ?? "prod",
|
|
201
|
+
};
|
|
202
|
+
})();
|