diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1c1e190c94..264e4e3cde 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,14 +1,66 @@ name: Bug Report -description: Clearly report a bug with detailed repro steps +description: Report a broken behavior in plain language with a minimal reproduction labels: ["bug"] +title: "[BUG] " body: - - type: input - id: version - attributes: - label: App Version - description: Specify exactly which version you're using (e.g., v3.3.1) - validations: - required: true + - type: markdown + attributes: + value: | + Thank you for your report! Please search existing issues first: + https://github.com/zgsm-ai/costrict/issues + + - type: textarea + id: problem + attributes: + label: Problem (one or two sentences) + description: Describe what went wrong in plain language. + placeholder: 'Example: "Expected the task to start, but nothing happened and no message appeared."' + validations: + required: true + + - type: textarea + id: context + attributes: + label: Context (who is affected and when) + description: Who sees this and in what situation? Keep it non-technical. + placeholder: 'Example: "Happens to new users when starting a run from the New Run page with dark theme enabled."' + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Reproduction steps + description: Provide clear, numbered steps so we can reproduce. + placeholder: | + 1) Environment/setup (OS, extension version, relevant settings) + 2) Exact actions (clicks, inputs, commands) + 3) What you observed after each step + validations: + required: true + + - type: input + id: expected + attributes: + label: Expected result + placeholder: e.g., "The task starts and shows progress." + validations: + required: true + + - type: input + id: actual + attributes: + label: Actual result + placeholder: e.g., "The button appears disabled and no progress is shown." + validations: + required: true + + - type: textarea + id: variations + attributes: + label: Variations tried (optional) + description: Different browsers, devices, providers, or settings you tried. + placeholder: e.g., "Tried Chrome/Firefox, disabling dark theme, switching providers." - type: dropdown id: provider @@ -54,7 +106,7 @@ body: - type: dropdown id: provider attributes: - label: API Provider + label: API Provider (optional) options: - Costrict - Anthropic @@ -81,36 +133,16 @@ body: - VS Code Language Model API - xAI (Grok) - Not Applicable / Other - validations: - required: true - - type: textarea - id: steps - attributes: - label: Detailed Steps to Reproduce - description: | - List the exact steps someone must follow to reproduce this bug: - 1. Starting conditions (software state, settings, environment) - 2. Precise actions taken (every click, selection, input) - 3. Clearly observe and report outcomes - value: | - 1. - 2. - 3. - validations: - required: true - - - type: textarea - id: logs - attributes: - label: Relevant API Request Output - description: Paste relevant API logs or outputs here (formatted automatically as code) - render: shell - validations: - required: true + - type: input + id: model + attributes: + label: Model Used (optional) + description: Exact model name (e.g., Claude 3.7 Sonnet). Use N/A if irrelevant. - - type: textarea - id: additional-context - attributes: - label: Additional Context - description: Include extra details, screenshots, or related issues. + - type: textarea + id: logs + attributes: + label: Relevant logs or errors (optional) + description: Paste relevant output or errors. Use triple backticks (```) for formatting. + render: shell diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 93b9d926ba..728606f609 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,61 +1,47 @@ -name: Detailed Feature Proposal -description: Report a specific problem that needs solving in Costrict -labels: ["proposal", "enhancement"] +name: Enhancement Request +description: Propose an improvement in plain language focused on user benefit +labels: ["enhancement"] +title: "[ENHANCEMENT] " body: - type: markdown attributes: value: | - **Thank you for submitting a feature request for Costrict!** - - This template helps you describe problems that need solving. Focus on the problem - the Roo team will work to design solutions unless you want to contribute the implementation yourself. - - **Quality over speed:** We prefer detailed, clear problem descriptions over quick ones. Vague requests often get closed or require multiple rounds of clarification, which wastes everyone's time. - - **Before submitting:** - - Search existing [Issues](https://github.com/zgsm-ai/costrict/issues) and [Discussions](https://github.com/zgsm-ai/costrict/discussions) to avoid duplicates - - For general ideas, use [GitHub Discussions](https://github.com/zgsm-ai/costrict/discussions/categories/feature-requests) instead of this template. + Thank you for helping improve Costrict! + Please focus on the problem and the desired behavior in plain language. - - type: markdown + - type: textarea + id: problem attributes: - value: | - ## ❌ Common mistakes that lead to request rejection: - - **Vague problem descriptions:** "UI is bad" -> Should be: "Submit button is invisible on dark theme" - - **Missing user impact:** "This would be cool" -> Should explain who benefits and how - - **No specific context:** Describe exactly when and how the problem occurs - + label: Problem (one or two sentences) + description: What problem are users facing? + placeholder: e.g., "Users often click Copy Run by mistake and duplicate runs unintentionally." + validations: + required: true - type: textarea - id: problem-description + id: context attributes: - label: What specific problem does this solve? - description: | - **Be concrete and detailed.** Explain the problem from a user's perspective. - - ✅ **Good examples (specific, clear impact):** - - "When running large tasks, users wait 5+ minutes because tasks execute sequentially instead of in parallel, blocking productivity" - - "AI can only read one file per request, forcing users to make multiple requests for multi-file projects, increasing wait time from 30s to 5+ minutes" - - "Dark theme users can't see the submit button because it uses white text on light grey background" - - ❌ **Poor examples (vague, unclear impact):** - - "The UI looks weird" -> What specifically looks weird? On which screen? What's the impact? - - "System prompt is not good" -> What's wrong with it? What behaviour does it cause? What should it do instead? - - "Performance could be better" -> Where? How slow is it currently? What's the user impact? - - **Your problem description should answer:** - - Who is affected? (all users, specific user types, etc.) - - When does this happen? (specific scenarios/steps) - - What's the current behaviour vs expected behaviour? - - What's the impact? (time wasted, errors caused, etc.) - placeholder: Be specific about the problem, who it affects, and the impact. Avoid generic statements like "it's slow" or "it's confusing." + label: Context (who is affected and when) + description: Who encounters this and in what situation? + placeholder: e.g., "Happens when browsing the Runs list; most visible for new users." validations: required: true + - type: textarea + id: desired + attributes: + label: Desired behavior (conceptual, not technical) + description: Describe what should happen in simple terms. + placeholder: e.g., "Ask for confirmation before copying a run." + validations: + required: true - type: textarea - id: additional-context + id: constraints attributes: - label: Additional context (optional) - description: Mockups, screenshots, links, user quotes, or other relevant information that supports your proposal. + label: Constraints / preferences (optional) + description: Any considerations like performance, accessibility, or UX expectations. + placeholder: e.g., "Keep it quick and unobtrusive; keyboard accessible." - type: checkboxes id: checklist @@ -64,128 +50,35 @@ body: options: - label: I've searched existing Issues and Discussions for duplicates required: true - - label: This describes a specific problem with clear impact and context + - label: This describes a specific problem with clear context and impact required: true - type: markdown attributes: value: | --- - - ## 🛠️ **Optional: Contributing & Technical Analysis** - - **🎯 Just reporting a problem?** You can click "Submit new issue" right now! The sections below are only needed if you want to contribute a solution via pull request. - - **⚠️ Only continue if you want to:** - - Propose a specific solution design - - Implement the feature yourself via pull request - - Provide technical analysis to help with implementation - - **For contributors who continue:** - - A maintainer (especially @hannesrudolph) will review this proposal. **Do not start implementation until approved and assigned.** We're a small team with limited resources, so every code addition needs careful consideration. We're always happy to receive clear, actionable proposals though! - - Join [Discord](https://discord.gg/roocode) and DM **Hannes Rudolph** (`hrudolph`) for guidance on implementation - - Check our [Roadmap](https://github.com/orgs/RooCodeInc/projects/1/views/1?query=sort%3Aupdated-desc+is%3Aopen&filterQuery=is%3Aissue%2Copen%2Cclosed+label%3A%22feature+request%22+status%3A%22Issue+%5BUnassigned%5D%22%2C%22Issue+%5BIn+Progress%5D%22) to see open feature requests ready to be implemented or currently being worked on - - - type: checkboxes - id: willingness-to-contribute - attributes: - label: Interested in implementing this? - description: | - **Important:** If you check "Yes" below, the technical sections become REQUIRED. - We need detailed technical analysis from contributors to ensure quality implementation. - options: - - label: Yes, I'd like to help implement this feature - required: false - - - type: checkboxes - id: implementation-approval - attributes: - label: Implementation requirements - options: - - label: I understand this needs approval before implementation begins - required: false - - - type: textarea - id: proposed-solution - attributes: - label: How should this be solved? (REQUIRED if contributing, optional otherwise) - description: | - **If you want to implement this feature, this section is REQUIRED.** - - **Describe your solution in detail.** Explain not just what to build, but how it should work. - - ✅ **Good examples:** - - "Add parallel task execution: Allow up to 3 tasks to run simultaneously with a queue system for additional tasks. Show progress for each active task in the UI." - - "Enable multi-file AI processing: Modify the request handler to accept multiple files in a single request and process them together, reducing round trips." - - "Fix button contrast: Change submit button to use primary colour on dark theme (white text on blue background) instead of current grey." - - ❌ **Poor examples:** - - "Make it faster" -> How? What specific changes? - - "Improve the UI" -> Which part? What specific improvements? - - "Fix the prompt" -> What should the new prompt do differently? - - **Your solution should explain:** - - What exactly will change? - - How will users interact with it? - - What will the new behaviour look like? - placeholder: Describe the specific changes and how they will work. Include user interaction details if relevant. + Optional (for contributors): You can stop here if you're just proposing the improvement. - type: textarea id: acceptance-criteria attributes: - label: How will we know it works? (Acceptance Criteria - REQUIRED if contributing, optional otherwise) - description: | - **If you want to implement this feature, this section is REQUIRED.** - - **This is crucial - don't skip it.** Define what "working" looks like with specific, testable criteria. - - **Format suggestion:** - ``` - Given [context/situation] + label: Acceptance criteria (optional) + description: Define what “working” looks like with specific, testable outcomes. + placeholder: | + Given [context] When [user action] Then [expected result] And [additional expectations] But [what should NOT happen] - ``` - - **Example:** - ``` - Given I have 5 large tasks to run - When I start all of them - Then they execute in parallel (max 3 at once, can be configured) - And I see progress for each active task - And queued tasks show "waiting" status - But the UI doesn't freeze or become unresponsive - ``` - placeholder: | - Define specific, testable criteria. What should users be able to do? What should happen? What should NOT happen? - Use the Given/When/Then format above or your own clear structure. - type: textarea - id: technical-considerations + id: proposed-solution attributes: - label: Technical considerations (REQUIRED if contributing, optional otherwise) - description: | - **If you want to implement this feature, this section is REQUIRED.** - - Share technical insights that could help planning: - - Implementation approach or architecture changes - - Performance implications - - Compatibility concerns - - Systems that might be affected - - Potential blockers you can foresee - placeholder: e.g., "Will need to refactor task manager", "Could impact memory usage on large files", "Requires a large portion of code to be rewritten" + label: Proposed approach (optional) + description: If you have an idea, describe it briefly in plain language. - type: textarea - id: trade-offs-and-risks + id: risks attributes: - label: Trade-offs and risks (REQUIRED if contributing, optional otherwise) - description: | - **If you want to implement this feature, this section is REQUIRED.** - - What could go wrong or what alternatives did you consider? - - Alternative approaches and why you chose this one - - Potential negative impacts (performance, UX, etc.) - - Breaking changes or migration concerns - - Edge cases that need careful handling - placeholder: 'e.g., "Alternative: use library X but it is 500KB larger", "Risk: might slow older devices", "Breaking: changes API response format"' + label: Trade-offs / risks (optional) + description: Potential downsides or alternatives considered. diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index f0eeb6fe16..63baa2e6b2 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -323,9 +323,13 @@ const sambaNovaSchema = apiModelIdProviderModelSchema.extend({ sambaNovaApiKey: z.string().optional(), }) +export const zaiApiLineSchema = z.enum(["international_coding", "international", "china_coding", "china"]) + +export type ZaiApiLine = z.infer + const zaiSchema = apiModelIdProviderModelSchema.extend({ zaiApiKey: z.string().optional(), - zaiApiLine: z.union([z.literal("china"), z.literal("international")]).optional(), + zaiApiLine: zaiApiLineSchema.optional(), }) const fireworksSchema = apiModelIdProviderModelSchema.extend({ diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index f724744827..b3838c1406 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -1,4 +1,5 @@ import type { ModelInfo } from "../model.js" +import { ZaiApiLine } from "../provider-settings.js" // Z AI // https://docs.z.ai/guides/llm/glm-4.5 @@ -103,3 +104,14 @@ export const mainlandZAiModels = { } as const satisfies Record export const ZAI_DEFAULT_TEMPERATURE = 0 + +export const zaiApiLineConfigs = { + international_coding: { + name: "International Coding Plan", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + isChina: false, + }, + international: { name: "International Standard", baseUrl: "https://api.z.ai/api/paas/v4", isChina: false }, + china_coding: { name: "China Coding Plan", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", isChina: true }, + china: { name: "China Standard", baseUrl: "https://open.bigmodel.cn/api/paas/v4", isChina: true }, +} satisfies Record diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c04ef677b5..2c82466c71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -669,8 +669,8 @@ importers: specifier: ^0.5.0 version: 0.5.0 axios: - specifier: ^1.7.4 - version: 1.9.0 + specifier: ^1.12.0 + version: 1.12.0 cheerio: specifier: ^1.0.0 version: 1.0.0 @@ -1030,8 +1030,8 @@ importers: specifier: ^1.4.0 version: 1.4.0(react@18.3.1) axios: - specifier: ^1.7.4 - version: 1.9.0 + specifier: ^1.12.0 + version: 1.12.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2146,8 +2146,8 @@ packages: '@libsql/client@0.15.8': resolution: {integrity: sha512-TskygwF+ToZeWhPPT0WennyGrP3tmkKraaKopT2YwUjqD6DWDRm6SG5iy0VqnaO+HC9FNBCDX0oQPODU3gqqPQ==} - '@libsql/core@0.15.14': - resolution: {integrity: sha512-b2eVQma78Ss+edIIFi7LnhhyUy5hAJjYvrSAD5RFdO/YKP2rEvNAT1pIn2Li7NrqcsMmoEQWlpUWH4fWMdXtpQ==} + '@libsql/core@0.15.15': + resolution: {integrity: sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA==} '@libsql/darwin-arm64@0.5.22': resolution: {integrity: sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA==} @@ -4566,8 +4566,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axios@1.12.0: + resolution: {integrity: sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==} azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} @@ -6131,6 +6131,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -11684,7 +11693,7 @@ snapshots: '@libsql/client@0.15.8': dependencies: - '@libsql/core': 0.15.14 + '@libsql/core': 0.15.15 '@libsql/hrana-client': 0.7.0 js-base64: 3.7.8 libsql: 0.5.22 @@ -11694,7 +11703,7 @@ snapshots: - utf-8-validate optional: true - '@libsql/core@0.15.14': + '@libsql/core@0.15.15': dependencies: js-base64: 3.7.8 optional: true @@ -14390,9 +14399,9 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.9.0: + axios@1.12.0: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.11 form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16115,6 +16124,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11: {} + follow-redirects@1.15.9: {} for-each@0.3.5: diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts index a16aa9fcdf..7928a4298d 100644 --- a/src/api/providers/__tests__/zai.spec.ts +++ b/src/api/providers/__tests__/zai.spec.ts @@ -41,7 +41,11 @@ describe("ZAiHandler", () => { it("should use the correct international Z AI base URL", () => { new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" }) - expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" })) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://api.z.ai/api/paas/v4", + }), + ) }) it("should use the provided API key for international", () => { @@ -109,7 +113,11 @@ describe("ZAiHandler", () => { describe("Default behavior", () => { it("should default to international when no zaiApiLine is specified", () => { const handlerDefault = new ZAiHandler({ zaiApiKey: "test-zai-api-key" }) - expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" })) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://api.z.ai/api/coding/paas/v4", + }), + ) const model = handlerDefault.getModel() expect(model.id).toBe(internationalZAiDefaultModelId) diff --git a/src/api/providers/utils/response-render-config.ts b/src/api/providers/utils/response-render-config.ts index f496935d39..3c4ec8d925 100644 --- a/src/api/providers/utils/response-render-config.ts +++ b/src/api/providers/utils/response-render-config.ts @@ -24,5 +24,5 @@ export function getApiResponseRenderMode() { .getConfiguration("zgsm") .get("apiResponseRenderMode", "medium") as "fast" | "medium" | "slow" | "noLimit" - return renderModes[apiResponseRenderMode] || renderModes["medium"] + return renderModes[apiResponseRenderMode] || renderModes["fast"] } diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index e37e37f01b..ce5aab9dd9 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -6,6 +6,7 @@ import { type InternationalZAiModelId, type MainlandZAiModelId, ZAI_DEFAULT_TEMPERATURE, + zaiApiLineConfigs, } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -14,14 +15,14 @@ import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" export class ZAiHandler extends BaseOpenAiCompatibleProvider { constructor(options: ApiHandlerOptions) { - const isChina = options.zaiApiLine === "china" + const isChina = zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].isChina const models = isChina ? mainlandZAiModels : internationalZAiModels const defaultModelId = isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId super({ ...options, providerName: "Z AI", - baseURL: isChina ? "https://open.bigmodel.cn/api/paas/v4" : "https://api.z.ai/api/paas/v4", + baseURL: zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].baseUrl, apiKey: options.zaiApiKey ?? "not-provided", defaultProviderModelId: defaultModelId, providerModels: models, diff --git a/src/api/providers/zgsm.ts b/src/api/providers/zgsm.ts index 2d756a6a4b..059db144c8 100644 --- a/src/api/providers/zgsm.ts +++ b/src/api/providers/zgsm.ts @@ -46,7 +46,7 @@ export class ZgsmAiHandler extends BaseProvider implements SingleCompletionHandl private chatType?: "user" | "system" private headers = {} private modelInfo = {} as ModelInfo - private apiResponseRenderModeInfo = renderModes.medium + private apiResponseRenderModeInfo = renderModes.fast private logger: ILogger private curStream: any = null @@ -104,6 +104,7 @@ export class ZgsmAiHandler extends BaseProvider implements SingleCompletionHandl // Performance monitoring log const requestId = uuidv7() await this.fetchModel() + this.apiResponseRenderModeInfo = getApiResponseRenderMode() // 1. Cache calculation results and configuration const { info: modelInfo, reasoning } = this.getModel() const modelUrl = this.baseURL || ZgsmAuthConfig.getInstance().getDefaultApiBaseUrl() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 1fe2812f70..ada068a0f4 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2938,7 +2938,12 @@ export const webviewMessageHandler = async ( TelemetryService.instance.captureTabShown(message.tab) } - await provider.postMessageToWebview({ type: "action", action: "switchTab", tab: message.tab }) + await provider.postMessageToWebview({ + type: "action", + action: "switchTab", + tab: message.tab, + values: message.values, + }) } break } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index a1c3ff5a21..d51979a227 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -225,5 +225,9 @@ "preventCompletionWithOpenTodos": { "description": "Prevent task completion when there are incomplete todos in the todo list" } + }, + "docsLink": { + "label": "Docs", + "url": "https://docs.roocode.com" } } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index a70fcab8a0..ddbe22c1f4 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -246,5 +246,9 @@ "preventCompletionWithOpenTodos": { "description": "当待办事项列表中有未完成的待办事项时阻止任务完成" } + }, + "docsLink": { + "label": "Docs", + "url": "https://docs.roocode.com" } } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index dc459a655c..64f440baa7 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -241,5 +241,9 @@ "preventCompletionWithOpenTodos": { "description": "當待辦事項清單中有未完成的待辦事項時阻止工作完成" } + }, + "docsLink": { + "label": "Docs", + "url": "https://docs.roocode.com" } } diff --git a/src/package.json b/src/package.json index 59de1d38ab..32b4e11381 100644 --- a/src/package.json +++ b/src/package.json @@ -418,6 +418,16 @@ } ] }, + "keybindings": [ + { + "command": "zgsm.addToContext", + "key": "cmd+l", + "mac": "cmd+l", + "win": "ctrl+l", + "linux": "ctrl+l", + "when": "editorTextFocus && editorHasSelection" + } + ], "submenus": [ { "id": "zgsm.contextMenu", @@ -641,7 +651,7 @@ "medium", "slow" ], - "default": "medium", + "default": "fast", "description": "%settings.apiResponseRenderMode.description%" }, "zgsm.newTaskRequireTodos": { @@ -657,15 +667,7 @@ "description": "%settings.codeIndex.embeddingBatchSize.description%" } } - }, - "keybindings": [ - { - "command": "zgsm.addToContext", - "key": "ctrl+l", - "mac": "cmd+l", - "when": "editorHasSelection" - } - ] + } }, "scripts": { "lint": "eslint . --ext=ts --max-warnings=0", @@ -698,7 +700,7 @@ "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", "async-mutex": "^0.5.0", - "axios": "^1.7.4", + "axios": "^1.12.0", "cheerio": "^1.0.0", "chokidar": "^4.0.3", "clone-deep": "^4.0.1", diff --git a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts index 0353771f60..ecde769151 100644 --- a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts +++ b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts @@ -30,11 +30,26 @@ vitest.mock("../../../../i18n", () => ({ "embeddings:textExceedsTokenLimit": `Text at index ${params?.index} exceeds maximum token limit (${params?.itemTokens} > ${params?.maxTokens}). Skipping.`, "embeddings:rateLimitRetry": `Rate limit hit, retrying in ${params?.delayMs}ms (attempt ${params?.attempt}/${params?.maxRetries})`, "embeddings:unknownError": "Unknown error", + "common:errors.api.invalidKeyInvalidChars": + "API key contains invalid characters. Please check your API key for special characters.", } return translations[key] || key }, })) +// Mock i18n/setup module used by the error handler +vitest.mock("../../../../i18n/setup", () => ({ + default: { + t: (key: string) => { + const translations: Record = { + "common:errors.api.invalidKeyInvalidChars": + "API key contains invalid characters. Please check your API key for special characters.", + } + return translations[key] || key + }, + }, +})) + const MockedOpenAI = OpenAI as MockedClass describe("OpenAICompatibleEmbedder", () => { @@ -114,6 +129,22 @@ describe("OpenAICompatibleEmbedder", () => { "embeddings:validation.baseUrlRequired", ) }) + + it("should handle API key with invalid characters (ByteString conversion error)", () => { + // API key with special characters that cause ByteString conversion error + const invalidApiKey = "sk-test•invalid" // Contains bullet character (U+2022) + + // Mock the OpenAI constructor to throw ByteString error + MockedOpenAI.mockImplementationOnce(() => { + throw new Error( + "Cannot convert argument to a ByteString because the character at index 7 has a value of 8226 which is greater than 255.", + ) + }) + + expect(() => new OpenAICompatibleEmbedder(testBaseUrl, invalidApiKey, testModelId)).toThrow( + "API key contains invalid characters", + ) + }) }) describe("embedderInfo", () => { diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index 06c4ba5282..6eaf2b6c2c 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -12,6 +12,7 @@ import { withValidationErrorHandling, HttpError, formatEmbeddingError } from ".. import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Mutex } from "async-mutex" +import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler" interface EmbeddingItem { embedding: string | number[] @@ -66,10 +67,18 @@ export class OpenAICompatibleEmbedder implements IEmbedder { this.baseUrl = baseUrl this.apiKey = apiKey - this.embeddingsClient = new OpenAI({ - baseURL: baseUrl, - apiKey: apiKey, - }) + + // Wrap OpenAI client creation to handle invalid API key characters + try { + this.embeddingsClient = new OpenAI({ + baseURL: baseUrl, + apiKey: apiKey, + }) + } catch (error) { + // Use the error handler to transform ByteString conversion errors + throw handleOpenAIError(error, "OpenAI Compatible") + } + this.defaultModelId = modelId || getDefaultModelId("openai-compatible") // Cache the URL type check for performance this.isFullUrl = this.isFullEndpointUrl(baseUrl) diff --git a/src/services/code-index/embedders/openai.ts b/src/services/code-index/embedders/openai.ts index 471c3fd090..b993e280d9 100644 --- a/src/services/code-index/embedders/openai.ts +++ b/src/services/code-index/embedders/openai.ts @@ -13,6 +13,7 @@ import { t } from "../../../i18n" import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers" import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" +import { handleOpenAIError } from "../../../api/providers/utils/openai-error-handler" /** * OpenAI implementation of the embedder interface with batching and rate limiting @@ -28,7 +29,15 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder { constructor(options: ApiHandlerOptions & { openAiEmbeddingModelId?: string }) { super(options) const apiKey = this.options.openAiNativeApiKey ?? "not-provided" - this.embeddingsClient = new OpenAI({ apiKey }) + + // Wrap OpenAI client creation to handle invalid API key characters + try { + this.embeddingsClient = new OpenAI({ apiKey }) + } catch (error) { + // Use the error handler to transform ByteString conversion errors + throw handleOpenAIError(error, "OpenAI") + } + this.defaultModelId = options.openAiEmbeddingModelId || "text-embedding-3-small" } diff --git a/src/services/tree-sitter/queries/c-sharp.ts b/src/services/tree-sitter/queries/c-sharp.ts index add5ece395..46f9651b36 100644 --- a/src/services/tree-sitter/queries/c-sharp.ts +++ b/src/services/tree-sitter/queries/c-sharp.ts @@ -3,61 +3,63 @@ C# Tree-Sitter Query Patterns */ export default ` ; Using directives -(using_directive) @name.definition.using - +(using_directive) @definition.using + ; Namespace declarations (including file-scoped) +; Support both simple names (TestNamespace) and qualified names (My.Company.Module) (namespace_declaration - name: (identifier) @name.definition.namespace) + name: (qualified_name) @name) @definition.namespace +(namespace_declaration + name: (identifier) @name) @definition.namespace +(file_scoped_namespace_declaration + name: (qualified_name) @name) @definition.namespace (file_scoped_namespace_declaration - name: (identifier) @name.definition.namespace) - + name: (identifier) @name) @definition.namespace + ; Class declarations (including generic, static, abstract, partial, nested) (class_declaration - name: (identifier) @name.definition.class) - + name: (identifier) @name) @definition.class + ; Interface declarations (interface_declaration - name: (identifier) @name.definition.interface) - + name: (identifier) @name) @definition.interface + ; Struct declarations (struct_declaration - name: (identifier) @name.definition.struct) - + name: (identifier) @name) @definition.struct + ; Enum declarations (enum_declaration - name: (identifier) @name.definition.enum) - + name: (identifier) @name) @definition.enum + ; Record declarations (record_declaration - name: (identifier) @name.definition.record) - + name: (identifier) @name) @definition.record + ; Method declarations (including async, static, generic) (method_declaration - name: (identifier) @name.definition.method) - + name: (identifier) @name) @definition.method + ; Property declarations (property_declaration - name: (identifier) @name.definition.property) - + name: (identifier) @name) @definition.property + ; Event declarations (event_declaration - name: (identifier) @name.definition.event) - + name: (identifier) @name) @definition.event + ; Delegate declarations (delegate_declaration - name: (identifier) @name.definition.delegate) - + name: (identifier) @name) @definition.delegate + ; Attribute declarations -(class_declaration - (attribute_list - (attribute - name: (identifier) @name.definition.attribute))) - +(attribute + name: (identifier) @name) @definition.attribute + ; Generic type parameters -(type_parameter_list - (type_parameter - name: (identifier) @name.definition.type_parameter)) - +(type_parameter + name: (identifier) @name) @definition.type_parameter + ; LINQ expressions -(query_expression) @name.definition.linq_expression +(query_expression) @definition.linq_expression ` diff --git a/webview-ui/package.json b/webview-ui/package.json index d967c083a1..9643a6d3a3 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -35,7 +35,7 @@ "@types/qrcode": "^1.5.5", "@vscode/codicons": "^0.0.36", "@vscode/webview-ui-toolkit": "^1.4.0", - "axios": "^1.7.4", + "axios": "^1.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 522af28c7b..d9c04b6512 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -175,7 +175,9 @@ const App = () => { if (message.action === "switchTab" && message.tab) { const targetTab = message.tab as Tab switchTab(targetTab) - setCurrentSection(undefined) + // Extract targetSection from values if provided + const targetSection = message.values?.section as string | undefined + setCurrentSection(targetSection) setCurrentMarketplaceTab(undefined) } else { // Handle other actions using the mapping diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 1a3247fb16..597bd2a498 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -30,7 +30,6 @@ import { AutoApproveDropdown } from "./AutoApproveDropdown" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" // import { IndexingStatusBadge } from "./IndexingStatusBadge" -import { SlashCommandsPopover } from "./SlashCommandsPopover" import { usePromptHistory } from "./hooks/usePromptHistory" import { useHoverEvent } from "./hooks/useHoverEvent" import { MODELS_BY_PROVIDER } from "../settings/constants" @@ -293,6 +292,11 @@ export const ChatTextArea = forwardRef( const allModes = useMemo(() => getAllModes(customModes), [customModes]) + // Memoized check for whether the input has content + const hasInputContent = useMemo(() => { + return inputValue.trim().length > 0 + }, [inputValue]) + const queryItems = useMemo(() => { return [ { type: ContextMenuOptionType.Problems, value: "problems" }, @@ -964,8 +968,8 @@ export const ChatTextArea = forwardRef( return (
{selectedImages.length > 0 && ( ( className={cn( "absolute", "bottom-full", - "left-0", + isEditMode ? "left-6" : "left-0", "right-0", "z-[1000]", - "mb-2", + isEditMode ? "-mb-3" : "mb-2", "filter", "drop-shadow-md", )}> @@ -1211,12 +1215,16 @@ export const ChatTextArea = forwardRef( "relative inline-flex items-center justify-center", "bg-transparent border-none p-1.5", "rounded-md min-w-[28px] min-h-[28px]", - "opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground", - "transition-all duration-150", - "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", - "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - "active:bg-[rgba(255,255,255,0.1)]", + "text-vscode-descriptionForeground hover:text-vscode-foreground", + "transition-all duration-1000", "cursor-pointer", + hasInputContent + ? "opacity-50 hover:opacity-100 delay-750 pointer-events-auto" + : "opacity-0 pointer-events-none duration-200 delay-0", + hasInputContent && + "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", + hasInputContent && "active:bg-[rgba(255,255,255,0.1)]", )}> @@ -1254,12 +1262,16 @@ export const ChatTextArea = forwardRef( "relative inline-flex items-center justify-center", "bg-transparent border-none p-1.5", "rounded-md min-w-[28px] min-h-[28px]", - "opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground", - "transition-all duration-150", - "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", + "text-vscode-descriptionForeground hover:text-vscode-foreground", + "transition-all duration-200", + hasInputContent + ? "opacity-100 hover:opacity-100 pointer-events-auto" + : "opacity-0 pointer-events-none", + hasInputContent && + "hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]", "focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder", - "active:bg-[rgba(255,255,255,0.1)]", - "cursor-pointer", + hasInputContent && "active:bg-[rgba(255,255,255,0.1)]", + hasInputContent && "cursor-pointer", )}> @@ -1363,7 +1375,6 @@ export const ChatTextArea = forwardRef( )} - {!isEditMode ? : null} {/* {!isEditMode ? : null} */}
) case ContextMenuOptionType.Problems: - return Problems + return {t("chat:contextMenu.problems")} case ContextMenuOptionType.Terminal: - return Terminal + return {t("chat:contextMenu.terminal")} case ContextMenuOptionType.URL: - return Paste URL to fetch contents + return {t("chat:contextMenu.url")} case ContextMenuOptionType.NoResults: - return No results found + return {t("chat:contextMenu.noResults")} case ContextMenuOptionType.Git: if (option.value) { return ( @@ -251,6 +254,17 @@ const ContextMenu: React.FC = ({ ) } + const handleSettingsClick = (e: React.MouseEvent) => { + // Prevent any default behavior + e.preventDefault() + // Switch to settings tab and navigate to slash commands section + vscode.postMessage({ + type: "switchTab", + tab: "settings", + values: { section: "slashCommands" }, + }) + } + return (
= ({ overflowY: "auto", overflowX: "hidden", }}> + {/* Settings button for slash commands */} + {searchQuery === "/" && ( +
+ {searchQuery.length === 1 && ( +
+

Slash Commands

+

+ + {t("common:docsLink.label")} + + ), + }} + /> +

+
+ )} + +
+ )} {filteredOptions && filteredOptions.length > 0 ? ( filteredOptions.map((option, index) => (
= ({ onClick={() => isOptionSelectable(option) && onSelect(option.type, option.value)} style={{ padding: - option.type === ContextMenuOptionType.SectionHeader ? "8px 6px 4px 6px" : "4px 6px", + option.type === ContextMenuOptionType.SectionHeader + ? "16px 8px 4px 8px" + : "4px 8px", cursor: isOptionSelectable(option) ? "pointer" : "default", color: "var(--vscode-dropdown-foreground)", display: "flex", @@ -367,7 +430,7 @@ const ContextMenu: React.FC = ({ color: "var(--vscode-foreground)", opacity: 0.7, }}> - No results found + {t("chat:contextMenu.noResults")}
)}
diff --git a/webview-ui/src/components/chat/SlashCommandItemSimple.tsx b/webview-ui/src/components/chat/SlashCommandItemSimple.tsx new file mode 100644 index 0000000000..50a12a74f7 --- /dev/null +++ b/webview-ui/src/components/chat/SlashCommandItemSimple.tsx @@ -0,0 +1,28 @@ +import React from "react" + +import type { Command } from "@roo/ExtensionMessage" + +interface SlashCommandItemSimpleProps { + command: Command + onClick?: (command: Command) => void +} + +export const SlashCommandItemSimple: React.FC = ({ command, onClick }) => { + return ( +
onClick?.(command)}> + {/* Command name */} +
+
+ /{command.name} + {command.description && ( +
+ {command.description} +
+ )} +
+
+
+ ) +} diff --git a/webview-ui/src/components/chat/SlashCommandsList.tsx b/webview-ui/src/components/chat/SlashCommandsList.tsx deleted file mode 100644 index 51fba74d2c..0000000000 --- a/webview-ui/src/components/chat/SlashCommandsList.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import React, { useState } from "react" -import { Plus, Globe, Folder, Settings } from "lucide-react" - -import type { Command } from "@roo/ExtensionMessage" - -import { useAppTranslation } from "@/i18n/TranslationContext" -import { useExtensionState } from "@/context/ExtensionStateContext" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - Button, -} from "@/components/ui" -import { vscode } from "@/utils/vscode" - -import { SlashCommandItem } from "./SlashCommandItem" - -interface SlashCommandsListProps { - commands: Command[] - onRefresh: () => void -} - -export const SlashCommandsList: React.FC = ({ commands, onRefresh }) => { - const { t } = useAppTranslation() - const { cwd } = useExtensionState() - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) - const [commandToDelete, setCommandToDelete] = useState(null) - const [globalNewName, setGlobalNewName] = useState("") - const [workspaceNewName, setWorkspaceNewName] = useState("") - - // Check if we're in a workspace/project - const hasWorkspace = Boolean(cwd) - - const handleDeleteClick = (command: Command) => { - setCommandToDelete(command) - setDeleteDialogOpen(true) - } - - const handleDeleteConfirm = () => { - if (commandToDelete) { - vscode.postMessage({ - type: "deleteCommand", - text: commandToDelete.name, - values: { source: commandToDelete.source }, - }) - setDeleteDialogOpen(false) - setCommandToDelete(null) - // Refresh the commands list after deletion - setTimeout(onRefresh, 100) - } - } - - const handleDeleteCancel = () => { - setDeleteDialogOpen(false) - setCommandToDelete(null) - } - - const handleCreateCommand = (source: "global" | "project", name: string) => { - if (!name.trim()) return - - // Append .md if not already present - const fileName = name.trim().endsWith(".md") ? name.trim() : `${name.trim()}.md` - - vscode.postMessage({ - type: "createCommand", - text: fileName, - values: { source }, - }) - - // Clear the input and refresh - if (source === "global") { - setGlobalNewName("") - } else { - setWorkspaceNewName("") - } - setTimeout(onRefresh, 500) - } - - const handleCommandClick = (command: Command) => { - // Insert the command into the textarea - vscode.postMessage({ - type: "insertTextIntoTextarea", - text: `/${command.name}`, - }) - } - - // Group commands by source - const builtInCommands = commands.filter((cmd) => cmd.source === "built-in") - const globalCommands = commands.filter((cmd) => cmd.source === "global") - const projectCommands = commands.filter((cmd) => cmd.source === "project") - - return ( - <> - {/* Commands list */} -
-
- {/* Global Commands Section */} -
- - {t("chat:slashCommands.globalCommands")} -
- {globalCommands.map((command) => ( - - ))} - {/* New global command input */} -
- setGlobalNewName(e.target.value)} - placeholder={t("chat:slashCommands.newGlobalCommandPlaceholder")} - className="flex-1 bg-transparent text-vscode-input-foreground placeholder-vscode-input-placeholderForeground border-none outline-none focus:outline-0 text-sm" - tabIndex={-1} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleCreateCommand("global", globalNewName) - } - }} - /> - -
- - {/* Workspace Commands Section - Only show if in a workspace */} - {hasWorkspace && ( - <> -
- - {t("chat:slashCommands.workspaceCommands")} -
- {projectCommands.map((command) => ( - - ))} - {/* New workspace command input */} -
- setWorkspaceNewName(e.target.value)} - placeholder={t("chat:slashCommands.newWorkspaceCommandPlaceholder")} - className="flex-1 bg-transparent text-vscode-input-foreground placeholder-vscode-input-placeholderForeground border-none outline-none focus:outline-0 text-sm" - tabIndex={-1} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleCreateCommand("project", workspaceNewName) - } - }} - /> - -
- - )} - - {/* Built-in Commands Section */} - {builtInCommands.length > 0 && ( - <> -
- - {t("chat:slashCommands.builtInCommands")} -
- {builtInCommands.map((command) => ( - - ))} - - )} -
-
- - - - - {t("chat:slashCommands.deleteDialog.title")} - - {t("chat:slashCommands.deleteDialog.description", { name: commandToDelete?.name })} - - - - - {t("chat:slashCommands.deleteDialog.cancel")} - - - {t("chat:slashCommands.deleteDialog.confirm")} - - - - - - ) -} diff --git a/webview-ui/src/components/chat/SlashCommandsPopover.tsx b/webview-ui/src/components/chat/SlashCommandsPopover.tsx deleted file mode 100644 index 451eefede2..0000000000 --- a/webview-ui/src/components/chat/SlashCommandsPopover.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useEffect, useState } from "react" -import { Zap } from "lucide-react" -import { Trans } from "react-i18next" - -import { useAppTranslation } from "@/i18n/TranslationContext" -import { useExtensionState } from "@/context/ExtensionStateContext" -import { Button, Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" -import { useRooPortal } from "@/components/ui/hooks/useRooPortal" -import { cn } from "@/lib/utils" -import { vscode } from "@/utils/vscode" -import { buildDocLink } from "@/utils/docLinks" - -import { SlashCommandsList } from "./SlashCommandsList" - -interface SlashCommandsPopoverProps { - className?: string -} - -export const SlashCommandsPopover: React.FC = ({ className }) => { - const { t } = useAppTranslation() - const { commands } = useExtensionState() - const [isOpen, setIsOpen] = useState(false) - const portalContainer = useRooPortal("roo-portal") - - // Request commands when popover opens - useEffect(() => { - if (isOpen && (!commands || commands.length === 0)) { - handleRefresh() - } - }, [isOpen, commands]) - - const handleRefresh = () => { - vscode.postMessage({ type: "requestCommands" }) - } - - const handleOpenChange = (open: boolean) => { - setIsOpen(open) - if (open) { - // Always refresh when opening to get latest commands - handleRefresh() - } - } - - return ( - - - - - - - - -
- {/* Header section */} -
-

- - Docs - - ), - }} - /> -

