Compare commits

...

1 Commits

Author SHA1 Message Date
Innei 518af5cbc6 init
Signed-off-by: Innei <tukon479@gmail.com>
2026-02-13 23:57:29 +08:00
29 changed files with 688 additions and 349 deletions
+3 -2
View File
@@ -1,11 +1,11 @@
/**
* Application settings storage related constants
*/
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { type NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { appStorageDir } from '@/const/dir';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import { ElectronMainStore } from '@/types/store';
import { type ElectronMainStore } from '@/types/store';
/**
* Storage name
@@ -32,4 +32,5 @@ export const STORE_DEFAULTS: ElectronMainStore = {
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
storagePath: appStorageDir,
themeMode: 'system',
workingDirectories: {},
};
+129 -2
View File
@@ -1,3 +1,5 @@
import { exec } from 'node:child_process';
import { join } from 'node:path';
import process from 'node:process';
import type { ElectronAppState, ThemeMode } from '@lobechat/electron-client-ipc';
@@ -21,6 +23,31 @@ import { ControllerModule, IpcMethod } from './index';
const logger = createLogger('controllers:SystemCtr');
interface KnownApp {
bundleName: string;
id: string;
name: string;
}
export interface InstalledApp {
icon: string;
id: string;
name: string;
}
const KNOWN_APPS: KnownApp[] = [
{ bundleName: 'Cursor.app', id: 'cursor', name: 'Cursor' },
{ bundleName: 'Visual Studio Code.app', id: 'vscode', name: 'VS Code' },
{ bundleName: 'Zed.app', id: 'zed', name: 'Zed' },
{ bundleName: 'Xcode.app', id: 'xcode', name: 'Xcode' },
{ bundleName: 'WebStorm.app', id: 'webstorm', name: 'WebStorm' },
{ bundleName: 'IntelliJ IDEA.app', id: 'idea', name: 'IntelliJ IDEA' },
{ bundleName: 'Ghostty.app', id: 'ghostty', name: 'Ghostty' },
{ bundleName: 'iTerm.app', id: 'iterm', name: 'iTerm' },
{ bundleName: 'Fork.app', id: 'fork', name: 'Fork' },
{ bundleName: 'Trae.app', id: 'trae', name: 'Trae' },
];
export default class SystemController extends ControllerModule {
static override readonly groupName = 'system';
private systemThemeListenerInitialized = false;
@@ -123,8 +150,8 @@ export default class SystemController extends ControllerModule {
buttons: [openSettingsButtonText, skipButtonText],
cancelId: 1,
defaultId: 0,
message: message,
title: title,
message,
title,
type: 'info',
});
@@ -230,6 +257,106 @@ export default class SystemController extends ControllerModule {
}
}
@IpcMethod()
getWorkingDirectories(): Record<string, string> {
return this.app.storeManager.get('workingDirectories', {});
}
@IpcMethod()
setWorkingDirectory(payload: { key: string; path: string }): void {
const current = this.app.storeManager.get('workingDirectories', {});
this.app.storeManager.set('workingDirectories', { ...current, [payload.key]: payload.path });
}
@IpcMethod()
removeWorkingDirectory(payload: { key: string }): void {
const current = this.app.storeManager.get('workingDirectories', {});
const { [payload.key]: _, ...rest } = current;
this.app.storeManager.set('workingDirectories', rest);
}
@IpcMethod()
async getInstalledApps(): Promise<InstalledApp[]> {
if (process.platform !== 'darwin') return [];
const results: InstalledApp[] = [];
// Finder is always available on macOS
const finderPath = '/System/Library/CoreServices/Finder.app';
try {
const finderIcon = await app.getFileIcon(finderPath, { size: 'small' });
results.push({ icon: finderIcon.toDataURL(), id: 'finder', name: 'Finder' });
} catch {
results.push({ icon: '', id: 'finder', name: 'Finder' });
}
// Terminal.app is always available
const terminalPaths = [
'/System/Applications/Utilities/Terminal.app',
'/Applications/Utilities/Terminal.app',
];
for (const tp of terminalPaths) {
if (await pathExists(tp)) {
try {
const icon = await app.getFileIcon(tp, { size: 'small' });
results.push({ icon: icon.toDataURL(), id: 'terminal', name: 'Terminal' });
} catch {
results.push({ icon: '', id: 'terminal', name: 'Terminal' });
}
break;
}
}
// Check known apps
for (const knownApp of KNOWN_APPS) {
const appPath = join('/Applications', knownApp.bundleName);
if (await pathExists(appPath)) {
try {
const icon = await app.getFileIcon(appPath, { size: 'small' });
results.push({ icon: icon.toDataURL(), id: knownApp.id, name: knownApp.name });
} catch {
results.push({ icon: '', id: knownApp.id, name: knownApp.name });
}
}
}
return results;
}
@IpcMethod()
async openDirectoryInApp(payload: { appId: string; path: string }): Promise<void> {
const { appId, path: dirPath } = payload;
if (appId === 'finder') {
await shell.openPath(dirPath);
return;
}
if (process.platform !== 'darwin') return;
// Find app name from known apps or built-in
let appName: string | undefined;
if (appId === 'terminal') {
appName = 'Terminal';
} else {
const known = KNOWN_APPS.find((a) => a.id === appId);
appName = known?.name;
}
if (!appName) return;
return new Promise<void>((resolve, reject) => {
exec(`open -a "${appName}" "${dirPath}"`, (error) => {
if (error) {
logger.error(`Failed to open directory in ${appName}:`, error);
reject(error);
} else {
resolve();
}
});
});
}
private async setSystemThemeMode(themeMode: ThemeMode) {
nativeTheme.themeSource = themeMode;
}
+2 -1
View File
@@ -1,4 +1,4 @@
import { DataSyncConfig, NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { type DataSyncConfig, type NetworkProxySettings } from '@lobechat/electron-client-ipc';
export interface ElectronMainStore {
dataSyncConfig: DataSyncConfig;
@@ -13,6 +13,7 @@ export interface ElectronMainStore {
shortcuts: Record<string, string>;
storagePath: string;
themeMode: 'dark' | 'light' | 'system';
workingDirectories: Record<string, string>;
}
export type StoreKey = keyof ElectronMainStore;
@@ -4,9 +4,9 @@ import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { useChatStore } from '@/store/chat';
import { topicSelectors } from '@/store/chat/selectors';
import { workingDirectorySelectors } from '@/store/electron/selectors';
import { useElectronStore } from '@/store/electron/store';
interface OutOfScopeWarningProps {
/**
@@ -34,9 +34,16 @@ const isPathWithinWorkingDirectory = (targetPath: string, workingDirectory: stri
const OutOfScopeWarning = memo<OutOfScopeWarningProps>(({ paths }) => {
const { t } = useTranslation('tool');
// Get working directory from topic or agent store
const topicWorkingDir = useChatStore(topicSelectors.currentTopicWorkingDirectory);
const agentWorkingDir = useAgentStore(agentSelectors.currentAgentWorkingDirectory);
const activeTopicId = useChatStore((s) => s.activeTopicId);
const activeAgentId = useAgentStore((s) => s.activeAgentId);
// Get working directory from Electron Store
const topicWorkingDir = useElectronStore((s) =>
activeTopicId ? workingDirectorySelectors.topicWorkingDirectory(activeTopicId)(s) : undefined,
);
const agentWorkingDir = useElectronStore((s) =>
activeAgentId ? workingDirectorySelectors.agentWorkingDirectory(activeAgentId)(s) : undefined,
);
const workingDirectory = topicWorkingDir || agentWorkingDir;
// Find paths that are outside the working directory
@@ -59,11 +59,11 @@ describe('TopicModel - Update', () => {
await serverDB.insert(topics).values({ userId, id: topicId, title: 'Test' });
const result = await topicModel.updateMetadata(topicId, {
workingDirectory: '/path/to/dir',
model: 'gpt-4',
});
expect(result).toHaveLength(1);
expect(result[0].metadata).toEqual({ workingDirectory: '/path/to/dir' });
expect(result[0].metadata).toEqual({ model: 'gpt-4' });
});
it('should merge metadata with existing metadata', async () => {
@@ -76,14 +76,13 @@ describe('TopicModel - Update', () => {
});
const result = await topicModel.updateMetadata(topicId, {
workingDirectory: '/new/path',
provider: 'anthropic',
});
expect(result).toHaveLength(1);
expect(result[0].metadata).toEqual({
model: 'gpt-4',
provider: 'openai',
workingDirectory: '/new/path',
provider: 'anthropic',
});
});
@@ -93,17 +92,17 @@ describe('TopicModel - Update', () => {
userId,
id: topicId,
title: 'Test',
metadata: { workingDirectory: '/old/path', model: 'gpt-4' },
metadata: { provider: 'openai', model: 'gpt-4' },
});
const result = await topicModel.updateMetadata(topicId, {
workingDirectory: '/new/path',
model: 'claude-3',
});
expect(result).toHaveLength(1);
expect(result[0].metadata).toEqual({
model: 'gpt-4',
workingDirectory: '/new/path',
model: 'claude-3',
provider: 'openai',
});
});
@@ -117,7 +116,7 @@ describe('TopicModel - Update', () => {
});
const result = await topicModel.updateMetadata(topicId, {
workingDirectory: '/path/to/dir',
model: 'gpt-4',
});
expect(result).toHaveLength(0);
@@ -17,6 +17,48 @@ declare global {
}
}
type BroadcastHandler<T extends MainBroadcastEventKey> = (data: MainBroadcastParams<T>) => void;
type SharedBroadcastHandler = BroadcastHandler<MainBroadcastEventKey>;
const eventHandlerMap = new Map<MainBroadcastEventKey, Set<SharedBroadcastHandler>>();
const eventListenerMap = new Map<MainBroadcastEventKey, (e: any, data: any) => void>();
const getIpcRenderer = () => {
if (typeof window === 'undefined') return;
return window.electron?.ipcRenderer;
};
const ensureEventListener = (event: MainBroadcastEventKey) => {
const ipcRenderer = getIpcRenderer();
if (!ipcRenderer) return;
if (eventListenerMap.has(event)) return;
const listener = (_e: any, data: unknown) => {
const handlers = eventHandlerMap.get(event);
if (!handlers) return;
for (const handler of handlers) {
handler(data as MainBroadcastParams<typeof event>);
}
};
eventListenerMap.set(event, listener);
ipcRenderer.on(event, listener);
};
const cleanupEventListener = (event: MainBroadcastEventKey) => {
const handlers = eventHandlerMap.get(event);
if (handlers && handlers.size > 0) return;
const ipcRenderer = getIpcRenderer();
const listener = eventListenerMap.get(event);
if (!ipcRenderer || !listener) return;
ipcRenderer.removeListener(event, listener);
eventListenerMap.delete(event);
};
export const useWatchBroadcast = <T extends MainBroadcastEventKey>(
event: T,
handler: (data: MainBroadcastParams<T>) => void,
@@ -28,16 +70,33 @@ export const useWatchBroadcast = <T extends MainBroadcastEventKey>(
}, [handler]);
useEffect(() => {
if (!window.electron) return;
const ipcRenderer = getIpcRenderer();
if (!ipcRenderer) return;
const listener = (_e: any, data: MainBroadcastParams<T>) => {
handlerRef.current(data);
let handlers = eventHandlerMap.get(event);
if (!handlers) {
handlers = new Set<SharedBroadcastHandler>();
eventHandlerMap.set(event, handlers);
}
const sharedHandler: SharedBroadcastHandler = (data) => {
handlerRef.current(data as MainBroadcastParams<T>);
};
window.electron.ipcRenderer.on(event, listener);
handlers.add(sharedHandler);
ensureEventListener(event);
return () => {
window.electron.ipcRenderer.removeListener(event, listener);
const nextHandlers = eventHandlerMap.get(event);
if (!nextHandlers) return;
nextHandlers.delete(sharedHandler);
if (nextHandlers.size === 0) {
eventHandlerMap.delete(event);
}
cleanupEventListener(event);
};
}, [event]);
};
-14
View File
@@ -6,17 +6,3 @@
* - implement: execute directly without asking
*/
export type AgentMode = 'auto' | 'plan' | 'ask' | 'implement';
/**
* Local System configuration (desktop only)
*/
export interface LocalSystemConfig {
/**
* Local System working directory (desktop only)
*/
workingDirectory?: string;
// Future extensions:
// allowedPaths?: string[];
// deniedCommands?: string[];
}
+58 -73
View File
@@ -1,8 +1,6 @@
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
import { z } from 'zod';
import { SearchMode } from '../search';
import { LocalSystemConfig } from './agentConfig';
import { type SearchMode } from '../search';
export interface WorkingModel {
model: string;
@@ -10,46 +8,64 @@ export interface WorkingModel {
}
export interface LobeAgentChatConfig {
/**
* Local System configuration (desktop only)
*/
localSystem?: LocalSystemConfig;
enableAutoCreateTopic?: boolean;
autoCreateTopicThreshold: number;
enableMaxTokens?: boolean;
/**
* Model ID to use for generating compression summaries
*/
compressionModelId?: string;
/**
* Whether to enable streaming output
* Disable context caching
*/
enableStreaming?: boolean;
disableContextCaching?: boolean;
effort?: 'low' | 'medium' | 'high' | 'max';
/**
* Whether to enable adaptive thinking (Claude Opus 4.6)
*/
enableAdaptiveThinking?: boolean;
enableAutoCreateTopic?: boolean;
/**
* Whether to auto-scroll during AI streaming output
* undefined = use global setting
*/
enableAutoScrollOnStreaming?: boolean;
/**
* Enable history message compression threshold
* @deprecated Use enableContextCompression instead
*/
enableCompressHistory?: boolean;
/**
* Enable context compression
* When enabled, old messages will be compressed into summaries when token threshold is reached
*/
enableContextCompression?: boolean;
/**
* Enable historical message count
*/
enableHistoryCount?: boolean;
enableMaxTokens?: boolean;
/**
* Whether to enable reasoning
*/
enableReasoning?: boolean;
/**
* Whether to enable adaptive thinking (Claude Opus 4.6)
*/
enableAdaptiveThinking?: boolean;
/**
* Custom reasoning effort level
*/
enableReasoningEffort?: boolean;
effort?: 'low' | 'medium' | 'high' | 'max';
reasoningBudgetToken?: number;
reasoningEffort?: 'low' | 'medium' | 'high';
gpt5ReasoningEffort?: 'minimal' | 'low' | 'medium' | 'high';
gpt5_1ReasoningEffort?: 'none' | 'low' | 'medium' | 'high';
gpt5_2ReasoningEffort?: 'none' | 'low' | 'medium' | 'high' | 'xhigh';
gpt5_2ProReasoningEffort?: 'medium' | 'high' | 'xhigh';
/**
* Output text verbosity control
* Whether to enable streaming output
*/
textVerbosity?: 'low' | 'medium' | 'high';
thinking?: 'disabled' | 'auto' | 'enabled';
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';
thinkingBudget?: number;
enableStreaming?: boolean;
gpt5_1ReasoningEffort?: 'none' | 'low' | 'medium' | 'high';
gpt5_2ProReasoningEffort?: 'medium' | 'high' | 'xhigh';
gpt5_2ReasoningEffort?: 'none' | 'low' | 'medium' | 'high' | 'xhigh';
gpt5ReasoningEffort?: 'minimal' | 'low' | 'medium' | 'high';
/**
* Number of historical messages
*/
historyCount?: number;
/**
* Image aspect ratio for image generation models
*/
@@ -58,41 +74,21 @@ export interface LobeAgentChatConfig {
* Image resolution for image generation models
*/
imageResolution?: '1K' | '2K' | '4K';
/**
* Disable context caching
*/
disableContextCaching?: boolean;
/**
* Number of historical messages
*/
historyCount?: number;
/**
* Enable historical message count
*/
enableHistoryCount?: boolean;
/**
* Enable history message compression threshold
* @deprecated Use enableContextCompression instead
*/
enableCompressHistory?: boolean;
/**
* Enable context compression
* When enabled, old messages will be compressed into summaries when token threshold is reached
*/
enableContextCompression?: boolean;
/**
* Model ID to use for generating compression summaries
*/
compressionModelId?: string;
inputTemplate?: string;
reasoningBudgetToken?: number;
reasoningEffort?: 'low' | 'medium' | 'high';
searchMode?: SearchMode;
searchFCModel?: WorkingModel;
urlContext?: boolean;
useModelBuiltinSearch?: boolean;
searchMode?: SearchMode;
/**
* Output text verbosity control
*/
textVerbosity?: 'low' | 'medium' | 'high';
thinking?: 'disabled' | 'auto' | 'enabled';
thinkingBudget?: number;
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high';
/**
* Maximum length for tool execution result content (in characters)
* This prevents context overflow when sending tool results back to LLM
@@ -100,20 +96,10 @@ export interface LobeAgentChatConfig {
*/
toolResultMaxLength?: number;
/**
* Whether to auto-scroll during AI streaming output
* undefined = use global setting
*/
enableAutoScrollOnStreaming?: boolean;
}
/* eslint-enable */
urlContext?: boolean;
/**
* Zod schema for LocalSystemConfig
*/
export const LocalSystemConfigSchema = z.object({
workingDirectory: z.string().optional(),
});
useModelBuiltinSearch?: boolean;
}
export const AgentChatConfigSchema = z.object({
autoCreateTopicThreshold: z.number().default(2),
@@ -137,7 +123,6 @@ export const AgentChatConfigSchema = z.object({
historyCount: z.number().optional(),
imageAspectRatio: z.string().optional(),
imageResolution: z.enum(['1K', '2K', '4K']).optional(),
localSystem: LocalSystemConfigSchema.optional(),
reasoningBudgetToken: z.number().optional(),
reasoningEffort: z.enum(['low', 'medium', 'high']).optional(),
searchFCModel: z
-6
View File
@@ -11,7 +11,6 @@ export type TimeGroupId =
| `${number}-${string}`
| `${number}`;
/* eslint-disable typescript-sort-keys/string-enum */
export enum TopicDisplayMode {
ByTime = 'byTime',
Flat = 'flat',
@@ -43,11 +42,6 @@ export interface ChatTopicMetadata {
provider?: string;
userMemoryExtractRunState?: TopicUserMemoryExtractRunState;
userMemoryExtractStatus?: 'pending' | 'completed' | 'failed';
/**
* Local System working directory (desktop only)
* Priority is higher than Agent-level settings
*/
workingDirectory?: string;
}
export interface ChatTopicSummary {
@@ -1,173 +1,235 @@
import { isDesktop } from '@lobechat/const';
import { Flexbox, Icon, Text } from '@lobehub/ui';
import { Button, Divider, Input, Space, Switch } from 'antd';
import { FolderOpen } from 'lucide-react';
import { memo, useCallback, useState } from 'react';
import { App, Button, Divider, Dropdown, Input, Space, Switch } from 'antd';
import { type MenuProps } from 'antd';
import { ClipboardCopy, FolderOpen } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { electronSystemService } from '@/services/electron/system';
import { useAgentStore } from '@/store/agent';
import { agentByIdSelectors } from '@/store/agent/selectors';
import { useChatStore } from '@/store/chat';
import { topicSelectors } from '@/store/chat/selectors';
import { workingDirectorySelectors } from '@/store/electron/selectors';
import { useElectronStore } from '@/store/electron/store';
interface WorkingDirectoryContentProps {
agentId: string;
effectiveWorkingDirectory?: string;
onClose?: () => void;
}
const WorkingDirectoryContent = memo<WorkingDirectoryContentProps>(({ agentId, onClose }) => {
const { t } = useTranslation('plugin');
const WorkingDirectoryContent = memo<WorkingDirectoryContentProps>(
({ agentId, effectiveWorkingDirectory, onClose }) => {
const { t } = useTranslation('plugin');
const { message } = App.useApp();
// Get current values
const agentWorkingDirectory = useAgentStore((s) =>
agentByIdSelectors.getAgentWorkingDirectoryById(agentId)(s),
);
const topicWorkingDirectory = useChatStore(topicSelectors.currentTopicWorkingDirectory);
const activeTopicId = useChatStore((s) => s.activeTopicId);
const activeTopicId = useChatStore((s) => s.activeTopicId);
// Actions
const updateAgentLocalSystemConfig = useAgentStore((s) => s.updateAgentLocalSystemConfigById);
const updateTopicMetadata = useChatStore((s) => s.updateTopicMetadata);
// Get current values from Electron Store
const agentWorkingDirectory = useElectronStore((s) =>
workingDirectorySelectors.agentWorkingDirectory(agentId)(s),
);
const topicWorkingDirectory = useElectronStore((s) =>
activeTopicId ? workingDirectorySelectors.topicWorkingDirectory(activeTopicId)(s) : undefined,
);
// Local state for editing
const [agentDir, setAgentDir] = useState(agentWorkingDirectory || '');
const [topicDir, setTopicDir] = useState(topicWorkingDirectory || '');
const [useTopicOverride, setUseTopicOverride] = useState(!!topicWorkingDirectory);
const [loading, setLoading] = useState(false);
// Installed apps
const installedApps = useElectronStore((s) => s.installedApps);
const useFetchInstalledApps = useElectronStore((s) => s.useFetchInstalledApps);
useFetchInstalledApps();
const handleSelectAgentFolder = useCallback(async () => {
if (!isDesktop) return;
const folder = await electronSystemService.selectFolder({
defaultPath: agentDir || undefined,
title: t('localSystem.workingDirectory.selectFolder'),
});
if (folder) setAgentDir(folder);
}, [agentDir, t]);
const openDirectoryInApp = useElectronStore((s) => s.openDirectoryInApp);
const handleSelectTopicFolder = useCallback(async () => {
if (!isDesktop) return;
const folder = await electronSystemService.selectFolder({
defaultPath: topicDir || undefined,
title: t('localSystem.workingDirectory.selectFolder'),
});
if (folder) setTopicDir(folder);
}, [topicDir, t]);
// Actions
const setWorkingDirectory = useElectronStore((s) => s.setWorkingDirectory);
const removeWorkingDirectory = useElectronStore((s) => s.removeWorkingDirectory);
const handleSave = useCallback(async () => {
setLoading(true);
try {
// Save agent working directory
await updateAgentLocalSystemConfig(agentId, {
workingDirectory: agentDir || undefined,
// Local state for editing
const [agentDir, setAgentDir] = useState(agentWorkingDirectory || '');
const [topicDir, setTopicDir] = useState(topicWorkingDirectory || '');
const [useTopicOverride, setUseTopicOverride] = useState(!!topicWorkingDirectory);
const [loading, setLoading] = useState(false);
const handleSelectAgentFolder = useCallback(async () => {
if (!isDesktop) return;
const folder = await electronSystemService.selectFolder({
defaultPath: agentDir || undefined,
title: t('localSystem.workingDirectory.selectFolder'),
});
if (folder) setAgentDir(folder);
}, [agentDir, t]);
// Save topic working directory if override is enabled
if (activeTopicId && useTopicOverride) {
await updateTopicMetadata(activeTopicId, {
workingDirectory: topicDir || undefined,
});
} else if (activeTopicId && !useTopicOverride && topicWorkingDirectory) {
// Clear topic override if disabled
await updateTopicMetadata(activeTopicId, {
workingDirectory: undefined,
});
const handleSelectTopicFolder = useCallback(async () => {
if (!isDesktop) return;
const folder = await electronSystemService.selectFolder({
defaultPath: topicDir || undefined,
title: t('localSystem.workingDirectory.selectFolder'),
});
if (folder) setTopicDir(folder);
}, [topicDir, t]);
const handleSave = useCallback(async () => {
setLoading(true);
try {
if (agentDir) {
await setWorkingDirectory(`agent:${agentId}`, agentDir);
} else {
await removeWorkingDirectory(`agent:${agentId}`);
}
if (activeTopicId && useTopicOverride) {
if (topicDir) {
await setWorkingDirectory(`topic:${activeTopicId}`, topicDir);
} else {
await removeWorkingDirectory(`topic:${activeTopicId}`);
}
} else if (activeTopicId && !useTopicOverride && topicWorkingDirectory) {
await removeWorkingDirectory(`topic:${activeTopicId}`);
}
onClose?.();
} finally {
setLoading(false);
}
}, [
agentId,
agentDir,
activeTopicId,
useTopicOverride,
topicDir,
topicWorkingDirectory,
setWorkingDirectory,
removeWorkingDirectory,
onClose,
]);
onClose?.();
} finally {
setLoading(false);
}
}, [
agentId,
agentDir,
activeTopicId,
useTopicOverride,
topicDir,
topicWorkingDirectory,
updateAgentLocalSystemConfig,
updateTopicMetadata,
onClose,
]);
const handleCopyPath = useCallback(() => {
if (!effectiveWorkingDirectory) return;
navigator.clipboard.writeText(effectiveWorkingDirectory);
message.success(t('localSystem.workingDirectory.copiedPath'));
}, [effectiveWorkingDirectory, message, t]);
return (
<Flexbox gap={12} style={{ maxWidth: 600, minWidth: 320 }}>
<Flexbox gap={4}>
<Text style={{ fontSize: 12 }} type="secondary">
{t('localSystem.workingDirectory.agentLevel')}
</Text>
<Flexbox horizontal gap={8}>
<Input
placeholder={t('localSystem.workingDirectory.placeholder')}
size="small"
style={{ flex: 1, fontSize: 12 }}
value={agentDir}
variant={'filled'}
onChange={(e) => setAgentDir(e.target.value)}
/>
{isDesktop && (
<Button size="small" type={'text'} onClick={handleSelectAgentFolder}>
<Icon icon={FolderOpen} size={14} />
const openInMenuItems: MenuProps['items'] = useMemo(() => {
if (!effectiveWorkingDirectory) return [];
const appItems: MenuProps['items'] = installedApps.map((app) => ({
icon: app.icon ? (
<img alt={app.name} src={app.icon} style={{ borderRadius: 4, height: 16, width: 16 }} />
) : undefined,
key: app.id,
label: app.name,
onClick: () => openDirectoryInApp(app.id, effectiveWorkingDirectory),
}));
return [
...appItems,
{ type: 'divider' as const },
{
icon: <Icon icon={ClipboardCopy} size={16} />,
key: 'copy-path',
label: t('localSystem.workingDirectory.copyPath'),
onClick: handleCopyPath,
},
];
}, [effectiveWorkingDirectory, installedApps, openDirectoryInApp, handleCopyPath, t]);
return (
<Flexbox gap={12} style={{ maxWidth: 600, minWidth: 320 }}>
{effectiveWorkingDirectory && (
<>
<Flexbox horizontal align="center" gap={8} justify="space-between">
<Text
ellipsis={{ tooltip: effectiveWorkingDirectory }}
style={{ flex: 1, fontFamily: 'var(--lobe-font-family-code)', fontSize: 12 }}
>
{effectiveWorkingDirectory}
</Text>
<Dropdown menu={{ items: openInMenuItems }} trigger={['click']}>
<Button size="small">{t('localSystem.workingDirectory.open')}</Button>
</Dropdown>
</Flexbox>
<Divider style={{ margin: 0 }} />
</>
)}
<Flexbox gap={4}>
<Text style={{ fontSize: 12 }} type="secondary">
{t('localSystem.workingDirectory.agentLevel')}
</Text>
<Flexbox horizontal gap={8}>
<Input
placeholder={t('localSystem.workingDirectory.placeholder')}
size="small"
style={{ flex: 1, fontSize: 12 }}
value={agentDir}
variant={'filled'}
onChange={(e) => setAgentDir(e.target.value)}
/>
{isDesktop && (
<Button size="small" type={'text'} onClick={handleSelectAgentFolder}>
<Icon icon={FolderOpen} size={14} />
</Button>
)}
</Flexbox>
</Flexbox>
{activeTopicId && (
<>
<Divider style={{ margin: 0 }} />
<Flexbox
horizontal
align="center"
gap={8}
style={{ cursor: 'pointer' }}
onClick={() => {
setUseTopicOverride(!useTopicOverride);
}}
>
<Switch checked={useTopicOverride} size="small" onChange={setUseTopicOverride} />
<Text style={{ fontSize: 12 }}>
{t('localSystem.workingDirectory.topicOverride')}
</Text>
</Flexbox>
{useTopicOverride && (
<Flexbox gap={4}>
<Text style={{ fontSize: 12 }} type="secondary">
{t('localSystem.workingDirectory.topicLevel')}
</Text>
<Flexbox horizontal gap={8}>
<Input
placeholder={t('localSystem.workingDirectory.placeholder')}
size="small"
style={{ flex: 1, fontSize: 12 }}
value={topicDir}
variant={'filled'}
onChange={(e) => setTopicDir(e.target.value)}
/>
{isDesktop && (
<Button size="small" type={'text'} onClick={handleSelectTopicFolder}>
<Icon icon={FolderOpen} size={14} />
</Button>
)}
</Flexbox>
</Flexbox>
)}
</>
)}
<Flexbox horizontal justify="flex-end">
<Space>
<Button size="small" onClick={onClose}>
{t('cancel', { ns: 'common' })}
</Button>
)}
<Button loading={loading} size="small" type="primary" onClick={handleSave}>
{t('save', { ns: 'common' })}
</Button>
</Space>
</Flexbox>
</Flexbox>
{activeTopicId && (
<>
<Divider style={{ margin: 0 }} />
<Flexbox
horizontal
align="center"
gap={8}
style={{ cursor: 'pointer' }}
onClick={() => {
setUseTopicOverride(!useTopicOverride);
}}
>
<Switch checked={useTopicOverride} size="small" onChange={setUseTopicOverride} />
<Text style={{ fontSize: 12 }}>{t('localSystem.workingDirectory.topicOverride')}</Text>
</Flexbox>
{useTopicOverride && (
<Flexbox gap={4}>
<Text style={{ fontSize: 12 }} type="secondary">
{t('localSystem.workingDirectory.topicLevel')}
</Text>
<Flexbox horizontal gap={8}>
<Input
placeholder={t('localSystem.workingDirectory.placeholder')}
size="small"
style={{ flex: 1, fontSize: 12 }}
value={topicDir}
variant={'filled'}
onChange={(e) => setTopicDir(e.target.value)}
/>
{isDesktop && (
<Button size="small" type={'text'} onClick={handleSelectTopicFolder}>
<Icon icon={FolderOpen} size={14} />
</Button>
)}
</Flexbox>
</Flexbox>
)}
</>
)}
<Flexbox horizontal justify="flex-end">
<Space>
<Button size="small" onClick={onClose}>
{t('cancel', { ns: 'common' })}
</Button>
<Button loading={loading} size="small" type="primary" onClick={handleSave}>
{t('save', { ns: 'common' })}
</Button>
</Space>
</Flexbox>
</Flexbox>
);
});
);
},
);
WorkingDirectoryContent.displayName = 'WorkingDirectoryContent';
@@ -2,13 +2,14 @@ import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system';
import { Flexbox, Icon, Popover, Tooltip } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { LaptopIcon, SquircleDashed } from 'lucide-react';
import { memo, useMemo, useState } from 'react';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAgentStore } from '@/store/agent';
import { agentByIdSelectors } from '@/store/agent/selectors';
import { useChatStore } from '@/store/chat';
import { topicSelectors } from '@/store/chat/selectors';
import { workingDirectorySelectors } from '@/store/electron/selectors';
import { useElectronStore } from '@/store/electron/store';
import WorkingDirectoryContent from './WorkingDirectoryContent';
@@ -36,6 +37,7 @@ const WorkingDirectory = memo(() => {
const [open, setOpen] = useState(false);
const agentId = useAgentStore((s) => s.activeAgentId);
const activeTopicId = useChatStore((s) => s.activeTopicId);
// Check if local-system plugin is enabled for current agent
const plugins = useAgentStore((s) =>
@@ -46,10 +48,32 @@ const WorkingDirectory = memo(() => {
[plugins],
);
// Get working directory from Topic (higher priority) or Agent (fallback)
const topicWorkingDirectory = useChatStore(topicSelectors.currentTopicWorkingDirectory);
const agentWorkingDirectory = useAgentStore((s) =>
agentId ? agentByIdSelectors.getAgentWorkingDirectoryById(agentId)(s) : undefined,
// Load working directories from Electron Store
const useFetchWorkingDirectories = useElectronStore((s) => s.useFetchWorkingDirectories);
useFetchWorkingDirectories();
// Lazy migration: move old chatConfig.localSystem.workingDirectory to Electron Store
const setWorkingDirectory = useElectronStore((s) => s.setWorkingDirectory);
const updateAgentChatConfig = useAgentStore((s) => s.updateAgentChatConfigById);
const migratedRef = useRef<Set<string>>(new Set());
useEffect(() => {
if (!agentId || migratedRef.current.has(agentId)) return;
const agentData = useAgentStore.getState().agentMap[agentId] as any;
const oldDir = agentData?.chatConfig?.localSystem?.workingDirectory;
if (oldDir && !useElectronStore.getState().workingDirectories[`agent:${agentId}`]) {
migratedRef.current.add(agentId);
setWorkingDirectory(`agent:${agentId}`, oldDir);
updateAgentChatConfig(agentId, { localSystem: undefined } as any);
}
}, [agentId, setWorkingDirectory, updateAgentChatConfig]);
// Get working directory from Electron Store: Topic (higher priority) or Agent (fallback)
const topicWorkingDirectory = useElectronStore((s) =>
activeTopicId ? workingDirectorySelectors.topicWorkingDirectory(activeTopicId)(s) : undefined,
);
const agentWorkingDirectory = useElectronStore((s) =>
agentId ? workingDirectorySelectors.agentWorkingDirectory(agentId)(s) : undefined,
);
const effectiveWorkingDirectory = topicWorkingDirectory || agentWorkingDirectory;
@@ -89,10 +113,16 @@ const WorkingDirectory = memo(() => {
);
return (
<Popover
content={<WorkingDirectoryContent agentId={agentId} onClose={() => setOpen(false)} />}
open={open}
placement="bottomRight"
trigger="click"
content={
<WorkingDirectoryContent
agentId={agentId}
effectiveWorkingDirectory={effectiveWorkingDirectory}
onClose={() => setOpen(false)}
/>
}
onOpenChange={setOpen}
>
<div>
+4 -5
View File
@@ -36,7 +36,6 @@ vi.mock('@/store/agent/selectors', () => ({
agentSelectors: {
currentAgentModel: () => 'gpt-4',
currentAgentModelProvider: () => 'openai',
currentAgentWorkingDirectory: () => undefined,
},
}));
@@ -46,10 +45,10 @@ vi.mock('@/store/chat', () => ({
},
}));
vi.mock('@/store/chat/selectors', () => ({
topicSelectors: {
currentTopicWorkingDirectory: () => undefined,
},
vi.mock('@/store/electron/store', () => ({
getElectronStoreState: () => ({
workingDirectories: {},
}),
}));
vi.mock('../GlobalAgentContextManager', () => ({
+15 -5
View File
@@ -4,7 +4,7 @@ import { template } from 'es-toolkit/compat';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { useChatStore } from '@/store/chat';
import { topicSelectors } from '@/store/chat/selectors';
import { getElectronStoreState } from '@/store/electron/store';
import { useUserStore } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
@@ -157,13 +157,23 @@ export const VARIABLE_GENERATORS = {
* Working directory: Topic-level setting takes priority over Agent-level setting
*/
workingDirectory: () => {
const electronState = getElectronStoreState();
const activeTopicId = useChatStore.getState().activeTopicId;
const activeAgentId = useAgentStore.getState().activeAgentId;
// First check topic-level working directory
const topicWorkingDir = topicSelectors.currentTopicWorkingDirectory(useChatStore.getState());
if (topicWorkingDir) return topicWorkingDir;
if (activeTopicId) {
const topicDir = electronState.workingDirectories[`topic:${activeTopicId}`];
if (topicDir) return topicDir;
}
// Fallback to agent-level working directory
const agentWorkingDir = agentSelectors.currentAgentWorkingDirectory(useAgentStore.getState());
return agentWorkingDir ?? '(not specified, use user Desktop directory as default)';
if (activeAgentId) {
const agentDir = electronState.workingDirectories[`agent:${activeAgentId}`];
if (agentDir) return agentDir;
}
return '(not specified, use user Desktop directory as default)';
},
} as Record<string, () => string>;
+4
View File
@@ -369,6 +369,10 @@ export default {
'localSystem.workingDirectory.topicDescription':
'Override Agent default for this conversation only',
'localSystem.workingDirectory.topicLevel': 'Conversation override',
'localSystem.workingDirectory.copiedPath': 'Copied!',
'localSystem.workingDirectory.copyPath': 'Copy Path',
'localSystem.workingDirectory.open': 'Open',
'localSystem.workingDirectory.openIn': 'Open In',
'localSystem.workingDirectory.topicOverride': 'Override for this conversation',
'mcpEmpty.deployment': 'No deployment options',
'mcpEmpty.prompts': 'No prompts',
-1
View File
@@ -515,7 +515,6 @@ export const topicRouter = router({
metadata: z.object({
model: z.string().optional(),
provider: z.string().optional(),
workingDirectory: z.string().optional(),
}),
}),
)
@@ -245,7 +245,7 @@ export class AgentRuntimeService {
// need be removed
modelRuntimeConfig,
userId,
workingDirectory: agentConfig?.chatConfig?.localSystem?.workingDirectory,
workingDirectory: appContext?.workingDirectory,
...appContext,
},
// modelRuntimeConfig at state level for executor fallback
@@ -76,6 +76,7 @@ export interface OperationCreationParams {
groupId?: string | null;
threadId?: string | null;
topicId?: string | null;
workingDirectory?: string;
};
autoStart?: boolean;
initialContext: AgentRuntimeContext;
+20
View File
@@ -65,6 +65,26 @@ class ElectronSystemService {
}): Promise<string | undefined> {
return this.ipc.system.selectFolder(params);
}
async getWorkingDirectories(): Promise<Record<string, string>> {
return this.ipc.system.getWorkingDirectories();
}
async setWorkingDirectory(key: string, path: string): Promise<void> {
return this.ipc.system.setWorkingDirectory({ key, path });
}
async removeWorkingDirectory(key: string): Promise<void> {
return this.ipc.system.removeWorkingDirectory({ key });
}
async getInstalledApps(): Promise<Array<{ icon: string; id: string; name: string }>> {
return this.ipc.system.getInstalledApps();
}
async openDirectoryInApp(appId: string, path: string): Promise<void> {
return this.ipc.system.openDirectoryInApp({ appId, path });
}
}
// Export a singleton instance of the service
+1 -4
View File
@@ -78,10 +78,7 @@ export class TopicService {
return lambdaClient.topic.updateTopic.mutate({ id, value: data });
};
updateTopicMetadata = (
id: string,
metadata: { model?: string; provider?: string; workingDirectory?: string },
) => {
updateTopicMetadata = (id: string, metadata: { model?: string; provider?: string }) => {
return lambdaClient.topic.updateTopicMetadata.mutate({ id, metadata });
};
@@ -1,7 +1,7 @@
import { DEFAULT_PROVIDER } from '@lobechat/business-const';
import { DEFAULT_MODEL, DEFAUTT_AGENT_TTS_CONFIG } from '@lobechat/const';
import { type AgentBuilderContext } from '@lobechat/context-engine';
import { type AgentMode, type LobeAgentTTSConfig, type LocalSystemConfig } from '@lobechat/types';
import { type AgentMode, type LobeAgentTTSConfig } from '@lobechat/types';
import { type AgentStoreState } from '../initialState';
import { agentSelectors } from './selectors';
@@ -73,23 +73,6 @@ const getAgentEnableModeById =
return mode !== undefined;
};
/**
* Get local system config by agentId
* Now reads from chatConfig.localSystem
*/
const getAgentLocalSystemConfigById =
(agentId: string) =>
(s: AgentStoreState): LocalSystemConfig | undefined =>
agentSelectors.getAgentConfigById(agentId)(s)?.chatConfig?.localSystem;
/**
* Get working directory by agentId
*/
const getAgentWorkingDirectoryById =
(agentId: string) =>
(s: AgentStoreState): string | undefined =>
getAgentLocalSystemConfigById(agentId)(s)?.workingDirectory;
/**
* Get agent builder context by agentId
* Used for injecting current agent config/meta into Agent Builder context
@@ -128,13 +111,11 @@ export const agentByIdSelectors = {
getAgentEnableModeById,
getAgentFilesById,
getAgentKnowledgeBasesById,
getAgentLocalSystemConfigById,
getAgentModeById,
getAgentModelById,
getAgentModelProviderById,
getAgentPluginsById,
getAgentSystemRoleById,
getAgentTTSById,
getAgentWorkingDirectoryById,
isAgentConfigLoadingById,
};
-16
View File
@@ -11,7 +11,6 @@ import {
type KnowledgeItem,
type LobeAgentConfig,
type LobeAgentTTSConfig,
type LocalSystemConfig,
type MetaData,
} from '@lobechat/types';
import { KnowledgeType } from '@lobechat/types';
@@ -247,19 +246,6 @@ const currentAgentMode = (s: AgentStoreState): AgentMode | undefined => {
*/
const isAgentModeEnabled = (s: AgentStoreState): boolean => currentAgentMode(s) !== undefined;
/**
* Get current agent's local system config
* Now reads from chatConfig.localSystem
*/
const currentAgentLocalSystemConfig = (s: AgentStoreState): LocalSystemConfig | undefined =>
currentAgentConfig(s)?.chatConfig?.localSystem;
/**
* Get current agent's working directory
*/
const currentAgentWorkingDirectory = (s: AgentStoreState): string | undefined =>
currentAgentLocalSystemConfig(s)?.workingDirectory;
const isCurrentAgentExternal = (s: AgentStoreState): boolean => !currentAgentData(s)?.virtual;
export const agentSelectors = {
@@ -269,7 +255,6 @@ export const agentSelectors = {
currentAgentDescription,
currentAgentFiles,
currentAgentKnowledgeBases,
currentAgentLocalSystemConfig,
currentAgentMeta,
currentAgentMode,
currentAgentModel,
@@ -280,7 +265,6 @@ export const agentSelectors = {
currentAgentTTSVoice,
currentAgentTags,
currentAgentTitle,
currentAgentWorkingDirectory,
currentEnabledKnowledge,
currentKnowledgeIds,
displayableAgentPlugins,
+1 -14
View File
@@ -11,11 +11,7 @@ import { agentService } from '@/services/agent';
import { type StoreSetter } from '@/store/types';
import { getUserStoreState } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
import {
type LobeAgentChatConfig,
type LobeAgentConfig,
type LocalSystemConfig,
} from '@/types/agent';
import { type LobeAgentChatConfig, type LobeAgentConfig } from '@/types/agent';
import { type MetaData } from '@/types/meta';
import { merge } from '@/utils/merge';
@@ -190,15 +186,6 @@ export class AgentSliceActionImpl {
await this.#get().optimisticUpdateAgentConfig(agentId, config, controller.signal);
};
updateAgentLocalSystemConfigById = async (
agentId: string,
config: Partial<LocalSystemConfig>,
): Promise<void> => {
if (!agentId) return;
await this.#get().updateAgentChatConfigById(agentId, { localSystem: config });
};
updateAgentMeta = async (meta: Partial<MetaData>): Promise<void> => {
const { activeAgentId } = this.#get();
@@ -31,6 +31,7 @@ import { getAgentStoreState } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { createAgentExecutors } from '@/store/chat/agents/createAgentExecutors';
import { type ChatStore } from '@/store/chat/store';
import { getElectronStoreState } from '@/store/electron/store';
import { getFileStoreState } from '@/store/file/store';
import { pageAgentRuntime } from '@/store/tool/slices/builtin/executors/lobe-page-agent';
import { type StoreSetter } from '@/store/types';
@@ -182,8 +183,13 @@ export class StreamingExecutorActionImpl {
provider: agentConfigData.provider!,
};
const topicWorkingDirectory = topicSelectors.currentTopicWorkingDirectory(this.#get());
const agentWorkingDirectory = agentSelectors.currentAgentWorkingDirectory(getAgentStoreState());
const electronState = getElectronStoreState();
const topicWorkingDirectory = topicId
? electronState.workingDirectories[`topic:${topicId}`]
: undefined;
const agentWorkingDirectory = agentId
? electronState.workingDirectories[`agent:${agentId}`]
: undefined;
const workingDirectory = topicWorkingDirectory ?? agentWorkingDirectory;
// Create initial state or use provided state
-10
View File
@@ -68,15 +68,6 @@ const currentActiveTopicSummary = (s: ChatStoreState): ChatTopicSummary | undefi
};
};
/**
* Get current active topic's working directory
* Returns undefined if no topic is active or no working directory is set
*/
const currentTopicWorkingDirectory = (s: ChatStoreState): string | undefined => {
const activeTopic = currentActiveTopic(s);
return activeTopic?.metadata?.workingDirectory;
};
const isCreatingTopic = (s: ChatStoreState) => s.creatingTopic;
const isUndefinedTopics = (s: ChatStoreState) => !currentTopics(s);
const isInSearchMode = (s: ChatStoreState) => s.inSearchingMode;
@@ -148,7 +139,6 @@ export const topicSelectors = {
currentTopicCount,
currentTopicData,
currentTopicLength,
currentTopicWorkingDirectory,
currentTopics,
currentTopicsWithoutCron,
currentUnFavTopics,
@@ -0,0 +1,82 @@
import isEqual from 'fast-deep-equal';
import { type SWRResponse } from 'swr';
import useSWR from 'swr';
import { mutate } from '@/libs/swr';
import { electronSystemService } from '@/services/electron/system';
import { type InstalledApp } from '@/store/electron/initialState';
import { type StoreSetter } from '@/store/types';
import { type ElectronStore } from '../store';
const ELECTRON_WORKING_DIRECTORIES_KEY = 'electron:getWorkingDirectories';
const ELECTRON_INSTALLED_APPS_KEY = 'electron:getInstalledApps';
type Setter = StoreSetter<ElectronStore>;
export const workingDirectorySlice = (set: Setter, get: () => ElectronStore, _api?: unknown) =>
new WorkingDirectoryActionImpl(set, get, _api);
export class WorkingDirectoryActionImpl {
readonly #get: () => ElectronStore;
readonly #set: Setter;
constructor(set: Setter, get: () => ElectronStore, _api?: unknown) {
void _api;
this.#set = set;
this.#get = get;
}
refreshWorkingDirectories = async (): Promise<void> => {
await mutate(ELECTRON_WORKING_DIRECTORIES_KEY);
};
setWorkingDirectory = async (key: string, path: string): Promise<void> => {
const current = this.#get().workingDirectories;
this.#set({ workingDirectories: { ...current, [key]: path } });
await electronSystemService.setWorkingDirectory(key, path);
};
removeWorkingDirectory = async (key: string): Promise<void> => {
const { [key]: _, ...rest } = this.#get().workingDirectories;
this.#set({ workingDirectories: rest });
await electronSystemService.removeWorkingDirectory(key);
};
openDirectoryInApp = async (appId: string, path: string): Promise<void> => {
await electronSystemService.openDirectoryInApp(appId, path);
};
useFetchInstalledApps = (): SWRResponse => {
return useSWR<InstalledApp[]>(
ELECTRON_INSTALLED_APPS_KEY,
async () => electronSystemService.getInstalledApps(),
{
onSuccess: (data: InstalledApp[]) => {
if (!isEqual(data, this.#get().installedApps)) {
this.#set({ installedApps: data });
}
},
revalidateOnFocus: false,
},
);
};
useFetchWorkingDirectories = (): SWRResponse => {
return useSWR<Record<string, string>>(
ELECTRON_WORKING_DIRECTORIES_KEY,
async () => electronSystemService.getWorkingDirectories(),
{
onSuccess: (data) => {
if (!isEqual(data, this.#get().workingDirectories)) {
this.#set({ workingDirectories: data });
}
},
},
);
};
}
export type WorkingDirectoryAction = Pick<
WorkingDirectoryActionImpl,
keyof WorkingDirectoryActionImpl
>;
+10
View File
@@ -20,10 +20,17 @@ export const defaultProxySettings: NetworkProxySettings = {
proxyType: 'http',
};
export interface InstalledApp {
icon: string;
id: string;
name: string;
}
export interface ElectronState extends NavigationHistoryState, RecentPagesState {
appState: ElectronAppState;
dataSyncConfig: DataSyncConfig;
desktopHotkeys: Record<string, string>;
installedApps: InstalledApp[];
isAppStateInit?: boolean;
isConnectingServer?: boolean;
isConnectionDrawerOpen?: boolean;
@@ -32,6 +39,7 @@ export interface ElectronState extends NavigationHistoryState, RecentPagesState
isSyncActive?: boolean;
proxySettings: NetworkProxySettings;
remoteServerSyncError?: { message?: string; type: RemoteServerError };
workingDirectories: Record<string, string>;
}
export const initialState: ElectronState = {
@@ -40,6 +48,7 @@ export const initialState: ElectronState = {
appState: {},
dataSyncConfig: { storageMode: 'cloud' },
desktopHotkeys: {},
installedApps: [],
isAppStateInit: false,
isConnectingServer: false,
isConnectionDrawerOpen: false,
@@ -47,4 +56,5 @@ export const initialState: ElectronState = {
isInitRemoteServerConfig: false,
isSyncActive: false,
proxySettings: defaultProxySettings,
workingDirectories: {},
};
+1
View File
@@ -1,3 +1,4 @@
export * from './desktopState';
export * from './hotkey';
export * from './sync';
export * from './workingDirectory';
@@ -0,0 +1,12 @@
import { type ElectronState } from '@/store/electron/initialState';
const agentWorkingDirectory = (agentId: string) => (s: ElectronState) =>
s.workingDirectories[`agent:${agentId}`];
const topicWorkingDirectory = (topicId: string) => (s: ElectronState) =>
s.workingDirectories[`topic:${topicId}`];
export const workingDirectorySelectors = {
agentWorkingDirectory,
topicWorkingDirectory,
};
+7 -2
View File
@@ -14,6 +14,8 @@ import { type ElectronSettingsAction } from './actions/settings';
import { settingsSlice } from './actions/settings';
import { type ElectronRemoteServerAction } from './actions/sync';
import { remoteSyncSlice } from './actions/sync';
import { type WorkingDirectoryAction } from './actions/workingDirectory';
import { workingDirectorySlice } from './actions/workingDirectory';
import { type ElectronState } from './initialState';
import { initialState } from './initialState';
@@ -26,7 +28,8 @@ export interface ElectronStore
ElectronAppAction,
ElectronSettingsAction,
NavigationHistoryAction,
RecentPagesAction {
RecentPagesAction,
WorkingDirectoryAction {
/* empty */
}
@@ -34,7 +37,8 @@ type ElectronStoreAction = ElectronRemoteServerAction &
ElectronAppAction &
ElectronSettingsAction &
NavigationHistoryAction &
RecentPagesAction;
RecentPagesAction &
WorkingDirectoryAction;
const createStore: StateCreator<ElectronStore, [['zustand/devtools', never]]> = (
...parameters: Parameters<StateCreator<ElectronStore, [['zustand/devtools', never]]>>
@@ -46,6 +50,7 @@ const createStore: StateCreator<ElectronStore, [['zustand/devtools', never]]> =
settingsSlice(...parameters),
createNavigationHistorySlice(...parameters),
createRecentPagesSlice(...parameters),
workingDirectorySlice(...parameters),
]),
});