-
-
-
-
diff --git a/src/renderer/src/components/chat-input/ChatInput.vue b/src/renderer/src/components/chat-input/ChatInput.vue
index 072c9e13f..51f8074ed 100644
--- a/src/renderer/src/components/chat-input/ChatInput.vue
+++ b/src/renderer/src/components/chat-input/ChatInput.vue
@@ -70,6 +70,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ mode.label }}
+
+
+
+
+
+
+
+
+ {{ t('chat.mode.current', { mode: chatMode.currentLabel.value }) }}
+
+
+
+
+
+
+
+
+
+
+ {{ workspace.tooltipTitle }}
+
+
+ {{ workspace.tooltipCurrent }}
+
+
+ {{ workspace.tooltipSelect }}
+
+
+
+
-
+
-
-
-
-
-
-
- {{ t('chat.input.acpWorkdirTooltip') }}
-
-
- {{ t('chat.input.acpWorkdirCurrent', { path: acpWorkdir.workdir.value }) }}
-
-
- {{ t('chat.input.acpWorkdirSelect') }}
-
-
-
-
@@ -377,6 +430,7 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from '@shadcn/components/ui/popover'
import { Icon } from '@iconify/vue'
import { Editor, EditorContent } from '@tiptap/vue-3'
+import { TextSelection } from '@tiptap/pm/state'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
@@ -404,6 +458,9 @@ import { useContextLength } from './composables/useContextLength'
import { useSendButtonState } from './composables/useSendButtonState'
import { useAcpWorkdir } from './composables/useAcpWorkdir'
import { useAcpMode } from './composables/useAcpMode'
+import { useChatMode, type ChatMode } from './composables/useChatMode'
+import { useAgentWorkspace } from './composables/useAgentWorkspace'
+import { useWorkspaceMention } from './composables/useWorkspaceMention'
// === Stores ===
import { useChatStore } from '@/stores/chat'
@@ -412,8 +469,11 @@ import { useThemeStore } from '@/stores/theme'
// === Mention System ===
import { Mention } from '../editor/mention/mention'
-import suggestion, { setPromptFilesHandler } from '../editor/mention/suggestion'
-import { mentionData } from '../editor/mention/suggestion'
+import suggestion, {
+ setPromptFilesHandler,
+ setWorkspaceMention
+} from '../editor/mention/suggestion'
+import { mentionData, type CategorizedData } from '../editor/mention/suggestion'
import { useEventListener } from '@vueuse/core'
// === Props & Emits ===
@@ -502,7 +562,12 @@ const showFakeCaret = computed(() => caretVisible.value && !props.disabled)
// === Composable Integrations ===
// Initialize settings management
-const { settings, toggleWebSearch } = useInputSettings()
+const { settings, setWebSearch, toggleWebSearch } = useInputSettings()
+
+// Initialize chat mode management
+const chatMode = useChatMode()
+const modeSelectOpen = ref(false)
+const canUseWebSearch = computed(() => chatMode.currentMode.value === 'chat')
// Initialize history composable first (needed for editor placeholder)
const history = useInputHistory(null as any, t)
@@ -674,6 +739,20 @@ const acpWorkdir = useAcpWorkdir({
conversationId
})
+// Unified workspace management (for agent and acp agent modes)
+const workspace = useAgentWorkspace({
+ conversationId,
+ activeModel: activeModelSource,
+ chatMode
+})
+
+const workspaceMention = useWorkspaceMention({
+ workspacePath: workspace.workspacePath,
+ chatMode: chatMode.currentMode,
+ conversationId
+})
+setWorkspaceMention(workspaceMention)
+
// Extract isStreaming first so we can pass it to useAcpMode
const { disabledSend, isStreaming } = sendButtonState
@@ -716,7 +795,7 @@ const emitSend = async () => {
text: editorComposable.inputText.value.trim(),
files: files.selectedFiles.value,
links: [],
- search: settings.value.webSearch,
+ search: canUseWebSearch.value ? settings.value.webSearch : false,
think: settings.value.deepThinking,
content: blocks
}
@@ -735,9 +814,22 @@ const emitSend = async () => {
}
const onWebSearchClick = async () => {
+ if (!canUseWebSearch.value) return
await toggleWebSearch()
}
+const handleModeSelect = async (mode: ChatMode) => {
+ await chatMode.setMode(mode)
+ if (conversationId.value && chatMode.currentMode.value === mode) {
+ try {
+ await chatStore.updateChatConfig({ chatMode: mode })
+ } catch (error) {
+ console.warn('Failed to update chat mode in conversation settings:', error)
+ }
+ }
+ modeSelectOpen.value = false
+}
+
const onKeydown = (e: KeyboardEvent) => {
if (e.code === 'Enter' && !e.shiftKey) {
editorComposable.handleEditorEnter(e, disabledSend.value, emitSend)
@@ -787,6 +879,57 @@ const handleVisibilityChange = () => {
}
}
+const hasContextMention = () => {
+ let found = false
+ editor.state.doc.descendants((node) => {
+ if (node.type.name === 'mention' && node.attrs?.category === 'context') {
+ found = true
+ return false
+ }
+ return true
+ })
+ return found
+}
+
+const setCaretToEnd = () => {
+ if (editor.isDestroyed) return
+ const { state, view } = editor
+ const endSelection = TextSelection.atEnd(state.doc)
+ view.dispatch(state.tr.setSelection(endSelection))
+ editor.commands.focus()
+}
+
+const scheduleCaretToEnd = () => {
+ const delays = [0, 50, 120]
+ for (const delay of delays) {
+ setTimeout(() => {
+ if (editor.isDestroyed) return
+ requestAnimationFrame(() => {
+ setCaretToEnd()
+ })
+ }, delay)
+ }
+}
+
+const appendCustomMention = (mention: CategorizedData) => {
+ const shouldInsertAtEnd = mention.category === 'context'
+ if (shouldInsertAtEnd && hasContextMention()) {
+ scheduleCaretToEnd()
+ return true
+ }
+ if (shouldInsertAtEnd) {
+ setCaretToEnd()
+ }
+ const insertPosition = shouldInsertAtEnd
+ ? editor.state.selection.to
+ : editor.state.selection.anchor
+ const inserted = editorComposable.insertMentionToEditor(mention, insertPosition)
+ if (inserted && shouldInsertAtEnd) {
+ scheduleCaretToEnd()
+ }
+ return inserted
+}
+
useEventListener(window, 'resize', updateFakeCaretPosition)
useEventListener(editorContainer, 'scroll', updateFakeCaretPosition)
@@ -831,6 +974,8 @@ onUnmounted(() => {
if (caretAnimationFrame) {
cancelAnimationFrame(caretAnimationFrame)
}
+
+ setWorkspaceMention(null)
})
// === Watchers ===
@@ -856,12 +1001,48 @@ watch(
}
)
+watch(
+ () => [chatMode.currentMode.value, settings.value.webSearch] as const,
+ ([mode, webSearch]) => {
+ if (mode !== 'chat' && webSearch) {
+ void setWebSearch(false)
+ }
+ },
+ { immediate: true }
+)
+
+watch(
+ () => [conversationId.value, chatStore.chatConfig.chatMode] as const,
+ async ([activeId, storedMode]) => {
+ if (!activeId) return
+ try {
+ if (!storedMode) {
+ await chatStore.updateChatConfig({ chatMode: chatMode.currentMode.value })
+ return
+ }
+ if (chatMode.currentMode.value !== storedMode) {
+ await chatMode.setMode(storedMode)
+ }
+ } catch (error) {
+ console.warn('Failed to sync chat mode for conversation:', error)
+ }
+ },
+ { immediate: true }
+)
+
// === Expose ===
defineExpose({
clearContent: editorComposable.clearContent,
appendText: editorComposable.appendText,
appendMention: (name: string) => editorComposable.appendMention(name, mentionData),
- restoreFocus
+ appendCustomMention,
+ restoreFocus,
+ getAgentWorkspacePath: () => {
+ const mode = chatMode.currentMode.value
+ if (mode !== 'agent') return null
+ return workspace.workspacePath.value
+ },
+ getChatMode: () => chatMode.currentMode.value
})
diff --git a/src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts b/src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts
index 67a4a7d59..654845cbb 100644
--- a/src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts
+++ b/src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts
@@ -33,6 +33,19 @@ export function useAcpWorkdir(options: UseAcpWorkdirOptions) {
chatStore.setAcpWorkdirPreference(agentId.value, value)
}
+ const hydrateFromPreference = () => {
+ if (!agentId.value) return
+ const stored = chatStore.chatConfig.acpWorkdirMap?.[agentId.value] ?? null
+ if (stored) {
+ workdir.value = stored
+ isCustom.value = true
+ pendingWorkdir.value = stored
+ } else {
+ pendingWorkdir.value = null
+ resetToDefault()
+ }
+ }
+
const resetToDefault = () => {
workdir.value = ''
isCustom.value = false
@@ -65,7 +78,7 @@ export function useAcpWorkdir(options: UseAcpWorkdirOptions) {
if (!options.conversationId.value || !agentId.value) {
if (!pendingWorkdir.value) {
- resetToDefault()
+ hydrateFromPreference()
}
return
}
@@ -126,8 +139,7 @@ export function useAcpWorkdir(options: UseAcpWorkdirOptions) {
watch(agentId, () => {
if (!hasConversation.value) {
- pendingWorkdir.value = null
- resetToDefault()
+ hydrateFromPreference()
}
lastWarmupKey.value = null
})
diff --git a/src/renderer/src/components/chat-input/composables/useAgentMcpData.ts b/src/renderer/src/components/chat-input/composables/useAgentMcpData.ts
new file mode 100644
index 000000000..c89d5d600
--- /dev/null
+++ b/src/renderer/src/components/chat-input/composables/useAgentMcpData.ts
@@ -0,0 +1,47 @@
+import { computed } from 'vue'
+import { useChatStore } from '@/stores/chat'
+import { useMcpStore } from '@/stores/mcp'
+
+const CUSTOM_PROMPTS_CLIENT = 'deepchat/custom-prompts-server'
+
+export function useAgentMcpData() {
+ const chatStore = useChatStore()
+ const mcpStore = useMcpStore()
+
+ const selectionSet = computed(() => {
+ const selections = chatStore.activeAgentMcpSelections
+ if (!chatStore.isAcpMode || !selections?.length) return null
+ return new Set(selections)
+ })
+
+ const tools = computed(() => {
+ if (!chatStore.isAcpMode) return mcpStore.tools
+ const set = selectionSet.value
+ if (!set) return []
+ return mcpStore.tools.filter((tool) => set.has(tool.server.name))
+ })
+
+ const resources = computed(() => {
+ if (!chatStore.isAcpMode) return mcpStore.resources
+ const set = selectionSet.value
+ if (!set) return []
+ return mcpStore.resources.filter((resource) => set.has(resource.client.name))
+ })
+
+ const prompts = computed(() => {
+ if (!chatStore.isAcpMode) return mcpStore.prompts
+ const set = selectionSet.value
+ if (!set)
+ return mcpStore.prompts.filter((prompt) => prompt.client?.name === CUSTOM_PROMPTS_CLIENT)
+ return mcpStore.prompts.filter(
+ (prompt) => prompt.client?.name === CUSTOM_PROMPTS_CLIENT || set.has(prompt.client?.name)
+ )
+ })
+
+ return {
+ tools,
+ resources,
+ prompts,
+ selectionSet
+ }
+}
diff --git a/src/renderer/src/components/chat-input/composables/useAgentWorkspace.ts b/src/renderer/src/components/chat-input/composables/useAgentWorkspace.ts
new file mode 100644
index 000000000..b2e8956da
--- /dev/null
+++ b/src/renderer/src/components/chat-input/composables/useAgentWorkspace.ts
@@ -0,0 +1,239 @@
+// === Vue Core ===
+import { ref, computed, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+// === Composables ===
+import { usePresenter } from '@/composables/usePresenter'
+import { useChatMode } from './useChatMode'
+import { useAcpWorkdir } from './useAcpWorkdir'
+import { useChatStore } from '@/stores/chat'
+
+// === Types ===
+import type { Ref } from 'vue'
+
+export interface UseAgentWorkspaceOptions {
+ conversationId: Ref
+ activeModel: Ref<{ id: string; providerId: string } | null>
+ chatMode?: ReturnType
+}
+
+/**
+ * Unified workspace path management composable
+ * Handles workspace path selection for both agent and acp agent modes
+ */
+export function useAgentWorkspace(options: UseAgentWorkspaceOptions) {
+ const { t } = useI18n()
+ const threadPresenter = usePresenter('threadPresenter')
+ const chatMode = options.chatMode ?? useChatMode()
+ const chatStore = useChatStore()
+
+ // Use ACP workdir for acp agent mode
+ const acpWorkdir = useAcpWorkdir({
+ conversationId: options.conversationId,
+ activeModel: options.activeModel
+ })
+
+ // Agent workspace path (for agent mode)
+ const agentWorkspacePath = ref(null)
+ const pendingWorkspacePath = ref(null)
+ const loading = ref(false)
+ const syncPreference = (workspacePath: string | null) => {
+ const setPreference = (
+ chatStore as {
+ setAgentWorkspacePreference?: (path: string | null) => void
+ updateChatConfig?: (config: { agentWorkspacePath?: string | null }) => Promise
+ }
+ ).setAgentWorkspacePreference
+
+ if (typeof setPreference === 'function') {
+ setPreference(workspacePath)
+ return
+ }
+
+ if (typeof chatStore.updateChatConfig === 'function') {
+ void chatStore.updateChatConfig({ agentWorkspacePath: workspacePath })
+ }
+ }
+
+ const hydrateWorkspaceFromPreference = () => {
+ if (pendingWorkspacePath.value || agentWorkspacePath.value) return
+ const storedPath = chatStore.chatConfig.agentWorkspacePath ?? null
+ if (storedPath) {
+ agentWorkspacePath.value = storedPath
+ }
+ }
+
+ // === Computed ===
+ const hasWorkspace = computed(() => {
+ if (chatMode.currentMode.value === 'acp agent') {
+ return acpWorkdir.hasWorkdir.value
+ }
+ return Boolean(pendingWorkspacePath.value ?? agentWorkspacePath.value)
+ })
+
+ const workspacePath = computed(() => {
+ if (chatMode.currentMode.value === 'acp agent') {
+ return acpWorkdir.workdir.value
+ }
+ return pendingWorkspacePath.value ?? agentWorkspacePath.value
+ })
+
+ const tooltipTitle = computed(() => {
+ if (chatMode.currentMode.value === 'acp agent') {
+ return t('chat.input.acpWorkdirTooltip')
+ }
+ return t('chat.input.agentWorkspaceTooltip')
+ })
+
+ const tooltipCurrent = computed(() => {
+ if (!hasWorkspace.value) return ''
+ if (chatMode.currentMode.value === 'acp agent') {
+ return t('chat.input.acpWorkdirCurrent', { path: workspacePath.value || '' })
+ }
+ return t('chat.input.agentWorkspaceCurrent', { path: workspacePath.value || '' })
+ })
+
+ const tooltipSelect = computed(() => {
+ if (chatMode.currentMode.value === 'acp agent') {
+ return t('chat.input.acpWorkdirSelect')
+ }
+ return t('chat.input.agentWorkspaceSelect')
+ })
+
+ // === Methods ===
+ const selectWorkspace = async () => {
+ if (chatMode.currentMode.value === 'acp agent') {
+ // Use ACP workdir selection
+ await acpWorkdir.selectWorkdir()
+ return
+ }
+
+ // For agent mode, select workspace path
+ loading.value = true
+ try {
+ const devicePresenter = usePresenter('devicePresenter')
+ const result = await devicePresenter.selectDirectory()
+
+ if (!result.canceled && result.filePaths && result.filePaths.length > 0) {
+ const selectedPath = result.filePaths[0]
+ agentWorkspacePath.value = selectedPath
+ syncPreference(selectedPath)
+
+ // Save to conversation settings when available
+ if (options.conversationId.value) {
+ await threadPresenter.updateConversationSettings(options.conversationId.value, {
+ agentWorkspacePath: selectedPath
+ })
+ pendingWorkspacePath.value = null
+ } else {
+ pendingWorkspacePath.value = selectedPath
+ }
+
+ // Register workspace with presenter
+ const workspacePresenter = usePresenter('workspacePresenter')
+ await workspacePresenter.registerWorkspace(selectedPath)
+ }
+ } catch (error) {
+ console.error('[useAgentWorkspace] Failed to select workspace:', error)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ // Load workspace path from conversation settings
+ const loadWorkspacePath = async () => {
+ if (chatMode.currentMode.value === 'acp agent') {
+ // ACP workdir is loaded by useAcpWorkdir
+ return
+ }
+
+ if (!options.conversationId.value) {
+ hydrateWorkspaceFromPreference()
+ return
+ }
+
+ try {
+ // Load agent workspace path from conversation settings
+ const conversation = await threadPresenter.getConversation(options.conversationId.value)
+ const savedPath = conversation?.settings?.agentWorkspacePath ?? null
+ if (savedPath) {
+ agentWorkspacePath.value = savedPath
+ pendingWorkspacePath.value = null
+ syncPreference(savedPath)
+ // Register workspace with presenter
+ const workspacePresenter = usePresenter('workspacePresenter')
+ await workspacePresenter.registerWorkspace(savedPath)
+ } else if (!pendingWorkspacePath.value) {
+ agentWorkspacePath.value = null
+ }
+ } catch (error) {
+ console.error('[useAgentWorkspace] Failed to load workspace path:', error)
+ }
+ }
+
+ const syncPendingWorkspaceWhenReady = async () => {
+ if (chatMode.currentMode.value !== 'agent') return
+ const selectedPath = pendingWorkspacePath.value
+ if (!selectedPath || !options.conversationId.value) return
+
+ loading.value = true
+ try {
+ await threadPresenter.updateConversationSettings(options.conversationId.value, {
+ agentWorkspacePath: selectedPath
+ })
+ agentWorkspacePath.value = selectedPath
+ pendingWorkspacePath.value = null
+
+ const workspacePresenter = usePresenter('workspacePresenter')
+ await workspacePresenter.registerWorkspace(selectedPath)
+ } catch (error) {
+ console.error('[useAgentWorkspace] Failed to sync pending workspace:', error)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ watch(
+ () => options.conversationId.value,
+ () => {
+ if (pendingWorkspacePath.value) {
+ void syncPendingWorkspaceWhenReady()
+ }
+ }
+ )
+
+ // Watch for chatMode and conversationId changes
+ watch(
+ [() => chatMode.currentMode.value, () => options.conversationId.value],
+ async ([newMode, conversationId]) => {
+ if (newMode === 'agent') {
+ if (pendingWorkspacePath.value && conversationId) {
+ await syncPendingWorkspaceWhenReady()
+ }
+ if (conversationId) {
+ await loadWorkspacePath()
+ } else {
+ hydrateWorkspaceFromPreference()
+ }
+ return
+ }
+
+ if (newMode === 'acp agent') {
+ // ACP workdir is handled by useAcpWorkdir
+ return
+ }
+ },
+ { immediate: true }
+ )
+
+ return {
+ hasWorkspace,
+ workspacePath,
+ loading,
+ tooltipTitle,
+ tooltipCurrent,
+ tooltipSelect,
+ selectWorkspace,
+ loadWorkspacePath
+ }
+}
diff --git a/src/renderer/src/components/chat-input/composables/useChatMode.ts b/src/renderer/src/components/chat-input/composables/useChatMode.ts
new file mode 100644
index 000000000..164994ba4
--- /dev/null
+++ b/src/renderer/src/components/chat-input/composables/useChatMode.ts
@@ -0,0 +1,178 @@
+// === Vue Core ===
+import { ref, computed, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+// === Composables ===
+import { usePresenter } from '@/composables/usePresenter'
+import { CONFIG_EVENTS } from '@/events'
+
+export type ChatMode = 'chat' | 'agent' | 'acp agent'
+
+const MODE_ICONS = {
+ chat: 'lucide:message-circle-more',
+ agent: 'lucide:bot',
+ 'acp agent': 'lucide:bot-message-square'
+} as const
+
+// Shared state so all callers observe the same mode.
+const currentMode = ref('chat')
+const hasAcpAgents = ref(false)
+let hasLoaded = false
+let loadPromise: Promise | null = null
+let modeUpdateVersion = 0
+let hasAcpListener = false
+
+/**
+ * Manages chat mode selection (chat, agent, acp agent)
+ * Similar to useInputSettings, stores mode in database via configPresenter
+ */
+export function useChatMode() {
+ // === Presenters ===
+ const configPresenter = usePresenter('configPresenter')
+ const { t } = useI18n()
+
+ // === Computed ===
+ const currentIcon = computed(() => MODE_ICONS[currentMode.value])
+ const currentLabel = computed(() => {
+ if (currentMode.value === 'chat') return t('chat.mode.chat')
+ if (currentMode.value === 'agent') return t('chat.mode.agent')
+ return t('chat.mode.acpAgent')
+ })
+ const isAgentMode = computed(
+ () => currentMode.value === 'agent' || currentMode.value === 'acp agent'
+ )
+
+ const modes = computed(() => {
+ const allModes = [
+ { value: 'chat' as ChatMode, label: t('chat.mode.chat'), icon: MODE_ICONS.chat },
+ { value: 'agent' as ChatMode, label: t('chat.mode.agent'), icon: MODE_ICONS.agent },
+ {
+ value: 'acp agent' as ChatMode,
+ label: t('chat.mode.acpAgent'),
+ icon: MODE_ICONS['acp agent']
+ }
+ ]
+ // Filter out 'acp agent' mode if no ACP agents are configured
+ if (!hasAcpAgents.value) {
+ return allModes.filter((mode) => mode.value !== 'acp agent')
+ }
+ return allModes
+ })
+
+ // === Public Methods ===
+ const setMode = async (mode: ChatMode) => {
+ // Prevent setting 'acp agent' mode if no agents are configured
+ if (mode === 'acp agent' && !hasAcpAgents.value) {
+ console.warn('Cannot set acp agent mode: no ACP agents configured')
+ return
+ }
+
+ const previousValue = currentMode.value
+ const updateVersion = ++modeUpdateVersion
+ currentMode.value = mode
+
+ try {
+ await configPresenter.setSetting('input_chatMode', mode)
+ } catch (error) {
+ // Revert to previous value on error
+ if (modeUpdateVersion === updateVersion) {
+ currentMode.value = previousValue
+ }
+ console.error('Failed to save chat mode:', error)
+ // TODO: Show user-facing notification when toast system is available
+ }
+ }
+
+ const checkAcpAgents = async () => {
+ try {
+ const acpEnabled = await configPresenter.getAcpEnabled()
+ if (!acpEnabled) {
+ hasAcpAgents.value = false
+ return
+ }
+ const agents = await configPresenter.getAcpAgents()
+ hasAcpAgents.value = agents.length > 0
+ } catch (error) {
+ console.warn('Failed to check ACP agents:', error)
+ hasAcpAgents.value = false
+ }
+ }
+
+ const loadMode = async () => {
+ const loadVersion = modeUpdateVersion
+ try {
+ // Check ACP agents availability first
+ await checkAcpAgents()
+
+ const saved = await configPresenter.getSetting('input_chatMode')
+ if (modeUpdateVersion === loadVersion) {
+ const savedMode = (saved as ChatMode) || 'chat'
+ // If saved mode is 'acp agent' but no agents are configured, fall back to 'chat'
+ if (savedMode === 'acp agent' && !hasAcpAgents.value) {
+ currentMode.value = 'chat'
+ // Save the fallback mode
+ await configPresenter.setSetting('input_chatMode', 'chat')
+ } else {
+ currentMode.value = savedMode
+ }
+ }
+ } catch (error) {
+ // Fall back to safe defaults on error
+ if (modeUpdateVersion === loadVersion) {
+ currentMode.value = 'chat'
+ }
+ console.error('Failed to load chat mode, using default:', error)
+ } finally {
+ hasLoaded = true
+ }
+ }
+
+ const ensureLoaded = () => {
+ if (hasLoaded) return
+ if (!loadPromise) {
+ loadPromise = loadMode().finally(() => {
+ loadPromise = null
+ })
+ }
+ }
+
+ ensureLoaded()
+
+ if (!hasAcpListener && window.electron?.ipcRenderer) {
+ hasAcpListener = true
+ window.electron.ipcRenderer.on(CONFIG_EVENTS.MODEL_LIST_CHANGED, (_, providerId?: string) => {
+ if (!providerId || providerId === 'acp') {
+ void checkAcpAgents()
+ }
+ })
+ }
+
+ // Watch for ACP agents changes and update availability
+ // This will be triggered when ACP agents are added/removed
+ watch(
+ () => hasAcpAgents.value,
+ (hasAgents) => {
+ // If current mode is 'acp agent' but agents are removed, switch to 'chat'
+ if (!hasAgents && currentMode.value === 'acp agent') {
+ setMode('chat')
+ }
+ }
+ )
+
+ // Periodically check for ACP agents changes (in case they're updated elsewhere)
+ // This is a simple approach; in production, you might want to use events
+ const refreshAcpAgents = async () => {
+ await checkAcpAgents()
+ }
+
+ return {
+ currentMode,
+ currentIcon,
+ currentLabel,
+ isAgentMode,
+ modes,
+ setMode,
+ loadMode,
+ refreshAcpAgents
+ }
+}
diff --git a/src/renderer/src/components/chat-input/composables/useInputSettings.ts b/src/renderer/src/components/chat-input/composables/useInputSettings.ts
index 396597b0f..1a5c83b28 100644
--- a/src/renderer/src/components/chat-input/composables/useInputSettings.ts
+++ b/src/renderer/src/components/chat-input/composables/useInputSettings.ts
@@ -18,9 +18,10 @@ export function useInputSettings() {
})
// === Public Methods ===
- const toggleWebSearch = async () => {
+ const setWebSearch = async (value: boolean) => {
const previousValue = settings.value.webSearch
- settings.value.webSearch = !settings.value.webSearch
+ if (previousValue === value) return
+ settings.value.webSearch = value
try {
await configPresenter.setSetting('input_webSearch', settings.value.webSearch)
@@ -32,6 +33,10 @@ export function useInputSettings() {
}
}
+ const toggleWebSearch = async () => {
+ await setWebSearch(!settings.value.webSearch)
+ }
+
const toggleDeepThinking = async () => {
const previousValue = settings.value.deepThinking
settings.value.deepThinking = !settings.value.deepThinking
@@ -69,6 +74,7 @@ export function useInputSettings() {
return {
settings,
+ setWebSearch,
toggleWebSearch,
toggleDeepThinking,
loadSettings
diff --git a/src/renderer/src/components/chat-input/composables/useMentionData.ts b/src/renderer/src/components/chat-input/composables/useMentionData.ts
index b0b08b5ae..152d15a73 100644
--- a/src/renderer/src/components/chat-input/composables/useMentionData.ts
+++ b/src/renderer/src/components/chat-input/composables/useMentionData.ts
@@ -6,7 +6,7 @@ import type { MessageFile } from '@shared/chat'
import type { CategorizedData } from '../../editor/mention/suggestion'
// === Stores ===
-import { useMcpStore } from '@/stores/mcp'
+import { useAgentMcpData } from './useAgentMcpData'
// === Mention System ===
import { mentionData } from '../../editor/mention/suggestion'
@@ -16,8 +16,7 @@ import { mentionData } from '../../editor/mention/suggestion'
* Watches various data sources (files, MCP resources/tools/prompts) and updates mention data
*/
export function useMentionData(selectedFiles: Ref) {
- // === Stores ===
- const mcpStore = useMcpStore()
+ const { tools, resources, prompts } = useAgentMcpData()
// === Watchers ===
/**
@@ -45,12 +44,12 @@ export function useMentionData(selectedFiles: Ref) {
* Watch MCP resources and update mention data
*/
watch(
- () => mcpStore.resources,
+ () => resources.value,
() => {
mentionData.value = mentionData.value
.filter((item) => item.type !== 'item' || item.category !== 'resources')
.concat(
- mcpStore.resources.map((resource) => ({
+ resources.value.map((resource) => ({
id: `${resource.client.name}.${resource.name ?? ''}`,
label: resource.name ?? '',
icon: 'lucide:tag',
@@ -67,12 +66,12 @@ export function useMentionData(selectedFiles: Ref) {
* Watch MCP tools and update mention data
*/
watch(
- () => mcpStore.tools,
+ () => tools.value,
() => {
mentionData.value = mentionData.value
.filter((item) => item.type !== 'item' || item.category !== 'tools')
.concat(
- mcpStore.tools.map((tool) => ({
+ tools.value.map((tool) => ({
id: `${tool.server.name}.${tool.function.name ?? ''}`,
label: `${tool.server.icons}${' '}${tool.function.name ?? ''}`,
icon: undefined,
@@ -89,12 +88,12 @@ export function useMentionData(selectedFiles: Ref) {
* Watch MCP prompts and update mention data
*/
watch(
- () => mcpStore.prompts,
+ () => prompts.value,
() => {
mentionData.value = mentionData.value
.filter((item) => item.type !== 'item' || item.category !== 'prompts')
.concat(
- mcpStore.prompts.map((prompt) => ({
+ prompts.value.map((prompt) => ({
id: prompt.name,
label: prompt.name,
icon: undefined,
diff --git a/src/renderer/src/components/chat-input/composables/usePromptInputEditor.ts b/src/renderer/src/components/chat-input/composables/usePromptInputEditor.ts
index d4e0a0806..ac6bcf07f 100644
--- a/src/renderer/src/components/chat-input/composables/usePromptInputEditor.ts
+++ b/src/renderer/src/components/chat-input/composables/usePromptInputEditor.ts
@@ -107,6 +107,10 @@ export function usePromptInputEditor(
fetchingMcpEntry.value = false
}
+ if (subBlock.attrs?.category === 'context' && subBlock.attrs?.content) {
+ content = subBlock.attrs.content as string
+ }
+
if (subBlock.attrs?.category === 'prompts') {
fetchingMcpEntry.value = true
try {
@@ -266,11 +270,13 @@ export function usePromptInputEditor(
*/
const insertMentionToEditor = (mentionData: CategorizedData, position: number): boolean => {
try {
+ const mentionContent =
+ mentionData.content ?? (mentionData.mcpEntry ? JSON.stringify(mentionData.mcpEntry) : '')
const mentionAttrs = {
id: mentionData.id,
label: mentionData.label,
category: mentionData.category,
- content: mentionData.mcpEntry ? JSON.stringify(mentionData.mcpEntry) : ''
+ content: mentionContent
}
const success = editor
diff --git a/src/renderer/src/components/chat-input/composables/useWorkspaceMention.ts b/src/renderer/src/components/chat-input/composables/useWorkspaceMention.ts
new file mode 100644
index 000000000..d2e8a3e8c
--- /dev/null
+++ b/src/renderer/src/components/chat-input/composables/useWorkspaceMention.ts
@@ -0,0 +1,101 @@
+import { computed, ref, watch, type Ref } from 'vue'
+import { useDebounceFn } from '@vueuse/core'
+import { usePresenter } from '@/composables/usePresenter'
+import type { WorkspaceFileNode } from '@shared/presenter'
+import type { CategorizedData } from '../../editor/mention/suggestion'
+
+export function useWorkspaceMention(options: {
+ workspacePath: Ref
+ chatMode: Ref<'chat' | 'agent' | 'acp agent'>
+ conversationId: Ref
+}) {
+ const workspacePresenter = usePresenter('workspacePresenter')
+ const workspaceFileResults = ref([])
+
+ const isEnabled = computed(() => {
+ const hasPath = !!options.workspacePath.value
+ const isAgentMode = options.chatMode.value === 'agent' || options.chatMode.value === 'acp agent'
+ const enabled = hasPath && isAgentMode
+ return enabled
+ })
+
+ const toDisplayPath = (filePath: string) => {
+ const root = options.workspacePath.value
+ if (!root) return filePath
+ const trimmedRoot = root.replace(/[\\/]+$/, '')
+ if (!filePath.startsWith(trimmedRoot)) return filePath
+ const relative = filePath.slice(trimmedRoot.length).replace(/^[\\/]+/, '')
+ return relative || filePath
+ }
+
+ const mapResults = (files: WorkspaceFileNode[]) =>
+ files.map((file) => {
+ const relativePath = toDisplayPath(file.path)
+ return {
+ id: file.path,
+ label: relativePath || file.name,
+ description: file.path,
+ icon: file.isDirectory ? 'lucide:folder' : 'lucide:file',
+ type: 'item' as const,
+ category: 'workspace' as const
+ }
+ })
+
+ const clearResults = () => {
+ workspaceFileResults.value = []
+ }
+
+ const searchWorkspaceFiles = useDebounceFn(async (query: string) => {
+ // Allow empty query to show some files when user just types "@"
+ // Empty query means show a limited list of files
+ if (!isEnabled.value || !options.workspacePath.value) {
+ clearResults()
+ return
+ }
+
+ const trimmed = query.trim()
+ // If query is empty, use "**/*" to show some files (limited by searchFiles)
+ // This is a standard glob pattern to match all files
+ const searchQuery = trimmed || '**/*'
+
+ try {
+ if (options.chatMode.value === 'acp agent') {
+ await workspacePresenter.registerWorkdir(options.workspacePath.value)
+ } else {
+ await workspacePresenter.registerWorkspace(options.workspacePath.value)
+ }
+ const results =
+ (await workspacePresenter.searchFiles(options.workspacePath.value, searchQuery)) ?? []
+ workspaceFileResults.value = mapResults(results)
+ } catch (error) {
+ console.error('[WorkspaceMention] Failed to search workspace files:', error)
+ clearResults()
+ }
+ }, 300)
+
+ watch(isEnabled, (enabled) => {
+ if (!enabled) {
+ clearResults()
+ }
+ })
+
+ watch(
+ () => options.workspacePath.value,
+ () => {
+ clearResults()
+ }
+ )
+
+ watch(
+ () => options.conversationId.value,
+ () => {
+ clearResults()
+ }
+ )
+
+ return {
+ searchWorkspaceFiles,
+ workspaceFileResults,
+ isEnabled
+ }
+}
diff --git a/src/renderer/src/components/editor/mention/suggestion.ts b/src/renderer/src/components/editor/mention/suggestion.ts
index f95cf6ea2..7c7c780b2 100644
--- a/src/renderer/src/components/editor/mention/suggestion.ts
+++ b/src/renderer/src/components/editor/mention/suggestion.ts
@@ -14,10 +14,12 @@ export interface CategorizedData {
category?: string
description?: string
mcpEntry?: ResourceListEntry | PromptListEntry
+ content?: string
}
// Sample categorized items
const categorizedData: CategorizedData[] = [
+ { label: 'context', icon: 'lucide:quote', type: 'category' },
{ label: 'files', icon: 'lucide:files', type: 'category' },
{ label: 'resources', icon: 'lucide:swatch-book', type: 'category' },
{ label: 'tools', icon: 'lucide:hammer', type: 'category' },
@@ -28,6 +30,18 @@ const categorizedData: CategorizedData[] = [
export const mentionSelected = ref(false)
export const mentionData: Ref = ref(categorizedData)
+export type WorkspaceMentionHandler = {
+ searchWorkspaceFiles: (query: string) => void
+ workspaceFileResults: Ref
+ isEnabled: Ref
+}
+
+let workspaceMentionHandler: WorkspaceMentionHandler | null = null
+
+export const setWorkspaceMention = (handler: WorkspaceMentionHandler | null) => {
+ workspaceMentionHandler = handler
+}
+
// 存储文件处理回调函数
let promptFilesHandler:
| ((
@@ -53,24 +67,39 @@ export const setPromptFilesHandler = (handler: typeof promptFilesHandler) => {
export const getPromptFilesHandler = () => promptFilesHandler
export default {
- allowedPrefixes: null,
+ char: '@',
+ allowedPrefixes: null, // null means allow @ after any character
items: ({ query }) => {
- // If there's a query, search across all categories
- if (query) {
- const allItems: CategorizedData[] = []
- // Flatten the structure and search in all categories
+ // Note: TipTap mention passes query WITHOUT the trigger character (@)
+ // So if user types "@", query is ""
+ // If user types "@p", query is "p"
+
+ // Collect workspace results if enabled
+ let workspaceResults: CategorizedData[] = []
+ if (workspaceMentionHandler?.isEnabled.value) {
+ workspaceMentionHandler.searchWorkspaceFiles(query)
+ workspaceResults = workspaceMentionHandler.workspaceFileResults.value
+ }
+ // Collect other mention data (prompts, tools, files, resources)
+ let otherItems: CategorizedData[] = []
+ if (query) {
+ // Search across all categories
for (const item of mentionData.value) {
if (item.label.toLowerCase().includes(query.toLowerCase())) {
- allItems.push(item)
+ otherItems.push(item)
}
}
-
- return allItems.slice(0, 5)
+ otherItems = otherItems.slice(0, 5)
+ } else {
+ // If no query, return all mention data
+ otherItems = mentionData.value
}
- // If no query, return the full list
- return mentionData.value
+ // Combine workspace results with other mention data
+ // Workspace results come first, then other mention data
+ const combined = [...workspaceResults, ...otherItems]
+ return combined
},
render: () => {
diff --git a/src/renderer/src/components/mcp-config/AgentMcpSelector.vue b/src/renderer/src/components/mcp-config/AgentMcpSelector.vue
new file mode 100644
index 000000000..46f35e62a
--- /dev/null
+++ b/src/renderer/src/components/mcp-config/AgentMcpSelector.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+ {{ t('settings.acp.mcpAccessTitle') }}
+
+
+
+ {{ t('settings.acp.loading') }}
+
+
+
+ {{ t('settings.acp.mcpAccessEmpty') }}
+
+
+
+
+
+
toggleServer(server.name, Boolean(value))"
+ />
+
+ {{ server.name }}
+
+
+
+
+
+
diff --git a/src/renderer/src/components/message/MessageContent.vue b/src/renderer/src/components/message/MessageContent.vue
index 78707bbdd..c3d809987 100644
--- a/src/renderer/src/components/message/MessageContent.vue
+++ b/src/renderer/src/components/message/MessageContent.vue
@@ -8,11 +8,11 @@
- {{ block.category === 'prompts' ? block.id : block.content }}
+ {{ getMentionLabel(block) }}
@@ -59,6 +59,7 @@ const handleMentionClick = (block: UserMessageMentionBlock) => {
// 根据 category 获取对应的图标
const getMentionIcon = (category: string) => {
const iconMap: Record = {
+ context: 'lucide:quote',
prompts: 'lucide:message-square-quote',
files: 'lucide:file-text',
tools: 'lucide:wrench',
@@ -72,4 +73,21 @@ const getMentionIcon = (category: string) => {
return iconMap[category] || iconMap.default
}
+
+const getMentionLabel = (block: UserMessageMentionBlock) => {
+ if (block.category === 'prompts') {
+ return block.id || block.content
+ }
+ if (block.category === 'context') {
+ return block.id || block.category
+ }
+ return block.content
+}
+
+const getMentionTitle = (block: UserMessageMentionBlock) => {
+ if (block.category === 'context') {
+ return block.id || ''
+ }
+ return block.content
+}
diff --git a/src/renderer/src/components/message/MessageItemAssistant.vue b/src/renderer/src/components/message/MessageItemAssistant.vue
index 5db60d49e..754b16387 100644
--- a/src/renderer/src/components/message/MessageItemAssistant.vue
+++ b/src/renderer/src/components/message/MessageItemAssistant.vue
@@ -22,7 +22,7 @@
-
+
-
+
{
}
const handleMentionClick = (block: UserMessageMentionBlock) => {
- // 处理 mention 点击事件,可以根据需要实现具体逻辑
- console.log('Mention clicked:', block)
+ if (block.category !== 'context') {
+ return
+ }
+ const activeThread = chatStore.activeThread
+ if (!activeThread?.parentConversationId || !activeThread.parentMessageId) {
+ return
+ }
+ chatStore.openThreadInNewTab(activeThread.parentConversationId, {
+ messageId: activeThread.parentMessageId,
+ childConversationId: activeThread.id
+ })
}
const autoResize = () => {
diff --git a/src/renderer/src/components/message/MessageList.vue b/src/renderer/src/components/message/MessageList.vue
index 7142e2d2f..111fe7528 100644
--- a/src/renderer/src/components/message/MessageList.vue
+++ b/src/renderer/src/components/message/MessageList.vue
@@ -65,7 +65,7 @@
@@ -254,4 +491,20 @@ defineExpose({
.dark .message-highlight {
background-color: rgba(59, 130, 246, 0.15);
}
+
+:global(.selection-highlight) {
+ background-color: rgba(250, 204, 21, 0.4);
+ cursor: pointer;
+ border-radius: 2px;
+ padding: 0 1px;
+}
+
+:global(.selection-highlight:hover) {
+ background-color: rgba(250, 204, 21, 0.6);
+}
+
+:global(.selection-highlight-active) {
+ background-color: rgba(250, 204, 21, 0.7);
+ box-shadow: 0 0 0 2px rgba(250, 204, 21, 0.5);
+}
diff --git a/src/renderer/src/components/message/MessageMinimap.vue b/src/renderer/src/components/message/MessageMinimap.vue
index 597856b25..36ba77c3c 100644
--- a/src/renderer/src/components/message/MessageMinimap.vue
+++ b/src/renderer/src/components/message/MessageMinimap.vue
@@ -95,14 +95,26 @@ const getMessageContentLength = (message: Message) => {
const userMessage = message as UserMessage
const content = userMessage.content
const textParts: string[] = []
+ const resolveUserBlockText = (block: {
+ type?: string
+ content?: string
+ category?: string
+ id?: string
+ }) => {
+ if (block?.type === 'mention' && block?.category === 'context') {
+ const label = block.id?.trim() || 'context'
+ return `@${label}`
+ }
+ return typeof block?.content === 'string' ? block.content : ''
+ }
+
if (content?.text) {
textParts.push(content.text)
+ } else if (content?.content) {
+ content.content.forEach((block) => {
+ textParts.push(resolveUserBlockText(block))
+ })
}
- content?.content?.forEach((block) => {
- if ('content' in block && typeof block.content === 'string') {
- textParts.push(block.content)
- }
- })
if (content?.files?.length) {
textParts.push(content.files.map((file) => file.name ?? '').join(' '))
}
diff --git a/src/renderer/src/components/message/SelectedTextContextMenu.vue b/src/renderer/src/components/message/SelectedTextContextMenu.vue
index e321fe317..2241d4878 100644
--- a/src/renderer/src/components/message/SelectedTextContextMenu.vue
+++ b/src/renderer/src/components/message/SelectedTextContextMenu.vue
@@ -4,6 +4,14 @@
diff --git a/src/renderer/src/components/workspace/WorkspaceBrowserTabs.vue b/src/renderer/src/components/workspace/WorkspaceBrowserTabs.vue
new file mode 100644
index 000000000..82da148a2
--- /dev/null
+++ b/src/renderer/src/components/workspace/WorkspaceBrowserTabs.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+ {{ t('chat.workspace.browser.empty') }}
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/src/components/acp-workspace/AcpWorkspaceFileNode.vue b/src/renderer/src/components/workspace/WorkspaceFileNode.vue
similarity index 78%
rename from src/renderer/src/components/acp-workspace/AcpWorkspaceFileNode.vue
rename to src/renderer/src/components/workspace/WorkspaceFileNode.vue
index e02ac2e49..3f32d365f 100644
--- a/src/renderer/src/components/acp-workspace/AcpWorkspaceFileNode.vue
+++ b/src/renderer/src/components/workspace/WorkspaceFileNode.vue
@@ -29,23 +29,23 @@
- {{ t('chat.acp.workspace.files.contextMenu.openFile') }}
+ {{ t('chat.workspace.files.contextMenu.openFile') }}
- {{ t('chat.acp.workspace.files.contextMenu.revealInFolder') }}
+ {{ t('chat.workspace.files.contextMenu.revealInFolder') }}
- {{ t('chat.acp.workspace.files.contextMenu.insertPath') }}
+ {{ t('chat.workspace.files.contextMenu.insertPath') }}
- ()
const emit = defineEmits<{
- toggle: [node: AcpFileNode]
+ toggle: [node: WorkspaceFileNode]
'append-path': [filePath: string]
}>()
const { t } = useI18n()
-const acpWorkspacePresenter = usePresenter('acpWorkspacePresenter')
+const workspacePresenter = usePresenter('workspacePresenter')
const extensionIconMap: Record = {
pdf: 'lucide:file-text',
@@ -139,17 +139,17 @@ const handleOpenFile = async () => {
}
try {
- await acpWorkspacePresenter.openFile(props.node.path)
+ await workspacePresenter.openFile(props.node.path)
} catch (error) {
- console.error(`[AcpWorkspace] Failed to open file: ${props.node.path}`, error)
+ console.error(`[Workspace] Failed to open file: ${props.node.path}`, error)
}
}
const handleRevealInFolder = async () => {
try {
- await acpWorkspacePresenter.revealFileInFolder(props.node.path)
+ await workspacePresenter.revealFileInFolder(props.node.path)
} catch (error) {
- console.error(`[AcpWorkspace] Failed to reveal path: ${props.node.path}`, error)
+ console.error(`[Workspace] Failed to reveal path: ${props.node.path}`, error)
}
}
@@ -157,3 +157,24 @@ const handleAppendFromMenu = () => {
emitAppendPath()
}
+
+
diff --git a/src/renderer/src/components/acp-workspace/AcpWorkspaceFiles.vue b/src/renderer/src/components/workspace/WorkspaceFiles.vue
similarity index 71%
rename from src/renderer/src/components/acp-workspace/AcpWorkspaceFiles.vue
rename to src/renderer/src/components/workspace/WorkspaceFiles.vue
index d71f49dc8..b3510604b 100644
--- a/src/renderer/src/components/acp-workspace/AcpWorkspaceFiles.vue
+++ b/src/renderer/src/components/workspace/WorkspaceFiles.vue
@@ -9,7 +9,7 @@
- {{ t('chat.acp.workspace.files.section') }}
+ {{ t(sectionKey) }}
{{ fileCount }}
@@ -23,10 +23,10 @@
- {{ t('chat.acp.workspace.files.loading') }}
+ {{ t(loadingKey) }}
- {{ t('chat.acp.workspace.files.empty') }}
+ {{ t(emptyKey) }}
@@ -47,18 +47,29 @@
import { ref, computed } from 'vue'
import { Icon } from '@iconify/vue'
import { useI18n } from 'vue-i18n'
-import { useAcpWorkspaceStore } from '@/stores/acpWorkspace'
-import AcpWorkspaceFileNode from './AcpWorkspaceFileNode.vue'
-import type { AcpFileNode } from '@shared/presenter'
+import { useWorkspaceStore } from '@/stores/workspace'
+import { useChatMode } from '@/components/chat-input/composables/useChatMode'
+import WorkspaceFileNode from './WorkspaceFileNode.vue'
+import type { WorkspaceFileNode as WorkspaceFileNodeType } from '@shared/presenter'
const { t } = useI18n()
-const store = useAcpWorkspaceStore()
+const store = useWorkspaceStore()
+const chatMode = useChatMode()
const showFiles = ref(true)
+
+const i18nPrefix = computed(() =>
+ chatMode.currentMode.value === 'acp agent' ? 'chat.acp.workspace' : 'chat.workspace'
+)
+
+const sectionKey = computed(() => `${i18nPrefix.value}.files.section`)
+const loadingKey = computed(() => `${i18nPrefix.value}.files.loading`)
+const emptyKey = computed(() => `${i18nPrefix.value}.files.empty`)
+
const emit = defineEmits<{
'append-path': [filePath: string]
}>()
-const countFiles = (nodes: AcpFileNode[]): number => {
+const countFiles = (nodes: WorkspaceFileNodeType[]): number => {
let count = 0
for (const node of nodes) {
count += 1
@@ -71,7 +82,7 @@ const countFiles = (nodes: AcpFileNode[]): number => {
const fileCount = computed(() => countFiles(store.fileTree))
-const handleToggle = async (node: AcpFileNode) => {
+const handleToggle = async (node: WorkspaceFileNodeType) => {
await store.toggleFileNode(node)
}
diff --git a/src/renderer/src/components/acp-workspace/AcpWorkspacePlan.vue b/src/renderer/src/components/workspace/WorkspacePlan.vue
similarity index 82%
rename from src/renderer/src/components/acp-workspace/AcpWorkspacePlan.vue
rename to src/renderer/src/components/workspace/WorkspacePlan.vue
index 7395820dc..f59fcee07 100644
--- a/src/renderer/src/components/acp-workspace/AcpWorkspacePlan.vue
+++ b/src/renderer/src/components/workspace/WorkspacePlan.vue
@@ -9,7 +9,7 @@
- {{ t('chat.acp.workspace.plan.section') }}
+ {{ t(sectionKey) }}
{{ store.completedPlanCount }}/{{ store.totalPlanCount }}
@@ -56,17 +56,25 @@