Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
999cc67
feat(app): define local AI IPC contracts
NarwhalChen Jul 30, 2026
2efe136
feat(app): expose local AI IPC bridge
NarwhalChen Jul 30, 2026
045c1ed
feat(app): stream chat through local AI bridge
NarwhalChen Jul 30, 2026
5b7229a
feat(app): select local AI providers
NarwhalChen Jul 30, 2026
3e4145a
refactor(app): hide cloud service surfaces
NarwhalChen Jul 30, 2026
553fc23
fix(app): harden local AI integration
NarwhalChen Jul 30, 2026
21daa2c
refactor(app): remove cloud service codepaths
NarwhalChen Jul 30, 2026
98bca90
fix(app): validate local AI requests
NarwhalChen Jul 30, 2026
bd3c918
feat(app): prepare local CLI providers
NarwhalChen Jul 30, 2026
542e29d
feat(app): add local AI runtime
NarwhalChen Jul 30, 2026
1e19482
refactor(app): complete AI SDK 6 migration
NarwhalChen Jul 30, 2026
00043e0
fix(app): discover compatible Codex models
NarwhalChen Jul 30, 2026
258ccae
chore(app): satisfy local runtime lint
NarwhalChen Jul 30, 2026
442ff8e
fix(app): load isolated Claude auth environment
NarwhalChen Jul 30, 2026
e559f14
fix(app): resolve provider default models
NarwhalChen Jul 30, 2026
617d230
fix(app): align text chat UI boundaries
NarwhalChen Jul 30, 2026
71d562f
fix(app): load privileged preload bridge
NarwhalChen Jul 30, 2026
ab86894
feat(app): connect local AI tools through MCP
NarwhalChen Jul 30, 2026
f9efca7
test(app): cover local tool interaction lifecycle
NarwhalChen Jul 30, 2026
4b8b8b9
fix(app): connect Codex to local MCP tools
NarwhalChen Jul 30, 2026
f5cf5c8
fix(app): preserve click results after UI transitions
NarwhalChen Jul 30, 2026
8b6c539
test(app): verify builtin tools reach local AI runtime
NarwhalChen Jul 30, 2026
ad928d2
fix(app): use native Codex HTTP MCP config
NarwhalChen Jul 30, 2026
bf2c1c3
feat(app): render Codex tool calls with AI SDK
NarwhalChen Jul 30, 2026
4d0c160
refactor(app): remove legacy tool call rendering
NarwhalChen Jul 30, 2026
2ba7f70
feat(app): add atomic computer control tool
NarwhalChen Jul 30, 2026
4d30d19
fix(app): preserve computer screenshots for Codex
NarwhalChen Jul 30, 2026
64668f5
fix(app): keep local AI tools type-safe
NarwhalChen Jul 30, 2026
70f562b
feat(app): select local AI provider in settings
NarwhalChen Jul 30, 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
55 changes: 55 additions & 0 deletions packages/app/automation/driver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it, vi } from "vitest";
import { ConveraDriver } from "./driver.js";

