Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0ca11a5
Allow spaces and parentheses in markdown file links
Jun 1, 2026
a05b8bb
Add terminal title override and mouse reset regression tests
Jun 1, 2026
093c1b3
Wire ChangedFilesTree to persist per-turn directory expansion state
Jun 1, 2026
f5cbe99
Wire terminal pane tab inline rename to store action
Jun 1, 2026
7e27b11
Add plan review panel with annotations and composer export
Jun 1, 2026
6c0f383
Add group tab inline rename with store persistence
Jun 1, 2026
9c35792
Add desktop exposure and platform regressions
Jun 2, 2026
989a437
Share auth HTTP route metadata
Jun 2, 2026
03573a7
Use schema-backed runtime cache writes
Jun 2, 2026
d140e0e
fixup! Share auth HTTP route metadata
Jun 2, 2026
3be5f3c
Add typed voice transcription auth errors
Jun 2, 2026
b9d50f8
Close upgraded sockets during server shutdown
Jun 2, 2026
b387adb
fixup! Add typed voice transcription auth errors
Jun 2, 2026
1231bd0
Recover desktop threads with missing projects
Jun 2, 2026
2a1ddff
Cover assistant markdown output in browser
Jun 2, 2026
fe6a9f3
Gate translucent Electron material to macOS
Jun 2, 2026
6f466ae
Add Codex launch args contracts
Jun 2, 2026
c276b41
Map Codex launch args through app settings
Jun 2, 2026
f120692
Pass Codex launch args to app-server
Jun 2, 2026
2ec7222
Expose Codex launch args in settings
Jun 2, 2026
aaac0f0
Migrate legacy keybinding commands
Jun 2, 2026
d3acf7f
Cover legacy keybinding migration paths
Jun 2, 2026
7cc2251
Format upstream roadmap UI changes
Jun 2, 2026
b7220af
Add provider start option contracts
Jun 2, 2026
c42d72d
Apply Codex options to discovery
Jun 2, 2026
645c6a6
Pass Codex options through web discovery
Jun 2, 2026
a30ca76
Fix changed files expansion state
Jun 2, 2026
2dd7515
Tighten plan and terminal review paths
Jun 2, 2026
e788479
Extract terminal inline rename field
Jun 2, 2026
6e22492
Preoptimize React DOM browser tests
Jun 2, 2026
82b73f9
Extract Codex provider options key
Jun 2, 2026
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
28 changes: 28 additions & 0 deletions apps/desktop/src/desktopServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,32 @@ describe("desktopServerExposure", () => {
"http://192.168.1.44:58090",
);
});

