codex-webapp 0.1.6 → 0.1.8

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.
@@ -0,0 +1,176 @@
1
+ (function () {
2
+ const APP_SESSION_ID = "codex-webapp-browser-session";
3
+ const REQUEST_TIMEOUT_MS = 10_000;
4
+ const sharedObjects = new Map([
5
+ ["host_config", { id: "local", display_name: "Local", kind: "local" }],
6
+ ["remote_connections", []],
7
+ ["remote_control_connections", []],
8
+ ]);
9
+ const pending = new Map();
10
+ const queue = [];
11
+ let nextId = 1;
12
+ let socket = null;
13
+ let socketOpen = false;
14
+ const nativeFetch = window.fetch?.bind(window);
15
+
16
+ if (nativeFetch) {
17
+ window.fetch = function codexWebappFetch(input, init) {
18
+ const url = typeof input === "string" ? input : input?.url;
19
+ if (String(url || "").startsWith("sentry-ipc://")) {
20
+ return Promise.resolve(new Response("{}", {
21
+ status: 200,
22
+ headers: { "content-type": "application/json" },
23
+ }));
24
+ }
25
+ return nativeFetch(input, init);
26
+ };
27
+ }
28
+
29
+ function connect() {
30
+ if (socket) return;
31
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
32
+ socket = new WebSocket(`${protocol}//${window.location.host}/__backend/ipc`);
33
+ socket.addEventListener("open", () => {
34
+ socketOpen = true;
35
+ while (queue.length > 0) socket.send(queue.shift());
36
+ });
37
+ socket.addEventListener("message", (event) => {
38
+ let message;
39
+ try {
40
+ message = JSON.parse(String(event.data));
41
+ } catch {
42
+ return;
43
+ }
44
+ if (message && message.id && pending.has(message.id)) {
45
+ const deferred = pending.get(message.id);
46
+ pending.delete(message.id);
47
+ clearTimeout(deferred.timer);
48
+ if (message.ok === false) {
49
+ deferred.reject(new Error(message.error || "Codex WebApp IPC request failed"));
50
+ return;
51
+ }
52
+ deferred.resolve(message.result);
53
+ return;
54
+ }
55
+ if (message && message.type === "event") {
56
+ receiveFromHost(message.payload);
57
+ }
58
+ });
59
+ socket.addEventListener("close", () => {
60
+ socketOpen = false;
61
+ socket = null;
62
+ });
63
+ }
64
+
65
+ function request(kind, payload) {
66
+ connect();
67
+ const id = String(nextId++);
68
+ const frame = JSON.stringify({ id, kind, payload });
69
+ if (socketOpen && socket?.readyState === WebSocket.OPEN) {
70
+ socket.send(frame);
71
+ } else {
72
+ queue.push(frame);
73
+ }
74
+ return new Promise((resolve, reject) => {
75
+ const timer = setTimeout(() => {
76
+ pending.delete(id);
77
+ reject(new Error(`Codex WebApp IPC timed out: ${kind}`));
78
+ }, REQUEST_TIMEOUT_MS);
79
+ pending.set(id, { resolve, reject, timer });
80
+ });
81
+ }
82
+
83
+ function receiveFromHost(payload) {
84
+ if (!payload || typeof payload !== "object") return;
85
+ if (payload.type === "shared-object-updated" && payload.key) {
86
+ sharedObjects.set(payload.key, payload.value);
87
+ }
88
+ window.dispatchEvent(new MessageEvent("message", {
89
+ data: payload,
90
+ origin: window.location.origin,
91
+ source: window,
92
+ }));
93
+ }
94
+
95
+ function sendMessageFromView(message) {
96
+ if (message?.type === "shared-object-set" && message.key) {
97
+ sharedObjects.set(message.key, message.value);
98
+ receiveFromHost({ type: "shared-object-updated", key: message.key, value: message.value });
99
+ }
100
+ return request("view-message", message).catch(() => undefined);
101
+ }
102
+
103
+ function sendWorkerMessageFromView(workerId, message) {
104
+ return request("worker-message", { workerId, message }).catch(() => undefined);
105
+ }
106
+
107
+ function subscribeToWorkerMessages(workerId, callback) {
108
+ const handler = (event) => {
109
+ const message = event.data;
110
+ if (message?.type === `codex_desktop:worker:${workerId}:for-view`) {
111
+ callback(message.payload);
112
+ }
113
+ };
114
+ window.addEventListener("message", handler);
115
+ return () => window.removeEventListener("message", handler);
116
+ }
117
+
118
+ function getSystemThemeVariant() {
119
+ return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light";
120
+ }
121
+
122
+ function subscribeToSystemThemeVariant(callback) {
123
+ const query = window.matchMedia?.("(prefers-color-scheme: dark)");
124
+ if (!query) return () => {};
125
+ const listener = () => callback(getSystemThemeVariant());
126
+ query.addEventListener?.("change", listener);
127
+ return () => query.removeEventListener?.("change", listener);
128
+ }
129
+
130
+ window.codexWindowType = "electron";
131
+ window.electronBridge = {
132
+ windowType: "electron",
133
+ sendMessageFromView,
134
+ getPathForFile(file) {
135
+ return file?.path || file?.name || "";
136
+ },
137
+ sendWorkerMessageFromView,
138
+ subscribeToWorkerMessages,
139
+ showContextMenu(options) {
140
+ return request("show-context-menu", options).catch(() => null);
141
+ },
142
+ showApplicationMenu(options) {
143
+ return request("show-application-menu", options).catch(() => null);
144
+ },
145
+ getFastModeRolloutMetrics() {
146
+ return Promise.resolve(null);
147
+ },
148
+ getSharedObjectSnapshotValue(key) {
149
+ return sharedObjects.get(key);
150
+ },
151
+ getSystemThemeVariant,
152
+ subscribeToSystemThemeVariant,
153
+ triggerSentryTestError() {
154
+ throw new Error("Codex WebApp browser Sentry test");
155
+ },
156
+ getSentryInitOptions() {
157
+ return {
158
+ appVersion: "0.0.0",
159
+ buildFlavor: "prod",
160
+ buildNumber: "0",
161
+ dsn: null,
162
+ environment: "browser",
163
+ codexAppSessionId: APP_SESSION_ID,
164
+ release: null,
165
+ };
166
+ },
167
+ getAppSessionId() {
168
+ return APP_SESSION_ID;
169
+ },
170
+ getBuildFlavor() {
171
+ return "prod";
172
+ },
173
+ };
174
+
175
+ connect();
176
+ })();
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { resolve } from "node:path";
3
3
 
