From 82eb8b7c6569f531f89edf71a101c4b229c486e7 Mon Sep 17 00:00:00 2001 From: ftboy Date: Mon, 15 Sep 2025 21:49:29 +0800 Subject: [PATCH] feat(prompt): Add a system prompt word function, which includes the following content - System information prompt words - Output prompt words in markdown format - User defined prompt words - Tool usage standard prompt words - Tool usage prompt words --- package.json | 2 +- src/main/presenter/configPresenter/index.ts | 16 +- .../sections/custom-instructions.ts | 212 ++++++++++++++++++ .../promptPresenter/sections/index.ts | 6 + .../sections/markdown-formatting.ts | 7 + .../promptPresenter/sections/objective.ts | 15 ++ .../promptPresenter/sections/system-info.ts | 16 ++ .../sections/tool-use-guidelines.ts | 40 ++++ .../promptPresenter/sections/tool-use.ts | 19 ++ src/main/presenter/promptPresenter/system.ts | 61 +++++ src/main/presenter/promptPresenter/types.ts | 49 ++++ .../components/settings/DisplaySettings.vue | 4 +- src/shared/language.ts | 56 +++++ src/shared/path.ts | 106 +++++++++ 14 files changed, 600 insertions(+), 9 deletions(-) create mode 100644 src/main/presenter/promptPresenter/sections/custom-instructions.ts create mode 100644 src/main/presenter/promptPresenter/sections/index.ts create mode 100644 src/main/presenter/promptPresenter/sections/markdown-formatting.ts create mode 100644 src/main/presenter/promptPresenter/sections/objective.ts create mode 100644 src/main/presenter/promptPresenter/sections/system-info.ts create mode 100644 src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts create mode 100644 src/main/presenter/promptPresenter/sections/tool-use.ts create mode 100644 src/main/presenter/promptPresenter/system.ts create mode 100644 src/main/presenter/promptPresenter/types.ts create mode 100644 src/shared/language.ts create mode 100644 src/shared/path.ts diff --git a/package.json b/package.json index e6bf86d..12e9653 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ }, "packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad", "scripts": { - "preinstall": "npx only-allow pnpm", "test": "vitest", "test:main": "vitest --config vitest.config.ts test/main", "test:renderer": "vitest --config vitest.config.renderer.ts test/renderer", @@ -85,6 +84,7 @@ "nanoid": "^5.1.5", "ollama": "^0.5.17", "openai": "^5.18.0", + "os-name": "^6.1.0", "pdf-parse-new": "^1.4.1", "run-applescript": "^7.0.0", "sharp": "^0.33.5", diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 24f02fc..69fdf1f 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -30,6 +30,7 @@ import { compare } from 'compare-versions' import { defaultShortcutKey, ShortcutKeySetting } from './shortcutKeySettings' import { ModelConfigHelper } from './modelConfig' import { KnowledgeConfHelper } from './knowledgeConfHelper' +import { SYSTEM_PROMPT } from '../promptPresenter/system' // Default system prompt constant const DEFAULT_SYSTEM_PROMPT = `You are DeepChat, a highly capable AI assistant. Your goal is to fully complete the user’s requested task before handing the conversation back to them. Keep working autonomously until the task is fully resolved. @@ -1231,12 +1232,8 @@ export class ConfigPresenter implements IConfigPresenter { // 获取默认系统提示词 async getDefaultSystemPrompt(): Promise { - const prompts = await this.getSystemPrompts() - const defaultPrompt = prompts.find((p) => p.isDefault) - if (defaultPrompt) { - return defaultPrompt.content - } - return this.getSetting('default_system_prompt') || '' + const default_system_prompt = await this.getBuildInSystemPrompt() + return default_system_prompt } // 设置默认系统提示词 @@ -1254,6 +1251,13 @@ export class ConfigPresenter implements IConfigPresenter { this.setSetting('default_system_prompt', '') } + private async getBuildInSystemPrompt(): Promise { + //获取内置的系统提示词 + return await (async () => { + return SYSTEM_PROMPT('', '', this.getLanguage(), '') + })() + } + async getSystemPrompts(): Promise { try { return this.systemPromptsStore.get('prompts') || [] diff --git a/src/main/presenter/promptPresenter/sections/custom-instructions.ts b/src/main/presenter/promptPresenter/sections/custom-instructions.ts new file mode 100644 index 0000000..4a58bd3 --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/custom-instructions.ts @@ -0,0 +1,212 @@ +import fs from 'fs/promises' +import path from 'path' +import { Dirent } from 'fs' + +import { LANGUAGES, isLanguage } from '../../../../shared/language' + +/** + * Safely read a file and return its trimmed content + */ +async function safeReadFile(filePath: string): Promise { + try { + const content = await fs.readFile(filePath, 'utf-8') + return content.trim() + } catch (err) { + const errorCode = (err as NodeJS.ErrnoException).code + if (!errorCode || !['ENOENT', 'EISDIR'].includes(errorCode)) { + throw err + } + return '' + } +} + +const MAX_DEPTH = 5 + +/** + * Recursively resolve directory entries and collect file paths + */ +async function resolveDirectoryEntry( + entry: Dirent, + dirPath: string, + fileInfo: Array<{ originalPath: string; resolvedPath: string }>, + depth: number +): Promise { + // Avoid cyclic symlinks + if (depth > MAX_DEPTH) { + return + } + + const fullPath = path.resolve(entry.parentPath || dirPath, entry.name) + if (entry.isFile()) { + // Regular file - both original and resolved paths are the same + fileInfo.push({ originalPath: fullPath, resolvedPath: fullPath }) + } else if (entry.isSymbolicLink()) { + // Await the resolution of the symbolic link + await resolveSymLink(fullPath, fileInfo, depth + 1) + } +} + +/** + * Recursively resolve a symbolic link and collect file paths + */ +async function resolveSymLink( + symlinkPath: string, + fileInfo: Array<{ originalPath: string; resolvedPath: string }>, + depth: number +): Promise { + // Avoid cyclic symlinks + if (depth > MAX_DEPTH) { + return + } + try { + // Get the symlink target + const linkTarget = await fs.readlink(symlinkPath) + // Resolve the target path (relative to the symlink location) + const resolvedTarget = path.resolve(path.dirname(symlinkPath), linkTarget) + + // Check if the target is a file + const stats = await fs.stat(resolvedTarget) + if (stats.isFile()) { + // For symlinks to files, store the symlink path as original and target as resolved + fileInfo.push({ originalPath: symlinkPath, resolvedPath: resolvedTarget }) + } else if (stats.isDirectory()) { + const anotherEntries = await fs.readdir(resolvedTarget, { + withFileTypes: true, + recursive: true + }) + // Collect promises for recursive calls within the directory + const directoryPromises: Promise[] = [] + for (const anotherEntry of anotherEntries) { + directoryPromises.push( + resolveDirectoryEntry(anotherEntry, resolvedTarget, fileInfo, depth + 1) + ) + } + // Wait for all entries in the resolved directory to be processed + await Promise.all(directoryPromises) + } else if (stats.isSymbolicLink()) { + // Handle nested symlinks by awaiting the recursive call + await resolveSymLink(resolvedTarget, fileInfo, depth + 1) + } + } catch (err) { + // Skip invalid symlinks + } +} + +/** + * Load rule files from global and project-local directories + * Global rules are loaded first, then project-local rules which can override global ones + */ +export async function loadRuleFiles(cwd: string): Promise { + const rules: string[] = [] + + // If we found rules in .roo/rules/ directories, return them + if (rules.length > 0) { + return '\n' + rules.join('\n\n') + } + + // Fall back to existing behavior for legacy .roorules/.clinerules files + const ruleFiles = ['.roorules', '.clinerules'] + + for (const file of ruleFiles) { + const content = await safeReadFile(path.join(cwd, file)) + if (content) { + return `\n# Rules from ${file}:\n${content}\n` + } + } + + return '' +} + +export async function addCustomInstructions( + modeCustomInstructions: string, + globalCustomInstructions: string, + cwd: string, + mode: string, + options: { + language?: string + IgnoreInstructions?: string + } = {} +): Promise { + const sections: string[] = [] + + // Load mode-specific rules if mode is provided + let modeRuleContent = '' + let usedRuleFile = '' + + if (mode) { + const modeRules: string[] = [] + + // If we found mode-specific rules in .roo/rules-${mode}/ directories, use them + if (modeRules.length > 0) { + modeRuleContent = '\n' + modeRules.join('\n\n') + usedRuleFile = `rules-${mode} directories` + } else { + // Fall back to existing behavior for legacy files + const rooModeRuleFile = `.roorules-${mode}` + modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile)) + if (modeRuleContent) { + usedRuleFile = rooModeRuleFile + } else { + const clineModeRuleFile = `.clinerules-${mode}` + modeRuleContent = await safeReadFile(path.join(cwd, clineModeRuleFile)) + if (modeRuleContent) { + usedRuleFile = clineModeRuleFile + } + } + } + } + + // Add language preference if provided + if (options.language) { + const languageName = isLanguage(options.language) + ? LANGUAGES[options.language] + : options.language + sections.push( + `Language Preference:\nYou should always speak and think in the "${languageName}" (${options.language}) language unless the user gives you instructions below to do otherwise.` + ) + } + + // Add global instructions first + if (typeof globalCustomInstructions === 'string' && globalCustomInstructions.trim()) { + sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`) + } + + // Add mode-specific instructions after + if (typeof modeCustomInstructions === 'string' && modeCustomInstructions.trim()) { + sections.push(`Mode-specific Instructions:\n${modeCustomInstructions.trim()}`) + } + + // Add rules - include both mode-specific and generic rules if they exist + const rules: string[] = [] + + // Add mode-specific rules first if they exist + if (modeRuleContent && modeRuleContent.trim()) { + if (usedRuleFile.includes(path.join('.roo', `rules-${mode}`))) { + rules.push(modeRuleContent.trim()) + } else { + rules.push(`# Rules from ${usedRuleFile}:\n${modeRuleContent}`) + } + } + + if (options.IgnoreInstructions) { + rules.push(options.IgnoreInstructions) + } + + if (rules.length > 0) { + sections.push(`Rules:\n\n${rules.join('\n\n')}`) + } + + const joinedSections = sections.join('\n\n') + + return joinedSections + ? ` + ==== + + USER'S CUSTOM INSTRUCTIONS + + The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. + + ${joinedSections} + ` + : '' +} diff --git a/src/main/presenter/promptPresenter/sections/index.ts b/src/main/presenter/promptPresenter/sections/index.ts new file mode 100644 index 0000000..e87c8b4 --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/index.ts @@ -0,0 +1,6 @@ +export { getSystemInfoSection } from './system-info' +export { getObjectiveSection } from './objective' +export { addCustomInstructions } from './custom-instructions' +export { getSharedToolUseSection } from './tool-use' +export { getToolUseGuidelinesSection } from './tool-use-guidelines' +export { markdownFormattingSection } from './markdown-formatting' diff --git a/src/main/presenter/promptPresenter/sections/markdown-formatting.ts b/src/main/presenter/promptPresenter/sections/markdown-formatting.ts new file mode 100644 index 0000000..ec102f2 --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/markdown-formatting.ts @@ -0,0 +1,7 @@ +export function markdownFormattingSection(): string { + return `==== + +MARKDOWN RULES + +ALL responses MUST show ANY \`language construct\` OR filename reference as clickable, exactly as [\`filename OR language.declaration()\`](relative/file/path.ext:line); line is required for \`syntax\` and optional for filename links. This applies to ALL markdown responses and ALSO those in ` +} diff --git a/src/main/presenter/promptPresenter/sections/objective.ts b/src/main/presenter/promptPresenter/sections/objective.ts new file mode 100644 index 0000000..7608eb5 --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/objective.ts @@ -0,0 +1,15 @@ +export function getObjectiveSection(): string { + const codebaseSearchInstruction = 'First, ' + + return `==== + +OBJECTIVE + +You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically. + +1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order. +2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. +3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. ${codebaseSearchInstruction}analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. +4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.` +} diff --git a/src/main/presenter/promptPresenter/sections/system-info.ts b/src/main/presenter/promptPresenter/sections/system-info.ts new file mode 100644 index 0000000..394b389 --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/system-info.ts @@ -0,0 +1,16 @@ +import os from 'os' +import osName from 'os-name' + +export function getSystemInfoSection(cwd: string): string { + let details = `==== +f +SYSTEM INFORMATION + +Operating System: ${osName()} +System Arch: ${os.arch()} +Home Directory: ${os.homedir()} +Current Workspace Directory: ${cwd} + +` + return details +} diff --git a/src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts b/src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts new file mode 100644 index 0000000..76f5281 --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/tool-use-guidelines.ts @@ -0,0 +1,40 @@ +export function getToolUseGuidelinesSection(): string { + let itemNumber = 1 + const guidelinesList: string[] = [] + + guidelinesList.push( + `${itemNumber++}. Analyze user intent, determine whether to call the tool based on its description, and select the most matching tool or tools.` + ) + + // First guideline is always the same + guidelinesList.push( + `${itemNumber++}. Assess what information you already have ,and proactively and clearly inquire about missing necessary parameters to ensure that the format of the call request fully complies with the tool interface specifications.` + ) + + // Remaining guidelines + guidelinesList.push( + `${itemNumber++}. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Each step must be informed by the previous step's result.` + ) + guidelinesList.push( + `${itemNumber++}. Formulate your tool use using the XML format specified for each tool.` + ) + guidelinesList.push( + `${itemNumber++}. Properly handle any errors in tool calls, convert them into user-friendly prompts that users can understand, and do not expose technical details.` + ) + guidelinesList.push( + `${itemNumber++}. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.` + ) + + // Join guidelines and add the footer + return `# Tool Use Guidelines + +${guidelinesList.join('\n')} + +It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to: +1. Confirm the success of each step before proceeding. +2. Address any issues or errors that arise immediately. +3. Adapt your approach based on new information or unexpected results. +4. Ensure that each action builds correctly on the previous ones. + +By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.` +} diff --git a/src/main/presenter/promptPresenter/sections/tool-use.ts b/src/main/presenter/promptPresenter/sections/tool-use.ts new file mode 100644 index 0000000..aa6ae2f --- /dev/null +++ b/src/main/presenter/promptPresenter/sections/tool-use.ts @@ -0,0 +1,19 @@ +export function getSharedToolUseSection(): string { + return `==== + +TOOL USE + +You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use. + +# Tool Use Formatting + +Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure: + + +value1 +value2 +... + + +Always use the actual tool name as the XML tag name for proper parsing and execution.` +} diff --git a/src/main/presenter/promptPresenter/system.ts b/src/main/presenter/promptPresenter/system.ts new file mode 100644 index 0000000..ce45fa4 --- /dev/null +++ b/src/main/presenter/promptPresenter/system.ts @@ -0,0 +1,61 @@ +import { presenter } from '@/presenter' +import type { PromptComponent, CustomModePrompts } from './types' +import { formatLanguage } from '../../../shared/language' +import { + getSystemInfoSection, + getObjectiveSection, + getSharedToolUseSection, + getToolUseGuidelinesSection, + addCustomInstructions, + markdownFormattingSection +} from './sections' + +// Helper function to get prompt component, filtering out empty objects +export function getPromptComponent( + customModePrompts: CustomModePrompts | undefined, + mode: string +): PromptComponent | undefined { + const component = customModePrompts?.[mode] + // Return undefined if component is empty + if (component == null) { + return undefined + } + return component +} + +async function generatePrompt( + cwd?: string, + globalCustomInstructions?: string, + language?: string, + IgnoreInstructions?: string +): Promise { + const basePrompt = ` + + ${markdownFormattingSection()} + + ${getSharedToolUseSection()} + )} + + ${getToolUseGuidelinesSection()} + + ${getSystemInfoSection(cwd || '')} + + ${getObjectiveSection()} + + ${await addCustomInstructions('', globalCustomInstructions || '', cwd || '', '', { + language: language ?? formatLanguage(presenter.configPresenter.getLanguage()), + IgnoreInstructions + })} + ` + + return basePrompt +} + +export const SYSTEM_PROMPT = async ( + cwd?: string, + globalCustomInstructions?: string, + language?: string, + IgnoreInstructions?: string +): Promise => { + return generatePrompt(cwd, globalCustomInstructions, language, IgnoreInstructions) +} diff --git a/src/main/presenter/promptPresenter/types.ts b/src/main/presenter/promptPresenter/types.ts new file mode 100644 index 0000000..7a39c19 --- /dev/null +++ b/src/main/presenter/promptPresenter/types.ts @@ -0,0 +1,49 @@ +import { z } from 'zod' +/** + * Settings passed to system prompt generation functions + */ +export interface SystemPromptSettings { + maxConcurrentFileReads: number + todoListEnabled: boolean + useAgentRules: boolean + newTaskRequireTodos: boolean +} + +/** + * PromptComponent + */ + +export const promptComponentSchema = z.object({ + roleDefinition: z.string().optional(), + whenToUse: z.string().optional(), + description: z.string().optional(), + customInstructions: z.string().optional() +}) + +export type PromptComponent = z.infer + +/** + * CustomModePrompts + */ + +export const customModePromptsSchema = z.record(z.string(), promptComponentSchema.optional()) + +export type CustomModePrompts = z.infer + +/** + * TodoStatus + */ +export const todoStatusSchema = z.enum(['pending', 'in_progress', 'completed'] as const) + +export type TodoStatus = z.infer + +/** + * TodoItem + */ +export const todoItemSchema = z.object({ + id: z.string(), + content: z.string(), + status: todoStatusSchema +}) + +export type TodoItem = z.infer diff --git a/src/renderer/src/components/settings/DisplaySettings.vue b/src/renderer/src/components/settings/DisplaySettings.vue index 75ffc51..c084f98 100644 --- a/src/renderer/src/components/settings/DisplaySettings.vue +++ b/src/renderer/src/components/settings/DisplaySettings.vue @@ -182,8 +182,8 @@ const languageOptions = [ { value: 'system', label: t('common.languageSystem') || '跟随系统' }, // 使用i18n key 或 默认值 { value: 'zh-CN', label: '简体中文' }, { value: 'en-US', label: 'English (US)' }, - { value: 'zh-TW', label: '繁體中文(台灣)' }, - { value: 'zh-HK', label: '繁體中文(香港)' }, + { value: 'zh-TW', label: '繁體中文(中国台灣)' }, + { value: 'zh-HK', label: '繁體中文(中国香港)' }, { value: 'ko-KR', label: '한국어' }, { value: 'ru-RU', label: 'Русский' }, { value: 'ja-JP', label: '日本語' }, diff --git a/src/shared/language.ts b/src/shared/language.ts new file mode 100644 index 0000000..3c05c47 --- /dev/null +++ b/src/shared/language.ts @@ -0,0 +1,56 @@ +import { z } from 'zod' +/** + * Language name mapping from ISO codes to full language names. + */ + +export const LANGUAGES: Record = { + 'zh-CN': '简体中文', + 'en-US': 'English (US)', + 'zh-TW': '繁體中文(中国台灣)', + 'zh-HK': '繁體中文(中国香港)', + 'ko-KR': '한국어', + 'ru-RU': 'Русский', + 'ja-JP': '日本語', + 'fr-FR': 'Français', + 'fa-IR': 'فارسی (ایران)' +} + +/** + * Formats a VSCode locale string to ensure the region code is uppercase. + * For example, transforms "en-us" to "en-US" or "fr-ca" to "fr-CA". + * + * @param vscodeLocale - The VSCode locale string to format (e.g., "en-us", "fr-ca") + * @returns The formatted locale string with uppercase region code + */ + +export function formatLanguage(vscodeLocale: string): Language { + if (!vscodeLocale) { + return 'en-US' + } + + const formattedLocale = vscodeLocale.replace(/-(\w+)$/, (_, region) => `-${region.toUpperCase()}`) + return isLanguage(formattedLocale) ? formattedLocale : 'en-US' +} + +/** + * Language + */ + +export const languages = [ + 'zh-CN', + 'en-US', + 'zh-TW', + 'zh-HK', + 'ko-KR', + 'ru-RU', + 'ja-JP', + 'fr-FR', + 'fa-IR' +] as const + +export const languagesSchema = z.enum(languages) + +export type Language = z.infer + +export const isLanguage = (value: string): value is Language => + languages.includes(value as Language) diff --git a/src/shared/path.ts b/src/shared/path.ts new file mode 100644 index 0000000..636ae44 --- /dev/null +++ b/src/shared/path.ts @@ -0,0 +1,106 @@ +import * as path from 'path' +import os from 'os' + +/* +The Node.js 'path' module resolves and normalizes paths differently depending on the platform: +- On Windows, it uses backslashes (\) as the default path separator. +- On POSIX-compliant systems (Linux, macOS), it uses forward slashes (/) as the default path separator. + +While modules like 'upath' can be used to normalize paths to use forward slashes consistently, +this can create inconsistencies when interfacing with other modules (like vscode.fs) that use +backslashes on Windows. + +Our approach: +1. We present paths with forward slashes to the AI and user for consistency. +2. We use the 'arePathsEqual' function for safe path comparisons. +3. Internally, Node.js gracefully handles both backslashes and forward slashes. + +This strategy ensures consistent path presentation while leveraging Node.js's built-in +path handling capabilities across different platforms. + +Note: When interacting with the file system or VS Code APIs, we still use the native path module +to ensure correct behavior on all platforms. The toPosixPath and arePathsEqual functions are +primarily used for presentation and comparison purposes, not for actual file system operations. + +Observations: +- Macos isn't so flexible with mixed separators, whereas windows can handle both. ("Node.js does automatically handle path separators on Windows, converting forward slashes to backslashes as needed. However, on macOS and other Unix-like systems, the path separator is always a forward slash (/), and backslashes are treated as regular characters.") +*/ + +function toPosixPath(p: string) { + // Extended-Length Paths in Windows start with "\\?\" to allow longer paths and bypass usual parsing. If detected, we return the path unmodified to maintain functionality, as altering these paths could break their special syntax. + const isExtendedLengthPath = p.startsWith('\\\\?\\') + + if (isExtendedLengthPath) { + return p + } + + return p.replace(/\\/g, '/') +} + +// Declaration merging allows us to add a new method to the String type +// You must import this file in your entry point (extension.ts) to have access at runtime +declare global { + interface String { + toPosix(): string + } +} + +String.prototype.toPosix = function (this: string): string { + return toPosixPath(this) +} + +// Safe path comparison that works across different platforms +export function arePathsEqual(path1?: string, path2?: string): boolean { + if (!path1 && !path2) { + return true + } + if (!path1 || !path2) { + return false + } + + path1 = normalizePath(path1) + path2 = normalizePath(path2) + + if (process.platform === 'win32') { + return path1.toLowerCase() === path2.toLowerCase() + } + return path1 === path2 +} + +function normalizePath(p: string): string { + // normalize resolve ./.. segments, removes duplicate slashes, and standardizes path separators + let normalized = path.normalize(p) + // however it doesn't remove trailing slashes + // remove trailing slash, except for root paths + if (normalized.length > 1 && (normalized.endsWith('/') || normalized.endsWith('\\'))) { + normalized = normalized.slice(0, -1) + } + return normalized +} + +export function getReadablePath(cwd: string, relPath?: string): string { + relPath = relPath || '' + // path.resolve is flexible in that it will resolve relative paths like '../../' to the cwd and even ignore the cwd if the relPath is actually an absolute path + const absolutePath = path.resolve(cwd, relPath) + if (arePathsEqual(cwd, path.join(os.homedir(), 'Desktop'))) { + // User opened vscode without a workspace, so cwd is the Desktop. Show the full absolute path to keep the user aware of where files are being created + return absolutePath.toPosix() + } + if (arePathsEqual(path.normalize(absolutePath), path.normalize(cwd))) { + return path.basename(absolutePath).toPosix() + } else { + // show the relative path to the cwd + const normalizedRelPath = path.relative(cwd, absolutePath) + if (absolutePath.includes(cwd)) { + return normalizedRelPath.toPosix() + } else { + // we are outside the cwd, so show the absolute path (useful for when cline passes in '../../' for example) + return absolutePath.toPosix() + } + } +} + +export const toRelativePath = (filePath: string, cwd: string) => { + const relativePath = path.relative(cwd, filePath).toPosix() + return filePath.endsWith('/') ? relativePath + '/' : relativePath +} -- Gitee