mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-30 13:59:22 +07:00
🐛 fix(operation): isolate loading state to current active topic (#10360)
* fix(operation): isolate loading state to current active topic - Modified isMainWindowAgentRuntimeRunning to only check operations in current active topic - Prevents loading state from other topics affecting the send button - Added comprehensive test case to verify topic isolation - Fixes issue where switching topics would still show loading state from previous topic * test: fix isMainWindowAgentRuntimeRunning tests to set active context - Added activeId and activeTopicId setup in test cases - Ensured operation context matches active context for proper filtering - Fixed tests to align with new getCurrentContextOperations-based implementation * fix: change activeTopicId from null to undefined in tests - Fixed TypeScript type error where null is not assignable to string | undefined - Changed all activeTopicId: null to activeTopicId: undefined * fix: check if operation's message is in current displayed messages - Changed from using getCurrentContextOperations to checking message presence - Prevents loading state from showing when switching back to default topic - Operation's context topicId is captured at creation time and doesn't update - Now checks if operation's message is in activeDisplayMessages instead * refactor * refactor to fix * try to fix stylelint ci issue * fix tests * fix tests
This commit is contained in:
@@ -396,9 +396,15 @@
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
},
|
||||
"overrides": {
|
||||
"stylelint-config-clean-order": "7.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@vercel/speed-insights"
|
||||
]
|
||||
],
|
||||
"overrides": {
|
||||
"stylelint-config-clean-order": "7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +129,6 @@ export class KnowledgeRepo {
|
||||
};
|
||||
});
|
||||
|
||||
console.log('[KnowledgeRepo.query] Fetched items:', {
|
||||
count: mappedResults.length,
|
||||
documents: mappedResults.filter((item) => item.sourceType === 'document'),
|
||||
sampleEditorData: mappedResults.find((item) => item.sourceType === 'document')?.editorData,
|
||||
});
|
||||
|
||||
return mappedResults;
|
||||
}
|
||||
|
||||
@@ -308,7 +302,10 @@ export class KnowledgeRepo {
|
||||
|
||||
// Exclude custom/document and source_type='file' from Documents category
|
||||
if (category === FilesTabs.Documents) {
|
||||
whereConditions.push(sql`${documents.fileType} != ${'custom/document'}`, sql`${documents.sourceType} != ${'file'}`);
|
||||
whereConditions.push(
|
||||
sql`${documents.fileType} != ${'custom/document'}`,
|
||||
sql`${documents.sourceType} != ${'file'}`,
|
||||
);
|
||||
}
|
||||
} else if (fileTypePrefix) {
|
||||
whereConditions.push(sql`${documents.fileType} ILIKE ${`${fileTypePrefix}%`}`);
|
||||
@@ -338,7 +335,7 @@ export class KnowledgeRepo {
|
||||
// Documents don't have knowledge base association currently, so skip if knowledgeBaseId is set
|
||||
if (knowledgeBaseId) {
|
||||
return sql`
|
||||
SELECT
|
||||
SELECT
|
||||
NULL::varchar(30) as id,
|
||||
NULL::text as name,
|
||||
NULL::varchar(255) as file_type,
|
||||
|
||||
@@ -51,73 +51,5 @@ export interface SearchEngineKeyVaults {
|
||||
}
|
||||
|
||||
export interface UserKeyVaults extends SearchEngineKeyVaults {
|
||||
ai21?: OpenAICompatibleKeyVault;
|
||||
ai302?: OpenAICompatibleKeyVault;
|
||||
ai360?: OpenAICompatibleKeyVault;
|
||||
aihubmix?: OpenAICompatibleKeyVault;
|
||||
akashchat?: OpenAICompatibleKeyVault;
|
||||
anthropic?: OpenAICompatibleKeyVault;
|
||||
azure?: AzureOpenAIKeyVault;
|
||||
azureai?: AzureOpenAIKeyVault;
|
||||
baichuan?: OpenAICompatibleKeyVault;
|
||||
bedrock?: AWSBedrockKeyVault;
|
||||
bfl?: any;
|
||||
cerebras?: OpenAICompatibleKeyVault;
|
||||
cloudflare?: CloudflareKeyVault;
|
||||
cohere?: OpenAICompatibleKeyVault;
|
||||
cometapi?: OpenAICompatibleKeyVault;
|
||||
comfyui?: ComfyUIKeyVault;
|
||||
deepseek?: OpenAICompatibleKeyVault;
|
||||
fal?: FalKeyVault;
|
||||
fireworksai?: OpenAICompatibleKeyVault;
|
||||
giteeai?: OpenAICompatibleKeyVault;
|
||||
github?: OpenAICompatibleKeyVault;
|
||||
google?: OpenAICompatibleKeyVault;
|
||||
groq?: OpenAICompatibleKeyVault;
|
||||
higress?: OpenAICompatibleKeyVault;
|
||||
huggingface?: OpenAICompatibleKeyVault;
|
||||
hunyuan?: OpenAICompatibleKeyVault;
|
||||
infiniai?: OpenAICompatibleKeyVault;
|
||||
internlm?: OpenAICompatibleKeyVault;
|
||||
jina?: OpenAICompatibleKeyVault;
|
||||
lmstudio?: OpenAICompatibleKeyVault;
|
||||
lobehub?: any;
|
||||
minimax?: OpenAICompatibleKeyVault;
|
||||
mistral?: OpenAICompatibleKeyVault;
|
||||
modelscope?: OpenAICompatibleKeyVault;
|
||||
moonshot?: OpenAICompatibleKeyVault;
|
||||
nebius?: OpenAICompatibleKeyVault;
|
||||
newapi?: OpenAICompatibleKeyVault;
|
||||
novita?: OpenAICompatibleKeyVault;
|
||||
nvidia?: OpenAICompatibleKeyVault;
|
||||
ollama?: OpenAICompatibleKeyVault;
|
||||
ollamacloud?: OpenAICompatibleKeyVault;
|
||||
openai?: OpenAICompatibleKeyVault;
|
||||
openrouter?: OpenAICompatibleKeyVault;
|
||||
password?: string;
|
||||
perplexity?: OpenAICompatibleKeyVault;
|
||||
ppio?: OpenAICompatibleKeyVault;
|
||||
qiniu?: OpenAICompatibleKeyVault;
|
||||
qwen?: OpenAICompatibleKeyVault;
|
||||
sambanova?: OpenAICompatibleKeyVault;
|
||||
search1api?: OpenAICompatibleKeyVault;
|
||||
sensenova?: OpenAICompatibleKeyVault;
|
||||
siliconcloud?: OpenAICompatibleKeyVault;
|
||||
spark?: OpenAICompatibleKeyVault;
|
||||
stepfun?: OpenAICompatibleKeyVault;
|
||||
taichu?: OpenAICompatibleKeyVault;
|
||||
tencentcloud?: OpenAICompatibleKeyVault;
|
||||
togetherai?: OpenAICompatibleKeyVault;
|
||||
upstage?: OpenAICompatibleKeyVault;
|
||||
v0?: OpenAICompatibleKeyVault;
|
||||
vercelaigateway?: OpenAICompatibleKeyVault;
|
||||
vertexai?: VertexAIKeyVault;
|
||||
vllm?: OpenAICompatibleKeyVault;
|
||||
volcengine?: OpenAICompatibleKeyVault;
|
||||
wenxin?: OpenAICompatibleKeyVault;
|
||||
xai?: OpenAICompatibleKeyVault;
|
||||
xinference?: OpenAICompatibleKeyVault;
|
||||
zenmux?: OpenAICompatibleKeyVault;
|
||||
zeroone?: OpenAICompatibleKeyVault;
|
||||
zhipu?: OpenAICompatibleKeyVault;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Button, InputPassword } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { keyVaultsConfigSelectors } from '@/store/user/selectors';
|
||||
|
||||
import { FormAction } from './style';
|
||||
|
||||
interface AccessCodeFormProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const AccessCodeForm = memo<AccessCodeFormProps>(({ id }) => {
|
||||
const { t } = useTranslation('error');
|
||||
const [password, updateKeyVaults] = useUserStore((s) => [
|
||||
keyVaultsConfigSelectors.password(s),
|
||||
s.updateKeyVaults,
|
||||
]);
|
||||
const [resend, deleteMessage] = useChatStore((s) => [s.delAndRegenerateMessage, s.deleteMessage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormAction
|
||||
avatar={'🗳'}
|
||||
description={t('unlock.password.description')}
|
||||
title={t('unlock.password.title')}
|
||||
>
|
||||
<InputPassword
|
||||
autoComplete={'new-password'}
|
||||
onChange={(e) => {
|
||||
updateKeyVaults({ password: e.target.value });
|
||||
}}
|
||||
placeholder={t('unlock.password.placeholder')}
|
||||
value={password}
|
||||
variant={'filled'}
|
||||
/>
|
||||
</FormAction>
|
||||
<Flexbox gap={12}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resend(id);
|
||||
deleteMessage(id);
|
||||
}}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('unlock.confirm')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteMessage(id);
|
||||
}}
|
||||
>
|
||||
{t('unlock.closeMessage')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default AccessCodeForm;
|
||||
@@ -23,10 +23,7 @@ const mockTogetherAIAPIKey = 'togetherai-api-key';
|
||||
// mock the traditional zustand
|
||||
vi.mock('zustand/traditional');
|
||||
|
||||
const setModelProviderConfig = <T extends GlobalLLMProviderKey>(
|
||||
provider: T,
|
||||
config: Partial<UserKeyVaults[T]>,
|
||||
) => {
|
||||
const setModelProviderConfig = (provider: string, config: any) => {
|
||||
useUserStore.setState({
|
||||
settings: { keyVaults: { [provider]: config } },
|
||||
});
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LOBE_URL_IMPORT_NAME } from '@/const/url';
|
||||
import { UserSettings } from '@/types/user/settings';
|
||||
|
||||
import { shareService } from '../share';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/utils/parseMarkdown', () => ({
|
||||
parseMarkdown: vi.fn(),
|
||||
}));
|
||||
|
||||
global.fetch = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe('ShareGPTService', () => {
|
||||
describe('ShareViaUrl', () => {
|
||||
describe('createShareSettingsUrl', () => {
|
||||
it('should create a share settings URL with the provided settings', () => {
|
||||
const settings: PartialDeep<UserSettings> = {
|
||||
keyVaults: {
|
||||
openai: {
|
||||
apiKey: 'user-key',
|
||||
},
|
||||
},
|
||||
};
|
||||
const url = shareService.createShareSettingsUrl(settings);
|
||||
expect(url).toBe(
|
||||
`/?${LOBE_URL_IMPORT_NAME}=%7B%22keyVaults%22:%7B%22openai%22:%7B%22apiKey%22:%22user-key%22%7D%7D%7D`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeShareSettings', () => {
|
||||
it('should decode share settings from search params', () => {
|
||||
const settings = '{"languageModel":{"openai":{"apiKey":"user-key"}}}';
|
||||
const decodedSettings = shareService.decodeShareSettings(settings);
|
||||
expect(decodedSettings).toEqual({
|
||||
data: {
|
||||
languageModel: {
|
||||
openai: {
|
||||
apiKey: 'user-key',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error message if decoding fails', () => {
|
||||
const settings = '%7B%22theme%22%3A%22dark%22%2C%22fontSize%22%3A16%';
|
||||
const decodedSettings = shareService.decodeShareSettings(settings);
|
||||
expect(decodedSettings).toEqual({
|
||||
message: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import { ModelProvider } from 'model-bank';
|
||||
|
||||
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { keyVaultsConfigSelectors, userProfileSelectors } from '@/store/user/selectors';
|
||||
import { userProfileSelectors } from '@/store/user/selectors';
|
||||
import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
|
||||
|
||||
import { resolveRuntimeProvider } from './chat/helper';
|
||||
@@ -105,10 +105,9 @@ export const getProviderAuthPayload = (
|
||||
};
|
||||
|
||||
const createAuthTokenWithPayload = (payload = {}) => {
|
||||
const accessCode = keyVaultsConfigSelectors.password(useUserStore.getState());
|
||||
const userId = userProfileSelectors.userId(useUserStore.getState());
|
||||
|
||||
return obfuscatePayloadWithXOR<ClientSecretPayload>({ accessCode, userId, ...payload });
|
||||
return obfuscatePayloadWithXOR<ClientSecretPayload>({ userId, ...payload });
|
||||
};
|
||||
|
||||
interface AuthParams {
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
LOBE_CHAT_ACCESS_CODE,
|
||||
LOBE_USER_ID,
|
||||
OPENAI_API_KEY_HEADER_KEY,
|
||||
OPENAI_END_POINT,
|
||||
} from '@/const/fetch';
|
||||
import { LOBE_USER_ID, OPENAI_API_KEY_HEADER_KEY, OPENAI_END_POINT } from '@/const/fetch';
|
||||
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { keyVaultsConfigSelectors } from '@/store/user/selectors';
|
||||
|
||||
/**
|
||||
* TODO: Need to be removed after tts refactor
|
||||
@@ -22,7 +16,6 @@ export const createHeaderWithOpenAI = (header?: HeadersInit): HeadersInit => {
|
||||
// eslint-disable-next-line no-undef
|
||||
return {
|
||||
...header,
|
||||
[LOBE_CHAT_ACCESS_CODE]: keyVaultsConfigSelectors.password(state),
|
||||
[LOBE_USER_ID]: state.user?.id || '',
|
||||
[OPENAI_API_KEY_HEADER_KEY]: keyVaults.apiKey || '',
|
||||
[OPENAI_END_POINT]: keyVaults.baseURL || '',
|
||||
|
||||
@@ -58,6 +58,9 @@ describe('call_llm executor', () => {
|
||||
sessionId: 'test-session',
|
||||
topicId: 'test-topic',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -229,6 +232,9 @@ describe('call_llm executor', () => {
|
||||
expect.objectContaining({
|
||||
parentId: 'msg_payload_parent',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -262,6 +268,9 @@ describe('call_llm executor', () => {
|
||||
expect.objectContaining({
|
||||
parentId: 'msg_context_parent',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1061,6 +1070,9 @@ describe('call_llm executor', () => {
|
||||
model: 'claude-3-opus',
|
||||
provider: 'anthropic',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(mockStore.internal_fetchAIChatMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -1180,6 +1192,9 @@ describe('call_llm executor', () => {
|
||||
expect.objectContaining({
|
||||
threadId,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1211,6 +1226,9 @@ describe('call_llm executor', () => {
|
||||
expect.objectContaining({
|
||||
threadId: undefined,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,17 +156,22 @@ describe('call_tool executor', () => {
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith({
|
||||
content: '',
|
||||
groupId: 'group_789',
|
||||
parentId: 'msg_parent_123',
|
||||
plugin: toolCall,
|
||||
role: 'tool',
|
||||
sessionId: 'sess_123',
|
||||
threadId: undefined,
|
||||
tool_call_id: 'tool_call_xyz',
|
||||
topicId: 'topic_456',
|
||||
});
|
||||
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: '',
|
||||
groupId: 'group_789',
|
||||
parentId: 'msg_parent_123',
|
||||
plugin: toolCall,
|
||||
role: 'tool',
|
||||
sessionId: 'sess_123',
|
||||
threadId: undefined,
|
||||
tool_call_id: 'tool_call_xyz',
|
||||
topicId: 'topic_456',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use assistant message groupId for tool message', async () => {
|
||||
@@ -194,6 +199,9 @@ describe('call_tool executor', () => {
|
||||
expect.objectContaining({
|
||||
groupId: 'group_special',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -222,6 +230,9 @@ describe('call_tool executor', () => {
|
||||
expect.objectContaining({
|
||||
parentId: 'msg_custom_parent',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -262,6 +273,9 @@ describe('call_tool executor', () => {
|
||||
expect.objectContaining({
|
||||
plugin: toolCall,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1542,6 +1556,9 @@ describe('call_tool executor', () => {
|
||||
expect.objectContaining({
|
||||
groupId: undefined,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(result.events).toHaveLength(1);
|
||||
});
|
||||
@@ -1570,6 +1587,9 @@ describe('call_tool executor', () => {
|
||||
expect.objectContaining({
|
||||
groupId: undefined,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1641,6 +1661,9 @@ describe('call_tool executor', () => {
|
||||
expect.objectContaining({
|
||||
topicId: undefined,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1679,6 +1702,9 @@ describe('call_tool executor', () => {
|
||||
type: 'builtin',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(result.events).toHaveLength(1);
|
||||
});
|
||||
@@ -1768,6 +1794,9 @@ describe('call_tool executor', () => {
|
||||
expect.objectContaining({
|
||||
groupId: 'group_latest',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,9 @@ export const expectMessageCreated = (mockStore: ChatStore, role: 'assistant' | '
|
||||
expect.objectContaining({
|
||||
role,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ describe('request_human_approve executor', () => {
|
||||
parentId: 'msg_assistant',
|
||||
groupId: assistantMessage.groupId,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -205,6 +208,9 @@ describe('request_human_approve executor', () => {
|
||||
expect.objectContaining({
|
||||
groupId: 'group_123',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -232,6 +238,9 @@ describe('request_human_approve executor', () => {
|
||||
expect.objectContaining({
|
||||
parentId: 'msg_assistant_456',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -330,6 +339,9 @@ describe('request_human_approve executor', () => {
|
||||
tool_call_id: toolCall.id,
|
||||
pluginIntervention: { status: 'pending' },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -539,6 +551,9 @@ describe('request_human_approve executor', () => {
|
||||
expect.objectContaining({
|
||||
parentId: 'msg_3_last',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,6 +57,9 @@ describe('resolve_aborted_tools executor', () => {
|
||||
topicId: 'test-topic',
|
||||
parentId: parentMessage.id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -114,6 +117,9 @@ describe('resolve_aborted_tools executor', () => {
|
||||
pluginIntervention: { status: 'aborted' },
|
||||
tool_call_id: toolCall.id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -193,17 +199,22 @@ describe('resolve_aborted_tools executor', () => {
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith({
|
||||
role: 'tool',
|
||||
content: 'Tool execution was aborted by user.',
|
||||
plugin: toolCall,
|
||||
pluginIntervention: { status: 'aborted' },
|
||||
tool_call_id: 'tool_abc',
|
||||
parentId: 'msg_parent',
|
||||
sessionId: 'sess_123',
|
||||
topicId: 'topic_456',
|
||||
threadId: undefined,
|
||||
});
|
||||
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
role: 'tool',
|
||||
content: 'Tool execution was aborted by user.',
|
||||
plugin: toolCall,
|
||||
pluginIntervention: { status: 'aborted' },
|
||||
tool_call_id: 'tool_abc',
|
||||
parentId: 'msg_parent',
|
||||
sessionId: 'sess_123',
|
||||
topicId: 'topic_456',
|
||||
threadId: undefined,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve tool payload details', async () => {
|
||||
@@ -240,6 +251,9 @@ describe('resolve_aborted_tools executor', () => {
|
||||
expect.objectContaining({
|
||||
plugin: toolCall,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -264,6 +278,9 @@ describe('resolve_aborted_tools executor', () => {
|
||||
expect.objectContaining({
|
||||
topicId: undefined,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -481,6 +498,9 @@ describe('resolve_aborted_tools executor', () => {
|
||||
expect.objectContaining({
|
||||
plugin: toolCall,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -571,6 +591,9 @@ describe('resolve_aborted_tools executor', () => {
|
||||
type: 'builtin',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -606,6 +629,9 @@ describe('resolve_aborted_tools executor', () => {
|
||||
type: 'default',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operationId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,16 +89,19 @@ export const createAgentExecutors = (context: {
|
||||
llmPayload.parentMessageId = context.parentId;
|
||||
}
|
||||
// Create assistant message (following server-side pattern)
|
||||
const assistantMessageItem = await context.get().optimisticCreateMessage({
|
||||
content: LOADING_FLAT,
|
||||
model: llmPayload.model,
|
||||
parentId: llmPayload.parentMessageId,
|
||||
provider: llmPayload.provider,
|
||||
role: 'assistant',
|
||||
sessionId: opContext.sessionId!,
|
||||
threadId: opContext.threadId,
|
||||
topicId: opContext.topicId ?? undefined,
|
||||
});
|
||||
const assistantMessageItem = await context.get().optimisticCreateMessage(
|
||||
{
|
||||
content: LOADING_FLAT,
|
||||
model: llmPayload.model,
|
||||
parentId: llmPayload.parentMessageId,
|
||||
provider: llmPayload.provider,
|
||||
role: 'assistant',
|
||||
sessionId: opContext.sessionId!,
|
||||
threadId: opContext.threadId,
|
||||
topicId: opContext.topicId ?? undefined,
|
||||
},
|
||||
{ operationId: context.operationId },
|
||||
);
|
||||
|
||||
if (!assistantMessageItem) {
|
||||
throw new Error('Failed to create assistant message');
|
||||
@@ -371,7 +374,9 @@ export const createAgentExecutors = (context: {
|
||||
topicId: opContext.topicId ?? undefined,
|
||||
};
|
||||
|
||||
const createPromise = context.get().optimisticCreateMessage(toolMessageParams);
|
||||
const createPromise = context
|
||||
.get()
|
||||
.optimisticCreateMessage(toolMessageParams, { operationId: createToolMsgOpId });
|
||||
context.get().updateOperationMetadata(createToolMsgOpId, {
|
||||
createMessagePromise: createPromise,
|
||||
});
|
||||
@@ -632,7 +637,9 @@ export const createAgentExecutors = (context: {
|
||||
topicId: opContext.topicId ?? undefined,
|
||||
};
|
||||
|
||||
const createResult = await context.get().optimisticCreateMessage(toolMessageParams);
|
||||
const createResult = await context
|
||||
.get()
|
||||
.optimisticCreateMessage(toolMessageParams, { operationId: context.operationId });
|
||||
|
||||
if (!createResult) {
|
||||
log(
|
||||
@@ -709,7 +716,9 @@ export const createAgentExecutors = (context: {
|
||||
topicId: opContext.topicId ?? undefined,
|
||||
};
|
||||
|
||||
const createResult = await context.get().optimisticCreateMessage(toolMessageParams);
|
||||
const createResult = await context
|
||||
.get()
|
||||
.optimisticCreateMessage(toolMessageParams, { operationId: context.operationId });
|
||||
|
||||
if (createResult) {
|
||||
log(
|
||||
|
||||
@@ -261,6 +261,10 @@ export const conversationLifecycle: StateCreator<
|
||||
|
||||
summaryTitle().catch(console.error);
|
||||
|
||||
// Complete sendMessage operation here - message creation is done
|
||||
// execAgentRuntime is a separate operation (child) that handles AI response generation
|
||||
get().completeOperation(operationId);
|
||||
|
||||
// Get the current messages to generate AI response
|
||||
const displayMessages = displayMessageSelectors.activeDisplayMessages(get());
|
||||
|
||||
@@ -287,16 +291,8 @@ export const conversationLifecycle: StateCreator<
|
||||
if (userFiles.length > 0) {
|
||||
await getAgentStoreState().addFilesToAgent(userFiles, false);
|
||||
}
|
||||
|
||||
// Complete operation on success
|
||||
get().completeOperation(operationId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Fail operation on error
|
||||
get().failOperation(operationId, {
|
||||
type: e instanceof Error ? e.name : 'unknown_error',
|
||||
message: e instanceof Error ? e.message : 'AI generation failed',
|
||||
});
|
||||
} finally {
|
||||
if (data.topicId) get().internal_updateTopicLoading(data.topicId, false);
|
||||
}
|
||||
|
||||
@@ -241,9 +241,18 @@ describe('search actions', () => {
|
||||
it('should update arguments and perform search', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const spy = vi.spyOn(result.current, 'search');
|
||||
const { triggerSearchAgain } = result.current;
|
||||
|
||||
const messageId = 'test-message-id';
|
||||
const operationId = 'op_test';
|
||||
|
||||
// Set up messageOperationMap so triggerSearchAgain can get operationId
|
||||
useChatStore.setState({
|
||||
messageOperationMap: {
|
||||
[messageId]: operationId,
|
||||
},
|
||||
});
|
||||
|
||||
const { triggerSearchAgain } = result.current;
|
||||
const query: SearchQuery = {
|
||||
query: 'test query',
|
||||
};
|
||||
@@ -252,7 +261,12 @@ describe('search actions', () => {
|
||||
await triggerSearchAgain(messageId, query, { aiSummary: true });
|
||||
});
|
||||
|
||||
expect(result.current.optimisticUpdatePluginArguments).toHaveBeenCalledWith(messageId, query);
|
||||
expect(result.current.optimisticUpdatePluginArguments).toHaveBeenCalledWith(
|
||||
messageId,
|
||||
query,
|
||||
false,
|
||||
{ operationId },
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(messageId, query, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,11 @@ export const localSystemSlice: StateCreator<
|
||||
},
|
||||
|
||||
reSearchLocalFiles: async (id, params) => {
|
||||
await get().optimisticUpdatePluginArguments(id, params);
|
||||
// Get operationId from messageOperationMap to ensure proper context isolation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
await get().optimisticUpdatePluginArguments(id, params, false, context);
|
||||
|
||||
return get().searchLocalFiles(id, params);
|
||||
},
|
||||
|
||||
@@ -276,7 +276,11 @@ export const searchSlice: StateCreator<
|
||||
},
|
||||
|
||||
triggerSearchAgain: async (id, data, options) => {
|
||||
await get().optimisticUpdatePluginArguments(id, data);
|
||||
// Get operationId from messageOperationMap to ensure proper context isolation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
await get().optimisticUpdatePluginArguments(id, data, false, context);
|
||||
|
||||
await get().search(id, data, options?.aiSummary);
|
||||
},
|
||||
|
||||
@@ -167,14 +167,22 @@ export const messagePublicApi: StateCreator<
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
||||
if (!message || message.role !== 'tool') return;
|
||||
|
||||
// Get operationId from messageOperationMap to ensure proper context isolation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
const removeToolInAssistantMessage = async () => {
|
||||
if (!message.parentId) return;
|
||||
await get().optimisticRemoveToolFromAssistantMessage(message.parentId, message.tool_call_id);
|
||||
await get().optimisticRemoveToolFromAssistantMessage(
|
||||
message.parentId,
|
||||
message.tool_call_id,
|
||||
context,
|
||||
);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
// 1. remove tool message
|
||||
get().optimisticDeleteMessage(id),
|
||||
get().optimisticDeleteMessage(id, context),
|
||||
// 2. remove the tool item in the assistant tools
|
||||
removeToolInAssistantMessage(),
|
||||
]);
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface MessageQueryAction {
|
||||
messages: UIChatMessage[],
|
||||
params?: {
|
||||
action?: any;
|
||||
operationId?: string;
|
||||
sessionId?: string;
|
||||
topicId?: string | null;
|
||||
},
|
||||
@@ -66,10 +67,22 @@ export const messageQuery: StateCreator<
|
||||
},
|
||||
|
||||
replaceMessages: (messages, params) => {
|
||||
const messagesKey = messageMapKey(
|
||||
params?.sessionId ?? get().activeId,
|
||||
params?.topicId ?? get().activeTopicId,
|
||||
);
|
||||
let sessionId: string;
|
||||
let topicId: string | null | undefined;
|
||||
|
||||
// Priority 1: Get context from operation if operationId is provided
|
||||
if (params?.operationId) {
|
||||
const { sessionId: opSessionId, topicId: opTopicId } =
|
||||
get().internal_getSessionContext(params);
|
||||
sessionId = opSessionId;
|
||||
topicId = opTopicId;
|
||||
} else {
|
||||
// Priority 2: Use explicit sessionId/topicId or fallback to global state
|
||||
sessionId = params?.sessionId ?? get().activeId;
|
||||
topicId = params?.topicId ?? get().activeTopicId;
|
||||
}
|
||||
|
||||
const messagesKey = messageMapKey(sessionId, topicId);
|
||||
|
||||
// Get raw messages from dbMessagesMap and apply reducer
|
||||
const nextDbMap = { ...get().dbMessagesMap, [messagesKey]: messages };
|
||||
|
||||
@@ -393,6 +393,11 @@ describe('Operation Selectors', () => {
|
||||
it('isMainWindowAgentRuntimeRunning should only detect main window operations', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Set active context
|
||||
act(() => {
|
||||
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
||||
});
|
||||
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
||||
|
||||
// Start a main window operation (inThread: false)
|
||||
@@ -400,7 +405,7 @@ describe('Operation Selectors', () => {
|
||||
act(() => {
|
||||
mainOpId = result.current.startOperation({
|
||||
type: 'execAgentRuntime',
|
||||
context: { sessionId: 'session1' },
|
||||
context: { sessionId: 'session1', topicId: null },
|
||||
metadata: { inThread: false },
|
||||
}).operationId;
|
||||
});
|
||||
@@ -420,12 +425,17 @@ describe('Operation Selectors', () => {
|
||||
it('isMainWindowAgentRuntimeRunning should exclude thread operations', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Set active context
|
||||
act(() => {
|
||||
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
||||
});
|
||||
|
||||
// Start a thread operation (inThread: true)
|
||||
let threadOpId: string;
|
||||
act(() => {
|
||||
threadOpId = result.current.startOperation({
|
||||
type: 'execAgentRuntime',
|
||||
context: { sessionId: 'session1', threadId: 'thread1' },
|
||||
context: { sessionId: 'session1', topicId: null, threadId: 'thread1' },
|
||||
metadata: { inThread: true },
|
||||
}).operationId;
|
||||
});
|
||||
@@ -447,6 +457,11 @@ describe('Operation Selectors', () => {
|
||||
it('isMainWindowAgentRuntimeRunning should distinguish between main and thread operations', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Set active context
|
||||
act(() => {
|
||||
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
||||
});
|
||||
|
||||
let mainOpId: string;
|
||||
let threadOpId: string;
|
||||
|
||||
@@ -454,13 +469,13 @@ describe('Operation Selectors', () => {
|
||||
act(() => {
|
||||
mainOpId = result.current.startOperation({
|
||||
type: 'execAgentRuntime',
|
||||
context: { sessionId: 'session1' },
|
||||
context: { sessionId: 'session1', topicId: null },
|
||||
metadata: { inThread: false },
|
||||
}).operationId;
|
||||
|
||||
threadOpId = result.current.startOperation({
|
||||
type: 'execAgentRuntime',
|
||||
context: { sessionId: 'session1', threadId: 'thread1' },
|
||||
context: { sessionId: 'session1', topicId: null, threadId: 'thread1' },
|
||||
metadata: { inThread: true },
|
||||
}).operationId;
|
||||
});
|
||||
@@ -489,11 +504,16 @@ describe('Operation Selectors', () => {
|
||||
it('isMainWindowAgentRuntimeRunning should exclude aborting operations', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Set active context
|
||||
act(() => {
|
||||
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
||||
});
|
||||
|
||||
let opId: string;
|
||||
act(() => {
|
||||
opId = result.current.startOperation({
|
||||
type: 'execAgentRuntime',
|
||||
context: { sessionId: 'session1' },
|
||||
context: { sessionId: 'session1', topicId: null },
|
||||
metadata: { inThread: false },
|
||||
}).operationId;
|
||||
});
|
||||
@@ -509,5 +529,73 @@ describe('Operation Selectors', () => {
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
||||
expect(operationSelectors.isAgentRuntimeRunning(result.current)).toBe(false);
|
||||
});
|
||||
|
||||
it('isMainWindowAgentRuntimeRunning should only detect operations in current active topic', () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Set active session and topic
|
||||
act(() => {
|
||||
useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
|
||||
});
|
||||
|
||||
let topic1OpId: string;
|
||||
let topic2OpId: string;
|
||||
|
||||
// Start operation in topic1 (current active topic)
|
||||
act(() => {
|
||||
topic1OpId = result.current.startOperation({
|
||||
type: 'execAgentRuntime',
|
||||
context: { sessionId: 'session1', topicId: 'topic1' },
|
||||
metadata: { inThread: false },
|
||||
}).operationId;
|
||||
});
|
||||
|
||||
// Should detect operation in current topic
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
||||
|
||||
// Start operation in topic2 (different topic)
|
||||
act(() => {
|
||||
topic2OpId = result.current.startOperation({
|
||||
type: 'execAgentRuntime',
|
||||
context: { sessionId: 'session1', topicId: 'topic2' },
|
||||
metadata: { inThread: false },
|
||||
}).operationId;
|
||||
});
|
||||
|
||||
// Should still only detect topic1 operation (current active topic)
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
||||
|
||||
// Switch to topic2
|
||||
act(() => {
|
||||
useChatStore.setState({ activeTopicId: 'topic2' });
|
||||
});
|
||||
|
||||
// Should now detect topic2 operation
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
||||
|
||||
// Complete topic2 operation
|
||||
act(() => {
|
||||
result.current.completeOperation(topic2OpId!);
|
||||
});
|
||||
|
||||
// Should not detect any operation in topic2 now
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
||||
|
||||
// Switch back to topic1
|
||||
act(() => {
|
||||
useChatStore.setState({ activeTopicId: 'topic1' });
|
||||
});
|
||||
|
||||
// Should detect topic1 operation again
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
||||
|
||||
// Complete topic1 operation
|
||||
act(() => {
|
||||
result.current.completeOperation(topic1OpId!);
|
||||
});
|
||||
|
||||
// Should not detect any operation now
|
||||
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,14 +180,27 @@ const isAgentRuntimeRunning = (s: ChatStoreState): boolean => {
|
||||
/**
|
||||
* Check if agent runtime is running in main window only
|
||||
* Used for main window UI state (e.g., send button loading)
|
||||
* Excludes thread operations to prevent cross-contamination
|
||||
* Excludes thread operations and operations from other topics to prevent cross-contamination
|
||||
*/
|
||||
const isMainWindowAgentRuntimeRunning = (s: ChatStoreState): boolean => {
|
||||
const operationIds = s.operationsByType['execAgentRuntime'] || [];
|
||||
|
||||
return operationIds.some((id) => {
|
||||
const op = s.operations[id];
|
||||
// Only include main window operations (not thread)
|
||||
return op && op.status === 'running' && !op.metadata.isAborting && !op.metadata.inThread;
|
||||
if (!op || op.status !== 'running' || op.metadata.isAborting || op.metadata.inThread) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Session must match
|
||||
if (s.activeId !== op.context.sessionId) return false;
|
||||
|
||||
// Topic comparison: normalize null/undefined (both mean "default topic")
|
||||
// activeTopicId can be null (initial state) or undefined (after topic operations)
|
||||
// Operation context topicId can also be null or undefined
|
||||
const activeTopicId = s.activeTopicId ?? null;
|
||||
const opTopicId = op.context.topicId ?? null;
|
||||
|
||||
return activeTopicId === opTopicId;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface PluginOptimisticUpdateAction {
|
||||
id: string,
|
||||
value: T,
|
||||
replace?: boolean,
|
||||
context?: OptimisticUpdateContext,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
@@ -106,7 +107,7 @@ export const pluginOptimisticUpdate: StateCreator<
|
||||
}
|
||||
},
|
||||
|
||||
optimisticUpdatePluginArguments: async (id, value, replace = false) => {
|
||||
optimisticUpdatePluginArguments: async (id, value, replace = false, context) => {
|
||||
const { refreshMessages } = get();
|
||||
const toolMessage = displayMessageSelectors.getDisplayMessageById(id)(get());
|
||||
if (!toolMessage || !toolMessage?.tool_call_id) return;
|
||||
@@ -121,20 +122,22 @@ export const pluginOptimisticUpdate: StateCreator<
|
||||
if (isEqual(prevJson, nextValue)) return;
|
||||
|
||||
// optimistic update
|
||||
get().internal_dispatchMessage({
|
||||
id,
|
||||
type: 'updateMessagePlugin',
|
||||
value: { arguments: JSON.stringify(nextValue) },
|
||||
});
|
||||
get().internal_dispatchMessage(
|
||||
{ id, type: 'updateMessagePlugin', value: { arguments: JSON.stringify(nextValue) } },
|
||||
context,
|
||||
);
|
||||
|
||||
// 同样需要更新 assistantMessage 的 pluginArguments
|
||||
if (assistantMessage) {
|
||||
get().internal_dispatchMessage({
|
||||
id: assistantMessage.id,
|
||||
type: 'updateMessageTools',
|
||||
tool_call_id: toolMessage?.tool_call_id,
|
||||
value: { arguments: JSON.stringify(nextValue) },
|
||||
});
|
||||
get().internal_dispatchMessage(
|
||||
{
|
||||
id: assistantMessage.id,
|
||||
type: 'updateMessageTools',
|
||||
tool_call_id: toolMessage?.tool_call_id,
|
||||
value: { arguments: JSON.stringify(nextValue) },
|
||||
},
|
||||
context,
|
||||
);
|
||||
assistantMessage = displayMessageSelectors.getDisplayMessageById(assistantMessage?.id)(get());
|
||||
}
|
||||
|
||||
@@ -183,11 +186,14 @@ export const pluginOptimisticUpdate: StateCreator<
|
||||
if (!assistantMessage) return;
|
||||
|
||||
const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get();
|
||||
internal_dispatchMessage({
|
||||
type: 'addMessageTool',
|
||||
value: tool,
|
||||
id: assistantMessage.id,
|
||||
});
|
||||
internal_dispatchMessage(
|
||||
{
|
||||
type: 'addMessageTool',
|
||||
value: tool,
|
||||
id: assistantMessage.id,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
await internal_refreshToUpdateMessageTools(id, context);
|
||||
},
|
||||
@@ -199,7 +205,7 @@ export const pluginOptimisticUpdate: StateCreator<
|
||||
const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get();
|
||||
|
||||
// optimistic update
|
||||
internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id });
|
||||
internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id }, context);
|
||||
|
||||
// update the message tools
|
||||
await internal_refreshToUpdateMessageTools(id, context);
|
||||
|
||||
@@ -10,12 +10,7 @@ const getVaultByProvider = (provider: string) => (s: UserStore) =>
|
||||
// @ts-ignore
|
||||
(keyVaultsSettings(s)[provider] || {}) as any;
|
||||
|
||||
const password = (s: UserStore) => keyVaultsSettings(s).password || '';
|
||||
|
||||
export const keyVaultsConfigSelectors = {
|
||||
getVaultByProvider,
|
||||
|
||||
keyVaultsSettings,
|
||||
|
||||
password,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user