feat: profiles slice
This commit is contained in:
parent
9c2db6b9c7
commit
b82d29c2df
|
@ -51,7 +51,7 @@
|
|||
"@continuedev/config-yaml": "^1.0.63",
|
||||
"@continuedev/fetch": "^1.0.4",
|
||||
"@continuedev/llm-info": "^1.0.2",
|
||||
"@continuedev/openai-adapters": "^1.0.10",
|
||||
"@continuedev/openai-adapters": "^1.0.18",
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { SlashCommandDescription } from "core";
|
||||
import { useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
|
||||
import {
|
||||
bookmarkSlashCommand,
|
||||
selectBookmarkedSlashCommands,
|
||||
selectSelectedProfileId,
|
||||
unbookmarkSlashCommand,
|
||||
} from "../../redux/slices/profiles/slice";
|
||||
import { setMainEditorContentTrigger } from "../../redux/slices/sessionSlice";
|
||||
import { getParagraphNodeFromString } from "../mainInput/utils";
|
||||
import { ConversationStarterCard } from "./ConversationStarterCard";
|
||||
|
@ -10,12 +15,23 @@ const NUM_CARDS_TO_RENDER_COLLAPSED = 3;
|
|||
|
||||
export function ConversationStarterCards() {
|
||||
const dispatch = useAppDispatch();
|
||||
const [bookmarkedCommands, setBookmarkedCommands] = useState<string[]>([]);
|
||||
|
||||
const slashCommands =
|
||||
useAppSelector((state) => state.config.config.slashCommands) ?? [];
|
||||
|
||||
const filteredSlashCommands = slashCommands?.filter(isDeprecatedCommandName);
|
||||
const selectedProfileId = useAppSelector(selectSelectedProfileId);
|
||||
const bookmarkedCommands = useAppSelector(selectBookmarkedSlashCommands);
|
||||
|
||||
const filteredSlashCommands = slashCommands.filter(isDeprecatedCommandName);
|
||||
|
||||
const bookmarkStatuses: Record<string, boolean> = {};
|
||||
if (selectedProfileId) {
|
||||
filteredSlashCommands.forEach((command) => {
|
||||
bookmarkStatuses[command.name] = bookmarkedCommands.includes(
|
||||
command.name,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function onClick(command: SlashCommandDescription) {
|
||||
if (command.prompt) {
|
||||
|
@ -26,15 +42,22 @@ export function ConversationStarterCards() {
|
|||
}
|
||||
|
||||
function handleBookmarkCommand(command: SlashCommandDescription) {
|
||||
setBookmarkedCommands((prev) => {
|
||||
if (prev.includes(command.name)) {
|
||||
return prev.filter((name) => name !== command.name);
|
||||
} else {
|
||||
return [...prev, command.name];
|
||||
}
|
||||
});
|
||||
}
|
||||
const isBookmarked = bookmarkStatuses[command.name];
|
||||
|
||||
if (isBookmarked) {
|
||||
dispatch(
|
||||
unbookmarkSlashCommand({
|
||||
commandName: command.name,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
bookmarkSlashCommand({
|
||||
commandName: command.name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!filteredSlashCommands || filteredSlashCommands.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -49,7 +72,7 @@ export function ConversationStarterCards() {
|
|||
command={command}
|
||||
onClick={onClick}
|
||||
onBookmark={handleBookmarkCommand}
|
||||
isBookmarked={bookmarkedCommands.includes(command.name)}
|
||||
isBookmarked={bookmarkStatuses[command.name]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -13,9 +13,9 @@ import React, {
|
|||
import ConfirmationDialog from "../components/dialogs/ConfirmationDialog";
|
||||
import { useWebviewListener } from "../hooks/useWebviewListener";
|
||||
import { updateOrgsThunk, updateProfilesThunk } from "../redux";
|
||||
import { selectSelectedProfile } from "../redux/";
|
||||
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
||||
import { setLastControlServerBetaEnabledStatus } from "../redux/slices/miscSlice";
|
||||
import { selectSelectedProfile } from "../redux/slices/sessionSlice";
|
||||
import { setDialogMessage, setShowDialog } from "../redux/slices/uiSlice";
|
||||
import { IdeMessengerContext } from "./IdeMessenger";
|
||||
|
||||
|
@ -45,9 +45,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
);
|
||||
|
||||
// Orgs
|
||||
const orgs = useAppSelector((store) => store.session.organizations);
|
||||
const orgs = useAppSelector((store) => store.profiles.organizations);
|
||||
const selectedOrgId = useAppSelector(
|
||||
(store) => store.session.selectedOrganizationId,
|
||||
(store) => store.profiles.selectedOrganizationId,
|
||||
);
|
||||
const selectedOrganization = useMemo(() => {
|
||||
if (!selectedOrgId) {
|
||||
|
@ -57,7 +57,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
}, [orgs, selectedOrgId]);
|
||||
|
||||
// Profiles
|
||||
const profiles = useAppSelector((store) => store.session.availableProfiles);
|
||||
const profiles = useAppSelector((store) => store.profiles.availableProfiles);
|
||||
const selectedProfile = useAppSelector(selectSelectedProfile);
|
||||
|
||||
const login: AuthContextType["login"] = (useOnboarding: boolean) => {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useAppDispatch, useAppSelector } from "../../redux/hooks";
|
|||
export function ScopeSelect() {
|
||||
const { organizations, selectedOrganization } = useAuth();
|
||||
const selectedOrgId = useAppSelector(
|
||||
(state) => state.session.selectedOrganizationId,
|
||||
(state) => state.profiles.selectedOrganizationId,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@ import { DiscordIcon } from "../../components/svg/DiscordIcon";
|
|||
import { GithubIcon } from "../../components/svg/GithubIcon";
|
||||
import { useAuth } from "../../context/Auth";
|
||||
import { IdeMessengerContext } from "../../context/IdeMessenger";
|
||||
import { selectSelectedProfile } from "../../redux/";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
|
||||
import { selectUseHub } from "../../redux/selectors";
|
||||
import { selectDefaultModel } from "../../redux/slices/configSlice";
|
||||
import { selectSelectedProfile } from "../../redux/slices/sessionSlice";
|
||||
import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
|
||||
import { isLocalProfile } from "../../util";
|
||||
import { providers } from "../AddNewModel/configs/providers";
|
||||
|
|
|
@ -2,11 +2,16 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||
import { ProfileDescription } from "core/config/ConfigHandler";
|
||||
import { OrganizationDescription } from "core/config/ProfileLifecycleManager";
|
||||
|
||||
interface PreferencesState {
|
||||
bookmarksByName: string[];
|
||||
}
|
||||
|
||||
interface ProfilesState {
|
||||
availableProfiles: ProfileDescription[] | null;
|
||||
selectedProfileId: string | null;
|
||||
organizations: OrganizationDescription[];
|
||||
selectedOrganizationId: string | null;
|
||||
preferencesByProfileId: Record<string, PreferencesState>;
|
||||
}
|
||||
|
||||
const initialState: ProfilesState = {
|
||||
|
@ -14,6 +19,7 @@ const initialState: ProfilesState = {
|
|||
selectedProfileId: null,
|
||||
organizations: [],
|
||||
selectedOrganizationId: null,
|
||||
preferencesByProfileId: {},
|
||||
};
|
||||
|
||||
export const profilesSlice = createSlice({
|
||||
|
@ -41,6 +47,43 @@ export const profilesSlice = createSlice({
|
|||
) => {
|
||||
state.selectedOrganizationId = payload;
|
||||
},
|
||||
bookmarkSlashCommand: (
|
||||
state,
|
||||
action: PayloadAction<{ commandName: string }>,
|
||||
) => {
|
||||
const { commandName } = action.payload;
|
||||
const profileId = state.selectedProfileId;
|
||||
|
||||
if (!profileId) return;
|
||||
|
||||
// Initialize preferences for this profile if needed
|
||||
if (!state.preferencesByProfileId[profileId]) {
|
||||
state.preferencesByProfileId[profileId] = {
|
||||
bookmarksByName: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Only add if not already bookmarked
|
||||
const bookmarks = state.preferencesByProfileId[profileId].bookmarksByName;
|
||||
if (!bookmarks.includes(commandName)) {
|
||||
bookmarks.push(commandName);
|
||||
}
|
||||
},
|
||||
|
||||
unbookmarkSlashCommand: (
|
||||
state,
|
||||
action: PayloadAction<{ commandName: string }>,
|
||||
) => {
|
||||
const { commandName } = action.payload;
|
||||
const profileId = state.selectedProfileId;
|
||||
|
||||
if (!profileId || !state.preferencesByProfileId[profileId]) return;
|
||||
|
||||
const preferences = state.preferencesByProfileId[profileId];
|
||||
preferences.bookmarksByName = preferences.bookmarksByName.filter(
|
||||
(cmd) => cmd !== commandName,
|
||||
);
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
selectSelectedProfile: (state) => {
|
||||
|
@ -50,6 +93,16 @@ export const profilesSlice = createSlice({
|
|||
) ?? null
|
||||
);
|
||||
},
|
||||
|
||||
selectSelectedProfileId: (state) => state.selectedProfileId,
|
||||
|
||||
selectBookmarkedSlashCommands: (state) => {
|
||||
if (!state.selectedProfileId) return [];
|
||||
const preferences = state.preferencesByProfileId[state.selectedProfileId];
|
||||
return preferences?.bookmarksByName || [];
|
||||
},
|
||||
|
||||
selectPreferencesByProfileId: (state) => state.preferencesByProfileId,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -58,8 +111,15 @@ export const {
|
|||
setSelectedProfile,
|
||||
setOrganizations,
|
||||
setSelectedOrganizationId,
|
||||
bookmarkSlashCommand,
|
||||
unbookmarkSlashCommand,
|
||||
} = profilesSlice.actions;
|
||||
|
||||
export const { selectSelectedProfile } = profilesSlice.selectors;
|
||||
export const {
|
||||
selectSelectedProfile,
|
||||
selectSelectedProfileId,
|
||||
selectBookmarkedSlashCommands,
|
||||
selectPreferencesByProfileId,
|
||||
} = profilesSlice.selectors;
|
||||
|
||||
export const { reducer: profilesReducer } = profilesSlice;
|
||||
|
|
|
@ -16,29 +16,29 @@ export const selectProfileThunk = createAsyncThunk<
|
|||
>("profiles/select", async (id, { dispatch, extra, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
if (state.session.availableProfiles === null) {
|
||||
if (state.profiles.availableProfiles === null) {
|
||||
// Currently in loading state
|
||||
return;
|
||||
}
|
||||
|
||||
const initialId = state.session.selectedProfileId;
|
||||
const initialId = state.profiles.selectedProfileId;
|
||||
|
||||
let newId = id;
|
||||
|
||||
// If no profiles, force clear
|
||||
if (state.session.availableProfiles.length === 0) {
|
||||
if (state.profiles.availableProfiles.length === 0) {
|
||||
newId = null;
|
||||
} else {
|
||||
// If new id doesn't match an existing profile, clear it
|
||||
if (newId) {
|
||||
if (!state.session.availableProfiles.find((p) => p.id === newId)) {
|
||||
if (!state.profiles.availableProfiles.find((p) => p.id === newId)) {
|
||||
newId = null;
|
||||
}
|
||||
}
|
||||
if (!newId) {
|
||||
// At this point if null ID and there ARE profiles,
|
||||
// Fallback to a profile, prioritizing the first in the list
|
||||
newId = state.session.availableProfiles[0].id;
|
||||
newId = state.profiles.availableProfiles[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,11 +56,11 @@ export const cycleProfile = createAsyncThunk<void, undefined, ThunkApiType>(
|
|||
async (_, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
if (state.session.availableProfiles === null) {
|
||||
if (state.profiles.availableProfiles === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const profileIds = state.session.availableProfiles.map(
|
||||
const profileIds = state.profiles.availableProfiles.map(
|
||||
(profile) => profile.id,
|
||||
);
|
||||
// In case of no profiles just does nothing
|
||||
|
@ -68,8 +68,8 @@ export const cycleProfile = createAsyncThunk<void, undefined, ThunkApiType>(
|
|||
return;
|
||||
}
|
||||
let nextId = profileIds[0];
|
||||
if (state.session.selectedProfileId) {
|
||||
const curIndex = profileIds.indexOf(state.session.selectedProfileId);
|
||||
if (state.profiles.selectedProfileId) {
|
||||
const curIndex = profileIds.indexOf(state.profiles.selectedProfileId);
|
||||
const nextIndex = (curIndex + 1) % profileIds.length;
|
||||
nextId = profileIds[nextIndex];
|
||||
}
|
||||
|
@ -96,15 +96,15 @@ export const selectOrgThunk = createAsyncThunk<
|
|||
ThunkApiType
|
||||
>("session/selectOrg", async (id, { dispatch, extra, getState }) => {
|
||||
const state = getState();
|
||||
const initialId = state.session.selectedOrganizationId;
|
||||
const initialId = state.profiles.selectedOrganizationId;
|
||||
let newId = id;
|
||||
|
||||
// If no orgs, force clear
|
||||
if (state.session.organizations.length === 0) {
|
||||
if (state.profiles.organizations.length === 0) {
|
||||
newId = null;
|
||||
} else if (newId) {
|
||||
// If new id doesn't match an existing org, clear it
|
||||
if (!state.session.organizations.find((o) => o.id === newId)) {
|
||||
if (!state.profiles.organizations.find((o) => o.id === newId)) {
|
||||
newId = null;
|
||||
}
|
||||
}
|
||||
|
@ -129,5 +129,5 @@ export const updateOrgsThunk = createAsyncThunk<
|
|||
dispatch(setOrganizations(orgs));
|
||||
|
||||
// This will trigger reselection if needed
|
||||
dispatch(selectOrgThunk(state.session.selectedOrganizationId));
|
||||
dispatch(selectOrgThunk(state.profiles.selectedOrganizationId));
|
||||
});
|
||||
|
|
|
@ -21,8 +21,6 @@ import {
|
|||
ToolCallDelta,
|
||||
ToolCallState,
|
||||
} from "core";
|
||||
import { ProfileDescription } from "core/config/ConfigHandler";
|
||||
import { OrganizationDescription } from "core/config/ProfileLifecycleManager";
|
||||
import { NEW_SESSION_TITLE } from "core/util/constants";
|
||||
import { incrementalParseJson } from "core/util/incrementalParseJson";
|
||||
import { renderChatMessage } from "core/util/messageContent";
|
||||
|
@ -45,11 +43,6 @@ type SessionState = {
|
|||
isStreaming: boolean;
|
||||
title: string;
|
||||
id: string;
|
||||
/** null indicates loading state */
|
||||
availableProfiles: ProfileDescription[] | null;
|
||||
selectedProfileId: string | null;
|
||||
organizations: OrganizationDescription[];
|
||||
selectedOrganizationId: string | null;
|
||||
streamAborter: AbortController;
|
||||
codeToEdit: CodeToEdit[];
|
||||
curCheckpointIndex: number;
|
||||
|
@ -88,10 +81,6 @@ const initialState: SessionState = {
|
|||
isStreaming: false,
|
||||
title: NEW_SESSION_TITLE,
|
||||
id: uuidv4(),
|
||||
selectedProfileId: null,
|
||||
availableProfiles: null,
|
||||
organizations: [],
|
||||
selectedOrganizationId: "",
|
||||
curCheckpointIndex: 0,
|
||||
streamAborter: new AbortController(),
|
||||
codeToEdit: [],
|
||||
|
@ -294,7 +283,6 @@ export const sessionSlice = createSlice({
|
|||
state.streamAborter = new AbortController();
|
||||
},
|
||||
streamUpdate: (state, action: PayloadAction<ChatMessage[]>) => {
|
||||
|
||||
if (state.history.length) {
|
||||
function toolCallDeltaToState(
|
||||
toolCallDelta: ToolCallDelta,
|
||||
|
@ -332,7 +320,7 @@ export const sessionSlice = createSlice({
|
|||
id: uuidv4(),
|
||||
},
|
||||
contextItems: [],
|
||||
})
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -554,30 +542,6 @@ export const sessionSlice = createSlice({
|
|||
|
||||
state.history[state.history.length - 1].contextItems = contextItems;
|
||||
},
|
||||
// Important: these reducers don't handle selected profile/organization fallback logic
|
||||
// That is done in thunks
|
||||
setSelectedProfile: (state, { payload }: PayloadAction<string | null>) => {
|
||||
state.selectedProfileId = payload;
|
||||
},
|
||||
setAvailableProfiles: (
|
||||
state,
|
||||
{ payload }: PayloadAction<ProfileDescription[] | null>,
|
||||
) => {
|
||||
state.availableProfiles = payload;
|
||||
},
|
||||
setOrganizations: (
|
||||
state,
|
||||
{ payload }: PayloadAction<OrganizationDescription[]>,
|
||||
) => {
|
||||
state.organizations = payload;
|
||||
},
|
||||
setSelectedOrganizationId: (
|
||||
state,
|
||||
{ payload }: PayloadAction<string | null>,
|
||||
) => {
|
||||
state.selectedOrganizationId = payload;
|
||||
},
|
||||
///////////////
|
||||
|
||||
updateCurCheckpoint: (
|
||||
state,
|
||||
|
@ -690,15 +654,6 @@ export const sessionSlice = createSlice({
|
|||
selectIsInEditMode: (state) => {
|
||||
return state.mode === "edit";
|
||||
},
|
||||
selectSelectedProfile: (state) => {
|
||||
// state.session.availableProfiles.find((p) => p.id === newId) ?? null,
|
||||
|
||||
return (
|
||||
state.availableProfiles?.find(
|
||||
(profile) => profile.id === state.selectedProfileId,
|
||||
) ?? null
|
||||
);
|
||||
},
|
||||
selectIsSingleRangeEditOrInsertion: (state) => {
|
||||
if (state.mode !== "edit") {
|
||||
return false;
|
||||
|
@ -784,17 +739,11 @@ export const {
|
|||
updateSessionMetadata,
|
||||
deleteSessionMetadata,
|
||||
setNewestCodeblocksForInput,
|
||||
|
||||
setAvailableProfiles,
|
||||
setSelectedProfile,
|
||||
setOrganizations,
|
||||
setSelectedOrganizationId,
|
||||
} = sessionSlice.actions;
|
||||
|
||||
export const {
|
||||
selectIsGatheringContext,
|
||||
selectIsInEditMode,
|
||||
selectSelectedProfile,
|
||||
selectIsSingleRangeEditOrInsertion,
|
||||
selectHasCodeToEdit,
|
||||
} = sessionSlice.selectors;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { createFilter } from "redux-persist-transform-filter";
|
|||
import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";
|
||||
import storage from "redux-persist/lib/storage";
|
||||
import { IdeMessenger, IIdeMessenger } from "../context/IdeMessenger";
|
||||
import { profilesReducer } from "./redux";
|
||||
import { profilesReducer } from "./slices";
|
||||
import configReducer from "./slices/configSlice";
|
||||
import editModeStateReducer from "./slices/editModeState";
|
||||
import indexingReducer from "./slices/indexingSlice";
|
||||
|
@ -38,8 +38,6 @@ const rootReducer = combineReducers({
|
|||
const saveSubsetFilters = [
|
||||
createFilter("session", [
|
||||
"history",
|
||||
"selectedOrganizationId",
|
||||
"selectedProfile",
|
||||
"id",
|
||||
"lastSessionId",
|
||||
"title",
|
||||
|
@ -63,6 +61,12 @@ const saveSubsetFilters = [
|
|||
createFilter("ui", ["toolSettings", "useTools"]),
|
||||
createFilter("indexing", []),
|
||||
createFilter("tabs", ["tabs"]),
|
||||
// Add this new filter for the profiles slice
|
||||
createFilter("profiles", [
|
||||
"preferencesByProfileId",
|
||||
"selectedOrganizationId",
|
||||
"selectedProfileId",
|
||||
]),
|
||||
];
|
||||
|
||||
const migrations: MigrationManifest = {
|
||||
|
|
Loading…
Reference in New Issue