mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Search for buttons, visual tweaks
This commit is contained in:
parent
60cb90230d
commit
07ab2c1295
28 changed files with 271 additions and 161 deletions
|
|
@ -35,7 +35,7 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
|
|||
hideAnnouncement()
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-96">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("chat:announcement.title", { version: Package.version })}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -88,8 +88,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
variant="ghost"
|
||||
className="px-1.5 -ml-2"
|
||||
onClick={onDone}
|
||||
aria-label={t("settings:back")}>
|
||||
aria-label={t("history:done")}
|
||||
data-testid="history-done-button">
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">{t("history:done")}</span>
|
||||
</Button>
|
||||
<h3 className="text-vscode-foreground m-0">{t("history:history")}</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -128,12 +128,12 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace
|
|||
/>
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
|
||||
className="cursor-pointer flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
|
||||
onClick={() => manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: "mcp" } })}>
|
||||
MCP
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
|
||||
className="cursor-pointer flex items-center justify-center gap-2 flex-1 text-sm font-medium rounded-sm transition-colors duration-300 relative z-10 text-vscode-foreground"
|
||||
onClick={() =>
|
||||
manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: "mode" } })
|
||||
}>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="border border-vscode-panel-border rounded-sm p-3 bg-vscode-editor-background">
|
||||
<div className="border border-vscode-panel-border rounded-xl cursor-default p-3 transition-colors bg-vscode-editor-background hover:bg-vscode-editor-foreground/5">
|
||||
<div className="flex gap-2 items-start justify-between">
|
||||
<div className="flex gap-2 items-start">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
VSCodePanelTab,
|
||||
VSCodePanelView,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { Webhook } from "lucide-react"
|
||||
|
||||
import { McpServer } from "@roo/mcp"
|
||||
|
||||
|
|
@ -49,7 +48,6 @@ const McpView = () => {
|
|||
<div>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Webhook className="w-4" />
|
||||
<div>{t("mcp:title")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { ChevronDown, X, Upload, Download, MessageSquare } from "lucide-react"
|
||||
import { ChevronDown, X, Upload, Download } from "lucide-react"
|
||||
|
||||
import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types"
|
||||
|
||||
|
|
@ -29,7 +29,6 @@ import { buildDocLink } from "@src/utils/docLinks"
|
|||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { Section } from "@src/components/settings/Section"
|
||||
import { SectionHeader } from "@src/components/settings/SectionHeader"
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
|
|
@ -593,16 +592,9 @@ const ModesView = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="w-4" />
|
||||
<div>{t("prompts:title")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div>
|
||||
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
|
||||
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mt-1 mb-3">
|
||||
<h3 className="text-vscode-foreground m-0">{t("prompts:modes.title")}</h3>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative inline-block">
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { Trans } from "react-i18next"
|
||||
import {
|
||||
Info,
|
||||
Download,
|
||||
Upload,
|
||||
TriangleAlert,
|
||||
Bug,
|
||||
Lightbulb,
|
||||
Shield,
|
||||
MessageCircle,
|
||||
MessagesSquare,
|
||||
} from "lucide-react"
|
||||
import { Download, Upload, TriangleAlert, Bug, Lightbulb, Shield, MessageCircle, MessagesSquare } from "lucide-react"
|
||||
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { TelemetrySetting } from "@roo-code/types"
|
||||
|
|
@ -35,19 +25,14 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro
|
|||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)} {...props}>
|
||||
<SectionHeader
|
||||
description={
|
||||
Package.sha
|
||||
? `Version: ${Package.version} (${Package.sha.slice(0, 8)})`
|
||||
: `Version: ${Package.version}`
|
||||
}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="w-4" />
|
||||
<div>{t("settings:sections.about")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.about")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<p>
|
||||
{Package.sha
|
||||
? `Version: ${Package.version} (${Package.sha.slice(0, 8)})`
|
||||
: `Version: ${Package.version}`}
|
||||
</p>
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={telemetrySetting !== "disabled"}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HTMLAttributes, useState } from "react"
|
||||
import { X, CheckCheck } from "lucide-react"
|
||||
import { X } from "lucide-react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { Package } from "@roo/package"
|
||||
|
||||
|
|
@ -107,12 +107,7 @@ export const AutoApproveSettings = ({
|
|||
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCheck className="w-4 h-4" />
|
||||
<div>{t("settings:sections.autoApprove")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.autoApprove")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { VSCodeCheckbox, VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { SquareMousePointer } from "lucide-react"
|
||||
import { HTMLAttributes, useEffect, useMemo, useState } from "react"
|
||||
import { Trans } from "react-i18next"
|
||||
|
||||
|
|
@ -108,12 +107,7 @@ export const BrowserSettings = ({
|
|||
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<SquareMousePointer className="w-4" />
|
||||
<div>{t("settings:sections.browser")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.browser")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div data-setting-id="browser.enable">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { GitBranch } from "lucide-react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { buildDocLink } from "@src/utils/docLinks"
|
||||
import { Slider } from "@/components/ui"
|
||||
|
|
@ -30,12 +29,7 @@ export const CheckpointSettings = ({
|
|||
const { t } = useAppTranslation()
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="w-4" />
|
||||
<div>{t("settings:sections.checkpoints")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.checkpoints")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div data-setting-id="checkpoints.enable">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { HTMLAttributes } from "react"
|
|||
import React from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Database, FoldVertical } from "lucide-react"
|
||||
import { FoldVertical } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider, Button } from "@/components/ui"
|
||||
|
|
@ -106,14 +106,12 @@ export const ContextManagementSettings = ({
|
|||
}
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)} {...props}>
|
||||
<SectionHeader description={t("settings:contextManagement.description")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="w-4" />
|
||||
<div>{t("settings:sections.contextManagement")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.contextManagement")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<p className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:contextManagement.description")}
|
||||
</p>
|
||||
<div data-setting-id="contextManagement.openTabs">
|
||||
<span className="block font-medium mb-1">{t("settings:contextManagement.openTabs.label")}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { FlaskConical } from "lucide-react"
|
||||
|
||||
import type { Experiments, ImageGenerationProvider } from "@roo-code/types"
|
||||
|
||||
|
|
@ -46,12 +45,7 @@ export const ExperimentalSettings = ({
|
|||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)} {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<FlaskConical className="w-4" />
|
||||
<div>{t("settings:sections.experimental")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.experimental")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
{Object.entries(experimentConfigsMap)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { Globe } from "lucide-react"
|
||||
|
||||
import type { Language } from "@roo-code/types"
|
||||
|
||||
|
|
@ -23,12 +22,7 @@ export const LanguageSettings = ({ language, setCachedStateField, className, ...
|
|||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)} {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4" />
|
||||
<div>{t("settings:sections.language")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.language")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div data-setting-id="language">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Bell } from "lucide-react"
|
||||
|
||||
import { SetCachedStateField } from "./types"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
|
|
@ -27,12 +26,7 @@ export const NotificationSettings = ({
|
|||
const { t } = useAppTranslation()
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="w-4" />
|
||||
<div>{t("settings:sections.notifications")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.notifications")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div data-setting-id="notifications.tts">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useEffect, FormEvent } from "react"
|
||||
import { VSCodeTextArea, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { MessageSquare } from "lucide-react"
|
||||
|
||||
import { supportPrompt, SupportPromptType } from "@roo/support-prompt"
|
||||
|
||||
|
|
@ -137,14 +136,10 @@ const PromptsSettings = ({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader description={t("settings:prompts.description")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="w-4" />
|
||||
<div>{t("settings:sections.prompts")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.prompts")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<p className="text-sm text-vscode-descriptionForeground">{t("settings:prompts.description")}</p>
|
||||
<div>
|
||||
<Select
|
||||
value={activeSupportOption}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,9 @@ type SectionHeaderProps = HTMLAttributes<HTMLDivElement> & {
|
|||
export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"sticky top-0 z-10 text-vscode-sideBar-foreground bg-vscode-sideBar-background brightness-90 px-5 py-4",
|
||||
className,
|
||||
)}
|
||||
className={cn("sticky top-0 z-10 text-vscode-foreground bg-vscode-sideBar-background px-5 py-4", className)}
|
||||
{...props}>
|
||||
<h4 className="m-0">{children}</h4>
|
||||
<h3 className="m-0">{children}</h3>
|
||||
{description && <p className="text-vscode-descriptionForeground text-sm mt-2 mb-0">{description}</p>}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -78,14 +78,14 @@ export function SettingsSearchResults({
|
|||
// If no results, show a message
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<div className="max-h-80 overflow-y-auto p-4 bg-vscode-sideBar-background text-vscode-descriptionForeground text-sm">
|
||||
<div className="max-h-80 overflow-y-auto p-4 text-vscode-descriptionForeground text-sm">
|
||||
{t("settings:search.noResults", { query })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-h-80 overflow-y-auto bg-vscode-sideBar-background" role="listbox">
|
||||
<div className="max-h-80 overflow-y-auto" role="listbox">
|
||||
{Object.entries(groupedResults).map(([tab, tabResults]) => {
|
||||
const Icon = sectionIconMap.get(tab as SectionName)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import React, {
|
|||
import {
|
||||
CheckCheck,
|
||||
SquareMousePointer,
|
||||
Webhook,
|
||||
GitBranch,
|
||||
Bell,
|
||||
Database,
|
||||
|
|
@ -675,6 +674,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
<StandardTooltip content={t("settings:header.doneButtonTooltip")}>
|
||||
<Button variant="ghost" className="px-1.5 -ml-2" onClick={() => checkUnsaveChanges(onDone)}>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">{t("settings:common.done")}</span>
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
<h3 className="text-vscode-foreground m-0 flex-shrink-0">{t("settings:header.title")}</h3>
|
||||
|
|
@ -689,7 +689,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
inputRef={searchInputRef}
|
||||
/>
|
||||
{searchQuery && isSearchFocused && (
|
||||
<div className="absolute top-full w-full min-w-50 right-0 mt-1 bg-vscode-dropdown-background border border-vscode-dropdown-border rounded shadow-lg z-50">
|
||||
<div className="absolute top-full w-full min-w-50 right-0 mt-1 border border-vscode-dropdown-border bg-vscode-sideBar-background rounded-xl overflow-clip shadow-lg z-50">
|
||||
<SettingsSearchResults
|
||||
results={searchResults}
|
||||
query={searchQuery}
|
||||
|
|
@ -784,12 +784,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
{/* Providers Section */}
|
||||
{activeTab === "providers" && (
|
||||
<div>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Webhook className="w-4" />
|
||||
<div>{t("settings:sections.providers")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.providers")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<ApiConfigManager
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { Plus, Globe, Folder, Settings, SquareSlash } from "lucide-react"
|
||||
import { Plus, Globe, Folder, Settings } from "lucide-react"
|
||||
import { Trans } from "react-i18next"
|
||||
|
||||
import type { Command } from "@roo/ExtensionMessage"
|
||||
|
|
@ -102,12 +102,7 @@ export const SlashCommandsSettings: React.FC = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<SquareSlash className="w-4" />
|
||||
<div>{t("settings:sections.slashCommands")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.slashCommands")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
{/* Description section */}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { HTMLAttributes, useState, useCallback } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { SquareTerminal } from "lucide-react"
|
||||
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { buildDocLink } from "@src/utils/docLinks"
|
||||
|
|
@ -87,12 +86,7 @@ export const TerminalSettings = ({
|
|||
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)} {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<SquareTerminal className="w-4" />
|
||||
<div>{t("settings:sections.terminal")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.terminal")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
{/* Basic Settings */}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { HTMLAttributes, useMemo } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Glasses } from "lucide-react"
|
||||
import { telemetryClient } from "@/utils/TelemetryClient"
|
||||
|
||||
import { SetCachedStateField } from "./types"
|
||||
|
|
@ -50,12 +49,7 @@ export const UISettings = ({
|
|||
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Glasses className="w-4" />
|
||||
<div>{t("settings:sections.ui")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
<SectionHeader>{t("settings:sections.ui")}</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div className="space-y-6">
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ describe("SettingsSearchResults", () => {
|
|||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.queryAllByRole("button")
|
||||
expect(buttons).toHaveLength(0)
|
||||
const options = screen.queryAllByRole("option")
|
||||
expect(options).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -254,8 +254,8 @@ describe("SettingsSearchResults", () => {
|
|||
/>,
|
||||
)
|
||||
|
||||
// Label should be present - check that button exists
|
||||
const checkpointButton = screen.getByRole("button")
|
||||
// Label should be present - check that option exists
|
||||
const checkpointButton = screen.getByRole("option")
|
||||
expect(checkpointButton).toBeInTheDocument()
|
||||
expect(checkpointButton.textContent).toContain("Checkpoint timeout")
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ describe("SettingsSearchResults", () => {
|
|||
expect(descriptionElements).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should render results as clickable buttons", () => {
|
||||
it("should render results as clickable listbox options", () => {
|
||||
const onSelectResult = vi.fn()
|
||||
render(
|
||||
<SettingsSearchResults
|
||||
|
|
@ -275,8 +275,8 @@ describe("SettingsSearchResults", () => {
|
|||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole("button")
|
||||
expect(buttons.length).toBe(mockBrowserResults.length)
|
||||
const options = screen.getAllByRole("option")
|
||||
expect(options.length).toBe(mockBrowserResults.length)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -292,9 +292,9 @@ describe("SettingsSearchResults", () => {
|
|||
/>,
|
||||
)
|
||||
|
||||
// Click the first button (the result item itself is a button)
|
||||
const buttons = screen.getAllByRole("button")
|
||||
fireEvent.click(buttons[0])
|
||||
// Click the first option (the result item itself is a button with role option)
|
||||
const options = screen.getAllByRole("option")
|
||||
fireEvent.click(options[0])
|
||||
|
||||
expect(onSelectResult).toHaveBeenCalledTimes(1)
|
||||
expect(onSelectResult).toHaveBeenCalledWith(mockBrowserResults[0])
|
||||
|
|
@ -311,14 +311,14 @@ describe("SettingsSearchResults", () => {
|
|||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole("button")
|
||||
const options = screen.getAllByRole("option")
|
||||
|
||||
// Click first result
|
||||
fireEvent.click(buttons[0])
|
||||
fireEvent.click(options[0])
|
||||
expect(onSelectResult).toHaveBeenLastCalledWith(mockBrowserResults[0])
|
||||
|
||||
// Click second result
|
||||
fireEvent.click(buttons[1])
|
||||
fireEvent.click(options[1])
|
||||
expect(onSelectResult).toHaveBeenLastCalledWith(mockBrowserResults[1])
|
||||
|
||||
expect(onSelectResult).toHaveBeenCalledTimes(2)
|
||||
|
|
@ -429,8 +429,8 @@ describe("SettingsSearchResults", () => {
|
|||
expect(screen.getByText("Checkpoint timeout")).toBeInTheDocument()
|
||||
|
||||
// Should have correct number of clickable results
|
||||
const buttons = screen.getAllByRole("button")
|
||||
expect(buttons).toHaveLength(allResults.length)
|
||||
const options = screen.getAllByRole("option")
|
||||
expect(options).toHaveLength(allResults.length)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function AlertDialogContent({ className, ...props }: React.ComponentProps<typeof
|
|||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-vscode-editor-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-3 rounded-sm border border-vscode-panel-border p-4 shadow-lg duration-200 sm:max-w-md",
|
||||
"bg-vscode-editor-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-4rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-4 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -51,11 +51,7 @@ function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">)
|
|||
|
||||
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn("flex flex-row justify-end gap-2 mt-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div data-slot="alert-dialog-footer" className={cn("flex flex-row justify-end gap-2", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -63,10 +59,7 @@ function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof A
|
|||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn(
|
||||
"text-base font-medium text-vscode-editor-foreground flex items-center gap-2 text-left",
|
||||
className,
|
||||
)}
|
||||
className={cn("text-[1.25em] leading-none font-semibold mt-0.5 mb-1.5 cursor-default", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
@ -79,7 +72,7 @@ function AlertDialogDescription({
|
|||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-vscode-descriptionForeground text-sm text-left", className)}
|
||||
className={cn("text-vscode-descriptionForeground text-md text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function DialogContent({ className, children, ...props }: React.ComponentProps<t
|
|||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-vscode-editor-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
"bg-vscode-editor-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-4rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-4 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
|
|
@ -78,7 +78,7 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
|
|||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold my-0 cursor-default", className)}
|
||||
className={cn("text-[1.25em] leading-none font-semibold mt-0.5 mb-1.5 cursor-default", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ vi.mock("@/i18n/locales/en/settings.json", () => ({
|
|||
label: "Browser viewport",
|
||||
description: "Configure the browser window size",
|
||||
},
|
||||
remote: {
|
||||
label: "Use remote browser",
|
||||
testButton: "Test Connection",
|
||||
},
|
||||
},
|
||||
notifications: {
|
||||
sound: {
|
||||
|
|
@ -42,6 +46,13 @@ vi.mock("@/i18n/locales/en/settings.json", () => ({
|
|||
label: "Checkpoint timeout",
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
settings: {
|
||||
import: "Import settings",
|
||||
export: "Export settings",
|
||||
reset: "Reset settings",
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
|
|
@ -57,9 +68,14 @@ describe("useSettingsSearch", () => {
|
|||
"settings:browser.enable.description": "Allows Roo to use a browser",
|
||||
"settings:browser.viewport.label": "Browser viewport",
|
||||
"settings:browser.viewport.description": "Configure the browser window size",
|
||||
"settings:browser.remote.label": "Use remote browser",
|
||||
"settings:browser.remote.testButton": "Test Connection",
|
||||
"settings:notifications.sound.label": "Sound effects",
|
||||
"settings:notifications.sound.description": "Play sound when Roo needs attention",
|
||||
"settings:checkpoints.timeout.label": "Checkpoint timeout",
|
||||
"settings:footer.settings.import": "Import settings",
|
||||
"settings:footer.settings.export": "Export settings",
|
||||
"settings:footer.settings.reset": "Reset settings",
|
||||
}
|
||||
|
||||
const mockT = (key: string) => mockTranslations[key] || key
|
||||
|
|
@ -208,6 +224,23 @@ describe("useSettingsSearch", () => {
|
|||
}
|
||||
})
|
||||
|
||||
it("should calculate matchScore when matching extra text like buttons", () => {
|
||||
const { result } = renderHook(() => useSettingsSearch("test connection"))
|
||||
|
||||
const buttonResult = result.current.find((r) => r.id === "browser.remote")
|
||||
expect(buttonResult).toBeDefined()
|
||||
expect(buttonResult?.matchScore).toBe(4) // extra match only
|
||||
})
|
||||
|
||||
it("should return standalone footer strings with label match score", () => {
|
||||
const { result } = renderHook(() => useSettingsSearch("export"))
|
||||
|
||||
const exportResult = result.current.find((r) => r.id === "footer.settings.export")
|
||||
expect(exportResult).toBeDefined()
|
||||
expect(exportResult?.tab).toBe("about")
|
||||
expect(exportResult?.matchScore).toBe(10)
|
||||
})
|
||||
|
||||
it("should have higher matchScore for label+description than description only", () => {
|
||||
const { result } = renderHook(() => useSettingsSearch("browser"))
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export interface SearchResult extends ParsedSetting {
|
|||
translatedLabel: string
|
||||
/** Translated description for the setting (if available) */
|
||||
translatedDescription?: string
|
||||
/** Translated extra texts (e.g., button labels) for the setting */
|
||||
translatedExtraTexts?: string[]
|
||||
/** Match score for sorting results (higher = better match) */
|
||||
matchScore: number
|
||||
}
|
||||
|
|
@ -52,15 +54,19 @@ export function useSettingsSearch(query: string): SearchResult[] {
|
|||
const translatedLabel = t(setting.labelKey)
|
||||
// Get translated description if it exists
|
||||
const translatedDescription = setting.descriptionKey ? t(setting.descriptionKey) : undefined
|
||||
// Get translated extra texts (e.g., button labels) if they exist
|
||||
const translatedExtraTexts = setting.extraTextKeys?.map((key) => t(key)).filter(Boolean)
|
||||
|
||||
// Check for matches (case-insensitive)
|
||||
const labelMatch = translatedLabel.toLowerCase().includes(normalizedQuery)
|
||||
const descriptionMatch = translatedDescription
|
||||
? translatedDescription.toLowerCase().includes(normalizedQuery)
|
||||
: false
|
||||
const extraMatch =
|
||||
translatedExtraTexts?.some((text) => text.toLowerCase().includes(normalizedQuery)) ?? false
|
||||
|
||||
// If no match, return null
|
||||
if (!labelMatch && !descriptionMatch) {
|
||||
if (!labelMatch && !descriptionMatch && !extraMatch) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -72,11 +78,15 @@ export function useSettingsSearch(query: string): SearchResult[] {
|
|||
if (descriptionMatch) {
|
||||
matchScore += 5
|
||||
}
|
||||
if (extraMatch) {
|
||||
matchScore += 4
|
||||
}
|
||||
|
||||
return {
|
||||
...setting,
|
||||
translatedLabel,
|
||||
translatedDescription,
|
||||
translatedExtraTexts,
|
||||
matchScore,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -91,6 +91,64 @@ describe("parseSettingsI18nKeys", () => {
|
|||
descriptionKey: "settings:autoApprove.readOnly.outsideWorkspace.description",
|
||||
})
|
||||
})
|
||||
|
||||
it("should collect extra searchable text keys like button labels", () => {
|
||||
const translations = {
|
||||
browser: {
|
||||
remote: {
|
||||
label: "Use remote browser",
|
||||
testButton: "Test Connection",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const results = parseSettingsI18nKeys(translations)
|
||||
|
||||
expect(results).toContainEqual({
|
||||
id: "browser.remote",
|
||||
tab: "browser",
|
||||
labelKey: "settings:browser.remote.label",
|
||||
descriptionKey: undefined,
|
||||
extraTextKeys: ["settings:browser.remote.testButton"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should create standalone entries for footer settings string leaves in about tab", () => {
|
||||
const translations = {
|
||||
footer: {
|
||||
settings: {
|
||||
import: "Import settings",
|
||||
export: "Export settings",
|
||||
reset: "Reset settings",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const results = parseSettingsI18nKeys(translations)
|
||||
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
id: "footer.settings.import",
|
||||
tab: "about",
|
||||
labelKey: "settings:footer.settings.import",
|
||||
descriptionKey: undefined,
|
||||
},
|
||||
{
|
||||
id: "footer.settings.export",
|
||||
tab: "about",
|
||||
labelKey: "settings:footer.settings.export",
|
||||
descriptionKey: undefined,
|
||||
},
|
||||
{
|
||||
id: "footer.settings.reset",
|
||||
tab: "about",
|
||||
labelKey: "settings:footer.settings.reset",
|
||||
descriptionKey: undefined,
|
||||
},
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("special tab entries", () => {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export interface ParsedSetting {
|
|||
labelKey: string
|
||||
/** i18n key for the description (optional), e.g., 'settings:browser.enable.description' */
|
||||
descriptionKey?: string
|
||||
/** Additional i18n keys within the same setting (e.g., button labels) to include in search */
|
||||
extraTextKeys?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,6 +119,7 @@ const sectionToTabMapping: Record<string, SectionName | undefined> = {
|
|||
// Additional mappings for nested sections that should map to specific tabs
|
||||
advanced: "providers", // advanced settings are part of providers tab
|
||||
codeIndex: "experimental", // codebase indexing is in experimental
|
||||
footer: "about", // footer controls live in About tab
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -138,6 +141,87 @@ function isSettingObject(obj: Record<string, unknown>): boolean {
|
|||
return typeof obj.label === "string"
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects additional string keys within a setting that should be searchable (e.g., button labels).
|
||||
* Skips nested setting objects (those with their own labels) so keys stay scoped to the current setting.
|
||||
*/
|
||||
function collectSearchableTextKeys(obj: Record<string, unknown>, path: string[], namespace: string): string[] {
|
||||
const collected: string[] = []
|
||||
|
||||
const walk = (current: Record<string, unknown>, currentPath: string[]) => {
|
||||
for (const [key, value] of Object.entries(current)) {
|
||||
if (key === "label" || key === "description") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
// If this nested object represents its own setting, don't collect from it here
|
||||
if (isSettingObject(value)) {
|
||||
continue
|
||||
}
|
||||
walk(value, [...currentPath, key])
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
collected.push(`${namespace}:${[...currentPath, key].join(".")}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(obj, path)
|
||||
return collected
|
||||
}
|
||||
|
||||
/**
|
||||
* Roots that should generate standalone searchable entries for string leaves, even when they are
|
||||
* not full "settings" objects (i.e., they lack a label/description). This helps surface buttons
|
||||
* like Import/Export/Reset in the About tab's footer controls.
|
||||
*/
|
||||
const standaloneStringRoots: Record<string, SectionName> = {
|
||||
"footer.settings": "about",
|
||||
}
|
||||
|
||||
function collectStandaloneStringEntries(
|
||||
obj: unknown,
|
||||
basePath: string[],
|
||||
namespace: string,
|
||||
tab: SectionName,
|
||||
existingIds: Set<string>,
|
||||
results: ParsedSetting[],
|
||||
): void {
|
||||
const walk = (value: unknown, currentPath: string[]) => {
|
||||
if (typeof value === "string") {
|
||||
const id = currentPath.join(".")
|
||||
if (!existingIds.has(id)) {
|
||||
results.push({
|
||||
id,
|
||||
tab,
|
||||
labelKey: `${namespace}:${id}`,
|
||||
descriptionKey: undefined,
|
||||
})
|
||||
existingIds.add(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!isPlainObject(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't recurse into nested setting objects to avoid duplicating their own entries
|
||||
if (isSettingObject(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
walk(child, [...currentPath, key])
|
||||
}
|
||||
}
|
||||
|
||||
walk(obj, basePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tab for a given section name.
|
||||
*/
|
||||
|
|
@ -193,6 +277,10 @@ function traverseTranslations(
|
|||
tab,
|
||||
labelKey,
|
||||
descriptionKey,
|
||||
...(() => {
|
||||
const keys = collectSearchableTextKeys(obj, path, namespace)
|
||||
return keys.length ? { extraTextKeys: keys } : {}
|
||||
})(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +355,25 @@ export function parseSettingsI18nKeys(
|
|||
traverseTranslations(sectionValue, [sectionKey], namespace, results, sectionKey)
|
||||
}
|
||||
|
||||
// Collect tabs that already have settings from parsing
|
||||
// Add standalone string leaves for specific roots (e.g., footer.settings.import/export/reset)
|
||||
const existingIds = new Set(results.map((r) => r.id))
|
||||
for (const [rootPath, tab] of Object.entries(standaloneStringRoots)) {
|
||||
const parts = rootPath.split(".")
|
||||
let current: unknown = translations
|
||||
for (const part of parts) {
|
||||
if (!isPlainObject(current) || !(part in current)) {
|
||||
current = undefined
|
||||
break
|
||||
}
|
||||
current = (current as Record<string, unknown>)[part]
|
||||
}
|
||||
|
||||
if (current !== undefined) {
|
||||
collectStandaloneStringEntries(current, parts, namespace, tab, existingIds, results)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect tabs that already have settings from parsing (including standalone entries)
|
||||
const tabsWithSettings = new Set(results.map((r) => r.tab))
|
||||
|
||||
// Add special tab entries for tabs that don't have any parsed settings
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue