Skip to content

Commit 0532b98

Browse files
authored
feat(chat): add ModeSwitch component to manage display modes (#798)
1 parent 7097298 commit 0532b98

File tree

3 files changed

+115
-56
lines changed

3 files changed

+115
-56
lines changed

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { ProviderSettings } from "@roo-code/types"
3939
import ProviderRenderer from "../settings/ProviderRenderer"
4040
import { RouterModels } from "@roo/api"
4141
// import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
42+
import { ModeSwitch } from "./ModeSwitch"
4243

4344
interface ChatTextAreaProps {
4445
inputValue: string
@@ -1108,7 +1109,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
11081109
? "border-2 border-dashed border-vscode-focusBorder"
11091110
: "border border-transparent",
11101111
"pl-2",
1111-
"py-2",
1112+
"py-8.5",
11121113
isEditMode ? "pr-20" : "pr-9",
11131114
"z-10",
11141115
"pb-[32px]",
@@ -1213,7 +1214,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12131214
"text-vscode-editor-font-size",
12141215
"leading-vscode-editor-line-height",
12151216
"cursor-text",
1216-
"py-2 pl-2",
1217+
"py-8.5 pl-2",
12171218
isFocused
12181219
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
12191220
: isDraggingOver
@@ -1421,6 +1422,11 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
14211422
<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
14221423
</div>
14231424
</div>
1425+
1426+
{/* ModeSwitch positioned at the top left of the input area */}
1427+
<div className="absolute top-2 left-4 z-30">
1428+
<ModeSwitch />
1429+
</div>
14241430
</div>
14251431
)
14261432
},

webview-ui/src/components/chat/ModeSelector.tsx

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Check, X } from "lucide-react"
44

55
import { type ModeConfig, type CustomModePrompts, TelemetryEventName } from "@roo-code/types"
66

7-
import { type Mode, ZgsmCodeMode, filterModesByZgsmCodeMode, getAllModes } from "@roo/modes"
7+
import { type Mode, filterModesByZgsmCodeMode, getAllModes } from "@roo/modes"
88

99
import { vscode } from "@/utils/vscode"
1010
import { telemetryClient } from "@/utils/TelemetryClient"
@@ -13,7 +13,6 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
1313
import { useAppTranslation } from "@/i18n/TranslationContext"
1414
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
1515
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
16-
import { SelectDropdown } from "@/components/ui/select-dropdown"
1716

1817
import { IconButton } from "./IconButton"
1918