-
- - {/* Commands list */} - -
-
-
- ) -} diff --git a/webview-ui/src/components/chat/__tests__/SlashCommandItemSimple.spec.tsx b/webview-ui/src/components/chat/__tests__/SlashCommandItemSimple.spec.tsx new file mode 100644 index 0000000000..7b2175950d --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/SlashCommandItemSimple.spec.tsx @@ -0,0 +1,163 @@ +import { render, screen, fireEvent } from "@/utils/test-utils" + +import type { Command } from "@roo/ExtensionMessage" + +import { SlashCommandItemSimple } from "../SlashCommandItemSimple" + +describe("SlashCommandItemSimple", () => { + const mockCommand: Command = { + name: "test-command", + description: "Test command description", + source: "global", + filePath: "/path/to/command.md", + } + + const mockOnClick = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders command name with slash prefix", () => { + render() + + expect(screen.getByText("/test-command")).toBeInTheDocument() + }) + + it("renders command description when provided", () => { + render() + + expect(screen.getByText("Test command description")).toBeInTheDocument() + }) + + it("does not render description when not provided", () => { + const commandWithoutDescription: Command = { + ...mockCommand, + description: undefined, + } + + render() + + expect(screen.queryByText("Test command description")).not.toBeInTheDocument() + }) + + it("calls onClick handler when clicked", () => { + render() + + // The outer div is the clickable element + const commandElement = screen.getByText("/test-command").closest("div.px-4") + expect(commandElement).toBeInTheDocument() + + fireEvent.click(commandElement!) + + expect(mockOnClick).toHaveBeenCalledTimes(1) + expect(mockOnClick).toHaveBeenCalledWith(mockCommand) + }) + + it("does not throw error when onClick is not provided", () => { + render() + + const commandElement = screen.getByText("/test-command").closest("div.px-4") + expect(commandElement).toBeInTheDocument() + + // Should not throw error + expect(() => fireEvent.click(commandElement!)).not.toThrow() + }) + + it("applies hover styles", () => { + render() + + // The outer div has the hover styles + const commandElement = screen.getByText("/test-command").closest("div.px-4") + expect(commandElement).toHaveClass("hover:bg-vscode-list-hoverBackground") + }) + + it("applies cursor pointer style", () => { + render() + + const commandElement = screen.getByText("/test-command").closest("div.px-4") + expect(commandElement).toHaveClass("cursor-pointer") + }) + + it("renders with correct layout classes", () => { + render() + + const commandElement = screen.getByText("/test-command").closest("div.px-4") + expect(commandElement).toHaveClass("px-4", "py-2", "text-sm", "flex", "items-center") + }) + + it("renders command name with correct text color", () => { + render() + + const nameElement = screen.getByText("/test-command") + expect(nameElement).toHaveClass("text-vscode-foreground") + }) + + it("renders description with correct text styling", () => { + render() + + const descriptionElement = screen.getByText("Test command description") + expect(descriptionElement).toHaveClass("text-xs", "text-vscode-descriptionForeground", "truncate", "mt-0.5") + }) + + it("handles built-in commands correctly", () => { + const builtInCommand: Command = { + ...mockCommand, + source: "built-in", + } + + render() + + expect(screen.getByText("/test-command")).toBeInTheDocument() + expect(screen.getByText("Test command description")).toBeInTheDocument() + + // Should still be clickable + const commandElement = screen.getByText("/test-command").closest("div.px-4") + fireEvent.click(commandElement!) + expect(mockOnClick).toHaveBeenCalledWith(builtInCommand) + }) + + it("handles project commands correctly", () => { + const projectCommand: Command = { + ...mockCommand, + source: "project", + } + + render() + + expect(screen.getByText("/test-command")).toBeInTheDocument() + expect(screen.getByText("Test command description")).toBeInTheDocument() + + // Should still be clickable + const commandElement = screen.getByText("/test-command").closest("div.px-4") + fireEvent.click(commandElement!) + expect(mockOnClick).toHaveBeenCalledWith(projectCommand) + }) + + it("truncates long command names", () => { + const longNameCommand: Command = { + ...mockCommand, + name: "this-is-a-very-long-command-name-that-should-be-truncated-in-the-ui", + } + + render() + + const nameElement = screen.getByText("/this-is-a-very-long-command-name-that-should-be-truncated-in-the-ui") + expect(nameElement).toHaveClass("truncate") + }) + + it("truncates long descriptions", () => { + const longDescriptionCommand: Command = { + ...mockCommand, + description: + "This is a very long description that should be truncated in the UI to prevent overflow and maintain a clean layout", + } + + render() + + const descriptionElement = screen.getByText( + "This is a very long description that should be truncated in the UI to prevent overflow and maintain a clean layout", + ) + expect(descriptionElement).toHaveClass("truncate") + }) +}) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 7396678407..8a3d2b3cd0 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -23,6 +23,7 @@ import { Info, MessageSquare, LucideIcon, + SquareSlash, } from "lucide-react" import { isEqual } from "lodash-es" import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types" @@ -65,6 +66,7 @@ import { LanguageSettings } from "./LanguageSettings" import { About } from "./About" import { Section } from "./Section" import PromptsSettings from "./PromptsSettings" +import { SlashCommandsSettings } from "./SlashCommandsSettings" export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden" export const settingsTabList = @@ -80,6 +82,7 @@ export interface SettingsViewRef { const sectionNames = [ "providers", "autoApprove", + "slashCommands", "browser", "checkpoints", "notifications", @@ -457,6 +460,7 @@ const SettingsView = forwardRef(({ onDone, t () => [ { id: "providers", icon: Webhook }, { id: "autoApprove", icon: CheckCheck }, + { id: "slashCommands", icon: SquareSlash }, { id: "browser", icon: SquareMousePointer }, { id: "checkpoints", icon: GitBranch }, { id: "notifications", icon: Bell }, @@ -679,6 +683,9 @@ const SettingsView = forwardRef(({ onDone, t /> )} + {/* Slash Commands Section */} + {activeTab === "slashCommands" && } + {/* Browser Section */} {activeTab === "browser" && ( { + const { t } = useAppTranslation() + const { commands, cwd } = useExtensionState() + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [commandToDelete, setCommandToDelete] = useState(null) + const [globalNewName, setGlobalNewName] = useState("") + const [workspaceNewName, setWorkspaceNewName] = useState("") + + // Check if we're in a workspace/project + const hasWorkspace = Boolean(cwd) + + // Request commands when component mounts + useEffect(() => { + handleRefresh() + }, []) + + const handleRefresh = () => { + vscode.postMessage({ type: "requestCommands" }) + } + + const handleDeleteClick = (command: Command) => { + setCommandToDelete(command) + setDeleteDialogOpen(true) + } + + const handleDeleteConfirm = () => { + if (commandToDelete) { + vscode.postMessage({ + type: "deleteCommand", + text: commandToDelete.name, + values: { source: commandToDelete.source }, + }) + setDeleteDialogOpen(false) + setCommandToDelete(null) + // Refresh the commands list after deletion + setTimeout(handleRefresh, 100) + } + } + + const handleDeleteCancel = () => { + setDeleteDialogOpen(false) + setCommandToDelete(null) + } + + const handleCreateCommand = (source: "global" | "project", name: string) => { + if (!name.trim()) return + + // Append .md if not already present + const fileName = name.trim().endsWith(".md") ? name.trim() : `${name.trim()}.md` + + vscode.postMessage({ + type: "createCommand", + text: fileName, + values: { source }, + }) + + // Clear the input and refresh + if (source === "global") { + setGlobalNewName("") + } else { + setWorkspaceNewName("") + } + setTimeout(handleRefresh, 500) + } + + const handleCommandClick = (command: Command) => { + // For now, we'll just show the command name - editing functionality can be added later + // This could be enhanced to open the command file in the editor + console.log(`Command clicked: ${command.name} (${command.source})`) + } + + // Group commands by source + const builtInCommands = commands?.filter((cmd) => cmd.source === "built-in") || [] + const globalCommands = commands?.filter((cmd) => cmd.source === "global") || [] + const projectCommands = commands?.filter((cmd) => cmd.source === "project") || [] + + return ( +
+ +
+ +
{t("settings:sections.slashCommands")}
+
+
+ +
+ {/* Description section */} +
+

+ + Docs + + ), + }} + /> +

