diff --git a/.vscode/settings.json b/.vscode/settings.json index e69f4107c..77acfbec6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { - "i18n-ally.localesPaths": ["src/renderer/src/i18n"], + "i18n-ally.localesPaths": [ + "src/renderer/src/i18n" + ], "i18n-ally.keystyle": "nested", "i18n-ally.sourceLanguage": "zh-CN", "i18n-ally.namespace": true, diff --git a/electron-builder-macx64.yml b/electron-builder-macx64.yml index 4baf06d29..beb6a7b84 100644 --- a/electron-builder-macx64.yml +++ b/electron-builder-macx64.yml @@ -60,6 +60,8 @@ electronLanguages: - pt - da-DK - da + - he-IL + - he mac: entitlementsInherit: build/entitlements.mac.plist extendInfo: diff --git a/electron-builder.yml b/electron-builder.yml index 08b8f3741..93c31acbb 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -59,6 +59,8 @@ electronLanguages: - pt - da-DK - da + - he-IL + - he win: executableName: DeepChat nsis: diff --git a/package.json b/package.json index 20266fe1a..601b1b883 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "DeepChat", - "version": "0.5.2", + "version": "0.5.3", "description": "DeepChat,一个简单易用的AI客户端", "main": "./out/main/index.js", "author": "ThinkInAIXYZ", @@ -81,6 +81,7 @@ "electron-window-state": "^5.0.3", "es-mime-types": "^0.1.4", "fflate": "^0.8.2", + "font-list": "^2.0.1", "glob": "^11.0.3", "https-proxy-agent": "^7.0.6", "jsonrepair": "^3.13.1", @@ -146,7 +147,7 @@ "katex": "^0.16.25", "lint-staged": "^16.2.6", "lucide-vue-next": "^0.544.0", - "markstream-vue": "0.0.2-beta.8", + "markstream-vue": "0.0.3-beta.3", "mermaid": "^11.12.1", "minimatch": "^10.1.1", "monaco-editor": "^0.52.2", @@ -156,7 +157,7 @@ "prettier": "^3.7.1", "reka-ui": "^2.5.1", "simple-git-hooks": "^2.13.1", - "stream-monaco": "^0.0.7", + "stream-monaco": "^0.0.8", "tailwind-merge": "^3.4.0", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.1.17", diff --git a/src/main/events.ts b/src/main/events.ts index 7002f85f7..e7d5ff5ea 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -32,6 +32,8 @@ export const CONFIG_EVENTS = { MODEL_CONFIG_CHANGED: 'config:model-config-changed', // 模型配置变更事件 MODEL_CONFIG_RESET: 'config:model-config-reset', // 模型配置重置事件 MODEL_CONFIGS_IMPORTED: 'config:model-configs-imported', // 模型配置批量导入事件 + FONT_FAMILY_CHANGED: 'config:font-family-changed', + CODE_FONT_FAMILY_CHANGED: 'config:code-font-family-changed', // OAuth相关事件 OAUTH_LOGIN_START: 'config:oauth-login-start', // OAuth登录开始 OAUTH_LOGIN_SUCCESS: 'config:oauth-login-success', // OAuth登录成功 @@ -238,3 +240,7 @@ export const ACP_WORKSPACE_EVENTS = { FILES_CHANGED: 'acp-workspace:files-changed', // File tree changed SESSION_MODES_READY: 'acp-workspace:session-modes-ready' // Session modes available } + +export const ACP_DEBUG_EVENTS = { + EVENT: 'acp-debug:event' +} diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index c98b549fa..86c03b1b6 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -74,6 +74,8 @@ interface IAppSettings { default_system_prompt?: string // Default system prompt webContentLengthLimit?: number // Web content truncation length limit, default 3000 characters updateChannel?: string // Update channel: 'stable' | 'canary' + fontFamily?: string // Custom UI font + codeFontFamily?: string // Custom code font [key: string]: unknown // Allow arbitrary keys, using unknown type instead of any } @@ -131,6 +133,8 @@ export class ConfigPresenter implements IConfigPresenter { copyWithCotEnabled: true, loggingEnabled: false, floatingButtonEnabled: false, + fontFamily: '', + codeFontFamily: '', default_system_prompt: '', webContentLengthLimit: 3000, updateChannel: 'stable', // Default to stable version @@ -686,7 +690,8 @@ export class ConfigPresenter implements IConfigPresenter { 'fr-FR', 'fa-IR', 'pt-BR', - 'da-DK' + 'da-DK', + 'he-IL' ] // Exact match @@ -862,6 +867,30 @@ export class ConfigPresenter implements IConfigPresenter { this.uiSettingsHelper.setTraceDebugEnabled(enabled) } + getFontFamily(): string { + return this.uiSettingsHelper.getFontFamily() + } + + setFontFamily(fontFamily?: string | null): void { + this.uiSettingsHelper.setFontFamily(fontFamily) + } + + getCodeFontFamily(): string { + return this.uiSettingsHelper.getCodeFontFamily() + } + + setCodeFontFamily(fontFamily?: string | null): void { + this.uiSettingsHelper.setCodeFontFamily(fontFamily) + } + + resetFontSettings(): void { + this.uiSettingsHelper.resetFontSettings() + } + + async getSystemFonts(): Promise { + return this.uiSettingsHelper.getSystemFonts() + } + // Get floating button switch status getFloatingButtonEnabled(): boolean { const value = this.getSetting('floatingButtonEnabled') ?? false @@ -1163,7 +1192,7 @@ export class ConfigPresenter implements IConfigPresenter { private refreshAcpProviderAgents(agentIds?: string[]): void { try { - const providerInstance = presenter?.llmproviderPresenter?.getProviderInstance('acp') + const providerInstance = presenter?.llmproviderPresenter?.getProviderInstance?.('acp') if (!providerInstance) { return } diff --git a/src/main/presenter/configPresenter/uiSettingsHelper.ts b/src/main/presenter/configPresenter/uiSettingsHelper.ts index e599442e0..e64ccab6e 100644 --- a/src/main/presenter/configPresenter/uiSettingsHelper.ts +++ b/src/main/presenter/configPresenter/uiSettingsHelper.ts @@ -1,5 +1,24 @@ import { eventBus, SendTarget } from '@/eventbus' import { CONFIG_EVENTS } from '@/events' +import fontList from 'font-list' + +const normalizeFontNameValue = (name: string): string => { + const trimmed = name + .replace(/\(.*?\)/g, '') + .replace(/\s+/g, ' ') + .trim() + if (!trimmed) return '' + + const stripped = trimmed + .replace( + /\b(Regular|Italic|Oblique|Bold|Light|Medium|Semi\s*Bold|Black|Narrow|Condensed|Extended|Book|Roman)\b/gi, + '' + ) + .replace(/\s+/g, ' ') + .trim() + + return stripped || trimmed +} type SetSetting = (key: string, value: T) => void type GetSetting = (key: string) => T | undefined @@ -12,6 +31,7 @@ interface UiSettingsHelperOptions { export class UiSettingsHelper { private readonly getSetting: GetSetting private readonly setSetting: SetSetting + private systemFontsCache: string[] | null = null constructor(options: UiSettingsHelperOptions) { this.getSetting = options.getSetting @@ -66,4 +86,92 @@ export class UiSettingsHelper { this.setSetting('notificationsEnabled', enabled) eventBus.send(CONFIG_EVENTS.NOTIFICATIONS_CHANGED, SendTarget.ALL_WINDOWS, Boolean(enabled)) } + + getFontFamily(): string { + return this.normalizeStoredFont(this.getSetting('fontFamily')) + } + + setFontFamily(fontFamily?: string | null): void { + const normalized = this.normalizeStoredFont(fontFamily) + this.setSetting('fontFamily', normalized) + eventBus.send(CONFIG_EVENTS.FONT_FAMILY_CHANGED, SendTarget.ALL_WINDOWS, normalized) + } + + getCodeFontFamily(): string { + return this.normalizeStoredFont(this.getSetting('codeFontFamily')) + } + + setCodeFontFamily(fontFamily?: string | null): void { + const normalized = this.normalizeStoredFont(fontFamily) + this.setSetting('codeFontFamily', normalized) + eventBus.send(CONFIG_EVENTS.CODE_FONT_FAMILY_CHANGED, SendTarget.ALL_WINDOWS, normalized) + } + + resetFontSettings(): void { + this.setFontFamily('') + this.setCodeFontFamily('') + } + + async getSystemFonts(): Promise { + if (this.systemFontsCache) { + return this.systemFontsCache + } + + const fonts = await this.loadSystemFonts() + this.systemFontsCache = fonts + return fonts + } + + private normalizeStoredFont(value?: string | null): string { + if (typeof value !== 'string') return '' + const cleaned = value + .replace(/[\r\n\t]/g, ' ') + .replace(/[;:{}()[\]<>]/g, '') + .replace(/['"`\\]/g, '') + .trim() + if (!cleaned) return '' + + const collapsed = cleaned.replace(/\s+/g, ' ').slice(0, 100) + + // If we already have detected system fonts cached, prefer an exact match from that list + if (this.systemFontsCache?.length) { + const match = this.systemFontsCache.find( + (font) => font.toLowerCase() === collapsed.toLowerCase() + ) + if (match) return match + } + + return collapsed + } + + private async loadSystemFonts(): Promise { + try { + const detected = await fontList.getFonts() + const normalized = detected + .map((font) => this.normalizeFontName(font)) + .filter((font): font is string => Boolean(font)) + return this.uniqueFonts(normalized) + } catch (error) { + console.warn('Failed to detect system fonts with font-list:', error) + return [] + } + } + + private uniqueFonts(fonts: string[]): string[] { + const seen = new Set() + const result: string[] = [] + fonts.forEach((font) => { + const name = font.trim() + if (!name) return + const key = name.toLowerCase() + if (seen.has(key)) return + seen.add(key) + result.push(name) + }) + return result + } + + private normalizeFontName(name: string): string { + return normalizeFontNameValue(name) + } } diff --git a/src/main/presenter/lifecyclePresenter/hooks/beforeQuit/acpCleanupHook.ts b/src/main/presenter/lifecyclePresenter/hooks/beforeQuit/acpCleanupHook.ts new file mode 100644 index 000000000..216fab8bc --- /dev/null +++ b/src/main/presenter/lifecyclePresenter/hooks/beforeQuit/acpCleanupHook.ts @@ -0,0 +1,37 @@ +/** + * Ensure ACP-related processes/PTYs are terminated during shutdown + */ + +import { LifecycleHook, LifecycleContext } from '@shared/presenter' +import { LifecyclePhase } from '@shared/lifecycle' +import { presenter } from '@/presenter' +import { killTerminal } from '../../../configPresenter/acpInitHelper' + +export const acpCleanupHook: LifecycleHook = { + name: 'acp-cleanup', + phase: LifecyclePhase.BEFORE_QUIT, + priority: 6, + critical: false, + execute: async (_context: LifecycleContext) => { + console.log('[Lifecycle][ACP] acpCleanupHook: shutting down ACP resources') + + try { + killTerminal() + } catch (error) { + console.warn('[Lifecycle][ACP] acpCleanupHook: failed to kill ACP init terminal:', error) + } + + try { + const llmPresenter = presenter?.llmproviderPresenter + // Avoid instantiating ACP provider during shutdown; only clean up if already created + const acpProvider = llmPresenter?.getExistingProviderInstance?.('acp') as + | { cleanup?: () => Promise } + | undefined + if (acpProvider?.cleanup) { + await acpProvider.cleanup() + } + } catch (error) { + console.warn('[Lifecycle][ACP] acpCleanupHook: failed to cleanup ACP provider:', error) + } + } +} diff --git a/src/main/presenter/lifecyclePresenter/hooks/index.ts b/src/main/presenter/lifecyclePresenter/hooks/index.ts index 284bd0797..58fa2ab8a 100644 --- a/src/main/presenter/lifecyclePresenter/hooks/index.ts +++ b/src/main/presenter/lifecyclePresenter/hooks/index.ts @@ -15,3 +15,4 @@ export { floatingDestroyHook } from './beforeQuit/floatingDestroyHook' export { presenterDestroyHook } from './beforeQuit/presenterDestroyHook' export { builtinKnowledgeDestroyHook } from './beforeQuit/builtinKnowledgeDestroyHook' export { windowQuittingHook } from './beforeQuit/windowQuittingHook' +export { acpCleanupHook } from './beforeQuit/acpCleanupHook' diff --git a/src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts b/src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts index fba9e2ef5..d919c5279 100644 --- a/src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts +++ b/src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts @@ -128,8 +128,13 @@ export class AcpContentMapper { payload.events.push(createStreamEvent.text(text)) payload.blocks.push(this.createBlock('content', text)) } else { + const timestamp = now() payload.events.push(createStreamEvent.reasoning(text)) - payload.blocks.push(this.createBlock('reasoning_content', text)) + payload.blocks.push( + this.createBlock('reasoning_content', text, { + reasoning_time: { start: timestamp, end: timestamp } + }) + ) } } diff --git a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts index 62ccc09e3..0ee653a51 100644 --- a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts +++ b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts @@ -18,14 +18,20 @@ import { RuntimeHelper } from '@/lib/runtimeHelper' import { buildClientCapabilities } from './acpCapabilities' import { AcpFsHandler } from './acpFsHandler' import { AcpTerminalManager } from './acpTerminalManager' +import { eventBus, SendTarget } from '@/eventbus' +import { ACP_WORKSPACE_EVENTS } from '@/events' export interface AcpProcessHandle extends AgentProcessHandle { child: ChildProcessWithoutNullStreams connection: ClientSideConnectionType agent: AcpAgentConfig readyAt: number + state: 'warmup' | 'bound' + boundConversationId?: string /** The working directory this process was spawned with */ workdir: string + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string } interface AcpProcessManagerOptions { @@ -65,6 +71,7 @@ export class AcpProcessManager implements AgentProcessManager Promise private readonly getUvRegistry?: () => Promise private readonly handles = new Map() + private readonly boundHandles = new Map() private readonly pendingHandles = new Map>() private readonly sessionListeners = new Map() private readonly permissionResolvers = new Map() @@ -73,6 +80,8 @@ export class AcpProcessManager implements AgentProcessManager() private readonly fsHandlers = new Map() private readonly agentLocks = new Map>() + private readonly preferredModes = new Map() + private shuttingDown = false constructor(options: AcpProcessManagerOptions) { this.providerId = options.providerId @@ -130,47 +139,100 @@ export class AcpProcessManager implements AgentProcessManager { - const resolvedWorkdir = this.resolveWorkdir(workdir) - const existing = this.handles.get(agent.id) + return await this.warmupProcess(agent, workdir) + } - // Fast-path for already-alive handles with matching workdir - if (existing && this.isHandleAlive(existing) && existing.workdir === resolvedWorkdir) { - return existing + /** + * Resolve workdir to an absolute path, using fallback if not provided. + */ + private resolveWorkdir(workdir?: string): string { + if (workdir && workdir.trim()) { + return workdir.trim() } + return this.getFallbackWorkdir() + } + /** + * Build a stable key for warmup handles scoped by agent and workdir. + */ + private getWarmupKey(agentId: string, workdir: string): string { + return `${agentId}::${workdir}` + } + + /** + * Warm up a process for the given agent/workdir without binding it to a conversation. + * Reuses an existing warmup handle when possible; never reuses bound handles. + */ + async warmupProcess(agent: AcpAgentConfig, workdir?: string): Promise { + if (this.shuttingDown) { + throw new Error('[ACP] Process manager is shutting down, refusing to spawn new process') + } + const resolvedWorkdir = this.resolveWorkdir(workdir) + const warmupKey = this.getWarmupKey(agent.id, resolvedWorkdir) + const preferredModeId = this.preferredModes.get(warmupKey) const releaseLock = await this.acquireAgentLock(agent.id) + try { - const currentHandle = this.handles.get(agent.id) - if (currentHandle && this.isHandleAlive(currentHandle)) { - if (currentHandle.workdir === resolvedWorkdir) { - return currentHandle - } + const warmupCount = this.getHandlesByAgent(agent.id).filter((handle) => + this.isHandleAlive(handle) + ).length + console.info( + `[ACP] Warmup requested for agent ${agent.id} (workdir=${resolvedWorkdir}, warmups=${warmupCount})` + ) + const reusable = this.findReusableHandle(agent.id, resolvedWorkdir) + if (reusable && this.isHandleAlive(reusable)) { console.info( - `[ACP] Workdir changed for agent ${agent.id}: "${currentHandle.workdir}" -> "${resolvedWorkdir}", recreating process` + `[ACP] Reusing warmup process for agent ${agent.id} (pid=${reusable.pid}, workdir=${resolvedWorkdir})` ) - await this.release(agent.id) + this.applyPreferredMode(reusable, preferredModeId) + return reusable } - const inflight = this.pendingHandles.get(agent.id) + const inflight = this.pendingHandles.get(warmupKey) if (inflight) { const inflightHandle = await inflight - if (inflightHandle.workdir === resolvedWorkdir) { + if ( + this.isHandleAlive(inflightHandle) && + inflightHandle.workdir === resolvedWorkdir && + inflightHandle.state === 'warmup' + ) { + console.info( + `[ACP] Awaiting inflight warmup for agent ${agent.id} (pid=${inflightHandle.pid}, workdir=${resolvedWorkdir})` + ) + this.applyPreferredMode(inflightHandle, preferredModeId) return inflightHandle } + if (inflightHandle.state === 'warmup') { + console.info( + `[ACP] Discarding inflight warmup for agent ${agent.id} (workdir "${inflightHandle.workdir}") in favor of "${resolvedWorkdir}"` + ) + await this.disposeHandle(inflightHandle) + } + } else { console.info( - `[ACP] Workdir mismatch for inflight agent ${agent.id}, recreating with workdir: "${resolvedWorkdir}"` + `[ACP] No inflight handle for agent ${agent.id} (workdir=${resolvedWorkdir}), spawning new warmup` ) - await this.release(agent.id) } const handlePromise = this.spawnProcess(agent, resolvedWorkdir) - this.pendingHandles.set(agent.id, handlePromise) + this.pendingHandles.set(warmupKey, handlePromise) + try { const handle = await handlePromise - this.handles.set(agent.id, handle) + handle.state = 'warmup' + handle.boundConversationId = undefined + handle.workdir = resolvedWorkdir + this.handles.set(warmupKey, handle) + void this.fetchProcessModes(handle).catch((error) => { + console.warn(`[ACP] Failed to fetch modes during warmup for agent ${agent.id}:`, error) + }) + this.applyPreferredMode(handle, preferredModeId) + console.info( + `[ACP] Warmup process ready for agent ${agent.id} (pid=${handle.pid}, workdir=${resolvedWorkdir})` + ) return handle } finally { - this.pendingHandles.delete(agent.id) + this.pendingHandles.delete(warmupKey) } } finally { releaseLock() @@ -178,38 +240,86 @@ export class AcpProcessManager implements AgentProcessManager { + const resolvedWorkdir = this.resolveWorkdir(workdir) + const warmupKey = this.getWarmupKey(agent.id, resolvedWorkdir) + this.preferredModes.set(warmupKey, modeId) + + // Apply to existing warmup handle if available + const existingWarmup = this.findReusableHandle(agent.id, resolvedWorkdir) + if (existingWarmup && this.isHandleAlive(existingWarmup)) { + existingWarmup.currentModeId = modeId + this.notifyModesReady(existingWarmup) } - return this.getFallbackWorkdir() } getProcess(agentId: string): AcpProcessHandle | null { - return this.handles.get(agentId) ?? null + const warmupHandle = Array.from(this.handles.values()).find( + (handle) => handle.agentId === agentId + ) + if (warmupHandle) return warmupHandle + + for (const handle of this.boundHandles.values()) { + if (handle.agentId === agentId) return handle + } + + return null } listProcesses(): AcpProcessHandle[] { - return Array.from(this.handles.values()) + const seen = new Set() + const processes: AcpProcessHandle[] = [] + + for (const handle of this.handles.values()) { + if (!seen.has(handle)) { + processes.push(handle) + seen.add(handle) + } + } + + for (const handle of this.boundHandles.values()) { + if (!seen.has(handle)) { + processes.push(handle) + seen.add(handle) + } + } + + return processes } async release(agentId: string): Promise { - const handle = this.handles.get(agentId) - if (!handle) return - - this.handles.delete(agentId) - this.clearSessionsForAgent(agentId) + const targets = this.getHandlesByAgent(agentId) + if (!targets.length) return - this.killChild(handle.child) + const releaseLock = await this.acquireAgentLock(agentId) + try { + await Promise.allSettled(targets.map((handle) => this.disposeHandle(handle))) + this.clearSessionsForAgent(agentId) + } finally { + releaseLock() + } } async shutdown(): Promise { - const releases = Array.from(this.handles.keys()).map((agentId) => this.release(agentId)) + if (this.shuttingDown) return + this.shuttingDown = true + // Kill eagerly so subprocesses don't survive app shutdown even if async cleanup is skipped + this.forceKillAllProcesses('shutdown') + const allAgents = new Set() + for (const handle of this.handles.values()) { + allAgents.add(handle.agentId) + } + for (const handle of this.boundHandles.values()) { + allAgents.add(handle.agentId) + } + const releases = Array.from(allAgents.values()).map((agentId) => this.release(agentId)) await Promise.allSettled(releases) await this.terminalManager.shutdown() this.handles.clear() + this.boundHandles.clear() this.sessionListeners.clear() this.permissionResolvers.clear() this.pendingHandles.clear() @@ -217,6 +327,86 @@ export class AcpProcessManager implements AgentProcessManager + handle.agentId === agentId && + handle.state === 'warmup' && + (!workdir || !handle.workdir || handle.workdir === resolvedWorkdir) + ) + const handle = + warmupHandles.find(([, candidate]) => candidate.workdir === resolvedWorkdir)?.[1] ?? + warmupHandles[0]?.[1] + if (!handle) { + console.warn(`[ACP] No warmup handle to bind for agent ${agentId}`) + return + } + if (handle.state !== 'warmup') { + console.warn( + `[ACP] Cannot bind handle in state "${handle.state}" for agent ${agentId}, expected warmup` + ) + return + } + if (!this.isHandleAlive(handle)) { + console.warn(`[ACP] Cannot bind dead handle for agent ${agentId}`) + void this.disposeHandle(handle) + return + } + + handle.state = 'bound' + handle.boundConversationId = conversationId + // Remove from warmup map + for (const [key, value] of this.handles.entries()) { + if (value === handle) { + this.handles.delete(key) + break + } + } + this.boundHandles.set(conversationId, handle) + + // Immediately notify renderer if modes are already known + this.notifyModesReady(handle, conversationId) + console.info( + `[ACP] Bound process for agent ${agentId} to conversation ${conversationId} (pid=${handle.pid}, workdir=${handle.workdir})` + ) + } + + async unbindProcess(agentId: string, conversationId: string): Promise { + const releaseLock = await this.acquireAgentLock(agentId) + try { + const handle = this.boundHandles.get(conversationId) + if (!handle || handle.agentId !== agentId) return + + await this.disposeHandle(handle) + } finally { + releaseLock() + } + } + + getProcessModes( + agentId: string, + workdir?: string + ): + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined { + const resolvedWorkdir = this.resolveWorkdir(workdir) + const candidates = this.getHandlesByAgent(agentId).filter( + (handle) => handle.workdir === resolvedWorkdir && this.isHandleAlive(handle) + ) + const handle = candidates[0] + if (!handle) return undefined + + return { + availableModes: handle.availableModes, + currentModeId: handle.currentModeId + } + } + registerSessionListener( agentId: string, sessionId: string, @@ -273,6 +463,7 @@ export class AcpProcessManager implements AgentProcessManager client, stream) + const handleSeed: Partial = {} // Add process health check before initialization if (child.killed) { @@ -328,12 +519,21 @@ export class AcpProcessManager implements AgentProcessManager ({ + id: m.id, + name: m.name ?? m.id, + description: m.description ?? '' + }) + ) + if (initAvailableModes) { console.info( - `[ACP] Available modes: ${JSON.stringify(resultData.modes.availableModes?.map((m) => m.id) ?? [])}` + `[ACP] Available modes: ${JSON.stringify(initAvailableModes.map((m) => m.id) ?? [])}` ) - console.info(`[ACP] Current mode: ${resultData.modes.currentModeId}`) + console.info(`[ACP] Current mode: ${resultData.modes?.currentModeId}`) } + handleSeed.availableModes = initAvailableModes + handleSeed.currentModeId = resultData.modes?.currentModeId } catch (error) { console.error(`[ACP] Connection initialization failed for agent ${agent.id}:`, error) @@ -356,22 +556,24 @@ export class AcpProcessManager implements AgentProcessManager { console.warn( `[ACP] Agent process for ${agent.id} exited (PID: ${child.pid}, code=${code ?? 'null'}, signal=${signal ?? 'null'})` ) - if (this.handles.get(agent.id)?.child === child) { - this.handles.delete(agent.id) - } + this.removeHandleReferences(handle) this.clearSessionsForAgent(agent.id) }) @@ -695,6 +897,121 @@ export class AcpProcessManager implements AgentProcessManager { + if (!this.isHandleAlive(handle)) return + try { + const response = await handle.connection.newSession({ + cwd: handle.workdir, + mcpServers: [] + }) + if (response.sessionId) { + this.registerSessionWorkdir(response.sessionId, handle.workdir) + } + + const modes = response.modes + if (modes?.availableModes?.length) { + handle.availableModes = modes.availableModes.map((mode) => ({ + id: mode.id, + name: mode.name ?? mode.id, + description: mode.description ?? '' + })) + // Preserve user-selected preferred mode if it exists in the available list + if ( + handle.currentModeId && + handle.availableModes.some((m) => m.id === handle.currentModeId) + ) { + // keep preferred + } else if (modes.currentModeId) { + handle.currentModeId = modes.currentModeId + } else { + handle.currentModeId = handle.availableModes[0]?.id ?? handle.currentModeId + } + this.notifyModesReady(handle) + } + + if (response.sessionId) { + try { + await handle.connection.cancel({ sessionId: response.sessionId }) + this.clearSession(response.sessionId) + } catch (cancelError) { + console.warn( + `[ACP] Failed to cancel warmup session ${response.sessionId} for agent ${handle.agentId}:`, + cancelError + ) + } + } + } catch (error) { + console.warn(`[ACP] Warmup session failed to fetch modes for agent ${handle.agentId}:`, error) + } + } + + private notifyModesReady(handle: AcpProcessHandle, conversationId?: string): void { + if (!handle.availableModes || handle.availableModes.length === 0) return + + eventBus.sendToRenderer(ACP_WORKSPACE_EVENTS.SESSION_MODES_READY, SendTarget.ALL_WINDOWS, { + conversationId: conversationId ?? handle.boundConversationId, + agentId: handle.agentId, + workdir: handle.workdir, + current: handle.currentModeId ?? 'default', + available: handle.availableModes + }) + } + + private getHandlesByAgent(agentId: string): AcpProcessHandle[] { + const handles: AcpProcessHandle[] = [] + for (const handle of this.handles.values()) { + if (handle.agentId === agentId && !handles.includes(handle)) { + handles.push(handle) + } + } + for (const handle of this.boundHandles.values()) { + if (handle.agentId === agentId && !handles.includes(handle)) { + handles.push(handle) + } + } + return handles + } + + private getRestartCount(agentId: string): number { + return this.getHandlesByAgent(agentId).reduce( + (max, handle) => Math.max(max, handle.restarts ?? 0), + 0 + ) + } + + private removeHandleReferences(handle: AcpProcessHandle): void { + for (const [key, warmupHandle] of this.handles.entries()) { + if (warmupHandle === handle) { + this.handles.delete(key) + } + } + + for (const [conversationId, boundHandle] of this.boundHandles.entries()) { + if (boundHandle === handle) { + this.boundHandles.delete(conversationId) + } + } + } + + private async disposeHandle(handle: AcpProcessHandle): Promise { + this.removeHandleReferences(handle) + this.killChild(handle.child, 'dispose') + } + + private findReusableHandle(agentId: string, workdir: string): AcpProcessHandle | undefined { + const candidates = this.getHandlesByAgent(agentId).filter( + (handle) => + handle.workdir === workdir && handle.state === 'warmup' && this.isHandleAlive(handle) + ) + return candidates[0] + } + + private applyPreferredMode(handle: AcpProcessHandle, preferredModeId?: string): void { + if (!preferredModeId) return + handle.currentModeId = preferredModeId + this.notifyModesReady(handle) + } + private clearSessionsForAgent(agentId: string): void { for (const [sessionId, entry] of this.sessionListeners.entries()) { if (entry.agentId === agentId) { @@ -707,14 +1024,50 @@ export class AcpProcessManager implements AgentProcessManager this.killChild(handle.child, reason)) } - private killChild(child: ChildProcessWithoutNullStreams): void { + private killChild(child: ChildProcessWithoutNullStreams, reason?: string): void { + const pid = child.pid + if (pid) { + if (process.platform === 'win32') { + try { + spawn('taskkill', ['/PID', `${pid}`, '/T', '/F'], { stdio: 'ignore' }) + } catch (error) { + console.warn(`[ACP] Failed to taskkill process ${pid} (${reason ?? 'unknown'}):`, error) + } + } else { + try { + spawn('pkill', ['-TERM', '-P', `${pid}`], { stdio: 'ignore' }) + } catch (error) { + console.warn(`[ACP] Failed to pkill children for process ${pid}:`, error) + } + try { + process.kill(pid, 'SIGTERM') + } catch (error) { + console.warn(`[ACP] Failed to SIGTERM process ${pid}:`, error) + } + } + } + if (!child.killed) { try { child.kill() } catch (error) { - console.warn('[ACP] Failed to kill agent process:', error) + console.warn( + `[ACP] Failed to kill agent process${pid ? ` ${pid}` : ''} (${reason ?? 'unknown'}):`, + error + ) } } } @@ -739,6 +1092,8 @@ export class AcpProcessManager implements AgentProcessManager { // Pass workdir to process manager so the process runs in the correct directory - const handle = await this.processManager.getConnection(agent, workdir) - const session = await this.initializeSession(handle, agent, workdir) + let handle: AcpProcessHandle + try { + handle = await this.processManager.getConnection(agent, workdir) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + if (message.includes('shutting down')) { + throw new Error('[ACP] Cannot create session: process manager is shutting down') + } + throw error + } + this.processManager.bindProcess(agent.id, conversationId, workdir) + + const session = await this.initializeSession(handle, agent, workdir).catch(async (error) => { + await this.processManager.unbindProcess(agent.id, conversationId) + throw error + }) const detachListeners = this.attachSessionHooks(agent.id, session.sessionId, hooks) // Register session workdir for fs/terminal operations @@ -168,6 +191,37 @@ export class AcpSessionManager { console.warn('[ACP] Failed to persist session metadata:', error) }) + const availableModes = session.availableModes ?? handle.availableModes + // Prefer handle.currentModeId (which may contain preferredMode from warmup) over session default + let currentModeId = handle.currentModeId ?? session.currentModeId + handle.availableModes = availableModes + handle.currentModeId = currentModeId + + // Apply preferred mode to session if it differs from session default and is valid + if ( + availableModes?.length && + currentModeId && + currentModeId !== session.currentModeId && + availableModes.some((mode) => mode.id === currentModeId) + ) { + try { + await handle.connection.setSessionMode({ + sessionId: session.sessionId, + modeId: currentModeId + }) + console.info( + `[ACP] Applied preferred mode "${currentModeId}" to session ${session.sessionId} for conversation ${conversationId}` + ) + } catch (error) { + console.warn( + `[ACP] Failed to apply preferred mode "${currentModeId}" for conversation ${conversationId}:`, + error + ) + // Fallback to session default mode if preferred mode application fails + currentModeId = session.currentModeId ?? currentModeId + } + } + return { ...session, providerId: this.providerId, @@ -180,8 +234,8 @@ export class AcpSessionManager { connection: handle.connection, detachHandlers: detachListeners, workdir, - availableModes: session.availableModes, - currentModeId: session.currentModeId + availableModes, + currentModeId } } @@ -220,12 +274,25 @@ export class AcpSessionManager { // Extract modes from response if available const modes = response.modes - const availableModes = modes?.availableModes?.map((m) => ({ - id: m.id, - name: m.name, - description: m.description ?? '' - })) - const currentModeId = modes?.currentModeId + const availableModes = + modes?.availableModes?.map((m) => ({ + id: m.id, + name: m.name, + description: m.description ?? '' + })) ?? handle.availableModes + + const preferredModeId = handle.currentModeId + const responseModeId = modes?.currentModeId + let currentModeId = preferredModeId + if ( + !currentModeId || + (availableModes && !availableModes.some((m) => m.id === currentModeId)) + ) { + currentModeId = responseModeId ?? currentModeId ?? availableModes?.[0]?.id + } + + handle.availableModes = availableModes + handle.currentModeId = currentModeId // Log available modes for the agent if (availableModes && availableModes.length > 0) { diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index b4eca04f1..1947dd76d 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -12,7 +12,9 @@ import { ModelScopeMcpSyncResult, IConfigPresenter, ISQLitePresenter, - AcpWorkdirInfo + AcpWorkdirInfo, + AcpDebugRequest, + AcpDebugRunResult } from '@shared/presenter' import { ProviderChange, ProviderBatchUpdate } from '@shared/provider-operations' import { eventBus } from '@/eventbus' @@ -143,6 +145,10 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { return this.providerInstanceManager.getProviderInstance(providerId) } + public getExistingProviderInstance(providerId: string): BaseLLMProvider | undefined { + return this.providerInstanceManager.getExistingProviderInstance(providerId) + } + async getModelList(providerId: string): Promise { return this.modelManager.getModelList(providerId) } @@ -479,6 +485,47 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { await this.acpSessionPersistence.updateWorkdir(conversationId, agentId, trimmed) } + async warmupAcpProcess(agentId: string, workdir: string): Promise { + const provider = this.getAcpProviderInstance() + if (!provider) return + try { + await provider.warmupProcess(agentId, workdir) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + if (message.includes('shutting down')) { + console.warn( + `[ACP] Cannot warmup process for agent ${agentId}: process manager is shutting down` + ) + return + } + throw error + } + } + + async getAcpProcessModes( + agentId: string, + workdir: string + ): Promise< + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined + > { + const provider = this.getAcpProviderInstance() + if (!provider) { + return undefined + } + return provider.getProcessModes(agentId, workdir) + } + + async setAcpPreferredProcessMode(agentId: string, workdir: string, modeId: string) { + const provider = this.getAcpProviderInstance() + if (!provider) return + + await provider.setPreferredProcessMode(agentId, workdir, modeId) + } + async setAcpSessionMode(conversationId: string, modeId: string): Promise { const provider = this.getAcpProviderInstance() if (!provider) { @@ -498,6 +545,14 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { return await provider.getSessionModes(conversationId) } + async runAcpDebugAction(request: AcpDebugRequest): Promise { + const provider = this.getAcpProviderInstance() + if (!provider) { + throw new Error('ACP provider unavailable') + } + return await provider.runDebugAction(request) + } + async resolveAgentPermission(requestId: string, granted: boolean): Promise { const provider = this.getAcpProviderInstance() if (!provider) { diff --git a/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts b/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts index b624646bc..8b3fc4854 100644 --- a/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts +++ b/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts @@ -218,6 +218,10 @@ export class ProviderInstanceManager { } } + getExistingProviderInstance(providerId: string): BaseLLMProvider | undefined { + return this.providerInstances.get(providerId) + } + getProviders(): LLM_PROVIDER[] { return Array.from(this.providers.values()) } diff --git a/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts b/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts index ee1333690..1f331ad7e 100644 --- a/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts @@ -8,6 +8,9 @@ import type { MODEL_META, ModelConfig, AcpAgentConfig, + AcpDebugEventEntry, + AcpDebugRequest, + AcpDebugRunResult, LLM_PROVIDER, IConfigPresenter } from '@shared/presenter' @@ -18,14 +21,17 @@ import { type PermissionRequestOption } from '@shared/types/core/llm-events' import { ModelType } from '@shared/model' +import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk' import { eventBus, SendTarget } from '@/eventbus' -import { CONFIG_EVENTS, ACP_WORKSPACE_EVENTS } from '@/events' -import { AcpProcessManager } from '../agent/acpProcessManager' +import { ACP_DEBUG_EVENTS, ACP_WORKSPACE_EVENTS, CONFIG_EVENTS } from '@/events' +import { app } from 'electron' +import { AcpProcessManager, type AcpProcessHandle } from '../agent/acpProcessManager' import { AcpSessionManager } from '../agent/acpSessionManager' import type { AcpSessionRecord } from '../agent/acpSessionManager' import { AcpContentMapper } from '../agent/acpContentMapper' import { AcpMessageFormatter } from '../agent/acpMessageFormatter' import { AcpSessionPersistence } from '../agent/acpSessionPersistence' +import { buildClientCapabilities } from '../agent/acpCapabilities' import { nanoid } from 'nanoid' import { presenter } from '@/presenter' @@ -347,6 +353,8 @@ export class AcpProvider extends BaseAgentProvider< SendTarget.ALL_WINDOWS, { conversationId: conversationKey, + agentId: agent.id, + workdir: session.workdir, current: session.currentModeId ?? 'default', available: session.availableModes } @@ -406,6 +414,382 @@ export class AcpProvider extends BaseAgentProvider< } } + public async warmupProcess(agentId: string, workdir: string): Promise { + const agent = await this.getAgentById(agentId) + if (!agent) return + + try { + await this.processManager.warmupProcess(agent, workdir) + } catch (error) { + console.warn(`[ACP] Failed to warmup ACP process for agent ${agentId}:`, error) + } + } + + public getProcessModes( + agentId: string, + workdir: string + ): + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined { + return this.processManager.getProcessModes(agentId, workdir) ?? undefined + } + + public async setPreferredProcessMode(agentId: string, workdir: string, modeId: string) { + const agent = await this.getAgentById(agentId) + if (!agent) return + + try { + await this.processManager.setPreferredMode(agent, workdir, modeId) + } catch (error) { + console.warn( + `[ACP] Failed to set preferred mode "${modeId}" for agent ${agentId} in workdir "${workdir}":`, + error + ) + } + } + + public async runDebugAction(request: AcpDebugRequest): Promise { + const agent = (await this.configPresenter.getAcpAgents()).find( + (item) => item.id === request.agentId + ) + if (!agent) { + throw new Error(`[ACP] Agent not found: ${request.agentId}`) + } + let handle: AcpProcessHandle + try { + handle = await this.processManager.getConnection(agent, request.workdir) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + if (message.includes('shutting down')) { + return { + status: 'error', + sessionId: undefined, + error: 'Process manager is shutting down', + events: [] + } + } + throw error + } + const connection = handle.connection + const events: AcpDebugEventEntry[] = [] + + const isPlainObject = (value: unknown): value is Record => + Boolean(value) && typeof value === 'object' && !Array.isArray(value) + + const pushEvent = (entry: Omit): void => { + const record: AcpDebugEventEntry = { + ...entry, + id: nanoid(), + timestamp: Date.now(), + agentId: agent.id + } + events.push(record) + if (request.webContentsId) { + eventBus.sendToRenderer(ACP_DEBUG_EVENTS.EVENT, SendTarget.ALL_WINDOWS, { + webContentsId: request.webContentsId, + agentId: agent.id, + event: record + }) + } + } + + let activeSessionId = + request.sessionId ?? + (isPlainObject(request.payload) && typeof request.payload.sessionId === 'string' + ? (request.payload.sessionId as string) + : undefined) + + let disposeNotification: (() => void) | undefined + let disposePermission: (() => void) | undefined + + const attachSession = (sessionId: string) => { + if (disposeNotification) { + disposeNotification() + disposeNotification = undefined + } + if (disposePermission) { + disposePermission() + disposePermission = undefined + } + + disposeNotification = this.processManager.registerSessionListener( + agent.id, + sessionId, + (notification) => { + pushEvent({ + kind: 'notification', + action: 'session/update', + sessionId, + payload: notification + }) + } + ) + disposePermission = this.processManager.registerPermissionResolver( + agent.id, + sessionId, + async (params) => { + pushEvent({ + kind: 'permission', + action: 'requestPermission', + sessionId, + payload: params + }) + return { outcome: { outcome: 'cancelled' } } + } + ) + } + + const defaultInitPayload = (): schema.InitializeRequest => ({ + protocolVersion: PROTOCOL_VERSION, + clientInfo: { name: 'DeepChat', version: app.getVersion() }, + clientCapabilities: buildClientCapabilities({ enableFs: true, enableTerminal: true }) + }) + + const resolveWorkdir = (): string | undefined => { + const cwd = request.workdir ?? handle.workdir + return cwd?.trim() || undefined + } + + try { + switch (request.action) { + case 'initialize': { + const body = isPlainObject(request.payload) + ? { ...defaultInitPayload(), ...request.payload } + : defaultInitPayload() + pushEvent({ kind: 'request', action: 'initialize', payload: body }) + const response = await connection.initialize(body) + const sessionIdFromInit = (response as unknown as { sessionId?: unknown }).sessionId + if (!activeSessionId && typeof sessionIdFromInit === 'string') { + activeSessionId = sessionIdFromInit + } + pushEvent({ + kind: 'response', + action: 'initialize', + sessionId: activeSessionId, + payload: response + }) + break + } + case 'newSession': { + const basePayload: schema.NewSessionRequest = { + cwd: resolveWorkdir() ?? process.cwd(), + mcpServers: [] + } + const body = isPlainObject(request.payload) + ? { ...basePayload, ...request.payload } + : basePayload + pushEvent({ kind: 'request', action: 'newSession', payload: body }) + const response = await connection.newSession(body) + activeSessionId = response.sessionId + pushEvent({ + kind: 'response', + action: 'newSession', + sessionId: activeSessionId, + payload: response + }) + break + } + case 'loadSession': { + const payloadOverrides = isPlainObject(request.payload) ? request.payload : undefined + const sessionFromPayload = + payloadOverrides && typeof payloadOverrides.sessionId === 'string' + ? payloadOverrides.sessionId + : undefined + const sessionToLoad = sessionFromPayload ?? activeSessionId + if (!sessionToLoad || typeof sessionToLoad !== 'string') { + throw new Error('Session ID is required for loadSession') + } + const body: schema.LoadSessionRequest = { + cwd: resolveWorkdir() ?? process.cwd(), + mcpServers: [], + sessionId: sessionToLoad + } + if (payloadOverrides) { + if (typeof payloadOverrides.cwd === 'string') { + body.cwd = payloadOverrides.cwd + } + if (Array.isArray(payloadOverrides.mcpServers)) { + body.mcpServers = payloadOverrides.mcpServers as schema.McpServer[] + } + if (isPlainObject(payloadOverrides._meta)) { + body._meta = payloadOverrides._meta + } + } + pushEvent({ + kind: 'request', + action: 'loadSession', + sessionId: sessionToLoad, + payload: body + }) + attachSession(sessionToLoad) + const response = await connection.loadSession(body) + activeSessionId = sessionToLoad + pushEvent({ + kind: 'response', + action: 'loadSession', + sessionId: activeSessionId, + payload: response + }) + break + } + case 'prompt': { + if (!activeSessionId) { + throw new Error('Session ID is required for prompt') + } + const body = isPlainObject(request.payload) + ? { ...request.payload, sessionId: activeSessionId } + : { sessionId: activeSessionId, prompt: [] } + pushEvent({ + kind: 'request', + action: 'prompt', + sessionId: activeSessionId, + payload: body + }) + attachSession(activeSessionId) + const response = await connection.prompt(body as schema.PromptRequest) + pushEvent({ + kind: 'response', + action: 'prompt', + sessionId: activeSessionId, + payload: response + }) + break + } + case 'cancel': { + if (!activeSessionId) { + throw new Error('Session ID is required for cancel') + } + const body = isPlainObject(request.payload) + ? { ...request.payload, sessionId: activeSessionId } + : { sessionId: activeSessionId } + pushEvent({ + kind: 'request', + action: 'cancel', + sessionId: activeSessionId, + payload: body + }) + attachSession(activeSessionId) + await connection.cancel(body as schema.CancelNotification) + pushEvent({ + kind: 'response', + action: 'cancel', + sessionId: activeSessionId, + payload: { ok: true } + }) + break + } + case 'setSessionMode': { + if (!activeSessionId) { + throw new Error('Session ID is required for setSessionMode') + } + const body = isPlainObject(request.payload) + ? { ...request.payload, sessionId: activeSessionId } + : { sessionId: activeSessionId, modeId: 'default' } + pushEvent({ + kind: 'request', + action: 'setSessionMode', + sessionId: activeSessionId, + payload: body + }) + attachSession(activeSessionId) + const response = await connection.setSessionMode(body as schema.SetSessionModeRequest) + pushEvent({ + kind: 'response', + action: 'setSessionMode', + sessionId: activeSessionId, + payload: response + }) + break + } + case 'setSessionModel': { + if (!activeSessionId) { + throw new Error('Session ID is required for setSessionModel') + } + const body = isPlainObject(request.payload) + ? { ...request.payload, sessionId: activeSessionId } + : { sessionId: activeSessionId } + pushEvent({ + kind: 'request', + action: 'setSessionModel', + sessionId: activeSessionId, + payload: body + }) + attachSession(activeSessionId) + const response = await connection.setSessionModel(body as schema.SetSessionModelRequest) + pushEvent({ + kind: 'response', + action: 'setSessionModel', + sessionId: activeSessionId, + payload: response + }) + break + } + case 'extMethod': { + const method = request.methodName?.trim() + if (!method) { + throw new Error('Custom method name is required for extMethod') + } + const body = isPlainObject(request.payload) ? request.payload : {} + pushEvent({ kind: 'request', action: `ext:${method}`, payload: body }) + const response = await connection.extMethod(method, body) + pushEvent({ + kind: 'response', + action: `ext:${method}`, + sessionId: activeSessionId, + payload: response + }) + break + } + case 'extNotification': { + const method = request.methodName?.trim() + if (!method) { + throw new Error('Custom method name is required for extNotification') + } + const body = isPlainObject(request.payload) ? request.payload : {} + pushEvent({ kind: 'request', action: `ext:${method}`, payload: body }) + await connection.extNotification(method, body) + pushEvent({ + kind: 'response', + action: `ext:${method}`, + sessionId: activeSessionId, + payload: { ok: true } + }) + break + } + default: + throw new Error(`Unsupported ACP debug action: ${request.action}`) + } + + return { + status: 'ok', + sessionId: activeSessionId, + events + } + } catch (error) { + const message = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error' + pushEvent({ + kind: 'error', + action: request.action, + sessionId: activeSessionId, + message, + payload: error instanceof Error ? { name: error.name, stack: error.stack } : error + }) + return { + status: 'error', + sessionId: activeSessionId, + error: message, + events + } + } finally { + disposeNotification?.() + disposePermission?.() + } + } + private async runPrompt( session: AcpSessionRecord, prompt: schema.ContentBlock[], @@ -703,6 +1087,17 @@ export class AcpProvider extends BaseAgentProvider< ) await session.connection.setSessionMode({ sessionId: session.sessionId, modeId }) session.currentModeId = modeId + const handle = this.processManager.getProcess(session.agentId) + if (handle && handle.boundConversationId === conversationId) { + handle.currentModeId = modeId + } + eventBus.sendToRenderer(ACP_WORKSPACE_EVENTS.SESSION_MODES_READY, SendTarget.ALL_WINDOWS, { + conversationId, + agentId: session.agentId, + workdir: session.workdir, + current: modeId, + available: session.availableModes ?? [] + }) console.info( `[ACP] Session mode successfully changed to "${modeId}" for conversation ${conversationId}` ) @@ -740,4 +1135,24 @@ export class AcpProvider extends BaseAgentProvider< return result } + + async cleanup(): Promise { + console.log('[ACP] Cleanup: shutting down ACP sessions and processes') + try { + await this.sessionManager.clearAllSessions() + } catch (error) { + console.warn('[ACP] Cleanup: failed to clear sessions:', error) + } + + try { + await this.processManager.shutdown() + } catch (error) { + console.warn('[ACP] Cleanup: failed to shutdown process manager:', error) + } + + for (const [requestId, state] of this.pendingPermissions.entries()) { + state.resolve({ outcome: { outcome: 'cancelled' } }) + this.pendingPermissions.delete(requestId) + } + } } diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts index fa3bfd4a9..02129fdaa 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts @@ -13,6 +13,14 @@ import { nanoid } from 'nanoid' import { Sandbox } from '@e2b/code-interpreter' import { RuntimeHelper } from '@/lib/runtimeHelper' +type ShellEnvironment = { + platform: 'windows' | 'mac' | 'linux' + shellName: 'powershell' | 'bash' + shellExecutable: string + buildArgs: (command: string) => string[] + promptHint: string +} + // Schema 定义 const GetTimeArgsSchema = z.object({ offset: z @@ -39,6 +47,26 @@ const RunNodeCodeArgsSchema = z.object({ .describe('Code execution timeout in milliseconds, default 5 seconds') }) +const RunShellCommandArgsSchema = z.object({ + command: z + .string() + .min(1) + .describe( + 'Shell command to execute. Provide the full command string including arguments and flags.' + ), + timeout: z + .number() + .optional() + .default(60000) + .describe('Command execution timeout in milliseconds, defaults to 60 seconds.'), + workdir: z + .string() + .optional() + .describe( + 'Optional working directory within the sandboxed temp area. If omitted, uses an isolated temporary directory under the application temp path.' + ) +}) + // E2B 代码执行 Schema const E2BRunCodeArgsSchema = z.object({ code: z @@ -72,19 +100,38 @@ const CODE_EXECUTION_FORBIDDEN_PATTERNS = [ /process\.env/gi ] +const SHELL_COMMAND_FORBIDDEN_PATTERNS = [ + /\b(sudo|su)\b/gi, + /\brm\s+-[^\n]*\brf\b/gi, + /\b(del|erase|rmdir|rd)\b\s+\/[^\n]*\b(s|q)\b/gi, + /\b(shutdown|reboot|halt|poweroff)\b/gi, + /\b(mkfs|diskpart|format)\b/gi, + /:\s*\(\s*\)\s*{\s*:\s*\|\s*:\s*&\s*}\s*;\s*:/g +] + export class PowerpackServer { private server: Server private useE2B: boolean = false + private enableShellCommandTool: boolean = false private e2bApiKey: string = '' private readonly runtimeHelper = RuntimeHelper.getInstance() + private readonly shellEnvironment: ShellEnvironment + private readonly shellWorkdir: string constructor(env?: Record) { // 从环境变量中获取 E2B 配置 this.parseE2BConfig(env) + this.parseShellCommandToolConfig(env) // 查找内置的运行时路径 this.runtimeHelper.initializeRuntimes() + // 检测当前系统的 Shell 环境 + this.shellEnvironment = this.detectShellEnvironment() + + // 确保 Shell 使用的工作目录 + this.shellWorkdir = this.ensureShellWorkdir() + // 创建服务器实例 this.server = new Server( { @@ -116,6 +163,36 @@ export class PowerpackServer { } } + private parseShellCommandToolConfig(env?: Record): void { + const enableValue = env?.ENABLE_SHELL_COMMAND_TOOL ?? process.env.ENABLE_SHELL_COMMAND_TOOL + this.enableShellCommandTool = enableValue === true || enableValue === 'true' + } + + private detectShellEnvironment(): ShellEnvironment { + if (process.platform === 'win32') { + return { + platform: 'windows', + shellName: 'powershell', + shellExecutable: 'powershell.exe', + buildArgs: (command) => ['-NoProfile', '-NonInteractive', '-Command', command], + promptHint: + 'Windows environment detected. Commands run with PowerShell, you can use built-in cmdlets and scripts.' + } + } + + const isMac = process.platform === 'darwin' + + return { + platform: isMac ? 'mac' : 'linux', + shellName: 'bash', + shellExecutable: process.env.SHELL || '/bin/bash', + buildArgs: (command) => ['-c', command], + promptHint: isMac + ? 'macOS environment detected. Commands run with bash; macOS utilities like osascript, open, defaults are available.' + : 'Linux environment detected. Commands run with bash and typical GNU utilities are available.' + } + } + // 启动服务器 public async startServer(transport: Transport): Promise { this.server.connect(transport) @@ -124,6 +201,7 @@ export class PowerpackServer { // 检查代码的安全性 private checkCodeSafety(code: string): boolean { for (const pattern of CODE_EXECUTION_FORBIDDEN_PATTERNS) { + pattern.lastIndex = 0 if (pattern.test(code)) { return false } @@ -141,12 +219,12 @@ export class PowerpackServer { // 所有平台都使用 Node.js const nodeRuntimePath = this.runtimeHelper.getNodeRuntimePath() if (!nodeRuntimePath) { - throw new Error('运行时未找到,无法执行代码') + throw new Error('Runtime not found; cannot execute code') } // 检查代码安全性 if (!this.checkCodeSafety(code)) { - throw new Error('代码包含不安全的操作,已被拒绝执行') + throw new Error('Code contains disallowed operations and was rejected') } // 创建临时文件 @@ -197,6 +275,104 @@ export class PowerpackServer { } } + private checkShellCommandSafety(command: string): void { + for (const pattern of SHELL_COMMAND_FORBIDDEN_PATTERNS) { + pattern.lastIndex = 0 + if (pattern.test(command)) { + throw new Error('Shell command contains disallowed operations and was rejected') + } + } + } + + private getRealPath(candidate: string): string { + try { + return fs.realpathSync(candidate) + } catch { + return path.resolve(candidate) + } + } + + private resolveShellCwd(workdir?: string): string { + const requestedCwd = workdir || this.shellWorkdir + const resolvedCwd = this.getRealPath(path.resolve(requestedCwd)) + const shellRoot = this.getRealPath(this.shellWorkdir) + const tempRoot = this.getRealPath(app.getPath('temp')) + + const isWithin = (child: string, parent: string) => + child === parent || child.startsWith(parent + path.sep) + + if (!isWithin(resolvedCwd, shellRoot) && !isWithin(resolvedCwd, tempRoot)) { + throw new Error(`workdir must be within sandboxed temp directories: ${shellRoot}`) + } + + if (!fs.existsSync(resolvedCwd) || !fs.statSync(resolvedCwd).isDirectory()) { + throw new Error(`workdir does not exist or is not a directory: ${resolvedCwd}`) + } + + return resolvedCwd + } + + private formatShellOutput(stdout?: string, stderr?: string, errorMessage?: string): string { + const outputParts: string[] = [] + const trimmedStdout = stdout?.trim() + const trimmedStderr = stderr?.trim() + + if (trimmedStdout) { + outputParts.push(`STDOUT:\n${trimmedStdout}`) + } + + if (trimmedStderr) { + outputParts.push(`STDERR:\n${trimmedStderr}`) + } + + if (errorMessage) { + const trimmedError = errorMessage.trim() + if (trimmedError) { + outputParts.push(`ERROR:\n${trimmedError}`) + } + } + + return outputParts.join('\n\n') || 'Command executed with no output' + } + + private async executeShellCommand( + command: string, + timeout: number, + workdir?: string + ): Promise { + this.checkShellCommandSafety(command) + const { shellExecutable, buildArgs } = this.shellEnvironment + const cwd = this.resolveShellCwd(workdir) + + try { + const { stdout, stderr } = await promisify(execFile)(shellExecutable, buildArgs(command), { + timeout, + cwd, + windowsHide: true, + maxBuffer: 10 * 1024 * 1024 + }) + + return this.formatShellOutput(stdout, stderr) + } catch (error) { + const stdout = typeof (error as any)?.stdout === 'string' ? (error as any).stdout : '' + const stderr = typeof (error as any)?.stderr === 'string' ? (error as any).stderr : '' + const errorMessage = error instanceof Error ? error.message : String(error) + throw new Error(this.formatShellOutput(stdout, stderr, errorMessage)) + } + } + + private ensureShellWorkdir(): string { + const baseTempDir = app.getPath('temp') + const shellDirPrefix = path.join(baseTempDir, 'powerpack_shell_') + + try { + return fs.mkdtempSync(shellDirPrefix) + } catch (error) { + console.error('Failed to ensure shell workdir, falling back to temp path:', error) + return baseTempDir + } + } + // 使用 E2B 执行代码 private async executeE2BCode(code: string): Promise { if (!this.useE2B) { @@ -282,6 +458,19 @@ export class PowerpackServer { } ] + if (this.enableShellCommandTool) { + const shellDescription = + `${this.shellEnvironment.promptHint} ` + + 'Use this tool for day-to-day automation, file inspection, networking, and scripting. ' + + 'Provide a full shell command string; output includes stdout and stderr. ' + + tools.push({ + name: 'run_shell_command', + description: shellDescription, + inputSchema: zodToJsonSchema(RunShellCommandArgsSchema) + }) + } + // 根据配置添加代码执行工具 if (this.useE2B) { // 使用 E2B 执行代码 @@ -327,7 +516,7 @@ export class PowerpackServer { case 'get_time': { const parsed = GetTimeArgsSchema.safeParse(args) if (!parsed.success) { - throw new Error(`无效的时间参数: ${parsed.error}`) + throw new Error(`Invalid time arguments: ${parsed.error}`) } const { offset } = parsed.data @@ -350,7 +539,7 @@ export class PowerpackServer { case 'get_web_info': { const parsed = GetWebInfoArgsSchema.safeParse(args) if (!parsed.success) { - throw new Error(`无效的URL参数: ${parsed.error}`) + throw new Error(`Invalid URL arguments: ${parsed.error}`) } const { url } = parsed.data @@ -383,6 +572,29 @@ export class PowerpackServer { } } + case 'run_shell_command': { + if (!this.enableShellCommandTool) { + throw new Error('run_shell_command tool is disabled by configuration') + } + const parsed = RunShellCommandArgsSchema.safeParse(args) + + if (!parsed.success) { + throw new Error(`Invalid command arguments: ${parsed.error}`) + } + + const { command, timeout, workdir } = parsed.data + const result = await this.executeShellCommand(command, timeout, workdir) + + return { + content: [ + { + type: 'text', + text: `Current shell environment: ${this.shellEnvironment.shellName}\n\nExecution result:\n${result}` + } + ] + } + } + case 'run_code': { // E2B 代码执行 if (!this.useE2B) { @@ -391,7 +603,7 @@ export class PowerpackServer { const parsed = E2BRunCodeArgsSchema.safeParse(args) if (!parsed.success) { - throw new Error(`无效的代码参数: ${parsed.error}`) + throw new Error(`Invalid code arguments: ${parsed.error}`) } const { code } = parsed.data @@ -401,7 +613,7 @@ export class PowerpackServer { content: [ { type: 'text', - text: `代码执行结果 (E2B Sandbox):\n\n${result}` + text: `Code execution result (E2B Sandbox):\n\n${result}` } ] } @@ -420,7 +632,7 @@ export class PowerpackServer { const parsed = RunNodeCodeArgsSchema.safeParse(args) if (!parsed.success) { - throw new Error(`无效的代码参数: ${parsed.error}`) + throw new Error(`Invalid code arguments: ${parsed.error}`) } const { code, timeout } = parsed.data @@ -430,7 +642,7 @@ export class PowerpackServer { content: [ { type: 'text', - text: `代码执行结果:\n\n${result}` + text: `Code execution result:\n\n${result}` } ] } @@ -442,7 +654,7 @@ export class PowerpackServer { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) return { - content: [{ type: 'text', text: `错误: ${errorMessage}` }], + content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true } } diff --git a/src/main/presenter/threadPresenter/handlers/llmEventHandler.ts b/src/main/presenter/threadPresenter/handlers/llmEventHandler.ts index c18723809..c220eff39 100644 --- a/src/main/presenter/threadPresenter/handlers/llmEventHandler.ts +++ b/src/main/presenter/threadPresenter/handlers/llmEventHandler.ts @@ -190,7 +190,7 @@ export class LLMEventHandler { const currentLastBlock = state.message.content[state.message.content.length - 1] if (!currentLastBlock || currentLastBlock.type !== 'reasoning_content') { this.finalizeLastBlock(state) - const reasoningStartTime = state.reasoningStartTime ?? currentTime + const reasoningStartTime = currentTime state.message.content.push({ type: 'reasoning_content', content: reasoning_content || '', @@ -207,7 +207,7 @@ export class LLMEventHandler { if (currentLastBlock.reasoning_time) { currentLastBlock.reasoning_time.end = currentTime } else { - const reasoningStartTime = state.reasoningStartTime ?? currentLastBlock.timestamp + const reasoningStartTime = currentLastBlock.timestamp ?? currentTime currentLastBlock.reasoning_time = { start: reasoningStartTime, end: currentTime diff --git a/src/main/presenter/threadPresenter/handlers/utilityHandler.ts b/src/main/presenter/threadPresenter/handlers/utilityHandler.ts index dc7f29c3b..d1852cdd0 100644 --- a/src/main/presenter/threadPresenter/handlers/utilityHandler.ts +++ b/src/main/presenter/threadPresenter/handlers/utilityHandler.ts @@ -369,7 +369,8 @@ export class UtilityHandler extends BaseHandler { } // Get provider and request preview - const provider = this.ctx.llmProviderPresenter.getProviderInstance(effectiveProviderId) + const provider = + this.ctx.llmProviderPresenter.getProviderInstance?.(effectiveProviderId) ?? null if (!provider) { throw new Error(`Provider ${effectiveProviderId} not found`) } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 268b5cc24..c33759a4a 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -762,6 +762,27 @@ export class ThreadPresenter implements IThreadPresenter { await this.llmProviderPresenter.setAcpWorkdir(conversationId, agentId, workdir) } + async warmupAcpProcess(agentId: string, workdir: string): Promise { + await this.llmProviderPresenter.warmupAcpProcess(agentId, workdir) + } + + async getAcpProcessModes( + agentId: string, + workdir: string + ): Promise< + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined + > { + return await this.llmProviderPresenter.getAcpProcessModes(agentId, workdir) + } + + async setAcpPreferredProcessMode(agentId: string, workdir: string, modeId: string) { + await this.llmProviderPresenter.setAcpPreferredProcessMode(agentId, workdir, modeId) + } + async setAcpSessionMode(conversationId: string, modeId: string): Promise { await this.llmProviderPresenter.setAcpSessionMode(conversationId, modeId) } diff --git a/src/main/presenter/threadPresenter/utils/promptBuilder.ts b/src/main/presenter/threadPresenter/utils/promptBuilder.ts index 8d4e7aba7..5ff191f76 100644 --- a/src/main/presenter/threadPresenter/utils/promptBuilder.ts +++ b/src/main/presenter/threadPresenter/utils/promptBuilder.ts @@ -445,7 +445,7 @@ function addContextMessages( contextMessages.forEach((msg) => { if (msg.role === 'user') { const msgContent = msg.content as VisionUserMessageContent - const normalizedText = getNormalizedUserMessageText(msgContent) + const finalUserContext = buildUserMessageContext(msgContent) if (vision && msgContent.images && msgContent.images.length > 0) { resultMessages.push({ role: 'user', @@ -454,13 +454,13 @@ function addContextMessages( type: 'image_url' as const, image_url: { url: image, detail: 'auto' as const } })), - { type: 'text' as const, text: normalizedText } + { type: 'text' as const, text: finalUserContext } ] }) } else { resultMessages.push({ role: 'user', - content: normalizedText + content: finalUserContext }) } } else if (msg.role === 'assistant') { @@ -532,7 +532,7 @@ function addContextMessages( contextMessages.forEach((msg) => { if (msg.role === 'user') { const msgContent = msg.content as VisionUserMessageContent - const normalizedText = getNormalizedUserMessageText(msgContent) + const finalUserContext = buildUserMessageContext(msgContent) if (vision && msgContent.images && msgContent.images.length > 0) { resultMessages.push({ role: 'user', @@ -541,13 +541,13 @@ function addContextMessages( type: 'image_url' as const, image_url: { url: image, detail: 'auto' as const } })), - { type: 'text' as const, text: normalizedText } + { type: 'text' as const, text: finalUserContext } ] }) } else { resultMessages.push({ role: 'user', - content: normalizedText + content: finalUserContext }) } } else if (msg.role === 'assistant') { @@ -589,7 +589,10 @@ function mergeConsecutiveMessages(messages: ChatMessage[]): ChatMessage[] { let allowMessagePropertiesMerge = false if (lastPushedMessage.role === currentMessage.role) { - if (currentMessage.role === 'assistant') { + // Never merge tool messages - each tool message must correspond to a specific tool_call_id + if (currentMessage.role === 'tool') { + allowMessagePropertiesMerge = false + } else if (currentMessage.role === 'assistant') { if (!lastPushedMessage.tool_calls && !currentMessage.tool_calls) { allowMessagePropertiesMerge = true } diff --git a/src/renderer/settings/App.vue b/src/renderer/settings/App.vue index adf19d78e..db26c0eb1 100644 --- a/src/renderer/settings/App.vue +++ b/src/renderer/settings/App.vue @@ -75,6 +75,7 @@ import { useSearchAssistantStore } from '@/stores/searchAssistantStore' import { useSearchEngineStore } from '@/stores/searchEngineStore' import { useMcpStore } from '@/stores/mcp' import { useMcpInstallDeeplinkHandler } from '../src/lib/storeInitializer' +import { useFontManager } from '../src/composables/useFontManager' const devicePresenter = usePresenter('devicePresenter') const windowPresenter = usePresenter('windowPresenter') @@ -82,6 +83,9 @@ const configPresenter = usePresenter('configPresenter') // Initialize stores const uiSettingsStore = useUiSettingsStore() +const { setupFontListener } = useFontManager() +setupFontListener() + const languageStore = useLanguageStore() const modelCheckStore = useModelCheckStore() const { toast } = useToast() diff --git a/src/renderer/settings/components/AcpDebugDialog.vue b/src/renderer/settings/components/AcpDebugDialog.vue new file mode 100644 index 000000000..d42e4bff2 --- /dev/null +++ b/src/renderer/settings/components/AcpDebugDialog.vue @@ -0,0 +1,585 @@ + + + diff --git a/src/renderer/settings/components/AcpSettings.vue b/src/renderer/settings/components/AcpSettings.vue index adf530f39..52e02ded1 100644 --- a/src/renderer/settings/components/AcpSettings.vue +++ b/src/renderer/settings/components/AcpSettings.vue @@ -119,9 +119,6 @@
- @@ -137,6 +134,9 @@ : t('settings.acp.initialize') }} +
@@ -224,6 +224,9 @@ : t('settings.acp.initialize') }} + @@ -268,6 +271,13 @@ :dependencies="missingDependencies" @update:open="(value) => (dependencyDialogOpen = value)" /> + + @@ -304,6 +314,7 @@ import AcpProfileDialog from './AcpProfileDialog.vue' import AcpProfileManagerDialog from './AcpProfileManagerDialog.vue' import AcpTerminalDialog from './AcpTerminalDialog.vue' import AcpDependencyDialog from './AcpDependencyDialog.vue' +import AcpDebugDialog from './AcpDebugDialog.vue' const { t } = useI18n() const { toast } = useToast() @@ -341,6 +352,11 @@ const missingDependencies = ref< requiredFor?: string[] }> >([]) +const debugDialogState = reactive({ + open: false, + agentId: '', + agentName: '' +}) const profileDialogState = reactive({ open: false, @@ -560,6 +576,12 @@ const openCustomAgentDialog = (agent?: AcpCustomAgent) => { profileDialogState.open = true } +const openDebugDialog = (agent: { id: string; name: string }) => { + debugDialogState.agentId = agent.id + debugDialogState.agentName = agent.name + debugDialogState.open = true +} + const handleProfileSave = async (payload: ProfilePayload) => { if (!profileDialogState.open) return savingProfile.value = true diff --git a/src/renderer/settings/components/DisplaySettings.vue b/src/renderer/settings/components/DisplaySettings.vue index 6b6008092..0ffbdfa59 100644 --- a/src/renderer/settings/components/DisplaySettings.vue +++ b/src/renderer/settings/components/DisplaySettings.vue @@ -2,30 +2,32 @@
-
- - - {{ t('settings.common.language') }} - -
- +
+
+ + + {{ t('settings.common.language') }} + +
+ +
@@ -212,6 +214,8 @@
+ +
{ diff --git a/src/renderer/settings/components/common/SearchAssistantModelSection.vue b/src/renderer/settings/components/common/SearchAssistantModelSection.vue index 7a4fbb2f3..1088d94e0 100644 --- a/src/renderer/settings/components/common/SearchAssistantModelSection.vue +++ b/src/renderer/settings/components/common/SearchAssistantModelSection.vue @@ -30,6 +30,7 @@ diff --git a/src/renderer/settings/components/common/WebContentLimitSetting.vue b/src/renderer/settings/components/common/WebContentLimitSetting.vue index 5aa9cbf68..ca02c3a88 100644 --- a/src/renderer/settings/components/common/WebContentLimitSetting.vue +++ b/src/renderer/settings/components/common/WebContentLimitSetting.vue @@ -43,7 +43,7 @@ - + {{ t('chat.acp.workspace.files.contextMenu.openFile') }} diff --git a/src/renderer/src/components/artifacts/CodeArtifact.vue b/src/renderer/src/components/artifacts/CodeArtifact.vue index 4ebf134d9..e71c20744 100644 --- a/src/renderer/src/components/artifacts/CodeArtifact.vue +++ b/src/renderer/src/components/artifacts/CodeArtifact.vue @@ -44,6 +44,7 @@ import { useThrottleFn } from '@vueuse/core' import { Icon } from '@iconify/vue' import { MermaidBlockNode } from 'markstream-vue' import { useArtifactStore } from '@/stores/artifact' +import { useUiSettingsStore } from '@/stores/uiSettingsStore' import { getLanguageIcon } from 'markstream-vue' import { detectLanguage, useMonaco } from 'stream-monaco' import { nanoid } from 'nanoid' @@ -63,9 +64,11 @@ const props = defineProps<{ }>() const { t } = useI18n() -const { createEditor, updateCode } = useMonaco({ +const uiSettingsStore = useUiSettingsStore() +const { createEditor, updateCode, getEditorView } = useMonaco({ wordWrap: 'on', - wrappingIndent: 'same' + wrappingIndent: 'same', + fontFamily: uiSettingsStore.formattedCodeFontFamily }) const artifactStore = useArtifactStore() const copyText = ref(t('common.copy')) @@ -89,6 +92,12 @@ const sanitizeLanguage = (language?: string | null) => { } const codeLanguage = ref(sanitizeLanguage(props.block.artifact?.language)) +const applyFontFamily = (fontFamily: string) => { + const editor = getEditorView() + if (editor) { + editor.updateOptions({ fontFamily }) + } +} // 创建节流版本的语言检测函数,1秒内最多执行一次 const throttledDetectLanguage = useThrottleFn( @@ -257,18 +266,19 @@ watch( onMounted(() => { if (!codeEditor.value) return createEditor(codeEditor.value, props.block.content, codeLanguage.value) + applyFontFamily(uiSettingsStore.formattedCodeFontFamily) }) + +watch( + () => uiSettingsStore.formattedCodeFontFamily, + (font) => { + applyFontFamily(font) + } +) diff --git a/src/renderer/src/components/artifacts/HTMLArtifact.vue b/src/renderer/src/components/artifacts/HTMLArtifact.vue index 793be09dd..6c784b53a 100644 --- a/src/renderer/src/components/artifacts/HTMLArtifact.vue +++ b/src/renderer/src/components/artifacts/HTMLArtifact.vue @@ -104,7 +104,7 @@ const setupIframe = () => { } html, body { height: 100%; - font-family: Arial, sans-serif; + font-family: var(--dc-font-family, Arial, sans-serif); } img { max-width: 100%; diff --git a/src/renderer/src/components/artifacts/MarkdownArtifact.vue b/src/renderer/src/components/artifacts/MarkdownArtifact.vue index d4c586d4d..f0123a51d 100644 --- a/src/renderer/src/components/artifacts/MarkdownArtifact.vue +++ b/src/renderer/src/components/artifacts/MarkdownArtifact.vue @@ -34,8 +34,16 @@ const handleCopyClick = () => { .markdown-content-wrapper { line-height: 1.75rem; - font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-family: var( + --dc-font-family, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif + ); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/src/renderer/src/components/artifacts/ReactTemplate.ts b/src/renderer/src/components/artifacts/ReactTemplate.ts index 0f7cb2ff2..72949026e 100644 --- a/src/renderer/src/components/artifacts/ReactTemplate.ts +++ b/src/renderer/src/components/artifacts/ReactTemplate.ts @@ -23,7 +23,7 @@ export const formatTemplate = (title: string, reactCode: string) => { } html, body { height: 100%; - font-family: Arial, sans-serif; + font-family: var(--dc-font-family, Arial, sans-serif); } img { max-width: 100%; diff --git a/src/renderer/src/components/chat-input/ChatInput.vue b/src/renderer/src/components/chat-input/ChatInput.vue index d8d8306f9..e39ec2db9 100644 --- a/src/renderer/src/components/chat-input/ChatInput.vue +++ b/src/renderer/src/components/chat-input/ChatInput.vue @@ -152,8 +152,6 @@
- -
@@ -14,7 +15,7 @@ import { useArtifactStore } from '@/stores/artifact' import { useReferenceStore } from '@/stores/reference' import { nanoid } from 'nanoid' import { useDebounceFn } from '@vueuse/core' -import { h, ref, watch } from 'vue' +import { computed, h, ref, watch } from 'vue' import NodeRenderer, { CodeBlockNode, ReferenceNode, @@ -27,12 +28,14 @@ import NodeRenderer, { import KatexWorker from 'markstream-vue/workers/katexRenderer.worker?worker&inline' import MermaidWorker from 'markstream-vue/workers/mermaidParser.worker?worker&inline' import { useThemeStore } from '@/stores/theme' +import { useUiSettingsStore } from '@/stores/uiSettingsStore' const props = defineProps<{ content: string debug?: boolean }>() const themeStore = useThemeStore() +const uiSettingsStore = useUiSettingsStore() getUseMonaco() setKaTeXWorker(new KatexWorker()) setMermaidWorker(new MermaidWorker()) @@ -45,6 +48,9 @@ const referenceStore = useReferenceStore() const threadPresenter = usePresenter('threadPresenter') const referenceNode = ref(null) const debouncedContent = ref(props.content) +const codeBlockMonacoOption = computed(() => ({ + fontFamily: uiSettingsStore.formattedCodeFontFamily +})) const updateContent = useDebounceFn( (value: string) => { @@ -93,12 +99,20 @@ setCustomComponents({ referenceStore.hideReference() } }), + mermaid: (_props) => { + // 对于 Mermaid 代码块,直接返回 MermaidNode 组件 + return h(MermaidBlockNode, { + ..._props, + isStrict: true + }) + }, code_block: (_props) => { const isMermaid = _props.node.language === 'mermaid' if (isMermaid) { // 对于 Mermaid 代码块,直接返回 MermaidNode 组件 return h(MermaidBlockNode, { - ..._props + ..._props, + isStrict: true }) } return h(CodeBlockNode, { diff --git a/src/renderer/src/components/mcp-config/components/McpJsonViewer.vue b/src/renderer/src/components/mcp-config/components/McpJsonViewer.vue index 0e5fab6b0..dc7623cdf 100644 --- a/src/renderer/src/components/mcp-config/components/McpJsonViewer.vue +++ b/src/renderer/src/components/mcp-config/components/McpJsonViewer.vue @@ -190,7 +190,7 @@ const copyToClipboard = async () => { diff --git a/src/renderer/src/components/think-content/ThinkContent.vue b/src/renderer/src/components/think-content/ThinkContent.vue index e69bc55e2..0c0d54af0 100644 --- a/src/renderer/src/components/think-content/ThinkContent.vue +++ b/src/renderer/src/components/think-content/ThinkContent.vue @@ -109,22 +109,15 @@ setCustomComponents(customId, { @apply my-1.5; } .think-prose :where(p, li, ol, ul) { - font-size: 12px; - line-height: 16px; + font-size: inherit; + line-height: inherit; letter-spacing: 0; - color: rgba(37, 37, 37, 0.5); } .think-prose :where(ol, ul) { padding-left: 1.5em; } .think-prose :where(p, li, ol, ul) :where(a) { - color: rgba(37, 37, 37, 0.6); + color: inherit; text-decoration: underline; } -.dark .think-prose :where(p, li, ol, ul) { - color: rgba(255, 255, 255, 0.5); -} -.dark .think-prose :where(p, li, ol, ul) :where(a) { - color: rgba(255, 255, 255, 0.6); -} diff --git a/src/renderer/src/components/trace/TraceDialog.vue b/src/renderer/src/components/trace/TraceDialog.vue index ecb8ac88e..931c6a674 100644 --- a/src/renderer/src/components/trace/TraceDialog.vue +++ b/src/renderer/src/components/trace/TraceDialog.vue @@ -106,16 +106,19 @@ import { Icon } from '@iconify/vue' import { useI18n } from 'vue-i18n' import { usePresenter } from '@/composables/usePresenter' import { useMonaco } from 'stream-monaco' +import { useUiSettingsStore } from '@/stores/uiSettingsStore' const { t } = useI18n() const threadPresenter = usePresenter('threadPresenter') +const uiSettingsStore = useUiSettingsStore() // Monaco Editor setup const jsonEditor = ref(null) -const { createEditor, updateCode, cleanupEditor } = useMonaco({ +const { createEditor, updateCode, cleanupEditor, getEditorView } = useMonaco({ readOnly: true, wordWrap: 'off', wrappingIndent: 'same', + fontFamily: uiSettingsStore.formattedCodeFontFamily, minimap: { enabled: false }, scrollBeyondLastLine: true, fontSize: 12, @@ -191,6 +194,12 @@ watch(isOpen, (newValue) => { // Track if editor is initialized const editorInitialized = ref(false) +const applyFontFamily = (fontFamily: string) => { + const editor = getEditorView() + if (editor) { + editor.updateOptions({ fontFamily }) + } +} // Initialize Monaco Editor when dialog opens and data is ready watch( @@ -205,6 +214,7 @@ watch( try { createEditor(editorEl, json, 'json') editorInitialized.value = true + applyFontFamily(uiSettingsStore.formattedCodeFontFamily) } catch (err) { console.error('Failed to create Monaco Editor:', err) } @@ -225,6 +235,7 @@ onMounted(async () => { try { createEditor(jsonEditor.value, formattedJson.value, 'json') editorInitialized.value = true + applyFontFamily(uiSettingsStore.formattedCodeFontFamily) } catch (err) { console.error('Failed to create Monaco Editor on mount:', err) } @@ -232,6 +243,13 @@ onMounted(async () => { } }) +watch( + () => uiSettingsStore.formattedCodeFontFamily, + (font) => { + applyFontFamily(font) + } +) + onBeforeUnmount(() => { cleanupEditor() editorInitialized.value = false diff --git a/src/renderer/src/composables/useArtifactCodeEditor.ts b/src/renderer/src/composables/useArtifactCodeEditor.ts index 464a776c1..5be3bc055 100644 --- a/src/renderer/src/composables/useArtifactCodeEditor.ts +++ b/src/renderer/src/composables/useArtifactCodeEditor.ts @@ -7,6 +7,7 @@ import { ref, watch, onBeforeUnmount } from 'vue' // === Composables === import { useMonaco, detectLanguage } from 'stream-monaco' +import { useUiSettingsStore } from '@/stores/uiSettingsStore' import { useThrottleFn } from '@vueuse/core' /** @@ -76,12 +77,20 @@ export function useArtifactCodeEditor( // === Local State === const codeLanguage = ref(normalizeLanguage(artifact.value)) + const uiSettingsStore = useUiSettingsStore() // === Monaco Integration === - const { createEditor, updateCode, cleanupEditor } = useMonaco({ + const { createEditor, updateCode, cleanupEditor, getEditorView } = useMonaco({ MAX_HEIGHT: '100%', wordWrap: 'on', - wrappingIndent: 'same' + wrappingIndent: 'same', + fontFamily: uiSettingsStore.formattedCodeFontFamily }) + const applyFontFamily = (fontFamily: string) => { + const editor = getEditorView() + if (editor) { + editor.updateOptions({ fontFamily }) + } + } // === Internal Helpers === /** @@ -159,6 +168,7 @@ export function useArtifactCodeEditor( ([editorEl, previewActive, open]) => { if (!open || previewActive || !editorEl) return void createEditor(editorEl, artifact.value?.content || '', codeLanguage.value) + applyFontFamily(uiSettingsStore.formattedCodeFontFamily) }, { flush: 'post', @@ -191,6 +201,13 @@ export function useArtifactCodeEditor( cleanupEditor() }) + watch( + () => uiSettingsStore.formattedCodeFontFamily, + (font) => { + applyFontFamily(font) + } + ) + // === Return API === return { codeLanguage diff --git a/src/renderer/src/composables/useChatConfigFields.ts b/src/renderer/src/composables/useChatConfigFields.ts index 36c82538f..3c0024422 100644 --- a/src/renderer/src/composables/useChatConfigFields.ts +++ b/src/renderer/src/composables/useChatConfigFields.ts @@ -74,7 +74,7 @@ export function useChatConfigFields(options: UseChatConfigFieldsOptions) { label: t('settings.model.temperature.label'), description: t('settings.model.temperature.description'), min: 0, - max: 1.5, + max: 2, step: 0.1, getValue: () => options.temperature.value, setValue: (val) => options.emit('update:temperature', val) diff --git a/src/renderer/src/composables/useFontManager.ts b/src/renderer/src/composables/useFontManager.ts new file mode 100644 index 000000000..032183d41 --- /dev/null +++ b/src/renderer/src/composables/useFontManager.ts @@ -0,0 +1,25 @@ +import { watch } from 'vue' +import { useUiSettingsStore } from '../stores/uiSettingsStore' + +export function useFontManager() { + const uiSettingsStore = useUiSettingsStore() + + const applyFontVariables = (textFont: string, codeFont: string) => { + document.documentElement.style.setProperty('--dc-font-family', textFont) + document.documentElement.style.setProperty('--dc-code-font-family', codeFont) + } + + const setupFontListener = () => { + watch( + [() => uiSettingsStore.formattedFontFamily, () => uiSettingsStore.formattedCodeFontFamily], + ([textFont, codeFont]) => { + applyFontVariables(textFont, codeFont) + }, + { immediate: true } + ) + } + + return { + setupFontListener + } +} diff --git a/src/renderer/src/events.ts b/src/renderer/src/events.ts index 85c1d48a3..c06ca6289 100644 --- a/src/renderer/src/events.ts +++ b/src/renderer/src/events.ts @@ -26,6 +26,8 @@ export const CONFIG_EVENTS = { SOUND_ENABLED_CHANGED: 'config:sound-enabled-changed', // 新增:声音启用状态变更事件 COPY_WITH_COT_CHANGED: 'config:copy-with-cot-enabled-changed', TRACE_DEBUG_CHANGED: 'config:trace-debug-changed', // Trace 调试功能开关变更事件 + FONT_FAMILY_CHANGED: 'config:font-family-changed', + CODE_FONT_FAMILY_CHANGED: 'config:code-font-family-changed', THEME_CHANGED: 'config:theme-changed', FONT_SIZE_CHANGED: 'config:font-size-changed', DEFAULT_SYSTEM_PROMPT_CHANGED: 'config:default-system-prompt-changed', @@ -179,3 +181,7 @@ export const ACP_WORKSPACE_EVENTS = { FILES_CHANGED: 'acp-workspace:files-changed', // File tree changed SESSION_MODES_READY: 'acp-workspace:session-modes-ready' // Session modes available } + +export const ACP_DEBUG_EVENTS = { + EVENT: 'acp-debug:event' +} diff --git a/src/renderer/src/i18n/da-DK/settings.json b/src/renderer/src/i18n/da-DK/settings.json index ca030a06e..6b60d58da 100644 --- a/src/renderer/src/i18n/da-DK/settings.json +++ b/src/renderer/src/i18n/da-DK/settings.json @@ -814,7 +814,18 @@ "text-xl": "Ekstra stor", "text-2xl": "XXL", "floatingButton": "Flydende knap", - "floatingButtonDesc": "Vis en flydende knap på skrivebordet for hurtigt at aktivere vinduet" + "floatingButtonDesc": "Vis en flydende knap på skrivebordet for hurtigt at aktivere vinduet", + "codeFontFamily": "monospace skrifttype", + "codeFontFamilyDesc": "Anvendes til kodeblokke og områder med konstant bredde.", + "fontDefaultLabel": "Standard (indbygget skrifttypestak)", + "fontFamily": "Interface skrifttype", + "fontFamilyDesc": "Vælg grænsefladens hovedskrifttype. Hvis det efterlades tomt, skal du bruge den indbyggede skrifttypestak.", + "fontReset": "nulstilles til standard", + "fontSearchEmpty": "Ingen matchende skrifttype", + "fontSearchPlaceholder": "Søg skrifttype", + "fontSystemLoading": "Indlæser systemskrifttyper...", + "fontTitle": "skrifttype", + "fontUsageHint": "Nogle grænseflader kræver genstart af applikationen for at træde i kraft. Hvis skrifttypen ikke er tilgængelig, vil den automatisk falde tilbage til standardskrifttypen." }, "shortcuts": { "title": "Indstillinger for genvejstaster", @@ -933,6 +944,48 @@ "copy": "Kopiér", "copied": "Kopieret til udklipsholder", "copyFailed": "Kunne ikke kopiere" + }, + "debug": { + "clearHistory": "Ryd historik", + "close": "Luk", + "customMethod": "Navn på tilpasset metode", + "customMethodPlaceholder": "For eksempel session/ping", + "customMethodRequired": "Udfyld venligst udvidelsesmetodens navn", + "description": "Send en ACP-anmodning til \"{name}\", og se svaret i realtid.", + "empty": "Der er ingen fejlretningsbegivenheder endnu.", + "entry": "Fejlsøg", + "eventCount": "{count} hændelser", + "eventKinds": { + "error": "fejl", + "notification": "Meddelelse", + "permission": "Tilladelse", + "request": "Anmodning", + "response": "Svar" + }, + "events": "Hændelser", + "format": "Formater JSON", + "methods": { + "cancel": "session/aflys", + "extMethod": "ext/metode", + "extNotification": "ext/meddelelse", + "initialize": "initialisere", + "loadSession": "session/belastning", + "newSession": "session/nyt", + "prompt": "session/prompt", + "setSessionMode": "session/setMode", + "setSessionModel": "session/sætModel" + }, + "needInitialize": "Udfør initialisering først", + "parseError": "JSON-parsing mislykkedes", + "payloadHint": "JSON kan justeres før afsendelse.", + "processNotReady": "Initialiser venligst først", + "processReady": "Processen er klar", + "requestFailed": "Anmodningen mislykkedes", + "resetTemplate": "Gendannelsesskabelon", + "send": "Send anmodning", + "sending": "Sender...", + "title": "ACP-fejlsøgning", + "workdirPlaceholder": "Lad tom for at bruge standard arbejdsmappe" } }, "rateLimit": { diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index f7cb503be..9b8a63682 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -807,6 +807,17 @@ "latestVersion": "Latest Version" }, "display": { + "fontTitle": "Fonts", + "fontFamily": "Interface font", + "fontFamilyDesc": "Select the primary UI font. Leave empty to use the built-in stack.", + "codeFontFamily": "Monospace font", + "codeFontFamilyDesc": "Applies to code blocks and any monospaced areas.", + "fontDefaultLabel": "Default (built-in stack)", + "fontSearchPlaceholder": "Search fonts", + "fontSearchEmpty": "No matching fonts", + "fontReset": "Reset to default", + "fontSystemLoading": "Loading system fonts...", + "fontUsageHint": "Some interfaces require restarting the application to take effect. If the font is unavailable, it will automatically fall back to the default font.", "fontSize": "Text size", "text-sm": "Small", "text-base": "Medium", @@ -933,6 +944,48 @@ "copy": "Copy", "copied": "Copied to clipboard", "copyFailed": "Failed to copy" + }, + "debug": { + "title": "ACP Debug", + "description": "Send raw ACP calls to \"{name}\" and watch the responses.", + "entry": "Debug", + "workdirPlaceholder": "Use agent default if empty", + "close": "Close", + "customMethod": "Custom method name", + "customMethodPlaceholder": "e.g. session/ping", + "payloadHint": "Edit the JSON body before sending.", + "format": "Format JSON", + "resetTemplate": "Reset template", + "clearHistory": "Clear history", + "send": "Send request", + "sending": "Sending...", + "processReady": "Process ready", + "processNotReady": "Initialize to start", + "needInitialize": "Run initialize first", + "events": "Events", + "eventCount": "{count} items", + "empty": "No debug events yet.", + "parseError": "Invalid JSON payload", + "customMethodRequired": "Provide a method name for extension calls", + "requestFailed": "Request failed", + "methods": { + "initialize": "initialize", + "newSession": "session/new", + "loadSession": "session/load", + "prompt": "session/prompt", + "cancel": "session/cancel", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel", + "extMethod": "ext/method", + "extNotification": "ext/notification" + }, + "eventKinds": { + "request": "Request", + "response": "Response", + "notification": "Notification", + "permission": "Permission", + "error": "Error" + } } }, "rateLimit": { diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 0fc4f9afd..632fd0ab4 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -814,7 +814,18 @@ "text-xl": "XL", "text-2xl": "XXL", "floatingButton": "دکمه شناور", - "floatingButtonDesc": "نمایش یک دکمه شناور بر روی دسکتاپ برای فعال‌سازی سریع پنجره برنامه" + "floatingButtonDesc": "نمایش یک دکمه شناور بر روی دسکتاپ برای فعال‌سازی سریع پنجره برنامه", + "codeFontFamily": "فونت تک فاصله", + "codeFontFamilyDesc": "برای بلوک های کد و مناطق با عرض ثابت استفاده می شود.", + "fontDefaultLabel": "پیش فرض (پشته فونت داخلی)", + "fontFamily": "فونت رابط", + "fontFamilyDesc": "فونت اصلی رابط را انتخاب کنید. اگر خالی باقی بماند، از پشته فونت داخلی استفاده کنید.", + "fontReset": "به حالت پیش فرض بازنشانی کنید", + "fontSearchEmpty": "بدون فونت منطبق", + "fontSearchPlaceholder": "جستجوی فونت", + "fontSystemLoading": "در حال بارگیری فونت های سیستم...", + "fontTitle": "فونت", + "fontUsageHint": "برخی از اینترفیس ها برای اعمال شدن نیاز به راه اندازی مجدد برنامه دارند. اگر فونت در دسترس نباشد، به طور خودکار به فونت پیش فرض برمی گردد." }, "shortcuts": { "title": "تنظیمات کلید میانبر", @@ -959,6 +970,48 @@ "downloadUrl": "لینک دانلود", "installCommands": "دستور نصب", "title": "عدم وابستگی خارجی" + }, + "debug": { + "clearHistory": "حذف تاریخچه", + "close": "بستن", + "customMethod": "نام روش سفارشی", + "customMethodPlaceholder": "به عنوان مثال جلسه / پینگ", + "customMethodRequired": "لطفاً نام روش پسوند را وارد کنید", + "description": "یک درخواست ACP به \"{name}\" ارسال کنید و پاسخ را در زمان واقعی مشاهده کنید.", + "empty": "هنوز هیچ رویداد اشکال‌زدایی وجود ندارد.", + "entry": "اشکال زدایی", + "eventCount": "{count} مورد", + "eventKinds": { + "error": "اشتباه", + "notification": "اطلاع‌رسانی", + "permission": "مجوزها", + "request": "درخواست", + "response": "پاسخ" + }, + "events": "رویداد", + "format": "فرمت JSON", + "methods": { + "cancel": "session/cancel", + "extMethod": "ext/method", + "extNotification": "ext/notification", + "initialize": "initialize", + "loadSession": "session/load", + "newSession": "session/new", + "prompt": "session/prompt", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel" + }, + "needInitialize": "لطفا ابتدا مقداردهی اولیه را اجرا کنید", + "parseError": "تجزیه JSON انجام نشد", + "payloadHint": "JSON را می توان قبل از ارسال تنظیم کرد.", + "processNotReady": "لطفا ابتدا مقداردهی اولیه کنید", + "processReady": "فرآیند آماده است", + "requestFailed": "درخواست ناموفق بود", + "resetTemplate": "قالب بازیابی", + "send": "ارسال درخواست", + "sending": "ارسال...", + "title": "اشکال زدایی ACP", + "workdirPlaceholder": "برای استفاده از دایرکتوری کاری پیش فرض، خالی بگذارید" } } } diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index ebfd842e1..f497f35cc 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -814,7 +814,18 @@ "text-xl": "XL", "text-2xl": "XXL", "floatingButton": "Bouton flottant", - "floatingButtonDesc": "Afficher un bouton flottant sur le bureau pour activer rapidement la fenêtre de l'application" + "floatingButtonDesc": "Afficher un bouton flottant sur le bureau pour activer rapidement la fenêtre de l'application", + "codeFontFamily": "police à espacement fixe", + "codeFontFamilyDesc": "Utilisé pour les blocs de code et les zones de largeur constante.", + "fontDefaultLabel": "Par défaut (pile de polices intégrée)", + "fontFamily": "Police d'interface", + "fontFamilyDesc": "Sélectionnez la police principale de l'interface. Si laissé vide, utilisez la pile de polices intégrée.", + "fontReset": "réinitialiser par défaut", + "fontSearchEmpty": "Aucune police correspondante", + "fontSearchPlaceholder": "Rechercher une police", + "fontSystemLoading": "Chargement des polices système...", + "fontTitle": "fonte", + "fontUsageHint": "Certaines interfaces nécessitent le redémarrage de l'application pour prendre effet. Si la police n'est pas disponible, elle reviendra automatiquement à la police par défaut." }, "shortcuts": { "title": "Paramètres des raccourcis clavier", @@ -959,6 +970,48 @@ "downloadUrl": "Lien de téléchargement", "installCommands": "Commande d'installation", "title": "Dépendances externes manquantes" + }, + "debug": { + "clearHistory": "enregistrement clair", + "close": "fermeture", + "customMethod": "Nom de la méthode personnalisée", + "customMethodPlaceholder": "Par exemple session/ping", + "customMethodRequired": "Veuillez remplir le nom de la méthode d'extension", + "description": "Envoyez une requête ACP à \"{name}\" et affichez la réponse en temps réel.", + "empty": "Il n'y a pas encore d'événements de débogage.", + "entry": "déboguer", + "eventCount": "{count} articles", + "eventKinds": { + "error": "erreur", + "notification": "notifier", + "permission": "Autorisations", + "request": "demander", + "response": "réponse" + }, + "events": "événement", + "format": "Formater JSON", + "methods": { + "cancel": "session/annuler", + "extMethod": "poste/méthode", + "extNotification": "poste/notification", + "initialize": "initialiser", + "loadSession": "session/chargement", + "newSession": "session/nouveau", + "prompt": "session/invite", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModèle" + }, + "needInitialize": "Veuillez d'abord exécuter initialize", + "parseError": "L'analyse JSON a échoué", + "payloadHint": "JSON peut être ajusté avant l'envoi.", + "processNotReady": "Veuillez d'abord initialiser", + "processReady": "Le processus est prêt", + "requestFailed": "La demande a échoué", + "resetTemplate": "Modèle de récupération", + "send": "Envoyer la demande", + "sending": "Envoi...", + "title": "Débogage ACP", + "workdirPlaceholder": "Laissez vide pour utiliser le répertoire de travail par défaut" } } } diff --git a/src/renderer/src/i18n/he-IL/about.json b/src/renderer/src/i18n/he-IL/about.json new file mode 100644 index 000000000..03e17c5bb --- /dev/null +++ b/src/renderer/src/i18n/he-IL/about.json @@ -0,0 +1,19 @@ +{ + "title": "DeepChat", + "description": "DeepChat הוא לקוח AI חוצה-פלטפורמות, המוקדש להנגשת הבינה המלאכותית ליותר אנשים.", + "website": "בקר באתר שלנו", + "deviceInfo": { + "title": "מידע על המכשיר", + "platform": "פלטפורמה", + "arch": "ארכיטקטורה", + "cpuModel": "דגם מעבד", + "totalMemory": "זיכרון כולל", + "osVersion": "גרסת מערכת" + }, + "disclaimerButton": "כתב ויתור", + "disclaimerTitle": "הצהרת תנאי שימוש", + "checkUpdateButton": "בדוק עדכונים", + "updateChannel": "ערוץ עדכון", + "stableChannel": "יציב (Stable)", + "canaryChannel": "קנרית (Canary)" +} diff --git a/src/renderer/src/i18n/he-IL/artifacts.json b/src/renderer/src/i18n/he-IL/artifacts.json new file mode 100644 index 000000000..28dc8fb98 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/artifacts.json @@ -0,0 +1,52 @@ +{ + "clickToOpen": "לחץ לפתיחה", + "codeSnippet": "קטע קוד", + "function": "פונקציה", + "class": "מחלקה (Class)", + "reactComponent": "רכיב React", + "moduleImport": "ייבוא מודול", + "variableDefinition": "הגדרת משתנה {name}", + "markdownDocument": "מסמך Markdown", + "htmlDocument": "מסמך HTML", + "svgImage": "תמונת SVG", + "flowchart": "תרשים זרימה", + "sequenceDiagram": "דיאגרמת רצף", + "classDiagram": "דיאגרמת מחלקות", + "stateDiagram": "דיאגרמת מצבים", + "erDiagram": "דיאגרמת ER", + "ganttChart": "תרשים גאנט", + "pieChart": "תרשים עוגה", + "mermaidDiagram": "דיאגרמת Mermaid", + "flowchartOf": "תרשים זרימה של {name}", + "sequenceDiagramBetween": "דיאגרמת רצף בין {participants}", + "classDiagramOf": "דיאגרמת מחלקות של {name}", + "stateDiagramOf": "דיאגרמת מצבים של {name}", + "erDiagramOf": "דיאגרמת ER של {name}", + "pieChartOf": "תרשים עוגה של {name}", + "unknownDocument": "מסמך לא ידוע", + "preview": "תצוגה מקדימה", + "code": "קוד", + "export": "ייצוא", + "htmlPreviewTitle": "תצוגה מקדימה של HTML", + "svgPreviewTitle": "תצוגה מקדימה של SVG", + "copy": "העתק", + "copySuccess": "הועתק", + "copySuccessDesc": "התוכן הועתק ללוח", + "copyFailed": "העתקה נכשלה", + "copyFailedDesc": "נכשל בהעתקת התוכן ללוח", + "copyAsImage": "העתק כתמונה", + "copyImageSuccessDesc": "התמונה הועתקה ללוח", + "copyImageFailedDesc": "לא ניתן היה להעתיק את התמונה ללוח.", + "desktop": "שולחן עבודה", + "tablet": "טאבלט", + "mobile": "נייד", + "responsive": "רספונסיבי", + "width": "רוחב", + "height": "גובה", + "sanitizingSvg": "מנקה תוכן SVG...", + "svgSanitizationFailed": "תוכן ה-SVG נכשל באימות אבטחה", + "noSvgContent": "אין תוכן SVG זמין", + "mermaid": { + "renderError": "עיבוד נכשל: {message}" + } +} diff --git a/src/renderer/src/i18n/he-IL/chat.json b/src/renderer/src/i18n/he-IL/chat.json new file mode 100644 index 000000000..effb8f617 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/chat.json @@ -0,0 +1,123 @@ +{ + "input": { + "placeholder": "שאל משהו? ניתן לצטט כלים, קבצים ומשאבים באמצעות {'@'}...", + "fileArea": "אזור קבצים", + "inputArea": "אזור קלט", + "functionSwitch": "מתג פונקציות", + "fileSelect": "בחר קובץ", + "pasteFiles": "הדבק קבצים מהלוח", + "dropFiles": "גרור קבצים לכאן", + "promptFilesAdded": "קבצי הנחיה (Prompt) נוספו", + "promptFilesAddedDesc": "נוספו בהצלחה {count} קבצים", + "promptFilesError": "שגיאה בעיבוד קבצים", + "promptFilesErrorDesc": "{count} קבצים נכשלו בעיבוד", + "historyPlaceholder": "(לחץ Tab להשלמה אוטומטית)", + "rateLimitQueue": "תור {count}", + "rateLimitWait": "המתן {seconds} שניות", + "rateLimitQueueTooltip": "{count} בקשות בתור, מרווח של {interval} שניות", + "rateLimitReadyTooltip": "מוכן לשליחה, מרווח של {interval} שניות", + "rateLimitWaitingTooltip": "המתן {seconds} שניות נוספות, מרווח של {interval} שניות", + "acpWorkdir": "תיקיית עבודה ACP", + "acpWorkdirTooltip": "הגדר תיקיית עבודה ל-ACP", + "acpWorkdirSelect": "בחר תיקייה שתשמש כתיקיית העבודה של ACP", + "acpWorkdirCurrent": "תיקיית עבודה נוכחית: {path}", + "acpMode": "דֶגֶם", + "acpModeTooltip": "מצב נוכחי: {mode}" + }, + "features": { + "webSearch": "חיפוש אינטרנט", + "thoughtForSeconds": "חשב במשך {seconds} שניות", + "thoughtForSecondsLoading": "חושב במשך {seconds} שניות...", + "artifactThinking": "חשיבת תוצר (Artifact Thinking)", + "modeChanged": "המצב הועבר ל: {mode}" + }, + "search": { + "results": "נמצאו {0} דפי אינטרנט", + "searching": "מחפש...", + "title": "תוצאות חיפוש", + "description": "נמצאו {0} תוצאות קשורות", + "optimizing": "ממטב את שאילתת החיפוש...", + "reading": "מחפש וקורא דפי אינטרנט...", + "error": "החיפוש נכשל" + }, + "messages": { + "thinking": "חושב...", + "rateLimitWaiting": "הגעת למגבלת הקצב, ממתין בתור...", + "rateLimitTitle": "מגבלת קצב פעילה", + "rateLimitQueue": "מיקום בתור", + "rateLimitEstimated": "המתנה משוערת", + "rateLimitQuickSettings": "התאם מגבלה", + "rateLimitSwitchProvider": "החלף ספק", + "rateLimitImmediately": "עכשיו", + "rateLimitSeconds": "שניות", + "rateLimitMinutes": "דקות" + }, + "rateLimit": { + "queueTooltip": "{count} בקשות בתור, מרווח של {interval} שניות", + "readyTooltip": "מוכן לשליחה, מרווח של {interval} שניות", + "waitingTooltip": "המתן {seconds} שניות נוספות, מרווח של {interval} שניות" + }, + "notify": { + "generationComplete": "היצירה הושלמה", + "generationError": "היצירה נכשלה" + }, + "navigation": { + "title": "ניווט בהודעות", + "searchPlaceholder": "חפש הודעות...", + "noResults": "לא נמצאו הודעות תואמות", + "noMessages": "אין הודעות עדיין", + "totalMessages": "{count} הודעות סה\"כ", + "searchResults": "נמצאו {count} תוצאות בתוך {total} הודעות", + "userMessage": "הודעת משתמש", + "assistantMessage": "תשובת עוזר", + "unknownMessage": "הודעה לא ידועה" + }, + "mcpUi": { + "title": "ממשק MCP", + "badge": "UI", + "expand": "הרחב", + "collapse": "צמצם" + }, + "toolCall": { + "title": "קריאה לכלי", + "calling": "קריאה לכלי בתהליך", + "response": "תגובת כלי", + "end": "קריאה לכלי הסתיימה", + "error": "שגיאה בקריאה לכלי", + "clickToView": "לחץ לצפייה בפרטים", + "functionName": "פונקציה", + "params": "פרמטרים", + "responseData": "נתוני תגובה" + }, + "acp": { + "workspace": { + "collapse": "לִסְגוֹר", + "files": { + "contextMenu": { + "insertPath": "הכנס לתוך תיבת הקלט", + "openFile": "לפתוח קובץ", + "revealInFolder": "פתח במנהל הקבצים" + }, + "empty": "עדיין אין קבצים", + "loading": "טוען קבצים...", + "section": "מִסְמָך" + }, + "plan": { + "empty": "עדיין אין משימות", + "section": "לְתַכְנֵן", + "status": { + "completed": "הושלם", + "failed": "לְהִכָּשֵׁל", + "in_progress": "בתהליך", + "pending": "תָלוּי וְעוֹמֵד", + "skipped": "דילג" + } + }, + "terminal": { + "empty": "עדיין אין פלט", + "section": "מָסוֹף" + }, + "title": "סביבת עבודה" + } + } +} diff --git a/src/renderer/src/i18n/he-IL/common.json b/src/renderer/src/i18n/he-IL/common.json new file mode 100644 index 000000000..87684afe2 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/common.json @@ -0,0 +1,82 @@ +{ + "enabled": "מופעל", + "disabled": "מושבת", + "loading": "טוען...", + "copySuccess": "הועתק בהצלחה", + "copySuccessDesc": "התוכן הועתק ללוח", + "copyImageSuccess": "הועתק בהצלחה", + "copyImageSuccessDesc": "התמונה הועתקה ללוח", + "copyFailed": "העתקה נכשלה", + "copyFailedDesc": "נכשל בהעתקת התוכן ללוח", + "copyCode": "העתק קוד", + "copy": "העתק", + "copied": "הועתק", + "paste": "הדבק", + "export": "ייצוא", + "newChat": "צ'אט חדש", + "newTopic": "נושא חדש", + "cancel": "ביטול", + "confirm": "אישור", + "close": "סגור", + "error": { + "requestFailed": "הבקשה נכשלה...", + "createChatFailed": "יצירת הצ'אט נכשלה", + "selectChatFailed": "בחירת הצ'אט נכשלה", + "renameChatFailed": "שינוי שם הצ'אט נכשל", + "deleteChatFailed": "מחיקת הצ'אט נכשלה", + "cleanMessagesFailed": "ניקוי ההודעות נכשל", + "userCanceledGeneration": "המשתמש ביטל את היצירה", + "sessionInterrupted": "ההפעלה הופסקה באופן בלתי צפוי, היצירה לא הושלמה", + "noModelResponse": "המודל לא החזיר תוכן, ייתכן שעבר הזמן הקצוב", + "invalidJson": "פורמט JSON לא חוקי", + "maximumToolCallsReached": "הגעת למספר המקסימלי של קריאות לכלים", + "causeOfError": "סיבות אפשריות לשגיאה:", + "error400": "בקשה שגויה, ייתכן שיש בעיה בפרמטרים או בתאימות", + "error401": "אימות נכשל, ייתכן שמפתח ה-API או הדומיין שגויים", + "error403": "הגישה למודל נאסרה, ייתכן שהיתרה אינה מספקת או שאין הרשאה", + "error404": "כתובת ה-URL המבוקשת לא נמצאה, ייתכן שהדומיין או שם המודל שגויים", + "error429": "יותר מדי בקשות, ייתכן שהשירות הגביל את הקצב (Rate Limit)", + "error500": "שגיאת שרת, ייתכן שהשירות אינו יציב, אנא נסה שוב מאוחר יותר", + "error502": "שגיאת Gateway, ייתכן שהשירות אינו יציב, אנא נסה שוב מאוחר יותר", + "error503": "השירות אינו זמין, ייתכן שהשירות אינו יציב, אנא נסה שוב מאוחר יותר", + "error504": "הבקשה הסתיימה (Timeout), ייתכן שהשירות אינו יציב או שיש בעיות רשת, אנא בדוק את הגדרות הפרוקסי ונסה שוב", + "operationFailed": "הפעולה נכשלה" + }, + "resetDataConfirmTitle": "לאפס את כל הנתונים?", + "resetDataConfirmDescription": "פעולה זו תאפס את כל הנתונים שלך להגדרות ברירת המחדל. לא ניתן לבטל פעולה זו.", + "proxyMode": "מצב פרוקסי", + "proxyModeSelect": "בחר מצב פרוקסי", + "proxyModeSystem": "פרוקסי מערכת", + "proxyModeNone": "ללא פרוקסי", + "proxyModeCustom": "פרוקסי מותאם אישית", + "customProxyUrl": "כתובת URL לפרוקסי מותאם", + "customProxyUrlPlaceholder": "דוגמה: http://127.0.0.1:7890", + "invalidProxyUrl": "כתובת פרוקסי לא חוקית, אנא הזן כתובת http/https תקינה", + "disclaimer": "כתב ויתור", + "resetData": "אפס נתונים", + "searchAssistantModel": "מודל עוזר", + "searchEngine": "מנוע חיפוש", + "searchEngineSelect": "בחר מנוע חיפוש", + "searchPreview": "תצוגה מקדימה של חיפוש", + "language": "שפה", + "languageSelect": "בחר שפה", + "selectModel": "בחר מודל", + "title": "הגדרות כלליות", + "languageSystem": "עקוב אחר המערכת", + "watermarkTip": "נוצר על ידי AI", + "collapse": "צמצם", + "expand": "הרחב", + "image": "תמונה", + "add": "הוסף", + "reset": "אפס", + "format": "פורמט", + "edit": "ערוך", + "delete": "מחק", + "save": "שמור", + "clear": "נקה", + "saved": "נשמר", + "newTab": "כרטיסייה חדשה", + "unknownError": "שגיאה לא ידועה", + "testing": "בדיקה בתהליך", + "saving": "שומר" +} diff --git a/src/renderer/src/i18n/he-IL/components.json b/src/renderer/src/i18n/he-IL/components.json new file mode 100644 index 000000000..ba813f421 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/components.json @@ -0,0 +1,41 @@ +{ + "emojiPicker": { + "search": "חפש אימוג'י", + "smileys": "סמיילים ורגשות", + "people": "אנשים וגוף", + "animals": "בעלי חיים וטבע", + "food": "אוכל ושתייה", + "travel": "נסיעות ומקומות", + "activities": "פעילויות", + "objects": "חפצים", + "symbols": "סמלים", + "flags": "דגלים" + }, + "messageBlockAction": { + "continue": "המשך", + "continued": "המשך" + }, + "messageBlockPermissionRequest": { + "title": "נדרשת הרשאה", + "allow": "אפשר", + "deny": "דחה", + "rememberChoice": "זכור בחירה זו", + "granted": "הרשאה ניתנה", + "denied": "הרשאה נדחתה", + "type": { + "read": "גישת קריאה", + "write": "גישת כתיבה", + "all": "גישה מלאה" + }, + "description": { + "read": "האם לאפשר ל-'{toolName}' מ-'{serverName}' לבצע פעולות קריאה?", + "write": "האם לאפשר ל-'{toolName}' מ-'{serverName}' לבצע פעולות כתיבה?", + "all": "האם לאפשר ל-'{toolName}' מ-'{serverName}' לבצע פעולות קריאה וכתיבה?" + } + }, + "promptParamsDialog": { + "title": "הגדרות פרמטרים עבור {name}", + "description": "אנא מלא את הפרמטרים למטה. שדות המסומנים ב-* הם שדות חובה.", + "required": "שדה זה הוא חובה." + } +} diff --git a/src/renderer/src/i18n/he-IL/contextMenu.json b/src/renderer/src/i18n/he-IL/contextMenu.json new file mode 100644 index 000000000..151bccdbc --- /dev/null +++ b/src/renderer/src/i18n/he-IL/contextMenu.json @@ -0,0 +1,17 @@ +{ + "translate": { + "title": "תרגם", + "original": "מקור", + "translated": "תרגום", + "error": "התרגום נכשל" + }, + "askAI": { + "title": "שאל את ה-AI", + "question": "שאלה", + "answer": "תשובה", + "error": "תגובת ה-AI נכשלה" + }, + "copy": "העתק", + "paste": "הדבק", + "cut": "גזור" +} diff --git a/src/renderer/src/i18n/he-IL/dialog.json b/src/renderer/src/i18n/he-IL/dialog.json new file mode 100644 index 000000000..5e6a87302 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/dialog.json @@ -0,0 +1,44 @@ +{ + "cancel": "ביטול", + "confirm": "אישור", + "close": "סגור", + "ok": "אישור", + "delete": { + "title": "האם אתה בטוח שברצונך למחוק שיחה זו?", + "description": "לא ניתן לבטל פעולה זו.", + "confirm": "מחק" + }, + "rename": { + "title": "שנה שם שיחה", + "description": "אנא הזן שם חדש לשיחה." + }, + "cleanMessages": { + "title": "נקה את כל ההודעות", + "description": "פעולה זו תמחק את כל ההודעות והקבצים בשיחה זו. האם אתה בטוח שברצונך להמשיך?", + "confirm": "נקה" + }, + "fork": { + "title": "צור ענף (Branch)", + "description": "פעולה זו תעתיק את כל ההודעות מההודעה הראשונה ועד להודעה הנוכחית לתוך שיחה חדשה בה תוכל להמשיך את הדו-שיח.", + "confirm": "צור ענף", + "tag": "ענף" + }, + "error": { + "title": "שגיאה" + }, + "mutualExclusive": { + "title": { + "reasoning": "אשר הפעלת חשיבה (Reasoning)", + "functionCall": "אשר הפעלת קריאה לפונקציות (Function Calling)" + }, + "message": { + "reasoning": "הפעלת חשיבה תשבית אוטומטית את הקריאה לפונקציות. זוהי מגבלה של מודלי DeepSeek-V3.1, ולא ניתן להשתמש בשתי התכונות בו-זמנית. להמשיך?", + "functionCall": "הפעלת קריאה לפונקציות תשבית אוטומטית את החשיבה. זוהי מגבלה של מודלי DeepSeek-V3.1, ולא ניתן להשתמש בשתי התכונות בו-זמנית. להמשיך?" + }, + "warningText": { + "reasoning": "שים לב: הפעלת חשיבה תשבית אוטומטית את הקריאה לפונקציות, שכן זו דרישה של מודלי DeepSeek-V3.1.", + "functionCall": "שים לב: הפעלת קריאה לפונקציות תשבית אוטומטית את החשיבה, שכן זו דרישה של מודלי DeepSeek-V3.1." + }, + "confirmEnable": "אשר הפעלה" + } +} diff --git a/src/renderer/src/i18n/he-IL/index.ts b/src/renderer/src/i18n/he-IL/index.ts new file mode 100644 index 000000000..9bca15c47 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/index.ts @@ -0,0 +1,58 @@ +import common from './common.json' +import update from './update.json' +import routes from './routes.json' +import chat from './chat.json' +import model from './model.json' +import thread from './thread.json' +import dialog from './dialog.json' +import settings from './settings.json' +import mcp from './mcp.json' +import welcome from './welcome.json' +import artifacts from './artifacts.json' +import sync from './sync.json' +import toolCall from './toolCall.json' +import components from './components.json' +import newThread from './newThread.json' +import about from './about.json' +import contextMenu from './contextMenu.json' +import promptSetting from './promptSetting.json' +import traceDialog from './traceDialog.json' +import plan from './plan.json' + +// Individual top-level keys +const others = { + Silicon: 'SiliconFlow', + Qiniu: 'Qiniu', + QwenLM: 'Qwen Model', + Doubao: 'Doubao', + PPIO: 'PPIO Cloud', + Moonshot: 'Moonshot AI', + DashScope: 'Alibaba Bailian', + Hunyuan: 'Hunyuan', + searchDisclaimer: + 'DeepChat הוא כלי עזר בלבד המארגן ומסכם נתונים ציבוריים המוחזרים על ידי מנועי חיפוש כאשר משתמשים יוזמים חיפושים באופן פעיל, ומסייע למשתמשים לצפות ולהבין את תוצאות החיפוש בצורה נוחה יותר.\n1. שימוש בנתונים ציבוריים\nתוכנה זו מעבדת רק נתונים הזמינים באופן ציבורי באתרי היעד או במנועי החיפוש ללא צורך בהתחברות. לפני השימוש, אנא הקפד לעיין ולציית לתנאי השירות של אתר היעד או מנוע החיפוש כדי להבטיח שהשימוש שלך חוקי ותואם לכללים.\n2. דיוק המידע ואחריות\nהתוכן המאורגן והנוצר על ידי תוכנה זו הוא לעיון בלבד ואינו מהווה כל צורה של ייעוץ משפטי, עסקי או אחר. המפתחים אינם מספקים ערובות לגבי הדיוק, השלמות, העדכניות או החוקיות של תוצאות החיפוש, וכל תוצאה הנובעת משימוש בתוכנה זו הינה באחריות המשתמש בלבד.\n3. סעיף ויתור (Disclaimer)\nתוכנה זו מסופקת "כפי שהיא" (as is), והמפתחים אינם נושאים באחריות מפורשת או משתמעת לביצועיה, יציבותה או התאמתה. במהלך השימוש בתוכנה זו, המפתחים אינם נושאים באחריות לכל מחלוקת, אובדן או חבות משפטית הנובעים מהפרות של חוקים ותקנות רלוונטיים או כללי אתר היעד.\n4. משמעת עצמית של המשתמש\nלפני השימוש בתוכנה זו, על המשתמשים להבין היטב ולאשר שהשימוש שלהם לא יפגע בזכויות קניין רוחני, סודות מסחריים או זכויות לגיטימיות אחרות של אחרים. כל מחלוקת משפטית ותוצאות הנובעות משימוש לא נאות בתוכנה זו על ידי משתמשים הינן באחריות המשתמשים בלבד.\nהשימוש בתוכנה זו מציין שהמשתמש קרא, הבין והסכים לכל תנאי כתב ויתור זה. אם יש לך שאלות, אנא התייעץ עם יועץ משפטי מקצועי.' +} + +export default { + common, + update, + routes, + chat, + model, + thread, + dialog, + settings, + mcp, + welcome, + artifacts, + sync, + toolCall, + components, + newThread, + about, + contextMenu, + promptSetting, + traceDialog, + plan, + ...others +} diff --git a/src/renderer/src/i18n/he-IL/mcp.json b/src/renderer/src/i18n/he-IL/mcp.json new file mode 100644 index 000000000..49b73af67 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/mcp.json @@ -0,0 +1,316 @@ +{ + "tools": { + "searchPlaceholder": "חפש כלים...", + "noToolsAvailable": "אין כלים זמינים", + "selectToolToDebug": "בחר כלי לניפוי באגים", + "dialogDescription": "ניפוי באגים ובדיקת כלים המסופקים על ידי שרתי MCP", + "toolsCount": "{count} כלים", + "availableTools": "כלים זמינים", + "toolList": "רשימת כלים", + "functionDescription": "תיאור פונקציה", + "invalidJson": "פורמט JSON לא חוקי", + "inputHint": "אנא הזן פרמטרים בפורמט JSON", + "required": "נדרש", + "noDescription": "אין תיאור", + "input": "ארגומנטים", + "path": "נתיב", + "pathPlaceholder": "הזן נתיב קובץ", + "searchPattern": "תבנית חיפוש", + "searchPatternPlaceholder": "הזן ביטוי רגולרי (Regex)", + "filePattern": "תבנית קובץ", + "filePatternPlaceholder": "הזן תבנית קובץ, למשל: *.md", + "executeButton": "הפעל כלי", + "resultTitle": "תוצאה", + "runningTool": "מבצע כלי...", + "loading": "טוען...", + "error": "טעינה נכשלה", + "available": "{count} זמינים", + "none": "אין כלים זמינים", + "title": "כלי MCP", + "description": "כלים המסופקים על ידי שרת ה-MCP", + "loadError": "טעינת הכלים נכשלה", + "parameters": "פרמטרים", + "refresh": "רענן", + "disabled": "MCP מושבת", + "enableToUse": "אנא הפעל את MCP כדי להשתמש בכלים", + "enabled": "הפעל את MCP", + "enabledDescription": "הפעל את פונקציונליות MCP כדי להשתמש בקריאות לכלים", + "empty": "ריק", + "jsonInputPlaceholder": "הזן את הפרמטרים בפורמט JSON", + "type": "סוג", + "annotations": "הערות (Annotations)", + "invalidJsonFormat": "פורמט ה-JSON שגוי", + "allowedValues": "ערכים מותרים", + "arrayItemValues": "ערכי פריט במערך" + }, + "addServer": "הוסף שרת", + "addServerDialog": { + "description": "הגדר שרת MCP חדש", + "title": "הוסף שרת" + }, + "confirmDelete": { + "cancel": "ביטול", + "confirm": "מחק", + "description": "האם אתה בטוח שברצונך למחוק את השרת {name}? \nלא ניתן לבטל פעולה זו.", + "title": "אשר מחיקה" + }, + "confirmRemoveServer": "האם אתה בטוח שברצונך למחוק את השרת {name}? \nלא ניתן לבטל פעולה זו.", + "default": "ברירת מחדל", + "deleteServer": "מחק שרת", + "description": "ניהול והגדרה של שרתי וכלים של MCP (Model Context Protocol)", + "editServer": "ערוך שרת", + "editServerDialog": { + "description": "ערוך תצורת שרת MCP", + "title": "ערוך שרת" + }, + "enableToAccess": "אנא הפעל את MCP כדי לגשת לאפשרויות התצורה.", + "enabledDescription": "הפעל או השבת פונקציונליות וכלים של MCP.", + "enabledTitle": "הפעל את MCP", + "isDefault": "שרת ברירת מחדל", + "noServersFound": "שרת לא נמצא", + "removeDefault": "הסר ברירת מחדל", + "removeServer": "הסר שרת", + "removeServerDialog": { + "title": "מחק שרת" + }, + "resetConfirm": "שחזר", + "resetConfirmDescription": "פעולה זו משחזרת את כל שרתי ברירת המחדל תוך שמירה על השרתים המותאמים אישית שלך. \nכל שינוי בשרת ברירת המחדל יאבד.", + "resetConfirmTitle": "שחזר שירות ברירת מחדל", + "resetToDefault": "שחזר שירות ברירת מחדל", + "running": "פועל", + "serverForm": { + "add": "הוסף", + "args": "ארגומנטים", + "argsPlaceholder": "הזן ארגומנט אחד בכל שורה", + "addArg": "הוסף ארגומנט", + "argPlaceholder": "הזן ערך ארגומנט", + "argsRequired": "הפרמטרים לא יכולים להיות ריקים", + "autoApprove": "אישור אוטומטי", + "autoApproveAll": "הכל", + "autoApproveHelp": "בחר את סוג הפעולה הדורש אישור אוטומטי ובצע ללא אישור משתמש", + "autoApproveRead": "קריאה", + "autoApproveWrite": "כתיבה", + "baseUrl": "כתובת בסיס (Base URL)", + "baseUrlPlaceholder": "הזן את כתובת הבסיס של השרת (לדוגמה: http://localhost:3000)", + "cancel": "ביטול", + "command": "פקודה", + "commandPlaceholder": "הזן פקודה", + "commandRequired": "הפקודה לא יכולה להיות ריקה", + "configImported": "ייבוא התצורה הצליח", + "description": "תיאור", + "descriptionPlaceholder": "הזן את תיאור השרת", + "descriptions": "תיאור", + "descriptionsPlaceholder": "הזן את תיאור השרת", + "env": "משתני סביבה", + "envInvalid": "משתני סביבה חייבים להיות בפורמט JSON חוקי", + "envPlaceholder": "הזן משתני סביבה בפורמט JSON", + "icon": "אייקון", + "iconPlaceholder": "הזן אייקון", + "icons": "אייקונים", + "iconsPlaceholder": "הזן אייקונים", + "jsonConfig": "תצורת JSON", + "jsonConfigExample": "דוגמה לתצורת JSON", + "jsonConfigIntro": "ניתן להדביק ישירות את תצורת ה-JSON או לבחור להגדיר את השרת ידנית.", + "jsonConfigPlaceholder": "אנא הדבק את תצורת השרת MCP בפורמט JSON", + "name": "שם השרת", + "namePlaceholder": "הזן את שם השרת", + "nameRequired": "שם השרת לא יכול להיות ריק", + "parseAndContinue": "נתח והמשך", + "parseError": "שגיאת ניתוח", + "parseSuccess": "התצורה נותחה בהצלחה", + "skipToManual": "דלג להגדרה ידנית", + "submit": "שלח", + "folders": "רשימת תיקיות", + "addFolder": "הוסף תיקייה", + "selectFolder": "בחר תיקייה", + "selectFolderError": "בחירת התיקייה נכשלה", + "noFoldersSelected": "לא נבחרו תיקיות", + "type": "סוג שרת", + "typeInMemory": "זיכרון (Memory)", + "typePlaceholder": "בחר סוג שרת", + "typeSse": "אירועים הנשלחים מהשרת (SSE)", + "typeStdio": "קלט ופלט סטנדרטי (Stdio)", + "update": "עדכן" + }, + "serverList": "רשימת שרתים", + "setAsDefault": "הגדר כשרת ברירת המחדל", + "setDefault": "הגדר כברירת מחדל", + "startServer": "התחל את השרת", + "stopServer": "עצור את השרת", + "stopped": "נעצר", + "sampling": { + "title": "דגימת בקשה מ-{server}", + "unknownServer": "שרת לא ידוע", + "description": "סקור את ההקשר המשותף על ידי שרת ה-MCP ובחר האם ליצור תגובה.", + "systemPrompt": "הנחיית מערכת (System prompt)", + "messagesTitle": "הקשר שיחה", + "preferencesTitle": "העדפות מודל", + "approve": "אשר", + "reject": "דחה", + "confirm": "שלח תגובה", + "confirming": "מאשר...", + "sendResponse": "שלח תגובה", + "selectModel": "בחר מודל", + "respondWith": "הגב באמצעות:", + "maxTokensInfo": "אורך תגובה מקסימלי: {maxTokens} טוקנים", + "visionWarning": "המודל שנבחר אינו תומך בקלט חזותי. אנא בחר מודל בעל יכולות ראייה לפני שתמשיך.", + "selectedModelLabel": "מגיב עם {model} ({provider})", + "unsupportedMessage": "סוג תוכן זה אינו נתמך.", + "noVisionModels": "לא מופעלים מודלים בעלי יכולת ראייה. הפעל מודל ראייה בהגדרות כדי להמשיך.", + "noModels": "לא מופעלים מודלים מתאימים. הפעל מודל ישים בהגדרות כדי להמשיך.", + "imageAlt": "תמונה {index}", + "unknownMime": "סוג MIME לא ידוע", + "unknownHint": "רמז ללא שם", + "autoApproving": "בקשת דגימת MCP מאת {server}", + "autoApproveIn": "מאשר אוטומטית בעוד {seconds} שניות באמצעות {model}", + "reviewRequest": "סקירה", + "sessionActive": "הפעלה פעילה - בקשות יאושרו אוטומטית", + "contentType": { + "text": "טקסט", + "image": "תמונה", + "audio": "שמע" + }, + "preference": { + "cost": "עדיפות עלות", + "speed": "עדיפות מהירות", + "intelligence": "עדיפות אינטליגנציה", + "hints": "רמזי מודל" + } + }, + "tabs": { + "servers": "שרתים", + "tools": "כלים" + }, + "title": "הגדרות MCP", + "inmemory": { + "Artifacts": { + "desc": "צור תוצרים (Artifacts) עשירים יותר ב-DeepChat", + "name": "תוצרים (Artifacts)" + }, + "bochaSearch": { + "desc": "Bocha Search API https://open.bochaai.com/", + "name": "Bocha Search" + }, + "buildInFileSystem": { + "desc": "אפשר ל-DeepChat אינטראקציה עם מערכת הקבצים המקומית.", + "name": "מערכת קבצים" + }, + "imageServer": { + "desc": "אפשר לכל מודל ב-DeepChat להבין וליצור תמונות.", + "name": "שירות תמונות" + }, + "braveSearch": { + "desc": "Brave Search API https://brave.com/search/api/", + "name": "Brave Search" + }, + "powerpack": { + "desc": "ספק למודלים שאילתות זמן, גלישה באינטרנט והרצת קוד מאובטחת.", + "name": "Power Pack" + }, + "difyKnowledge": { + "desc": "שירות חיפוש בסיס ידע של Dify, המסוגל לאחזר תוכן מתוך Dify Knowledge Base", + "name": "חיפוש בסיס ידע Dify" + }, + "ragflowKnowledge": { + "name": "חיפוש בסיס ידע RAGFlow", + "desc": "שירות חיפוש בסיס ידע של RAGFlow, יכול לחפש בתוכן בסיס הידע של RAGFlow" + }, + "fastGptKnowledge": { + "name": "חיפוש בסיס ידע FastGPT", + "desc": "שירות חיפוש בסיס ידע של FastGPT, יכול לחפש בתוכן בסיס הידע של FastGPT" + }, + "deepchat-inmemory/custom-prompts-server": { + "desc": "שירות הנחיות מותאמות אישית מובנה של DeepChat", + "name": "הנחיות מותאמות אישית" + }, + "deepchat-inmemory/deep-research-server": { + "desc": "מחקר עמוק מובנה ב-DeepChat המופעל על ידי Bocha Search. מומלץ להשתמש במודלים בעלי הקשר ארוך (Long-context).", + "name": "מחקר עמוק (DeepResearch)" + }, + "deepchat-inmemory/auto-prompting-server": { + "name": "הנחיה אוטומטית לפי תבנית", + "desc": "בחר אוטומטית את ההנחיה המותאמת הטובה ביותר בהתבסס על הקלט ומלא את התבנית בצורה חכמה." + }, + "deepchat-inmemory/conversation-search-server": { + "name": "חיפוש היסטוריית שיחות", + "desc": "חיפוש היסטוריית שיחות מובנה של DeepChat עבור צ'אטים והודעות עבר." + }, + "builtinKnowledge": { + "desc": "חיפוש בסיס ידע מובנה של DeepChat עבור מסמכים ומדריכים של DeepChat.", + "name": "חיפוש בסיס ידע מובנה" + }, + "deepchat-inmemory/meeting-server": { + "name": "פגישות מרובות סוכנים (Multi-Agent)", + "desc": "פגישות מובנות ב-DeepChat לאירוח דיונים מרובי סוכנים." + }, + "deepchat/apple-server": { + "desc": "אפשר למודלים להפעיל אפליקציות macOS כמו יומן, אנשי קשר, דואר, מפות, פתקים ותזכורות.", + "name": "עוזר מערכת macOS" + } + }, + "prompts": { + "noPromptsAvailable": "אין הנחיות (Prompts) זמינות", + "noDescription": "אין תיאור עדיין", + "selectPrompt": "פרטים עבור ההנחיה שנבחרה יוצגו כאן.", + "parameters": "פרמטרים", + "input": "פרמטרים", + "runningPrompt": "מביא הנחיה...", + "executeButton": "קבל הנחיה", + "resultTitle": "פרטי הנחיה", + "invalidJson": "פורמט JSON לא חוקי", + "parametersHint": "אנא הזן את הפרמטרים בפורמט JSON, תומך בעיצוב אוטומטי", + "resetToDefault": "אפס לפרמטרים ברירת מחדל", + "dialogDescription": "ניפוי באגים ובדיקת הנחיות המסופקות על ידי שרתי MCP" + }, + "resources": { + "noResourcesAvailable": "אין משאבים זמינים", + "selectResource": "בחר משאב לצפייה בתוכנו.", + "loading": "טוען...", + "loadContent": "טען תוכן", + "pleaseSelect": "לחץ לצפייה בפרטי המשאב.", + "dialogDescription": "עיין וצפה במשאבים המסופקים על ידי שרתי MCP" + }, + "errors": { + "loadConfigFailed": "טעינת תצורת MCP נכשלה", + "setEnabledFailed": "הגדרת מצב הפעלה של MCP נכשלה", + "getServerStatusFailed": "קבלת סטטוס עבור השרת {serverName} נכשלה", + "addServerFailed": "הוספת השרת נכשלה", + "updateServerFailed": "עדכון השרת נכשל", + "removeServerFailed": "הסרת השרת נכשלה", + "maxDefaultServersReached": "הגעת למספר המקסימלי של שרתי ברירת מחדל (30)", + "toggleDefaultServerFailed": "שינוי סטטוס שרת ברירת המחדל נכשל", + "resetToDefaultFailed": "שחזור לשרתי ברירת מחדל נכשל", + "toggleServerFailed": "שינוי מצב השרת {serverName} נכשל", + "loadToolsFailed": "טעינת הכלים נכשלה", + "loadPromptsFailed": "טעינת ההנחיות נכשלה", + "loadResourcesFailed": "טעינת המשאבים נכשלה", + "callToolFailed": "הקריאה לכלי {toolName} נכשלה", + "toolCallError": "שגיאה בקריאה לכלי: {error}", + "mcpDisabled": "MCP מושבת", + "getPromptFailed": "קבלת ההנחיה נכשלה", + "readResourceFailed": "קריאת המשאב נכשלה", + "promptNotFound": "הנחיה '{name}' לא נמצאה", + "emptyPromptContent": "להנחיה '{name}' אין תוכן", + "missingParameters": "חסרים פרמטרים נדרשים: {params}", + "invalidParameters": "פרמטרים לא חוקיים עבור: {params}" + }, + "market": { + "browseBuiltin": "עיין בחנות ה-MCP המובנית", + "builtinTitle": "חנות MCP", + "poweredBy": "מופעל על ידי MCPRouter", + "keyGuide": "קבל מפתח API", + "keyHelpText": "אנא עבור אל", + "keyHelpEnd": "כדי להגיש בקשה למפתח API ומלא אותו בתיבת הקלט למעלה", + "apiKeyPlaceholder": "הזן מפתח API של MCPRouter", + "apiKeyRequiredTitle": "נדרש מפתח API", + "apiKeyRequiredDesc": "אנא מלא את מפתח ה-API של MCPRouter לפני ההתקנה", + "install": "התקן", + "installed": "מותקן", + "installSuccess": "ההתקנה הצליחה", + "installFailed": "ההתקנה נכשלה", + "noMore": "אין עוד", + "empty": "אין שירותים", + "loadMore": "טען עוד", + "pullDownToLoad": "המשך למשוך למטה כדי לטעון עוד" + } +} diff --git a/src/renderer/src/i18n/he-IL/model.json b/src/renderer/src/i18n/he-IL/model.json new file mode 100644 index 000000000..f3d32b26c --- /dev/null +++ b/src/renderer/src/i18n/he-IL/model.json @@ -0,0 +1,30 @@ +{ + "search": { + "placeholder": "חפש מודלים..." + }, + "error": { + "loadFailed": "טעינת המודלים נכשלה" + }, + "type": { + "custom": "מודל מותאם אישית", + "official": "מודל רשמי" + }, + "add": { + "namePlaceholder": "שם המודל", + "idPlaceholder": "מזהה מודל (ID)", + "contextLengthPlaceholder": "אורך הקשר (Context)", + "maxTokensPlaceholder": "מקסימום טוקנים" + }, + "actions": { + "add": "הוסף מודל", + "enableAll": "הפעל הכל", + "disableAll": "השבת הכל" + }, + "tags": { + "reasoning": "חשיבה (Reasoning)", + "chat": "צ'אט", + "code": "קוד", + "writing": "כתיבה", + "analysis": "ניתוח" + } +} diff --git a/src/renderer/src/i18n/he-IL/newThread.json b/src/renderer/src/i18n/he-IL/newThread.json new file mode 100644 index 000000000..0c5c81044 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/newThread.json @@ -0,0 +1,4 @@ +{ + "greeting": "שלום!", + "prompt": "על מה תרצה לדבר?" +} diff --git a/src/renderer/src/i18n/he-IL/plan.json b/src/renderer/src/i18n/he-IL/plan.json new file mode 100644 index 000000000..d46fe794b --- /dev/null +++ b/src/renderer/src/i18n/he-IL/plan.json @@ -0,0 +1,4 @@ +{ + "completed": "הושלם", + "title": "תכנון משימה" +} diff --git a/src/renderer/src/i18n/he-IL/promptSetting.json b/src/renderer/src/i18n/he-IL/promptSetting.json new file mode 100644 index 000000000..287113a5c --- /dev/null +++ b/src/renderer/src/i18n/he-IL/promptSetting.json @@ -0,0 +1,95 @@ +{ + "title": "ניהול הנחיות (Prompt Management)", + "addTitle": "הוסף הנחיה", + "addDescription": "צור תבנית הנחיה מותאמת אישית חדשה", + "editTitle": "ערוך הנחיה", + "editDescription": "שנה את תבנית ההנחיה שנבחרה", + "name": "שם", + "namePlaceholder": "הזן שם עבור ההנחיה שלך", + "description": "תיאור", + "descriptionPlaceholder": "אנא הזן תיאור (אופציונלי)", + "promptContent": "הנחיה (Prompt)", + "contentPlaceholder": "אנא הזן את תוכן ההנחיה", + "basicInfo": "מידע בסיסי", + "contentTip": "תומך במצייני מקום של משתנים כמו {openBrace}{openBrace}variable{closeBrace}{closeBrace}, ניתן להגדיר פרמטרים מתאימים בסעיף הפרמטרים", + "noPrompt": "אין הנחיות עדיין", + "noPromptDesc": "לחץ על כפתור \"+\" בפינה העליונה כדי ליצור את ההנחיה הראשונה שלך.", + "active": "פעיל", + "noDescription": "אין תיאור", + "customDate": "מותאם אישית", + "showMore": "הצג עוד", + "showLess": "הצג פחות", + "export": "ייצוא", + "import": "ייבוא", + "exportSuccess": "הייצוא הושלם בהצלחה", + "exportFailed": "הייצוא נכשל", + "importSuccess": "הייבוא הושלם בהצלחה", + "importFailed": "הייבוא נכשל", + "importStats": "נוספו {added}, עודכנו {updated} הנחיות", + "parameters": "פרמטרים", + "addParameter": "הוסף פרמטר", + "noParameters": "אין פרמטרים", + "noParametersDesc": "לחץ על הכפתור למעלה כדי להוסיף פרמטרים, שבהם ניתן להשתמש ליצירת מצייני מקום למשתנים בהנחיות", + "parameterName": "שם הפרמטר", + "parameterDescription": "תיאור הפרמטר", + "parameterNamePlaceholder": "אנא הזן שם פרמטר", + "parameterDescriptionPlaceholder": "אנא הזן תיאור פרמטר", + "required": "נדרש", + "characters": "תווים", + "fileManagement": "ניהול קבצים", + "uploadFromDevice": "העלה מהמכשיר", + "uploadFromDeviceDesc": "תומך בטקסט, מסמכים, CSV ועוד.", + "uploadedFiles": "קבצים שהועלו", + "noFiles": "אין קבצים", + "noFilesUploadDesc": "לחץ למעלה להעלאת קבצים", + "uploadSuccess": "ההעלאה הושלמה", + "uploadedCount": "הועלו {count} קבצים", + "confirmDelete": "האם אתה בטוח שברצונך למחוק את ההנחיה \"{name}\"?", + "confirmDeleteDescription": "לא ניתן לבטל פעולה זו. ההנחיה תימחק לצמיתות.", + "confirmDeleteSystemPrompt": "האם אתה בטוח שברצונך למחוק את הנחיית המערכת \"{name}\"?", + "confirmDeleteSystemPromptDescription": "לא ניתן לבטל פעולה זו. הנחיית המערכת תימחק לצמיתות.", + "deleteSuccess": "ההנחיה נמחקה", + "deleteFailed": "מחיקת ההנחיה נכשלה", + "inactive": "לא פעיל", + "clickToEnable": "לחץ להפעלה", + "clickToDisable": "לחץ להשבתה", + "enableSuccess": "הנחיה הופעלה", + "disableSuccess": "הנחיה הושבתה", + "toggleFailed": "שינוי הסטטוס נכשל", + "enablePrompt": "הפעל הנחיה זו", + "sourceLocal": "מקומי", + "sourceImported": "מיובא", + "sourceBuiltin": "מובנה", + "defaultSystemPrompt": "הנחיית מערכת ברירת מחדל", + "defaultSystemPromptPlaceholder": "הזן את הנחיית המערכת שתשמש כברירת מחדל לכל השיחות החדשות...", + "defaultSystemPromptDescription": "הנחיה זו תחול על כל השיחות החדשות. ניתן לשנות אותה בעת יצירת שיחה. שים לב שהגדרה זו תיכנס לתוקף בפעם הבאה שתיצור שיחה חדשה.", + "typing": "מקליד...", + "saving": "שומר...", + "saved": "נשמר", + "saveDefaultPromptFailed": "שמירת הנחיית מערכת ברירת המחדל נכשלה", + "systemPrompts": "הנחיות מערכת", + "customPrompts": "הנחיות מותאמות אישית", + "addSystemPrompt": "הוסף הנחיית מערכת", + "addCustomPrompt": "הוסף הנחיה מותאמת אישית", + "editSystemPrompt": "ערוך הנחיית מערכת", + "addSystemPromptDesc": "צור הנחיית מערכת חדשה", + "editSystemPromptDesc": "שנה את הנחיית המערכת שנבחרה", + "selectSystemPrompt": "בחר הנחיית מערכת", + "systemPromptDescription": "הנחיית המערכת שנבחרה תשמש כברירת מחדל לשיחות חדשות", + "emptySystemPromptOption": "ללא הנחיית מערכת", + "emptySystemPromptDescription": "כאשר אפשרות זו נבחרה, שיחות חדשות יתחילו ללא הנחיית מערכת.", + "preview": "תצוגה מקדימה", + "systemPromptChanged": "הנחיית המערכת שונתה בהצלחה", + "systemPromptChangeFailed": "שינוי הנחיית המערכת נכשל", + "systemPromptAdded": "הנחיית מערכת נוספה בהצלחה", + "systemPromptAddedAndSwitched": "הנחיית מערכת נוספה והוגדרה אוטומטית כברירת מחדל", + "systemPromptUpdated": "הנחיית מערכת עודכנה בהצלחה", + "systemPromptSaveFailed": "שמירת הנחיית המערכת נכשלה", + "systemPromptDeleted": "הנחיית המערכת נמחקה בהצלחה", + "systemPromptDeleteFailed": "מחיקת הנחיית המערכת נכשלה", + "systemPromptEditTip": "שמירה אוטומטית בעת אובדן פוקוס לאחר עריכה", + "resetToDefault": "אפס לברירת מחדל", + "resetToDefaultSuccess": "אופס לתוכן ברירת המחדל בהצלחה", + "resetToDefaultFailed": "האיפוס לברירת המחדל נכשל", + "parameterRequired": "פרמטרים נדרשים" +} diff --git a/src/renderer/src/i18n/he-IL/routes.json b/src/renderer/src/i18n/he-IL/routes.json new file mode 100644 index 000000000..fa3db69b0 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/routes.json @@ -0,0 +1,17 @@ +{ + "chat": "צ'אט", + "welcome": "ברוכים הבאים", + "playground": "אזור ניסויים (Playground)", + "settings": "הגדרות", + "settings-common": "הגדרות כלליות", + "settings-provider": "ספקים", + "settings-mcp": "הגדרות MCP", + "settings-database": "נתונים", + "settings-about": "אודות", + "settings-shortcut": "קיצורי מקשים", + "settings-display": "תצוגה", + "settings-knowledge-base": "בסיס ידע", + "settings-prompt": "הנחיות (Prompts)", + "settings-mcp-market": "חנות MCP", + "settings-acp": "סוכני ACP" +} diff --git a/src/renderer/src/i18n/he-IL/settings.json b/src/renderer/src/i18n/he-IL/settings.json new file mode 100644 index 000000000..300cd9c9c --- /dev/null +++ b/src/renderer/src/i18n/he-IL/settings.json @@ -0,0 +1,1017 @@ +{ + "title": "הגדרות", + "common": { + "title": "הגדרות כלליות", + "resetData": "אפס נתונים", + "language": "שפה", + "languageSelect": "בחר שפה", + "searchEngine": "מנוע חיפוש", + "searchEngineSelect": "בחר מנוע חיפוש", + "searchPreview": "תצוגה מקדימה של חיפוש", + "searchAssistantModel": "מודל עוזר", + "selectModel": "בחר מודל", + "proxyMode": "מצב פרוקסי", + "proxyModeSelect": "בחר מצב פרוקסי", + "proxyModeSystem": "פרוקסי מערכת", + "proxyModeNone": "ללא פרוקסי", + "proxyModeCustom": "פרוקסי מותאם אישית", + "customProxyUrl": "כתובת URL לפרוקסי מותאם", + "customProxyUrlPlaceholder": "דוגמה: http://127.0.0.1:7890", + "invalidProxyUrl": "כתובת פרוקסי לא חוקית, אנא הזן כתובת http/https תקינה", + "addCustomSearchEngine": "הוסף מנוע חיפוש מותאם", + "addCustomSearchEngineDesc": "הוסף מנוע חיפוש חדש על ידי מתן שם וכתובת URL לחיפוש. הכתובת חייבת לכלול את {query} כמציין מקום לשאילתה.", + "searchEngineName": "שם מנוע החיפוש", + "searchEngineNamePlaceholder": "הזן את שם מנוע החיפוש", + "searchEngineUrl": "כתובת URL לחיפוש", + "searchEngineUrlPlaceholder": "דוגמה: https://a.com/search?q={'{'}query{'}'}", + "searchEngineUrlError": "הכתובת חייבת לכלול את {'{'}query{'}'} כמציין מקום לשאילתה", + "deleteCustomSearchEngine": "מחק מנוע חיפוש מותאם", + "deleteCustomSearchEngineDesc": "האם אתה בטוח שברצונך למחוק את מנוע החיפוש המותאם \"{name}\"? לא ניתן לבטל פעולה זו.", + "testSearchEngine": "בדוק מנוע חיפוש", + "testSearchEngineDesc": "חיפוש בדיקה עבור \"weather\" יבוצע באמצעות מנוע החיפוש {engine}.", + "testSearchEngineNote": "אם דף החיפוש דורש התחברות או פעולות אחרות, ניתן לבצע אותן בחלון הבדיקה. אנא סגור את חלון הבדיקה בסיום.", + "theme": "ערכת נושא", + "themeSelect": "בחר ערכת נושא", + "themeLight": "בהיר", + "themeDark": "כהה", + "themeSystem": "עקוב אחר המערכת", + "closeToQuit": "צא מהאפליקציה בעת סגירת החלון", + "contentProtectionDialogTitle": "אשר שינוי הגנת מסך", + "contentProtectionEnableDesc": "מנע מיישומי שיתוף מסך ללכוד את חלון DeepChat כדי לעזור להגן על פרטיותך. לא כל האפליקציות מכבדות הגדרה זו; בסביבות מסוימות עשוי להופיע חלון שחור.", + "contentProtectionDisableDesc": "אפשר ליישומי שיתוף מסך ללכוד את חלון DeepChat.", + "contentProtectionRestartNotice": "שינוי הגדרה זו יגרום להפעלה מחדש של האפליקציה. האם להמשיך?", + "soundEnabled": "הפעל אפקטים קוליים", + "copyWithCotEnabled": "העתק פרטי COT (שרשרת מחשבה)", + "traceDebugEnabled": "מעקב אחר קריאה (Trace Call)", + "loggingEnabled": "הפעל רישום לוגים", + "loggingDialogTitle": "אשר שינוי הגדרת רישום לוגים", + "loggingEnableDesc": "הפעלת רישום לוגים תעזור לנו לאבחן בעיות ולשפר את האפליקציה. קבצי הלוג עשויים להכיל מידע רגיש.", + "loggingDisableDesc": "השבתת רישום לוגים תפסיק את איסוף יומני האפליקציה.", + "webContentLengthLimit": "מגבלת אורך תוכן אינטרנט", + "webContentLengthLimitHint": "(הגדר ל-0 ללא הגבלה)", + "charactersUnit": "תווים", + "webContentLengthLimitTooltip": "הגדר את האורך המקסימלי של טקסט המופק מדפי אינטרנט (מספר תווים), טווח: 0-50000. הגדרה ל-0 משמעותה ללא הגבלה ושליפת תוכן האינטרנט המלא; ערכים גבוהים יותר מספקים תוכן שלם יותר אך עשויים להגדיל את זמן העיבוד ואת השימוש בטוקנים.", + "loggingRestartNotice": "שינוי הגדרה זו יגרום להפעלה מחדש של האפליקציה. האם להמשיך?", + "openLogFolder": "פתח תיקיית לוגים", + "shortcut": { + "newChat": "צור צ'אט חדש", + "title": "הגדרות קיצורי מקשים" + }, + "notifications": "התראות מערכת", + "notificationsDesc": "כאשר DeepChat אינו בחזית, אם נוצרת תשובה, תישלח התראת מערכת", + "contentProtection": "הגנה מפני לכידת מסך", + "fileMaxSize": "גודל קובץ מקסימלי", + "fileMaxSizeHint": "מגביל את הגודל המקסימלי של קובץ בודד המועלה" + }, + "data": { + "title": "הגדרות נתונים", + "syncEnable": "הפעל סנכרון נתונים", + "syncFolder": "תיקיית סנכרון", + "openSyncFolder": "פתח תיקיית סנכרון", + "lastSyncTime": "זמן סנכרון אחרון", + "never": "מעולם לא", + "startBackup": "גבה כעת", + "backingUp": "מגבה...", + "importData": "ייבוא נתונים", + "incrementImport": "ייבוא מצטבר (Incremental)", + "overwriteImport": "ייבוא דורס (Overwrite)", + "backupSelectLabel": "בחר גיבוי", + "backupSelectDescription": "בחר את תמונת המצב של הגיבוי לייבוא.", + "selectBackupPlaceholder": "בחר גיבוי", + "noBackupsAvailable": "אין גיבויים זמינים. בצע גיבוי תחילה.", + "importConfirmTitle": "אשר ייבוא נתונים", + "importConfirmDescription": "הייבוא ידרוס את כל הנתונים הנוכחיים, כולל היסטוריית צ'אט והגדרות. ודא שגיבית נתונים חשובים. יהיה עליך להפעיל מחדש את האפליקציה לאחר הייבוא.", + "importing": "מייבא...", + "confirmImport": "אשר ייבוא", + "importSuccessTitle": "הייבוא הושלם בהצלחה", + "importErrorTitle": "הייבוא נכשל", + "resetData": "אפס נתונים", + "resetConfirmTitle": "אשר איפוס נתונים", + "resetConfirmDescription": "אנא בחר את סוג הנתונים לאיפוס. לא ניתן לבטל פעולה זו והאפליקציה תופעל מחדש אוטומטית לאחר האיפוס.", + "resetChatData": "אפס נתוני צ'אט", + "resetChatDataDesc": "מחק את כל היסטוריית הצ'אט ורשומות השיחות", + "resetKnowledgeData": "אפס נתוני בסיס ידע", + "resetKnowledgeDataDesc": "מחק את כל קבצי בסיס הידע ונתוני הוקטורים", + "resetConfig": "אפס תצורה", + "resetConfigDesc": "מחק את כל הגדרות האפליקציה, תצורות המודלים והנחיות מותאמות אישית", + "resetAll": "איפוס מלא", + "resetAllDesc": "מחק את כל הנתונים כולל היסטוריית צ'אט, תצורות וקבצי מטמון", + "resetting": "מאפס...", + "confirmReset": "אשר איפוס", + "resetCompleteDevTitle": "איפוס הנתונים הושלם", + "resetCompleteDevMessage": "אנא הפעל מחדש ידנית את האפליקציה במצב פיתוח. עצור את התהליך הנוכחי והרץ שוב pnpm run dev", + "toast": { + "backupSuccessTitle": "גיבוי הושלם בהצלחה", + "backupSuccessMessage": "הנתונים גובו בהצלחה" + } + }, + "model": { + "title": "הגדרות מודל", + "systemPrompt": { + "label": "הנחיית מערכת", + "placeholder": "אנא הזן את הנחיית המערכת...", + "description": "הגדר את הנחיית המערכת עבור עוזר ה-AI כדי להגדיר את התנהגותו ותפקידו" + }, + "temperature": { + "label": "טמפרטורת מודל", + "description": "שולט באקראיות של הפלט; ערכים גבוהים יותר מייצרים תשובות יצירתיות יותר" + }, + "contextLength": { + "label": "אורך הקשר (Context)", + "description": "הגדר את האורך המקסימלי של הקשר השיחה" + }, + "responseLength": { + "label": "אורך תגובה", + "description": "הגדר את האורך המקסימלי של תגובת ה-AI" + }, + "artifacts": { + "description": "הפעלת תכונת ה-Artifacts (תוצרים) מאפשרת ל-AI ליצור תוכן עשיר יותר", + "title": "תוצרים (Artifacts)" + }, + "addModel": "הוסף מודל", + "configureModel": "הגדר מודל", + "modelList": "רשימת מודלים", + "provider": "ספק שירות", + "providerSetting": "הגדרות ספק שירות", + "selectModel": "בחר מודל", + "modelConfig": { + "cancel": "ביטול", + "contextLength": { + "description": "הגדר את אורך ההקשר שהמודל יכול לטפל בו", + "label": "אורך הקשר" + }, + "description": "שים לב שתצורה זו תקפה רק למודל הנוכחי ולא תשפיע על מודלים אחרים. אנא שנה אותה בזהירות. פרמטרים שגויים עלולים לגרום למודל לא לפעול כראוי.", + "name": { + "label": "שם המודל", + "description": "השם המוצג של המודל", + "placeholder": "הזן שם מודל", + "required": "שם המודל נדרש", + "readonly": "שם המודל (קריאה בלבד)" + }, + "id": { + "label": "מזהה מודל (ID)", + "description": "המזהה הייחודי של המודל", + "placeholder": "הזן מזהה מודל", + "required": "מזהה מודל נדרש", + "readonly": "מזהה מודל (קריאה בלבד)", + "duplicate": "מודל עם מזהה זה כבר קיים" + }, + "createTitle": "הוסף מודל מותאם אישית", + "editTitle": "ערוך תצורת מודל - {name}", + "functionCall": { + "description": "האם המודל תומך בקריאות לפונקציות באופן טבעי (DeepChat יסמלץ אוטומטית קריאות לפונקציות לאחר כיבוי אפשרות זו)", + "label": "קריאות לפונקציות" + }, + "maxTokens": { + "description": "הגדר את המספר המקסימלי של טוקנים לפלט יחיד של המודל", + "label": "אורך פלט מקסימלי" + }, + "reasoning": { + "description": "האם המודל תומך ביכולת חשיבה (Reasoning)?", + "label": "יכולת חשיבה" + }, + "enableSearch": { + "label": "חיפוש אינטרנט", + "description": "אפשר למודל לחפש באינטרנט מידע עדכני" + }, + "forcedSearch": { + "label": "חיפוש כפוי", + "description": "כפה על המודל לבצע חיפוש באינטרנט; כשמושבת, המודל מחליט אוטומטית" + }, + "searchStrategy": { + "label": "אסטרטגיית חיפוש", + "description": "בחר מצב ביצועי חיפוש: turbo מאזן מהירות ויעילות, max מספק תוצאות חיפוש אופטימליות", + "placeholder": "בחר אסטרטגיית חיפוש", + "options": { + "turbo": "Turbo - איזון בין מהירות תגובה ליעילות חיפוש (מומלץ)", + "max": "Max - מצב ביצועים גבוהים, מספק תוצאות חיפוש אופטימליות" + } + }, + "thinkingBudget": { + "label": "תקציב חשיבה", + "description": "הגדר את אורך הטוקנים המקסימלי לתהליך החשיבה כדי לשלוט בעומק החשיבה", + "placeholder": "הזן ערך תקציב חשיבה", + "range": "טווח: {min} - {max}", + "validation": { + "required": "אנא הזן ערך תקציב חשיבה", + "minValue": "ערך תקציב החשיבה נמוך מהמינימום", + "maxValue": "ערך תקציב החשיבה אינו יכול לעלות על {max}" + } + }, + "resetConfirm": { + "confirm": "אשר איפוס", + "message": "האם אתה בטוח שברצונך לאפס את תצורת המודל הזה לברירת המחדל? \nפעולה זו בלתי הפיכה.", + "title": "אשר איפוס" + }, + "reasoningEffort": { + "label": "מאמץ חשיבה", + "description": "שולט בעומק החשיבה של המודל; מאמץ גבוה יותר מפיק תוצאות טובות יותר אך תגובות איטיות יותר", + "placeholder": "בחר מאמץ חשיבה", + "options": { + "minimal": "מינימלי - התגובה המהירה ביותר", + "low": "נמוך - מאמץ נמוך", + "medium": "בינוני - מאמץ בינוני", + "high": "גבוה - מאמץ גבוה" + } + }, + "verbosity": { + "label": "רמת פירוט", + "description": "שולט ברמת הפרטים ואורך תשובות המודל", + "placeholder": "בחר רמת פירוט", + "options": { + "low": "נמוכה - תשובות תמציתיות", + "medium": "בינונית - פירוט מאוזן", + "high": "גבוהה - תשובות מפורטות" + } + }, + "resetToDefault": "אפס לברירת מחדל", + "saveConfig": "שמור תצורה", + "useModelDefault": "השתמש בתצורת ברירת המחדל של המודל", + "currentUsingModelDefault": "משתמש כעת בתצורת ברירת המחדל של המודל", + "temperature": { + "description": "שלוט באקראיות הפלט. רוב המודלים הם 0-1, וחלקם תומכים בין 0-2. ערכים גבוהים יותר מגבירים את האקראיות.", + "label": "טמפרטורה" + }, + "title": "פרמטרים מותאמים למודל", + "type": { + "description": "בחר את סוג המודל", + "label": "סוג מודל", + "options": { + "chat": "מודל שפה (Chat)", + "embedding": "מודל הטמעה (Embedding)", + "imageGeneration": "מודל יצירת תמונות", + "rerank": "מודל דירוג מחדש (Rerank)" + } + }, + "validation": { + "contextLengthMax": "אורך ההקשר אינו יכול לעלות על 10000000", + "contextLengthMin": "אורך ההקשר חייב להיות גדול מ-0", + "contextLengthRequired": "אורך ההקשר אינו יכול להיות ריק", + "maxTokensMax": "אורך הפלט המקסימלי אינו יכול לעלות על 1000000", + "maxTokensMin": "אורך הפלט המקסימלי חייב להיות גדול מ-0", + "maxTokensRequired": "אורך הפלט המקסימלי אינו יכול להיות ריק", + "temperatureMax": "הטמפרטורה חייבת להיות קטנה או שווה ל-2", + "temperatureMin": "הטמפרטורה חייבת להיות גדולה או שווה ל-0", + "temperatureRequired": "הטמפרטורה אינה יכולה להיות ריקה" + }, + "vision": { + "description": "האם המודל תומך ביכולת חזותית?", + "label": "יכולת חזותית" + }, + "searchLimit": { + "label": "מגבלת חיפוש", + "description": "מגבלת מודל: הפעלת חיפוש באינטרנט תשבית קריאות לפונקציות כלים" + } + } + }, + "provider": { + "search": "חפש פלטפורמות ספקים...", + "enable": "הפעל שירות", + "enabled": "מופעל", + "disabled": "מושבת", + "urlPlaceholder": "אנא הזן כתובת API", + "keyPlaceholder": "אנא הזן מפתח API", + "accessKeyIdPlaceholder": "אנא הזן AWS Access Key ID", + "secretAccessKeyPlaceholder": "אנא הזן AWS Secret Access Key", + "regionPlaceholder": "אנא הזן אזור AWS", + "vertexProjectId": "מזהה פרויקט (Project ID)", + "vertexProjectIdPlaceholder": "אנא הזן את מזהה הפרויקט שלך ב-Google Cloud", + "vertexLocation": "אזור", + "vertexLocationPlaceholder": "אנא הזן אזור (למשל us-central1)", + "vertexServiceEmail": "אימייל חשבון שירות", + "vertexServiceEmailPlaceholder": "אנא הזן את אימייל חשבון השירות", + "vertexPrivateKey": "מפתח פרטי של חשבון שירות", + "vertexPrivateKeyPlaceholder": "הדבק את המפתח הפרטי (תומך בשורה אחת \\n)", + "vertexApiVersion": "גרסת API", + "vertexEndpointMode": "מצב נקודת קצה", + "vertexEndpointStandard": "סטנדרטי (נקודת קצה אזורית)", + "vertexEndpointExpress": "אקספרס (נקודת קצה גלובלית)", + "verifyKey": "אמת מפתח", + "howToGet": "כיצד להשיג", + "getKeyTip": "אנא בקר ב", + "getKeyTipEnd": "כדי לקבל מפתח API", + "urlFormat": "דוגמת API: {defaultUrl}", + "modelList": "רשימת מודלים", + "enableModels": "הפעל מודלים", + "disableAllModels": "השבת את כל המודלים", + "modelsEnabled": "מודלים הופעלו", + "noModelsEnabled": { + "title": "אין מודלים מופעלים", + "description": "אנא לחץ על כפתור \"הפעל מודלים\" כדי לבחור ידנית את המודלים שברצונך להשתמש בהם." + }, + "verifyLink": "אמת קישור", + "syncModelsFailed": "סנכרון המודלים נכשל...", + "addCustomProvider": "הוסף ספק מותאם אישית", + "delete": "מחק", + "stopModel": "עצור מודל", + "pulling": "מושך...", + "runModel": "הרץ מודל", + "dialog": { + "disableModel": { + "title": "אשר השבתת מודל", + "content": "האם אתה בטוח שברצונך להשבית את המודל \"{name}\"?", + "confirm": "השבת" + }, + "disableAllModels": { + "title": "אשר השבתת כל המודלים", + "content": "האם אתה בטוח שברצונך להשבית את כל המודלים?", + "confirm": "השבת הכל" + }, + "configModels": { + "title": "הגדר רשימת מודלים", + "description": "בחר מודלים להפעלה או השבתה" + }, + "verify": { + "missingFields": "אנא הזן מפתח API וכתובת API", + "failed": "האימות נכשל", + "success": "האימות עבר בהצלחה", + "failedDesc": "אימות מפתח ה-API או התצורה נכשל, אנא בדוק את ההגדרות שלך", + "successDesc": "מפתח ה-API והתצורה אומתו בהצלחה, מוכן לשימוש", + "connectionError": "שגיאת חיבור, אנא בדוק את חיבור הרשת ואת כתובת ה-API", + "serverError": "שגיאת שרת, אנא נסה שוב מאוחר יותר", + "unauthorized": "האימות נכשל, מפתח ה-API אינו חוקי או פג תוקף" + }, + "addCustomProvider": { + "title": "הוסף ספק מותאם אישית", + "description": "אנא מלא את המידע הדרוש עבור הספק", + "name": "שם", + "namePlaceholder": "אנא הזן את שם הספק", + "apiType": "סוג API", + "apiTypePlaceholder": "אנא בחר את סוג ה-API", + "apiKey": "מפתח API", + "apiKeyPlaceholder": "אנא הזן את מפתח ה-API", + "baseUrl": "כתובת בסיס ל-API", + "baseUrlPlaceholder": "אנא הזן את כתובת הבסיס ל-API", + "enable": "הפעל ספק" + }, + "deleteProvider": { + "title": "אשר מחיקת ספק", + "content": "האם אתה בטוח שברצונך למחוק את הספק \"{name}\"? לא ניתן לבטל פעולה זו.", + "confirm": "מחק" + }, + "deleteModel": { + "title": "אשר מחיקת מודל", + "content": "האם אתה בטוח שברצונך למחוק את המודל \"{name}\"? לא ניתן לבטל פעולה זו.", + "confirm": "מחק" + }, + "pullModel": { + "title": "משיכת מודל (Pull)", + "description": "בחר מודלים להורדה מקומית", + "pull": "משוך" + }, + "modelCheck": { + "title": "בדיקת מודל", + "description": "בחר מודל לבדיקת קישוריות וזמינות", + "model": "בחר מודל", + "modelPlaceholder": "אנא בחר מודל לבדיקה", + "test": "התחל בדיקה", + "checking": "בודק...", + "success": "בדיקת המודל הצליחה", + "failed": "בדיקת המודל נכשלה", + "noModels": "אין מודלים זמינים עבור ספק זה" + } + }, + "pullModels": "משוך מודלים", + "refreshModels": "רענן מודלים", + "modelsRunning": "מודלים רצים", + "runningModels": "מודלים רצים", + "noRunningModels": "אין מודלים רצים", + "deleteModel": "מחק מודל", + "deleteModelConfirm": "האם אתה בטוח שברצונך למחוק את המודל \"{name}\"? לא ניתן לבטל פעולה זו.", + "noLocalModels": "אין מודלים מקומיים", + "localModels": "מודלים מקומיים", + "azureApiVersion": "גרסת API", + "safety": { + "title": "הגדרות בטיחות", + "blockHighest": "חסום סיכון גבוה", + "blockMost": "חסום סיכון בינוני", + "blockNone": "לא נחסם", + "blockSome": "חסום סיכון נמוך" + }, + "serverList": "רשימת שרתים", + "totalServers": "סה\"כ שרתים", + "addServer": "הוסף שרת", + "autoStart": "הפעלה אוטומטית", + "githubCopilotAuth": "אימות GitHub Copilot", + "githubCopilotConnected": "GitHub Copilot מחובר", + "githubCopilotNotConnected": "GitHub Copilot אינו מחובר", + "loginWithGitHub": "התחבר עם GitHub", + "loggingIn": "מתחבר...", + "githubCopilotLoginTip": "אשר ל-DeepChat לגשת למנוי GitHub Copilot שלך. ההרשאות 'read:user' ו-'read:org' נדרשות כדי לגשת ל-API של Copilot.", + "loginSuccess": "ההתחברות הצליחה", + "loginFailed": "ההתחברות נכשלה", + "tokenValid": "הטוקן תקין", + "tokenInvalid": "הטוקן אינו תקין", + "disconnect": "התנתק", + "disconnected": "נותק בהצלחה", + "disconnectFailed": "הניתוק נכשל", + "keyStatus": { + "remaining": "מכסה נותרת", + "usage": "בשימוש" + }, + "refreshingModels": "מרענן...", + "toast": { + "modelRunning": "המודל פועל", + "modelRunningDesc": "אנא עצור את המודל {model} תחילה ואז מחק אותו.", + "backupSuccessTitle": "הגיבוי הושלם", + "backupSuccessMessage": "הגיבוי נשמר ב-{time} ({size})", + "importSuccessTitle": "הייבוא הושלם", + "importSuccessMessage": "יובאו בהצלחה {count} שיחות" + }, + "modelscope": { + "mcpSync": { + "title": "סנכרן שירותי MCP", + "description": "סנכרן שרתי MCP מ-ModelScope לתצורה המקומית, מה שמאפשר הוספה מהירה של כלי MCP נפוצים.", + "sync": "התחל סנכרון", + "syncing": "מסנכרן...", + "pageSize": "גודל עמוד", + "imported": "יובאו {count} שירותים", + "skipped": "דולגו {count} שירותים", + "errors": "{count} שגיאות", + "errorDetails": "פרטי שגיאה", + "noApiKey": "אנא הגדר מפתח API של ModelScope תחילה", + "noServersFound": "לא נמצאו שירותי MCP זמינים", + "authenticationFailed": "האימות נכשל, אנא בדוק את מפתח ה-API", + "convertingServers": "ממיר תצורת שרת...", + "fetchingServers": "מקבל את רשימת שרתי ה-MCP...", + "importingServers": "מייבא תצורת שרת...", + "noOperationalUrls": "לא נמצאה כתובת הפעלה זמינה", + "pageNumber": "מספר עמוד", + "pageNumberPlaceholder": "אנא הזן את מספר העמוד", + "serverAlreadyExists": "השרת כבר קיים, מדלג על ייבוא", + "syncComplete": "הסנכרון הושלם", + "invalidServerData": "נתוני שרת לא חוקיים" + }, + "apiKey": "מפתח API", + "apiKeyHelper": "קבל את מפתח ה-API שלך במסוף ModelScope", + "apiKeyPlaceholder": "אנא הזן מפתח API של ModelScope", + "baseUrl": "כתובת API", + "baseUrlHelper": "כתובת שירות ה-API של ModelScope", + "connected": "מחובר", + "connecting": "מתחבר...", + "description": "ModelScope היא פלטפורמת שיתוף מודל-כשירות שהושקה על ידי Alibaba Damo Academy", + "details": { + "apiConfig": "תצורת API", + "mcpSync": "סנכרון MCP", + "modelManagement": "ניהול מודלים", + "operationalDescription": "סנכרן שרתי MCP שניתן להשתמש בהם ישירות בפלטפורמת ModelScope", + "operationalServers": "שרתים פועלים", + "rateLimitConfig": "תצורת מגבלת קצב", + "safetySettings": "הגדרות אבטחה", + "specialConfig": "תצורה מיוחדת", + "syncFromModelScope": "סנכרן מ-ModelScope", + "title": "פרטי הגדרות ספק" + }, + "invalidKey": "מפתח API לא חוקי", + "keyRequired": "אנא הזן מפתח API", + "name": "ModelScope", + "networkError": "שגיאת חיבור רשת", + "notConnected": "לא מחובר", + "verifyFailed": "האימות נכשל", + "verifySuccess": "האימות הצליח" + }, + "anthropicApiKeyTip": "אנא עבור למסוף Anthropic כדי לקבל את מפתח ה-API שלך", + "anthropicConnected": "Anthropic מחובר", + "anthropicNotConnected": "Anthropic אינו מחובר", + "anthropicOAuthTip": "לחץ כדי לאשר ל-DeepChat לגשת לחשבון Anthropic שלך", + "oauthLogin": "התחברות OAuth", + "authMethod": "שיטת אימות", + "authMethodPlaceholder": "בחר שיטת אימות", + "apiKeyLabel": "מפתח API", + "apiUrlLabel": "כתובת API", + "anthropicOAuthFlowTip": "המערכת תפתח אוטומטית את חלון האישור. אנא חזור והזן את קוד האישור לאחר האישור.", + "anthropicBrowserOpened": "הדפדפן החיצוני פתוח", + "anthropicCodeInstruction": "אנא השלם את האישור בדפדפן החיצוני והדבק את קוד האישור שהתקבל בתיבת הקלט למטה", + "browserOpenedSuccess": "הדפדפן החיצוני נפתח, אנא השלם את האישור", + "codeRequired": "אנא הזן את קוד האישור", + "inputOAuthCode": "הזן את קוד האישור", + "codeExchangeFailed": "החלפת קוד האישור נכשלה", + "invalidCode": "קוד אישור לא חוקי", + "oauthCodeHint": "אנא הדבק את קוד האישור כאן לאחר השלמת האישור בדפדפן חיצוני", + "oauthCodePlaceholder": "אנא הזן את קוד האישור...", + "verifyConnection": "אמת חיבור", + "manageModels": "נהל מודלים", + "anthropicOAuthActiveTip": "אימות OAuth מופעל, ניתן להשתמש בשירותי Anthropic ישירות", + "oauthVerifySuccess": "חיבור OAuth אומת בהצלחה", + "oauthVerifyFailed": "אימות חיבור OAuth נכשל", + "configurationSaved": "התצורה נשמרה", + "configurationUpdated": "התצורה עודכנה", + "dataRefreshed": "הנתונים רועננו", + "operationFailed": "הפעולה נכשלה", + "operationSuccess": "הפעולה הצליחה", + "settingsApplied": "ההגדרות הוחלו", + "bedrockLimitTip": "* נתמך רק עבור Anthropic Claude (כולל מודלי Opus, Sonnet, Haiku)", + "bedrockVerifyTip": "DeepChat משתמש ב-Claude 3.5 Sonnet לאימות. אם אין לך הרשאה להפעיל אותו, האימות ייכשל. זה לא ישפיע על השימוש במודלים אחרים." + }, + "knowledgeBase": { + "title": "הגדרות בסיס ידע", + "addKnowledgeBase": "הוסף בסיס ידע", + "selectKnowledgeBaseType": "אנא בחר את סוג בסיס הידע להוספה", + "difyDescription": "בסיס ידע Dify עוזר לך לנהל ולהשתמש בנתוני מסמכים", + "comingSoon": "בקרוב", + "featureNotAvailable": "תכונה זו אינה זמינה עדיין", + "addDifyConfig": "הוסף תצורת Dify", + "apiKey": "מפתח API", + "datasetId": "מזהה ערכת נתונים (Dataset ID)", + "endpoint": "נקודת קצה (API Endpoint)", + "configAdded": "תצורה נוספה", + "configAddedDesc": "תצורת {name} נוספה בהצלחה", + "addConfig": "הוסף תצורה", + "moreComingSoon": "סוגי בסיסי ידע נוספים יגיעו בקרוב", + "configUpdated": "תצורה עודכנה", + "configUpdatedDesc": "תצורת {name} עודכנה בהצלחה", + "descriptionPlaceholder": "דוגמה: בסיס ידע לתיעוד מוצרי החברה", + "ragflowTitle": "בסיס ידע RAGFlow", + "ragflowDescription": "RAGFlow היא מערכת ניהול בסיס ידע חזקה התומכת בשיטות אחזור מרובות ותכונות ניהול מסמכים.", + "addRagflowConfig": "הוסף תצורת RAGFlow", + "editRagflowConfig": "ערוך תצורת RAGFlow", + "dify": "בסיס ידע Dify", + "editDifyConfig": "שנה את תצורת Dify", + "fastgptTitle": "בסיס ידע FastGPT", + "fastgptDescription": "FastGPT היא מערכת ניהול בסיס ידע חזקה התומכת בשיטות אחזור מרובות ותכונות ניהול מסמכים.", + "addFastGptConfig": "הוסף תצורת FastGPT", + "editFastGptConfig": "ערוך תצורת FastGPT", + "builtInKnowledgeDescription": "בסיס הידע המובנה מספק יישומים פשוטים המאפשרים פונקציות בסיסיות בסביבה לא מקוונת.", + "builtInKnowledgeTitle": "בסיס ידע מובנה", + "addBuiltinKnowledgeConfig": "הוסף תצורת בסיס ידע מובנה", + "editBuiltinKnowledgeConfig": "ערוך את תצורת בסיס הידע המובנה", + "chunkSize": "גודל מקטע (Chunk)", + "chunkSizeHelper": "חתוך את המסמך למקטעים, גודל כל מקטע לא יכול לעלות על מגבלת ההקשר של המודל", + "chunkOverlap": "גודל חפיפה", + "chunkOverlapHelper": "כמות התוכן החוזר בין מקטעי טקסט סמוכים מבטיחה שעדיין קיים הקשר בין מקטעי טקסט מפוצלים, ומשפרת את האפקט הכולל של עיבוד המודל לטקסטים ארוכים", + "selectEmbeddingModel": "בחר מודל הטמעה (Embedding)", + "modelNotFound": "ספק השירות {provider} או המודל {model} לא נמצאו", + "modelNotFoundDesc": "ודא שהמודל מוגדר כראוי ושמודל זה מופעל. \nתוכל לבדוק את תצורת המודל בהגדרות ספק השירות.", + "removeBuiltinKnowledgeConfirmDesc": "מחיקת תצורת בסיס הידע המובנה תמחק את כל הנתונים הרלוונטיים ולא ניתן לשחזרם. אנא היזהר.", + "removeBuiltinKnowledgeConfirmTitle": "האם לאשר את מחיקת בסיס הידע המובנה {name}?", + "descriptionDesc": "תיאור בסיס הידע כך שה-AI יחליט אם לאחזר בסיס ידע זה", + "advanced": "אפשרויות מתקדמות", + "autoDetectDimensions": "זהה אוטומטית ממדים מוטמעים", + "autoDetectHelper": "מזהה אוטומטית ממדים מוטמעים, צורך כמות קטנה של טוקנים", + "chunkOverlapPlaceholder": "ערך ברירת מחדל, לא מומלץ לשנות", + "chunkSizePlaceholder": "ערך ברירת מחדל, לא מומלץ לשנות", + "dimensions": "ממדי הטמעה (Embed dimensions)", + "dimensionsPlaceholder": "גודל ממד הטמעה, כגון 1024", + "selectEmbeddingModelHelper": "מודלי הטמעה אסורים לשינוי לאחר יצירת בסיס הידע", + "dimensionsHelper": "ודא שהמודל תומך בגודל ממד ההטמעה שהוגדר", + "autoDetectDimensionsError": "זיהוי אוטומטי של ממד מוטמע נכשל", + "fragmentsNumber": "מספר מקטעי מסמך מבוקשים", + "fragmentsNumberHelper": "ככל שמבוקשים יותר מקטעי מסמך, כך מתקבל יותר מידע, אך נדרשים יותר טוקנים לצריכה", + "selectRerankModel": "בחר את מודל הדירוג מחדש (Rerank)", + "rerankModel": "מודל דירוג מחדש", + "embeddingModel": "מודל הטמעה", + "return": "חזור", + "uploadHelper": "לחץ להעלאה או גרור את הקובץ לכאן", + "fileSupport": "תומך ב-{accept} וב-{count} פורמטים נוספים", + "searchKnowledge": "חפש בבסיס הידע", + "searchKnowledgePlaceholder": "אנא הזן את תוכן השאילתה", + "noData": "אין נתונים עדיין", + "file": "מסמך", + "uploadProcessing": "מעלה", + "uploadCompleted": "ההעלאה הושלמה", + "reAdd": "העלה מחדש", + "uploadError": "ההעלאה נכשלה", + "delete": "מחק", + "reason": "סיבה", + "deleteSuccess": "נמחק בהצלחה", + "copy": "העתק", + "copySuccess": "הועתק בהצלחה", + "source": "מקור", + "normalized": "נרמול L2", + "normalizedHelper": "אנא ודא שהמודל תומך בנרמול L2 של וקטורי פלט", + "dialog": { + "beforequit": { + "cancel": "ביטול", + "confirm": "אשר", + "title": "אישור יציאה", + "description": "ישנה משימת בסיס ידע שפועלת. האם אתה בטוח שברצונך לצאת מהתוכנה? \nניתן לשחזר משימות שהופסקו לאחר הפעלת התוכנה מחדש." + } + }, + "searchError": "השאילתה נכשלה", + "processing": "מעלה", + "paused": "העלאה מושהית", + "unknown": "סטטוס לא ידוע", + "reAddFile": { + "title": "אישור העלאה מחדש", + "content": "האם אתה בטוח שברצונך להעלות מחדש את הקובץ \"{fileName}\"?" + }, + "nowledgeMem": { + "title": "ייצוא ל-Nowledge Mem", + "description": "ייצוא שיחות לשירות Nowledge Mem.", + "testConnection": "בדוק חיבור", + "configuration": "תצורה", + "baseUrl": "כתובת בסיס (Base URL)", + "apiKey": "מפתח API", + "apiKeyHint": "אופציונלי. אם השירות שלך דורש מפתח API, הזן אותו כאן.", + "timeout": "פסק זמן (Timeout)", + "saveConfig": "שמור", + "resetConfig": "אפס", + "seconds": "שניות" + }, + "deleteFile": { + "title": "אישור מחיקת קובץ", + "content": "האם אתה בטוח שברצונך למחוק את הקובץ \"{fileName}\"? \nפעולה זו אינה ניתנת לשחזור." + }, + "resumeAllPausedTasks": "שחזור בלחיצה אחת", + "pauseAllRunningTasks": "השהייה בלחיצה אחת", + "separators": "מפריד מקטעים", + "separatorsHelper": "מפריד פיצול מסמכים, מפריד בודד מוקף במרכאות כפולות (\"\"), ומפרידים מופרדים בפסיקים (,)", + "invalidSeparators": "מפריד לא חוקי", + "selectLanguage": "בחר הגדרה מוגדרת מראש", + "separatorsPreset": "טוען הגדרות מוגדרות מראש" + }, + "mcp": { + "title": "הגדרות MCP", + "description": "ניהול והגדרה של שרתי וכלים של MCP (Model Context Protocol)", + "enabledTitle": "הפעל את MCP", + "enabledDescription": "הפעל או השבת פונקציונליות וכלים של MCP", + "enableToAccess": "אנא הפעל את MCP כדי לגשת לאפשרויות התצורה", + "marketplace": "עבור לחנות MCP להתקנה בלחיצה אחת", + "technicalDetails": "פרטים טכניים", + "httpServer": "שרת HTTP", + "localProcess": "תהליך מקומי", + "restartServer": "הפעל מחדש שרת", + "viewLogs": "צפה בלוגים", + "starting": "מתחיל", + "error": "שגיאה", + "tabs": { + "servers": "שרתים", + "tools": "כלים", + "prompts": "הנחיות (Prompts)", + "resources": "משאבים" + }, + "serverList": "רשימת שרתים", + "totalServers": "סה\"כ שרתים", + "addServer": "הוסף שרת", + "running": "פועל", + "stopped": "נעצר", + "stopServer": "עצור שרת", + "startServer": "התחל שרת", + "noServersFound": "לא נמצאו שרתים", + "addServerDialog": { + "title": "הוסף שרת", + "description": "הגדר שרת MCP חדש" + }, + "editServerDialog": { + "title": "ערוך שרת", + "description": "ערוך תצורת שרת MCP" + }, + "serverForm": { + "name": "שם השרת", + "namePlaceholder": "הזן את שם השרת", + "nameRequired": "שם השרת נדרש", + "type": "סוג שרת", + "typePlaceholder": "בחר סוג שרת", + "typeStdio": "קלט ופלט סטנדרטי (Stdio)", + "typeSse": "אירועים הנשלחים מהשרת (SSE)", + "typeInMemory": "בזיכרון (In-Memory)", + "typeHttp": "בקשות HTTP זורמות (HTTP)", + "baseUrl": "כתובת בסיס (Base URL)", + "baseUrlPlaceholder": "הזן את כתובת הבסיס של השרת (לדוגמה: http://localhost:3000)", + "command": "פקודה", + "commandPlaceholder": "הזן פקודה", + "commandRequired": "פקודה נדרשת", + "args": "ארגומנטים", + "argsPlaceholder": "הזן ארגומנט אחד בכל שורה", + "addArg": "הוסף ארגומנט", + "argPlaceholder": "הזן ערך ארגומנט", + "argsRequired": "ארגומנטים נדרשים", + "env": "משתני סביבה", + "envPlaceholder": "הזן משתני סביבה בפורמט JSON", + "envInvalid": "משתני סביבה חייבים להיות JSON חוקי", + "description": "תיאור", + "descriptionPlaceholder": "הזן את תיאור השרת", + "descriptions": "תיאור", + "descriptionsPlaceholder": "הזן את תיאור השרת", + "icon": "אייקון", + "iconPlaceholder": "הזן אייקון", + "icons": "אייקון", + "iconsPlaceholder": "הזן אייקון", + "autoApprove": "אישור אוטומטי", + "autoApproveAll": "הכל", + "autoApproveRead": "קריאה", + "autoApproveWrite": "כתיבה", + "autoApproveHelp": "בחר סוגי פעולות לאישור אוטומטי ללא אישור המשתמש", + "submit": "שלח", + "add": "הוסף", + "update": "עדכן", + "cancel": "ביטול", + "jsonConfigIntro": "אתה יכול להדביק תצורת JSON ישירות או לבחור להגדיר את השרת באופן ידני.", + "jsonConfig": "תצורת JSON", + "jsonConfigPlaceholder": "הדבק את תצורת שרת MCP שלך בפורמט JSON", + "jsonConfigExample": "דוגמה לתצורת JSON", + "parseSuccess": "התצורה פוענחה", + "configImported": "התצורה יובאה בהצלחה", + "parseError": "שגיאת פענוח", + "skipToManual": "דלג להגדרה ידנית", + "parseAndContinue": "פענח והמשך", + "jsonParseError": "פענוח JSON נכשל", + "browseMarketplace": "עיין בחנות MCP", + "imageModel": "בחר מודל ראייה", + "customHeadersParseError": "פענוח כותרות מותאמות אישית נכשל", + "customHeaders": "כותרות בקשה מותאמות אישית", + "clickToEdit": "לחץ לעריכה וצפייה בתוכן המלא", + "invalidKeyValueFormat": "פורמט כותרת בקשה שגוי, אנא בדוק אם הקלט תקין.", + "npmRegistry": "NPM Registry מותאם אישית", + "npmRegistryPlaceholder": "הגדר NPM Registry מותאם אישית, השאר ריק כדי שהמערכת תבחר אוטומטית את המהיר ביותר", + "browseHigress": "צפה בחנות Higress MCP", + "selectFolderError": "שגיאה בבחירת תיקייה", + "folders": "תיקיות מותרות", + "addFolder": "הוסף תיקייה", + "noFoldersSelected": "לא נבחרו תיקיות", + "useE2B": "הפעל ארגז חול E2B", + "e2bDescription": "הרץ קוד Python באמצעות ארגז החול של E2B", + "e2bApiKey": "E2B ApiKey", + "e2bApiKeyPlaceholder": "הזן את מפתחות ה-API של E2B כאן, למשל e2b_1111xx*******", + "e2bApiKeyHelp": "עבור ל-e2b.dev כדי לקבל את ה-ApiKey שלך", + "e2bApiKeyRequired": "חובה להזין ApiKey כדי להפעיל את פונקציית E2B" + }, + "deleteServer": "מחק שרת", + "editServer": "ערוך שרת", + "setDefault": "הגדר כברירת מחדל", + "removeDefault": "הסר ברירת מחדל", + "isDefault": "שרת ברירת מחדל", + "default": "ברירת מחדל", + "setAsDefault": "הגדר כברירת מחדל", + "removeServer": "הסר שרת", + "autoStart": "הפעלה אוטומטית", + "confirmRemoveServer": "האם אתה בטוח שברצונך למחוק את השרת {name}? לא ניתן לבטל פעולה זו.", + "removeServerDialog": { + "title": "מחק שרת" + }, + "confirmDelete": { + "title": "אשר מחיקה", + "description": "האם אתה בטוח שברצונך למחוק את השרת {name}? לא ניתן לבטל פעולה זו.", + "confirm": "מחק", + "cancel": "ביטול" + }, + "resetToDefault": "אפס לברירת מחדל", + "resetConfirmTitle": "אפס לשרתי ברירת המחדל", + "resetConfirmDescription": "פעולה זו תשחזר את כל שרתי ברירת המחדל תוך שמירה על השרתים המותאמים אישית שלך. כל שינוי בשרתי ברירת המחדל יאבד.", + "resetConfirm": "אפס", + "builtInServers": "שרתים מובנים", + "customServers": "שרתים מותאמים אישית", + "builtIn": "מובנה", + "cannotRemoveBuiltIn": "לא ניתן להסיר שרת מובנה", + "builtInServerCannotBeRemoved": "לא ניתן להסיר שרתים מובנים, ניתן לשנות רק פרמטרים ומשתני סביבה", + "maxDefaultServersReached": "ניתן להגדיר לכל היותר 3 שרתי ברירת מחדל", + "removeDefaultFirst": "אנא הסר תחילה מספר שרתי ברירת מחדל", + "higressMarket": "עבור להתקנת Higress MCP", + "npmRegistry": { + "title": "תצורת NPM Registry", + "currentSource": "מקור נוכחי", + "cached": "במטמון", + "lastChecked": "נבדק לאחרונה", + "refresh": "רענן", + "advanced": "מתקדם", + "advancedSettings": "הגדרות מתקדמות", + "advancedSettingsDesc": "הגדר אפשרויות מתקדמות של NPM registry, כולל זיהוי אוטומטי והגדרות מקור מותאמות אישית", + "autoDetect": "זהה אוטומטית מקור אופטימלי", + "autoDetectDesc": "מזהה ומשתמש אוטומטית ב-NPM registry המהיר ביותר בעת ההפעלה", + "customSource": "מקור מותאם אישית", + "customSourcePlaceholder": "הזן כתובת URL של NPM registry מותאם אישית", + "currentCustom": "מקור מותאם אישית נוכחי", + "justNow": "ממש עכשיו", + "minutesAgo": "לפני {minutes} דקות", + "hoursAgo": "לפני {hours} שעות", + "daysAgo": "לפני {days} ימים", + "refreshSuccess": "NPM registry רוענן בהצלחה", + "refreshSuccessDesc": "זוהה ועודכן ה-NPM registry האופטימלי", + "refreshFailed": "רענון NPM registry נכשל", + "autoDetectUpdated": "הגדרת זיהוי אוטומטי עודכנה", + "autoDetectEnabled": "זיהוי אוטומטי של NPM registry אופטימלי הופעל", + "autoDetectDisabled": "זיהוי אוטומטי מושבת, ישתמש ב-registry ברירת המחדל", + "updateFailed": "עדכון ההגדרה נכשל", + "customSourceSet": "מקור מותאם אישית הוגדר", + "customSourceSetDesc": "NPM registry מותאם אישית הוגדר: {registry}", + "customSourceCleared": "מקור מותאם אישית נוקה", + "customSourceClearedDesc": "NPM registry מותאם אישית נוקה, ישתמש בזיהוי אוטומטי", + "invalidUrl": "כתובת URL לא חוקית", + "invalidUrlDesc": "אנא הזן כתובת HTTP או HTTPS תקינה", + "testing": "בודק NPM registry", + "testingDesc": "בודק קישוריות ל-{registry}...", + "testFailed": "בדיקת NPM registry נכשלה", + "testFailedDesc": "לא ניתן להתחבר ל-{registry}, שגיאה: {error}. אנא בדוק אם הכתובת נכונה או בדוק את חיבור הרשת.", + "redetectingOptimal": "מזהה מחדש NPM registry אופטימלי...", + "redetectComplete": "זיהוי מחדש הושלם", + "redetectCompleteDesc": "זוהה והוגדר ה-NPM registry האופטימלי הנוכחי", + "redetectFailed": "זיהוי מחדש נכשל", + "redetectFailedDesc": "לא ניתן לזהות מחדש registry אופטימלי, ישתמש בתצורת ברירת המחדל" + } + }, + "about": { + "title": "אודותינו", + "version": "גרסה", + "checkUpdate": "בדוק עדכון", + "checking": "בודק...", + "latestVersion": "גרסה אחרונה" + }, + "display": { + "fontSize": "גודל טקסט", + "text-sm": "קטן", + "text-base": "בינוני", + "text-lg": "גדול", + "text-xl": "גדול מאוד", + "text-2xl": "ענק", + "floatingButton": "כפתור צף", + "floatingButtonDesc": "הצג כפתור צף על שולחן העבודה להפעלה מהירה של חלון האפליקציה", + "codeFontFamily": "גופן מונו-רווח", + "codeFontFamilyDesc": "משמש עבור בלוקי קוד ואזורים ברוחב קבוע.", + "fontDefaultLabel": "ברירת מחדל (ערימת גופנים מובנית)", + "fontFamily": "גופן ממשק", + "fontFamilyDesc": "בחר את הגופן הראשי של הממשק. אם נשאר ריק, השתמש בערימת הגופנים המובנית.", + "fontReset": "לאפס לברירת המחדל", + "fontSearchEmpty": "אין גופן תואם", + "fontSearchPlaceholder": "חיפוש גופן", + "fontSystemLoading": "טוען גופני מערכת...", + "fontTitle": "גוֹפָן", + "fontUsageHint": "ממשקים מסוימים דורשים הפעלה מחדש של היישום כדי להיכנס לתוקף. אם הגופן אינו זמין, הוא יחזור אוטומטית לגופן ברירת המחדל." + }, + "shortcuts": { + "title": "הגדרות קיצורי מקשים", + "pressKeys": "לחץ על מקשים", + "pressEnterToSave": "לחץ Enter לשמירה, Esc לביטול", + "noModifierOnly": "לא ניתן להשתמש במקש שינוי (Modifier) בלבד כקיצור דרך", + "keyConflict": "התנגשות מקשי קיצור, אנא בחר שילוב אחר", + "clearShortcut": "נקה קיצור דרך", + "cleanHistory": "נקה היסטוריית צ'אט", + "deleteConversation": "מחק שיחה", + "goSettings": "פתח הגדרות", + "hideWindow": "הסתר חלון", + "quitApp": "צא מהאפליקציה", + "zoomIn": "הגדל תצוגה ", + "zoomOut": "הקטן תצוגה ", + "zoomReset": "אפס תצוגה", + "closeTab": "סגור את הכרטיסייה הנוכחית", + "newTab": "צור כרטיסייה חדשה", + "newWindow": "פתח חלון חדש", + "showHideWindow": "הצג/הסתר חלון", + "newConversation": "שיחה חדשה", + "nextTab": "עבור לכרטיסייה הבאה", + "previousTab": "עבור לכרטיסייה הקודמת", + "specificTab": "עבור לכרטיסייה מסוימת (1-8)", + "lastTab": "עבור לכרטיסייה האחרונה" + }, + "acp": { + "title": "סוכני ACP", + "description": "ניהול סוכני ACP מקומיים (Agent Client Protocol) המופעלים על ידי DeepChat.", + "enabledTitle": "הפעל את ACP", + "enabledDescription": "כאשר מופעל, סוכני ACP מוגדרים יופיעו כמודלים בבוחר.", + "useBuiltinRuntimeTitle": "השתמש בסביבת הרצה מובנית של DeepChat", + "useBuiltinRuntimeDescription": "כאשר מופעל, עוקף פקודות node ו-uv של המערכת, ומשתמש בגרסאות המצורפות ל-DeepChat.", + "enableToAccess": "הפעל את ACP כדי להגדיר סוכנים.", + "addCustomAgent": "הוסף סוכן מותאם אישית", + "customEmpty": "אין סוכנים מותאמים אישית עדיין.", + "customDeleteConfirm": "למחוק את הסוכן המותאם אישית \"{name}\"?", + "builtinSectionTitle": "סוכנים מובנים", + "builtinSectionDescription": "לכל סוכן מובנה יכולים להיות מספר פרופילי הפעלה. רק סוכנים מופעלים עם פרופיל פעיל מופיעים ברשימת המודלים.", + "builtinHint": "הפעל את {name} עם הגדרות אלה.", + "disabledBadge": "מושבת", + "manageProfiles": "נהל פרופילים", + "addProfile": "הוסף פרופיל", + "activeProfile": "פרופיל פעיל", + "profilePlaceholder": "בחר פרופיל", + "profileSwitched": "פרופיל הוחלף", + "customSectionTitle": "סוכנים מותאמים אישית", + "customSectionDescription": "עטוף כל פקודה תואמת ACP כרשומת מודל לשימוש חוזר.", + "loading": "טוען נתוני ACP...", + "none": "ללא", + "saveSuccess": "התצורה נשמרה", + "saveFailed": "שמירת הסוכן נכשלה", + "deleteSuccess": "נמחק בהצלחה", + "initialize": "אתחל", + "initializing": "מאתחל...", + "initializeDescription": "טרמינל נפתח עם פקודות אתחול", + "initializeSuccess": "האתחול התחיל", + "initializeFailed": "האתחול נכשל", + "missingFieldsTitle": "שם ופקודה הם חובה", + "missingFieldsDesc": "אנא מלא גם שם וגם פקודה לפני השמירה.", + "command": "פקודה", + "commandPlaceholder": "נתיב קובץ הפעלה או סקריפט", + "args": "ארגומנטים", + "argsPlaceholder": "אופציונלי, מופרדים ברווחים. השתמש במרכאות כדי לשמור על ארגומנטים יחד.", + "env": "משתני סביבה", + "addEnv": "הוסף משתנה", + "envKeyPlaceholder": "מפתח (KEY)", + "envValuePlaceholder": "ערך (VALUE)", + "profileDialog": { + "addBuiltinTitle": "הוסף פרופיל {name}", + "editBuiltinTitle": "ערוך פרופיל {name}", + "addCustomTitle": "הוסף סוכן מותאם אישית", + "editCustomTitle": "ערוך סוכן מותאם אישית", + "builtinHint": "השתמש בהגדרות קבועות מראש שונות לכל תרחיש והחלף ביניהן בבוחר.", + "customHint": "ספק את הפקודה, הארגומנטים ומשתני הסביבה כדי להפעיל את הסוכן שלך.", + "profileName": "שם הפרופיל", + "profileNamePlaceholder": "לדוגמה: תחנת עבודה", + "agentName": "שם הסוכן", + "agentNamePlaceholder": "לדוגמה: ה-ACP המקומי שלי" + }, + "profileManager": { + "title": "פרופילים", + "description": "החלף, ערוך או מחק פרופילי הפעלה שמורים.", + "count": "{count} פרופילים", + "empty": "אין פרופילים עדיין.", + "active": "פעיל", + "setActive": "הגדר כפעיל", + "deleteConfirm": "למחוק את הפרופיל \"{name}\"?", + "cannotDeleteTitle": "השאר לפחות פרופיל אחד", + "cannotDeleteDesc": "סוכנים מובנים דורשים לפחות תצורה אחת.", + "noAgent": "בחר סוכן כדי לנהל את הפרופילים שלו." + }, + "terminal": { + "title": "טרמינל אתחול", + "waiting": "ממתין לתחילת האתחול...", + "starting": "מתחיל אתחול...", + "close": "סגור", + "closing": "סוגר...", + "exitSuccess": "התהליך הושלם בהצלחה (קוד יציאה: {code})", + "exitError": "התהליך יצא עם שגיאה (קוד יציאה: {code})", + "processError": "שגיאת תהליך", + "paste": "הדבק", + "pasteError": "ההדבקה מהלוח נכשלה", + "status": { + "idle": "בהמתנה", + "running": "פועל", + "completed": "הושלם", + "error": "שגיאה" + } + }, + "dependency": { + "title": "תלויות חיצוניות חסרות", + "description": "יש להתקין את התלויות הבאות לפני האתחול.", + "installCommands": "פקודות התקנה", + "downloadUrl": "קישור להורדה", + "copy": "העתק", + "copied": "הועתק ללוח", + "copyFailed": "ההעתקה נכשלה" + }, + "debug": { + "clearHistory": "שיא ברור", + "close": "סֶגֶר", + "customMethod": "שם שיטה מותאמת אישית", + "customMethodPlaceholder": "למשל סשן/פינג", + "customMethodRequired": "נא למלא את שם שיטת ההרחבה", + "description": "שלח בקשת ACP אל \"{name}\" והצג את התגובה בזמן אמת.", + "empty": "עדיין אין אירועי ניפוי באגים.", + "entry": "לְנַפּוֹת", + "eventCount": "{count} פריטים", + "eventKinds": { + "error": "טָעוּת", + "notification": "לְהוֹדִיעַ", + "permission": "הרשאות", + "request": "לִשְׁאוֹל", + "response": "תְגוּבָה" + }, + "events": "מִקרֶה", + "format": "פורמט JSON", + "methods": { + "cancel": "הפעלה/ביטול", + "extMethod": "הרחבה/שיטה", + "extNotification": "שלוחה/הודעה", + "initialize": "לְאַתחֵל", + "loadSession": "הפעלה/טעינה", + "newSession": "מושב/חדש", + "prompt": "הפעלה/הנחיה", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel" + }, + "needInitialize": "נא לבצע אתחול תחילה", + "parseError": "ניתוח JSON נכשל", + "payloadHint": "ניתן להתאים JSON לפני השליחה.", + "processNotReady": "נא לאתחל תחילה", + "processReady": "התהליך מוכן", + "requestFailed": "הבקשה נכשלה", + "resetTemplate": "תבנית שחזור", + "send": "שלח בקשה", + "sending": "שְׁלִיחָה...", + "title": "איתור באגים של ACP", + "workdirPlaceholder": "השאר ריק כדי להשתמש בספריית העבודה המוגדרת כברירת מחדל" + } + }, + "rateLimit": { + "title": "מגבלת קצב (Rate Limit)", + "description": "שלוט במרווח הבקשות כדי למנוע חריגה ממגבלות ה-API", + "intervalLimit": "מרווח בקשות", + "intervalUnit": "שניות", + "intervalHelper": "מרווח מינימלי בין בקשות, השבת את מגבלת הקצב אם אין צורך", + "lastRequestTime": "בקשה אחרונה", + "queueLength": "אורך תור", + "nextAllowedTime": "הבא מותר", + "never": "מעולם לא", + "justNow": "ממש עכשיו", + "secondsAgo": "לפני שניות", + "minutesAgo": "לפני דקות", + "immediately": "עכשיו", + "secondsLater": "שניות מאוחר יותר", + "confirmDisableTitle": "אשר ביטול מגבלת קצב", + "confirmDisableMessage": "הערך לא יכול להיות קטן או שווה ל-0. האם ברצונך לבטל את מגבלת הקצב?", + "confirmDisable": "בטל מגבלה", + "disabled": "מגבלת קצב מושבתת", + "disabledDescription": "מגבלת הקצב הושבתה" + }, + "promptSetting": { + "resetToDefault": "אפס להנחיית ברירת מחדל", + "resetToDefaultSuccess": "איפוס להנחיית מערכת ברירת מחדל הצליח", + "resetToDefaultFailed": "האיפוס נכשל, אנא נסה שוב" + } +} diff --git a/src/renderer/src/i18n/he-IL/sync.json b/src/renderer/src/i18n/he-IL/sync.json new file mode 100644 index 000000000..e641785a2 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/sync.json @@ -0,0 +1,17 @@ +{ + "success": { + "importComplete": "יובאו בהצלחה {count} שיחות. היישום יופעל מחדש כעת." + }, + "error": { + "notEnabled": "הסנכרון אינו מופעל", + "folderNotExists": "תיקיית הסנכרון אינה קיימת", + "noValidBackup": "לא נמצאו קבצי גיבוי תקינים בתיקיית הסנכרון.", + "dbNotExists": "קובץ מסד הנתונים אינו קיים", + "configNotExists": "קובץ התצורה אינו קיים", + "tempDbFailed": "נכשל ביצירת קובץ גיבוי זמני למסד הנתונים", + "tempConfigFailed": "נכשל ביצירת קובץ גיבוי זמני לתצורה", + "importFailed": "הייבוא נכשל, הנתונים המקוריים שוחזרו", + "importProcess": "אירעה שגיאה במהלך תהליך הייבוא", + "unknown": "שגיאה לא ידועה" + } +} diff --git a/src/renderer/src/i18n/he-IL/thread.json b/src/renderer/src/i18n/he-IL/thread.json new file mode 100644 index 000000000..da2240d9b --- /dev/null +++ b/src/renderer/src/i18n/he-IL/thread.json @@ -0,0 +1,46 @@ +{ + "actions": { + "rename": "שנה שם", + "delete": "מחק", + "cleanMessages": "נקה הודעות", + "pin": "נעץ", + "unpin": "בטל נעיצה", + "export": "ייצוא", + "exportText": "טקסט רגיל", + "exportNowledgeMem": "Nowledge-mem" + }, + "toolbar": { + "save": "שמור", + "cancel": "ביטול", + "previousVariant": "גרסה קודמת", + "nextVariant": "גרסה הבאה", + "copy": "העתק כ-Markdown", + "copyImage": "העתק כתמונה", + "copyImageWithLongPress": "העתק כתמונה (לחיצה ארוכה ללכידה מלמעלה)", + "copyFromTopSuccess": "תמונת השיחה המלאה הועתקה", + "capturing": "לוכד...", + "retry": "צור מחדש", + "fork": "צור ענף לשיחה חדשה", + "edit": "ערוך הודעה", + "delete": "מחק הודעה", + "trace": "עקוב אחר בקשה (Trace)" + }, + "message": { + "toolbar": { + "save": "שמור" + } + }, + "export": { + "failed": "הייצוא נכשל", + "failedDesc": "אירעה שגיאה במהלך תהליך הייצוא, אנא נסה שוב", + "success": "הייצוא הושלם בהצלחה", + "successDesc": "השיחה יוצאה בהצלחה", + "nowledgeMemSuccess": "ייצוא ל-Nowledge-mem הושלם בהצלחה", + "nowledgeMemSuccessDesc": "השיחה יוצאה בהצלחה לפורמט nowledge-mem", + "nowledgeMemSubmitPrompt": "האם ברצונך לשלוח שיחה זו ל-nowledge-mem?", + "nowledgeMemSubmitSuccess": "השליחה ל-Nowledge-mem הושלמה בהצלחה", + "nowledgeMemSubmitSuccessDesc": "השיחה נשלחה בהצלחה ל-nowledge-mem", + "nowledgeMemSubmitFailed": "השליחה ל-Nowledge-mem נכשלה", + "nowledgeMemSubmitFailedDesc": "נכשל בשליחת השיחה ל-nowledge-mem" + } +} diff --git a/src/renderer/src/i18n/he-IL/toolCall.json b/src/renderer/src/i18n/he-IL/toolCall.json new file mode 100644 index 000000000..9524fee9e --- /dev/null +++ b/src/renderer/src/i18n/he-IL/toolCall.json @@ -0,0 +1,19 @@ +{ + "calling": "קורא...", + "response": "רץ...", + "end": "בוצע", + "error": "שגיאה", + "title": "קריאה לכלי", + "clickToView": "לחץ לצפייה בפרטים", + "functionName": "שם הפונקציה", + "permission": "מבקש הרשאה...", + "params": "פרמטרי פונקציה", + "responseData": "תגובה", + "failed": "לְהִכָּשֵׁל", + "fileOperation": "פעולות קובץ", + "filePath": "נתיב הקובץ", + "fileRead": "לקרוא קובץ", + "fileWrite": "לכתוב קובץ", + "success": "הַצלָחָה", + "terminalOutput": "פלט מסוף" +} diff --git a/src/renderer/src/i18n/he-IL/traceDialog.json b/src/renderer/src/i18n/he-IL/traceDialog.json new file mode 100644 index 000000000..703332a23 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/traceDialog.json @@ -0,0 +1,17 @@ +{ + "title": "תצוגה מקדימה של הבקשה", + "provider": "ספק", + "model": "מודל", + "endpoint": "נקודת קצה (API Endpoint)", + "headers": "כותרות (Headers)", + "body": "גוף הבקשה (Body)", + "copyJson": "העתק JSON", + "copySuccess": "הועתק ללוח", + "close": "סגור", + "loading": "טוען...", + "error": "הטעינה נכשלה", + "errorDesc": "לא ניתן להביא תצוגה מקדימה של הבקשה, אנא נסה שוב", + "notImplemented": "לא נתמך", + "notImplementedDesc": "ספק זה טרם יישם תצוגה מקדימה של הבקשה", + "mayNotMatch": "הערה: תצוגה מקדימה זו משוחזרת מהגדרות השיחה הנוכחיות ועשויה שלא להתאים במדויק לפרמטרי הבקשה בפועל" +} diff --git a/src/renderer/src/i18n/he-IL/update.json b/src/renderer/src/i18n/he-IL/update.json new file mode 100644 index 000000000..1ca614070 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/update.json @@ -0,0 +1,16 @@ +{ + "newVersion": "גרסה חדשה נמצאה", + "version": "גרסה", + "releaseDate": "תאריך שחרור", + "releaseNotes": "הערות גרסה", + "later": "מאוחר יותר", + "githubDownload": "הורדה מ-GitHub", + "netdiskDownload": "הורדה מהענן", + "checkUpdate": "בדוק עדכונים", + "downloading": "מוריד", + "installNow": "התקן כעת", + "autoUpdate": "עדכון אוטומטי", + "restarting": "מפעיל מחדש", + "alreadyUpToDate": "התוכנה מעודכנת", + "alreadyUpToDateDesc": "ה-DeepChat שלך כבר מעודכן לגרסה האחרונה, אין צורך בעדכון." +} diff --git a/src/renderer/src/i18n/he-IL/welcome.json b/src/renderer/src/i18n/he-IL/welcome.json new file mode 100644 index 000000000..5d742a512 --- /dev/null +++ b/src/renderer/src/i18n/he-IL/welcome.json @@ -0,0 +1,37 @@ +{ + "steps": { + "welcome": { + "title": "ברוכים הבאים", + "description": "בוא נתחיל להגדיר את DeepChat" + }, + "provider": { + "title": "ספק", + "description": "בחר את ספק המודל המועדף עליך" + }, + "configuration": { + "title": "תצורה", + "description": "הגדר את המודלים שברצונך להשתמש בהם" + }, + "complete": { + "title": "סיום", + "description": "הכל מוכן ואפשר לצאת לדרך!" + } + }, + "title": "ברוכים הבאים ל-DeepChat", + "description": "בוא נעבור יחד על ההתקנה.", + "provider": { + "select": "בחר ספק", + "apiUrl": "כתובת API", + "apiKey": "מפתח API", + "verifyLink": "אמת קישור" + }, + "complete": { + "title": "סיימנו!", + "description": "אתה מוכן לשימוש. בוא נתחיל!" + }, + "buttons": { + "getStarted": "התחל", + "next": "הבא", + "back": "חזור" + } +} diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 429e4ff46..12990b2c6 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -9,6 +9,7 @@ import frFR from './fr-FR' import faIR from './fa-IR' import ptBR from './pt-BR' import daDK from './da-DK' +import heIL from './he-IL' const locales = { 'zh-CN': zhCN, @@ -22,11 +23,13 @@ const locales = { 'fa-IR': faIR, 'pt-BR': ptBR, 'da-DK': daDK, + 'he-IL': heIL, zh: zhCN, en: enUS, fr: frFR, pt: ptBR, - da: daDK + da: daDK, + he: heIL } console.log('locales', locales) export default locales diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 4a9728c75..31ca36c73 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -814,7 +814,18 @@ "text-xl": "XL", "text-2xl": "XXL", "floatingButton": "フローティングボタン", - "floatingButtonDesc": "デスクトップにフローティングボタンを表示し、アプリケーションウィンドウを素早く起動できます" + "floatingButtonDesc": "デスクトップにフローティングボタンを表示し、アプリケーションウィンドウを素早く起動できます", + "codeFontFamily": "等幅フォント", + "codeFontFamilyDesc": "コードブロックと固定幅領域に使用されます。", + "fontDefaultLabel": "デフォルト(内蔵フォントスタック)", + "fontFamily": "インターフェースフォント", + "fontFamilyDesc": "インターフェースのメインフォントを選択します。空白のままにすると、組み込みのフォント スタックが使用されます。", + "fontReset": "デフォルトにリセットする", + "fontSearchEmpty": "一致するフォントがありません", + "fontSearchPlaceholder": "フォントの検索", + "fontSystemLoading": "システムフォントを読み込んでいます...", + "fontTitle": "フォント", + "fontUsageHint": "一部のインターフェイスを有効にするには、アプリケーションを再起動する必要があります。フォントが使用できない場合は、自動的にデフォルトのフォントに戻ります。" }, "shortcuts": { "title": "ショートカットキー設定", @@ -959,6 +970,48 @@ "downloadUrl": "ダウンロードリンク", "installCommands": "インストールコマンド", "title": "外部依存関係が欠落している" + }, + "debug": { + "clearHistory": "明確な記録", + "close": "閉鎖", + "customMethod": "カスタムメソッド名", + "customMethodPlaceholder": "たとえばセッション/ping", + "customMethodRequired": "拡張メソッド名を入力してください", + "description": "ACP リクエストを「{name}」に送信し、リアルタイムで応答を表示します。", + "empty": "デバッグ イベントはまだありません。", + "entry": "デバッグ", + "eventCount": "{count} 個のアイテム", + "eventKinds": { + "error": "間違い", + "notification": "通知する", + "permission": "権限", + "request": "聞く", + "response": "応答" + }, + "events": "イベント", + "format": "JSON形式", + "methods": { + "cancel": "セッション/キャンセル", + "extMethod": "外部/メソッド", + "extNotification": "内線/通知", + "initialize": "初期化する", + "loadSession": "セッション/ロード", + "newSession": "セッション/新規", + "prompt": "セッション/プロンプト", + "setSessionMode": "セッション/セットモード", + "setSessionModel": "セッション/セットモデル" + }, + "needInitialize": "最初に初期化を実行してください", + "parseError": "JSONの解析に失敗しました", + "payloadHint": "JSON は送信前に調整できます。", + "processNotReady": "まず初期化してください", + "processReady": "プロセスの準備が完了しました", + "requestFailed": "リクエストが失敗しました", + "resetTemplate": "リカバリテンプレート", + "send": "リクエストの送信", + "sending": "送信中...", + "title": "ACPのデバッグ", + "workdirPlaceholder": "デフォルトの作業ディレクトリを使用するには空白のままにします" } } } diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index cdcf336f2..7aa6ce5bd 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -814,7 +814,18 @@ "text-xl": "XL", "text-2xl": "XXL", "floatingButton": "플로팅 버튼", - "floatingButtonDesc": "데스크톱에 플로팅 버튼을 표시하여 애플리케이션 창을 빠르게 활성화할 수 있습니다" + "floatingButtonDesc": "데스크톱에 플로팅 버튼을 표시하여 애플리케이션 창을 빠르게 활성화할 수 있습니다", + "codeFontFamily": "고정 폭 글꼴", + "codeFontFamilyDesc": "코드 블록 및 고정 너비 영역에 사용됩니다.", + "fontDefaultLabel": "기본값(내장 글꼴 스택)", + "fontFamily": "인터페이스 글꼴", + "fontFamilyDesc": "인터페이스의 기본 글꼴을 선택합니다. 비워두면 내장된 글꼴 스택을 사용합니다.", + "fontReset": "기본값으로 재설정", + "fontSearchEmpty": "일치하는 글꼴이 없습니다.", + "fontSearchPlaceholder": "글꼴 검색", + "fontSystemLoading": "시스템 글꼴 로드 중...", + "fontTitle": "세례반", + "fontUsageHint": "일부 인터페이스를 적용하려면 애플리케이션을 다시 시작해야 합니다. 글꼴을 사용할 수 없는 경우 자동으로 기본 글꼴로 돌아갑니다." }, "shortcuts": { "title": "단축키 설정", @@ -959,6 +970,48 @@ "downloadUrl": "다운로드 링크", "installCommands": "설치 명령", "title": "외부 종속성 누락" + }, + "debug": { + "clearHistory": "기록 지우기", + "close": "닫기", + "customMethod": "커스텀 메소드 이름", + "customMethodPlaceholder": "예를 들어 세션/핑", + "customMethodRequired": "확장 메서드 이름을 입력하세요.", + "description": "\"{name}\"에게 ACP 요청을 보내고 실시간으로 응답을 확인하세요.", + "empty": "아직 디버깅 이벤트가 없습니다.", + "entry": "디버그", + "eventCount": "{count}개 항목", + "eventKinds": { + "error": "오류", + "notification": "알림", + "permission": "권한", + "request": "요청", + "response": "응답" + }, + "events": "이벤트", + "format": "JSON 형식", + "methods": { + "cancel": "session/cancel", + "extMethod": "ext/method", + "extNotification": "ext/notification", + "initialize": "initialize", + "loadSession": "session/load", + "newSession": "session/new", + "prompt": "session/prompt", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel" + }, + "needInitialize": "먼저 초기화를 실행해 주세요", + "parseError": "JSON 구문 분석에 실패했습니다.", + "payloadHint": "전송하기 전에 JSON을 조정할 수 있습니다.", + "processNotReady": "먼저 초기화해주세요", + "processReady": "프로세스가 준비되었습니다.", + "requestFailed": "요청 실패", + "resetTemplate": "복구 템플릿", + "send": "요청 보내기", + "sending": "전송 중...", + "title": "ACP 디버깅", + "workdirPlaceholder": "기본 작업 디렉터리를 사용하려면 비워 두세요." } } } diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index 862d63c4a..034fe7ffd 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -814,7 +814,18 @@ "text-xl": "X-Grande", "text-2xl": "XX-Grande", "floatingButton": "Botão Flutuante", - "floatingButtonDesc": "Exibir um botão flutuante na área de trabalho para ativar rapidamente a janela do aplicativo" + "floatingButtonDesc": "Exibir um botão flutuante na área de trabalho para ativar rapidamente a janela do aplicativo", + "codeFontFamily": "fonte monoespaçada", + "codeFontFamilyDesc": "Usado para blocos de código e áreas de largura constante.", + "fontDefaultLabel": "Padrão (pilha de fontes integrada)", + "fontFamily": "Fonte da interface", + "fontFamilyDesc": "Selecione a fonte principal da interface. Se deixado em branco, use a pilha de fontes integrada.", + "fontReset": "redefinir para o padrão", + "fontSearchEmpty": "Nenhuma fonte correspondente", + "fontSearchPlaceholder": "Fonte de pesquisa", + "fontSystemLoading": "Carregando fontes do sistema...", + "fontTitle": "fonte", + "fontUsageHint": "Algumas interfaces exigem a reinicialização do aplicativo para que tenham efeito. Se a fonte não estiver disponível, ela retornará automaticamente à fonte padrão." }, "shortcuts": { "title": "Configurações de Teclas de Atalho", @@ -959,6 +970,48 @@ "downloadUrl": "Link de download", "installCommands": "Comandos de instalação", "title": "Dependências externas ausentes" + }, + "debug": { + "clearHistory": "limpar registro", + "close": "encerramento", + "customMethod": "Nome do método personalizado", + "customMethodPlaceholder": "Por exemplo sessão/ping", + "customMethodRequired": "Por favor preencha o nome do método de extensão", + "description": "Envie uma solicitação ACP para \"{name}\" e veja a resposta em tempo real.", + "empty": "Ainda não há eventos de depuração.", + "entry": "depurar", + "eventCount": "{count} itens", + "eventKinds": { + "error": "erro", + "notification": "notificar", + "permission": "Permissões", + "request": "perguntar", + "response": "resposta" + }, + "events": "evento", + "format": "Formatar JSON", + "methods": { + "cancel": "session/cancel", + "extMethod": "ext/method", + "extNotification": "ext/notification", + "initialize": "initialize", + "loadSession": "session/load", + "newSession": "session/new", + "prompt": "session/prompt", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel" + }, + "needInitialize": "Por favor execute a inicialização primeiro", + "parseError": "Falha na análise JSON", + "payloadHint": "JSON pode ser ajustado antes do envio.", + "processNotReady": "Por favor inicialize primeiro", + "processReady": "O processo está pronto", + "requestFailed": "Falha na solicitação", + "resetTemplate": "Modelo de recuperação", + "send": "Enviar solicitação", + "sending": "Enviando...", + "title": "Depuração ACP", + "workdirPlaceholder": "Deixe em branco para usar o diretório de trabalho padrão" } } } diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index 26f7dc715..a0f81b753 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -814,7 +814,18 @@ "text-xl": "XL", "text-2xl": "XXL", "floatingButton": "Плавающая кнопка", - "floatingButtonDesc": "Отображать плавающую кнопку на рабочем столе для быстрого активации окна приложения" + "floatingButtonDesc": "Отображать плавающую кнопку на рабочем столе для быстрого активации окна приложения", + "codeFontFamily": "моноширинный шрифт", + "codeFontFamilyDesc": "Используется для блоков кода и областей постоянной ширины.", + "fontDefaultLabel": "По умолчанию (встроенный стек шрифтов)", + "fontFamily": "Шрифт интерфейса", + "fontFamilyDesc": "Выберите основной шрифт интерфейса. Если оставить пустым, используйте встроенный стек шрифтов.", + "fontReset": "сбросить настройки по умолчанию", + "fontSearchEmpty": "Нет подходящего шрифта", + "fontSearchPlaceholder": "Поиск шрифта", + "fontSystemLoading": "Загрузка системных шрифтов...", + "fontTitle": "шрифт", + "fontUsageHint": "Для вступления в силу некоторых интерфейсов требуется перезапуск приложения. Если шрифт недоступен, он автоматически вернется к шрифту по умолчанию." }, "shortcuts": { "title": "Настройки сочетаний клавиш", @@ -959,6 +970,48 @@ "downloadUrl": "Ссылка для скачивания", "installCommands": "Команда установки", "title": "Отсутствуют внешние зависимости" + }, + "debug": { + "clearHistory": "Очистить историю", + "close": "Закрыть", + "customMethod": "Имя пользовательского метода", + "customMethodPlaceholder": "Например сеанс/пинг", + "customMethodRequired": "Пожалуйста, введите имя метода расширения", + "description": "Отправьте запрос ACP на «{name}» и просмотрите ответ в режиме реального времени.", + "empty": "Отладочных событий пока нет.", + "entry": "Отладка", + "eventCount": "{count} событий", + "eventKinds": { + "error": "ошибка", + "notification": "Уведомление", + "permission": "Разрешение", + "request": "Запрос", + "response": "Ответ" + }, + "events": "События", + "format": "Формат JSON", + "methods": { + "cancel": "сеанс/отмена", + "extMethod": "расширение/метод", + "extNotification": "расширение/уведомление", + "initialize": "инициализировать", + "loadSession": "сеанс/загрузка", + "newSession": "сессия/новая", + "prompt": "сеанс/подсказка", + "setSessionMode": "сеанс/setMode", + "setSessionModel": "сеанс/setModel" + }, + "needInitialize": "Пожалуйста, сначала выполните инициализацию", + "parseError": "Разбор JSON не удался", + "payloadHint": "JSON можно настроить перед отправкой.", + "processNotReady": "Пожалуйста, сначала инициализируйте", + "processReady": "Процесс готов", + "requestFailed": "Запрос не выполнен", + "resetTemplate": "Шаблон восстановления", + "send": "Отправить запрос", + "sending": "Отправка…", + "title": "Отладка ACP", + "workdirPlaceholder": "Оставьте пустым, чтобы использовать рабочий каталог по умолчанию." } } } diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 3b299947f..0ba74312a 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -807,6 +807,17 @@ "latestVersion": "已是最新版本" }, "display": { + "fontTitle": "字体", + "fontFamily": "界面字体", + "fontFamilyDesc": "选择界面主字体,留空时使用内置字体栈。", + "codeFontFamily": "等宽字体", + "codeFontFamilyDesc": "用于代码块和等宽区域。", + "fontDefaultLabel": "默认(内置字体栈)", + "fontSearchPlaceholder": "搜索字体", + "fontSearchEmpty": "没有匹配的字体", + "fontReset": "重置为默认", + "fontSystemLoading": "正在加载系统字体...", + "fontUsageHint": "部分界面需要重启应用后才会生效,如果字体不可用,将自动回落到默认字体。", "fontSize": "文字大小", "text-sm": "小", "text-base": "标准", @@ -933,6 +944,48 @@ "copy": "复制", "copied": "已复制到剪贴板", "copyFailed": "复制失败" + }, + "debug": { + "title": "ACP 调试", + "description": "向「{name}」发送 ACP 请求并实时查看返回。", + "entry": "调试", + "workdirPlaceholder": "留空使用默认工作目录", + "close": "关闭", + "customMethod": "自定义方法名", + "customMethodPlaceholder": "例如 session/ping", + "payloadHint": "发送前可以调整 JSON。", + "format": "格式化 JSON", + "resetTemplate": "恢复模板", + "clearHistory": "清空记录", + "send": "发送请求", + "sending": "发送中...", + "processReady": "进程已就绪", + "processNotReady": "请先初始化", + "needInitialize": "请先执行 initialize", + "events": "事件", + "eventCount": "{count} 条", + "empty": "暂无调试事件。", + "parseError": "JSON 解析失败", + "customMethodRequired": "请填写扩展方法名", + "requestFailed": "请求失败", + "methods": { + "initialize": "initialize", + "newSession": "session/new", + "loadSession": "session/load", + "prompt": "session/prompt", + "cancel": "session/cancel", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel", + "extMethod": "ext/method", + "extNotification": "ext/notification" + }, + "eventKinds": { + "request": "请求", + "response": "响应", + "notification": "通知", + "permission": "权限", + "error": "错误" + } } }, "rateLimit": { diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index c5a0b83ea..805bd266c 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -814,7 +814,18 @@ "text-xl": "特大", "text-2xl": "超大", "floatingButton": "懸浮按鈕", - "floatingButtonDesc": "在桌面顯示一個懸浮按鈕,可以快速喚起應用程式視窗" + "floatingButtonDesc": "在桌面顯示一個懸浮按鈕,可以快速喚起應用程式視窗", + "codeFontFamily": "等寬字體", + "codeFontFamilyDesc": "用於代碼塊和等寬區域。", + "fontDefaultLabel": "默認(內置字體棧)", + "fontFamily": "界面字體", + "fontFamilyDesc": "選擇界面主字體,留空時使用內置字體棧。", + "fontReset": "重置為默認", + "fontSearchEmpty": "沒有匹配的字體", + "fontSearchPlaceholder": "搜索字體", + "fontSystemLoading": "正在加載系統字體...", + "fontTitle": "字體", + "fontUsageHint": "部分界面需要重啟應用後才會生效,如果字體不可用,將自動回落到默認字體。" }, "shortcuts": { "title": "快捷鍵設置", @@ -959,6 +970,48 @@ "downloadUrl": "下載鏈接", "installCommands": "安裝命令", "title": "缺少外部依賴" + }, + "debug": { + "clearHistory": "清空記錄", + "close": "關閉", + "customMethod": "自定義方法名", + "customMethodPlaceholder": "例如 session/ping", + "customMethodRequired": "請填寫擴展方法名", + "description": "向「{name}」發送 ACP 請求並實時查看返回。", + "empty": "暫無調試事件。", + "entry": "偵錯", + "eventCount": "{count} 條", + "eventKinds": { + "error": "錯誤", + "notification": "通知", + "permission": "權限", + "request": "請求", + "response": "響應" + }, + "events": "事件", + "format": "格式化 JSON", + "methods": { + "cancel": "session/cancel", + "extMethod": "ext/method", + "extNotification": "ext/notification", + "initialize": "initialize", + "loadSession": "session/load", + "newSession": "session/new", + "prompt": "session/prompt", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel" + }, + "needInitialize": "請先執行 initialize", + "parseError": "JSON 解析失敗", + "payloadHint": "發送前可以調整 JSON。", + "processNotReady": "請先初始化", + "processReady": "進程已就緒", + "requestFailed": "請求失敗", + "resetTemplate": "恢復模板", + "send": "發送請求", + "sending": "發送中...", + "title": "ACP 調試", + "workdirPlaceholder": "留空使用默認工作目錄" } } } diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index f887800e6..c6cb16423 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -814,7 +814,18 @@ "text-xl": "特大", "text-2xl": "超大", "floatingButton": "懸浮按鈕", - "floatingButtonDesc": "在桌面顯示一個懸浮按鈕,可以快速喚起應用程式視窗" + "floatingButtonDesc": "在桌面顯示一個懸浮按鈕,可以快速喚起應用程式視窗", + "codeFontFamily": "等寬字體", + "codeFontFamilyDesc": "用於代碼塊和等寬區域。", + "fontDefaultLabel": "默認(內置字體棧)", + "fontFamily": "界面字體", + "fontFamilyDesc": "選擇界面主字體,留空時使用內置字體棧。", + "fontReset": "重置為默認", + "fontSearchEmpty": "沒有匹配的字體", + "fontSearchPlaceholder": "搜索字體", + "fontSystemLoading": "正在加載系統字體...", + "fontTitle": "字體", + "fontUsageHint": "部分界面需要重啟應用後才會生效,如果字體不可用,將自動回落到默認字體。" }, "shortcuts": { "title": "快捷鍵設置", @@ -959,6 +970,48 @@ }, "title": "初始化終端", "waiting": "等待初始化開始..." + }, + "debug": { + "clearHistory": "清空記錄", + "close": "關閉", + "customMethod": "自定義方法名", + "customMethodPlaceholder": "例如 session/ping", + "customMethodRequired": "請填寫擴展方法名", + "description": "向「{name}」發送 ACP 請求並實時查看返回。", + "empty": "暫無調試事件。", + "entry": "偵錯", + "eventCount": "{count} 條", + "eventKinds": { + "error": "錯誤", + "notification": "通知", + "permission": "權限", + "request": "請求", + "response": "響應" + }, + "events": "事件", + "format": "格式化 JSON", + "methods": { + "cancel": "session/cancel", + "extMethod": "ext/method", + "extNotification": "ext/notification", + "initialize": "initialize", + "loadSession": "session/load", + "newSession": "session/new", + "prompt": "session/prompt", + "setSessionMode": "session/setMode", + "setSessionModel": "session/setModel" + }, + "needInitialize": "請先執行 initialize", + "parseError": "JSON 解析失敗", + "payloadHint": "發送前可以調整 JSON。", + "processNotReady": "請先初始化", + "processReady": "進程已就緒", + "requestFailed": "請求失敗", + "resetTemplate": "恢復模板", + "send": "發送請求", + "sending": "發送中...", + "title": "ACP 調試", + "workdirPlaceholder": "留空使用默認工作目錄" } } } diff --git a/src/renderer/src/stores/language.ts b/src/renderer/src/stores/language.ts index 2a5377350..f6b38d2a5 100644 --- a/src/renderer/src/stores/language.ts +++ b/src/renderer/src/stores/language.ts @@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n' import { usePresenter } from '@/composables/usePresenter' import { CONFIG_EVENTS } from '@/events' -const RTL_LIST = ['fa-IR'] +const RTL_LIST = ['fa-IR', 'he-IL'] export const useLanguageStore = defineStore('language', () => { const { locale } = useI18n({ useScope: 'global' }) const language = ref('system') diff --git a/src/renderer/src/stores/searchAssistantStore.ts b/src/renderer/src/stores/searchAssistantStore.ts index 9434b2131..8127d1874 100644 --- a/src/renderer/src/stores/searchAssistantStore.ts +++ b/src/renderer/src/stores/searchAssistantStore.ts @@ -26,6 +26,13 @@ export const useSearchAssistantStore = defineStore('searchAssistant', () => { const searchAssistantModel = computed(() => modelRef.value) const searchAssistantProvider = computed(() => providerRef.value) + const isProviderAllowed = (providerId?: string) => providerId !== 'acp' + const isModelEnabledForProvider = (model: RENDERER_MODEL_META, providerId: string) => + enabledModels.value.some( + (provider) => + provider.providerId === providerId && + provider.models.some((enabledModel) => enabledModel.id === model.id) + ) const findPriorityModel = (): { model: RENDERER_MODEL_META; providerId: string } | null => { if (!enabledModels.value || enabledModels.value.length === 0) { @@ -34,6 +41,9 @@ export const useSearchAssistantStore = defineStore('searchAssistant', () => { for (const keyword of priorities) { for (const providerModels of enabledModels.value) { + if (!isProviderAllowed(providerModels.providerId)) { + continue + } for (const model of providerModels.models) { if ( model.id.toLowerCase().includes(keyword.toLowerCase()) || @@ -49,6 +59,7 @@ export const useSearchAssistantStore = defineStore('searchAssistant', () => { } const fallback = enabledModels.value + .filter((provider) => isProviderAllowed(provider.providerId)) .flatMap((provider) => provider.models.map((model) => ({ ...model, providerId: provider.providerId })) ) @@ -65,6 +76,10 @@ export const useSearchAssistantStore = defineStore('searchAssistant', () => { } const setSearchAssistantModel = async (model: RENDERER_MODEL_META, providerId: string) => { + if (!isProviderAllowed(providerId)) { + await initOrUpdateSearchAssistantModel() + return + } const rawModel = toRaw(model) modelRef.value = rawModel providerRef.value = providerId @@ -82,10 +97,18 @@ export const useSearchAssistantStore = defineStore('searchAssistant', () => { 'searchAssistantModel' ) savedModel = toRaw(savedModel) - if (savedModel) { - modelRef.value = savedModel.model - providerRef.value = savedModel.providerId - threadP.setSearchAssistantModel(savedModel.model, savedModel.providerId) + const hasEnabledModels = enabledModels.value.length > 0 + const usableSavedModel = + savedModel && + isProviderAllowed(savedModel.providerId) && + (!hasEnabledModels || isModelEnabledForProvider(savedModel.model, savedModel.providerId)) + ? savedModel + : null + + if (usableSavedModel) { + modelRef.value = usableSavedModel.model + providerRef.value = usableSavedModel.providerId + threadP.setSearchAssistantModel(usableSavedModel.model, usableSavedModel.providerId) return } @@ -113,9 +136,14 @@ export const useSearchAssistantStore = defineStore('searchAssistant', () => { return } - const stillAvailable = enabledModels.value.some((provider) => - provider.models.some((model) => model.id === currentModel.id) - ) + const resolvedProviderId = providerRef.value || currentModel.providerId || '' + if (!isProviderAllowed(resolvedProviderId)) { + await initOrUpdateSearchAssistantModel() + return + } + + const stillAvailable = + resolvedProviderId !== '' && isModelEnabledForProvider(currentModel, resolvedProviderId) if (!stillAvailable) { await initOrUpdateSearchAssistantModel() diff --git a/src/renderer/src/stores/uiSettingsStore.ts b/src/renderer/src/stores/uiSettingsStore.ts index 5c30270cb..5bca34005 100644 --- a/src/renderer/src/stores/uiSettingsStore.ts +++ b/src/renderer/src/stores/uiSettingsStore.ts @@ -5,11 +5,27 @@ import { CONFIG_EVENTS } from '@/events' const FONT_SIZE_CLASSES = ['text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl'] const DEFAULT_FONT_SIZE_LEVEL = 1 +const DEFAULT_FONT_STACK = + "'Geist', Noto Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'" +const DEFAULT_CODE_FONT_STACK = + "'JetBrains Mono', 'Fira Code', 'Menlo', 'Monaco', 'Consolas', 'Courier New', monospace" + +const buildFontStack = (custom: string, fallback: string) => { + const normalized = (custom || '').trim() + if (!normalized) return fallback + const wrapped = + /\s/.test(normalized) && !normalized.includes(',') ? `"${normalized}"` : normalized + return `${wrapped}, ${fallback}` +} export const useUiSettingsStore = defineStore('uiSettings', () => { const configP = usePresenter('configPresenter') const fontSizeLevel = ref(DEFAULT_FONT_SIZE_LEVEL) + const fontFamily = ref('') + const codeFontFamily = ref('') + const systemFonts = ref([]) + const isLoadingFonts = ref(false) const artifactsEffectEnabled = ref(false) const searchPreviewEnabled = ref(true) const contentProtectionEnabled = ref(false) @@ -22,12 +38,19 @@ export const useUiSettingsStore = defineStore('uiSettings', () => { () => FONT_SIZE_CLASSES[fontSizeLevel.value] || FONT_SIZE_CLASSES[DEFAULT_FONT_SIZE_LEVEL] ) + const formattedFontFamily = computed(() => buildFontStack(fontFamily.value, DEFAULT_FONT_STACK)) + const formattedCodeFontFamily = computed(() => + buildFontStack(codeFontFamily.value, DEFAULT_CODE_FONT_STACK) + ) + const loadSettings = async () => { fontSizeLevel.value = (await configP.getSetting('fontSizeLevel')) ?? DEFAULT_FONT_SIZE_LEVEL if (fontSizeLevel.value < 0 || fontSizeLevel.value >= FONT_SIZE_CLASSES.length) { fontSizeLevel.value = DEFAULT_FONT_SIZE_LEVEL } + fontFamily.value = (await configP.getFontFamily()) ?? '' + codeFontFamily.value = (await configP.getCodeFontFamily()) ?? '' artifactsEffectEnabled.value = (await configP.getSetting('artifactsEffectEnabled')) ?? false searchPreviewEnabled.value = await configP.getSearchPreviewEnabled() @@ -44,6 +67,35 @@ export const useUiSettingsStore = defineStore('uiSettings', () => { await configP.setSetting('fontSizeLevel', validLevel) } + const setFontFamily = async (value: string) => { + fontFamily.value = (value || '').trim() + await configP.setFontFamily(fontFamily.value) + } + + const setCodeFontFamily = async (value: string) => { + codeFontFamily.value = (value || '').trim() + await configP.setCodeFontFamily(codeFontFamily.value) + } + + const resetFontSettings = async () => { + fontFamily.value = '' + codeFontFamily.value = '' + await configP.resetFontSettings() + } + + const fetchSystemFonts = async () => { + if (isLoadingFonts.value || systemFonts.value.length > 0) return + isLoadingFonts.value = true + try { + const fonts = await configP.getSystemFonts() + systemFonts.value = fonts || [] + } catch (error) { + console.warn('Failed to fetch system fonts', error) + } finally { + isLoadingFonts.value = false + } + } + const setSearchPreviewEnabled = async (enabled: boolean) => { searchPreviewEnabled.value = enabled await configP.setSearchPreviewEnabled(enabled) @@ -99,6 +151,12 @@ export const useUiSettingsStore = defineStore('uiSettings', () => { window.electron.ipcRenderer.on(CONFIG_EVENTS.NOTIFICATIONS_CHANGED, (_event, value) => { notificationsEnabled.value = value }) + window.electron.ipcRenderer.on(CONFIG_EVENTS.FONT_FAMILY_CHANGED, (_event, value) => { + fontFamily.value = value ?? '' + }) + window.electron.ipcRenderer.on(CONFIG_EVENTS.CODE_FONT_FAMILY_CHANGED, (_event, value) => { + codeFontFamily.value = value ?? '' + }) } onMounted(() => { @@ -114,11 +172,19 @@ export const useUiSettingsStore = defineStore('uiSettings', () => { window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.COPY_WITH_COT_CHANGED) window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.TRACE_DEBUG_CHANGED) window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.NOTIFICATIONS_CHANGED) + window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.FONT_FAMILY_CHANGED) + window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.CODE_FONT_FAMILY_CHANGED) }) return { fontSizeLevel, fontSizeClass, + fontFamily, + codeFontFamily, + systemFonts, + isLoadingFonts, + formattedFontFamily, + formattedCodeFontFamily, artifactsEffectEnabled, searchPreviewEnabled, contentProtectionEnabled, @@ -127,6 +193,10 @@ export const useUiSettingsStore = defineStore('uiSettings', () => { notificationsEnabled, loggingEnabled, updateFontSizeLevel, + setFontFamily, + setCodeFontFamily, + resetFontSettings, + fetchSystemFonts, setSearchPreviewEnabled, setArtifactsEffectEnabled, setContentProtectionEnabled, diff --git a/src/shared/types/presenters/legacy.presenters.d.ts b/src/shared/types/presenters/legacy.presenters.d.ts index ba893e731..bc062b8b2 100644 --- a/src/shared/types/presenters/legacy.presenters.d.ts +++ b/src/shared/types/presenters/legacy.presenters.d.ts @@ -439,6 +439,13 @@ export interface IConfigPresenter { // Chain of Thought copy settings getCopyWithCotEnabled(): boolean setCopyWithCotEnabled(enabled: boolean): void + // Font settings + getFontFamily(): string + setFontFamily(fontFamily?: string | null): void + getCodeFontFamily(): string + setCodeFontFamily(fontFamily?: string | null): void + resetFontSettings(): void + getSystemFonts(): Promise // Floating button settings getFloatingButtonEnabled(): boolean setFloatingButtonEnabled(enabled: boolean): void @@ -686,6 +693,47 @@ export type LLM_EMBEDDING_ATTRS = { normalized: boolean } +export type AcpDebugActionType = + | 'initialize' + | 'newSession' + | 'loadSession' + | 'prompt' + | 'cancel' + | 'setSessionMode' + | 'setSessionModel' + | 'extMethod' + | 'extNotification' + +export type AcpDebugEventKind = 'request' | 'response' | 'notification' | 'permission' | 'error' + +export interface AcpDebugRequest { + agentId: string + action: AcpDebugActionType + payload?: Record + sessionId?: string + workdir?: string + methodName?: string + webContentsId?: number +} + +export interface AcpDebugEventEntry { + id: string + kind: AcpDebugEventKind + action: string + agentId: string + sessionId?: string + timestamp: number + payload?: unknown + message?: string +} + +export interface AcpDebugRunResult { + status: 'ok' | 'error' + sessionId?: string + error?: string + events: AcpDebugEventEntry[] +} + export type AcpBuiltinAgentId = 'kimi-cli' | 'claude-code-acp' | 'codex-acp' export interface AcpAgentProfile { @@ -789,6 +837,7 @@ export interface ILlmProviderPresenter { getProviders(): LLM_PROVIDER[] getProviderById(id: string): LLM_PROVIDER isAgentProvider(providerId: string): boolean + getExistingProviderInstance?(providerId: string): unknown getModelList(providerId: string): Promise updateModelStatus(providerId: string, modelId: string, enabled: boolean): Promise addCustomModel( @@ -873,13 +922,27 @@ export interface ILlmProviderPresenter { ): Promise getAcpWorkdir(conversationId: string, agentId: string): Promise setAcpWorkdir(conversationId: string, agentId: string, workdir: string | null): Promise + warmupAcpProcess(agentId: string, workdir: string): Promise + getAcpProcessModes( + agentId: string, + workdir: string + ): Promise< + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined + > + setAcpPreferredProcessMode(agentId: string, workdir: string, modeId: string): Promise setAcpSessionMode(conversationId: string, modeId: string): Promise getAcpSessionModes(conversationId: string): Promise<{ current: string available: Array<{ id: string; name: string; description: string }> } | null> resolveAgentPermission(requestId: string, granted: boolean): Promise + runAcpDebugAction(request: AcpDebugRequest): Promise getProviderInstance(providerId: string): unknown + getExistingProviderInstance?(providerId: string): unknown } export type CONVERSATION_SETTINGS = { @@ -1002,6 +1065,18 @@ export interface IThreadPresenter { destroy(): void getAcpWorkdir(conversationId: string, agentId: string): Promise setAcpWorkdir(conversationId: string, agentId: string, workdir: string | null): Promise + warmupAcpProcess(agentId: string, workdir: string): Promise + getAcpProcessModes( + agentId: string, + workdir: string + ): Promise< + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined + > + setAcpPreferredProcessMode(agentId: string, workdir: string, modeId: string): Promise setAcpSessionMode(conversationId: string, modeId: string): Promise getAcpSessionModes(conversationId: string): Promise<{ current: string diff --git a/src/shared/types/presenters/llmprovider.presenter.d.ts b/src/shared/types/presenters/llmprovider.presenter.d.ts index f15b62b78..89ab6abcb 100644 --- a/src/shared/types/presenters/llmprovider.presenter.d.ts +++ b/src/shared/types/presenters/llmprovider.presenter.d.ts @@ -1,7 +1,7 @@ import { ShowResponse } from 'ollama' import { ChatMessage, LLMAgentEvent } from '../core/chat' import { ModelType } from '../core/model' -import type { AcpWorkdirInfo } from './legacy.presenters' +import type { AcpDebugRequest, AcpDebugRunResult, AcpWorkdirInfo } from './legacy.presenters' /** * LLM Provider Presenter Interface @@ -142,6 +142,7 @@ export interface ILlmProviderPresenter { getProviderById(id: string): LLM_PROVIDER isAgentProvider(providerId: string): boolean getProviderInstance(providerId: string): unknown + getExistingProviderInstance(providerId: string): unknown getModelList(providerId: string): Promise updateModelStatus(providerId: string, modelId: string, enabled: boolean): Promise addCustomModel( @@ -227,10 +228,23 @@ export interface ILlmProviderPresenter { getAcpWorkdir(conversationId: string, agentId: string): Promise setAcpWorkdir(conversationId: string, agentId: string, workdir: string | null): Promise + warmupAcpProcess(agentId: string, workdir: string): Promise + getAcpProcessModes( + agentId: string, + workdir: string + ): Promise< + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined + > + setAcpPreferredProcessMode(agentId: string, workdir: string, modeId: string): Promise setAcpSessionMode(conversationId: string, modeId: string): Promise getAcpSessionModes(conversationId: string): Promise<{ current: string available: Array<{ id: string; name: string; description: string }> } | null> + runAcpDebugAction(request: AcpDebugRequest): Promise resolveAgentPermission(requestId: string, granted: boolean): Promise } diff --git a/src/shared/types/presenters/thread.presenter.d.ts b/src/shared/types/presenters/thread.presenter.d.ts index e72c6f772..0edf4ed9d 100644 --- a/src/shared/types/presenters/thread.presenter.d.ts +++ b/src/shared/types/presenters/thread.presenter.d.ts @@ -177,6 +177,18 @@ export interface IThreadPresenter { // ACP workdir controls getAcpWorkdir(conversationId: string, agentId: string): Promise setAcpWorkdir(conversationId: string, agentId: string, workdir: string | null): Promise + warmupAcpProcess(agentId: string, workdir: string): Promise + getAcpProcessModes( + agentId: string, + workdir: string + ): Promise< + | { + availableModes?: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined + > + setAcpPreferredProcessMode(agentId: string, workdir: string, modeId: string): Promise setAcpSessionMode(conversationId: string, modeId: string): Promise getAcpSessionModes(conversationId: string): Promise<{ current: string diff --git a/test/main/presenter/acpProvider.test.ts b/test/main/presenter/acpProvider.test.ts new file mode 100644 index 000000000..0db4feac4 --- /dev/null +++ b/test/main/presenter/acpProvider.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi } from 'vitest' +import { AcpProvider } from '../../../src/main/presenter/llmProviderPresenter/providers/acpProvider' + +vi.mock('electron', () => ({ + app: { + getVersion: vi.fn(() => '0.0.0-test') + } +})) + +vi.mock('@/eventbus', () => ({ + eventBus: { + on: vi.fn(), + sendToRenderer: vi.fn(), + emit: vi.fn(), + send: vi.fn() + }, + SendTarget: { + ALL_WINDOWS: 'ALL_WINDOWS' + } +})) + +vi.mock('@/presenter', () => ({ + presenter: { + mcpPresenter: { + getAllToolDefinitions: vi.fn().mockResolvedValue([]), + callTool: vi.fn().mockResolvedValue({ content: '', rawData: {} }) + } + } +})) + +vi.mock('@/presenter/proxyConfig', () => ({ + proxyConfig: { + getProxyUrl: vi.fn().mockReturnValue(null) + } +})) + +describe('AcpProvider runDebugAction error handling', () => { + const agent = { id: 'agent1', name: 'Agent 1' } + + it('returns error result when process manager is shutting down', async () => { + const provider = Object.create(AcpProvider.prototype) as any + provider.configPresenter = { + getAcpAgents: vi.fn().mockResolvedValue([agent]) + } + provider.processManager = { + getConnection: vi + .fn() + .mockRejectedValue(new Error('[ACP] Process manager is shutting down, refusing to spawn')) + } + + const result = await provider.runDebugAction({ + agentId: 'agent1', + action: 'initialize', + workdir: '/tmp' + } as any) + + expect(result).toEqual({ + status: 'error', + sessionId: undefined, + error: 'Process manager is shutting down', + events: [] + }) + }) + + it('rethrows non-shutdown getConnection errors', async () => { + const provider = Object.create(AcpProvider.prototype) as any + provider.configPresenter = { + getAcpAgents: vi.fn().mockResolvedValue([agent]) + } + provider.processManager = { + getConnection: vi.fn().mockRejectedValue(new Error('boom')) + } + + await expect( + provider.runDebugAction({ + agentId: 'agent1', + action: 'initialize', + workdir: '/tmp' + } as any) + ).rejects.toThrow('boom') + }) +}) diff --git a/test/main/presenter/acpSessionManager.test.ts b/test/main/presenter/acpSessionManager.test.ts new file mode 100644 index 000000000..8b9ac651c --- /dev/null +++ b/test/main/presenter/acpSessionManager.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi } from 'vitest' +import { AcpSessionManager } from '../../../src/main/presenter/llmProviderPresenter/agent/acpSessionManager' + +vi.mock('electron', () => ({ + app: { + on: vi.fn() + } +})) + +describe('AcpSessionManager createSession error handling', () => { + const agent = { id: 'agent1', name: 'Agent 1' } + + it('throws explicit shutdown error when process manager is shutting down', async () => { + const manager = Object.create(AcpSessionManager.prototype) as any + manager.processManager = { + getConnection: vi + .fn() + .mockRejectedValue( + new Error('[ACP] Process manager is shutting down, refusing to spawn new process') + ) + } + + await expect(manager.createSession('conv1', agent as any, {} as any, '/tmp')).rejects.toThrow( + '[ACP] Cannot create session: process manager is shutting down' + ) + }) + + it('rethrows non-shutdown getConnection errors', async () => { + const manager = Object.create(AcpSessionManager.prototype) as any + manager.processManager = { + getConnection: vi.fn().mockRejectedValue(new Error('boom')) + } + + await expect(manager.createSession('conv1', agent as any, {} as any, '/tmp')).rejects.toThrow( + 'boom' + ) + }) +}) diff --git a/test/main/presenter/configPresenter/uiSettingsHelper.test.ts b/test/main/presenter/configPresenter/uiSettingsHelper.test.ts new file mode 100644 index 000000000..c8f581d4f --- /dev/null +++ b/test/main/presenter/configPresenter/uiSettingsHelper.test.ts @@ -0,0 +1,44 @@ +import fontList from 'font-list' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { UiSettingsHelper } from '@/presenter/configPresenter/uiSettingsHelper' + +vi.mock('font-list', () => { + const getFonts = vi.fn() + return { default: { getFonts } } +}) + +const getFontsMock = vi.mocked(fontList.getFonts) + +const createHelper = () => + new UiSettingsHelper({ + getSetting: () => undefined, + setSetting: () => undefined + }) + +describe('UiSettingsHelper.getSystemFonts', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('normalizes and caches fonts from font-list', async () => { + getFontsMock.mockResolvedValue(['Inter Regular', 'Inter Bold', 'Menlo']) + const helper = createHelper() + + const fonts = await helper.getSystemFonts() + const cachedFonts = await helper.getSystemFonts() + + expect(getFontsMock).toHaveBeenCalledTimes(1) + expect(fonts).toEqual(['Inter', 'Menlo']) + expect(cachedFonts).toBe(fonts) + }) + + it('returns an empty array when font detection fails', async () => { + getFontsMock.mockRejectedValue(new Error('failed to load')) + const helper = createHelper() + + const fonts = await helper.getSystemFonts() + + expect(fonts).toEqual([]) + }) +}) diff --git a/test/main/presenter/llmProviderPresenter.test.ts b/test/main/presenter/llmProviderPresenter.test.ts index 7798e6529..9a50718b9 100644 --- a/test/main/presenter/llmProviderPresenter.test.ts +++ b/test/main/presenter/llmProviderPresenter.test.ts @@ -541,6 +541,36 @@ describe('LLMProviderPresenter Integration Tests', () => { }).toThrow('Provider non-existent not found') }) + it('should swallow ACP warmup shutdown errors', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const mockAcpProvider = { + warmupProcess: vi + .fn() + .mockRejectedValue(new Error('[ACP] Process manager is shutting down, refusing to spawn')) + } + vi.spyOn(llmProviderPresenter as any, 'getAcpProviderInstance').mockReturnValue( + mockAcpProvider as any + ) + + await expect( + llmProviderPresenter.warmupAcpProcess('agent-test', '/tmp') + ).resolves.toBeUndefined() + warnSpy.mockRestore() + }) + + it('should rethrow non-shutdown ACP warmup errors', async () => { + const mockAcpProvider = { + warmupProcess: vi.fn().mockRejectedValue(new Error('boom')) + } + vi.spyOn(llmProviderPresenter as any, 'getAcpProviderInstance').mockReturnValue( + mockAcpProvider as any + ) + + await expect(llmProviderPresenter.warmupAcpProcess('agent-test', '/tmp')).rejects.toThrow( + 'boom' + ) + }) + it('should handle provider check failure for invalid config', async () => { // 创建一个无效配置的provider const invalidProvider: LLM_PROVIDER = {