it("falls back to loopback when network mode has no usable LAN address", () => {
const state = resolveDesktopServerExposureState({
mode: "network-accessible",
activeMode: "network-accessible",
port: 58090,
networkInterfaces: {
en0: [{ address: "169.254.1.20", family: "IPv4", internal: false }],
lo: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
},
});

expect(state.endpointUrl).toBeNull();

const endpoints = resolveDesktopAdvertisedEndpoints(state);

expect(endpoints).toEqual([
{
id: "desktop-loopback:58090",
label: "This machine",
httpBaseUrl: "http://127.0.0.1:58090",
wsBaseUrl: "ws://127.0.0.1:58090",
reachability: "loopback",
isDefault: true,
description: "Reachable from this desktop app and browser on the same machine.",
},
]);
});
});
51 changes: 41 additions & 10 deletions apps/server/src/auth/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type http from "node:http";
import {
AuthBootstrapInput,
AuthCreatePairingCredentialInput,
AuthHttpRoutes,
AuthRevokeClientSessionInput,
AuthRevokePairingLinkInput,
} from "@jcode/contracts";
Expand Down Expand Up @@ -216,13 +217,19 @@ export const serveAuthHttpRoute = Effect.fn(function* (input: AuthHttpRouteOptio
const headers = authRequest.headers;

const route = Effect.gen(function* () {
if (method === "GET" && input.url.pathname === "/api/auth/session") {
if (
method === AuthHttpRoutes.session.method &&
input.url.pathname === AuthHttpRoutes.session.pathname
) {
const session = yield* input.serverAuth.getSessionState(authRequest);
respondJson(input.respond, 200, session);
return;
}

if (method === "POST" && input.url.pathname === "/api/auth/bootstrap") {
if (
method === AuthHttpRoutes.bootstrap.method &&
input.url.pathname === AuthHttpRoutes.bootstrap.pathname
) {
const payload = yield* readJsonBody(input.req, "Invalid bootstrap payload.").pipe(
Effect.flatMap((body) =>
decodeBootstrapInput(body).pipe(
Expand Down Expand Up @@ -251,7 +258,10 @@ export const serveAuthHttpRoute = Effect.fn(function* (input: AuthHttpRouteOptio
return;
}

if (method === "POST" && input.url.pathname === "/api/auth/bootstrap/bearer") {
if (
method === AuthHttpRoutes.bootstrapBearer.method &&
input.url.pathname === AuthHttpRoutes.bootstrapBearer.pathname
) {
const payload = yield* readJsonBody(input.req, "Invalid bootstrap payload.").pipe(
Effect.flatMap((body) =>
decodeBootstrapInput(body).pipe(
Expand Down Expand Up @@ -299,14 +309,20 @@ export const serveAuthHttpRoute = Effect.fn(function* (input: AuthHttpRouteOptio
return;
}

if (method === "POST" && input.url.pathname === "/api/auth/ws-token") {
if (
method === AuthHttpRoutes.webSocketToken.method &&
input.url.pathname === AuthHttpRoutes.webSocketToken.pathname
) {
const session = yield* input.serverAuth.authenticateHttpRequest(authRequest);
const result = yield* input.serverAuth.issueWebSocketToken(session);
respondJson(input.respond, 200, result);
return;
}

if (method === "POST" && input.url.pathname === "/api/auth/pairing-token") {
if (
method === AuthHttpRoutes.pairingToken.method &&
input.url.pathname === AuthHttpRoutes.pairingToken.pathname
) {
const session = yield* input.serverAuth.authenticateHttpRequest(authRequest);
if (session.role !== "owner") {
return yield* new AuthError({
Expand Down Expand Up @@ -335,14 +351,20 @@ export const serveAuthHttpRoute = Effect.fn(function* (input: AuthHttpRouteOptio
return;
}

if (method === "GET" && input.url.pathname === "/api/auth/pairing-links") {
if (
method === AuthHttpRoutes.pairingLinks.method &&
input.url.pathname === AuthHttpRoutes.pairingLinks.pathname
) {
yield* authenticateOwnerSession({ serverAuth: input.serverAuth, authRequest });
const pairingLinks = yield* input.serverAuth.listPairingLinks();
respondJson(input.respond, 200, pairingLinks);
return;
}

if (method === "POST" && input.url.pathname === "/api/auth/pairing-links/revoke") {
if (
method === AuthHttpRoutes.revokePairingLink.method &&
input.url.pathname === AuthHttpRoutes.revokePairingLink.pathname
) {
yield* authenticateOwnerSession({ serverAuth: input.serverAuth, authRequest });
const payload = yield* readJsonBody(input.req, "Invalid revoke pairing link payload.").pipe(
Effect.flatMap((body) =>
Expand All @@ -363,7 +385,10 @@ export const serveAuthHttpRoute = Effect.fn(function* (input: AuthHttpRouteOptio
return;
}

if (method === "GET" && input.url.pathname === "/api/auth/clients") {
if (
method === AuthHttpRoutes.clients.method &&
input.url.pathname === AuthHttpRoutes.clients.pathname
) {
const session = yield* authenticateOwnerSession({
serverAuth: input.serverAuth,
authRequest,
Expand All @@ -373,7 +398,10 @@ export const serveAuthHttpRoute = Effect.fn(function* (input: AuthHttpRouteOptio
return;
}

if (method === "POST" && input.url.pathname === "/api/auth/clients/revoke") {
if (
method === AuthHttpRoutes.revokeClient.method &&
input.url.pathname === AuthHttpRoutes.revokeClient.pathname
) {
const session = yield* authenticateOwnerSession({
serverAuth: input.serverAuth,
authRequest,
Expand All @@ -400,7 +428,10 @@ export const serveAuthHttpRoute = Effect.fn(function* (input: AuthHttpRouteOptio
return;
}

if (method === "POST" && input.url.pathname === "/api/auth/clients/revoke-others") {
if (
method === AuthHttpRoutes.revokeOtherClients.method &&
input.url.pathname === AuthHttpRoutes.revokeOtherClients.pathname
) {
const session = yield* authenticateOwnerSession({
serverAuth: input.serverAuth,
authRequest,
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/auth/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,15 @@ describe("auth utils", () => {
browser: "Chrome",
});
});

it("classifies Linux user agents before Mac compatibility tokens", () => {
expect(
deriveAuthClientMetadata({
headers: {
"user-agent":
"Mozilla/5.0 (X11; Linux x86_64; Macintosh; Intel Mac OS X) AppleWebKit Chrome/123",
},
}).os,
).toBe("Linux");
});
});
2 changes: 1 addition & 1 deletion apps/server/src/auth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ function inferOs(userAgent: string | undefined): string | undefined {
if (!normalized) return undefined;
if (/iphone|ipad|ipod/.test(normalized)) return "iOS";
if (/android/.test(normalized)) return "Android";
if (/mac os x|macintosh/.test(normalized)) return "macOS";
if (/windows nt/.test(normalized)) return "Windows";
if (/linux/.test(normalized)) return "Linux";
if (/mac os x|macintosh/.test(normalized)) return "macOS";
return undefined;
}

Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ApprovalRequestId, ThreadId } from "@jcode/contracts";

import {
buildCodexProcessEnv,
buildCodexAppServerArgs,
buildCodexInitializeParams,
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
Expand Down Expand Up @@ -532,6 +533,13 @@ describe("resolveCodexModelForAccount", () => {
});

describe("startSession", () => {
it("places Codex launch arguments before the app-server subcommand", () => {
expect(
buildCodexAppServerArgs('--config model_provider=ollama --sandbox "danger full"'),
).toEqual(["--config", "model_provider=ollama", "--sandbox", "danger full", "app-server"]);
expect(buildCodexAppServerArgs(" ")).toEqual(["app-server"]);
});

it("enables Codex experimental api capabilities during initialize", () => {
expect(buildCodexInitializeParams()).toEqual({
clientInfo: {
Expand Down
82 changes: 78 additions & 4 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ import {
resolveJCodeCodexHomeOverlayPath,
shouldDisableJCodeBrowserPlugin,
} from "./codexHomePaths.ts";
import { transcribeVoiceWithChatGptSession } from "./voiceTranscription.ts";
import {
transcribeVoiceWithChatGptSession,
VoiceTranscriptionAuthExpiredError,
} from "./voiceTranscription.ts";

type PendingRequestKey = string;

Expand Down Expand Up @@ -378,6 +381,71 @@ function asString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}

function parseCodexLaunchArgs(value: string | undefined): string[] {
const source = value?.trim();
if (!source) {
return [];
}

const args: string[] = [];
let current = "";
let quote: '"' | "'" | null = null;
let escaping = false;

for (const char of source) {
if (escaping) {
current += char;
escaping = false;
continue;
}

if (char === "\\") {
escaping = true;
continue;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (quote) {
if (char === quote) {
quote = null;
} else {
current += char;
}
continue;
}

if (char === '"' || char === "'") {
quote = char;
continue;
}

if (/\s/.test(char)) {
if (current.length > 0) {
args.push(current);
current = "";
}
continue;
}

current += char;
}

if (escaping) {
current += "\\";
}
if (quote) {
throw new Error("Codex launch arguments contain an unterminated quoted string.");
}
if (current.length > 0) {
args.push(current);
}

return args;
}

export function buildCodexAppServerArgs(launchArgs?: string): string[] {
return [...parseCodexLaunchArgs(launchArgs), "app-server"];
}

export function buildCodexProcessEnv(
input: {
readonly env?: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -893,12 +961,13 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
const codexOptions = readCodexProviderOptions(input);
const codexBinaryPath = codexOptions.binaryPath ?? "codex";
const codexHomePath = codexOptions.homePath;
const codexLaunchArgs = codexOptions.launchArgs;
this.assertSupportedCodexCliVersion({
binaryPath: codexBinaryPath,
cwd: resolvedCwd,
...(codexHomePath ? { homePath: codexHomePath } : {}),
});
const child = spawn(codexBinaryPath, ["app-server"], {
const child = spawn(codexBinaryPath, buildCodexAppServerArgs(codexLaunchArgs), {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
cwd: resolvedCwd,
env: buildCodexProcessEnv({
...(codexHomePath ? { homePath: codexHomePath } : {}),
Expand Down Expand Up @@ -1511,12 +1580,13 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
});
const codexBinaryPath = codexOptions.binaryPath ?? "codex";
const codexHomePath = codexOptions.homePath;
const codexLaunchArgs = codexOptions.launchArgs;
this.assertSupportedCodexCliVersion({
binaryPath: codexBinaryPath,
cwd: resolvedCwd,
...(codexHomePath ? { homePath: codexHomePath } : {}),
});
const child = spawn(codexBinaryPath, ["app-server"], {
const child = spawn(codexBinaryPath, buildCodexAppServerArgs(codexLaunchArgs), {
cwd: resolvedCwd,
env: buildCodexProcessEnv({
...(codexHomePath ? { homePath: codexHomePath } : {}),
Expand Down Expand Up @@ -2053,7 +2123,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
}

if (!token) {
throw new Error("No ChatGPT session token is available. Sign in to ChatGPT in Codex.");
throw new VoiceTranscriptionAuthExpiredError(
"No ChatGPT session token is available. Sign in to ChatGPT in Codex.",
);
}
if (authMethod !== "chatgpt" && authMethod !== "chatgptAuthTokens") {
throw new Error("Voice transcription requires a ChatGPT-authenticated Codex session.");
Expand Down Expand Up @@ -3424,6 +3496,7 @@ function normalizeProviderThreadId(value: string | undefined): string | undefine
function readCodexProviderOptions(input: CodexAppServerStartSessionInput): {
readonly binaryPath?: string;
readonly homePath?: string;
readonly launchArgs?: string;
} {
const options = input.providerOptions?.codex;
if (!options) {
Expand All @@ -3432,6 +3505,7 @@ function readCodexProviderOptions(input: CodexAppServerStartSessionInput): {
return {
...(options.binaryPath ? { binaryPath: options.binaryPath } : {}),
...(options.homePath ? { homePath: options.homePath } : {}),
...(options.launchArgs ? { launchArgs: options.launchArgs } : {}),
};
}

Expand Down
Loading
Loading