describe("ConveraDriver click", () => {
it("returns the pre-click snapshot when the clicked element disappears", async () => {
let exists = true;
const click = vi.fn(async () => {
exists = false;
});
const element = {
attributes: [{ name: "role", value: "option" }],
click,
doubleClick: vi.fn(),
getTagName: vi.fn(async () => "button"),
getText: vi.fn(async () => "Alpha"),
getValue: vi.fn(async () => null),
isClickable: vi.fn(async () => exists),
isDisplayed: vi.fn(async () => exists),
isEnabled: vi.fn(async () => true),
isExisting: vi.fn(async () => exists),
waitForDisplayed: vi.fn(async () => undefined),
waitForExist: vi.fn(async () => undefined),
};
const browser = {
$: vi.fn(async () => element),
execute: vi.fn(
async (
operation: (node: typeof element) => unknown,
node: typeof element,
) => operation(node),
),
waitUntil: vi.fn(async (condition: () => Promise<boolean>) => {
if (!(await condition())) throw new Error("condition not met");
return true;
}),
};
const driver = new ConveraDriver();
Reflect.set(driver, "browser", browser);

await expect(driver.click('[role="option"]')).resolves.toMatchObject({
selector: '[role="option"]',
tag: "button",
text: "Alpha",
displayed: true,
enabled: true,
clickable: true,
attributes: { role: "option" },
action: "click",
completed: true,
});
expect(click).toHaveBeenCalledOnce();
expect(browser.$).toHaveBeenCalledTimes(2);
expect(element.isExisting).toHaveBeenCalledTimes(1);
});
});
7 changes: 6 additions & 1 deletion packages/app/automation/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,9 +547,14 @@ export class ConveraDriver {

async click(selector: string, double = false) {
const element = await this.readyElement(selector, true);
const before = await this.inspectElement(selector);
if (double) await element.doubleClick();
else await element.click();
return this.inspectElement(selector);
return {
...before,
action: double ? "double_click" : "click",
completed: true,
};
}

async hover(selector: string) {
Expand Down
24 changes: 5 additions & 19 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
"test:all": "vitest run && playwright test",
"automation": "WDIO_LOG_LEVEL=silent tsx automation/server.ts",
"automation:prepare": "electron-forge package && tsx automation/prepare-driver.ts",
"automation:typecheck": "tsc --noEmit -p automation/tsconfig.json",
"chat-server": "ts-node src/server/startServer.ts"
"automation:typecheck": "tsc --noEmit -p automation/tsconfig.json"
},
"author": "Smal1boy <541898146chen@gmail.com>",
"license": "MIT",
Expand Down Expand Up @@ -88,19 +87,11 @@
"webdriverio": "9.27.1"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.10",
"@daveyplate/better-auth-tanstack": "1.3.6",
"@daveyplate/better-auth-ui": "1.7.4",
"@fontsource/inter": "^5.2.5",
"@google-cloud/speech": "^7.1.0",
"@hono/node-server": "^1.14.3",
"@hono/zod-validator": "^0.7.0",
"@hookform/resolvers": "^5.0.1",
"@hurdlegroup/robotjs": "^0.12.3",
"@icons-pack/react-simple-icons": "^12.2.0",
"@leeoniya/ufuzzy": "^1.0.18",
"@modelcontextprotocol/sdk": "1.12.3",
"@openrouter/ai-sdk-provider": "^0.4.5",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
Expand Down Expand Up @@ -130,25 +121,24 @@
"@smithery/sdk": "^1.0.4",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.14",
"@tanstack/react-query": "^5.69.0",
"@tanstack/react-router": "^1.120.18",
"@tanstack/router-devtools": "^1.120.18",
"@tiptap/extension-placeholder": "^2.11.7",
"@tiptap/pm": "^2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@types/cors": "^2.8.17",
"@types/hast": "^3.0.4",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.4",
"ai": "^4.3.19",
"ai": "^6.0.238",
"ai-sdk-provider-claude-code": "3.1.0",
"ai-sdk-provider-codex-cli": "^1.3.1",
"babel-plugin-react-compiler": "^19.0.0-beta-e993439-20250328",
"bufferutil": "^4.0.9",
"cheerio": "^1.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"cors": "^2.8.5",
"date-fns": "^3.6.0",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
Expand All @@ -157,22 +147,18 @@
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.6.3",
"get-windows": "^9.2.0",
"hono": "^4.7.11",
"i18next": "^24.2.3",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"next-themes": "^0.4.6",
"node-record-lpcm16": "^1.0.1",
"node-window-manager": "^2.2.4",
"openai": "^4.93.0",
"react": "^19.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^19.0.0",
"react-hook-form": "^7.55.0",
"react-i18next": "^15.4.1",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
"reconnecting-eventsource": "^1.6.4",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"sonner": "^2.0.3",
Expand All @@ -183,7 +169,7 @@
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"ws": "^8.18.1",
"zod": "^3.24.4",
"zod": "^3.25.76",
"zustand": "^5.0.4"
},
"lint-staged": {
Expand Down
10 changes: 10 additions & 0 deletions packages/app/src/electro-bridge/ipc/listeners-register.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { WindowSizeConfig } from "@/electron/windows/window-size";
import { ThemeMode } from "@/shared/types/electron";
import type { LocalAIRuntimeService } from "@/shared/types/local-ai";
import { BrowserWindow, ipcMain, IpcRenderer } from "electron";
import { getAppIcon, getPlatform } from "./active-app-context";
import { CHANNELS, IPCServer, methodChannelMap } from "./channels";
Expand All @@ -25,6 +26,7 @@ import {
updateGlobalShortcut,
} from "./ipc-handlers";
import { setupLoggerIPC } from "./logger-context";
import { setupLocalAIIPC } from "./local-ai-context";
import { setupMCPIPC } from "./mcp-context";

// Extended interface that includes additional methods beyond IPCServer
Expand Down Expand Up @@ -88,6 +90,7 @@ export function createElectronAPI(ipcRenderer: IpcRenderer): ElectronAPI {
export interface ListenerOptions {
mainWindow?: () => BrowserWindow | null;
registerGlobalShortcuts?: () => void;
localAIRuntime?: LocalAIRuntimeService;
}

/**
Expand Down Expand Up @@ -231,5 +234,12 @@ export function registerListeners(options: ListenerOptions = {}) {
setupLoggerIPC();
setupElectronAPIIPC(options);
setupEnvIPC();
setupLocalAIIPC({
runtime: options.localAIRuntime,
getAllowedWebContents: () => {
const window = options.mainWindow?.();
return window && !window.isDestroyed() ? window.webContents : null;
},
});
console.log("All IPC listeners registered successfully");
}
Loading
Loading