diff --git a/locales/ca/README.md b/locales/ca/README.md index c75275881ca..e16a534ec18 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -226,4 +226,4 @@ Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code! --- -**Gaudiu de Roo Code!** Tant si el manteniu amb corretja curta com si el deixeu actuar de forma autònoma, estem impacients per veure què construïu. Si teniu preguntes o idees per a noves funcionalitats, passeu per la nostra [comunitat de Reddit](https://www.reddit.com/r/RooCode/) o [Discord](https://discord.gg/roocode). Feliç programació! +**Gaudiu de Roo Code!** Tant si el manteniu amb corretja curta com si el deixeu actuar de forma autònoma, estem impacients per veure què construïu. Si teniu preguntes o idees per a noves funcionalitats, passeu per la nostra [comunitat de Reddit](https://www.reddit.com/r/RooCode/) o [Discord](https://discord.gg/roocode). Feliç programació! \ No newline at end of file diff --git a/locales/de/README.md b/locales/de/README.md index e9357daa476..b4475333bf4 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -226,4 +226,4 @@ Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code zu verbessern! --- -**Genießen Sie Roo Code!** Ob Sie ihn an der kurzen Leine halten oder autonom agieren lassen, wir können es kaum erwarten zu sehen, was Sie bauen. Wenn Sie Fragen oder Funktionsideen haben, schauen Sie in unserer [Reddit-Community](https://www.reddit.com/r/RooCode/) oder auf [Discord](https://discord.gg/roocode) vorbei. Frohes Coding! +**Genießen Sie Roo Code!** Ob Sie ihn an der kurzen Leine halten oder autonom agieren lassen, wir können es kaum erwarten zu sehen, was Sie bauen. Wenn Sie Fragen oder Funktionsideen haben, schauen Sie in unserer [Reddit-Community](https://www.reddit.com/r/RooCode/) oder auf [Discord](https://discord.gg/roocode) vorbei. Frohes Coding! \ No newline at end of file diff --git a/locales/es/README.md b/locales/es/README.md index 09eb1594798..3f721c23691 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -226,4 +226,4 @@ Usamos [changesets](https://github.com/changesets/changesets) para versionar y p --- -**¡Disfruta Roo Code!** Ya sea que lo mantengas con correa corta o lo dejes vagar de forma autónoma, estamos ansiosos por ver lo que construyes. Si tienes preguntas o ideas para nuevas funciones, visita nuestra [comunidad de Reddit](https://www.reddit.com/r/RooCode/) o [Discord](https://discord.gg/roocode). ¡Feliz programación! +**¡Disfruta Roo Code!** Ya sea que lo mantengas con correa corta o lo dejes vagar de forma autónoma, estamos ansiosos por ver lo que construyes. Si tienes preguntas o ideas para nuevas funciones, visita nuestra [comunidad de Reddit](https://www.reddit.com/r/RooCode/) o [Discord](https://discord.gg/roocode). ¡Feliz programación! \ No newline at end of file diff --git a/locales/fr/README.md b/locales/fr/README.md index 0ca48da21e2..6ddb66a02ee 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -226,4 +226,4 @@ Merci à tous nos contributeurs qui ont aidé à améliorer Roo Code ! --- -**Profitez de Roo Code !** Que vous le gardiez en laisse courte ou que vous le laissiez se déplacer de manière autonome, nous avons hâte de voir ce que vous allez construire. Si vous avez des questions ou des idées de fonctionnalités, passez par notre [communauté Reddit](https://www.reddit.com/r/RooCode/) ou [Discord](https://discord.gg/roocode). Bon codage ! +**Profitez de Roo Code !** Que vous le gardiez en laisse courte ou que vous le laissiez se déplacer de manière autonome, nous avons hâte de voir ce que vous allez construire. Si vous avez des questions ou des idées de fonctionnalités, passez par notre [communauté Reddit](https://www.reddit.com/r/RooCode/) ou [Discord](https://discord.gg/roocode). Bon codage ! \ No newline at end of file diff --git a/locales/hi/README.md b/locales/hi/README.md index aa3da73d0f0..582076856be 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -226,4 +226,4 @@ Roo Code को बेहतर बनाने में मदद करने --- -**Roo Code का आनंद लें!** चाहे आप इसे छोटी रस्सी पर रखें या स्वायत्त रूप से घूमने दें, हम यह देखने के लिए इंतज़ार नहीं कर सकते कि आप क्या बनाते हैं। यदि आपके पास प्रश्न या फीचर आइडिया हैं, तो हमारे [Reddit समुदाय](https://www.reddit.com/r/RooCode/) या [Discord](https://discord.gg/roocode) पर आएं। हैप्पी कोडिंग! +**Roo Code का आनंद लें!** चाहे आप इसे छोटी रस्सी पर रखें या स्वायत्त रूप से घूमने दें, हम यह देखने के लिए इंतज़ार नहीं कर सकते कि आप क्या बनाते हैं। यदि आपके पास प्रश्न या फीचर आइडिया हैं, तो हमारे [Reddit समुदाय](https://www.reddit.com/r/RooCode/) या [Discord](https://discord.gg/roocode) पर आएं। हैप्पी कोडिंग! \ No newline at end of file diff --git a/locales/id/README.md b/locales/id/README.md index 2aa0d6b4235..6aefda5683c 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -220,4 +220,4 @@ Terima kasih kepada semua kontributor kami yang telah membantu membuat Roo Code --- -**Nikmati Roo Code!** Baik kamu menggunakannya dengan ketat atau membiarkannya berjalan otonom, kami tidak sabar melihat apa yang kamu bangun. Jika kamu memiliki pertanyaan atau ide fitur, kunjungi [komunitas Reddit](https://www.reddit.com/r/RooCode/) atau [Discord](https://discord.gg/roocode) kami. Selamat coding! +**Nikmati Roo Code!** Baik kamu menggunakannya dengan ketat atau membiarkannya berjalan otonom, kami tidak sabar melihat apa yang kamu bangun. Jika kamu memiliki pertanyaan atau ide fitur, kunjungi [komunitas Reddit](https://www.reddit.com/r/RooCode/) atau [Discord](https://discord.gg/roocode) kami. Selamat coding! \ No newline at end of file diff --git a/locales/it/README.md b/locales/it/README.md index c357f4330d1..ff3115e696c 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -226,4 +226,4 @@ Grazie a tutti i nostri contributori che hanno aiutato a migliorare Roo Code! --- -**Goditi Roo Code!** Che tu lo tenga al guinzaglio corto o lo lasci vagare autonomamente, non vediamo l'ora di vedere cosa costruirai. Se hai domande o idee per funzionalità, passa dalla nostra [community di Reddit](https://www.reddit.com/r/RooCode/) o [Discord](https://discord.gg/roocode). Buona programmazione! +**Goditi Roo Code!** Che tu lo tenga al guinzaglio corto o lo lasci vagare autonomamente, non vediamo l'ora di vedere cosa costruirai. Se hai domande o idee per funzionalità, passa dalla nostra [community di Reddit](https://www.reddit.com/r/RooCode/) o [Discord](https://discord.gg/roocode). Buona programmazione! \ No newline at end of file diff --git a/locales/ja/README.md b/locales/ja/README.md index 2c8b5ecf275..d99f214d32d 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -226,4 +226,4 @@ Roo Codeの改善に貢献してくれたすべての貢献者に感謝します --- -**Roo Codeをお楽しみください!** 短いリードで保持するか、自律的に動き回らせるかにかかわらず、あなたが何を構築するのか楽しみにしています。質問や機能のアイデアがある場合は、[Redditコミュニティ](https://www.reddit.com/r/RooCode/)や[Discord](https://discord.gg/roocode)にお立ち寄りください。ハッピーコーディング! +**Roo Codeをお楽しみください!** 短いリードで保持するか、自律的に動き回らせるかにかかわらず、あなたが何を構築するのか楽しみにしています。質問や機能のアイデアがある場合は、[Redditコミュニティ](https://www.reddit.com/r/RooCode/)や[Discord](https://discord.gg/roocode)にお立ち寄りください。ハッピーコーディング! \ No newline at end of file diff --git a/locales/ko/README.md b/locales/ko/README.md index 3b41d7927d1..96f3e5ee169 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -226,4 +226,4 @@ Roo Code를 더 좋게 만드는 데 도움을 준 모든 기여자에게 감사 --- -**Roo Code를 즐기세요!** 짧은 목줄에 묶어두든 자율적으로 돌아다니게 하든, 여러분이 무엇을 만들지 기대됩니다. 질문이나 기능 아이디어가 있으시면 [Reddit 커뮤니티](https://www.reddit.com/r/RooCode/) 또는 [Discord](https://discord.gg/roocode)를 방문해 주세요. 행복한 코딩 되세요! +**Roo Code를 즐기세요!** 짧은 목줄에 묶어두든 자율적으로 돌아다니게 하든, 여러분이 무엇을 만들지 기대됩니다. 질문이나 기능 아이디어가 있으시면 [Reddit 커뮤니티](https://www.reddit.com/r/RooCode/) 또는 [Discord](https://discord.gg/roocode)를 방문해 주세요. 행복한 코딩 되세요! \ No newline at end of file diff --git a/locales/nl/README.md b/locales/nl/README.md index 77372be7ff1..e751dc589e8 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -226,4 +226,4 @@ Dank aan alle bijdragers die Roo Code beter hebben gemaakt! --- -**Veel plezier met Roo Code!** Of je het nu kort houdt of autonoom laat werken, we zijn benieuwd wat je bouwt. Heb je vragen of ideeën voor functies, kom dan langs op onze [Reddit-community](https://www.reddit.com/r/RooCode/) of [Discord](https://discord.gg/roocode). Veel programmeerplezier! +**Veel plezier met Roo Code!** Of je het nu kort houdt of autonoom laat werken, we zijn benieuwd wat je bouwt. Heb je vragen of ideeën voor functies, kom dan langs op onze [Reddit-community](https://www.reddit.com/r/RooCode/) of [Discord](https://discord.gg/roocode). Veel programmeerplezier! \ No newline at end of file diff --git a/locales/pl/README.md b/locales/pl/README.md index 5a44275d6e2..b30145803bd 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -226,4 +226,4 @@ Dziękujemy wszystkim naszym współtwórcom, którzy pomogli ulepszyć Roo Code --- -**Ciesz się Roo Code!** Niezależnie od tego, czy trzymasz go na krótkiej smyczy, czy pozwalasz mu swobodnie działać autonomicznie, nie możemy się doczekać, aby zobaczyć, co zbudujesz. Jeśli masz pytania lub pomysły na funkcje, wpadnij na naszą [społeczność Reddit](https://www.reddit.com/r/RooCode/) lub [Discord](https://discord.gg/roocode). Szczęśliwego kodowania! +**Ciesz się Roo Code!** Niezależnie od tego, czy trzymasz go na krótkiej smyczy, czy pozwalasz mu swobodnie działać autonomicznie, nie możemy się doczekać, aby zobaczyć, co zbudujesz. Jeśli masz pytania lub pomysły na funkcje, wpadnij na naszą [społeczność Reddit](https://www.reddit.com/r/RooCode/) lub [Discord](https://discord.gg/roocode). Szczęśliwego kodowania! \ No newline at end of file diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 8412b94ffc5..ecadea0b403 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -226,4 +226,4 @@ Obrigado a todos os nossos contribuidores que ajudaram a tornar o Roo Code melho --- -**Aproveite o Roo Code!** Seja você o mantenha em uma coleira curta ou deixe-o vagar autonomamente, mal podemos esperar para ver o que você construirá. Se você tiver dúvidas ou ideias de recursos, passe por nossa [comunidade Reddit](https://www.reddit.com/r/RooCode/) ou [Discord](https://discord.gg/roocode). Feliz codificação! +**Aproveite o Roo Code!** Seja você o mantenha em uma coleira curta ou deixe-o vagar autonomamente, mal podemos esperar para ver o que você construirá. Se você tiver dúvidas ou ideias de recursos, passe por nossa [comunidade Reddit](https://www.reddit.com/r/RooCode/) ou [Discord](https://discord.gg/roocode). Feliz codificação! \ No newline at end of file diff --git a/locales/ru/README.md b/locales/ru/README.md index e3161a73d85..808fe5a6c2f 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -226,4 +226,4 @@ code --install-extension bin/roo-cline-.vsix --- -**Наслаждайтесь Roo Code!** Независимо от того, держите ли вы его на коротком поводке или позволяете действовать автономно, мы с нетерпением ждем, что вы создадите. Если у вас есть вопросы или идеи для функций, заходите в наше [сообщество Reddit](https://www.reddit.com/r/RooCode/) или [Discord](https://discord.gg/roocode). Счастливого кодирования! +**Наслаждайтесь Roo Code!** Независимо от того, держите ли вы его на коротком поводке или позволяете действовать автономно, мы с нетерпением ждем, что вы создадите. Если у вас есть вопросы или идеи для функций, заходите в наше [сообщество Reddit](https://www.reddit.com/r/RooCode/) или [Discord](https://discord.gg/roocode). Счастливого кодирования! \ No newline at end of file diff --git a/locales/tr/README.md b/locales/tr/README.md index 7e65ebbafd2..05080ae1d87 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -226,4 +226,4 @@ Roo Code'u daha iyi hale getirmeye yardımcı olan tüm katkıda bulunanlara te --- -**Roo Code'un keyfini çıkarın!** İster kısa bir tasmayla tutun ister otonom dolaşmasına izin verin, ne inşa edeceğinizi görmek için sabırsızlanıyoruz. Sorularınız veya özellik fikirleriniz varsa, [Reddit topluluğumuza](https://www.reddit.com/r/RooCode/) veya [Discord'umuza](https://discord.gg/roocode) uğrayın. Mutlu kodlamalar! +**Roo Code'un keyfini çıkarın!** İster kısa bir tasmayla tutun ister otonom dolaşmasına izin verin, ne inşa edeceğinizi görmek için sabırsızlanıyoruz. Sorularınız veya özellik fikirleriniz varsa, [Reddit topluluğumuza](https://www.reddit.com/r/RooCode/) veya [Discord'umuza](https://discord.gg/roocode) uğrayın. Mutlu kodlamalar! \ No newline at end of file diff --git a/locales/vi/README.md b/locales/vi/README.md index 5483d8c2cf5..c44554217c2 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -226,4 +226,4 @@ Cảm ơn tất cả những người đóng góp đã giúp cải thiện Roo C --- -**Hãy tận hưởng Roo Code!** Cho dù bạn giữ nó trên dây ngắn hay để nó tự do hoạt động, chúng tôi rất mong được thấy những gì bạn xây dựng. Nếu bạn có câu hỏi hoặc ý tưởng về tính năng, hãy ghé qua [cộng đồng Reddit](https://www.reddit.com/r/RooCode/) hoặc [Discord](https://discord.gg/roocode) của chúng tôi. Chúc lập trình vui vẻ! +**Hãy tận hưởng Roo Code!** Cho dù bạn giữ nó trên dây ngắn hay để nó tự do hoạt động, chúng tôi rất mong được thấy những gì bạn xây dựng. Nếu bạn có câu hỏi hoặc ý tưởng về tính năng, hãy ghé qua [cộng đồng Reddit](https://www.reddit.com/r/RooCode/) hoặc [Discord](https://discord.gg/roocode) của chúng tôi. Chúc lập trình vui vẻ! \ No newline at end of file diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index a3af91c9350..c0f7ec6bf9e 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -226,4 +226,4 @@ code --install-extension bin/roo-cline-.vsix --- -**享受 Roo Code!** 无论您是让它保持短绳还是让它自主漫游,我们都迫不及待地想看看您会构建什么。如果您有问题或功能想法,请访问我们的 [Reddit 社区](https://www.reddit.com/r/RooCode/)或 [Discord](https://discord.gg/roocode)。编码愉快! +**享受 Roo Code!** 无论您是让它保持短绳还是让它自主漫游,我们都迫不及待地想看看您会构建什么。如果您有问题或功能想法,请访问我们的 [Reddit 社区](https://www.reddit.com/r/RooCode/)或 [Discord](https://discord.gg/roocode)。编码愉快! \ No newline at end of file diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 16f0f7c9363..c620aa9e28c 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -227,4 +227,4 @@ code --install-extension bin/roo-cline-.vsix --- -**享受 Roo Code!** 無論您是將它拴在短繩上還是讓它自主漫遊,我們迫不及待地想看看您會建構什麼。如果您有問題或功能想法,請造訪我們的 [Reddit 社群](https://www.reddit.com/r/RooCode/)或 [Discord](https://discord.gg/roocode)。祝您開發愉快! +**享受 Roo Code!** 無論您是將它拴在短繩上還是讓它自主漫遊,我們迫不及待地想看看您會建構什麼。如果您有問題或功能想法,請造訪我們的 [Reddit 社群](https://www.reddit.com/r/RooCode/)或 [Discord](https://discord.gg/roocode)。祝您開發愉快! \ No newline at end of file diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index d5e76ecceac..6cb5f5ebd1b 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -100,6 +100,7 @@ export const globalSettingsSchema = z.object({ maxOpenTabsContext: z.number().optional(), maxWorkspaceFiles: z.number().optional(), showRooIgnoredFiles: z.boolean().optional(), + enableSvnContext: z.boolean().optional(), maxReadFileLine: z.number().optional(), terminalOutputLineLimit: z.number().optional(), diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index 00f6bbbcba9..0c08cd461b8 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -53,6 +53,8 @@ export const commandIds = [ "focusInput", "acceptInput", "focusPanel", + + "debugSvn", ] as const export type CommandId = (typeof commandIds)[number] diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index bd925b0e900..0469d535d88 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -16,6 +16,7 @@ import { CodeIndexManager } from "../services/code-index/manager" import { importSettingsWithFeedback } from "../core/config/importExport" import { MdmService } from "../services/mdm/MdmService" import { t } from "../i18n" +import { checkSvnInstalled, checkSvnRepo, getSvnRepositoryInfo, searchSvnCommits, SvnLogger } from "../utils/svn" /** * Helper to get the visible ClineProvider instance or log if not found. @@ -218,6 +219,82 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt visibleProvider.postMessageToWebview({ type: "acceptInput" }) }, + debugSvn: async () => { + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + vscode.window.showErrorMessage("No workspace folder is open") + return + } + + const workspaceRoot = workspaceFolders[0].uri.fsPath + SvnLogger.info(`Starting SVN debug for workspace: ${workspaceRoot}`) + + try { + // Check SVN installation and repository status + // Note: These functions now handle their own user-friendly error messages + SvnLogger.info("Checking if SVN is installed...") + const svnInstalled = await checkSvnInstalled() + SvnLogger.info(`SVN installed: ${svnInstalled}`) + + if (!svnInstalled) { + // Error message already shown by checkSvnInstalled + return + } + + SvnLogger.info("Checking if current directory is an SVN repository...") + const isSvnRepo = await checkSvnRepo(workspaceRoot) + SvnLogger.info(`Is SVN repository: ${isSvnRepo}`) + + if (!isSvnRepo) { + // Error message already shown by checkSvnRepo + return + } + + // Get SVN repository information + SvnLogger.info("Getting SVN repository information...") + const repoInfo = await getSvnRepositoryInfo(workspaceRoot) + SvnLogger.info(`Repository info: ${JSON.stringify(repoInfo, null, 2)}`) + + // Search for recent commits + SvnLogger.info("Searching for recent commits...") + const commits = await searchSvnCommits("", workspaceRoot) + SvnLogger.info(`Found ${commits.length} commits`) + + commits.forEach((commit, index) => { + SvnLogger.info(`Commit ${index + 1}: r${commit.revision} - ${commit.message}`) + }) + + // Show success message + const action = await vscode.window.showInformationMessage( + `SVN Debug Complete! Found ${commits.length} commits.\n\nRepository URL: ${repoInfo.repositoryUrl || "Unknown"}\nWorking Copy Root: ${repoInfo.workingCopyRoot || "Unknown"}\n\nCheck "Roo Code - SVN Debug" output channel for detailed information.`, + { modal: false }, + "Show Output", + ) + + if (action === "Show Output") { + SvnLogger.showOutput() + } + } catch (error) { + // This should rarely happen now since individual functions handle their own errors + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.error("SVN debug failed", svnError) + + vscode.window + .showErrorMessage( + "SVN debug operation failed", + { + modal: false, + detail: `Unexpected error: ${svnError.message}\n\nPlease check the SVN Debug output channel for more details.`, + }, + "Show Output", + ) + .then((action) => { + if (action === "Show Output") { + SvnLogger.showOutput() + } + }) + } + }, }) export const openClineInNewTab = async ({ context, outputChannel }: Omit) => { diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 76e502e6c7d..ee27367e19d 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -224,8 +224,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH if (this.options.awsUseApiKey && this.options.awsApiKey) { // Use API key/token-based authentication if enabled and API key is set - clientConfig.token = { token: this.options.awsApiKey } - clientConfig.authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems. + ;(clientConfig as any).token = { token: this.options.awsApiKey } + ;(clientConfig as any).authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems. } else if (this.options.awsUseProfile && this.options.awsProfile) { // Use profile-based credentials if enabled and profile is set clientConfig.credentials = fromIni({ diff --git a/src/core/mentions/__tests__/processUserContentMentions.spec.ts b/src/core/mentions/__tests__/processUserContentMentions.spec.ts index 3aebd66e53b..24dcba18d33 100644 --- a/src/core/mentions/__tests__/processUserContentMentions.spec.ts +++ b/src/core/mentions/__tests__/processUserContentMentions.spec.ts @@ -52,6 +52,7 @@ describe("processUserContentMentions", () => { mockFileContextTracker, mockRooIgnoreController, true, + false, // enableSvnContext true, // includeDiagnosticMessages 50, // maxDiagnosticMessages 100, @@ -81,6 +82,7 @@ describe("processUserContentMentions", () => { mockFileContextTracker, mockRooIgnoreController, true, + false, // enableSvnContext true, // includeDiagnosticMessages 50, // maxDiagnosticMessages undefined, @@ -111,6 +113,7 @@ describe("processUserContentMentions", () => { mockFileContextTracker, mockRooIgnoreController, true, + false, // enableSvnContext true, // includeDiagnosticMessages 50, // maxDiagnosticMessages -1, @@ -315,6 +318,7 @@ describe("processUserContentMentions", () => { mockFileContextTracker, undefined, true, // showRooIgnoredFiles should default to true + false, // enableSvnContext true, // includeDiagnosticMessages 50, // maxDiagnosticMessages undefined, @@ -344,6 +348,7 @@ describe("processUserContentMentions", () => { mockFileContextTracker, undefined, false, + false, // enableSvnContext true, // includeDiagnosticMessages 50, // maxDiagnosticMessages undefined, diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index d0d305d0965..aff9764d9bf 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -7,6 +7,7 @@ import { isBinaryFile } from "isbinaryfile" import { mentionRegexGlobal, unescapeSpaces } from "../../shared/context-mentions" import { getCommitInfo, getWorkingState } from "../../utils/git" +import { getSvnWorkingState, getSvnCommitInfoForMentions } from "../../utils/svn" import { getWorkspacePath } from "../../utils/path" import { openFile } from "../../integrations/misc/open-file" @@ -80,6 +81,7 @@ export async function parseMentions( fileContextTracker?: FileContextTracker, rooIgnoreController?: RooIgnoreController, showRooIgnoredFiles: boolean = true, + enableSvnContext: boolean = false, includeDiagnosticMessages: boolean = true, maxDiagnosticMessages: number = 50, maxReadFileLine?: number, @@ -98,8 +100,12 @@ export async function parseMentions( return `Workspace Problems (see below for diagnostics)` } else if (mention === "git-changes") { return `Working directory changes (see below for details)` + } else if (enableSvnContext && mention === "svn-changes") { + return `Working directory changes (see below for details)` } else if (/^[a-f0-9]{7,40}$/.test(mention)) { return `Git commit '${mention}' (see below for commit info)` + } else if (enableSvnContext && /^r\d+$/.test(mention)) { + return `SVN revision '${mention}' (see below for commit info)` } else if (mention === "terminal") { return `Terminal Output (see below for output)` } @@ -186,6 +192,29 @@ export async function parseMentions( } catch (error) { parsedText += `\n\n\nError fetching working state: ${error.message}\n` } + } else if (enableSvnContext && mention === "svn-changes") { + try { + const svnWorkingState = await getSvnWorkingState(cwd) + console.log("[DEBUG] SVN working state object:", JSON.stringify(svnWorkingState, null, 2)) + + // Format the SVN working state properly + let formattedState = "" + if (svnWorkingState.status) { + formattedState += `Status:\n${svnWorkingState.status}\n\n` + } + if (svnWorkingState.diff) { + formattedState += `Diff:\n${svnWorkingState.diff}` + } + if (!formattedState.trim()) { + formattedState = "No changes detected in working directory." + } + + console.log("[DEBUG] Formatted SVN state for AI:", formattedState) + parsedText += `\n\n\n${formattedState}\n` + } catch (error) { + console.error("[DEBUG] Error fetching SVN working state:", error) + parsedText += `\n\n\nError fetching SVN working state: ${error.message}\n` + } } else if (/^[a-f0-9]{7,40}$/.test(mention)) { try { const commitInfo = await getCommitInfo(mention, cwd) @@ -193,6 +222,13 @@ export async function parseMentions( } catch (error) { parsedText += `\n\n\nError fetching commit info: ${error.message}\n` } + } else if (enableSvnContext && /^r\d+$/.test(mention)) { + try { + const commitInfo = await getSvnCommitInfoForMentions(mention, cwd) + parsedText += `\n\n\n${commitInfo}\n` + } catch (error) { + parsedText += `\n\n\nError fetching commit info: ${error.message}\n` + } } else if (mention === "terminal") { try { const terminalOutput = await getLatestTerminalOutput() diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts index 245a25b3793..74cdd761087 100644 --- a/src/core/mentions/processUserContentMentions.ts +++ b/src/core/mentions/processUserContentMentions.ts @@ -13,6 +13,7 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles = true, + enableSvnContext = false, includeDiagnosticMessages = true, maxDiagnosticMessages = 50, maxReadFileLine, @@ -23,6 +24,7 @@ export async function processUserContentMentions({ fileContextTracker: FileContextTracker rooIgnoreController?: any showRooIgnoredFiles?: boolean + enableSvnContext?: boolean includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number maxReadFileLine?: number @@ -52,6 +54,7 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles, + enableSvnContext, includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, @@ -72,6 +75,7 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles, + enableSvnContext, includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, @@ -93,6 +97,7 @@ export async function processUserContentMentions({ fileContextTracker, rooIgnoreController, showRooIgnoredFiles, + enableSvnContext, includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index fe8fd0f68fe..e170126b9c2 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1228,6 +1228,7 @@ export class Task extends EventEmitter { const { showRooIgnoredFiles = true, + enableSvnContext = false, includeDiagnosticMessages = true, maxDiagnosticMessages = 50, maxReadFileLine = -1, @@ -1240,6 +1241,7 @@ export class Task extends EventEmitter { fileContextTracker: this.fileContextTracker, rooIgnoreController: this.rooIgnoreController, showRooIgnoredFiles, + enableSvnContext, includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 905e657b37e..eee3929373f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1439,6 +1439,8 @@ export class ClineProvider profileThresholds, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, + diagnosticsEnabled, + enableSvnContext, includeDiagnosticMessages, maxDiagnosticMessages, } = await this.getState() @@ -1561,6 +1563,8 @@ export class ClineProvider hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, + diagnosticsEnabled: diagnosticsEnabled ?? true, + enableSvnContext: enableSvnContext ?? false, includeDiagnosticMessages: includeDiagnosticMessages ?? true, maxDiagnosticMessages: maxDiagnosticMessages ?? 50, } @@ -1701,6 +1705,7 @@ export class ClineProvider browserToolEnabled: stateValues.browserToolEnabled ?? true, telemetrySetting: stateValues.telemetrySetting || "unset", showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, + enableSvnContext: stateValues.enableSvnContext ?? false, maxReadFileLine: stateValues.maxReadFileLine ?? -1, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 344b0988165..ab62cbcc3ee 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -542,6 +542,9 @@ describe("ClineProvider", () => { profileThresholds: {}, hasOpenedModeSelector: false, diagnosticsEnabled: true, + enableSvnContext: true, + includeDiagnosticMessages: true, + maxDiagnosticMessages: 50, } const message: ExtensionMessage = { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 82c780adf47..294b5a829ef 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -37,6 +37,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts" import { singleCompletionHandler } from "../../utils/single-completion-handler" import { searchCommits } from "../../utils/git" +import { searchSvnCommits } from "../../utils/svn" import { exportSettings, importSettingsWithFeedback } from "../config/importExport" import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" @@ -1257,6 +1258,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("showRooIgnoredFiles", message.bool ?? true) await provider.postStateToWebview() break + case "enableSvnContext": + await updateGlobalState("enableSvnContext", message.bool ?? true) + await provider.postStateToWebview() + break case "hasOpenedModeSelector": await updateGlobalState("hasOpenedModeSelector", message.bool ?? true) await provider.postStateToWebview() @@ -1410,6 +1415,24 @@ export const webviewMessageHandler = async ( } break } + case "searchSvnCommits": { + const cwd = provider.cwd + if (cwd) { + try { + const svnCommits = await searchSvnCommits(message.query || "", cwd) + await provider.postMessageToWebview({ + type: "svnCommitSearchResults", + svnCommits, + }) + } catch (error) { + provider.log( + `Error searching SVN commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage("Error searching SVN commits") + } + } + break + } case "searchFiles": { const workspacePath = getWorkspacePath() diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 64820beffb4..7ec822c6b5c 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -187,7 +187,10 @@ export class DiffViewProvider { } } - async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{ + async saveChanges( + diagnosticsEnabled: boolean = true, + writeDelayMs: number = DEFAULT_WRITE_DELAY_MS, + ): Promise<{ newProblemsMessage: string | undefined userEdits: string | undefined finalContent: string | undefined @@ -222,22 +225,22 @@ export class DiffViewProvider { // and can address them accordingly. If problems don't change immediately after // applying a fix, won't be notified, which is generally fine since the // initial fix is usually correct and it may just take time for linters to catch up. - + let newProblemsMessage = "" - + if (diagnosticsEnabled) { // Add configurable delay to allow linters time to process and clean up issues // like unused imports (especially important for Go and other languages) // Ensure delay is non-negative const safeDelayMs = Math.max(0, writeDelayMs) - + try { await delay(safeDelayMs) } catch (error) { // Log error but continue - delay failure shouldn't break the save operation console.warn(`Failed to apply write delay: ${error}`) } - + const postDiagnostics = vscode.languages.getDiagnostics() // Get diagnostic settings from state diff --git a/src/package.json b/src/package.json index c816837bb71..dcc5534c03f 100644 --- a/src/package.json +++ b/src/package.json @@ -174,6 +174,11 @@ "command": "roo-cline.acceptInput", "title": "%command.acceptInput.title%", "category": "%configuration.title%" + }, + { + "command": "roo-cline.debugSvn", + "title": "%command.debugSvn.title%", + "category": "%configuration.title%" } ], "menus": { diff --git a/src/package.nls.json b/src/package.nls.json index 1285a2367f3..fd2dbd052f8 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -25,6 +25,7 @@ "command.terminal.fixCommand.title": "Fix This Command", "command.terminal.explainCommand.title": "Explain This Command", "command.acceptInput.title": "Accept Input/Suggestion", + "command.debugSvn.title": "Debug SVN Integration", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled", "commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.", diff --git a/src/services/checkpoints/excludes.ts b/src/services/checkpoints/excludes.ts index 382e400f188..15e9ede492b 100644 --- a/src/services/checkpoints/excludes.ts +++ b/src/services/checkpoints/excludes.ts @@ -200,6 +200,7 @@ const getLfsPatterns = async (workspacePath: string) => { export const getExcludePatterns = async (workspacePath: string) => [ ".git/", + ".svn/", ...getBuildArtifactPatterns(), ...getMediaFilePatterns(), ...getCacheFilePatterns(), diff --git a/src/services/search/file-search.ts b/src/services/search/file-search.ts index a25dd4068f9..23bcbbc541f 100644 --- a/src/services/search/file-search.ts +++ b/src/services/search/file-search.ts @@ -98,6 +98,8 @@ export async function executeRipgrepForFiles( "-g", "!**/.git/**", "-g", + "!**/.svn/**", + "-g", "!**/out/**", "-g", "!**/dist/**", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index bdd32c4e36b..d18f54305b0 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -13,6 +13,7 @@ import type { } from "@roo-code/types" import { GitCommit } from "../utils/git" +import { SvnCommit } from "../utils/svn" import { McpServer } from "./mcp" import { Mode } from "./modes" @@ -61,6 +62,7 @@ export interface ExtensionMessage { | "mcpServers" | "enhancedPrompt" | "commitSearchResults" + | "svnCommitSearchResults" | "listApiConfig" | "routerModels" | "openAiModels" @@ -156,6 +158,7 @@ export interface ExtensionMessage { }> mcpServers?: McpServer[] commits?: GitCommit[] + svnCommits?: SvnCommit[] listApiConfig?: ProviderSettingsEntry[] mode?: Mode customMode?: ModeConfig @@ -272,6 +275,7 @@ export type ExtensionState = Pick< maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500) maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500) showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings + enableSvnContext: boolean // Whether to enable SVN context features (commits and changes) maxReadFileLine: number // Maximum number of lines to read from a file before truncating experiments: Experiments // Map of experiment IDs to their enabled state diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 795e2765222..4b7a26ccba8 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -130,6 +130,7 @@ export interface WebviewMessage { | "mcpEnabled" | "enableMcpServerCreation" | "searchCommits" + | "searchSvnCommits" | "alwaysApproveResubmit" | "requestDelaySeconds" | "setApiConfigPassword" @@ -157,6 +158,7 @@ export interface WebviewMessage { | "codebaseIndexEnabled" | "telemetrySetting" | "showRooIgnoredFiles" + | "enableSvnContext" | "testBrowserConnection" | "browserConnectionResult" | "remoteBrowserEnabled" diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts index 2edb99de6ad..8dd5281603d 100644 --- a/src/shared/context-mentions.ts +++ b/src/shared/context-mentions.ts @@ -9,7 +9,7 @@ Mention regex: - `/@`: - **@**: The mention must start with the '@' symbol. - - `((?:\/|\w+:\/\/)[^\s]+?|problems\b|git-changes\b)`: + - `((?:\/|\w+:\/\/)[^\s]+?|problems\b|git-changes\b|svn-changes\b)`: - **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns. - `(?:\/|\w+:\/\/)`: - **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing. @@ -46,6 +46,7 @@ Mention regex: - URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters). - The exact word 'problems'. - The exact word 'git-changes'. + - The exact word 'svn-changes'. - The exact word 'terminal'. - It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text. @@ -54,11 +55,11 @@ Mention regex: */ export const mentionRegex = - /(? ({ + mockExecAsync: vi.fn(), +})) + +// Mock modules before importing the functions +vi.mock("vscode", () => ({ + workspace: { + workspaceFolders: [ + { + uri: { + fsPath: "/test/workspace", + }, + }, + ], + }, + window: { + createOutputChannel: vi.fn(() => ({ + appendLine: vi.fn(), + show: vi.fn(), + })), + showErrorMessage: vi.fn(() => Promise.resolve("Show Output")), + showWarningMessage: vi.fn(() => Promise.resolve()), + showInformationMessage: vi.fn(() => Promise.resolve()), + }, + env: { + openExternal: vi.fn(), + }, + Uri: { + parse: vi.fn(), + }, +})) + +vi.mock("fs", () => ({ + promises: { + access: vi.fn(), + }, +})) + +vi.mock("child_process", () => ({ + exec: vi.fn(), +})) + +vi.mock("util", () => ({ + promisify: vi.fn(() => mockExecAsync), +})) + +// Import functions after mocking +import { + checkSvnInstalled, + checkSvnRepo, + getSvnRepositoryInfo, + extractSvnRepositoryName, + searchSvnCommits, + getSvnCommitInfoForMentions, + getWorkspaceSvnInfo, +} from "../svn" + +describe("SVN Utilities", () => { + let mockFsAccess: any + + beforeEach(() => { + vi.clearAllMocks() + + // Setup fs.access mock + mockFsAccess = vi.mocked(fs.access) + }) + + describe("checkSvnInstalled", () => { + it("should return true when SVN is installed", async () => { + mockExecAsync.mockResolvedValue({ stdout: "svn, version 1.14.0\n", stderr: "" }) + + const result = await checkSvnInstalled() + expect(result).toBe(true) + expect(mockExecAsync).toHaveBeenCalledWith("svn --version") + }) + + it("should return false when SVN is not installed", async () => { + mockExecAsync.mockRejectedValue(new Error("Command not found")) + + const result = await checkSvnInstalled() + expect(result).toBe(false) + }) + }) + + describe("checkSvnRepo", () => { + it("should return true for valid SVN repository", async () => { + mockFsAccess.mockResolvedValue(undefined) + + const result = await checkSvnRepo("/test/workspace") + expect(result).toBe(true) + // Use path.join to handle platform-specific path separators + expect(mockFsAccess).toHaveBeenCalledWith(path.join("/test/workspace", ".svn")) + }) + + it("should return false for non-SVN directory", async () => { + mockFsAccess.mockRejectedValue(new Error("ENOENT")) + + const result = await checkSvnRepo("/test/workspace") + expect(result).toBe(false) + }) + }) + + describe("getSvnRepositoryInfo", () => { + it("should return repository info for valid SVN workspace", async () => { + mockFsAccess.mockResolvedValue(undefined) + mockExecAsync.mockResolvedValue({ + stdout: `URL: https://svn.example.com/myproject/trunk +Working Copy Root Path: /test/workspace`, + stderr: "", + }) + + const result = await getSvnRepositoryInfo("/test/workspace") + expect(result.repositoryUrl).toBe("https://svn.example.com/myproject/trunk") + expect(result.repositoryName).toBe("myproject") + expect(result.workingCopyRoot).toBe("/test/workspace") + }) + + it("should return empty object for non-SVN directory", async () => { + mockFsAccess.mockRejectedValue(new Error("ENOENT")) + + const result = await getSvnRepositoryInfo("/test/workspace") + expect(result).toEqual({}) + }) + + it("should handle SVN command failure gracefully", async () => { + mockFsAccess.mockResolvedValue(undefined) + mockExecAsync.mockRejectedValue(new Error("SVN command failed")) + + const result = await getSvnRepositoryInfo("/test/workspace") + expect(result).toEqual({}) + }) + }) + + describe("extractSvnRepositoryName", () => { + it("should extract repository name from trunk URL", () => { + const result = extractSvnRepositoryName("https://svn.example.com/myproject/trunk") + expect(result).toBe("myproject") + }) + + it("should extract repository name from branches URL", () => { + const result = extractSvnRepositoryName("https://svn.example.com/myproject/branches/feature") + expect(result).toBe("myproject") + }) + + it("should extract repository name from tags URL", () => { + const result = extractSvnRepositoryName("https://svn.example.com/myproject/tags/v1.0") + expect(result).toBe("myproject") + }) + + it("should extract repository name from simple URL", () => { + const result = extractSvnRepositoryName("https://svn.example.com/myproject") + expect(result).toBe("myproject") + }) + + it("should handle invalid URLs gracefully", () => { + const result = extractSvnRepositoryName("") + expect(result).toBe("") + }) + }) + + describe("searchSvnCommits", () => { + it("should return commits matching search query", async () => { + // Mock checkSvnInstalled to return true + mockExecAsync.mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }).mockResolvedValueOnce({ + stdout: ` + + +john.doe +2023-01-15T10:30:00.000000Z +Test commit message + +`, + stderr: "", + }) + + // Mock checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + const result = await searchSvnCommits("test", "/test/workspace") + expect(result).toHaveLength(1) + expect(result[0].revision).toBe("123") + expect(result[0].author).toBe("john.doe") + expect(result[0].message).toBe("Test commit message") + }) + + it("should search for specific revision when query is in 'r123' format", async () => { + // Mock checkSvnInstalled to return true + mockExecAsync.mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }).mockResolvedValueOnce({ + stdout: ` + + +jane.smith +2023-02-15T14:30:00.000000Z +Specific revision commit + +`, + stderr: "", + }) + + // Mock checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + const result = await searchSvnCommits("r456", "/test/workspace") + expect(result).toHaveLength(1) + expect(result[0].revision).toBe("456") + expect(result[0].author).toBe("jane.smith") + expect(result[0].message).toBe("Specific revision commit") + }) + + it("should handle case-insensitive 'r' prefix in revision search", async () => { + // Mock checkSvnInstalled to return true + mockExecAsync.mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }).mockResolvedValueOnce({ + stdout: ` + + +test.user +2023-03-15T16:30:00.000000Z +Case insensitive test + +`, + stderr: "", + }) + + // Mock checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + const result = await searchSvnCommits("R789", "/test/workspace") + expect(result).toHaveLength(1) + expect(result[0].revision).toBe("789") + }) + + it("should NOT treat pure numbers as revision searches", async () => { + // Mock checkSvnInstalled to return true + mockExecAsync.mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }).mockResolvedValueOnce({ + stdout: ` + + +john.doe +2023-01-15T10:30:00.000000Z +Message containing 456 number + + +jane.smith +2023-02-15T14:30:00.000000Z +Another commit + +`, + stderr: "", + }) + + // Mock checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + // Search for pure number "456" should search in message content, not as specific revision + const result = await searchSvnCommits("456", "/test/workspace") + // Should find the commit with "456" in the message, NOT the commit with revision 456 + expect(result).toHaveLength(1) + expect(result[0].revision).toBe("123") + expect(result[0].message).toContain("456") + }) + + it("should return empty array when SVN is not available", async () => { + mockExecAsync.mockRejectedValue(new Error("Command not found")) + + const result = await searchSvnCommits("test", "/test/workspace") + expect(result).toEqual([]) + }) + }) + + describe("getSvnCommitInfoForMentions", () => { + it("should return commit info for valid revision", async () => { + // Mock checkSvnInstalled and checkSvnRepo + mockExecAsync.mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }).mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r123 | john.doe | 2023-01-15 10:30:00 +0000 (Sun, 15 Jan 2023) | 1 line +Changed paths: + M /test.txt + +Test commit message +------------------------------------------------------------------------`, + stderr: "", + }) + + mockFsAccess.mockResolvedValue(undefined) + + const result = await getSvnCommitInfoForMentions("123", "/test/workspace") + expect(result).toContain("r123") + expect(result).toContain("john.doe") + expect(result).toContain("Test commit message") + }) + + it("should parse changed files information correctly", async () => { + // Mock checkSvnInstalled and checkSvnRepo + mockFsAccess.mockResolvedValue(undefined) + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r456 | jane.smith | 2023-02-15 14:30:00 +0800 (Wed, 15 Feb 2023) | 2 lines +Changed paths: + A /src/new-file.ts + M /src/existing-file.ts + D /src/old-file.ts + +Added new feature +Fixed bug in existing functionality +------------------------------------------------------------------------`, + stderr: "", + }) + .mockResolvedValueOnce({ + stdout: `Index: src/new-file.ts +=================================================================== +--- src/new-file.ts (nonexistent) ++++ src/new-file.ts (revision 456) +@@ -0,0 +1,3 @@ ++export function newFunction() { ++ return 'Hello World'; ++}`, + stderr: "", + }) + + const result = await getSvnCommitInfoForMentions("456", "/test/workspace") + + // Should contain basic commit info + expect(result).toContain("r456 by jane.smith") + expect(result).toContain("Added new feature") + expect(result).toContain("Fixed bug in existing functionality") + + // Should contain changed files section + expect(result).toContain("Changed files:") + expect(result).toContain("A /src/new-file.ts") + expect(result).toContain("M /src/existing-file.ts") + expect(result).toContain("D /src/old-file.ts") + + // Should contain diff section + expect(result).toContain("Diff:") + expect(result).toContain("export function newFunction()") + }) + + it("should handle date parsing with Chinese day names gracefully", async () => { + // Mock checkSvnInstalled and checkSvnRepo + mockFsAccess.mockResolvedValue(undefined) + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r789 | chinese.user | 2023-03-15 16:30:00 +0800 (星期三, 15 三月 2023) | 1 line +Changed paths: + M /中文文件.txt + +测试中文提交信息 +------------------------------------------------------------------------`, + stderr: "", + }) + .mockResolvedValueOnce({ + stdout: `Index: 中文文件.txt +=================================================================== +--- 中文文件.txt (revision 788) ++++ 中文文件.txt (revision 789) +@@ -1 +1 @@ +-旧内容 ++新内容`, + stderr: "", + }) + + const result = await getSvnCommitInfoForMentions("789", "/test/workspace") + + // Should contain commit info with extracted date + expect(result).toContain("r789 by chinese.user") + expect(result).toContain("2023-03-15 16:30:00 +0800") + expect(result).toContain("测试中文提交信息") + + // Should handle Chinese file names and content + expect(result).toContain("M /中文文件.txt") + expect(result).toContain("新内容") + }) + + it("should handle commit message extraction from improved format", async () => { + // Mock checkSvnInstalled and checkSvnRepo + mockFsAccess.mockResolvedValue(undefined) + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r100 | developer | 2023-01-01 12:00:00 +0000 (Sun, 01 Jan 2023) | 3 lines +Changed paths: + M /src/component.ts + +This is a multi-line commit message +with detailed explanation +and some additional notes +------------------------------------------------------------------------`, + stderr: "", + }) + .mockResolvedValueOnce({ + stdout: "", + stderr: "", + }) + + const result = await getSvnCommitInfoForMentions("100", "/test/workspace") + + // Should extract multi-line commit message correctly + expect(result).toContain("This is a multi-line commit message") + expect(result).toContain("with detailed explanation") + expect(result).toContain("and some additional notes") + }) + + it("should handle revision with 'r' prefix input", async () => { + // Mock checkSvnInstalled and checkSvnRepo + mockFsAccess.mockResolvedValue(undefined) + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r555 | test.author | 2023-05-15 10:15:00 +0000 (Mon, 15 May 2023) | 1 line +Changed paths: + M /test-file.txt + +Test with r prefix input +------------------------------------------------------------------------`, + stderr: "", + }) + .mockResolvedValueOnce({ + stdout: "", + stderr: "", + }) + + const result = await getSvnCommitInfoForMentions("r555", "/test/workspace") + expect(result).toContain("r555 by test.author") + expect(result).toContain("Test with r prefix input") + }) + + it("should return error message for invalid revision", async () => { + // Mock checkSvnInstalled to fail + mockExecAsync.mockRejectedValue(new Error("Command not found")) + + const result = await getSvnCommitInfoForMentions("invalid", "/test/workspace") + expect(result).toBe("Error: SVN not available or not an SVN repository") + }) + + it("should handle diff output with UTF-8 encoding", async () => { + // Mock checkSvnInstalled and checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + // Create a buffer with UTF-8 encoded Chinese characters + const utf8Buffer = Buffer.from("测试文件内容", "utf8") + + // Mock the svn log and diff commands + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r123 | john.doe | 2023-01-15 10:30:00 +0000 (Sun, 15 Jan 2023) | 1 line +Changed paths: + M /test.txt + +Test commit message +------------------------------------------------------------------------`, + stderr: "", + }) + .mockResolvedValueOnce({ stdout: utf8Buffer, stderr: "" }) + + const result = await getSvnCommitInfoForMentions("123", "/test/workspace") + expect(result).toContain("r123") + expect(result).toContain("john.doe") + expect(result).toContain("Test commit message") + expect(result).toContain("测试文件内容") + }) + + it("should handle diff output with problematic encoding", async () => { + // Mock checkSvnInstalled and checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + // Create a buffer with problematic encoding (contains replacement characters) + const problematicBuffer = Buffer.from("Test file with � characters", "utf8") + + // Mock the svn log and diff commands + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r123 | john.doe | 2023-01-15 10:30:00 +0000 (Sun, 15 Jan 2023) | 1 line +Changed paths: + M /test.txt + +Test commit message +------------------------------------------------------------------------`, + stderr: "", + }) + .mockResolvedValueOnce({ stdout: problematicBuffer, stderr: "" }) + + const result = await getSvnCommitInfoForMentions("123", "/test/workspace") + expect(result).toContain("r123") + expect(result).toContain("john.doe") + expect(result).toContain("Test commit message") + // Should handle the problematic encoding gracefully + expect(result).toContain("Test file with") + }) + + it("should handle string output from svn diff command", async () => { + // Mock checkSvnInstalled and checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + // Mock the svn log command with string output instead of Buffer + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r123 | john.doe | 2023-01-15 10:30:00 +0000 (Sun, 15 Jan 2023) | 1 line +Changed paths: + M /test.txt + +Test commit message +------------------------------------------------------------------------`, + stderr: "", + }) + .mockResolvedValueOnce({ + stdout: "Index: test.txt\n===================================================================\n--- test.txt\t(revision 122)\n+++ test.txt\t(revision 123)\n@@ -1 +1 @@\n-old content\n+new content", + stderr: "", + }) + + const result = await getSvnCommitInfoForMentions("123", "/test/workspace") + expect(result).toContain("r123") + expect(result).toContain("john.doe") + expect(result).toContain("Test commit message") + expect(result).toContain("new content") + }) + + it("should handle diff command failure gracefully", async () => { + // Mock checkSvnInstalled and checkSvnRepo to return true + mockFsAccess.mockResolvedValue(undefined) + + // Mock the svn log command to succeed but diff to fail + mockExecAsync + .mockResolvedValueOnce({ stdout: "svn, version 1.14.0\n", stderr: "" }) + .mockResolvedValueOnce({ + stdout: `------------------------------------------------------------------------ +r123 | john.doe | 2023-01-15 10:30:00 +0000 (Sun, 15 Jan 2023) | 1 line +Changed paths: + M /test.txt + +Test commit message +------------------------------------------------------------------------`, + stderr: "", + }) + .mockRejectedValueOnce(new Error("svn: E160013: File not found")) + + const result = await getSvnCommitInfoForMentions("123", "/test/workspace") + expect(result).toContain("r123") + expect(result).toContain("john.doe") + expect(result).toContain("Test commit message") + // Should not contain diff section when diff fails + expect(result).not.toContain("Diff:") + }) + }) + + describe("getWorkspaceSvnInfo", () => { + it("should return SVN info for workspace", async () => { + // Mock fs.access to simulate .svn directory exists + mockFsAccess.mockResolvedValue(undefined) + + // Mock execAsync for svn info command + mockExecAsync.mockResolvedValue({ + stdout: `URL: https://svn.example.com/workspace/trunk +Working Copy Root Path: /test/workspace`, + stderr: "", + }) + + const result = await getWorkspaceSvnInfo() + expect(result.repositoryUrl).toBe("https://svn.example.com/workspace/trunk") + expect(result.repositoryName).toBe("workspace") + expect(result.workingCopyRoot).toBe("/test/workspace") + }) + + it("should return empty object when no workspace folders", async () => { + // Mock vscode workspace with no folders + const vscode = await import("vscode") + const mockWorkspace = vi.mocked(vscode.workspace) + + // Use Object.defineProperty to mock the readonly property + Object.defineProperty(mockWorkspace, "workspaceFolders", { + value: undefined, + writable: true, + configurable: true, + }) + + const result = await getWorkspaceSvnInfo() + expect(result).toEqual({}) + + // Restore the original value for other tests + Object.defineProperty(mockWorkspace, "workspaceFolders", { + value: [ + { + uri: { + fsPath: "/test/workspace", + }, + }, + ], + writable: true, + configurable: true, + }) + }) + }) +}) diff --git a/src/utils/svn.ts b/src/utils/svn.ts new file mode 100644 index 00000000000..eae99e0aa8b --- /dev/null +++ b/src/utils/svn.ts @@ -0,0 +1,901 @@ +import * as vscode from "vscode" +import * as path from "path" +import { promises as fs } from "fs" +import { exec } from "child_process" +import { promisify } from "util" +import { truncateOutput } from "../integrations/misc/extract-text" +import * as os from "os" + +/** + * Convert SVN command output buffer to string with cross-platform encoding support + * @param output Buffer or string output from SVN command + * @returns Properly decoded string + */ +function convertSvnOutput(output: Buffer | string): string { + if (typeof output === "string") { + return output + } + + // Try UTF-8 first (works for Linux/macOS and modern Windows) + try { + const utf8Result = output.toString("utf8") + // Check if the result contains replacement characters (indicates encoding issues) + if (!utf8Result.includes("\uFFFD")) { + return utf8Result + } + } catch (error) { + // UTF-8 decoding failed + } + + // On Windows, try common Chinese encodings if UTF-8 failed + if (os.platform() === "win32") { + try { + // Try GBK/GB2312 encoding (common on Chinese Windows systems) + // Note: Node.js doesn't support GBK directly, so we'll use latin1 as fallback + // and let the system handle the encoding + return output.toString("latin1") + } catch (error) { + // Fallback to latin1 if all else fails + } + } + + // Final fallback: use UTF-8 even if it has replacement characters + return output.toString("utf8") +} + +const execAsync = promisify(exec) +const SVN_OUTPUT_LINE_LIMIT = 500 + +// SVN Debug Logger +export class SvnLogger { + private static outputChannel: vscode.OutputChannel | null = null + + static getOutputChannel(): vscode.OutputChannel { + if (!this.outputChannel) { + this.outputChannel = vscode.window.createOutputChannel("Roo Code - SVN Debug") + } + return this.outputChannel + } + + static debug(message: string, ...args: any[]) { + const timestamp = new Date().toISOString() + const logMessage = `[${timestamp}] [DEBUG] ${message}` + + // Log to console + console.log(logMessage, ...args) + + // Log to VS Code output channel + const channel = this.getOutputChannel() + channel.appendLine(logMessage) + if (args.length > 0) { + channel.appendLine(` Args: ${JSON.stringify(args, null, 2)}`) + } + } + + static error(message: string, error?: any) { + const timestamp = new Date().toISOString() + const logMessage = `[${timestamp}] [ERROR] ${message}` + + // Log to console + console.error(logMessage, error) + + // Log to VS Code output channel + const channel = this.getOutputChannel() + channel.appendLine(logMessage) + if (error) { + channel.appendLine(` Error: ${error.toString()}`) + if (error.stack) { + channel.appendLine(` Stack: ${error.stack}`) + } + } + } + + static info(message: string, ...args: any[]) { + const timestamp = new Date().toISOString() + const logMessage = `[${timestamp}] [INFO] ${message}` + + // Log to console + console.log(logMessage, ...args) + + // Log to VS Code output channel + const channel = this.getOutputChannel() + channel.appendLine(logMessage) + if (args.length > 0) { + channel.appendLine(` Data: ${JSON.stringify(args, null, 2)}`) + } + } + + static showOutput() { + this.getOutputChannel().show() + } +} + +export interface SvnRepositoryInfo { + repositoryUrl?: string + repositoryName?: string + workingCopyRoot?: string +} + +export interface SvnCommit { + revision: string + author: string + date: string + message: string +} + +/** + * Extracts SVN repository information from the workspace's .svn directory + * @param workspaceRoot The root path of the workspace + * @returns SVN repository information or empty object if not an SVN repository + */ +export async function getSvnRepositoryInfo(workspaceRoot: string): Promise { + SvnLogger.debug("getSvnRepositoryInfo called", { workspaceRoot }) + + try { + const svnDir = path.join(workspaceRoot, ".svn") + SvnLogger.debug("Checking SVN directory", { svnDir }) + + // Check if .svn directory exists + try { + await fs.access(svnDir) + SvnLogger.debug("SVN directory found") + } catch (error) { + SvnLogger.debug("SVN directory not found - not an SVN repository", { + error: (error instanceof Error ? error : new Error(String(error))).toString(), + }) + return {} + } + + const svnInfo: SvnRepositoryInfo = {} + + // Try to get SVN info using svn info command + try { + SvnLogger.debug("Executing 'svn info' command", { cwd: workspaceRoot }) + const { stdout } = await execAsync("svn info", { cwd: workspaceRoot }) + SvnLogger.debug("SVN info command output", { stdout }) + + // Parse SVN info output + const urlMatch = stdout.match(/^URL:\s*(.+)$/m) + if (urlMatch && urlMatch[1]) { + const url = urlMatch[1].trim() + svnInfo.repositoryUrl = url + svnInfo.repositoryName = extractSvnRepositoryName(url) + SvnLogger.debug("Extracted repository info", { url, repositoryName: svnInfo.repositoryName }) + } else { + SvnLogger.debug("No URL found in SVN info output") + } + + const rootMatch = stdout.match(/^Working Copy Root Path:\s*(.+)$/m) + if (rootMatch && rootMatch[1]) { + svnInfo.workingCopyRoot = rootMatch[1].trim() + SvnLogger.debug("Found working copy root", { workingCopyRoot: svnInfo.workingCopyRoot }) + } else { + SvnLogger.debug("No working copy root found in SVN info output") + } + } catch (error) { + SvnLogger.error("SVN info command failed", error instanceof Error ? error : new Error(String(error))) + } + + SvnLogger.info("Final SVN repository info", svnInfo) + return svnInfo + } catch (error) { + SvnLogger.error("Error in getSvnRepositoryInfo", error instanceof Error ? error : new Error(String(error))) + return {} + } +} + +/** + * Extracts repository name from an SVN URL + * @param url The SVN URL + * @returns Repository name or undefined + */ +export function extractSvnRepositoryName(url: string): string { + try { + // Handle different SVN URL formats + const patterns = [ + // Standard SVN: https://svn.example.com/repo/trunk -> repo + /\/([^\/]+)\/(?:trunk|branches|tags)(?:\/.*)?$/, + // Simple repo: https://svn.example.com/repo -> repo + /\/([^\/]+)\/?$/, + ] + + for (const pattern of patterns) { + const match = url.match(pattern) + if (match && match[1]) { + return match[1] + } + } + + // Fallback: use the last part of the URL + const parts = url.split("/").filter(Boolean) + return parts[parts.length - 1] || "" + } catch { + return "" + } +} + +/** + * Gets SVN repository information for the current VSCode workspace + * @returns SVN repository information or empty object if not available + */ +export async function getWorkspaceSvnInfo(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + return {} + } + + // Use the first workspace folder + const workspaceRoot = workspaceFolders[0].uri.fsPath + return getSvnRepositoryInfo(workspaceRoot) +} + +/** + * Enhanced error handling with user-friendly messages + */ +class SvnErrorHandler { + /** + * Shows user-friendly error message with guidance + */ + static async showSvnNotInstalledError(): Promise { + const action = await vscode.window.showErrorMessage( + "SVN is not installed or not available in PATH", + { + modal: false, + detail: "SVN command line tool is required for SVN operations. Please install SVN and ensure it's available in your system PATH.", + }, + "Install Guide", + "Check PATH", + ) + + if (action === "Install Guide") { + vscode.env.openExternal(vscode.Uri.parse("https://subversion.apache.org/packages.html")) + } else if (action === "Check PATH") { + vscode.window.showInformationMessage("To check if SVN is in PATH, open terminal and run: svn --version", { + modal: false, + }) + } + } + + /** + * Shows user-friendly message when not in SVN repository + */ + static async showNotSvnRepositoryError(workspacePath: string): Promise { + const action = await vscode.window.showWarningMessage( + "Current workspace is not an SVN repository", + { + modal: false, + detail: `The directory "${workspacePath}" does not contain a .svn folder. SVN operations are only available in SVN working copies.`, + }, + "Learn More", + "Initialize SVN", + ) + + if (action === "Learn More") { + vscode.env.openExternal(vscode.Uri.parse("https://svnbook.red-bean.com/en/1.7/svn.basic.html")) + } else if (action === "Initialize SVN") { + vscode.window.showInformationMessage( + "To initialize SVN repository, use: svn checkout or svn import", + { modal: false }, + ) + } + } + + /** + * Shows network/connection error with guidance + */ + static async showNetworkError(error: Error): Promise { + const action = await vscode.window.showErrorMessage( + "SVN network operation failed", + { + modal: false, + detail: `Network error: ${error.message}. This could be due to network connectivity issues, server problems, or authentication failures.`, + }, + "Retry", + "Check Connection", + ) + + if (action === "Check Connection") { + vscode.window.showInformationMessage( + "Please check:\n• Network connectivity\n• SVN server status\n• Authentication credentials\n• Firewall settings", + { modal: false }, + ) + } + } + + /** + * Shows repository corruption error with guidance + */ + static async showRepositoryCorruptionError(workspacePath: string): Promise { + const action = await vscode.window.showErrorMessage( + "SVN repository appears to be corrupted", + { + modal: false, + detail: `The .svn directory in "${workspacePath}" may be corrupted or incomplete. This can happen due to interrupted operations or file system issues.`, + }, + "Cleanup", + "Get Help", + ) + + if (action === "Cleanup") { + vscode.window.showInformationMessage( + "Try running: svn cleanup\nIf that fails, you may need to re-checkout the repository.", + { modal: false }, + ) + } else if (action === "Get Help") { + vscode.env.openExternal(vscode.Uri.parse("https://svnbook.red-bean.com/en/1.7/svn.ref.svn.c.cleanup.html")) + } + } + + /** + * Shows permission error with guidance + */ + static async showPermissionError(error: Error): Promise { + const action = await vscode.window.showErrorMessage( + "SVN operation failed due to permission issues", + { + modal: false, + detail: `Permission denied: ${error.message}. This could be due to file system permissions or SVN server access rights.`, + }, + "Check Permissions", + "Get Help", + ) + + if (action === "Check Permissions") { + vscode.window.showInformationMessage( + "Please check:\n• File/folder permissions\n• SVN server access rights\n• User authentication\n• Write permissions in working directory", + { modal: false }, + ) + } + } + + /** + * Shows feature unavailable message with graceful degradation + */ + static showFeatureUnavailable(feature: string, reason: string): void { + vscode.window.showWarningMessage(`SVN ${feature} is currently unavailable`, { + modal: false, + detail: `${reason}. Some SVN features may be limited until this is resolved.`, + }) + } + + /** + * Determines error type and shows appropriate message + */ + static async handleSvnError(error: Error, context: string, workspacePath?: string): Promise { + const errorMessage = error.message.toLowerCase() + + // Network/connection errors + if ( + errorMessage.includes("network") || + errorMessage.includes("connection") || + errorMessage.includes("timeout") || + errorMessage.includes("unreachable") || + errorMessage.includes("resolve") + ) { + await this.showNetworkError(error) + return + } + + // Permission errors + if ( + errorMessage.includes("permission") || + errorMessage.includes("access denied") || + errorMessage.includes("forbidden") || + errorMessage.includes("eacces") + ) { + await this.showPermissionError(error) + return + } + + // Repository corruption + if ( + errorMessage.includes("corrupt") || + errorMessage.includes("invalid") || + errorMessage.includes("malformed") || + errorMessage.includes("cleanup") + ) { + if (workspacePath) { + await this.showRepositoryCorruptionError(workspacePath) + return + } + } + + // SVN not found + if ( + errorMessage.includes("not found") || + errorMessage.includes("command not found") || + errorMessage.includes("is not recognized") + ) { + await this.showSvnNotInstalledError() + return + } + + // Generic error with context + vscode.window + .showErrorMessage( + `SVN ${context} failed`, + { + modal: false, + detail: `Error: ${error.message}\n\nPlease check the SVN Debug output channel for more details.`, + }, + "Show Output", + ) + .then((action) => { + if (action === "Show Output") { + SvnLogger.showOutput() + } + }) + } +} + +/** + * Checks if SVN is installed and available + * @returns True if SVN is available, false otherwise + */ +export async function checkSvnInstalled(): Promise { + SvnLogger.debug("checkSvnInstalled called") + + try { + const { stdout } = await execAsync("svn --version") + SvnLogger.debug("SVN is installed", { version: stdout.split("\n")[0] }) + return true + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.error("SVN is not installed or not available", svnError) + + // Show user-friendly error message + await SvnErrorHandler.showSvnNotInstalledError() + return false + } +} + +/** + * Checks if the given directory is an SVN repository + * @param cwd The directory to check + * @returns True if it's an SVN repository, false otherwise + */ +export async function checkSvnRepo(cwd: string): Promise { + SvnLogger.debug("checkSvnRepo called", { cwd }) + + try { + const svnDir = path.join(cwd, ".svn") + await fs.access(svnDir) + SvnLogger.debug("SVN repository detected", { svnDir }) + return true + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.debug("Not an SVN repository", { + cwd, + error: svnError.toString(), + }) + + // Show user-friendly message for workspace operations + if (cwd === vscode.workspace.workspaceFolders?.[0]?.uri.fsPath) { + await SvnErrorHandler.showNotSvnRepositoryError(cwd) + } + return false + } +} + +/** + * Searches for SVN commits by revision number or message content + * @param query The search query (revision number or message text) + * @param cwd The working directory + * @returns Array of matching SVN commits + */ +export async function searchSvnCommits(query: string, cwd: string): Promise { + SvnLogger.debug("searchSvnCommits called", { query, cwd }) + + try { + // Check if SVN is available + const svnInstalled = await checkSvnInstalled() + if (!svnInstalled) { + SvnErrorHandler.showFeatureUnavailable("search", "SVN is not installed") + return [] + } + + const isSvnRepo = await checkSvnRepo(cwd) + if (!isSvnRepo) { + SvnErrorHandler.showFeatureUnavailable("search", "Not an SVN repository") + return [] + } + + SvnLogger.debug("SVN availability check", { svnInstalled, isSvnRepo }) + + const commits: SvnCommit[] = [] + + // If query looks like a revision number with 'r' prefix, search for that specific revision + // Only support "r123" format, not pure numbers + const revisionMatch = query.match(/^r(\d+)$/i) + if (revisionMatch) { + const revisionNumber = revisionMatch[1] + SvnLogger.debug("Query looks like revision number, searching for specific revision", { + originalQuery: query, + revision: revisionNumber, + }) + try { + const command = `svn log -r ${revisionNumber} --xml` + SvnLogger.debug("Executing revision search command", { command, cwd }) + + const { stdout } = await execAsync(command, { cwd }) + SvnLogger.debug("Revision search output", { stdout }) + + const revisionCommits = parseSvnLogXml(stdout) + commits.push(...revisionCommits) + SvnLogger.debug("Found commits for revision", { + count: revisionCommits.length, + commits: revisionCommits, + }) + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.debug("Revision search failed, continuing with general search", { + error: svnError.toString(), + }) + await SvnErrorHandler.handleSvnError(svnError, "revision search", cwd) + } + } + + // Search in commit messages (get recent commits and filter) + try { + const command = "svn log -l 100 --xml" + SvnLogger.debug("Executing message search command", { command, cwd }) + + const { stdout } = await execAsync(command, { cwd }) + SvnLogger.debug("Message search output length", { outputLength: stdout.length }) + + const allCommits = parseSvnLogXml(stdout) + SvnLogger.debug("Parsed all commits", { count: allCommits.length }) + + // Filter commits by message content or revision match + const messageMatches = allCommits.filter((commit) => { + // Check message content match + const messageMatch = commit.message.toLowerCase().includes(query.toLowerCase()) + + // Check revision match (only handle "r123" format, not pure numbers) + let revisionMatch = false + const revisionMatchResult = query.match(/^r(\d+)$/i) + if (revisionMatchResult) { + const queryRevision = revisionMatchResult[1] + revisionMatch = commit.revision === queryRevision + } + + return messageMatch || revisionMatch + }) + SvnLogger.debug("Filtered commits by message", { matchCount: messageMatches.length, query }) + + // Add unique commits (avoid duplicates from revision search) + messageMatches.forEach((commit) => { + if (!commits.some((c) => c.revision === commit.revision)) { + commits.push(commit) + } + }) + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.error("Message search failed", svnError) + await SvnErrorHandler.handleSvnError(svnError, "commit search", cwd) + } + + const finalCommits = commits.slice(0, 20) // Limit results + SvnLogger.info("Search completed", { + query, + totalFound: commits.length, + returned: finalCommits.length, + commits: finalCommits, + }) + return finalCommits + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.error("Error searching SVN commits", svnError) + await SvnErrorHandler.handleSvnError(svnError, "commit search", cwd) + return [] + } +} + +/** + * Parses SVN log XML output into commit objects + * @param xmlOutput The XML output from svn log --xml + * @returns Array of SVN commits + */ +function parseSvnLogXml(xmlOutput: string): SvnCommit[] { + const commits: SvnCommit[] = [] + + try { + // Simple XML parsing for SVN log entries + const entryRegex = /]*revision="(\d+)"[^>]*>([\s\S]*?)<\/logentry>/g + const authorRegex = /([\s\S]*?)<\/author>/ + const dateRegex = /([\s\S]*?)<\/date>/ + const msgRegex = /([\s\S]*?)<\/msg>/ + + let match + while ((match = entryRegex.exec(xmlOutput)) !== null) { + const [, revision, entryContent] = match + + const authorMatch = authorRegex.exec(entryContent) + const dateMatch = dateRegex.exec(entryContent) + const msgMatch = msgRegex.exec(entryContent) + + commits.push({ + revision, + author: authorMatch ? authorMatch[1].trim() : "Unknown", + date: dateMatch ? new Date(dateMatch[1]).toISOString() : "", + message: msgMatch ? msgMatch[1].trim() : "", + }) + } + } catch (error) { + console.error("Error parsing SVN log XML:", error) + } + + return commits +} + +/** + * Gets detailed information about a specific SVN revision + * @param revision The revision number + * @param cwd The working directory + * @returns Detailed commit information including diff + */ +export async function getSvnCommitInfo( + revision: string, + cwd: string, +): Promise<{ + commit: SvnCommit | null + diff: string + stats: string +}> { + try { + const svnInstalled = await checkSvnInstalled() + const isSvnRepo = await checkSvnRepo(cwd) + + if (!svnInstalled || !isSvnRepo) { + if (!svnInstalled) { + SvnErrorHandler.showFeatureUnavailable("commit info", "SVN is not installed") + } else { + SvnErrorHandler.showFeatureUnavailable("commit info", "Not an SVN repository") + } + return { commit: null, diff: "", stats: "" } + } + + // Get commit info + const { stdout: logOutput } = await execAsync(`svn log -r ${revision} --xml`, { cwd }) + const commits = parseSvnLogXml(logOutput) + const commit = commits[0] || null + + // Get diff + let diff = "" + try { + const { stdout: diffOutput } = await execAsync(`svn diff -c ${revision}`, { cwd }) + diff = truncateOutput(diffOutput, SVN_OUTPUT_LINE_LIMIT) + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.debug("Diff not available for revision", { revision, error: svnError.message }) + } + + // Get stats (changed files) + let stats = "" + try { + const { stdout: changedOutput } = await execAsync(`svn log -r ${revision} -v`, { cwd }) + const changedMatch = changedOutput.match(/Changed paths:([\s\S]*?)(?=\n\n|\n-{72}|\n$)/) + if (changedMatch) { + stats = changedMatch[1].trim() + } + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.debug("Stats not available for revision", { revision, error: svnError.message }) + } + + return { commit, diff, stats } + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + SvnLogger.error("Error getting SVN commit info", svnError) + await SvnErrorHandler.handleSvnError(svnError, "commit info", cwd) + return { commit: null, diff: "", stats: "" } + } +} + +/** + * Gets the current working state of the SVN repository + * @param cwd The working directory + * @returns Object containing status and diff information + */ +export async function getSvnWorkingState(cwd: string): Promise<{ + status: string + diff: string +}> { + try { + console.log("[DEBUG] getSvnWorkingState called with cwd:", cwd) + + const svnInstalled = await checkSvnInstalled() + const isSvnRepo = await checkSvnRepo(cwd) + + if (!svnInstalled || !isSvnRepo) { + console.log("[DEBUG] SVN not installed or not a repository") + if (!svnInstalled) { + SvnErrorHandler.showFeatureUnavailable("working state", "SVN is not installed") + } else { + SvnErrorHandler.showFeatureUnavailable("working state", "Not an SVN repository") + } + return { status: "", diff: "" } + } + + let status = "" + try { + console.log("[DEBUG] Executing 'svn status' command") + const { stdout } = await execAsync("svn status", { cwd }) + console.log("[DEBUG] SVN status raw output:", JSON.stringify(stdout)) + status = truncateOutput(stdout, SVN_OUTPUT_LINE_LIMIT) + console.log("[DEBUG] SVN status after truncation:", JSON.stringify(status)) + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + console.log("[DEBUG] SVN status command failed:", svnError) + await SvnErrorHandler.handleSvnError(svnError, "status check", cwd) + } + + let diff = "" + try { + console.log("[DEBUG] Executing 'svn diff' command") + const { stdout } = await execAsync("svn diff", { cwd }) + console.log("[DEBUG] SVN diff raw output length:", stdout.length) + console.log("[DEBUG] SVN diff raw output preview:", JSON.stringify(stdout.substring(0, 500))) + diff = truncateOutput(stdout, SVN_OUTPUT_LINE_LIMIT) + console.log("[DEBUG] SVN diff after truncation length:", diff.length) + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + console.log("[DEBUG] SVN diff command failed:", svnError) + await SvnErrorHandler.handleSvnError(svnError, "diff check", cwd) + } + + // If no diff but there are changes, show untracked files info + if (!diff.trim() && status.trim()) { + const lines = status.split("\n").filter((line) => line.trim()) + if (lines.length > 3) { + const visibleLines = lines.slice(0, 3) + diff = visibleLines.join("\n") + `\n... and ${lines.length - 3} more untracked files` + } else { + diff = status + } + } + + const result = { status, diff } + console.log("[DEBUG] getSvnWorkingState returning:", JSON.stringify(result)) + return result + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + console.error("[DEBUG] Error in getSvnWorkingState:", svnError) + await SvnErrorHandler.handleSvnError(svnError, "working state", cwd) + return { status: "", diff: "" } + } +} + +/** + * Gets SVN commit information for mentions with enhanced error handling + * @param revision The revision number (with or without 'r' prefix) + * @param cwd The working directory + * @returns Formatted commit information or error message + */ +export async function getSvnCommitInfoForMentions(revision: string, cwd: string): Promise { + try { + console.log("[DEBUG] getSvnCommitInfoForMentions called with revision:", revision, "cwd:", cwd) + + const svnInstalled = await checkSvnInstalled() + const isSvnRepo = await checkSvnRepo(cwd) + + if (!svnInstalled || !isSvnRepo) { + console.log("[DEBUG] SVN not installed or not a repository") + return "Error: SVN not available or not an SVN repository" + } + + const cleanRevision = revision.replace(/^r/i, "") + if (!/^\d+$/.test(cleanRevision)) { + throw new Error("Invalid revision number format") + } + console.log("[DEBUG] Clean revision:", cleanRevision) + + // Use UTF-8 encoding to handle Chinese characters properly + const { stdout } = await execAsync(`svn log -r ${cleanRevision} -v`, { + cwd, + encoding: "utf8", + }) + console.log("[DEBUG] SVN log output:", stdout) + + // Parse the log output more carefully + const lines = stdout.split("\n") + + // Find the header line (contains revision info) + const headerLineIndex = lines.findIndex((line) => line.includes(`r${cleanRevision} |`)) + if (headerLineIndex === -1) { + return `Revision ${revision} not found` + } + + const headerLine = lines[headerLineIndex] + const parts = headerLine.split("|").map((part) => part.trim()) + + if (parts.length < 3) { + return `Revision ${revision}: Unable to parse commit information` + } + + const [rev, author, dateInfo] = parts + // Extract just the date part, ignore the Chinese day name to avoid encoding issues + const dateMatch = dateInfo.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4})/) + const date = dateMatch ? dateMatch[1] : dateInfo.split("(")[0].trim() + + // Find the commit message (after "Changed paths:" section) + let message = "No message" + let changedPaths = "" + + // Look for "Changed paths:" section + const changedPathsIndex = lines.findIndex((line) => line.includes("Changed paths:")) + if (changedPathsIndex !== -1) { + // Extract changed paths + const pathLines = [] + for (let i = changedPathsIndex + 1; i < lines.length; i++) { + const line = lines[i] + if (line.trim() === "" || line.startsWith("---")) { + break + } + if (line.trim().match(/^[AMDRC]\s+\//)) { + pathLines.push(line.trim()) + } + } + changedPaths = pathLines.join("\n") + + // Find message after the changed paths section + for (let i = changedPathsIndex + 1; i < lines.length; i++) { + const line = lines[i] + if (line.trim() === "" && i + 1 < lines.length) { + // Check if next line is not a separator and not empty + const nextLine = lines[i + 1] + if (nextLine && !nextLine.startsWith("---") && nextLine.trim() !== "") { + // Found the message + const messageLines = [] + for (let j = i + 1; j < lines.length; j++) { + const msgLine = lines[j] + if (msgLine.startsWith("---")) { + break + } + if (msgLine.trim() !== "") { + messageLines.push(msgLine) + } + } + if (messageLines.length > 0) { + message = messageLines.join("\n").trim() + } + break + } + } + } + } + + // Get diff information with cross-platform encoding compatibility + let diff = "" + try { + console.log("[DEBUG] Getting diff for revision:", cleanRevision) + const { stdout: rawDiffOutput } = await execAsync(`svn diff -c ${cleanRevision}`, { + cwd, + }) + const diffOutput = convertSvnOutput(rawDiffOutput) + diff = truncateOutput(diffOutput, SVN_OUTPUT_LINE_LIMIT) + console.log("[DEBUG] Diff length:", diff.length) + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + console.log("[DEBUG] Diff not available for revision:", svnError.message) + } + + // Format the result with changed files and diff + let result = `${rev} by ${author} on ${date}\n${message}` + + if (changedPaths) { + result += `\n\nChanged files:\n${changedPaths}` + } + + if (diff && diff.trim()) { + result += `\n\nDiff:\n${diff}` + } + + return result + } catch (error) { + const svnError = error instanceof Error ? error : new Error(String(error)) + console.error("[DEBUG] Error getting SVN commit info for mentions:", svnError) + await SvnErrorHandler.handleSvnError(svnError, "commit info for mentions", cwd) + return `Error retrieving revision ${revision}: ${svnError.message}` + } +} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6c541353eb2..fecfe51793e 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -86,6 +86,7 @@ const ChatTextArea = forwardRef( togglePinnedApiConfig, taskHistory, clineMessages, + enableSvnContext, } = useExtensionState() // Find the ID and display text for the currently selected API configuration @@ -98,6 +99,7 @@ const ChatTextArea = forwardRef( }, [listApiConfigMeta, currentApiConfigName]) const [gitCommits, setGitCommits] = useState([]) + const [svnCommits, setSvnCommits] = useState([]) const [showDropdown, setShowDropdown] = useState(false) const [fileSearchResults, setFileSearchResults] = useState([]) const [searchLoading, setSearchLoading] = useState(false) @@ -153,6 +155,16 @@ const ChatTextArea = forwardRef( })) setGitCommits(commits) + } else if (message.type === "svnCommitSearchResults") { + const commits = message.svnCommits.map((commit: any) => ({ + type: ContextMenuOptionType.Svn, + value: `r${commit.revision}`, + label: commit.message, + description: `r${commit.revision} by ${commit.author} on ${commit.date}`, + icon: "$(git-commit)", + })) + + setSvnCommits(commits) } else if (message.type === "fileSearchResults") { setSearchLoading(false) if (message.requestId === searchRequestId) { @@ -201,6 +213,17 @@ const ChatTextArea = forwardRef( } }, [selectedType, searchQuery]) + // Fetch SVN commits when SVN is selected or when typing a revision number with 'r' prefix. + useEffect(() => { + if (selectedType === ContextMenuOptionType.Svn || /^r\d+$/i.test(searchQuery)) { + const message: WebviewMessage = { + type: "searchSvnCommits", + query: searchQuery || "", + } as const + vscode.postMessage(message) + } + }, [selectedType, searchQuery]) + const handleEnhancePrompt = useCallback(() => { if (sendingDisabled) { return @@ -223,6 +246,7 @@ const ChatTextArea = forwardRef( { type: ContextMenuOptionType.Problems, value: "problems" }, { type: ContextMenuOptionType.Terminal, value: "terminal" }, ...gitCommits, + ...svnCommits, ...openedTabs .filter((tab) => tab.path) .map((tab) => ({ @@ -237,7 +261,7 @@ const ChatTextArea = forwardRef( value: path, })), ] - }, [filePaths, gitCommits, openedTabs]) + }, [filePaths, gitCommits, svnCommits, openedTabs]) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -276,7 +300,8 @@ const ChatTextArea = forwardRef( if ( type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder || - type === ContextMenuOptionType.Git + type === ContextMenuOptionType.Git || + type === ContextMenuOptionType.Svn ) { if (!value) { setSelectedType(type) @@ -302,6 +327,8 @@ const ChatTextArea = forwardRef( insertValue = "terminal" } else if (type === ContextMenuOptionType.Git) { insertValue = value || "" + } else if (type === ContextMenuOptionType.Svn) { + insertValue = value || "" } const { newValue, mentionIndex } = insertMention( @@ -348,6 +375,7 @@ const ChatTextArea = forwardRef( queryItems, fileSearchResults, allModes, + enableSvnContext, ) const optionsLength = options.length @@ -385,6 +413,7 @@ const ChatTextArea = forwardRef( queryItems, fileSearchResults, allModes, + enableSvnContext, )[selectedMenuIndex] if ( selectedOption && @@ -475,6 +504,7 @@ const ChatTextArea = forwardRef( fileSearchResults, handleHistoryNavigation, resetHistoryNavigation, + enableSvnContext, ], ) @@ -1245,6 +1275,7 @@ const ChatTextArea = forwardRef( modes={allModes} loading={searchLoading} dynamicSearchResults={fileSearchResults} + enableSvnContext={enableSvnContext} /> )} diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 1672c35ee3d..f44111bf469 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -23,6 +23,7 @@ interface ContextMenuProps { modes?: ModeConfig[] loading?: boolean dynamicSearchResults?: SearchResult[] + enableSvnContext?: boolean } const ContextMenu: React.FC = ({ @@ -36,13 +37,22 @@ const ContextMenu: React.FC = ({ queryItems, modes, dynamicSearchResults = [], + enableSvnContext = false, }) => { const [materialIconsBaseUri, setMaterialIconsBaseUri] = useState("") const menuRef = useRef(null) const filteredOptions = useMemo(() => { - return getContextMenuOptions(searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes) - }, [searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes]) + return getContextMenuOptions( + searchQuery, + inputValue, + selectedType, + queryItems, + dynamicSearchResults, + modes, + enableSvnContext, + ) + }, [searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes, enableSvnContext]) useEffect(() => { if (menuRef.current) { @@ -116,6 +126,45 @@ const ContextMenu: React.FC = ({ } else { return Git Commits } + case ContextMenuOptionType.Svn: + if (option.value) { + return ( +
+ + {option.label} + + + {option.description} + +
+ ) + } else { + return SVN Commits + } case ContextMenuOptionType.File: case ContextMenuOptionType.OpenedFile: case ContextMenuOptionType.Folder: @@ -177,6 +226,8 @@ const ContextMenu: React.FC = ({ return "link" case ContextMenuOptionType.Git: return "git-commit" + case ContextMenuOptionType.Svn: + return "git-commit" case ContextMenuOptionType.NoResults: return "info" default: @@ -282,7 +333,8 @@ const ContextMenu: React.FC = ({ {(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder || - option.type === ContextMenuOptionType.Git) && + option.type === ContextMenuOptionType.Git || + option.type === ContextMenuOptionType.Svn) && !option.value && ( & { maxOpenTabsContext: number maxWorkspaceFiles: number showRooIgnoredFiles?: boolean + enableSvnContext?: boolean maxReadFileLine?: number maxConcurrentFileReads?: number profileThresholds?: Record @@ -31,6 +32,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "maxOpenTabsContext" | "maxWorkspaceFiles" | "showRooIgnoredFiles" + | "enableSvnContext" | "maxReadFileLine" | "maxConcurrentFileReads" | "profileThresholds" @@ -47,6 +49,7 @@ export const ContextManagementSettings = ({ maxOpenTabsContext, maxWorkspaceFiles, showRooIgnoredFiles, + enableSvnContext, setCachedStateField, maxReadFileLine, maxConcurrentFileReads, @@ -170,6 +173,20 @@ export const ContextManagementSettings = ({ +
+ setCachedStateField("enableSvnContext", e.target.checked)} + data-testid="enable-svn-context-checkbox"> + + +
+ {t("settings:contextManagement.svnContext.description")} +
+
+
{t("settings:contextManagement.maxReadFile.label")} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index a416afd48de..985b3028fdd 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -177,6 +177,7 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowFollowupQuestions, alwaysAllowUpdateTodoList, followupAutoApproveTimeoutMs, + enableSvnContext, includeDiagnosticMessages, maxDiagnosticMessages, } = cachedState @@ -320,6 +321,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext }) vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 }) vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) + vscode.postMessage({ type: "enableSvnContext", bool: enableSvnContext }) vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 }) vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 }) vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages }) @@ -666,6 +668,7 @@ const SettingsView = forwardRef(({ onDone, t maxOpenTabsContext={maxOpenTabsContext} maxWorkspaceFiles={maxWorkspaceFiles ?? 200} showRooIgnoredFiles={showRooIgnoredFiles} + enableSvnContext={enableSvnContext} maxReadFileLine={maxReadFileLine} maxConcurrentFileReads={maxConcurrentFileReads} profileThresholds={profileThresholds} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ff1ce31c53c..6a7402a99ae 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -65,6 +65,7 @@ export interface ExtensionStateContextType extends ExtensionState { setAlwaysAllowSubtasks: (value: boolean) => void setBrowserToolEnabled: (value: boolean) => void setShowRooIgnoredFiles: (value: boolean) => void + setEnableSvnContext: (value: boolean) => void setShowAnnouncement: (value: boolean) => void setAllowedCommands: (value: string[]) => void setDeniedCommands: (value: string[]) => void @@ -206,6 +207,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode browserToolEnabled: true, telemetrySetting: "unset", showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior). + enableSvnContext: false, // Default to disable SVN context features (commits and changes). renderContext: "sidebar", maxReadFileLine: -1, // Default max read file line limit pinnedApiConfigs: {}, // Empty object for pinned API configs @@ -445,6 +447,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setBrowserToolEnabled: (value) => setState((prevState) => ({ ...prevState, browserToolEnabled: value })), setTelemetrySetting: (value) => setState((prevState) => ({ ...prevState, telemetrySetting: value })), setShowRooIgnoredFiles: (value) => setState((prevState) => ({ ...prevState, showRooIgnoredFiles: value })), + setEnableSvnContext: (value) => setState((prevState) => ({ ...prevState, enableSvnContext: value })), setRemoteBrowserEnabled: (value) => setState((prevState) => ({ ...prevState, remoteBrowserEnabled: value })), setAwsUsePromptCache: (value) => setState((prevState) => ({ ...prevState, awsUsePromptCache: value })), setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 1e5867d3fc3..6370e4f4a1f 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -209,6 +209,7 @@ describe("mergeExtensionState", () => { sharingEnabled: false, profileThresholds: {}, hasOpenedModeSelector: false, // Add the new required property + enableSvnContext: false, // Add the SVN context property } const prevState: ExtensionState = { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 828790cbb43..27e743fd4fd 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -492,6 +492,10 @@ "label": "Mostrar fitxers .rooignore en llistes i cerques", "description": "Quan està habilitat, els fitxers que coincideixen amb els patrons a .rooignore es mostraran en llistes amb un símbol de cadenat. Quan està deshabilitat, aquests fitxers s'ocultaran completament de les llistes de fitxers i cerques." }, + "svnContext": { + "label": "Habilitar funcions de context SVN", + "description": "Quan està habilitat, podeu utilitzar @svn, @svn-changes i @r123 al xat per referenciar commits SVN i canvis del directori de treball. Quan està deshabilitat, aquestes funcions de menció de context relacionades amb SVN no estaran disponibles." + }, "maxReadFile": { "label": "Llindar d'auto-truncament de lectura de fitxers", "description": "Roo llegeix aquest nombre de línies quan el model omet els valors d'inici/final. Si aquest nombre és menor que el total del fitxer, Roo genera un índex de números de línia de les definicions de codi. Casos especials: -1 indica a Roo que llegeixi tot el fitxer (sense indexació), i 0 indica que no llegeixi cap línia i proporcioni només índexs de línia per a un context mínim. Valors més baixos minimitzen l'ús inicial de context, permetent lectures posteriors de rangs de línies precisos. Les sol·licituds amb inici/final explícits no estan limitades per aquesta configuració.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 83662adb489..8eaa14c51fb 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -492,6 +492,10 @@ "label": ".rooignore-Dateien in Listen und Suchen anzeigen", "description": "Wenn aktiviert, werden Dateien, die mit Mustern in .rooignore übereinstimmen, in Listen mit einem Schlosssymbol angezeigt. Wenn deaktiviert, werden diese Dateien vollständig aus Dateilisten und Suchen ausgeblendet." }, + "svnContext": { + "label": "SVN-Kontextfunktionen aktivieren", + "description": "Wenn aktiviert, können Sie @svn, @svn-changes und @r123 im Chat verwenden, um auf SVN-Commits und Arbeitsverzeichnisänderungen zu verweisen. Wenn deaktiviert, sind diese SVN-bezogenen Kontexterwähnungsfunktionen nicht verfügbar." + }, "maxConcurrentFileReads": { "label": "Concurrent file reads limit", "description": "Maximum number of files the 'read_file' tool can process concurrently. Higher values may speed up reading multiple small files but increase memory usage." diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 2b4d3b8fe0f..2799741b1c8 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -492,6 +492,10 @@ "label": "Show .rooignore'd files in lists and searches", "description": "When enabled, files matching patterns in .rooignore will be shown in lists with a lock symbol. When disabled, these files will be completely hidden from file lists and searches." }, + "svnContext": { + "label": "Enable SVN context features", + "description": "When enabled, you can use @svn, @svn-changes, and @r123 in chat to reference SVN commits and working directory changes. When disabled, these SVN-related context mention features will be unavailable." + }, "maxConcurrentFileReads": { "label": "Concurrent file reads limit", "description": "Maximum number of files the 'read_file' tool can process concurrently. Higher values may speed up reading multiple small files but increase memory usage." diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 45b72097974..f7545a5aa33 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -492,6 +492,10 @@ "label": "Mostrar archivos .rooignore en listas y búsquedas", "description": "Cuando está habilitado, los archivos que coinciden con los patrones en .rooignore se mostrarán en listas con un símbolo de candado. Cuando está deshabilitado, estos archivos se ocultarán completamente de las listas de archivos y búsquedas." }, + "svnContext": { + "label": "Habilitar funciones de contexto SVN", + "description": "Cuando está habilitado, puedes usar @svn, @svn-changes y @r123 en el chat para referenciar commits de SVN y cambios del directorio de trabajo. Cuando está deshabilitado, estas funciones de mención de contexto relacionadas con SVN no estarán disponibles." + }, "maxReadFile": { "label": "Umbral de auto-truncado de lectura de archivos", "description": "Roo lee este número de líneas cuando el modelo omite valores de inicio/fin. Si este número es menor que el total del archivo, Roo genera un índice de números de línea de las definiciones de código. Casos especiales: -1 indica a Roo que lea el archivo completo (sin indexación), y 0 indica que no lea líneas y proporcione solo índices de línea para un contexto mínimo. Valores más bajos minimizan el uso inicial de contexto, permitiendo lecturas posteriores de rangos de líneas precisos. Las solicitudes con inicio/fin explícitos no están limitadas por esta configuración.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 1f846f1e680..f28ab68948b 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -492,6 +492,10 @@ "label": "Afficher les fichiers .rooignore dans les listes et recherches", "description": "Lorsque cette option est activée, les fichiers correspondant aux modèles dans .rooignore seront affichés dans les listes avec un symbole de cadenas. Lorsqu'elle est désactivée, ces fichiers seront complètement masqués des listes de fichiers et des recherches." }, + "svnContext": { + "label": "Activer les fonctionnalités de contexte SVN", + "description": "Lorsque cette option est activée, vous pouvez utiliser @svn, @svn-changes et @r123 dans le chat pour référencer les commits SVN et les changements du répertoire de travail. Lorsqu'elle est désactivée, ces fonctionnalités de mention de contexte liées à SVN ne seront pas disponibles." + }, "maxReadFile": { "label": "Seuil d'auto-troncature de lecture de fichier", "description": "Roo lit ce nombre de lignes lorsque le modèle omet les valeurs de début/fin. Si ce nombre est inférieur au total du fichier, Roo génère un index des numéros de ligne des définitions de code. Cas spéciaux : -1 indique à Roo de lire le fichier entier (sans indexation), et 0 indique de ne lire aucune ligne et de fournir uniquement les index de ligne pour un contexte minimal. Des valeurs plus basses minimisent l'utilisation initiale du contexte, permettant des lectures ultérieures de plages de lignes précises. Les requêtes avec début/fin explicites ne sont pas limitées par ce paramètre.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index ebe8524aa5a..5dd67757825 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -492,6 +492,10 @@ "label": "सूचियों और खोजों में .rooignore फाइलें दिखाएँ", "description": "जब सक्षम होता है, .rooignore में पैटर्न से मेल खाने वाली फाइलें लॉक प्रतीक के साथ सूचियों में दिखाई जाएंगी। जब अक्षम होता है, ये फाइलें फाइल सूचियों और खोजों से पूरी तरह छिपा दी जाएंगी।" }, + "svnContext": { + "label": "SVN संदर्भ सुविधा सक्षम करें", + "description": "सक्षम होने पर, चैट में @svn, @svn-changes और @r123 जैसे तरीकों से SVN कमिट रिकॉर्ड और कार्यक्षेत्र परिवर्तनों का संदर्भ दिया जा सकता है। अक्षम होने पर, ये SVN संबंधी संदर्भ उल्लेख सुविधाएं उपलब्ध नहीं होंगी।" + }, "maxReadFile": { "label": "फ़ाइल पढ़ने का स्वचालित काटने की सीमा", "description": "जब मॉडल प्रारंभ/अंत मान नहीं देता है, तो Roo इतनी पंक्तियाँ पढ़ता है। यदि यह संख्या फ़ाइल की कुल पंक्तियों से कम है, तो Roo कोड परिभाषाओं का पंक्ति क्रमांक इंडेक्स बनाता है। विशेष मामले: -1 Roo को पूरी फ़ाइल पढ़ने का निर्देश देता है (इंडेक्सिंग के बिना), और 0 कोई पंक्ति न पढ़ने और न्यूनतम संदर्भ के लिए केवल पंक्ति इंडेक्स प्रदान करने का निर्देश देता है। कम मान प्रारंभिक संदर्भ उपयोग को कम करते हैं, जो बाद में सटीक पंक्ति श्रेणी पढ़ने की अनुमति देता है। स्पष्ट प्रारंभ/अंत अनुरोध इस सेटिंग से सीमित नहीं हैं।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index bddc9a98c1a..dc48ce56cc6 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -522,6 +522,10 @@ "label": "Tampilkan file .rooignore'd dalam daftar dan pencarian", "description": "Ketika diaktifkan, file yang cocok dengan pola di .rooignore akan ditampilkan dalam daftar dengan simbol kunci. Ketika dinonaktifkan, file ini akan sepenuhnya disembunyikan dari daftar file dan pencarian." }, + "svnContext": { + "label": "Aktifkan fitur konteks SVN", + "description": "Ketika diaktifkan, dalam chat dapat menggunakan cara seperti @svn, @svn-changes dan @r123 untuk mereferensikan catatan commit SVN dan perubahan direktori kerja. Ketika dinonaktifkan, fitur penyebutan konteks terkait SVN ini tidak akan tersedia." + }, "maxConcurrentFileReads": { "label": "Batas pembacaan file bersamaan", "description": "Jumlah maksimum file yang dapat diproses oleh tool 'read_file' secara bersamaan. Nilai yang lebih tinggi dapat mempercepat pembacaan beberapa file kecil tetapi meningkatkan penggunaan memori." diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 4e3038c242f..e8315513259 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -492,6 +492,10 @@ "label": "Mostra file .rooignore negli elenchi e nelle ricerche", "description": "Quando abilitato, i file che corrispondono ai pattern in .rooignore verranno mostrati negli elenchi con un simbolo di blocco. Quando disabilitato, questi file saranno completamente nascosti dagli elenchi di file e dalle ricerche." }, + "svnContext": { + "label": "Abilita funzionalità di contesto SVN", + "description": "Quando abilitato, puoi utilizzare @svn, @svn-changes e @r123 nella chat per fare riferimento ai commit SVN e alle modifiche della directory di lavoro. Quando disabilitato, queste funzionalità di menzione del contesto relative a SVN non saranno disponibili." + }, "maxReadFile": { "label": "Soglia di auto-troncamento lettura file", "description": "Roo legge questo numero di righe quando il modello omette i valori di inizio/fine. Se questo numero è inferiore al totale del file, Roo genera un indice dei numeri di riga delle definizioni di codice. Casi speciali: -1 indica a Roo di leggere l'intero file (senza indicizzazione), e 0 indica di non leggere righe e fornire solo indici di riga per un contesto minimo. Valori più bassi minimizzano l'utilizzo iniziale del contesto, permettendo successive letture precise di intervalli di righe. Le richieste con inizio/fine espliciti non sono limitate da questa impostazione.", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 0aa402d1399..2cb61c42eee 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -492,6 +492,10 @@ "label": "リストと検索で.rooignoreファイルを表示", "description": "有効にすると、.rooignoreのパターンに一致するファイルがロックシンボル付きでリストに表示されます。無効にすると、これらのファイルはファイルリストや検索から完全に非表示になります。" }, + "svnContext": { + "label": "SVNコンテキスト機能を有効化", + "description": "有効にすると、チャットで@svn、@svn-changes、@r123を使用してSVNコミットや作業ディレクトリの変更を参照できます。無効にすると、これらのSVN関連のコンテキスト参照機能は利用できません。" + }, "maxReadFile": { "label": "ファイル読み込み自動切り詰めしきい値", "description": "モデルが開始/終了の値を指定しない場合、Rooはこの行数を読み込みます。この数がファイルの総行数より少ない場合、Rooはコード定義の行番号インデックスを生成します。特殊なケース:-1はRooにファイル全体を読み込むよう指示し(インデックス作成なし)、0は行を読み込まず最小限のコンテキストのために行インデックスのみを提供するよう指示します。低い値は初期コンテキスト使用量を最小限に抑え、後続の正確な行範囲の読み込みを可能にします。明示的な開始/終了の要求はこの設定による制限を受けません。", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 25b79b795f9..a8d1ce3a6dd 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -492,6 +492,10 @@ "label": "목록 및 검색에서 .rooignore 파일 표시", "description": "활성화되면 .rooignore의 패턴과 일치하는 파일이 잠금 기호와 함께 목록에 표시됩니다. 비활성화되면 이러한 파일은 파일 목록 및 검색에서 완전히 숨겨집니다." }, + "svnContext": { + "label": "SVN 컨텍스트 기능 활성화", + "description": "활성화되면 채팅에서 @svn, @svn-changes, @r123을 사용하여 SVN 커밋 및 작업 디렉토리 변경 사항을 참조할 수 있습니다. 비활성화되면 이러한 SVN 관련 컨텍스트 언급 기능을 사용할 수 없습니다." + }, "maxReadFile": { "label": "파일 읽기 자동 축소 임계값", "description": "모델이 시작/끝 값을 지정하지 않을 때 Roo가 읽는 줄 수입니다. 이 수가 파일의 총 줄 수보다 적으면 Roo는 코드 정의의 줄 번호 인덱스를 생성합니다. 특수한 경우: -1은 Roo에게 전체 파일을 읽도록 지시하고(인덱싱 없이), 0은 줄을 읽지 않고 최소한의 컨텍스트를 위해 줄 인덱스만 제공하도록 지시합니다. 낮은 값은 초기 컨텍스트 사용을 최소화하고, 이후 정확한 줄 범위 읽기를 가능하게 합니다. 명시적 시작/끝 요청은 이 설정의 제한을 받지 않습니다.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 36f5f50c4e2..cb7fef97820 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -492,6 +492,10 @@ "label": ".rooignore-bestanden tonen in lijsten en zoekopdrachten", "description": "Indien ingeschakeld, worden bestanden die overeenkomen met patronen in .rooignore getoond in lijsten met een slotje. Indien uitgeschakeld, worden deze bestanden volledig verborgen in lijsten en zoekopdrachten." }, + "svnContext": { + "label": "SVN-contextfunctie inschakelen", + "description": "Indien ingeschakeld, kunnen in de chat methoden zoals @svn, @svn-changes en @r123 worden gebruikt om SVN-commitrecords en werkdirectorywijzigingen te refereren. Indien uitgeschakeld, zijn deze SVN-gerelateerde contextverwijzingsfuncties niet beschikbaar." + }, "maxReadFile": { "label": "Automatisch afkappen bij bestandslezen", "description": "Roo leest dit aantal regels wanneer het model geen begin/eindwaarden opgeeft. Als dit aantal lager is dan het totaal, genereert Roo een index van codelijnen. Speciale gevallen: -1 laat Roo het hele bestand lezen (zonder indexering), 0 leest geen regels en geeft alleen een minimale index. Lagere waarden minimaliseren het initiële contextgebruik en maken precieze vervolg-leesopdrachten mogelijk. Expliciete begin/eind-aanvragen worden niet door deze instelling beperkt.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 455beebfdaf..2c182bda0ab 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -492,6 +492,10 @@ "label": "Pokaż pliki .rooignore na listach i w wyszukiwaniach", "description": "Gdy włączone, pliki pasujące do wzorców w .rooignore będą pokazywane na listach z symbolem kłódki. Gdy wyłączone, te pliki będą całkowicie ukryte z list plików i wyszukiwań." }, + "svnContext": { + "label": "Włącz funkcję kontekstu SVN", + "description": "Gdy włączone, w czacie można używać metod takich jak @svn, @svn-changes i @r123 do odwoływania się do rekordów commitów SVN i zmian w katalogu roboczym. Gdy wyłączone, te funkcje kontekstowe związane z SVN nie będą dostępne." + }, "maxReadFile": { "label": "Próg automatycznego skracania odczytu pliku", "description": "Roo odczytuje tę liczbę linii, gdy model nie określa wartości początkowej/końcowej. Jeśli ta liczba jest mniejsza niż całkowita liczba linii pliku, Roo generuje indeks numerów linii definicji kodu. Przypadki specjalne: -1 nakazuje Roo odczytać cały plik (bez indeksowania), a 0 nakazuje nie czytać żadnych linii i dostarczyć tylko indeksy linii dla minimalnego kontekstu. Niższe wartości minimalizują początkowe użycie kontekstu, umożliwiając późniejsze precyzyjne odczyty zakresów linii. Jawne żądania początku/końca nie są ograniczone tym ustawieniem.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 0e77668246c..95d77111e53 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -492,6 +492,10 @@ "label": "Mostrar arquivos .rooignore em listas e pesquisas", "description": "Quando ativado, os arquivos que correspondem aos padrões em .rooignore serão mostrados em listas com um símbolo de cadeado. Quando desativado, esses arquivos serão completamente ocultos das listas de arquivos e pesquisas." }, + "svnContext": { + "label": "Ativar funcionalidade de contexto SVN", + "description": "Quando ativado, você pode usar @svn, @svn-changes e @r123 no chat para referenciar commits SVN e mudanças no diretório de trabalho. Quando desativado, essas funcionalidades de menção de contexto relacionadas ao SVN não estarão disponíveis." + }, "maxReadFile": { "label": "Limite de auto-truncamento de leitura de arquivo", "description": "O Roo lê este número de linhas quando o modelo omite valores de início/fim. Se este número for menor que o total do arquivo, o Roo gera um índice de números de linha das definições de código. Casos especiais: -1 instrui o Roo a ler o arquivo inteiro (sem indexação), e 0 instrui a não ler linhas e fornecer apenas índices de linha para contexto mínimo. Valores mais baixos minimizam o uso inicial de contexto, permitindo leituras posteriores precisas de intervalos de linhas. Requisições com início/fim explícitos não são limitadas por esta configuração.", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 27502028562..c7f31684b0f 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -492,6 +492,10 @@ "label": "Показывать .rooignore-файлы в списках и поиске", "description": "Если включено, файлы, совпадающие с шаблонами в .rooignore, будут отображаться в списках с символом замка. Если выключено, такие файлы полностью скрываются из списков и поиска." }, + "svnContext": { + "label": "Включить функции контекста SVN", + "description": "Если включено, вы можете использовать @svn, @svn-changes и @r123 в чате для ссылки на коммиты SVN и изменения рабочей директории. Если выключено, эти функции упоминания контекста SVN будут недоступны." + }, "maxReadFile": { "label": "Порог автообрезки при чтении файла", "description": "Roo читает столько строк, если модель не указала явно начало/конец. Если число меньше общего количества строк в файле, Roo создаёт индекс определений кода по строкам. Особые случаи: -1 — Roo читает весь файл (без индексации), 0 — не читает строки, а создаёт только минимальный индекс. Меньшие значения минимизируют начальный контекст, позволяя точнее читать нужные диапазоны строк. Явные запросы начала/конца не ограничиваются этим параметром.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index a6d8a2b6b9a..1c3dd9cb0e0 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -492,6 +492,10 @@ "label": "Listelerde ve aramalarda .rooignore dosyalarını göster", "description": "Etkinleştirildiğinde, .rooignore'daki desenlerle eşleşen dosyalar kilit sembolü ile listelerde gösterilecektir. Devre dışı bırakıldığında, bu dosyalar dosya listelerinden ve aramalardan tamamen gizlenecektir." }, + "svnContext": { + "label": "SVN bağlam özelliklerini etkinleştir", + "description": "Etkinleştirildiğinde, sohbette SVN commit'leri ve çalışma dizini değişikliklerine referans vermek için @svn, @svn-changes ve @r123 kullanabilirsiniz. Devre dışı bırakıldığında, bu SVN ile ilgili bağlam bahsetme özellikleri kullanılamaz." + }, "maxReadFile": { "label": "Dosya okuma otomatik kısaltma eşiği", "description": "Model başlangıç/bitiş değerlerini belirtmediğinde Roo bu sayıda satırı okur. Bu sayı dosyanın toplam satır sayısından azsa, Roo kod tanımlamalarının satır numarası dizinini oluşturur. Özel durumlar: -1, Roo'ya tüm dosyayı okumasını (dizinleme olmadan), 0 ise hiç satır okumamasını ve minimum bağlam için yalnızca satır dizinleri sağlamasını belirtir. Düşük değerler başlangıç bağlam kullanımını en aza indirir ve sonraki hassas satır aralığı okumalarına olanak tanır. Açık başlangıç/bitiş istekleri bu ayarla sınırlı değildir.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index b0e0fb7edff..fb772315f95 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -492,6 +492,10 @@ "label": "Hiển thị tệp .rooignore trong danh sách và tìm kiếm", "description": "Khi được bật, các tệp khớp với mẫu trong .rooignore sẽ được hiển thị trong danh sách với biểu tượng khóa. Khi bị tắt, các tệp này sẽ hoàn toàn bị ẩn khỏi danh sách tệp và tìm kiếm." }, + "svnContext": { + "label": "Bật tính năng ngữ cảnh SVN", + "description": "Khi được bật, bạn có thể sử dụng @svn, @svn-changes và @r123 trong trò chuyện để tham chiếu đến các commit SVN và thay đổi thư mục làm việc. Khi bị tắt, các tính năng đề cập ngữ cảnh liên quan đến SVN này sẽ không khả dụng." + }, "maxReadFile": { "label": "Ngưỡng tự động cắt ngắn khi đọc tệp", "description": "Roo đọc số dòng này khi mô hình không chỉ định giá trị bắt đầu/kết thúc. Nếu số này nhỏ hơn tổng số dòng của tệp, Roo sẽ tạo một chỉ mục số dòng của các định nghĩa mã. Trường hợp đặc biệt: -1 chỉ thị Roo đọc toàn bộ tệp (không tạo chỉ mục), và 0 chỉ thị không đọc dòng nào và chỉ cung cấp chỉ mục dòng cho ngữ cảnh tối thiểu. Giá trị thấp hơn giảm thiểu việc sử dụng ngữ cảnh ban đầu, cho phép đọc chính xác các phạm vi dòng sau này. Các yêu cầu có chỉ định bắt đầu/kết thúc rõ ràng không bị giới hạn bởi cài đặt này.", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 509176a6f46..1000415a7ec 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -492,6 +492,10 @@ "label": "在列表和搜索中显示 .rooignore 文件", "description": "启用后,与 .rooignore 中模式匹配的文件将在列表中显示锁定符号。禁用时,这些文件将从文件列表和搜索中完全隐藏。" }, + "svnContext": { + "label": "启用 SVN 上下文功能", + "description": "启用后,在聊天中可以使用 @svn、@svn-changes 和 @r123 等方式引用 SVN 提交记录和工作目录变更。禁用后,这些 SVN 相关的上下文提及功能将不可用。" + }, "maxReadFile": { "label": "文件读取自动截断阈值", "description": "自动读取文件行数设置:-1=完整读取 0=仅生成行号索引,较小值可节省token,支持后续使用行号进行读取。 <0>了解更多", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 044ac0a7517..cb9e7fe93be 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -492,6 +492,10 @@ "label": "在列表和搜尋中顯示被 .rooignore 排除的檔案", "description": "啟用後,符合 .rooignore 規則的檔案會在列表中顯示並標示鎖定圖示。停用後,這些檔案將完全從檔案列表和搜尋結果中隱藏。" }, + "svnContext": { + "label": "啟用 SVN 上下文功能", + "description": "啟用後,可在聊天中使用 @svn、@svn-changes 和 @r123 等方式引用 SVN 提交記錄和工作目錄變更。停用後,這些 SVN 相關的上下文提及功能將無法使用。" + }, "maxReadFile": { "label": "檔案讀取自動截斷閾值", "description": "當模型未指定起始/結束值時,Roo 讀取的行數。如果此數值小於檔案總行數,Roo 將產生程式碼定義的行號索引。特殊情況:-1 指示 Roo 讀取整個檔案(不建立索引),0 指示不讀取任何行並僅提供行索引以取得最小上下文。較低的值可最小化初始上下文使用,允許後續精確的行範圍讀取。明確指定起始/結束的請求不受此設定限制。 <0>瞭解更多", diff --git a/webview-ui/src/utils/__tests__/context-mentions.spec.ts b/webview-ui/src/utils/__tests__/context-mentions.spec.ts index 50fb1b1c504..164c9217da9 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.spec.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.spec.ts @@ -195,8 +195,8 @@ describe("getContextMenuOptions", () => { ] it("should return all option types for empty query", () => { - const result = getContextMenuOptions("", "", null, []) - expect(result).toHaveLength(6) + const result = getContextMenuOptions("", "", null, [], [], undefined, true) + expect(result).toHaveLength(7) expect(result.map((item) => item.type)).toEqual([ ContextMenuOptionType.Problems, ContextMenuOptionType.Terminal, @@ -204,6 +204,7 @@ describe("getContextMenuOptions", () => { ContextMenuOptionType.Folder, ContextMenuOptionType.File, ContextMenuOptionType.Git, + ContextMenuOptionType.Svn, ]) }) diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 889dca9dbea..158ceca82d0 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -103,6 +103,7 @@ export enum ContextMenuOptionType { Terminal = "terminal", URL = "url", Git = "git", + Svn = "svn", NoResults = "noResults", Mode = "mode", // Add mode type } @@ -122,6 +123,7 @@ export function getContextMenuOptions( queryItems: ContextMenuQueryItem[], dynamicSearchResults: SearchResult[] = [], modes?: ModeConfig[], + enableSvnContext: boolean = false, ): ContextMenuQueryItem[] { // Handle slash commands for modes if (query.startsWith("/") && inputValue.startsWith("/")) { @@ -165,6 +167,14 @@ export function getContextMenuOptions( icon: "$(git-commit)", } + const svnWorkingChanges: ContextMenuQueryItem = { + type: ContextMenuOptionType.Svn, + value: "svn-changes", + label: "Working changes", + description: "Current uncommitted changes", + icon: "$(git-commit)", + } + if (query === "") { if (selectedType === ContextMenuOptionType.File) { const files = queryItems @@ -191,7 +201,12 @@ export function getContextMenuOptions( return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges] } - return [ + if (enableSvnContext && selectedType === ContextMenuOptionType.Svn) { + const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Svn) + return commits.length > 0 ? [svnWorkingChanges, ...commits] : [svnWorkingChanges] + } + + const defaultOptions = [ { type: ContextMenuOptionType.Problems }, { type: ContextMenuOptionType.Terminal }, { type: ContextMenuOptionType.URL }, @@ -199,6 +214,12 @@ export function getContextMenuOptions( { type: ContextMenuOptionType.File }, { type: ContextMenuOptionType.Git }, ] + + if (enableSvnContext) { + defaultOptions.push({ type: ContextMenuOptionType.Svn }) + } + + return defaultOptions } const lowerQuery = query.toLowerCase() @@ -215,6 +236,16 @@ export function getContextMenuOptions( } else if ("git-changes".startsWith(lowerQuery)) { suggestions.push(workingChanges) } + if (enableSvnContext && "svn".startsWith(lowerQuery)) { + suggestions.push({ + type: ContextMenuOptionType.Svn, + label: "SVN Commits", + description: "Search repository history", + icon: "$(git-commit)", + }) + } else if (enableSvnContext && "svn-changes".startsWith(lowerQuery)) { + suggestions.push(svnWorkingChanges) + } if ("problems".startsWith(lowerQuery)) { suggestions.push({ type: ContextMenuOptionType.Problems }) } @@ -262,6 +293,8 @@ export function getContextMenuOptions( const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git) + const svnMatches = enableSvnContext ? matchingItems.filter((item) => item.type === ContextMenuOptionType.Svn) : [] + // Convert search results to queryItems format const searchResultItems = dynamicSearchResults.map((result) => { // Ensure paths start with / for consistency @@ -282,7 +315,7 @@ export function getContextMenuOptions( } }) - const allItems = [...suggestions, ...openedFileMatches, ...searchResultItems, ...gitMatches] + const allItems = [...suggestions, ...openedFileMatches, ...searchResultItems, ...gitMatches, ...svnMatches] // Remove duplicates - normalize paths by ensuring all have leading slashes const seen = new Set()