Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { basename, dirname } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

import { AppServerBridge } from "./lib/app-server-bridge.js";
import {
AppServerBridge,
buildLocalHostConfig,
resolveLocalHostKind,
} from "./lib/app-server-bridge.js";
import { normalizeCliArgv } from "./lib/cli-args.js";
import { loadCodexBundle, resolveDefaultCodexAppPath } from "./lib/codex-bundle.js";
import { renderBootstrapScript } from "./lib/bootstrap-script.js";
Expand Down Expand Up @@ -64,9 +68,11 @@ async function main(): Promise<void> {
buildFlavor: bundle.buildFlavor,
buildNumber: bundle.buildNumber,
}).catch(() => null);
const hostKind = await resolveLocalHostKind(process.cwd());
const relay = await AppServerBridge.connect({
appPath: options.appPath,
cwd: process.cwd(),
hostKind,
});

const sentryOptions: SentryInitOptions = {
Expand Down Expand Up @@ -97,6 +103,7 @@ async function main(): Promise<void> {
return patchIndexHtml(indexHtml, {
bootstrapScript: renderBootstrapScript({
devMode: options.devMode,
hostConfig: buildLocalHostConfig("local", hostKind),
sentryOptions,
stylesheetHref: POCODEX_STYLESHEET_HREF,
importIconSvg: await readFile(importIconSvgPath, "utf8"),
Expand Down
35 changes: 29 additions & 6 deletions src/lib/app-server-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,22 @@ interface AppServerBridgeOptions {
appPath: string;
cwd: string;
hostId?: string;
hostKind?: LocalHostKind;
codexHomePath?: string;
persistedAtomRegistryPath?: string;
workspaceRootRegistryPath?: string;
gitWorkerBridge?: CodexDesktopGitWorkerBridge;
codexCliPath?: string;
}

export type LocalHostKind = "git" | "local";

export interface LocalHostConfig {
id: string;
display_name: string;
kind: LocalHostKind;
}

interface WhamUsageCredits {
has_credits: boolean;
unlimited: boolean;
Expand Down Expand Up @@ -272,6 +281,7 @@ const LOCAL_UNSUPPORTED_FETCH_BODY = {
export class AppServerBridge extends EventEmitter implements HostBridge {
private readonly child: ChildProcessWithoutNullStreams;
private readonly hostId: string;
private readonly hostKind: LocalHostKind;
private readonly cwd: string;
private readonly terminalManager: TerminalSessionManager;
private readonly localRequests = new Map<
Expand Down Expand Up @@ -316,6 +326,7 @@ export class AppServerBridge extends EventEmitter implements HostBridge {
private constructor(options: AppServerBridgeOptions) {
super();
this.hostId = options.hostId ?? "local";
this.hostKind = options.hostKind ?? "local";
this.cwd = options.cwd;
this.codexHomePath = options.codexHomePath ?? deriveCodexHomePath();
this.persistedAtomRegistryPath =
Expand Down Expand Up @@ -2990,12 +3001,8 @@ export class AppServerBridge extends EventEmitter implements HostBridge {
});
}

private buildHostConfig(): Record<string, string> {
return {
id: this.hostId,
display_name: "Local",
kind: "local",
};
private buildHostConfig(): LocalHostConfig {
return buildLocalHostConfig(this.hostId, this.hostKind);
}

private emitFetchSuccess(
Expand Down Expand Up @@ -3588,6 +3595,22 @@ async function resolveGitOrigins(
};
}

export function buildLocalHostConfig(
hostId = "local",
hostKind: LocalHostKind = "local",
): LocalHostConfig {
return {
id: hostId,
display_name: "Local",
kind: hostKind,
};
}

export async function resolveLocalHostKind(cwd: string): Promise<LocalHostKind> {
const repository = await resolveGitRepository(cwd, new Map<string, GitRepositoryInfo>());
return repository ? "git" : "local";
}

async function resolveGitOrigin(
dir: string,
repositoriesByRoot: Map<string, GitRepositoryInfo>,
Expand Down
80 changes: 80 additions & 0 deletions src/lib/bootstrap-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { serializeInlineScript } from "./inline-script.js";

export interface BootstrapScriptConfig {
devMode?: boolean;
hostConfig?: {
id: string;
display_name: string;
kind: "git" | "local";
};
sentryOptions: SentryInitOptions;
stylesheetHref: string;
importIconSvg?: string;
Expand Down Expand Up @@ -99,8 +104,12 @@ function bootstrapPocodexInBrowser(config: BootstrapScriptConfig): void {
windowType: "electron";
sendMessageFromView(message: unknown): Promise<void>;
getPathForFile(): null;
getSharedObjectSnapshotValue?(key: string): unknown;
getSystemThemeVariant?(): "light" | "dark";
sendWorkerMessageFromView(workerName: string, message: unknown): Promise<void>;
showApplicationMenu?(menuId: string, x: number, y: number): Promise<void>;
subscribeToWorkerMessages(workerName: string, callback: WorkerMessageListener): () => void;
subscribeToSystemThemeVariant?(callback: () => void): () => void;
showContextMenu(): Promise<void>;
getFastModeRolloutMetrics(): Promise<Record<string, never>>;
triggerSentryTestError(): Promise<void>;
Expand Down Expand Up @@ -146,6 +155,17 @@ function bootstrapPocodexInBrowser(config: BootstrapScriptConfig): void {

const workerSubscribers = new Map<string, Set<WorkerMessageListener>>();
const restorableTerminalAttachments = new Map<string, RestorableTerminalAttachment>();
const sharedObjectSnapshots = new Map<string, unknown>([
[
"host_config",
config.hostConfig ?? {
id: LOCAL_HOST_ID,
display_name: "Local",
kind: "local",
},
],
["remote_connections", []],
]);
const pendingMessages: string[] = [];
const toastHost = document.createElement("div");
const statusHost = document.createElement("div");
Expand Down Expand Up @@ -2323,11 +2343,22 @@ function bootstrapPocodexInBrowser(config: BootstrapScriptConfig): void {
const overriddenMessage = overrideEnterBehaviorInMessage(message);
rememberDispatchedEnterBehavior(overriddenMessage);
syncSidebarModeWithBridgeMessage(overriddenMessage);
if (isRecord(overriddenMessage)) {
rememberSharedObjectSnapshot(overriddenMessage);
}
syncThreadQueryWithBridgeMessage(overriddenMessage);
syncRestorableTerminalAttachments(overriddenMessage, "incoming");
return overriddenMessage;
}

function rememberSharedObjectSnapshot(message: Record<string, unknown>): void {
if (message.type !== "shared-object-updated" || typeof message.key !== "string") {
return;
}

sharedObjectSnapshots.set(message.key, message.value ?? null);
}

function overrideEnterBehaviorInMessage(message: Record<string, unknown>): unknown {
if (!shouldForceNewlineEnterBehavior()) {
return message;
Expand Down Expand Up @@ -3478,6 +3509,48 @@ function bootstrapPocodexInBrowser(config: BootstrapScriptConfig): void {
return typeof value === "object" && value !== null;
}

function readSystemThemeVariant(): "light" | "dark" {
if (typeof window.matchMedia !== "function") {
return "light";
}

return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}

function observeSystemThemeVariant(callback: () => void): () => void {
if (typeof window.matchMedia !== "function") {
return () => {};
}

const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
const listener = () => {
callback();
};

if (typeof mediaQueryList.addEventListener === "function") {
mediaQueryList.addEventListener("change", listener);
return () => {
mediaQueryList.removeEventListener("change", listener);
};
}

const legacyMediaQueryList = mediaQueryList as MediaQueryList & {
addListener?: (listener: (event: unknown) => void) => void;
removeListener?: (listener: (event: unknown) => void) => void;
};
if (
typeof legacyMediaQueryList.addListener === "function" &&
typeof legacyMediaQueryList.removeListener === "function"
) {
legacyMediaQueryList.addListener(listener);
return () => {
legacyMediaQueryList.removeListener?.(listener);
};
}

return () => {};
}

function addWorkerSubscriber(workerName: string, callback: WorkerMessageListener): () => void {
let listeners = workerSubscribers.get(workerName);
if (!listeners) {
Expand Down Expand Up @@ -3518,10 +3591,16 @@ function bootstrapPocodexInBrowser(config: BootstrapScriptConfig): void {
}
},
getPathForFile: () => null,
getSharedObjectSnapshotValue: (key) => sharedObjectSnapshots.get(key) ?? null,
getSystemThemeVariant: () => readSystemThemeVariant(),
sendWorkerMessageFromView: async (workerName, message) => {
sendEnvelope({ type: "worker_message", workerName, message });
},
showApplicationMenu: async () => {
showNotice("Application menus are not available in Pocodex.");
},
subscribeToWorkerMessages: (workerName, callback) => addWorkerSubscriber(workerName, callback),
subscribeToSystemThemeVariant: (callback) => observeSystemThemeVariant(callback),
showContextMenu: async () => {
showNotice("Context menus are not available in Pocodex.");
},
Expand All @@ -3533,6 +3612,7 @@ function bootstrapPocodexInBrowser(config: BootstrapScriptConfig): void {
};

const nativeFetch: typeof window.fetch = window.fetch.bind(window);

window.fetch = (input, init) => {
const url =
typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
Expand Down
9 changes: 7 additions & 2 deletions src/lib/codex-desktop-git-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { randomUUID } from "node:crypto";
import { EventEmitter } from "node:events";
import { cp, mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { platform, tmpdir } from "node:os";
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
import process from "node:process";
import { Worker } from "node:worker_threads";
Expand All @@ -11,6 +11,7 @@ import { ensureCodexDesktopWorkerScript, type CodexDesktopWorkerScript } from ".
import { debugLog, isDebugEnabled } from "./debug.js";

type GitWorkerMainRpcMethod =
| "platform-family"
| "worktree-cleanup-inputs"
| "fs-read-file"
| "fs-write-file"
Expand Down Expand Up @@ -352,12 +353,12 @@ export class DefaultCodexDesktopGitWorkerBridge
});

worker.on("message", (message) => {
const responseId = extractWorkerResponseId(message);
if (isWorkerMainRpcRequestEnvelope(message)) {
void this.handleMainRpcRequest(worker, message);
return;
}

const responseId = extractWorkerResponseId(message);
if (responseId) {
this.pendingRequests.delete(responseId);
}
Expand Down Expand Up @@ -408,6 +409,10 @@ export class DefaultCodexDesktopGitWorkerBridge

try {
switch (message.method as GitWorkerMainRpcMethod) {
case "platform-family": {
postMainRpcSuccess(worker, message, platform() === "win32" ? "windows" : "unix");
return;
}
case "worktree-cleanup-inputs": {
const params = parseWorktreeCleanupInputs(message.params);
postMainRpcSuccess(worker, message, {
Expand Down
9 changes: 8 additions & 1 deletion src/lib/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { basename, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { EventEmitter } from "node:events";

import { AppServerBridge } from "./app-server-bridge.js";
import {
AppServerBridge,
buildLocalHostConfig,
resolveLocalHostKind,
} from "./app-server-bridge.js";
import { renderBootstrapScript } from "./bootstrap-script.js";
import { loadCodexBundle } from "./codex-bundle.js";
import { patchIndexHtml } from "./html-patcher.js";
Expand Down Expand Up @@ -152,10 +156,12 @@ class ManagedPocodexRuntime extends EventEmitter implements PocodexRuntime {
const bundle = await loadCodexBundle(this.options.appPath);
const pocodexCssPath = fileURLToPath(new URL("../pocodex.css", import.meta.url));
const importIconSvgPath = fileURLToPath(new URL("../images/import.svg", import.meta.url));
const hostKind = await resolveLocalHostKind(this.options.cwd);

relay = await AppServerBridge.connect({
appPath: this.options.appPath,
cwd: this.options.cwd,
hostKind,
});

relayErrorListener = (error) => {
Expand Down Expand Up @@ -191,6 +197,7 @@ class ManagedPocodexRuntime extends EventEmitter implements PocodexRuntime {
return patchIndexHtml(indexHtml, {
bootstrapScript: renderBootstrapScript({
devMode: this.options.devMode,
hostConfig: buildLocalHostConfig("local", hostKind),
sentryOptions,
stylesheetHref: POCODEX_STYLESHEET_HREF,
importIconSvg: await readFile(importIconSvgPath, "utf8"),
Expand Down
37 changes: 37 additions & 0 deletions test/app-server-bridge-host-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { execFileSync } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, describe, expect, it } from "vitest";

import { buildLocalHostConfig, resolveLocalHostKind } from "../src/lib/app-server-bridge.js";

const tempDirectories: string[] = [];

afterEach(async () => {
await Promise.all(
tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })),
);
});

describe("local host config helpers", () => {
it("builds host config for git-backed workspaces", () => {
expect(buildLocalHostConfig("workspace", "git")).toEqual({
id: "workspace",
display_name: "Local",
kind: "git",
});
});

it("detects whether a workspace is git-backed", async () => {
const directory = await mkdtemp(join(tmpdir(), "pocodex-host-kind-"));
tempDirectories.push(directory);

await expect(resolveLocalHostKind(directory)).resolves.toBe("local");

execFileSync("git", ["init", "-q"], { cwd: directory });

await expect(resolveLocalHostKind(directory)).resolves.toBe("git");
});
});
Loading