@@ -48,46 +47,8 @@ export const ModeSelector = ({
4847
const selectedItemRef = React.useRef<HTMLDivElement>(null)
4948
const scrollContainerRef = React.useRef<HTMLDivElement>(null)
5049
const portalContainer = useRooPortal("roo-portal")
51-
const { hasOpenedModeSelector, setHasOpenedModeSelector, zgsmCodeMode, setZgsmCodeMode, apiConfiguration } =
52-
useExtensionState()
50+
const { hasOpenedModeSelector, setHasOpenedModeSelector, zgsmCodeMode, apiConfiguration } = useExtensionState()
5351
const { t } = useAppTranslation()
54-
const handleCodeModeChange = React.useCallback(
55-
(newMode: string) => {
56-
if (apiConfiguration?.apiProvider !== "zgsm") {
57-
vscode.postMessage({
58-
type: "zgsmProviderTip",
59-
values: {
60-
tipType: "info",
61-
msg: t("settings:codebase.general.onlyCostrictProviderSupport"),
62-
},
63-
})
64-
return
65-
}
66-
67-
const slug = newMode as ZgsmCodeMode
68-
setZgsmCodeMode(slug)
69-
vscode.postMessage({
70-
type: "zgsmCodeMode",
71-
text: slug,
72-
})
73-
74-
// Map the mode to the appropriate mode slug
75-
let modeSlug: string
76-
if (slug === "vibe") {
77-
modeSlug = "code"
78-
} else if (slug === "plan") {
79-
modeSlug = "plan"
80-
} else {
81-
modeSlug = "strict"
82-
}
83-
84-
vscode.postMessage({
85-
type: "mode",
86-
text: modeSlug,
87-
})
88-
},
89-
[apiConfiguration?.apiProvider, setZgsmCodeMode, t],
90-
)
9152
const trackModeSelectorOpened = React.useCallback(() => {
9253
// Track telemetry every time the mode selector is opened.
9354
telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED)
@@ -364,19 +325,6 @@ export const ModeSelector = ({
364325
setOpen(false)
365326
}}
366327
/>
367-
<SelectDropdown
368-
value={zgsmCodeMode || "vibe"}
369-
options={[
370-
{ value: "vibe", label: "Vibe" },
371-
{ value: "strict", label: "Strict" },
372-
{ value: "plan", label: "Plan" },
373-
]}
374-
onChange={handleCodeModeChange}
375-
disabled={apiConfiguration?.apiProvider !== "zgsm"}
376-
triggerClassName="h-7 text-xs"
377-
disableSearch={true}
378-
placeholder="Select mode"
379-
/>
380328
</div>
381329

382330
{/* Info icon and title on the right - only show info icon when search bar is visible */}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useExtensionState } from "@/context/ExtensionStateContext"
2+
import styled from "styled-components"
3+
import { cn } from "@src/lib/utils"
4+
import { type ExtensionState } from "@roo/ExtensionMessage"
5+
import { StandardTooltip } from "@src/components/ui"
6+
import { useTranslation } from "react-i18next"
7+
import { vscode } from "@/utils/vscode"
8+
import { type ZgsmCodeMode } from "@roo/modes"
9+
10+
const mapDisplayToOriginal = (displayMode: "vibe" | "plan" | "spec"): string => {
11+
if (displayMode === "vibe") return "vibe"
12+
if (displayMode === "plan") return "plan"
13+
if (displayMode === "spec") return "strict"
14+
return displayMode
15+
}
16+
17+
const mapModeToDisplay = (mode: ExtensionState["zgsmCodeMode"]): "vibe" | "plan" | "spec" => {
18+
if (mode === "vibe") return "vibe"
19+
if (mode === "plan") return "plan"
20+
if (mode === "strict") return "spec"
21+
return mode as "vibe" | "plan" | "spec"
22+
}
23+
24+
const SwitchContainer = styled.div<{ disabled: boolean }>`
25+
display: flex;
26+
align-items: center;
27+
background-color: transparent;
28+
border: 1px solid var(--vscode-input-border);
29+
border-radius: 12px;
30+
overflow: hidden;
31+
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
32+
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
33+
transform: scale(1);
34+
transform-origin: right center;
35+
margin-left: 0;
36+
user-select: none;
37+
`
38+
39+
const Slider = styled.div.withConfig({
40+
shouldForwardProp: (prop) => !["isVibe", "isPlan", "isSpec"].includes(prop),
41+
})<{ isVibe: boolean; isPlan?: boolean; isSpec?: boolean }>`
42+
position: absolute;
43+
height: 100%;
44+
width: 33.33%;
45+
background-color: var(--vscode-focusBorder);
46+
transition: transform 0.2s ease;
47+
transform: translateX(${(props) => (props.isVibe ? "0%" : props.isSpec ? "200%" : "100%")});
48+
`
49+
50+
export const ModeSwitch = () => {
51+
const { zgsmCodeMode, setZgsmCodeMode } = useExtensionState()
52+
const displayMode = mapModeToDisplay(zgsmCodeMode)
53+
const { t } = useTranslation("welcome")
54+
55+
const handleModeClick = (selectedMode: "vibe" | "plan" | "spec", forceMode?: string) => {
56+
const originalMode = mapDisplayToOriginal(selectedMode)
57+
setZgsmCodeMode(originalMode as ZgsmCodeMode)
58+
59+
vscode.postMessage({
60+
type: "zgsmCodeMode",
61+
text: originalMode,
62+
})
63+
64+
vscode.postMessage({
65+
type: "mode",
66+
text: forceMode || (originalMode === "vibe" ? "code" : "strict"),
67+
})
68+
}
69+
70+
const getModeTip = (mode: string) => {
71+
if (mode === "vibe") {
72+
return t("vibe.description")
73+
} else if (mode === "spec") {
74+
return t("strict.description")
75+
} else if (mode === "plan") {
76+
return t("plan.description")
77+
}
78+
return ""
79+
}
80+
81+
return (
82+
<SwitchContainer data-testid="mode-switch" disabled={false}>
83+
<Slider isVibe={displayMode === "vibe"} isPlan={displayMode === "plan"} isSpec={displayMode === "spec"} />
84+
{["Vibe", "Plan", "Spec"].map((m) => (
85+
<StandardTooltip content={getModeTip(m.toLowerCase())} key={m}>
86+
<div
87+
aria-checked={displayMode === m.toLowerCase()}
88+
className={cn(
89+
"pt-0.5 pb-px px-2 z-10 text-xs w-1/3 text-center bg-transparent cursor-pointer",
90+
displayMode === m.toLowerCase() ? "text-white" : "text-input-foreground",
91+
)}
92+
onClick={() =>
93+
handleModeClick(
94+
m.toLowerCase() as "vibe" | "plan" | "spec",
95+
m === "Plan" ? "plan" : undefined,
96+
)
97+
}
98+
role="switch">
99+
{m}
100+
</div>
101+
</StandardTooltip>
102+
))}
103+
</SwitchContainer>
104+
)
105+
}

0 commit comments

Comments
 (0)