Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion apps/desktop/src/main/appStateStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe("AppStateStore", () => {
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
opencode: [],
},
});
store.setWindowState({ width: 1440, height: 920, x: 48, y: 72, isMaximized: false });
Expand Down Expand Up @@ -113,6 +114,7 @@ describe("AppStateStore", () => {
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
opencode: [],
},
});
expect(reloaded.getWindowState()).toEqual({
Expand Down Expand Up @@ -185,7 +187,7 @@ describe("AppStateStore", () => {
expect(store.getPaneState()).toEqual({
projectPaneWidth: 300,
sessionPaneWidth: 360,
projectProviders: ["claude", "codex", "gemini", "cursor"],
projectProviders: ["claude", "codex", "gemini", "cursor", "opencode"],
historyCategories: ["assistant"],
});
});
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/appStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ function sanitizeSystemMessageRegexRules(value: unknown): Record<Provider, strin
codex: [],
gemini: [],
cursor: [],
opencode: [],
};

for (const provider of PROVIDER_VALUES) {
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/main/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ vi.mock("@codetrail/core", async () => {
geminiHistoryRoot: null,
geminiProjectsPath: null,
cursorRoot: "/cursor/root",
opencodeDbPath: "/mock/.local/share/opencode/opencode.db",
},
initializeDatabase: mockInitializeDatabase,
resolveSystemMessageRegexRules: mockResolveSystemMessageRegexRules,
Expand Down Expand Up @@ -233,6 +234,7 @@ describe("bootstrapMainProcess", () => {
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
opencode: [],
},
};

Expand Down Expand Up @@ -329,6 +331,7 @@ describe("bootstrapMainProcess", () => {
geminiHistoryRoot: "/Users/test/.gemini/history",
geminiProjectsPath: "/Users/test/.gemini/projects.json",
cursorRoot: "/cursor/root",
opencodeDbPath: "/mock/.local/share/opencode/opencode.db",
});

const projectPayload = { providers: ["claude"], query: "" };
Expand Down Expand Up @@ -493,6 +496,7 @@ describe("bootstrapMainProcess", () => {
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
opencode: [],
},
});

Expand Down Expand Up @@ -529,6 +533,7 @@ describe("bootstrapMainProcess", () => {
"/gemini/root",
"/Users/test/.gemini/history",
"/cursor/root",
"/mock/.local/share/opencode",
],
backend: "kqueue",
});
Expand All @@ -539,6 +544,7 @@ describe("bootstrapMainProcess", () => {
"/gemini/root",
"/Users/test/.gemini/history",
"/cursor/root",
"/mock/.local/share/opencode",
],
expect.any(Function),
expect.objectContaining({
Expand Down Expand Up @@ -627,6 +633,7 @@ describe("bootstrapMainProcess", () => {
"/gemini/root",
"/Users/test/.gemini/history",
"/cursor/root",
"/mock/.local/share/opencode",
],
expect.any(Function),
expect.objectContaining({
Expand All @@ -642,6 +649,7 @@ describe("bootstrapMainProcess", () => {
"/gemini/root",
"/Users/test/.gemini/history",
"/cursor/root",
"/mock/.local/share/opencode",
],
expect.any(Function),
expect.objectContaining({
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "./fileWatcherService";
import { WorkerIndexingRunner } from "./indexingRunner";
import { registerIpcHandlers } from "./ipc";
import { initializeOpenCodeReaders } from "./openCodeReaders";
import { WatchStatsStore } from "./watchStatsStore";

const MIN_ZOOM_PERCENT = 60;
Expand Down Expand Up @@ -65,6 +66,7 @@ export async function bootstrapMainProcess(

const dbBootstrap = initializeDatabase(dbPath);
initializeBookmarkStore(bookmarksDbPath);
initializeOpenCodeReaders();
const watchStatsStore = new WatchStatsStore();
const indexingRunner = new WorkerIndexingRunner(dbPath, {
bookmarksDbPath,
Expand Down Expand Up @@ -107,6 +109,7 @@ export async function bootstrapMainProcess(
DEFAULT_DISCOVERY_CONFIG.geminiRoot,
geminiHistoryRoot,
DEFAULT_DISCOVERY_CONFIG.cursorRoot,
dirname(DEFAULT_DISCOVERY_CONFIG.opencodeDbPath),
];
registerIpcHandlers(ipcMain, {
"app:getHealth": () => ({
Expand All @@ -128,6 +131,7 @@ export async function bootstrapMainProcess(
geminiHistoryRoot,
geminiProjectsPath,
cursorRoot: DEFAULT_DISCOVERY_CONFIG.cursorRoot,
opencodeDbPath: DEFAULT_DISCOVERY_CONFIG.opencodeDbPath,
},
}),
"db:getSchemaVersion": () => ({
Expand Down Expand Up @@ -404,6 +408,7 @@ function getAllowedOpenInFileManagerRoots(input: {
addRoot(input.geminiProjectsPath);
addRoot(dirname(input.geminiProjectsPath));
addRoot(DEFAULT_DISCOVERY_CONFIG.cursorRoot);
addRoot(dirname(DEFAULT_DISCOVERY_CONFIG.opencodeDbPath));

try {
// Indexed project paths are dynamic, so fold them into the static provider/app roots cache.
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/main/indexingRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type WorkerMessage =
| {
type: "file-issue";
issue: {
provider: "claude" | "codex" | "gemini" | "cursor";
provider: "claude" | "codex" | "gemini" | "cursor" | "opencode";
sessionId: string;
filePath: string;
stage: "read" | "parse" | "persist";
Expand All @@ -18,7 +18,7 @@ type WorkerMessage =
| {
type: "notice";
notice: {
provider: "claude" | "codex" | "gemini" | "cursor";
provider: "claude" | "codex" | "gemini" | "cursor" | "opencode";
sessionId: string;
filePath: string;
stage: "read" | "parse" | "persist";
Expand All @@ -38,6 +38,7 @@ type WorkerRequest = {
codex?: string[];
gemini?: string[];
cursor?: string[];
opencode?: string[];
};
};

Expand Down Expand Up @@ -219,6 +220,7 @@ describe("WorkerIndexingRunner", () => {
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
opencode: [],
}),
});

Expand All @@ -233,6 +235,7 @@ describe("WorkerIndexingRunner", () => {
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
opencode: [],
},
},
{},
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/indexingWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
runIncrementalIndexing,
} from "@codetrail/core";

import { initializeOpenCodeReaders } from "./openCodeReaders";

type IncrementalRequest = {
kind: "incremental";
dbPath: string;
Expand Down Expand Up @@ -129,6 +131,8 @@ function handleRequest(request: IndexingWorkerRequest): void {
}
}

initializeOpenCodeReaders();

if (parentPort) {
parentPort.on("message", (request: IndexingWorkerRequest) => {
handleRequest(request);
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe("registerIpcHandlers", () => {
geminiHistoryRoot: "/Users/test/.gemini/history",
geminiProjectsPath: "/Users/test/.gemini/projects.json",
cursorRoot: "/Users/test/.cursor/projects",
opencodeDbPath: "/mock/.local/share/opencode/opencode.db",
},
}),
"db:getSchemaVersion": () => ({ schemaVersion: 1 }),
Expand Down Expand Up @@ -188,6 +189,7 @@ describe("registerIpcHandlers", () => {
geminiHistoryRoot: "/Users/test/.gemini/history",
geminiProjectsPath: "/Users/test/.gemini/projects.json",
cursorRoot: "/Users/test/.cursor/projects",
opencodeDbPath: "/mock/.local/share/opencode/opencode.db",
},
}),
"db:getSchemaVersion": () => ({ schemaVersion: 1 }),
Expand Down
157 changes: 157 additions & 0 deletions apps/desktop/src/main/openCodeReaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { existsSync } from "node:fs";

import Database from "better-sqlite3";

import { setOpenCodeDbReader, setOpenCodeMessagePartReader } from "@codetrail/core";

function openReadonlyDb(dbPath: string): InstanceType<typeof Database> | null {
if (!existsSync(dbPath)) {
return null;
}
try {
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
db.pragma("journal_mode = WAL");
return db;
} catch {
return null;
}
}

export function initializeOpenCodeReaders(): void {
setOpenCodeDbReader({
readSessions: (dbPath) => {
const db = openReadonlyDb(dbPath);
if (!db) {
return [];
}
try {
return db
.prepare(
`SELECT id, project_id, parent_id, title, directory,
time_created, time_updated
FROM session
ORDER BY time_updated DESC`,
)
.all() as Array<{
id: string;
project_id: string;
parent_id: string | null;
title: string;
directory: string;
time_created: number;
time_updated: number;
}>;
} finally {
db.close();
}
},
readProjects: (dbPath) => {
const db = openReadonlyDb(dbPath);
if (!db) {
return [];
}
try {
return db.prepare("SELECT id, worktree, name FROM project").all() as Array<{
id: string;
worktree: string;
name: string | null;
}>;
} finally {
db.close();
}
},
});

setOpenCodeMessagePartReader({
readSessionMessagesWithParts: (dbPath, sessionId) => {
const db = openReadonlyDb(dbPath);
if (!db) {
return [];
}
try {
const messages = db
.prepare(
`SELECT id, session_id, time_created, data
FROM message
WHERE session_id = ?
ORDER BY time_created ASC, id ASC`,
)
.all(sessionId) as Array<{
id: string;
session_id: string;
time_created: number;
data: string;
}>;

const parts = db
.prepare(
`SELECT id, message_id, data
FROM part
WHERE session_id = ?
ORDER BY message_id, id`,
)
.all(sessionId) as Array<{
id: string;
message_id: string;
data: string;
}>;

const partsByMessage = new Map<string, typeof parts>();
for (const part of parts) {
const existing = partsByMessage.get(part.message_id) ?? [];
existing.push(part);
partsByMessage.set(part.message_id, existing);
}

return messages.map((msg) => {
let msgData: Record<string, unknown> = {};
try {
msgData = JSON.parse(msg.data) as Record<string, unknown>;
} catch {
// ignore
}

const time = msgData.time as Record<string, unknown> | undefined;
const tokens = msgData.tokens as Record<string, unknown> | undefined;
const path = msgData.path as Record<string, unknown> | undefined;
const nestedModel = msgData.model as Record<string, unknown> | undefined;

const messageParts = partsByMessage.get(msg.id) ?? [];
return {
messageId: msg.id,
role: (msgData.role as string) ?? "user",
timeCreated: msg.time_created,
timeCompleted:
typeof time?.completed === "number" ? (time.completed as number) : null,
modelId:
(msgData.modelID as string) ??
(nestedModel?.modelID as string) ??
null,
providerId:
(msgData.providerID as string) ??
(nestedModel?.providerID as string) ??
null,
cwd: (path?.cwd as string) ?? null,
tokenInput: typeof tokens?.input === "number" ? (tokens.input as number) : null,
tokenOutput: typeof tokens?.output === "number" ? (tokens.output as number) : null,
parts: messageParts.map((p) => {
let partData: Record<string, unknown> = {};
try {
partData = JSON.parse(p.data) as Record<string, unknown>;
} catch {
// ignore
}
return {
id: p.id,
type: (partData.type as string) ?? "unknown",
data: p.data,
};
}),
};
});
} finally {
db.close();
}
},
});
}
1 change: 1 addition & 0 deletions apps/desktop/src/renderer/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const EMPTY_SYSTEM_MESSAGE_REGEX_RULES: SystemMessageRegexRules = {
codex: [],
gemini: [],
cursor: [],
opencode: [],
};

export const SHORTCUT_ITEMS = [
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/components/SettingsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const info = {
geminiHistoryRoot: "/Users/test/.gemini/history",
geminiProjectsPath: "/Users/test/.gemini/projects.json",
cursorRoot: "/Users/test/.cursor/projects",
opencodeDbPath: "/mock/.local/share/opencode/opencode.db",
},
};

Expand Down Expand Up @@ -111,6 +112,7 @@ function createBaseProps() {
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
opencode: [],
},
onAddSystemMessageRegexRule: vi.fn(),
onUpdateSystemMessageRegexRule: vi.fn(),
Expand Down
Loading