From a1599fb32c0e78f5effacb355959d898e14b0f69 Mon Sep 17 00:00:00 2001
From: Val Alexander Restoring mobile pairing...
+ {t("root.loading.restoringMobilePairing")}
+
- Connecting to {APP_DISPLAY_NAME} server...
+ {t("root.loading.connectingServer", { appName: APP_DISPLAY_NAME })}
{message}
{details}
diff --git a/apps/web/src/routes/_chat.settings.index.tsx b/apps/web/src/routes/_chat.settings.index.tsx
index 8cc09865f..2db0bdde6 100644
--- a/apps/web/src/routes/_chat.settings.index.tsx
+++ b/apps/web/src/routes/_chat.settings.index.tsx
@@ -77,12 +77,19 @@ import {
getProviderStatusDescription,
getProviderStatusHeading,
} from "../components/chat/providerStatusPresentation";
+import { APP_LOCALE_PREFERENCES } from "../i18n/types";
+import { useT } from "../i18n/useI18n";
-const TIMESTAMP_FORMAT_LABELS = {
- locale: "System default",
- "12-hour": "12-hour",
- "24-hour": "24-hour",
-} as const;
+const TIMESTAMP_FORMAT_OPTIONS = [
+ { value: "locale", labelKey: "settings.general.timeFormat.option.locale" },
+ { value: "12-hour", labelKey: "settings.general.timeFormat.option.12Hour" },
+ { value: "24-hour", labelKey: "settings.general.timeFormat.option.24Hour" },
+] as const;
+
+const LANGUAGE_OPTIONS = APP_LOCALE_PREFERENCES.map((value) => ({
+ value,
+ labelKey: `settings.general.language.option.${value}` as const,
+}));
const PR_REVIEW_REQUEST_CHANGES_TONE_OPTIONS: ReadonlyArray<{
value: PrReviewRequestChangesTone;
@@ -435,6 +442,7 @@ function BuildInfoBlock({ label, buildInfo }: { label: string; buildInfo: BuildM
}
function SettingsRouteView() {
+ const { t } = useT();
const navigate = useNavigate();
const {
settingsState: { settings, defaults, updateSettings },
@@ -709,6 +717,15 @@ function SettingsRouteView() {
[settings, updateSettings],
);
+ const languageOptionLabel = (value: (typeof APP_LOCALE_PREFERENCES)[number]) =>
+ t(`settings.general.language.option.${value}`);
+
+ const timestampFormatOptionLabel = (value: (typeof TIMESTAMP_FORMAT_OPTIONS)[number]["value"]) =>
+ t(
+ TIMESTAMP_FORMAT_OPTIONS.find((option) => option.value === value)?.labelKey ??
+ "settings.general.timeFormat.option.locale",
+ );
+
return (
+
+ updateSettings({
+ locale: defaults.locale,
+ })
+ }
+ />
+ ) : null
+ }
+ control={
+
+ }
+ />
+
{
- if (value !== "locale" && value !== "12-hour" && value !== "24-hour") {
+ if (!TIMESTAMP_FORMAT_OPTIONS.some((option) => option.value === value)) {
return;
}
updateSettings({
- timestampFormat: value,
+ timestampFormat: value as (typeof TIMESTAMP_FORMAT_OPTIONS)[number]["value"],
});
}}
>
- {TIMESTAMP_FORMAT_LABELS[settings.timestampFormat]}
+
+ {timestampFormatOptionLabel(settings.timestampFormat)}
+
-
- {TIMESTAMP_FORMAT_LABELS.locale}
-
-
- {TIMESTAMP_FORMAT_LABELS["12-hour"]}
-
-
- {TIMESTAMP_FORMAT_LABELS["24-hour"]}
-
+ {TIMESTAMP_FORMAT_OPTIONS.map((option) => (
+
+ {timestampFormatOptionLabel(option.value)}
+
+ ))}
}
diff --git a/package.json b/package.json
index d34a8da54..655aa5a6e 100644
--- a/package.json
+++ b/package.json
@@ -35,13 +35,13 @@
"build": "turbo run build",
"build:marketing": "turbo run build --filter=@okcode/marketing",
"build:desktop": "turbo run build --filter=@okcode/desktop --filter=okcodes",
- "typecheck": "turbo run typecheck",
- "lint": "oxlint --report-unused-disable-directives",
+ "typecheck": "bunx turbo run typecheck",
+ "lint": "bunx oxlint --report-unused-disable-directives",
"test": "turbo run test",
"test:desktop-smoke": "turbo run smoke-test --filter=@okcode/desktop",
"test:desktop-release-smoke": "turbo run release-smoke --filter=@okcode/desktop",
- "fmt": "oxfmt",
- "fmt:check": "oxfmt --check",
+ "fmt": "bunx oxfmt",
+ "fmt:check": "bunx oxfmt --check",
"build:contracts": "turbo run build --filter=@okcode/contracts",
"dist:desktop:artifact": "node scripts/build-desktop-artifact.ts",
"dist:desktop:dmg": "node scripts/build-desktop-artifact.ts --platform mac --target dmg",
diff --git a/packages/contracts/package.json b/packages/contracts/package.json
index 9d6bbfcf3..c05f516cf 100644
--- a/packages/contracts/package.json
+++ b/packages/contracts/package.json
@@ -20,7 +20,7 @@
"dev": "tsdown src/index.ts --format esm,cjs --dts --watch --clean",
"build": "tsdown src/index.ts --format esm,cjs --dts --clean",
"prepare": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || node ../../scripts/patch-effect-language-service.ts",
- "typecheck": "tsc --noEmit",
+ "typecheck": "bunx tsc@5.7.3 --noEmit",
"test": "vitest run"
},
"dependencies": {
diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts
index c765ffbb4..e7154983b 100644
--- a/packages/contracts/src/orchestration.ts
+++ b/packages/contracts/src/orchestration.ts
@@ -746,7 +746,7 @@ export type ProjectDeletedReason = typeof ProjectDeletedReason.Type;
export const ProjectDeletedPayload = Schema.Struct({
projectId: ProjectId,
deletedAt: IsoDateTime,
- reason: ProjectDeletedReason.pipe(Schema.withDecodingDefault(() => "manual")),
+ reason: ProjectDeletedReason.pipe(Schema.withDecodingDefault(() => "manual" as const)),
});
export const ThreadCreatedPayload = Schema.Struct({
@@ -771,7 +771,7 @@ export type ThreadDeletedReason = typeof ThreadDeletedReason.Type;
export const ThreadDeletedPayload = Schema.Struct({
threadId: ThreadId,
deletedAt: IsoDateTime,
- reason: ThreadDeletedReason.pipe(Schema.withDecodingDefault(() => "manual")),
+ reason: ThreadDeletedReason.pipe(Schema.withDecodingDefault(() => "manual" as const)),
});
export const ThreadMetaUpdatedPayload = Schema.Struct({
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 5951bd026..21aa7fe1b 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -63,7 +63,7 @@
},
"scripts": {
"prepare": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || node ../../scripts/patch-effect-language-service.ts",
- "typecheck": "tsc --noEmit",
+ "typecheck": "bunx tsc@5.7.3 --noEmit",
"test": "vitest run"
},
"dependencies": {
From 46a85185edf0347c19cdca7fa4019ee2f76db0e4 Mon Sep 17 00:00:00 2001
From: Val Alexander
Date: Sun, 12 Apr 2026 21:56:26 -0500
Subject: [PATCH 2/2] Refine auth proxy and server event handling
- simplify request and header handling in the auth router
- make WebSocket HTTP routing await API handlers safely
- clean up gateway waiter iteration and scope normalization
---
apps/server/src/api/authRouter.ts | 6 +-
apps/server/src/api/router.test.ts | 135 +++++++++---------
apps/server/src/openclaw/GatewayClient.ts | 2 +-
.../Layers/OpenclawGatewayConfig.ts | 2 +-
apps/server/src/sme/authValidation.ts | 5 -
apps/server/src/wsServer.ts | 4 +-
6 files changed, 79 insertions(+), 75 deletions(-)
diff --git a/apps/server/src/api/authRouter.ts b/apps/server/src/api/authRouter.ts
index 0d04fc96e..c7f9cd29f 100644
--- a/apps/server/src/api/authRouter.ts
+++ b/apps/server/src/api/authRouter.ts
@@ -26,7 +26,7 @@ function respondJson(
): void {
res.writeHead(statusCode, {
"Content-Type": "application/json",
- ...(headers ?? {}),
+ ...headers,
});
res.end(JSON.stringify(body));
}
@@ -52,7 +52,9 @@ function mergeAnthropicBetaHeader(value: string | null): string {
return parts.join(",");
}
-async function readJsonRequestBody(req: http.IncomingMessage): Promise | null> {
+async function readJsonRequestBody(
+ req: http.IncomingMessage,
+): Promise | null> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
diff --git a/apps/server/src/api/router.test.ts b/apps/server/src/api/router.test.ts
index 7c5fbb89b..c507bb8d6 100644
--- a/apps/server/src/api/router.test.ts
+++ b/apps/server/src/api/router.test.ts
@@ -87,21 +87,24 @@ describe("createApiRouter", () => {
tokenManager,
});
- await withServer((req, res) => {
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
- void tryHandleApiRequest(req, res, url);
- }, async (baseUrl) => {
- const response = await request(baseUrl, "/api/pairing?ttl=300");
- const body = JSON.parse(response.body) as {
- pairingUrl: string;
- serverUrl: string;
- expiresAt: string;
- };
- expect(response.statusCode).toBe(200);
- expect(body.serverUrl).toBe("http://127.0.0.1:31337");
- expect(body.pairingUrl).toContain("okcode://pair?server=");
- expect(body.expiresAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
- });
+ await withServer(
+ (req, res) => {
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
+ void tryHandleApiRequest(req, res, url);
+ },
+ async (baseUrl) => {
+ const response = await request(baseUrl, "/api/pairing?ttl=300");
+ const body = JSON.parse(response.body) as {
+ pairingUrl: string;
+ serverUrl: string;
+ expiresAt: string;
+ };
+ expect(response.statusCode).toBe(200);
+ expect(body.serverUrl).toBe("http://127.0.0.1:31337");
+ expect(body.pairingUrl).toContain("okcode://pair?server=");
+ expect(body.expiresAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
+ },
+ );
});
it("proxies Anthropic message requests with the Claude Code envelope", async () => {
@@ -142,49 +145,50 @@ describe("createApiRouter", () => {
anthropicBaseUrl: `http://127.0.0.1:${upstreamAddress.port}`,
});
- await withServer((req, res) => {
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
- void tryHandleApiRequest(req, res, url);
- }, async (baseUrl) => {
- const response = await request(baseUrl, "/api/auth/anthropic/v1/messages", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "x-api-key": "test-key",
- "anthropic-version": "2023-06-01",
- "anthropic-beta": "tools-2024-04-04",
- },
- body: JSON.stringify({
+ await withServer(
+ (req, res) => {
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
+ void tryHandleApiRequest(req, res, url);
+ },
+ async (baseUrl) => {
+ const response = await request(baseUrl, "/api/auth/anthropic/v1/messages", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": "test-key",
+ "anthropic-version": "2023-06-01",
+ "anthropic-beta": "tools-2024-04-04",
+ },
+ body: JSON.stringify({
+ model: "claude-sonnet-4-20250514",
+ max_tokens: 64,
+ system: "Original system prompt",
+ messages: [{ role: "user", content: "Hello" }],
+ stream: true,
+ }),
+ });
+
+ expect(response.statusCode).toBe(200);
+ expect(JSON.parse(response.body)).toEqual({ ok: true, proxied: true });
+ expect(upstreamHeaders?.["x-api-key"]).toBe("test-key");
+ expect(upstreamHeaders?.["anthropic-version"]).toBe("2023-06-01");
+ expect(upstreamHeaders?.["anthropic-beta"]).toBe("claude-code-20250219,tools-2024-04-04");
+ expect(upstreamBody).toMatchObject({
model: "claude-sonnet-4-20250514",
- max_tokens: 64,
- system: "Original system prompt",
- messages: [{ role: "user", content: "Hello" }],
stream: true,
- }),
- });
-
- expect(response.statusCode).toBe(200);
- expect(JSON.parse(response.body)).toEqual({ ok: true, proxied: true });
- expect(upstreamHeaders?.["x-api-key"]).toBe("test-key");
- expect(upstreamHeaders?.["anthropic-version"]).toBe("2023-06-01");
- expect(upstreamHeaders?.["anthropic-beta"]).toBe(
- "claude-code-20250219,tools-2024-04-04",
- );
- expect(upstreamBody).toMatchObject({
- model: "claude-sonnet-4-20250514",
- stream: true,
- });
- expect(upstreamBody?.system).toEqual([
- {
- type: "text",
- text: "You are Claude Code, Anthropic's official CLI for Claude.",
- },
- {
- type: "text",
- text: "Original system prompt",
- },
- ]);
- });
+ });
+ expect(upstreamBody?.system).toEqual([
+ {
+ type: "text",
+ text: "You are Claude Code, Anthropic's official CLI for Claude.",
+ },
+ {
+ type: "text",
+ text: "Original system prompt",
+ },
+ ]);
+ },
+ );
});
it("returns a JSON 404 for unknown API routes", async () => {
@@ -195,13 +199,16 @@ describe("createApiRouter", () => {
tokenManager: new TokenManager(undefined),
});
- await withServer((req, res) => {
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
- void tryHandleApiRequest(req, res, url);
- }, async (baseUrl) => {
- const response = await request(baseUrl, "/api/unknown");
- expect(response.statusCode).toBe(404);
- expect(JSON.parse(response.body)).toEqual({ error: "Not Found" });
- });
+ await withServer(
+ (req, res) => {
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
+ void tryHandleApiRequest(req, res, url);
+ },
+ async (baseUrl) => {
+ const response = await request(baseUrl, "/api/unknown");
+ expect(response.statusCode).toBe(404);
+ expect(JSON.parse(response.body)).toEqual({ error: "Not Found" });
+ },
+ );
});
});
diff --git a/apps/server/src/openclaw/GatewayClient.ts b/apps/server/src/openclaw/GatewayClient.ts
index 734bbee23..3bff51c5f 100644
--- a/apps/server/src/openclaw/GatewayClient.ts
+++ b/apps/server/src/openclaw/GatewayClient.ts
@@ -466,7 +466,7 @@ export class OpenclawGatewayClient {
if (frame.type === "event" && typeof frame.event === "string") {
let matchedWaiter = false;
- for (const waiter of [...this.pendingEventWaiters]) {
+ for (const waiter of this.pendingEventWaiters) {
if (waiter.eventName === frame.event) {
matchedWaiter = true;
this.pendingEventWaiters.delete(waiter);
diff --git a/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts b/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts
index 26ec67d32..310e0939d 100644
--- a/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts
+++ b/apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts
@@ -68,7 +68,7 @@ function normalizeScopes(scopes: ReadonlyArray | undefined): string[] {
unique.add(trimmed);
}
}
- return [...unique].sort((left, right) => left.localeCompare(right));
+ return [...unique].toSorted((left, right) => left.localeCompare(right));
}
function fromGeneratedIdentity(identity: ReturnType) {
diff --git a/apps/server/src/sme/authValidation.ts b/apps/server/src/sme/authValidation.ts
index 3dfc18f99..20cadfbe5 100644
--- a/apps/server/src/sme/authValidation.ts
+++ b/apps/server/src/sme/authValidation.ts
@@ -18,11 +18,6 @@ import {
const OPENAI_MODEL_PROVIDERS = new Set(["openai"]);
-function normalizeOptionalValue(value: string | undefined | null): string | null {
- const trimmed = value?.trim();
- return trimmed && trimmed.length > 0 ? trimmed : null;
-}
-
export function getAllowedSmeAuthMethods(provider: ProviderKind): readonly SmeAuthMethod[] {
switch (provider) {
case "claudeAgent":
diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts
index 8aa9df6f0..066c855b0 100644
--- a/apps/server/src/wsServer.ts
+++ b/apps/server/src/wsServer.ts
@@ -647,7 +647,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
});
// HTTP server — serves static files or redirects to Vite dev server
- const httpServer = http.createServer((req, res) => {
+ const httpServer = http.createServer(async (req, res) => {
const respond = (
statusCode: number,
headers: Record,
@@ -660,7 +660,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
void Effect.runPromise(
Effect.gen(function* () {
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
- if (await tryHandleApiRequest(req, res, url)) {
+ if (yield* Effect.promise(() => tryHandleApiRequest(req, res, url))) {
return;
}