diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000000..8bb9df7241 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [ + ".plans", + "dist", + "dist-electron", + "node_modules", + "bun.lock", + "*.tsbuildinfo" + ], + "experimentalSortPackageJson": {} +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000000..d45179a4c4 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,13 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "ignorePatterns": ["dist", "dist-electron", "node_modules", "bun.lock", "*.tsbuildinfo"], + "plugins": ["eslint", "oxc", "react", "unicorn", "typescript"], + "categories": { + "correctness": "warn", + "suspicious": "warn", + "perf": "warn" + }, + "rules": { + "react-in-jsx-scope": "off" + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..99e2f7ddf7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["oxc.oxc-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..554e703ccb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "always" + }, + "oxc.unusedDisableDirectives": "warn" +} diff --git a/README.md b/README.md index 2009730add..567c2659a0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Mode changes apply across all threads. Existing live sessions are restarted so o - `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`. Optional: + - `ELECTRON_RENDERER_PORT=5180 bun run dev` if `5173` is already in use. ## Provider architecture diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9d945ea3fb..1252fe990d 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -4,14 +4,13 @@ "private": true, "main": "dist-electron/main.js", "scripts": { - "dev": "concurrently -k -n BUNDLE,ELECTRON \"bun run dev:bundle\" \"bun run dev:electron\"", + "dev": "bun run --parallel dev:bundle dev:electron", "dev:bundle": "tsup --watch", "dev:electron": "bun run scripts/dev-electron.mjs", "build": "tsup", "start": "electron dist-electron/main.js", "postinstall": "electron-rebuild", "typecheck": "tsc --noEmit", - "lint": "biome check src/", "test": "vitest run", "smoke-test": "node scripts/smoke-test.mjs" }, @@ -23,7 +22,6 @@ "devDependencies": { "@electron/rebuild": "^3.7.0", "@types/node": "^22.10.2", - "concurrently": "^9.1.2", "electronmon": "^2.0.2", "tsup": "^8.3.5", "typescript": "^5.7.3", diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs index d0522bb4e7..3b0526c478 100644 --- a/apps/desktop/scripts/dev-electron.mjs +++ b/apps/desktop/scripts/dev-electron.mjs @@ -6,15 +6,10 @@ const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173); const devServerUrl = `http://localhost:${port}`; await waitOn({ - resources: [ - `tcp:${port}`, - "file:dist-electron/main.js", - "file:dist-electron/preload.js", - ], + resources: [`tcp:${port}`, "file:dist-electron/main.js", "file:dist-electron/preload.js"], }); -const command = - process.platform === "win32" ? "electronmon.cmd" : "electronmon"; +const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon"; const child = spawn(command, ["dist-electron/main.js"], { stdio: "inherit", env: { diff --git a/apps/desktop/src/codexAppServerManager.test.ts b/apps/desktop/src/codexAppServerManager.test.ts index fd028607d3..162f1896cd 100644 --- a/apps/desktop/src/codexAppServerManager.test.ts +++ b/apps/desktop/src/codexAppServerManager.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from "vitest"; -import { - classifyCodexStderrLine, - normalizeCodexModelSlug, -} from "./codexAppServerManager"; +import { classifyCodexStderrLine, normalizeCodexModelSlug } from "./codexAppServerManager"; describe("classifyCodexStderrLine", () => { it("ignores empty lines", () => { @@ -23,8 +20,7 @@ describe("classifyCodexStderrLine", () => { }); it("keeps unknown structured errors", () => { - const line = - "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure"; + const line = "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure"; expect(classifyCodexStderrLine(line)).toEqual({ message: line, }); @@ -45,9 +41,7 @@ describe("normalizeCodexModelSlug", () => { }); it("prefers codex id when model differs", () => { - expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe( - "gpt-5.3-codex", - ); + expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex"); }); it("keeps non-aliased models as-is", () => { diff --git a/apps/desktop/src/codexAppServerManager.ts b/apps/desktop/src/codexAppServerManager.ts index d40c761e07..55a4947d88 100644 --- a/apps/desktop/src/codexAppServerManager.ts +++ b/apps/desktop/src/codexAppServerManager.ts @@ -91,9 +91,7 @@ export function normalizeCodexModelSlug( return normalized; } -export function classifyCodexStderrLine( - rawLine: string, -): { message: string } | null { +export function classifyCodexStderrLine(rawLine: string): { message: string } | null { const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim(); if (!line) { return null; @@ -106,9 +104,7 @@ export function classifyCodexStderrLine( return null; } - const isBenignError = BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => - line.includes(snippet), - ); + const isBenignError = BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => line.includes(snippet)); if (isBenignError) { return null; } @@ -124,9 +120,7 @@ export interface CodexAppServerManagerEvents { export class CodexAppServerManager extends EventEmitter { private readonly sessions = new Map(); - async startSession( - input: ProviderSessionStartInput, - ): Promise { + async startSession(input: ProviderSessionStartInput): Promise { const sessionId = randomUUID(); const now = new Date().toISOString(); @@ -160,11 +154,7 @@ export class CodexAppServerManager extends EventEmitter { + async sendTurn(input: ProviderSendTurnInput): Promise { const context = this.requireSession(input.sessionId); if (!context.session.threadId) { throw new Error("Session is missing a thread id."); @@ -252,11 +230,7 @@ export class CodexAppServerManager extends EventEmitter, - ): void { + private updateSession(context: CodexSessionContext, updates: Partial): void { context.session = { ...context.session, ...updates, @@ -762,8 +703,7 @@ export class CodexAppServerManager extends EventEmitter; - const hasId = - typeof candidate.id === "string" || typeof candidate.id === "number"; + const hasId = typeof candidate.id === "string" || typeof candidate.id === "number"; const hasMethod = typeof candidate.method === "string"; return hasId && !hasMethod; } @@ -783,11 +723,9 @@ export class CodexAppServerManager extends EventEmitter | undefined { + private readObject(value: unknown, key?: string): Record | undefined { const target = key === undefined ? value diff --git a/apps/desktop/src/ipcHelpers.test.ts b/apps/desktop/src/ipcHelpers.test.ts index 33fbbc8315..ce3318f061 100644 --- a/apps/desktop/src/ipcHelpers.test.ts +++ b/apps/desktop/src/ipcHelpers.test.ts @@ -55,11 +55,9 @@ describe("withParsedPayload", () => { describe("withParsedArgs", () => { it("parses tuple arguments before invoking handler", () => { - const handler = vi.fn( - (_event: unknown, sessionId: string, data: string) => { - return `${sessionId}:${data}`; - }, - ); + const handler = vi.fn((_event: unknown, sessionId: string, data: string) => { + return `${sessionId}:${data}`; + }); const wrapped = withParsedArgs( { parse(args: unknown[]): [string, string] { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 4b32d7bdb5..454cbe65f1 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -105,8 +105,7 @@ function registerIpcHandlers(): void { ); ipcMain.handle(IPC_CHANNELS.dialogPickFolder, async () => { - const owner = - BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]; + const owner = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]; const result = owner ? await dialog.showOpenDialog(owner, { properties: ["openDirectory", "createDirectory"], @@ -175,12 +174,9 @@ function registerIpcHandlers(): void { // Provider handlers ipcMain.handle( IPC_CHANNELS.providerSessionStart, - withParsedPayload( - providerSessionStartInputSchema, - async (_event, payload) => { - return providerManager.startSession(payload); - }, - ), + withParsedPayload(providerSessionStartInputSchema, async (_event, payload) => { + return providerManager.startSession(payload); + }), ); ipcMain.handle( @@ -192,12 +188,9 @@ function registerIpcHandlers(): void { ipcMain.handle( IPC_CHANNELS.providerTurnInterrupt, - withParsedPayload( - providerInterruptTurnInputSchema, - async (_event, payload) => { - await providerManager.interruptTurn(payload); - }, - ), + withParsedPayload(providerInterruptTurnInputSchema, async (_event, payload) => { + await providerManager.interruptTurn(payload); + }), ); ipcMain.handle( @@ -212,12 +205,9 @@ function registerIpcHandlers(): void { ipcMain.handle( IPC_CHANNELS.providerSessionStop, - withParsedPayload( - providerStopSessionInputSchema, - async (_event, payload) => { - providerManager.stopSession(payload); - }, - ), + withParsedPayload(providerStopSessionInputSchema, async (_event, payload) => { + providerManager.stopSession(payload); + }), ); ipcMain.handle(IPC_CHANNELS.providerSessionList, async () => { @@ -225,18 +215,14 @@ function registerIpcHandlers(): void { }); } -async function runTerminalCommand( - input: TerminalCommandInput, -): Promise { +async function runTerminalCommand(input: TerminalCommandInput): Promise { const shellPath = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : (process.env.SHELL ?? "/bin/sh"); const args = - process.platform === "win32" - ? ["/d", "/s", "/c", input.command] - : ["-lc", input.command]; + process.platform === "win32" ? ["/d", "/s", "/c", input.command] : ["-lc", input.command]; return new Promise((resolve, reject) => { const child = spawn(shellPath, args, { diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 68b9311f59..11fdfe4e77 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -18,14 +18,12 @@ const nativeApi: NativeApi = { agent: { spawn: (config) => ipcRenderer.invoke(IPC_CHANNELS.agentSpawn, config), kill: (sessionId) => ipcRenderer.invoke(IPC_CHANNELS.agentKill, sessionId), - write: (sessionId, data) => - ipcRenderer.invoke(IPC_CHANNELS.agentWrite, sessionId, data), + write: (sessionId, data) => ipcRenderer.invoke(IPC_CHANNELS.agentWrite, sessionId, data), onOutput: (callback) => { const listener = (_event: Electron.IpcRendererEvent, chunk: unknown) => callback(chunk as Parameters[0]); ipcRenderer.on(IPC_CHANNELS.agentOutput, listener); - return () => - ipcRenderer.removeListener(IPC_CHANNELS.agentOutput, listener); + return () => ipcRenderer.removeListener(IPC_CHANNELS.agentOutput, listener); }, onExit: (callback) => { const listener = (_event: Electron.IpcRendererEvent, exit: unknown) => @@ -35,23 +33,17 @@ const nativeApi: NativeApi = { }, }, providers: { - startSession: (input) => - ipcRenderer.invoke(IPC_CHANNELS.providerSessionStart, input), - sendTurn: (input) => - ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input), - interruptTurn: (input) => - ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input), - respondToRequest: (input) => - ipcRenderer.invoke(IPC_CHANNELS.providerRequestRespond, input), - stopSession: (input) => - ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input), + startSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStart, input), + sendTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input), + interruptTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input), + respondToRequest: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerRequestRespond, input), + stopSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input), listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.providerSessionList), onEvent: (callback) => { const listener = (_event: Electron.IpcRendererEvent, payload: unknown) => callback(payload as Parameters[0]); ipcRenderer.on(IPC_CHANNELS.providerEvent, listener); - return () => - ipcRenderer.removeListener(IPC_CHANNELS.providerEvent, listener); + return () => ipcRenderer.removeListener(IPC_CHANNELS.providerEvent, listener); }, }, shell: { diff --git a/apps/desktop/src/processManager.ts b/apps/desktop/src/processManager.ts index 0ea9dac78a..7297615a90 100644 --- a/apps/desktop/src/processManager.ts +++ b/apps/desktop/src/processManager.ts @@ -68,7 +68,6 @@ export class ProcessManager extends EventEmitter { } private spawnPty(sessionId: string, config: AgentConfig): string { - // eslint-disable-next-line @typescript-eslint/no-var-requires const pty = require("node-pty") as typeof import("node-pty"); const ptyProcess = pty.spawn(config.command, config.args, { @@ -76,9 +75,7 @@ export class ProcessManager extends EventEmitter { cols: 120, rows: 30, cwd: config.cwd ?? process.cwd(), - env: (config.env - ? { ...process.env, ...config.env } - : process.env) as Record, + env: (config.env ? { ...process.env, ...config.env } : process.env) as Record, }); this.ptySessions.set(sessionId, ptyProcess); diff --git a/apps/desktop/src/todoStore.ts b/apps/desktop/src/todoStore.ts index 992d7e0e83..a27ec06020 100644 --- a/apps/desktop/src/todoStore.ts +++ b/apps/desktop/src/todoStore.ts @@ -82,11 +82,7 @@ export class TodoStore { } private async persist(): Promise { - await fs.writeFile( - this.filePath, - JSON.stringify(this.todos, null, 2), - "utf8", - ); + await fs.writeFile(this.filePath, JSON.stringify(this.todos, null, 2), "utf8"); } private async runExclusive(operation: () => Promise): Promise { diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 4145229d26..893a95a419 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "composite": true, "types": ["node", "electron"], - "lib": ["ES2022", "DOM"] + "lib": ["ES2023", "DOM"] }, "include": ["src", "tsup.config.ts"] } diff --git a/apps/renderer/package.json b/apps/renderer/package.json index df06a3e1e2..b91d3d1dae 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -8,15 +8,14 @@ "build": "vite build", "preview": "vite preview", "typecheck": "tsc --noEmit", - "lint": "biome check src/", "test": "vitest run --passWithNoTests" }, "dependencies": { "@acme/contracts": "workspace:*", "highlight.js": "^11.11.1", - "react-markdown": "^10.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", "zod": "^3.24.1" diff --git a/apps/renderer/src/components/ChatMarkdown.tsx b/apps/renderer/src/components/ChatMarkdown.tsx index d43ebef94a..1d51a76851 100644 --- a/apps/renderer/src/components/ChatMarkdown.tsx +++ b/apps/renderer/src/components/ChatMarkdown.tsx @@ -18,9 +18,7 @@ export default function ChatMarkdown({ text }: ChatMarkdownProps) {
{text} diff --git a/apps/renderer/src/components/ChatView.tsx b/apps/renderer/src/components/ChatView.tsx index d73aa4d6bf..8c1d89eee6 100644 --- a/apps/renderer/src/components/ChatView.tsx +++ b/apps/renderer/src/components/ChatView.tsx @@ -144,9 +144,7 @@ export default function ChatView() { const editorMenuRef = useRef(null); const activeThread = state.threads.find((t) => t.id === state.activeThreadId); - const activeProject = state.projects.find( - (p) => p.id === activeThread?.projectId, - ); + const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); const selectedModel = resolveModelSlug( activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, ); @@ -164,7 +162,7 @@ export default function ChatView() { ); const assistantCompletionByItemId = useMemo(() => { const map = new Map(); - const ordered = [...(activeThread?.events ?? [])].reverse(); + const ordered = [...(activeThread?.events ?? [])].toReversed(); for (const event of ordered) { if (event.method !== "item/completed") continue; if (!event.itemId) continue; @@ -264,18 +262,15 @@ export default function ChatView() { // Auto-scroll on new messages const messageCount = activeThread?.messages.length ?? 0; const workLogCount = workLogEntries.length; - // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on message count change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messageCount]); - // biome-ignore lint/correctness/useExhaustiveDependencies: auto-scroll while active work-log events stream in useEffect(() => { if (phase !== "running") return; messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [phase, workLogCount]); // Auto-resize textarea - // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on prompt change useEffect(() => { const ta = textareaRef.current; if (!ta) return; @@ -298,10 +293,7 @@ export default function ChatView() { const handleClickOutside = (event: MouseEvent) => { if (!modelMenuRef.current) return; - if ( - event.target instanceof Node && - !modelMenuRef.current.contains(event.target) - ) { + if (event.target instanceof Node && !modelMenuRef.current.contains(event.target)) { setIsModelMenuOpen(false); } }; @@ -394,8 +386,7 @@ export default function ChatView() { // Auto-title from first message if (activeThread.messages.length === 0) { - const title = - trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed; + const title = trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed; dispatch({ type: "SET_THREAD_TITLE", threadId: activeThread.id, @@ -501,9 +492,7 @@ export default function ChatView() {
-

- Select a thread or create a new one to get started. -

+

Select a thread or create a new one to get started.

@@ -513,7 +502,7 @@ export default function ChatView() { return (
{/* Top bar */} -
+

{activeThread.title} @@ -654,9 +643,7 @@ export default function ChatView() {
{activeThread.messages.length === 0 && !isWorking ? (
-

- Send a message to start the conversation. -

+

Send a message to start the conversation.

) : (
@@ -770,7 +757,7 @@ export default function ChatView() { {/* Input bar */}
-
+
{/* Textarea area */}