From 15eb3def5d6c364740e454654b4c6859970235db Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 17:19:46 +0100 Subject: [PATCH 01/10] ci: add code reviewer --- .github/workflows/code-review.yml | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/code-review.yml diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml new file mode 100644 index 000000000..10c9ead70 --- /dev/null +++ b/.github/workflows/code-review.yml @@ -0,0 +1,33 @@ +name: Code Review + +permissions: + contents: read + pull-requests: write + +on: + pull_request_target: + types: [opened, synchronize, reopened] + pull_request_review_comment: + types: [created] + +concurrency: + group: ${{ github.repository }}-${{ github.event.number || github.head_ref || + github.sha }}-${{ github.workflow }}-${{ github.event_name == + 'pull_request_review_comment' && 'pr_comment' || 'pr' }} + cancel-in-progress: ${{ github.event_name != 'pull_request_review_comment' }} + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: coderabbitai/ai-pr-reviewer@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + with: + debug: false + review_simple_changes: false + review_comment_lgtm: false + openai_base_url: "https://openrouter.ai/api/v1" + openai_light_model: "openai/gpt-5-nano" + openai_heavy_model: "openai/gpt-5-mini" From 218ecc829a4e7cbf25942cc11b00e312294055d8 Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 17:24:03 +0100 Subject: [PATCH 02/10] chore: test changes --- .../notes/controller/NotesController.test.ts | 17 ++ src/features/App/Profile/index.tsx | 85 ++++--- .../Profile/services/useVaultConfigSync.tsx | 38 +--- .../ProfileCreator/index.tsx | 0 .../ProfileLoginForm/index.tsx | 2 +- .../{WorkspaceManager => }/ProfilesForm.tsx | 0 src/features/App/VaultScreenManager.tsx | 131 +++++++++++ src/features/App/Workspace/index.tsx | 37 +--- .../Workspace/services/WorkspaceServices.tsx | 3 + .../services/useWorkspaceConfigSync.tsx | 45 ++++ .../Workspace/useWorkspaceInitialization.tsx | 152 +++++++++++++ .../App/Workspace/useWorkspaceState.tsx | 70 ++++++ src/features/App/WorkspaceManager/index.tsx | 209 ------------------ src/features/App/index.tsx | 103 ++++----- src/features/App/useProfilesList.ts | 3 +- src/features/App/useRecentProfile.ts | 10 +- src/features/App/useVaultOpener.tsx | 92 ++++++++ src/state/redux/profiles/profiles.ts | 20 ++ 18 files changed, 627 insertions(+), 390 deletions(-) rename src/features/App/{WorkspaceManager => }/ProfileCreator/index.tsx (100%) rename src/features/App/{WorkspaceManager => }/ProfileLoginForm/index.tsx (97%) rename src/features/App/{WorkspaceManager => }/ProfilesForm.tsx (100%) create mode 100644 src/features/App/VaultScreenManager.tsx create mode 100644 src/features/App/Workspace/services/useWorkspaceConfigSync.tsx create mode 100644 src/features/App/Workspace/useWorkspaceInitialization.tsx create mode 100644 src/features/App/Workspace/useWorkspaceState.tsx delete mode 100644 src/features/App/WorkspaceManager/index.tsx create mode 100644 src/features/App/useVaultOpener.tsx diff --git a/src/core/features/notes/controller/NotesController.test.ts b/src/core/features/notes/controller/NotesController.test.ts index c9b2b6373..f7aaeab27 100644 --- a/src/core/features/notes/controller/NotesController.test.ts +++ b/src/core/features/notes/controller/NotesController.test.ts @@ -392,6 +392,23 @@ describe('data fetching', () => { await db.close(); }); + + test('method getById returns all found notes and ignores missing ones', async () => { + const db = await openDatabase(dbFile); + const registry = new NotesController(db, FAKE_WORKSPACE_ID); + + const notesId = await registry + .get({ limit: 10 }) + .then((notes) => notes.map((note) => note.id)); + + const notExistingNote = getUUID(); + + await expect( + registry.getById([...notesId, notExistingNote]), + ).resolves.toHaveLength(10); + + await db.close(); + }); }); describe('multi instances', () => { diff --git a/src/features/App/Profile/index.tsx b/src/features/App/Profile/index.tsx index 11cf83e6b..8a72325b8 100644 --- a/src/features/App/Profile/index.tsx +++ b/src/features/App/Profile/index.tsx @@ -1,10 +1,12 @@ import React, { createContext, FC, useEffect, useMemo, useState } from 'react'; import { isEqual } from 'lodash'; +import { Box } from '@chakra-ui/react'; import { FileController } from '@core/features/files/FileController'; import { StateFile } from '@core/features/files/StateFile'; import { LexemesRegistry } from '@core/features/notes/controller/LexemesRegistry'; import { WorkspacesController } from '@core/features/workspaces/WorkspacesController'; import { StatusBarProvider } from '@features/MainScreen/StatusBar/StatusBarProvider'; +import { SplashScreen } from '@features/SplashScreen'; import { GLOBAL_COMMANDS } from '@hooks/commands'; import { useShortcutsBinding } from '@hooks/commands/shortcuts/useShortcutsBinding'; import { useCommandCallback } from '@hooks/commands/useCommandCallback'; @@ -14,9 +16,8 @@ import { createWorkspaceObject, defaultVaultConfig, ProfileConfigScheme, + selectIsActiveWorkspaceReady, selectWorkspacesInfo, - WorkspaceConfigScheme, - WorkspaceData, workspacesApi, } from '@state/redux/profiles/profiles'; import { createContextGetterHook } from '@utils/react/createContextGetterHook'; @@ -77,20 +78,6 @@ export const Profile: FC = ({ profile: currentProfile, controls }) ]).then(async ([workspaces, config, state]) => { const [defaultWorkspace] = workspaces; - const workspaceConfigs = await Promise.all( - workspaces.map(async (workspace) => { - const config = await new StateFile( - new FileController( - `workspaces/${workspace.id}/config.json`, - controls.profile.files, - ), - WorkspaceConfigScheme, - ).get(); - - return [workspace.id, config] as const; - }), - ).then((entries) => Object.fromEntries(entries)); - if (!defaultWorkspace) return; dispatch( @@ -99,19 +86,10 @@ export const Profile: FC = ({ profile: currentProfile, controls }) profile: { activeWorkspace: null, workspaces: Object.fromEntries( - workspaces.map((workspace) => { - const workspaceObject = createWorkspaceObject(workspace); - return [ - workspace.id, - { - ...workspaceObject, - config: { - ...workspaceObject.config, - ...workspaceConfigs[workspace.id], - }, - } satisfies WorkspaceData, - ]; - }), + workspaces.map((workspace) => [ + workspace.id, + createWorkspaceObject(workspace), + ]), ), config: { ...defaultVaultConfig, @@ -158,28 +136,39 @@ export const Profile: FC = ({ profile: currentProfile, controls }) enabled: controls.profile.profile.isEncrypted, }); + const isActiveWorkspaceReady = useAppSelector( + selectIsActiveWorkspaceReady({ profileId }), + ); + return ( + {!isActiveWorkspaceReady && } {workspaces.length > 0 && } - {workspaces.map((workspace) => - workspace.touched ? ( - - - - - {isDevMode && ( - - )} - - - ) : null, - )} + + {workspaces.map((workspace) => + workspace.touched ? ( + + + + + {isDevMode && ( + + )} + + + ) : null, + )} + {isDevMode && ( { }, }); }, [files, profileId, watchSelector]); - - const workspaces = useAppSelector(selectWorkspacesInfo({ profileId }), isEqual); - useEffect(() => { - const cleanups = workspaces - .filter((workspace) => workspace.touched) - .map((workspace) => { - const workspaceConfig = new StateFile( - new FileController(`workspaces/${workspace.id}/config.json`, files), - WorkspaceConfigScheme, - ); - - return watchSelector({ - selector: createAppSelector( - selectWorkspace({ profileId, workspaceId: workspace.id }), - (workspace) => workspace?.config, - ), - onChange(config) { - if (!config) return; - - workspaceConfig.set(config).then(() => { - console.debug('Workspace config is saved'); - }); - }, - }); - }); - - return () => cleanups.forEach((cleanup) => cleanup()); - }, [files, profileId, watchSelector, workspaces]); }; diff --git a/src/features/App/WorkspaceManager/ProfileCreator/index.tsx b/src/features/App/ProfileCreator/index.tsx similarity index 100% rename from src/features/App/WorkspaceManager/ProfileCreator/index.tsx rename to src/features/App/ProfileCreator/index.tsx diff --git a/src/features/App/WorkspaceManager/ProfileLoginForm/index.tsx b/src/features/App/ProfileLoginForm/index.tsx similarity index 97% rename from src/features/App/WorkspaceManager/ProfileLoginForm/index.tsx rename to src/features/App/ProfileLoginForm/index.tsx index 9cfae097e..2ae42b13e 100644 --- a/src/features/App/WorkspaceManager/ProfileLoginForm/index.tsx +++ b/src/features/App/ProfileLoginForm/index.tsx @@ -2,11 +2,11 @@ import React, { FC, useCallback, useEffect, useId, useState } from 'react'; import { Button, Input, useToast, VStack } from '@chakra-ui/react'; import { TELEMETRY_EVENT_NAME } from '@core/features/telemetry'; import { ProfileObject } from '@core/storage/ProfilesManager'; +import { OnPickProfile } from '@features/App/useVaultOpener'; import { useTelemetryTracker } from '@features/telemetry'; import { useFocusableRef } from '@hooks/useFocusableRef'; import { ProfilesForm } from '../ProfilesForm'; -import { OnPickProfile } from '..'; export type ProfileLoginFormProps = { profile: ProfileObject; diff --git a/src/features/App/WorkspaceManager/ProfilesForm.tsx b/src/features/App/ProfilesForm.tsx similarity index 100% rename from src/features/App/WorkspaceManager/ProfilesForm.tsx rename to src/features/App/ProfilesForm.tsx diff --git a/src/features/App/VaultScreenManager.tsx b/src/features/App/VaultScreenManager.tsx new file mode 100644 index 000000000..6de9a7488 --- /dev/null +++ b/src/features/App/VaultScreenManager.tsx @@ -0,0 +1,131 @@ +import React, { useMemo, useState } from 'react'; +import { FaUser } from 'react-icons/fa6'; +import { Button, Divider, HStack, Text } from '@chakra-ui/react'; +import { NestedList } from '@components/NestedList'; +import { TELEMETRY_EVENT_NAME } from '@core/features/telemetry'; +import { telemetry } from '@electron/requests/telemetry/renderer'; +import { getRandomItem } from '@utils/collections/getRandomItem'; + +import { ProfileCreator } from './ProfileCreator'; +import { ProfileLoginForm } from './ProfileLoginForm'; +import { ProfilesForm } from './ProfilesForm'; +import { ProfilesListApi } from './useProfilesList'; +import { OnPickProfile } from './useVaultOpener'; + +export type VaultScreenManagerProps = { + profiles: ProfilesListApi; + currentProfile: string | null; + onChooseProfile: (id: string | null) => void; + onOpenProfile: OnPickProfile; + onLoginStart?: () => void; +}; + +export const defaultVaultNames = [ + 'Creative drafts', + 'Second brain', + 'Digital garden', + 'Creative space', + 'Mind space', + 'Idea lab', +]; + +export const VaultScreenManager = ({ + currentProfile, + profiles, + onOpenProfile, + onChooseProfile, + onLoginStart, +}: VaultScreenManagerProps) => { + const currentVaultObject = useMemo( + () => profiles.profiles?.find((profile) => profile.id === currentProfile) ?? null, + [currentProfile, profiles.profiles], + ); + + const [screen, setScreen] = useState<'create' | 'choose'>('choose'); + const hasNoVaults = profiles.profiles.length === 0; + + if (currentVaultObject) { + return ( + { + if (onLoginStart) onLoginStart(); + return await onOpenProfile(...arg); + }} + onPickAnotherProfile={() => onChooseProfile(null)} + /> + ); + } + + if (screen === 'create' || hasNoVaults) { + return ( + + profiles.createProfile(profile).then((newProfile) => { + onOpenProfile(newProfile, profile.password ?? undefined).then( + console.warn, + ); + }) + } + onCancel={hasNoVaults ? undefined : () => setScreen('choose')} + defaultProfileName={ + hasNoVaults ? getRandomItem(defaultVaultNames) : undefined + } + /> + ); + } + + return ( + setScreen('create')} + > + Create new profile + + } + > + } + sx={{ + w: '100%', + borderRadius: '4px', + maxHeight: '230px', + overflow: 'auto', + border: '1px solid', + }} + items={profiles.profiles.map((profile) => ({ + id: profile.id, + content: ( + { + onChooseProfile(profile.id); + + if (profile.encryption === null) { + onOpenProfile(profile); + } + + telemetry.track(TELEMETRY_EVENT_NAME.PROFILE_SELECTED); + }} + > + + {profile.name} + + ), + }))} + /> + + ); +}; diff --git a/src/features/App/Workspace/index.tsx b/src/features/App/Workspace/index.tsx index bc044011c..9f6275845 100644 --- a/src/features/App/Workspace/index.tsx +++ b/src/features/App/Workspace/index.tsx @@ -1,9 +1,8 @@ -import React, { createContext, FC, useEffect, useMemo } from 'react'; +import React, { createContext, FC, useMemo } from 'react'; import { isEqual } from 'lodash'; import { Box } from '@chakra-ui/react'; import { INote } from '@core/features/notes'; import { MainScreen } from '@features/MainScreen'; -import { SplashScreen } from '@features/SplashScreen'; import { WorkspaceModalProvider } from '@features/WorkspaceModal/useWorkspaceModal'; import { useAppDispatch, useAppSelector } from '@state/redux/hooks'; import { useWorkspaceData, useWorkspaceSelector } from '@state/redux/profiles/hooks'; @@ -18,6 +17,7 @@ import { ProfileContainer } from '../Profiles/hooks/useProfileContainers'; import { SettingsWindow } from '../Settings/SettingsWindow'; import { WorkspaceServices } from './services/WorkspaceServices'; import { useWorkspace } from './useWorkspace'; +import { useWorkspaceInitialization } from './useWorkspaceInitialization'; import { WorkspaceProvider } from './WorkspaceProvider'; import { WorkspaceStatusBarItems } from './WorkspaceStatusBarItems'; @@ -39,34 +39,9 @@ export const Workspace: FC = ({ profile }) => { const dispatch = useAppDispatch(); const workspaceData = useWorkspaceData(); - const { name: workspaceName } = useWorkspaceSelector(selectWorkspaceName); - - // Close notes by unmount - useEffect(() => { - return () => { - // TODO: use actual workspace id once will be implemented - dispatch(workspacesApi.setActiveNote({ ...workspaceData, noteId: null })); - dispatch(workspacesApi.setOpenedNotes({ ...workspaceData, notes: [] })); - dispatch(workspacesApi.setNoteIds({ ...workspaceData, noteIds: [] })); - }; - }, [dispatch, workspaceData]); - - // TODO: replace to hook - // Load tags - useEffect(() => { - if (!workspace) return; + useWorkspaceInitialization(workspace); - const { tagsRegistry } = workspace; - const updateTags = () => - tagsRegistry.getTags().then((tags) => { - dispatch(workspacesApi.setTags({ ...workspaceData, tags })); - }); - - updateTags(); - - const cleanup = tagsRegistry.onChange(updateTags); - return cleanup; - }, [dispatch, workspace, workspaceData]); + const { name: workspaceName } = useWorkspaceSelector(selectWorkspaceName); const { profileId } = useWorkspaceData(); @@ -91,7 +66,7 @@ export const Workspace: FC = ({ profile }) => { backgroundColor: 'surface.background', }} > - {workspace ? ( + {workspace && ( = ({ profile }) => { - ) : ( - )} ); diff --git a/src/features/App/Workspace/services/WorkspaceServices.tsx b/src/features/App/Workspace/services/WorkspaceServices.tsx index fc17a3334..c9b70bca8 100644 --- a/src/features/App/Workspace/services/WorkspaceServices.tsx +++ b/src/features/App/Workspace/services/WorkspaceServices.tsx @@ -1,7 +1,10 @@ import { useLexemesRegistryPrune } from './useLexemesRegistryPrune'; +import { useWorkspaceConfigSync } from './useWorkspaceConfigSync'; export const WorkspaceServices = () => { useLexemesRegistryPrune(); + useWorkspaceConfigSync(); + return null; }; diff --git a/src/features/App/Workspace/services/useWorkspaceConfigSync.tsx b/src/features/App/Workspace/services/useWorkspaceConfigSync.tsx new file mode 100644 index 000000000..07815f945 --- /dev/null +++ b/src/features/App/Workspace/services/useWorkspaceConfigSync.tsx @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; +import { FileController } from '@core/features/files/FileController'; +import { StateFile } from '@core/features/files/StateFile'; +import { useWatchSelector } from '@hooks/useWatchSelector'; +import { useWorkspaceData } from '@state/redux/profiles/hooks'; +import { selectWorkspace, WorkspaceConfigScheme } from '@state/redux/profiles/profiles'; +import { createAppSelector } from '@state/redux/utils'; + +import { useProfileControls } from '../../Profile'; + +export const useWorkspaceConfigSync = () => { + const { + profile: { + files, + profile: { id: profileId }, + }, + } = useProfileControls(); + + const workspaceData = useWorkspaceData(); + const watchSelector = useWatchSelector(); + + useEffect(() => { + const workspaceConfig = new StateFile( + new FileController( + `workspaces/${workspaceData.workspaceId}/config.json`, + files, + ), + WorkspaceConfigScheme, + ); + + return watchSelector({ + selector: createAppSelector( + selectWorkspace({ profileId, workspaceId: workspaceData.workspaceId }), + (workspace) => workspace?.config, + ), + onChange(config) { + if (!config) return; + + workspaceConfig.set(config).then(() => { + console.debug('Workspace config is saved'); + }); + }, + }); + }, [files, profileId, watchSelector, workspaceData.workspaceId]); +}; diff --git a/src/features/App/Workspace/useWorkspaceInitialization.tsx b/src/features/App/Workspace/useWorkspaceInitialization.tsx new file mode 100644 index 000000000..7b5f3b229 --- /dev/null +++ b/src/features/App/Workspace/useWorkspaceInitialization.tsx @@ -0,0 +1,152 @@ +import { useEffect } from 'react'; +import { FileController } from '@core/features/files/FileController'; +import { StateFile } from '@core/features/files/StateFile'; +import { useAppDispatch } from '@state/redux/hooks'; +import { useWorkspaceData } from '@state/redux/profiles/hooks'; +import { + NOTES_VIEW, + WorkspaceConfigScheme, + workspacesApi, +} from '@state/redux/profiles/profiles'; + +import { useProfileControls } from '../Profile'; +import { WorkspaceContainer } from './useWorkspace'; +import { useWorkspaceState } from './useWorkspaceState'; + +export const useWorkspaceInitialization = (workspace: WorkspaceContainer | null) => { + const dispatch = useAppDispatch(); + const workspaceData = useWorkspaceData(); + const controls = useProfileControls(); + + // Load workspace config + useEffect(() => { + const initConfig = async () => { + const workspaceConfig = await new StateFile( + new FileController( + `workspaces/${workspaceData.workspaceId}/config.json`, + controls.profile.files, + ), + WorkspaceConfigScheme, + ).get(); + + if (!workspaceConfig) return; + + dispatch( + workspacesApi.setWorkspaceNoteTemplateConfig({ + ...workspaceData, + title: workspaceConfig.newNote.title, + tags: workspaceConfig.newNote.tags, + }), + ); + }; + + initConfig(); + }, [controls.profile.files, dispatch, workspaceData]); + + // Init workspace state + const getWorkspaceState = useWorkspaceState({ + sync: Boolean(workspace), + controls, + workspaceId: workspaceData.workspaceId, + }); + useEffect(() => { + if (!workspace) return; + + const initWorkspace = async () => { + try { + const [state, tags] = await Promise.all([ + getWorkspaceState(), + workspace.tagsRegistry.getTags(), + ]); + + // Load tags + dispatch(workspacesApi.setTags({ ...workspaceData, tags })); + + if (!state) return; + const { search, view, selectedTagId, openedNoteIds, activeNoteId } = + state; + + dispatch( + workspacesApi.setSearch({ + ...workspaceData, + search: search ?? '', + }), + ); + + if (view) { + dispatch( + workspacesApi.setView({ + ...workspaceData, + view: view as NOTES_VIEW, + }), + ); + } + + // Restore selected tag only if it exists + const selectedTag = tags.find((tag) => tag.id === selectedTagId); + if (selectedTag) { + dispatch( + workspacesApi.setSelectedTag({ + ...workspaceData, + tag: selectedTag.id, + }), + ); + } + + // Restore notes + if (!openedNoteIds || openedNoteIds.length === 0) return; + + const notes = await workspace.notesRegistry.getById(openedNoteIds); + if (!notes || notes.length === 0) return; + + dispatch( + workspacesApi.setOpenedNotes({ + ...workspaceData, + notes, + }), + ); + + // Fallback to first note if saved active note is missing + const activeNote = + (activeNoteId && notes.find((n) => n.id === activeNoteId)) || + notes[0]; + dispatch( + workspacesApi.setActiveNote({ + ...workspaceData, + noteId: activeNote.id, + }), + ); + } finally { + dispatch( + workspacesApi.setIsWorkspaceReady({ + ...workspaceData, + isReady: true, + }), + ); + } + }; + initWorkspace(); + + // Cleanup notes and ready state on unmount + return () => { + dispatch( + workspacesApi.setIsWorkspaceReady({ ...workspaceData, isReady: false }), + ); + + dispatch(workspacesApi.setActiveNote({ ...workspaceData, noteId: null })); + dispatch(workspacesApi.setOpenedNotes({ ...workspaceData, notes: [] })); + dispatch(workspacesApi.setNoteIds({ ...workspaceData, noteIds: [] })); + }; + }, [workspace, workspaceData, getWorkspaceState, dispatch]); + + // Sync tag changes with the store + useEffect(() => { + if (!workspace) return; + + return workspace.tagsRegistry.onChange(() => { + workspace.tagsRegistry.getTags().then((tags) => { + dispatch(workspacesApi.setTags({ ...workspaceData, tags })); + }); + }); + }, [workspace, workspaceData, dispatch]); +}; diff --git a/src/features/App/Workspace/useWorkspaceState.tsx b/src/features/App/Workspace/useWorkspaceState.tsx new file mode 100644 index 000000000..4ca2b9561 --- /dev/null +++ b/src/features/App/Workspace/useWorkspaceState.tsx @@ -0,0 +1,70 @@ +import { useCallback, useEffect, useState } from 'react'; +import z from 'zod'; +import { FileController } from '@core/features/files/FileController'; +import { StateFile } from '@core/features/files/StateFile'; +import { useWatchSelector } from '@hooks/useWatchSelector'; +import { selectWorkspace } from '@state/redux/profiles/profiles'; +import { createAppSelector } from '@state/redux/utils'; + +import { ProfileControls } from '../Profile'; + +const WorkspaceStateScheme = z.object({ + openedNoteIds: z.array(z.string()).nullable(), + activeNoteId: z.string().nullable(), + selectedTagId: z.string().nullable(), + view: z.string().nullable(), + search: z.string().nullable(), +}); + +export const useWorkspaceState = ({ + sync, + controls: { + profile: { + files, + profile: { id: profileId }, + }, + }, + workspaceId, +}: { + sync: boolean; + controls: ProfileControls; + workspaceId: string; +}) => { + const [workspaceState] = useState( + () => + new StateFile( + new FileController(`workspaces/${workspaceId}/state.json`, files), + WorkspaceStateScheme.partial(), + ), + ); + + const watchSelector = useWatchSelector(); + useEffect(() => { + if (!sync) return; + + return watchSelector({ + selector: createAppSelector( + selectWorkspace({ profileId, workspaceId }), + (state) => { + if (!state) return null; + + return { + openedNoteIds: state.openedNotes.map((n) => n.id), + activeNoteId: state.activeNote, + selectedTagId: state.tags.selected, + view: state.view, + search: state.search, + }; + }, + ), + onChange(state) { + if (!state) return; + + workspaceState.set(state); + }, + init: false, + }); + }, [profileId, sync, workspaceState, watchSelector, workspaceId]); + + return useCallback(() => workspaceState.get(), [workspaceState]); +}; diff --git a/src/features/App/WorkspaceManager/index.tsx b/src/features/App/WorkspaceManager/index.tsx deleted file mode 100644 index 5ba3dfa58..000000000 --- a/src/features/App/WorkspaceManager/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React, { FC, useCallback, useMemo, useState } from 'react'; -import { FaUser } from 'react-icons/fa6'; -import { useDebounce } from 'use-debounce'; -import { Box, Button, Divider, HStack, Text } from '@chakra-ui/react'; -import { NestedList } from '@components/NestedList'; -import { TELEMETRY_EVENT_NAME } from '@core/features/telemetry'; -import { ProfileObject } from '@core/storage/ProfilesManager'; -import { SplashScreen } from '@features/SplashScreen'; -import { useTelemetryTracker } from '@features/telemetry'; -import { getRandomItem } from '@utils/collections/getRandomItem'; - -import { ProfilesApi } from '../Profiles/hooks/useProfileContainers'; -import { ProfilesListApi } from '../useProfilesList'; -import { ProfileCreator } from './ProfileCreator'; -import { ProfileLoginForm } from './ProfileLoginForm'; -import { ProfilesForm } from './ProfilesForm'; - -type PickProfileResponse = { - status: 'ok' | 'error'; - message?: string; -}; - -export type OnPickProfile = ( - profile: ProfileObject, - password?: string, -) => Promise; - -export type IWorkspacePickerProps = { - profiles: ProfilesApi; - profilesManager: ProfilesListApi; - currentProfile: string | null; - onChooseProfile: (id: string | null) => void; -}; - -const defaultProfileNames = [ - 'Creative drafts', - 'Second brain', - 'Digital garden', - 'Creative space', - 'Mind space', - 'Idea lab', -]; - -/** - * Manages a workspace profiles - */ -export const WorkspaceManager: FC = ({ - profilesManager, - profiles, - currentProfile, - onChooseProfile, -}) => { - const telemetry = useTelemetryTracker(); - - const [isProfileLoading, setIsProfileLoading] = useState(false); - const onOpenProfile: OnPickProfile = useCallback( - async (profile: ProfileObject, password?: string) => { - setIsProfileLoading(true); - - // Profiles with no password - if (!profile.encryption) { - await profiles.openProfile({ profile }, true); - return { status: 'ok' }; - } - - // Profiles with password - if (password === undefined) - return { status: 'error', message: 'Enter password' }; - - try { - await profiles.openProfile({ profile, password }, true); - return { status: 'ok' }; - } catch (err) { - console.error(err); - - return { status: 'error', message: 'Invalid password' }; - } finally { - setIsProfileLoading(false); - } - }, - [profiles], - ); - - const currentProfileObject = useMemo( - () => - profilesManager.profiles?.find((profile) => profile.id === currentProfile) ?? - null, - [currentProfile, profilesManager.profiles], - ); - - const [isProfileCreationScreen, setIsProfileCreationScreen] = useState(false); - - const [isShowSplash] = useDebounce(isProfileLoading, 500); - const content = useMemo(() => { - if (currentProfileObject) { - return ( - { - onChooseProfile(null); - }} - /> - ); - } - - // show a loading screen while the profile is opening - if (isProfileLoading || isShowSplash) return ; - - const hasNoProfiles = profilesManager.profiles.length === 0; - if (isProfileCreationScreen || hasNoProfiles) { - return ( - - profilesManager.createProfile(profile).then((newProfile) => { - setIsProfileCreationScreen(false); - onOpenProfile(newProfile, profile.password ?? undefined).then( - console.warn, - ); - }) - } - onCancel={ - hasNoProfiles - ? undefined - : () => setIsProfileCreationScreen(false) - } - defaultProfileName={ - hasNoProfiles ? getRandomItem(defaultProfileNames) : undefined - } - /> - ); - } - - return ( - - - - } - > - } - sx={{ - w: '100%', - borderRadius: '4px', - maxHeight: '230px', - overflow: 'auto', - - border: '1px solid', - }} - items={(profilesManager.profiles ?? []).map((profile) => ({ - id: profile.id, - content: ( - { - onChooseProfile(profile.id); - if (profile.encryption === null) { - onOpenProfile(profile); - } - - telemetry.track( - TELEMETRY_EVENT_NAME.PROFILE_SELECTED, - ); - }} - > - - {profile.name} - - ), - }))} - /> - - ); - }, [ - currentProfileObject, - onChooseProfile, - onOpenProfile, - profilesManager, - telemetry, - isShowSplash, - isProfileLoading, - isProfileCreationScreen, - ]); - - return ( - - - {content} - - - ); -}; diff --git a/src/features/App/index.tsx b/src/features/App/index.tsx index 1884c19b0..4c6e2a16d 100644 --- a/src/features/App/index.tsx +++ b/src/features/App/index.tsx @@ -1,5 +1,4 @@ import React, { FC, useEffect, useState } from 'react'; -import { useDebounce } from 'use-debounce'; import { Box } from '@chakra-ui/react'; import { ConfigStorage } from '@core/storage/ConfigStorage'; import { ElectronFilesController, storageApi } from '@electron/requests/storage/renderer'; @@ -11,7 +10,8 @@ import { useProfileContainers } from './Profiles/hooks/useProfileContainers'; import { useProfileSelector } from './useProfileSelector'; import { useProfilesList } from './useProfilesList'; import { useRecentProfile } from './useRecentProfile'; -import { WorkspaceManager } from './WorkspaceManager'; +import { useVaultOpener } from './useVaultOpener'; +import { VaultScreenManager } from './VaultScreenManager'; export const App: FC = () => { const [config] = useState( @@ -25,79 +25,66 @@ export const App: FC = () => { const profilesList = useProfilesList(); const profileContainers = useProfileContainers(); - const [loadingState, setLoadingState] = useState<{ - isProfilesLoading: boolean; - isProfileLoading: boolean; - }>({ - isProfilesLoading: true, - isProfileLoading: false, - }); - const [currentProfile, setCurrentProfile] = useProfileSelector(config); + const [currentProfileId, setCurrentProfileId] = useProfileSelector(config); // Open recent profile const recentProfile = useRecentProfile(config); - useEffect( - () => { - if (!profilesList.isProfilesLoaded || !recentProfile.isLoaded) return; - - // Restore profile id - setCurrentProfile(recentProfile.profileId); - - const profile = profilesList.profiles.find( - (profile) => profile.id === recentProfile.profileId, - ); - if (!profile || profile.encryption) { - setLoadingState((state) => ({ ...state, isProfilesLoading: false })); - return; - } - - // Automatically open profile with no encryption - profileContainers.openProfile({ profile }, true); - setLoadingState({ - isProfilesLoading: false, - isProfileLoading: true, - }); - }, + const { isProfileOpening, onOpenProfile } = useVaultOpener({ + profilesList, + recentProfile, + setCurrentProfileId, + profiles: profileContainers, + }); - // Depends only of loading status and run only once - // eslint-disable-next-line react-hooks/exhaustive-deps - [profilesList.isProfilesLoaded, recentProfile.isLoaded], - ); + const [isAutoLoading, setIsAutoLoading] = useState(true); + const isLoading = + !profilesList.isProfilesLoaded || + !recentProfile.isLoaded || + (isProfileOpening && isAutoLoading); - // Handle case with auto open profile. Wait the end of loading + // Show Splash immediately, but delay hiding it + const [isShowSplash, setIsShowSplash] = useState(isLoading); useEffect(() => { - if (profileContainers.profiles.length > 0) { - setLoadingState((state) => ({ ...state, isProfileLoading: false })); + if (isLoading) { + setIsShowSplash(true); + return; } - }, [profileContainers.profiles.length]); - const isLoadingState = Object.values(loadingState).some(Boolean); - const [isShowSplash] = useDebounce(isLoadingState, 500); + const timer = setTimeout(() => setIsShowSplash(false), 400); + return () => clearTimeout(timer); + }, [isLoading]); + if (isShowSplash) { return ; } - if (profileContainers.profiles.length === 0) { + // Vault screen + if (profileContainers.profiles.length > 0) { return ( - + + + + ); } return ( - - - + + + setIsAutoLoading(false)} + /> + ); }; diff --git a/src/features/App/useProfilesList.ts b/src/features/App/useProfilesList.ts index 35e1f624b..4b32dac04 100644 --- a/src/features/App/useProfilesList.ts +++ b/src/features/App/useProfilesList.ts @@ -4,7 +4,8 @@ import { getRandomBytes } from '@core/encryption/utils/random'; import { createEncryption } from '@core/features/encryption/createEncryption'; import { ProfileObject, ProfilesManager } from '@core/storage/ProfilesManager'; import { ElectronFilesController, storageApi } from '@electron/requests/storage/renderer'; -import { NewProfile } from '@features/App/WorkspaceManager/ProfileCreator'; + +import { NewProfile } from './ProfileCreator'; export type ProfilesListApi = { isProfilesLoaded: boolean; diff --git a/src/features/App/useRecentProfile.ts b/src/features/App/useRecentProfile.ts index cb1272617..8d45bf1f5 100644 --- a/src/features/App/useRecentProfile.ts +++ b/src/features/App/useRecentProfile.ts @@ -1,11 +1,13 @@ import { useEffect, useRef, useState } from 'react'; import { ConfigStorage } from '@core/storage/ConfigStorage'; +export type RecentProfile = { + isLoaded: boolean; + profileId: string | null; +}; + export const useRecentProfile = (config: ConfigStorage) => { - const [profileId, setProfileId] = useState<{ - isLoaded: boolean; - profileId: string | null; - }>({ + const [profileId, setProfileId] = useState({ isLoaded: false, profileId: null, }); diff --git a/src/features/App/useVaultOpener.tsx b/src/features/App/useVaultOpener.tsx new file mode 100644 index 000000000..8352b74cb --- /dev/null +++ b/src/features/App/useVaultOpener.tsx @@ -0,0 +1,92 @@ +import { useCallback, useEffect, useState } from 'react'; +import { ProfileObject } from '@core/storage/ProfilesManager'; + +import { ProfilesApi } from './Profiles/hooks/useProfileContainers'; +import { ProfilesListApi } from './useProfilesList'; +import { RecentProfile } from './useRecentProfile'; + +type PickProfileResponse = { + status: 'ok' | 'error'; + message?: string; +}; + +export type OnPickProfile = ( + profile: ProfileObject, + password?: string, +) => Promise; + +type UseProfileLoaderProps = { + profilesList: ProfilesListApi; + profiles: ProfilesApi; + recentProfile: RecentProfile; + setCurrentProfileId: (id: string | null) => void; +}; + +/** + * Handles vault opening, including auto-opening unencrypted vault + */ +export const useVaultOpener = ({ + profilesList, + profiles, + recentProfile, + setCurrentProfileId, +}: UseProfileLoaderProps) => { + const [isProfileOpening, setIsProfileOpening] = useState(false); + + const onOpenProfile: OnPickProfile = useCallback( + async (profile: ProfileObject, password?: string) => { + setIsProfileOpening(true); + + // Profiles with no password + if (!profile.encryption) { + await profiles.openProfile({ profile }, true); + setIsProfileOpening(false); + return { status: 'ok' }; + } + + // Profiles with password + if (password === undefined) + return { status: 'error', message: 'Enter password' }; + + try { + await profiles.openProfile({ profile, password }, true); + return { status: 'ok' }; + } catch (err) { + console.error(err); + + return { status: 'error', message: 'Invalid password' }; + } finally { + setIsProfileOpening(false); + } + }, + [profiles], + ); + + // Automatically open an unencrypted vault + useEffect( + () => { + if (!profilesList.isProfilesLoaded || !recentProfile.isLoaded) return; + + // Restore profile id + setCurrentProfileId(recentProfile.profileId); + + const profile = profilesList.profiles.find( + (profile) => profile.id === recentProfile.profileId, + ); + + if (!profile || profile.encryption) { + return; + } + + onOpenProfile(profile); + }, + // Depends only of loading status and run only once + // eslint-disable-next-line react-hooks/exhaustive-deps + [profilesList.isProfilesLoaded, recentProfile.isLoaded], + ); + + return { + isProfileOpening, + onOpenProfile, + }; +}; diff --git a/src/state/redux/profiles/profiles.ts b/src/state/redux/profiles/profiles.ts index 52968e2cc..a9024b7cc 100644 --- a/src/state/redux/profiles/profiles.ts +++ b/src/state/redux/profiles/profiles.ts @@ -59,6 +59,7 @@ export const createWorkspaceObject = (workspace: { }): WorkspaceData => ({ ...workspace, touched: false, + isWorkspaceReady: false, activeNote: null, recentlyClosedNotes: [], @@ -101,6 +102,7 @@ export const WorkspaceConfigScheme = z.object({ export type WorkspaceData = { id: string; name: string; + isWorkspaceReady: boolean; /** * Defines were workspace opened by user or not @@ -189,6 +191,18 @@ export const profilesSlice = createSlice({ state.activeProfile = payload; }, + setIsWorkspaceReady: ( + state, + { + payload: { profileId, workspaceId, isReady }, + }: PayloadAction>, + ) => { + const workspace = selectWorkspaceObject(state, { profileId, workspaceId }); + if (!workspace) return; + + workspace.isWorkspaceReady = isReady; + }, + setWorkspaces: ( state, { @@ -519,5 +533,11 @@ export const selectActiveWorkspaceInfo = (scope: ProfileScoped) => workspace ? getWorkspaceInfo(workspace) : null, ); +export const selectIsActiveWorkspaceReady = (scope: ProfileScoped) => + createAppSelector( + selectActiveWorkspace(scope), + (workspace) => workspace?.isWorkspaceReady ?? false, + ); + export * from './selectors/notes'; export * from './selectors/tags'; From 6bd613dd137413dcee6a16ac4c02edfafcce3ff2 Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 17:28:34 +0100 Subject: [PATCH 03/10] ci: run on pull_request --- .github/workflows/code-review.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 10c9ead70..13ede43aa 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -5,6 +5,7 @@ permissions: pull-requests: write on: + pull_request: pull_request_target: types: [opened, synchronize, reopened] pull_request_review_comment: From 9c7eca3efebe5f74bd369e469b5ee578acb3a939 Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 18:49:02 +0100 Subject: [PATCH 04/10] ci: enable debug mode --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 13ede43aa..2fda516f8 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -26,7 +26,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} with: - debug: false + debug: true review_simple_changes: false review_comment_lgtm: false openai_base_url: "https://openrouter.ai/api/v1" From 519ce50621e0f7a266b5d3cb1f9330a5ad882dac Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 19:05:51 +0100 Subject: [PATCH 05/10] ci: use another reviewer --- .github/workflows/code-review.yml | 51 ++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 2fda516f8..a00bd9b87 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -10,6 +10,23 @@ on: types: [opened, synchronize, reopened] pull_request_review_comment: types: [created] + workflow_dispatch: + inputs: + review-command: + type: choice + default: run + options: + - run + - run-inline + - run-context + - run-summary + - run-inline-reply + - run-summary-reply + - clear-inline + - clear-summary + pull-request-number: + type: string + required: true concurrency: group: ${{ github.repository }}-${{ github.event.number || github.head_ref || @@ -18,17 +35,29 @@ concurrency: cancel-in-progress: ${{ github.event_name != 'pull_request_review_comment' }} jobs: - review: + ai-review: runs-on: ubuntu-latest steps: - - uses: coderabbitai/ai-pr-reviewer@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: Nikita-Filonov/ai-review@v0.59.0 with: - debug: true - review_simple_changes: false - review_comment_lgtm: false - openai_base_url: "https://openrouter.ai/api/v1" - openai_light_model: "openai/gpt-5-nano" - openai_heavy_model: "openai/gpt-5-mini" + review-command: ${{ inputs.review-command || 'run-context' }} + env: + # --- LLM configuration --- + LLM__PROVIDER: "OPENROUTER" + LLM__META__MODEL: "openai/gpt-5-mini" + LLM__META__MAX_TOKENS: "400000" + LLM__META__TEMPERATURE: "0.3" + LLM__HTTP_CLIENT__API_URL: "https://openrouter.ai/api/v1" + LLM__HTTP_CLIENT__API_TOKEN: ${{ secrets.OPENAI_API_KEY }} + + # --- GitHub integration --- + VCS__PROVIDER: "GITHUB" + VCS__PIPELINE__OWNER: ${{ github.repository_owner }} + VCS__PIPELINE__REPO: ${{ github.event.repository.name }} + VCS__PIPELINE__PULL_NUMBER: ${{ github.event.pull_request.number || inputs.pull-request-number }} + VCS__HTTP_CLIENT__API_URL: "https://api.github.com" + VCS__HTTP_CLIENT__API_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 77d5cd63478be3d9bec72b90e415fa5ade29eb39 Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 19:07:52 +0100 Subject: [PATCH 06/10] ci: update tokens limit --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index a00bd9b87..1b236eab1 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -49,7 +49,7 @@ jobs: # --- LLM configuration --- LLM__PROVIDER: "OPENROUTER" LLM__META__MODEL: "openai/gpt-5-mini" - LLM__META__MAX_TOKENS: "400000" + LLM__META__MAX_TOKENS: "100000" LLM__META__TEMPERATURE: "0.3" LLM__HTTP_CLIENT__API_URL: "https://openrouter.ai/api/v1" LLM__HTTP_CLIENT__API_TOKEN: ${{ secrets.OPENAI_API_KEY }} From 3549d1c00ac3b04c084d2c16d0b8a11016f1a86b Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 19:17:23 +0100 Subject: [PATCH 07/10] ci: try meta-llama/llama-3-70b-instruct --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 1b236eab1..2d1a8a529 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -48,7 +48,7 @@ jobs: env: # --- LLM configuration --- LLM__PROVIDER: "OPENROUTER" - LLM__META__MODEL: "openai/gpt-5-mini" + LLM__META__MODEL: "meta-llama/llama-3-70b-instruct" LLM__META__MAX_TOKENS: "100000" LLM__META__TEMPERATURE: "0.3" LLM__HTTP_CLIENT__API_URL: "https://openrouter.ai/api/v1" From 25cb137de0483b09916774f3dce16e7e3106bd49 Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 19:19:38 +0100 Subject: [PATCH 08/10] ci: try anthropic/claude-sonnet-4.6 --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 2d1a8a529..4437dd776 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -48,7 +48,7 @@ jobs: env: # --- LLM configuration --- LLM__PROVIDER: "OPENROUTER" - LLM__META__MODEL: "meta-llama/llama-3-70b-instruct" + LLM__META__MODEL: "anthropic/claude-sonnet-4.6" LLM__META__MAX_TOKENS: "100000" LLM__META__TEMPERATURE: "0.3" LLM__HTTP_CLIENT__API_URL: "https://openrouter.ai/api/v1" From c07a80b049206236c4ca69801b9000e6daf04703 Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 19:31:41 +0100 Subject: [PATCH 09/10] ci: try ai-pr-reviewer with sonnet --- .github/workflows/code-review.yml | 51 +++++++------------------------ 1 file changed, 11 insertions(+), 40 deletions(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 4437dd776..c1ab835af 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -10,23 +10,6 @@ on: types: [opened, synchronize, reopened] pull_request_review_comment: types: [created] - workflow_dispatch: - inputs: - review-command: - type: choice - default: run - options: - - run - - run-inline - - run-context - - run-summary - - run-inline-reply - - run-summary-reply - - clear-inline - - clear-summary - pull-request-number: - type: string - required: true concurrency: group: ${{ github.repository }}-${{ github.event.number || github.head_ref || @@ -35,29 +18,17 @@ concurrency: cancel-in-progress: ${{ github.event_name != 'pull_request_review_comment' }} jobs: - ai-review: + review: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - uses: Nikita-Filonov/ai-review@v0.59.0 - with: - review-command: ${{ inputs.review-command || 'run-context' }} + - uses: coderabbitai/ai-pr-reviewer@latest env: - # --- LLM configuration --- - LLM__PROVIDER: "OPENROUTER" - LLM__META__MODEL: "anthropic/claude-sonnet-4.6" - LLM__META__MAX_TOKENS: "100000" - LLM__META__TEMPERATURE: "0.3" - LLM__HTTP_CLIENT__API_URL: "https://openrouter.ai/api/v1" - LLM__HTTP_CLIENT__API_TOKEN: ${{ secrets.OPENAI_API_KEY }} - - # --- GitHub integration --- - VCS__PROVIDER: "GITHUB" - VCS__PIPELINE__OWNER: ${{ github.repository_owner }} - VCS__PIPELINE__REPO: ${{ github.event.repository.name }} - VCS__PIPELINE__PULL_NUMBER: ${{ github.event.pull_request.number || inputs.pull-request-number }} - VCS__HTTP_CLIENT__API_URL: "https://api.github.com" - VCS__HTTP_CLIENT__API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + with: + debug: true + review_simple_changes: false + review_comment_lgtm: false + openai_base_url: "https://openrouter.ai/api/v1" + openai_light_model: "openai/gpt-5-nano" + openai_heavy_model: "anthropic/claude-sonnet-4.6" From 50e9a3a2fc5eb721ca6a92bba52b1733b4491ea1 Mon Sep 17 00:00:00 2001 From: Robert Vitonsky Date: Tue, 3 Mar 2026 19:36:44 +0100 Subject: [PATCH 10/10] ci: try meta-llama/llama-3-70b-instruct --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index c1ab835af..b760d82c7 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -31,4 +31,4 @@ jobs: review_comment_lgtm: false openai_base_url: "https://openrouter.ai/api/v1" openai_light_model: "openai/gpt-5-nano" - openai_heavy_model: "anthropic/claude-sonnet-4.6" + openai_heavy_model: "meta-llama/llama-3-70b-instruct"