Skip to content
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ Code Trail reads session files from the default provider directories:
| Gemini CLI | `~/.gemini/tmp/` and `~/.gemini/history/` |
| Cursor | `~/.cursor/projects/` |
| VS Code Copilot | `.../Code/User/workspaceStorage/*/chatSessions/` |
| OpenCode | `~/.local/share/opencode/opencode.db` |

Each session file is parsed into a canonical message format, indexed into a local SQLite database with FTS5 for full-text search, and made available through the UI. The database and settings are stored in the Electron `userData` directory, typically `~/Library/Application Support/Code Trail/` on macOS or `%APPDATA%\Code Trail\` on Windows.

Expand Down
38 changes: 37 additions & 1 deletion apps/desktop/src/main/appStateStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ describe("AppStateStore", () => {
gemini: [],
cursor: [],
copilot: [],
opencode: [],
},
});
store.setIndexingState({
Expand Down Expand Up @@ -160,10 +161,11 @@ describe("AppStateStore", () => {
gemini: [],
cursor: [],
copilot: [],
opencode: [],
},
});
expect(reloaded.getIndexingState()).toEqual({
enabledProviders: ["claude", "codex", "gemini", "cursor", "copilot"],
enabledProviders: ["claude", "codex", "gemini", "cursor", "copilot", "opencode"],
removeMissingSessionsDuringIncrementalIndexing: true,
});
expect(reloaded.getWindowState()).toEqual({
Expand Down Expand Up @@ -299,6 +301,40 @@ describe("AppStateStore", () => {
expect(store.getWindowState()).toBeNull();
});

it("heals the legacy default provider selection to include opencode", () => {
const filePath = "/tmp/codetrail-legacy-enabled-providers-ui-state.json";
const fs = createMemoryFs({
[filePath]: JSON.stringify({
indexing: {
enabledProviders: ["claude", "codex", "gemini", "cursor", "copilot"],
},
}),
});

const store = new AppStateStore(filePath, { fs });

expect(store.getIndexingState()).toEqual({
enabledProviders: ["claude", "codex", "gemini", "cursor", "copilot", "opencode"],
});
});

it("preserves intentional custom enabled-provider selections", () => {
const filePath = "/tmp/codetrail-custom-enabled-providers-ui-state.json";
const fs = createMemoryFs({
[filePath]: JSON.stringify({
indexing: {
enabledProviders: ["claude", "cursor"],
},
}),
});

const store = new AppStateStore(filePath, { fs });

expect(store.getIndexingState()).toEqual({
enabledProviders: ["claude", "cursor"],
});
});

it("does not infer indexing config from pane data when indexing is absent", () => {
const filePath = "/tmp/codetrail-pane-only-state.json";
const fs = createMemoryFs({
Expand Down
28 changes: 27 additions & 1 deletion apps/desktop/src/main/appStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ const AUTO_REFRESH_STRATEGY_VALUES = [
"scan-5min",
] as const;
const CURRENT_AUTO_REFRESH_STRATEGY_VALUES = ["off", ...AUTO_REFRESH_STRATEGY_VALUES] as const;
const LEGACY_PRE_OPENCODE_PROVIDER_VALUES = [
"claude",
"codex",
"gemini",
"cursor",
"copilot",
] as const satisfies readonly Provider[];
const DEFAULT_FILE_SYSTEM: AppStateStoreFileSystem = {
existsSync: (path) => existsSync(path),
mkdirSync: (path, options) => mkdirSync(path, options),
Expand Down Expand Up @@ -502,7 +509,9 @@ function sanitizeIndexingState(value: unknown): IndexingConfigState | null {
}

const record = value as Record<string, unknown>;
const enabledProviders = sanitizeStringArray(record.enabledProviders, PROVIDER_VALUES);
const enabledProviders = healLegacyEnabledProviders(
sanitizeStringArray(record.enabledProviders, PROVIDER_VALUES),
);
const removeMissingSessionsDuringIncrementalIndexing = sanitizeOptionalBoolean(
record.removeMissingSessionsDuringIncrementalIndexing,
);
Expand All @@ -518,6 +527,23 @@ function sanitizeIndexingState(value: unknown): IndexingConfigState | null {
};
}

function healLegacyEnabledProviders(providers: Provider[] | null): Provider[] | null {
if (providers === null) {
return null;
}
if (!matchesLegacyDefaultProviderSelection(providers)) {
return providers;
}
return addMissingProviders(providers);
}

function matchesLegacyDefaultProviderSelection(providers: readonly Provider[]): boolean {
if (providers.length !== LEGACY_PRE_OPENCODE_PROVIDER_VALUES.length) {
return false;
}
return LEGACY_PRE_OPENCODE_PROVIDER_VALUES.every((provider) => providers.includes(provider));
}

function sanitizeWindowState(value: unknown): WindowState | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/main/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ const {
claude: ["^<command-name>"],
codex: ["^<environment_context>"],
gemini: [],
cursor: [],
copilot: [],
opencode: [],
}
);
}),
Expand Down Expand Up @@ -241,6 +244,7 @@ const {
gemini: 0,
cursor: 0,
copilot: 0,
opencode: 0,
},
sessions: [],
claudeHookState: {
Expand Down Expand Up @@ -291,6 +295,7 @@ vi.mock("@codetrail/core", async () => {
geminiProjectsPath: null,
cursorRoot: "/cursor/root",
copilotRoot: "/copilot/root",
opencodeRoot: "/opencode/root",
includeClaudeSubagents: false,
},
initializeDatabase: mockInitializeDatabase,
Expand Down Expand Up @@ -475,6 +480,7 @@ describe("bootstrapMainProcess", () => {
gemini: [],
cursor: [],
copilot: [],
opencode: [],
},
};

Expand Down Expand Up @@ -939,6 +945,7 @@ describe("bootstrapMainProcess", () => {
gemini: [],
cursor: [],
copilot: [],
opencode: [],
},
});
expect(getRequiredHandler(handlers, "indexer:getConfig")({})).toEqual({
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ export async function bootstrapMainProcess(
options.onCommandStateChanged?.(payload);
return { ok: true };
},
"dashboard:getStats": () => queryService.getDashboardStats(),
"indexer:refresh": async (payload) => {
invalidateAllowedRootsCache();
if (payload.projectId && !payload.force) {
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/main/data/bookmarkStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export type BookmarkStore = {
searchMode?: SearchMode,
) => Record<MessageCategory, number>;
countProjectBookmarksByProjectIds?: (projectIds: string[]) => Record<string, number>;
countAllBookmarks?: () => number;
countSessionBookmarks: (projectId: string, sessionId: string) => number;
countSessionBookmarksBySessionIds?: (
projectId: string,
Expand Down Expand Up @@ -132,6 +133,7 @@ export function createBookmarkStore(bookmarksDbPath: string): BookmarkStore {
WHERE project_id = ? AND message_id = ?`,
);
const countStmt = db.prepare("SELECT COUNT(*) as cnt FROM bookmarks WHERE project_id = ?");
const countAllStmt = db.prepare("SELECT COUNT(*) as cnt FROM bookmarks");
const countSessionStmt = db.prepare(
"SELECT COUNT(*) as cnt FROM bookmarks WHERE project_id = ? AND session_id = ?",
);
Expand Down Expand Up @@ -351,6 +353,10 @@ export function createBookmarkStore(bookmarksDbPath: string): BookmarkStore {
countProjectBookmarksByProjectIds: (projectIds) => {
return countBookmarksByProjectIds(db, projectIds);
},
countAllBookmarks: () => {
const row = countAllStmt.get() as { cnt: number } | undefined;
return Number(row?.cnt ?? 0);
},
countSessionBookmarks: (projectId, sessionId) => {
const row = countSessionStmt.get(projectId, sessionId) as { cnt: number } | undefined;
return Number(row?.cnt ?? 0);
Expand Down
Loading
Loading