diff --git a/src/components/chat-view/Chat.tsx b/src/components/chat-view/Chat.tsx index b7628b5..6746862 100644 --- a/src/components/chat-view/Chat.tsx +++ b/src/components/chat-view/Chat.tsx @@ -14,6 +14,7 @@ import { } from 'react' import { v4 as uuidv4 } from 'uuid' +import { ModeSelect } from './chat-input/ModeSelect' import { ApplyViewState } from '../../ApplyView' import { APPLY_VIEW_TYPE } from '../../constants' import { useApp } from '../../contexts/AppContext' @@ -90,7 +91,7 @@ export type ChatProps = { const Chat = forwardRef((props, ref) => { const app = useApp() - const { settings } = useSettings() + const { settings, setSettings } = useSettings() const { getRAGEngine } = useRAG() const { @@ -565,6 +566,25 @@ const Chat = forwardRef((props, ref) => { mentionables: [], } } + } else if (toolArgs.type === 'switch_mode') { + setSettings({ + ...settings, + mode: toolArgs.mode, + }) + const formattedContent = `[switch_mode to ${toolArgs.mode}] Result: successfully switched to ${toolArgs.mode}\n` + return { + type: 'switch_mode', + applyMsgId, + applyStatus: ApplyStatus.Applied, + returnMsg: { + role: 'user', + applyStatus: ApplyStatus.Idle, + content: null, + promptContent: formattedContent, + id: uuidv4(), + mentionables: [], + } + } } } catch (error) { console.error('Failed to apply changes', error) @@ -745,7 +765,7 @@ const Chat = forwardRef((props, ref) => { return (
-

CHAT

+
+
+
+ + {reason} + +
+ ) +} diff --git a/src/components/chat-view/ReactMarkdown.tsx b/src/components/chat-view/ReactMarkdown.tsx index 857fc67..3db924a 100644 --- a/src/components/chat-view/ReactMarkdown.tsx +++ b/src/components/chat-view/ReactMarkdown.tsx @@ -16,6 +16,7 @@ import MarkdownRegexSearchFilesBlock from './MarkdownRegexSearchFilesBlock' import MarkdownSearchAndReplace from './MarkdownSearchAndReplace' import MarkdownSearchWebBlock from './MarkdownSearchWebBlock' import MarkdownSemanticSearchFilesBlock from './MarkdownSemanticSearchFilesBlock' +import MarkdownSwitchModeBlock from './MarkdownSwitchModeBlock' import MarkdownWithIcons from './MarkdownWithIcon' function ReactMarkdown({ applyStatus, @@ -132,6 +133,15 @@ function ReactMarkdown({ markdownContent={ ` ${block.question && block.question.trimStart()}`} /> + ) : block.type === 'switch_mode' ? ( + ) : block.type === 'search_web' ? ( { + setMode(settings.mode) + }, [settings.mode]) + + + return ( + + +
+ {isOpen ? : } +
+
+ {modes.find((m) => m.slug === mode)?.name} +
+
+ + + +
    + {modes.map((mode) => ( + { + setMode(mode.slug) + setSettings({ + ...settings, + mode: mode.slug, + }) + }} + asChild + > +
  • {mode.name}
  • +
    + ))} +
+
+
+
+ ) +} diff --git a/src/components/chat-view/chat-input/ModelSelect.tsx b/src/components/chat-view/chat-input/ModelSelect.tsx index 0fc24a8..86e150f 100644 --- a/src/components/chat-view/chat-input/ModelSelect.tsx +++ b/src/components/chat-view/chat-input/ModelSelect.tsx @@ -18,15 +18,16 @@ export function ModelSelect() { try { const models = await GetProviderModelIds(settings.chatModelProvider) setProviderModels(models) + setChatModelId(settings.chatModelId) } catch (error) { console.error('Failed to fetch provider models:', error) } finally { setIsLoading(false) } } - + fetchModels() - }, [settings.chatModelProvider]) + }, [settings]) return ( diff --git a/src/components/chat-view/chat-input/PromptInputWithActions.tsx b/src/components/chat-view/chat-input/PromptInputWithActions.tsx index 725ac70..77abdcf 100644 --- a/src/components/chat-view/chat-input/PromptInputWithActions.tsx +++ b/src/components/chat-view/chat-input/PromptInputWithActions.tsx @@ -30,6 +30,7 @@ import { ImageUploadButton } from './ImageUploadButton' import LexicalContentEditable from './LexicalContentEditable' import MentionableBadge from './MentionableBadge' import { ModelSelect } from './ModelSelect' +import { ModeSelect } from './ModeSelect' import { MentionNode } from './plugins/mention/MentionNode' import { NodeMutations } from './plugins/on-mutation/OnMutationPlugin' import { SubmitButton } from './SubmitButton' diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts index 67b91a8..021b5bf 100644 --- a/src/core/prompts/sections/modes.ts +++ b/src/core/prompts/sections/modes.ts @@ -1,16 +1,16 @@ -import { promises as fs } from "fs" -import * as path from "path" -import * as vscode from "vscode" +// import { promises as fs } from "fs" +// import * as path from "path" +// import * as vscode from "vscode" import { ModeConfig, getAllModesWithPrompts } from "../../../utils/modes" -export async function getModesSection(context: vscode.ExtensionContext): Promise { - const settingsDir = path.join(context.globalStorageUri.fsPath, "settings") - await fs.mkdir(settingsDir, { recursive: true }) - const customModesPath = path.join(settingsDir, "cline_custom_modes.json") +export async function getModesSection(): Promise { + // const settingsDir = path.join(context.globalStorageUri.fsPath, "settings") + // await fs.mkdir(settingsDir, { recursive: true }) + // const customModesPath = path.join(settingsDir, "cline_custom_modes.json") // Get all modes with their overrides from extension state - const allModes = await getAllModesWithPrompts(context) + const allModes = await getAllModesWithPrompts() return `==== @@ -18,43 +18,5 @@ MODES - These are the currently available modes: ${allModes.map((mode: ModeConfig) => ` * "${mode.name}" mode (${mode.slug}) - ${mode.roleDefinition.split(".")[0]}`).join("\n")} - -- Custom modes can be configured in two ways: - 1. Globally via '${customModesPath}' (created automatically on startup) - 2. Per-workspace via '.roomodes' in the workspace root directory - - When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes. - - If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file. - -- The following fields are required and must not be empty: - * slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better. - * name: The display name for the mode - * roleDefinition: A detailed description of the mode's role and capabilities - * groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files) - -- The customInstructions field is optional. - -- For multi-line text, include newline characters in the string like "This is the first line.\\nThis is the next line.\\n\\nThis is a double line break." - -Both files should follow this structure: -{ - "customModes": [ - { - "slug": "designer", // Required: unique slug with lowercase letters, numbers, and hyphens - "name": "Designer", // Required: mode display name - "roleDefinition": "You are Infio, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:\\n- Creating and maintaining design systems\\n- Implementing responsive and accessible web interfaces\\n- Working with CSS, HTML, and modern frontend frameworks\\n- Ensuring consistent user experiences across platforms", // Required: non-empty - "groups": [ // Required: array of tool groups (can be empty) - "read", // Read files group (read_file, search_files, list_files) - "edit", // Edit files group (apply_diff, write_to_file) - allows editing any file - // Or with file restrictions: - // ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], // Edit group that only allows editing markdown files - "browser", // Browser group (browser_action) - "command", // Command group (execute_command) - "mcp" // MCP group (use_mcp_tool, access_mcp_resource) - ], - "customInstructions": "Additional instructions for the Designer mode" // Optional - } - ] -}` +` } diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index bbd3df9..ebb47ae 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -15,7 +15,7 @@ import { addCustomInstructions, getCapabilitiesSection, getMcpServersSection, - // getModesSection, + getModesSection, getObjectiveSection, getRulesSection, getSharedToolUseSection, @@ -44,7 +44,7 @@ async function generatePrompt( // } const searchTool = "semantic" - + // If diff is disabled, don't pass the diffStrategy const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined @@ -52,9 +52,12 @@ async function generatePrompt( const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0] const roleDefinition = promptComponent?.roleDefinition || modeConfig.roleDefinition - const mcpServersSection = modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp") - ? await getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation) - : "" + const [modesSection, mcpServersSection] = await Promise.all([ + getModesSection(), + modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp") + ? getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation) + : Promise.resolve(""), + ]) const basePrompt = `${roleDefinition} @@ -77,19 +80,21 @@ ${getToolUseGuidelinesSection()} ${mcpServersSection} ${getCapabilitiesSection( - mode, - cwd, - searchTool, -)} + mode, + cwd, + searchTool, + )} + +${modesSection} ${getRulesSection( - mode, - cwd, - searchTool, - supportsComputerUse, - effectiveDiffStrategy, - experiments, -)} + mode, + cwd, + searchTool, + supportsComputerUse, + effectiveDiffStrategy, + experiments, + )} ${getSystemInfoSection(cwd)} diff --git a/src/types/apply.ts b/src/types/apply.ts index cd54429..1a078eb 100644 --- a/src/types/apply.ts +++ b/src/types/apply.ts @@ -76,4 +76,11 @@ export type FetchUrlsContentToolArgs = { finish?: boolean; } -export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs; +export type SwitchModeToolArgs = { + type: 'switch_mode'; + mode: string; + reason: string; + finish?: boolean; +} + +export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs; diff --git a/src/types/settings.ts b/src/types/settings.ts index 3abbbb7..2ece36c 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -219,6 +219,9 @@ export const InfioSettingsSchema = z.object({ embeddingModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Google), embeddingModelId: z.string().catch(''), + // Mode + mode: z.string().catch('ask'), + /// [compatible] // activeModels [compatible] activeModels: z.array( diff --git a/src/utils/modes.ts b/src/utils/modes.ts index bd01adc..e4089a1 100644 --- a/src/utils/modes.ts +++ b/src/utils/modes.ts @@ -248,15 +248,15 @@ export const defaultPrompts: Readonly = Object.freeze( ) // Helper function to get all modes with their prompt overrides from extension state -export async function getAllModesWithPrompts(context: vscode.ExtensionContext): Promise { - const customModes = (await context.globalState.get("customModes")) || [] - const customModePrompts = (await context.globalState.get("customModePrompts")) || {} +export async function getAllModesWithPrompts(): Promise { + // const customModes = (await context.globalState.get("customModes")) || [] + // const customModePrompts = (await context.globalState.get("customModePrompts")) || {} - const allModes = getAllModes(customModes) + const allModes = getAllModes() return allModes.map((mode) => ({ ...mode, - roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition, - customInstructions: customModePrompts[mode.slug]?.customInstructions ?? mode.customInstructions, + roleDefinition: mode.roleDefinition, + customInstructions: mode.customInstructions, })) } diff --git a/src/utils/parse-infio-block.ts b/src/utils/parse-infio-block.ts index a24e79f..b1d73a5 100644 --- a/src/utils/parse-infio-block.ts +++ b/src/utils/parse-infio-block.ts @@ -68,6 +68,11 @@ export type ParsedMsgBlock = type: 'fetch_urls_content' urls: string[] finish: boolean + } | { + type: 'switch_mode' + mode: string + reason: string + finish: boolean } export function parseMsgBlocks( @@ -399,7 +404,6 @@ export function parseMsgBlocks( result, }) lastEndOffset = endOffset - } else if (node.nodeName === 'ask_followup_question') { if (!node.sourceCodeLocation) { throw new Error('sourceCodeLocation is undefined') @@ -423,8 +427,40 @@ export function parseMsgBlocks( question, }) lastEndOffset = endOffset - } - else if (node.nodeName === 'search_web') { + } else if (node.nodeName === 'switch_mode') { + if (!node.sourceCodeLocation) { + throw new Error('sourceCodeLocation is undefined') + } + const startOffset = node.sourceCodeLocation.startOffset + const endOffset = node.sourceCodeLocation.endOffset + if (startOffset > lastEndOffset) { + parsedResult.push({ + type: 'string', + content: input.slice(lastEndOffset, startOffset), + }) + } + + let mode: string = '' + let reason: string = '' + + for (const childNode of node.childNodes) { + if (childNode.nodeName === 'mode_slug' && childNode.childNodes.length > 0) { + // @ts-ignore - 忽略 value 属性的类型错误 + mode = childNode.childNodes[0].value + } else if (childNode.nodeName === 'reason' && childNode.childNodes.length > 0) { + // @ts-ignore - 忽略 value 属性的类型错误 + reason = childNode.childNodes[0].value + } + } + + parsedResult.push({ + type: 'switch_mode', + mode, + reason, + finish: node.sourceCodeLocation.endTag !== undefined + }) + lastEndOffset = endOffset + } else if (node.nodeName === 'search_web') { if (!node.sourceCodeLocation) { throw new Error('sourceCodeLocation is undefined') } diff --git a/src/utils/prompt-generator.ts b/src/utils/prompt-generator.ts index 4c02fcd..a931bd1 100644 --- a/src/utils/prompt-generator.ts +++ b/src/utils/prompt-generator.ts @@ -16,7 +16,7 @@ import { MentionableVault } from '../types/mentionable' import { InfioSettings } from '../types/settings' -import { defaultModeSlug, getFullModeDetails } from "../utils/modes" +import { Mode, defaultModeSlug, getFullModeDetails, getModeBySlug } from "../utils/modes" import { readTFileContent @@ -157,8 +157,8 @@ export class PromptGenerator { similaritySearchResults, }, ] - - const systemMessage = await this.getSystemMessageNew() + console.log('this.settings.mode', this.settings.mode) + const systemMessage = await this.getSystemMessageNew(this.settings.mode) const requestMessages: RequestMessage[] = [ systemMessage, @@ -225,7 +225,7 @@ export class PromptGenerator { details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})` // Add current mode details - const currentMode = defaultModeSlug + const currentMode = this.settings.mode const modeDetails = await getFullModeDetails(currentMode) details += `\n\n# Current Mode\n` details += `${currentMode}\n` @@ -446,8 +446,8 @@ export class PromptGenerator { } } - private async getSystemMessageNew(): Promise { - const systemPrompt = await SYSTEM_PROMPT(this.app.vault.getRoot().path, false) + private async getSystemMessageNew(mode: Mode): Promise { + const systemPrompt = await SYSTEM_PROMPT(this.app.vault.getRoot().path, false, mode) return { role: 'system', diff --git a/styles.css b/styles.css index 8e1b7c2..046dbf8 100644 --- a/styles.css +++ b/styles.css @@ -493,7 +493,7 @@ button:not(.clickable-icon).infio-chat-list-dropdown { /* position: fixed; */ border: 1px solid var(--background-modifier-border); overflow: hidden; - width: 240px; + min-width: 60px; max-width: 240px; }