+
+ + {/* Global Commands Section */} +
+
+ +

{t("chat:slashCommands.globalCommands")}

+
+
+ {globalCommands.map((command) => ( + + ))} + {/* New global command input */} +
+ setGlobalNewName(e.target.value)} + placeholder={t("chat:slashCommands.newGlobalCommandPlaceholder")} + className="flex-1 bg-vscode-input-background text-vscode-input-foreground placeholder-vscode-input-placeholderForeground border border-vscode-input-border rounded px-2 py-1 text-sm focus:outline-none focus:border-vscode-focusBorder" + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateCommand("global", globalNewName) + } + }} + /> + +
+
+
+ + {/* Workspace Commands Section - Only show if in a workspace */} + {hasWorkspace && ( +
+
+ +

{t("chat:slashCommands.workspaceCommands")}

+
+
+ {projectCommands.map((command) => ( + + ))} + {/* New workspace command input */} +
+ setWorkspaceNewName(e.target.value)} + placeholder={t("chat:slashCommands.newWorkspaceCommandPlaceholder")} + className="flex-1 bg-vscode-input-background text-vscode-input-foreground placeholder-vscode-input-placeholderForeground border border-vscode-input-border rounded px-2 py-1 text-sm focus:outline-none focus:border-vscode-focusBorder" + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateCommand("project", workspaceNewName) + } + }} + /> + +
+
+
+ )} + + {/* Built-in Commands Section */} + {builtInCommands.length > 0 && ( +
+
+ +

{t("chat:slashCommands.builtInCommands")}

+
+
+ {builtInCommands.map((command) => ( + + ))} +
+
+ )} +
+ + + + + {t("chat:slashCommands.deleteDialog.title")} + + {t("chat:slashCommands.deleteDialog.description", { name: commandToDelete?.name })} + + + + + {t("chat:slashCommands.deleteDialog.cancel")} + + + {t("chat:slashCommands.deleteDialog.confirm")} + + + + +
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx new file mode 100644 index 0000000000..05ec7b9fd1 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/SlashCommandsSettings.spec.tsx @@ -0,0 +1,525 @@ +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" + +import type { Command } from "@roo/ExtensionMessage" + +import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext" +import { vscode } from "@/utils/vscode" + +import { SlashCommandsSettings } from "../SlashCommandsSettings" + +// Mock vscode +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock the translation hook +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string, params?: any) => { + if (params?.name) { + return `${key} ${params.name}` + } + return key + }, + }), +})) + +// Mock the doc links utility +vi.mock("@/utils/docLinks", () => ({ + buildDocLink: (path: string, anchor?: string) => `https://docs.example.com/${path}${anchor ? `#${anchor}` : ""}`, +})) + +// Mock UI components +vi.mock("@/components/ui", () => ({ + AlertDialog: ({ children, open }: any) => ( +
+ {open && children} +
+ ), + AlertDialogContent: ({ children }: any) =>
{children}
, + AlertDialogHeader: ({ children }: any) =>
{children}
, + AlertDialogTitle: ({ children }: any) =>
{children}
, + AlertDialogDescription: ({ children }: any) =>
{children}
, + AlertDialogFooter: ({ children }: any) =>
{children}
, + AlertDialogAction: ({ children, onClick }: any) => ( + + ), + AlertDialogCancel: ({ children, onClick }: any) => ( + + ), + Button: ({ children, onClick, disabled, className, variant, size, tabIndex }: any) => ( + + ), + StandardTooltip: ({ children, content }: any) => ( +
+ {children} +
+ ), +})) + +// Mock SlashCommandItem component - we need to handle the built-in check +vi.mock("../../chat/SlashCommandItem", () => ({ + SlashCommandItem: ({ command, onDelete, onClick }: any) => ( +
+ {command.name} + {command.description && {command.description}} + {command.source !== "built-in" && ( + + )} + +
+ ), +})) + +// Mock SectionHeader and Section components +vi.mock("../SectionHeader", () => ({ + SectionHeader: ({ children }: any) =>
{children}
, +})) + +vi.mock("../Section", () => ({ + Section: ({ children }: any) =>
{children}
, +})) + +const mockCommands: Command[] = [ + { + name: "built-in-command", + description: "A built-in command", + source: "built-in", + }, + { + name: "global-command", + description: "A global command", + source: "global", + filePath: "/path/to/global.md", + }, + { + name: "project-command", + description: "A project command", + source: "project", + filePath: "/path/to/project.md", + }, +] + +// Create a variable to hold the mock state +let mockExtensionState: any = {} + +// Mock the useExtensionState hook +vi.mock("@/context/ExtensionStateContext", () => ({ + ExtensionStateContextProvider: ({ children }: any) => children, + useExtensionState: () => mockExtensionState, +})) + +const renderSlashCommandsSettings = (commands: Command[] = mockCommands, cwd?: string) => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) + + // Update the mock state before rendering + mockExtensionState = { + commands, + cwd: cwd || "/workspace", + } + + return render( + + + + + , + ) +} + +describe("SlashCommandsSettings", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders section header with icon and title", () => { + renderSlashCommandsSettings() + + expect(screen.getByTestId("section-header")).toBeInTheDocument() + expect(screen.getByText("settings:sections.slashCommands")).toBeInTheDocument() + }) + + it("renders description with documentation link", () => { + renderSlashCommandsSettings() + + // The Trans component doesn't render the link in our mock, so we just check for the description + const description = screen.getByText((_content, element) => { + return element?.className === "text-sm text-vscode-descriptionForeground" + }) + expect(description).toBeInTheDocument() + }) + + it("requests commands on mount", () => { + renderSlashCommandsSettings() + + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "requestCommands" }) + }) + + it("displays built-in commands in their own section", () => { + renderSlashCommandsSettings() + + expect(screen.getByText("chat:slashCommands.builtInCommands")).toBeInTheDocument() + expect(screen.getByTestId("command-item-built-in-command")).toBeInTheDocument() + }) + + it("displays global commands in their own section", () => { + renderSlashCommandsSettings() + + expect(screen.getByText("chat:slashCommands.globalCommands")).toBeInTheDocument() + expect(screen.getByTestId("command-item-global-command")).toBeInTheDocument() + }) + + it("displays project commands when in a workspace", () => { + renderSlashCommandsSettings() + + expect(screen.getByText("chat:slashCommands.workspaceCommands")).toBeInTheDocument() + expect(screen.getByTestId("command-item-project-command")).toBeInTheDocument() + }) + + it("does not display project commands when not in a workspace", () => { + // Pass empty string for cwd to simulate no workspace + // The component checks Boolean(cwd) which is false for empty string + // However, it seems the component still renders the section but without commands + const commandsWithoutProject = mockCommands.filter((cmd) => cmd.source !== "project") + renderSlashCommandsSettings(commandsWithoutProject, "") + + // Project commands should not be shown + expect(screen.queryByTestId("command-item-project-command")).not.toBeInTheDocument() + + // The section might still be rendered but should be empty of project commands + // This is acceptable behavior as it allows users to add project commands even without a workspace + }) + + it("shows input field for creating new global command", () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText("chat:slashCommands.newGlobalCommandPlaceholder")[0] + expect(input).toBeInTheDocument() + }) + + it("shows input field for creating new workspace command when in workspace", () => { + renderSlashCommandsSettings() + + const input = screen.getByPlaceholderText("chat:slashCommands.newWorkspaceCommandPlaceholder") + expect(input).toBeInTheDocument() + }) + + it("creates new global command when entering name and clicking add button", async () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + const addButton = screen.getAllByTestId("button")[0] + + fireEvent.change(input, { target: { value: "new-command" } }) + fireEvent.click(addButton) + + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "createCommand", + text: "new-command.md", + values: { source: "global" }, + }) + }) + + expect(input.value).toBe("") + }) + + it("creates new workspace command when entering name and clicking add button", async () => { + renderSlashCommandsSettings() + + const input = screen.getByPlaceholderText( + "chat:slashCommands.newWorkspaceCommandPlaceholder", + ) as HTMLInputElement + const addButtons = screen.getAllByTestId("button") + const workspaceAddButton = addButtons[1] // Second add button is for workspace + + fireEvent.change(input, { target: { value: "workspace-command" } }) + fireEvent.click(workspaceAddButton) + + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "createCommand", + text: "workspace-command.md", + values: { source: "project" }, + }) + }) + + expect(input.value).toBe("") + }) + + it("appends .md extension if not present when creating command", async () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + const addButton = screen.getAllByTestId("button")[0] + + fireEvent.change(input, { target: { value: "command-without-extension" } }) + fireEvent.click(addButton) + + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "createCommand", + text: "command-without-extension.md", + values: { source: "global" }, + }) + }) + }) + + it("does not double-append .md extension if already present", async () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + const addButton = screen.getAllByTestId("button")[0] + + fireEvent.change(input, { target: { value: "command-with-extension.md" } }) + fireEvent.click(addButton) + + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "createCommand", + text: "command-with-extension.md", + values: { source: "global" }, + }) + }) + }) + + it("creates command on Enter key press", async () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + + fireEvent.change(input, { target: { value: "enter-command" } }) + fireEvent.keyDown(input, { key: "Enter" }) + + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "createCommand", + text: "enter-command.md", + values: { source: "global" }, + }) + }) + }) + + it("disables add button when input is empty", () => { + renderSlashCommandsSettings() + + const addButton = screen.getAllByTestId("button")[0] + expect(addButton).toBeDisabled() + }) + + it("enables add button when input has value", () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + const addButton = screen.getAllByTestId("button")[0] + + fireEvent.change(input, { target: { value: "test" } }) + expect(addButton).not.toBeDisabled() + }) + + it("opens delete confirmation dialog when delete button is clicked", () => { + renderSlashCommandsSettings() + + const deleteButton = screen.getByTestId("delete-global-command") + fireEvent.click(deleteButton) + + expect(screen.getByTestId("alert-dialog")).toHaveAttribute("data-open", "true") + expect(screen.getByText("chat:slashCommands.deleteDialog.title")).toBeInTheDocument() + expect(screen.getByText("chat:slashCommands.deleteDialog.description global-command")).toBeInTheDocument() + }) + + it("deletes command when confirmation is clicked", async () => { + renderSlashCommandsSettings() + + const deleteButton = screen.getByTestId("delete-global-command") + fireEvent.click(deleteButton) + + const confirmButton = screen.getByTestId("alert-dialog-action") + fireEvent.click(confirmButton) + + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "deleteCommand", + text: "global-command", + values: { source: "global" }, + }) + }) + + expect(screen.getByTestId("alert-dialog")).toHaveAttribute("data-open", "false") + }) + + it("cancels deletion when cancel is clicked", () => { + renderSlashCommandsSettings() + + const deleteButton = screen.getByTestId("delete-global-command") + fireEvent.click(deleteButton) + + const cancelButton = screen.getByTestId("alert-dialog-cancel") + fireEvent.click(cancelButton) + + expect(screen.getByTestId("alert-dialog")).toHaveAttribute("data-open", "false") + expect(vscode.postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "deleteCommand", + }), + ) + }) + + it("refreshes commands after deletion", async () => { + renderSlashCommandsSettings() + + const deleteButton = screen.getByTestId("delete-global-command") + fireEvent.click(deleteButton) + + const confirmButton = screen.getByTestId("alert-dialog-action") + fireEvent.click(confirmButton) + + // Wait for the setTimeout to execute + await waitFor( + () => { + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "requestCommands" }) + }, + { timeout: 200 }, + ) + }) + + it("refreshes commands after creating new command", async () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + const addButton = screen.getAllByTestId("button")[0] + + fireEvent.change(input, { target: { value: "new-command" } }) + fireEvent.click(addButton) + + // Wait for the setTimeout to execute + await waitFor( + () => { + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "requestCommands" }) + }, + { timeout: 600 }, + ) + }) + + it("handles command click event", () => { + renderSlashCommandsSettings() + + const commandButton = screen.getByTestId("click-global-command") + fireEvent.click(commandButton) + + // The current implementation just logs to console + // In a real scenario, this might open the command file for editing + expect(commandButton).toBeInTheDocument() + }) + + it("does not show delete button for built-in commands", () => { + renderSlashCommandsSettings() + + // The SlashCommandItem component handles this internally + // We're just verifying the command is rendered + expect(screen.getByTestId("command-item-built-in-command")).toBeInTheDocument() + }) + + it("trims whitespace from command names", async () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + const addButton = screen.getAllByTestId("button")[0] + + fireEvent.change(input, { target: { value: " trimmed-command " } }) + fireEvent.click(addButton) + + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "createCommand", + text: "trimmed-command.md", + values: { source: "global" }, + }) + }) + }) + + it("does not create command with empty name after trimming", () => { + renderSlashCommandsSettings() + + const input = screen.getAllByPlaceholderText( + "chat:slashCommands.newGlobalCommandPlaceholder", + )[0] as HTMLInputElement + const addButton = screen.getAllByTestId("button")[0] + + fireEvent.change(input, { target: { value: " " } }) + + expect(addButton).toBeDisabled() + }) + + it("renders empty state when no commands exist", () => { + renderSlashCommandsSettings([]) + + // Should still show the input fields for creating new commands + expect(screen.getAllByPlaceholderText("chat:slashCommands.newGlobalCommandPlaceholder")[0]).toBeInTheDocument() + }) + + it("handles multiple commands of the same type", () => { + const multipleCommands: Command[] = [ + { + name: "global-1", + description: "First global", + source: "global", + }, + { + name: "global-2", + description: "Second global", + source: "global", + }, + { + name: "global-3", + description: "Third global", + source: "global", + }, + ] + + renderSlashCommandsSettings(multipleCommands) + + expect(screen.getByTestId("command-item-global-1")).toBeInTheDocument() + expect(screen.getByTestId("command-item-global-2")).toBeInTheDocument() + expect(screen.getByTestId("command-item-global-3")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/settings/providers/ZAi.tsx b/webview-ui/src/components/settings/providers/ZAi.tsx index bc23f28346..c7f44510c1 100644 --- a/webview-ui/src/components/settings/providers/ZAi.tsx +++ b/webview-ui/src/components/settings/providers/ZAi.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react" import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" -import type { ProviderSettings } from "@roo-code/types" +import { zaiApiLineConfigs, zaiApiLineSchema, type ProviderSettings } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" @@ -33,15 +33,17 @@ export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) =>
- - api.z.ai - - - open.bigmodel.cn - + {zaiApiLineSchema.options.map((zaiApiLine) => { + const config = zaiApiLineConfigs[zaiApiLine] + return ( + + {config.name} ({config.baseUrl}) + + ) + })}
{t("settings:providers.zaiEntrypointDescription")} @@ -62,7 +64,7 @@ export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) => {!apiConfiguration?.zaiApiKey && ( Docs", + "manageCommands": "Manage slash commands in settings", "builtInCommands": "Built-in Commands", "globalCommands": "Global Commands", "workspaceCommands": "Workspace Commands", @@ -392,5 +393,11 @@ "queuedMessages": { "title": "Queued Messages:", "clickToEdit": "Click to edit message" + }, + "contextMenu": { + "noResults": "No results", + "problems": "Problems", + "terminal": "Terminal", + "url": "Paste URL to fetch contents" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 746d9d4789..3839fc1565 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -28,11 +28,15 @@ "notifications": "Notifications", "contextManagement": "Context", "terminal": "Terminal", + "slashCommands": "Slash Commands", "prompts": "Prompts", "experimental": "Experimental", "language": "Language", "about": "About Roo Code" }, + "slashCommands": { + "description": "Manage your slash commands to quickly execute custom workflows and actions. Learn more" + }, "prompts": { "description": "Configure support prompts that are used for quick actions like enhancing prompts, explaining code, and fixing issues. These prompts help Roo provide better assistance for common development tasks." }, diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 79353a32db..e8374c76ec 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -375,6 +375,7 @@ "tooltip": "管理斜杠命令", "title": "斜杠命令", "description": "使用内置斜杠命令或创建自定义命令,快速访问常用提示词和工作流程。文档", + "manageCommands": "在设置中管理斜杠命令", "builtInCommands": "内置命令", "globalCommands": "全局命令", "workspaceCommands": "工作区命令", @@ -390,6 +391,12 @@ "confirm": "删除" } }, + "contextMenu": { + "noResults": "无结果", + "problems": "问题", + "terminal": "终端", + "url": "粘贴URL以获取内容" + }, "queuedMessages": { "title": "队列消息:", "clickToEdit": "点击编辑消息" diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index c78d5e7d0c..23ec6ac242 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -28,11 +28,15 @@ "notifications": "通知", "contextManagement": "上下文", "terminal": "终端", + "slashCommands": "斜杠命令", "prompts": "提示词", "experimental": "实验性", "language": "语言", "about": "关于 Roo Code" }, + "slashCommands": { + "description": "管理您的斜杠命令,以快速执行自定义工作流和操作。 了解更多" + }, "prompts": { "description": "配置用于快速操作的支持提示词,如增强提示词、解释代码和修复问题。这些提示词帮助 Roo 为常见开发任务提供更好的支持。" }, diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 0805ff3f0b..30043f2144 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -375,6 +375,7 @@ "tooltip": "管理斜線命令", "title": "斜線命令", "description": "使用內建斜線命令或建立自訂命令,以便快速存取常用的提示詞和工作流程。說明文件", + "manageCommands": "在設定中管理斜線命令", "builtInCommands": "內建命令", "globalCommands": "全域命令", "workspaceCommands": "工作區命令", @@ -390,6 +391,12 @@ "confirm": "刪除" } }, + "contextMenu": { + "noResults": "沒有結果", + "problems": "問題", + "terminal": "終端機", + "url": "貼上 URL 以擷取內容" + }, "queuedMessages": { "title": "佇列中的訊息:", "clickToEdit": "點選以編輯訊息" diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index daadecb4aa..b494ce39f8 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -28,11 +28,15 @@ "notifications": "通知", "contextManagement": "上下文", "terminal": "終端機", + "slashCommands": "斜線命令", "prompts": "提示詞", "experimental": "實驗性", "language": "語言", "about": "關於 Roo Code" }, + "slashCommands": { + "description": "管理您的斜線命令,以便快速執行自訂工作流程和動作。 了解更多" + }, "prompts": { "description": "設定用於快速操作的支援提示詞,如增強提示詞、解釋程式碼和修復問題。這些提示詞幫助 Roo 為常見開發工作提供更好的支援。" }, diff --git a/webview-ui/src/utils/docLinks.ts b/webview-ui/src/utils/docLinks.ts index 2ace42def7..ce9fe297d8 100644 --- a/webview-ui/src/utils/docLinks.ts +++ b/webview-ui/src/utils/docLinks.ts @@ -6,6 +6,7 @@ * @returns The full docs URL with UTM parameters */ export function buildDocLink(path: string, campaign: string): string { + // todo: add docs path return "https://costrict.ai" // Remove any leading slash from path const cleanPath = path.replace(/^\//, "")