4
- const DEFAULT_TEXT = "What should we";
4
+ const DEFAULT_TEXT = "Codex";
5
5
  const DEFAULT_MAX_RESPONSE_MS = 5_000;
6
6
 
7
7
  export function parseSmokeArgs(args = []) {
@@ -66,7 +66,7 @@ async function runBrowserSmokeWithOptions(options) {
66
66
  waitUntil: "networkidle",
67
67
  timeout: options.timeoutMs,
68
68
  });
69
- await page.getByText(options.text, { exact: false }).waitFor({
69
+ await page.waitForFunction((needle) => document.body?.innerText.includes(needle), options.text, {
70
70
  timeout: options.timeoutMs,
71
71
  });
72
72
  if (options.screenshot) {
@@ -0,0 +1,12 @@
1
+ export {
2
+ BROWSER_PRELOAD_ROUTE,
3
+ DEFAULT_CODEX_APP_ASAR,
4
+ DEFAULT_CODEX_APP_PATH,
5
+ DEFAULT_RENDERER_CACHE_ROOT,
6
+ createStaticRendererAssetSource as createStaticRenderer,
7
+ headersForAsset,
8
+ isUnsafeRendererPath,
9
+ matchRendererAsset,
10
+ prepareRendererAssetSource as prepareCodexAppRenderer,
11
+ streamAsset,
12
+ } from "./rendererAssetSource.js";
package/src/codexWeb.js CHANGED
@@ -1,6 +1,3 @@
1
- export const CODEX_WEB_UPSTREAM = "github:0xcaff/codex-web";
2
- export const CODEX_WEB_COMMIT = "585613f5a3a355af5aefc388ca4e31b07a472cda";
3
- export const CODEX_WEB_REFERENCE = `${CODEX_WEB_UPSTREAM}#${CODEX_WEB_COMMIT}`;
4
1
  export const DEFAULT_WEB_HOST = "127.0.0.1";
5
2
  export const DEFAULT_WEB_PORT = 8214;
6
3
 
@@ -21,21 +18,17 @@ export function buildWebUrl({ host = DEFAULT_WEB_HOST, port = DEFAULT_WEB_PORT }
21
18
  export function assertSafeHost(host, { allowNonLoopback = false } = {}) {
22
19
  if (allowNonLoopback || isLoopbackHost(host)) return;
23
20
  throw new Error(
24
- "Refusing to bind codex-web to a non-loopback host without --allow-non-loopback. Put Tailscale, Cloudflare Access, WireGuard, SSH tunneling, or an equivalent trusted boundary in front before remote access.",
21
+ "Refusing to bind Codex WebApp to a non-loopback host without --allow-non-loopback. Put Tailscale, Cloudflare Access, WireGuard, SSH tunneling, or an equivalent trusted boundary in front before remote access.",
25
22
  );
26
23
  }
27
24
 
28
- export function buildCodexWebNpxArgs({ host = DEFAULT_WEB_HOST, port = DEFAULT_WEB_PORT } = {}) {
29
- return [
30
- "--yes",
31
- "--package",
32
- CODEX_WEB_REFERENCE,
33
- "codex-web",
34
- "--host",
25
+ export function buildLocalServerSummary({ host = DEFAULT_WEB_HOST, port = DEFAULT_WEB_PORT } = {}) {
26
+ return {
35
27
  host,
36
- "--port",
37
- String(port),
38
- ];
28
+ port,
29
+ url: buildWebUrl({ host, port }),
30
+ runtime: "package-owned Codex App renderer bridge",
31
+ };
39
32
  }
40
33
 
41
34
  export function isLoopbackHost(host) {
package/src/commands.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from "node:child_process";
1
+ import { spawnSync } from "node:child_process";
2
2
  import { createServer } from "node:net";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
@@ -9,10 +9,10 @@ import {
9
9
  MIN_CODEX_VERSION,
10
10
  } from "./version.js";
11
11
  import { runSmoke } from "./browserSmoke.js";
12
+ import { startLocalServer } from "./localServer.js";
12
13
  import {
13
- CODEX_WEB_REFERENCE,
14
14
  assertSafeHost,
15
- buildCodexWebNpxArgs,
15
+ buildLocalServerSummary,
16
16
  buildWebUrl,
17
17
  parseStartArgs,
18
18
  } from "./codexWeb.js";
@@ -60,8 +60,9 @@ export function main(argv = process.argv) {
60
60
  }
61
61
 
62
62
  export function doctor() {
63
- const codex = spawnSync("codex", ["--version"], { encoding: "utf8" });
64
63
  const codexPath = resolveCodexPath();
64
+ const codexCommand = codexPath || "codex";
65
+ const codex = spawnSync(codexCommand, ["--version"], { encoding: "utf8" });
65
66
  if (codex.error || codex.status !== 0) {
66
67
  fail([
67
68
  "Codex CLI was not found.",
@@ -89,7 +90,7 @@ export function doctor() {
89
90
  ]);
90
91
  }
91
92
 
92
- const helpResult = spawnSync("codex", ["remote-control", "--help"], {
93
+ const helpResult = spawnSync(codexCommand, ["remote-control", "--help"], {
93
94
  encoding: "utf8",
94
95
  });
95
96
  if (helpResult.status !== 0) {
@@ -102,12 +103,25 @@ export function doctor() {
102
103
  ]);
103
104
  }
104
105
 
106
+ const appServerResult = spawnSync(codexCommand, ["app-server", "--help"], {
107
+ encoding: "utf8",
108
+ });
109
+ if (appServerResult.status !== 0) {
110
+ fail([
111
+ "`codex app-server --help` did not run successfully.",
112
+ "",
113
+ "Codex is installed, but the local app-server substrate is unavailable.",
114
+ "Update Codex and try again:",
115
+ " npm install -g @openai/codex@latest",
116
+ ]);
117
+ }
118
+
105
119
  ok([
106
120
  BANNER,
107
121
  `Codex CLI is ready: ${versionText}`,
108
122
  `Codex executable: ${codexPath || "unknown"}`,
109
123
  "`codex remote-control` is available.",
110
- `codex-web reference: ${CODEX_WEB_REFERENCE}`,
124
+ "`codex app-server` is available.",
111
125
  "Next:",
112
126
  " npx -y codex-webapp start",
113
127
  "",
@@ -117,20 +131,16 @@ export function doctor() {
117
131
 
118
132
  export async function start(args = []) {
119
133
  const options = parseStartArgs(args);
120
- const check = spawnSync("codex", ["remote-control", "--help"], {
134
+ const codexPath = resolveCodexPath();
135
+ const codexCommand = codexPath || process.env.CODEX_CLI_PATH || "codex";
136
+ const check = spawnSync(codexCommand, ["app-server", "--help"], {
121
137
  encoding: "utf8",
122
138
  });
123
- if (check.status !== 0) {
124
- fail([
125
- "Cannot start because `codex remote-control` is unavailable.",
126
- "Run:",
127
- " npx codex-webapp doctor",
128
- ]);
129
- }
139
+ const appServerReady = check.status === 0;
130
140
 
131
141
  assertSafeHost(options.host, { allowNonLoopback: options.allowNonLoopback });
132
142
  const plannedWebUrl = buildWebUrl({ host: options.host, port: options.port });
133
- const npxArgs = buildCodexWebNpxArgs({
143
+ const localServer = buildLocalServerSummary({
134
144
  host: options.host,
135
145
  port: options.port,
136
146
  });
@@ -139,12 +149,10 @@ export async function start(args = []) {
139
149
  ok([
140
150
  BANNER,
141
151
  "Dry run passed.",
142
- `Would start codex-web: ${plannedWebUrl}`,
152
+ `Would start Codex WebApp: ${plannedWebUrl}`,
153
+ `Runtime: ${localServer.runtime}`,
143
154
  "",
144
155
  ...codexAppFlowLines(plannedWebUrl),
145
- "",
146
- "Would start:",
147
- ` npx ${npxArgs.join(" ")}`,
148
156
  ]);
149
157
  return;
150
158
  }
@@ -153,7 +161,7 @@ export async function start(args = []) {
153
161
 
154
162
  if (!options.yes) {
155
163
  console.log(BANNER);
156
- console.log(`About to start codex-web: ${plannedWebUrl}`);
164
+ console.log(`About to start Codex WebApp: ${plannedWebUrl}`);
157
165
  console.log("Default expectation: keep it on localhost or behind Tailscale, Cloudflare Access, or an equivalent trusted boundary.");
158
166
  console.log("Anyone who can reach this URL can operate Codex on this host.");
159
167
  const rl = createInterface({ input, output });
@@ -165,22 +173,21 @@ export async function start(args = []) {
165
173
  }
166
174
  }
167
175
 
168
- console.log(`Starting codex-web from ${CODEX_WEB_REFERENCE}...`);
176
+ console.log("Starting Codex WebApp local server...");
177
+ if (!appServerReady) {
178
+ console.log("Warning: `codex app-server --help` did not pass. The UI will still open, but health will show the Codex substrate issue.");
179
+ console.log("Run `npx codex-webapp doctor` in another terminal for the exact fix.");
180
+ }
169
181
  console.log(`Open: ${plannedWebUrl}`);
170
182
  console.log("");
171
183
  for (const line of codexAppFlowLines(plannedWebUrl)) console.log(line);
172
184
  console.log("");
173
185
  console.log("Keep this terminal open. Expose it only through a trusted local, Tailscale, Cloudflare Access, or equivalent boundary.");
174
- const codexPath = resolveCodexPath();
175
- const child = spawn("npx", npxArgs, {
176
- stdio: "inherit",
177
- env: {
178
- ...process.env,
179
- CODEX_CLI_PATH: codexPath || process.env.CODEX_CLI_PATH || "codex",
180
- },
181
- });
182
- child.on("exit", (code) => {
183
- process.exit(code ?? 0);
186
+ await startLocalServer({
187
+ host: options.host,
188
+ port: options.port,
189
+ codexPath: codexCommand,
190
+ cwd: process.cwd(),
184
191
  });
185
192
  }
186
193
 
@@ -206,7 +213,7 @@ export function help() {
206
213
 
207
214
  Commands:
208
215
  codex-webapp doctor Check Codex CLI >= ${MIN_CODEX_VERSION}
209
- codex-webapp start Confirm, then start codex-web
216
+ codex-webapp start Confirm, then start Codex WebApp
210
217
  codex-webapp start --dry-run
211
218
  codex-webapp start --yes Start without the confirmation prompt
212
219
  codex-webapp start --port 8214
@@ -239,7 +246,7 @@ function fail(lines) {
239
246
  }
240
247
 
241
248
  export function resolveCodexPath() {
242
- const result = spawnSync("bash", ["-lc", "command -v codex"], {
249
+ const result = spawnSync("bash", ["-l", "-c", "which codex"], {
243
250
  encoding: "utf8",
244
251
  });
245
252
  if (result.status !== 0) return "";