Skip to content

Commit e68076e

Browse files
committed
feat: Allow scheduling model switches during execution
- Modified ApiConfigSelector to remain interactive when disabled - Added scheduledConfigId and onScheduleChange props - Implemented visual indicators (clock icons) for scheduled switches - Added state management in ChatTextArea for tracking scheduled config - Automatically applies scheduled switch when request completes - Added comprehensive unit tests for the new functionality - Added translation keys for new UI elements Fixes #8334
1 parent 5e218fe commit e68076e

File tree

7 files changed

+236
-384
lines changed

7 files changed

+236
-384
lines changed

.review/pr-8274

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit e46929b8d8add0cd3c412d69f8ac882c405a4ba9

tmp/pr-8287-Roo-Code

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 88a473b017af37091c85ce3056e444e856f80d6e

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

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface ApiConfigSelectorProps {
2020
listApiConfigMeta: Array<{ id: string; name: string; modelId?: string }>
2121
pinnedApiConfigs?: Record<string, boolean>
2222
togglePinnedApiConfig: (id: string) => void
23+
scheduledConfigId?: string
24+
onScheduleChange?: (configId: string | undefined) => void
2325
}
2426

2527
export const ApiConfigSelector = ({
@@ -32,6 +34,8 @@ export const ApiConfigSelector = ({
3234
listApiConfigMeta,
3335
pinnedApiConfigs,
3436
togglePinnedApiConfig,
37+
scheduledConfigId,
38+
onScheduleChange,
3539
}: ApiConfigSelectorProps) => {
3640
const { t } = useAppTranslation()
3741
const [open, setOpen] = useState(false)
@@ -73,11 +77,23 @@ export const ApiConfigSelector = ({
7377

7478
const handleSelect = useCallback(
7579
(configId: string) => {
76-
onChange(configId)
80+
if (disabled && onScheduleChange) {
81+
// When disabled, schedule the change instead of applying immediately
82+
if (scheduledConfigId === configId) {
83+
// If clicking the same config, cancel the scheduled change
84+
onScheduleChange(undefined)
85+
} else {
86+
// Schedule the new config
87+
onScheduleChange(configId)
88+
}
89+
} else {
90+
// Apply immediately when not disabled
91+
onChange(configId)
92+
}
7793
setOpen(false)
7894
setSearchValue("")
7995
},
80-
[onChange],
96+
[disabled, onChange, onScheduleChange, scheduledConfigId],
8197
)
8298

8399
const handleEditClick = useCallback(() => {
@@ -88,6 +104,7 @@ export const ApiConfigSelector = ({
88104
const renderConfigItem = useCallback(
89105
(config: { id: string; name: string; modelId?: string }, isPinned: boolean) => {
90106
const isCurrentConfig = config.id === value
107+
const isScheduledConfig = config.id === scheduledConfigId
91108

92109
return (
93110
<div
@@ -98,6 +115,7 @@ export const ApiConfigSelector = ({
98115
"hover:bg-vscode-list-hoverBackground",
99116
isCurrentConfig &&
100117
"bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground",
118+
isScheduledConfig && !isCurrentConfig && "border-l-2 border-vscode-focusBorder",
101119
)}>
102120
<div className="flex-1 min-w-0 flex items-center gap-1 overflow-hidden">
103121
<span className="flex-shrink-0">{config.name}</span>
@@ -112,11 +130,18 @@ export const ApiConfigSelector = ({
112130
)}
113131
</div>
114132
<div className="flex items-center gap-1">
115-
{isCurrentConfig && (
133+
{isCurrentConfig && !isScheduledConfig && (
116134
<div className="size-5 p-1 flex items-center justify-center">
117135
<span className="codicon codicon-check text-xs" />
118136
</div>
119137
)}
138+
{isScheduledConfig && (
139+
<StandardTooltip content={disabled ? t("chat:scheduledSwitch") : t("chat:nextModel")}>
140+
<div className="size-5 p-1 flex items-center justify-center">
141+
<span className="codicon codicon-clock text-xs text-vscode-focusBorder" />
142+
</div>
143+
</StandardTooltip>
144+
)}
120145
<StandardTooltip content={isPinned ? t("chat:unpin") : t("chat:pin")}>
121146
<Button
122147
variant="ghost"
@@ -138,25 +163,40 @@ export const ApiConfigSelector = ({
138163
</div>
139164
)
140165
},
141-
[value, handleSelect, t, togglePinnedApiConfig],
166+
[value, scheduledConfigId, handleSelect, t, togglePinnedApiConfig, disabled],
142167
)
143168

169+
// Get the scheduled config's display name
170+
const scheduledConfigName = useMemo(() => {
171+
if (!scheduledConfigId) return null
172+
const config = listApiConfigMeta.find((c) => c.id === scheduledConfigId)
173+
return config?.name || null
174+
}, [scheduledConfigId, listApiConfigMeta])
175+
144176
return (
145177
<Popover open={open} onOpenChange={setOpen} data-testid="api-config-selector-root">
146-
<StandardTooltip content={title}>
178+
<StandardTooltip
179+
content={scheduledConfigName && disabled ? `${title} (Scheduled: ${scheduledConfigName})` : title}>
147180
<PopoverTrigger
148-
disabled={disabled}
181+
disabled={false} // Always allow opening the dropdown
149182
data-testid="dropdown-trigger"
150183
className={cn(
151184
"min-w-0 inline-flex items-center relative whitespace-nowrap px-1.5 py-1 text-xs",
152-
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
185+
"bg-transparent border rounded-md text-vscode-foreground",
153186
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
154187
disabled
155-
? "opacity-50 cursor-not-allowed"
156-
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
188+
? scheduledConfigId
189+
? "border-vscode-focusBorder opacity-70 cursor-pointer hover:opacity-90"
190+
: "border-[rgba(255,255,255,0.08)] opacity-50 cursor-pointer hover:opacity-70"
191+
: "border-[rgba(255,255,255,0.08)] opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
157192
triggerClassName,
158193
)}>
159-
<span className="truncate">{displayName}</span>
194+
<span className="truncate flex items-center gap-1">
195+
{displayName}
196+
{scheduledConfigId && disabled && (
197+
<span className="codicon codicon-clock text-xs text-vscode-focusBorder" />
198+
)}
199+
</span>
160200
</PopoverTrigger>
161201
</StandardTooltip>
162202
<PopoverContent
@@ -188,7 +228,11 @@ export const ApiConfigSelector = ({
188228
) : (
189229
<div className="p-3 border-b border-vscode-dropdown-border">
190230
<p className="text-xs text-vscode-descriptionForeground m-0">
191-
{t("prompts:apiConfiguration.select")}
231+
{disabled && scheduledConfigId
232+
? t("prompts:apiConfiguration.selectToCancel")
233+
: disabled
234+
? t("prompts:apiConfiguration.selectToSchedule")
235+
: t("prompts:apiConfiguration.select")}
192236
</p>
193237
</div>
194238
)}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
5858
{
5959
inputValue,
6060
setInputValue,
61+
sendingDisabled,
6162
selectApiConfigDisabled,
6263
placeholderText,
6364
selectedImages,
@@ -91,6 +92,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
9192
cloudUserInfo,
9293
} = useExtensionState()
9394

95+
// State for scheduled model switch
96+
const [scheduledConfigId, setScheduledConfigId] = useState<string | undefined>(undefined)
97+
9498
// Find the ID and display text for the currently selected API configuration.
9599
const { currentConfigId, displayName } = useMemo(() => {
96100
const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
@@ -907,6 +911,21 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
907911
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
908912
}, [])
909913

914+
// Handle scheduled config change
915+
const handleScheduledConfigChange = useCallback((configId: string | undefined) => {
916+
setScheduledConfigId(configId)
917+
// We'll apply the change when sendingDisabled becomes false
918+
}, [])
919+
920+
// Effect to apply scheduled config change when sending is re-enabled
921+
useEffect(() => {
922+
if (!sendingDisabled && scheduledConfigId) {
923+
// Apply the scheduled change
924+
handleApiConfigChange(scheduledConfigId)
925+
setScheduledConfigId(undefined)
926+
}
927+
}, [sendingDisabled, scheduledConfigId, handleApiConfigChange])
928+
910929
return (
911930
<div
912931
className={cn(
@@ -1231,6 +1250,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12311250
listApiConfigMeta={listApiConfigMeta || []}
12321251
pinnedApiConfigs={pinnedApiConfigs}
12331252
togglePinnedApiConfig={togglePinnedApiConfig}
1253+
scheduledConfigId={scheduledConfigId}
1254+
onScheduleChange={handleScheduledConfigChange}
12341255
/>
12351256
<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
12361257
</div>

0 commit comments

Comments
 (0)