diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1acc35d37..fe8e749a3 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -9,7 +9,7 @@ "dev:electron": "bun run scripts/dev-electron.mjs", "build": "tsdown", "start": "bun run scripts/start-electron.mjs", - "typecheck": "tsc --noEmit", + "typecheck": "bunx tsc@5.7.3 --noEmit", "test": "vitest run --passWithNoTests", "smoke-test": "node scripts/smoke-test.mjs", "release-smoke": "node ../../scripts/release-smoke.ts" diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 882effbd3..78bddc3c4 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -10,7 +10,7 @@ "preview": "next start", "start": "next start", "lint": "eslint .", - "typecheck": "tsc --noEmit" + "typecheck": "bunx tsc@5.7.3 --noEmit" }, "dependencies": { "@hookform/resolvers": "^3.10.0", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 87bfb8299..caa5993ca 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "bun run scripts/build-mobile-shell.mjs", - "typecheck": "tsc --noEmit", + "typecheck": "bunx tsc@5.7.3 --noEmit", "test": "vitest run --passWithNoTests", "sync": "cap sync", "open:ios": "cap open ios", diff --git a/apps/server/package.json b/apps/server/package.json index d9801b1fb..5ee5f7a6d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -19,7 +19,7 @@ "build": "node scripts/cli.ts build", "start": "node dist/index.mjs", "prepare": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || (node ../../scripts/patch-effect-language-service.ts && node ../../scripts/patch-effect-smol-peer-installs.mjs)", - "typecheck": "tsc --noEmit", + "typecheck": "bunx tsc@5.7.3 --noEmit", "test": "vitest run" }, "dependencies": { 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; } diff --git a/apps/web/package.json b/apps/web/package.json index 68cbcbada..f9fd93111 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,7 @@ "build": "vite build", "prepare": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || node ../../scripts/patch-effect-language-service.ts", "preview": "vite preview", - "typecheck": "tsc --noEmit", + "typecheck": "bunx tsc@5.7.3 --noEmit", "test": "vitest run --passWithNoTests", "test:browser": "node ../../scripts/run-browser-tests.mjs vitest.browser.config.ts", "test:browser:install": "playwright install --with-deps chromium" diff --git a/apps/web/src/components/settings/SettingsRouteContext.tsx b/apps/web/src/components/settings/SettingsRouteContext.tsx index b83e77fd9..0967140d1 100644 --- a/apps/web/src/components/settings/SettingsRouteContext.tsx +++ b/apps/web/src/components/settings/SettingsRouteContext.tsx @@ -106,6 +106,7 @@ export function SettingsRouteContextProvider({ children }: { children: ReactNode ? ["PR request changes button"] : []), ...(settings.timestampFormat !== defaults.timestampFormat ? ["Time format"] : []), + ...(settings.locale !== defaults.locale ? ["Language"] : []), ...(settings.showStitchBorder !== defaults.showStitchBorder ? ["Stitch border"] : []), ...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming ? ["Assistant output"] diff --git a/apps/web/src/components/settings/SettingsShell.tsx b/apps/web/src/components/settings/SettingsShell.tsx index ede0e4555..eabba642f 100644 --- a/apps/web/src/components/settings/SettingsShell.tsx +++ b/apps/web/src/components/settings/SettingsShell.tsx @@ -17,6 +17,7 @@ import { Button } from "../ui/button"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { SidebarInset, SidebarTrigger } from "../ui/sidebar"; import { cn } from "../../lib/utils"; +import { useT } from "../../i18n/useI18n"; export type SettingsSectionId = | "general" @@ -104,6 +105,7 @@ export function SettingsShell({ children: ReactNode; }) { const navigate = useNavigate(); + const { t } = useT(); const activeItemLabel = useMemo( () => SETTINGS_NAV_ITEMS.find((item) => item.id === activeItem)?.label ?? "Settings", [activeItem], @@ -142,7 +144,7 @@ export function SettingsShell({ disabled={changedSettingLabels.length === 0} onClick={() => void onRestoreDefaults()} > - Restore defaults + {t("common.actions.restoreDefaults")} @@ -163,7 +165,7 @@ export function SettingsShell({ disabled={changedSettingLabels.length === 0} onClick={() => void onRestoreDefaults()} > - Restore defaults + {t("common.actions.restoreDefaults")} diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 1c4fa8e06..063b5868b 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -38,6 +38,10 @@ type SidebarContextProps = { toggleSidebar: () => void; }; +type CookieStoreLike = { + set(options: { expires: number; name: string; path: string; value: string }): Promise; +}; + type SidebarResizableOptions = { maxWidth?: number; minWidth?: number; @@ -147,7 +151,9 @@ function SidebarProvider({ } // This sets the cookie to keep the sidebar state. - await cookieStore.set({ + const cookieStore = (globalThis as typeof globalThis & { cookieStore?: CookieStoreLike }) + .cookieStore; + await cookieStore?.set({ expires: Date.now() + SIDEBAR_COOKIE_MAX_AGE * 1000, name: SIDEBAR_COOKIE_NAME, path: "/", diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index cc3b14515..6c37aec6f 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -33,6 +33,7 @@ import { MobileConnectionBanner } from "../components/mobile/MobileConnectionBan import { MobilePairingScreen } from "../components/mobile/MobilePairingScreen"; import { useMobilePairingState } from "../hooks/useMobilePairingState"; import { I18nProvider } from "../i18n/I18nProvider"; +import { useT } from "../i18n/useI18n"; import { VoodooStitches } from "../components/VoodooStitches"; export const Route = createRootRouteWithContext<{ @@ -57,13 +58,16 @@ function RootRouteView() { } function RootRouteContent() { + const { t } = useT(); const { isMobileShell, isLoading, pairingState } = useMobilePairingState(); if (isMobileShell && isLoading) { return (
-

Restoring mobile pairing...

+

+ {t("root.loading.restoringMobilePairing")} +

); @@ -78,7 +82,7 @@ function RootRouteContent() {

- Connecting to {APP_DISPLAY_NAME} server... + {t("root.loading.connectingServer", { appName: APP_DISPLAY_NAME })}

@@ -107,6 +111,7 @@ function RootRouteErrorView({ error, reset }: ErrorComponentProps) { } function RootRouteErrorContent({ error, reset }: ErrorComponentProps) { + const { t } = useT(); const message = errorMessage(error); const details = errorDetails(error); @@ -122,23 +127,23 @@ function RootRouteErrorContent({ error, reset }: ErrorComponentProps) { {APP_DISPLAY_NAME}

- Something went wrong. + {t("root.error.title")}

{message}

- Show error details - Hide error details + {t("root.error.showDetails")} + {t("root.error.hideDetails")}
             {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": {