Skip to content

Commit d86f15d

Browse files
committed
code review
1 parent 2a79f7c commit d86f15d

File tree

8 files changed

+91
-17
lines changed

8 files changed

+91
-17
lines changed

packages/types/src/followup.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { z } from "zod"
2+
3+
/**
4+
* Interface for follow-up data structure used in follow-up questions
5+
* This represents the data structure for follow-up questions that the LLM can ask
6+
* to gather more information needed to complete a task.
7+
*/
8+
export interface FollowUpData {
9+
/** The question being asked by the LLM */
10+
question?: string
11+
/** Array of suggested answers that the user can select */
12+
suggest?: Array<string | SuggestionItem>
13+
}
14+
15+
/**
16+
* Interface for a suggestion item with optional mode switching
17+
*/
18+
export interface SuggestionItem {
19+
/** The text of the suggestion */
20+
answer: string
21+
/** Optional mode to switch to when selecting this suggestion */
22+
mode?: string
23+
}
24+
25+
/**
26+
* Zod schema for SuggestionItem
27+
*/
28+
export const suggestionItemSchema = z.object({
29+
answer: z.string(),
30+
mode: z.string().optional(),
31+
})
32+
33+
/**
34+
* Zod schema for FollowUpData
35+
*/
36+
export const followUpDataSchema = z.object({
37+
question: z.string().optional(),
38+
suggest: z.array(z.union([z.string(), suggestionItemSchema])).optional(),
39+
})
40+
41+
export type FollowUpDataType = z.infer<typeof followUpDataSchema>

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from "./api.js"
44
export * from "./codebase-index.js"
55
export * from "./cloud.js"
66
export * from "./experiment.js"
7+
export * from "./followup.js"
78
export * from "./global-settings.js"
89
export * from "./history.js"
910
export * from "./ipc.js"

src/core/webview/ClineProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,7 +1524,7 @@ export class ClineProvider
15241524
cloudApiUrl: getRooCodeApiUrl(),
15251525
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
15261526
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
1527-
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 10000,
1527+
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
15281528
}
15291529
}
15301530

@@ -1606,7 +1606,7 @@ export class ClineProvider
16061606
alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
16071607
alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
16081608
alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false,
1609-
followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 10000,
1609+
followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000,
16101610
allowedMaxRequests: stateValues.allowedMaxRequests,
16111611
autoCondenseContext: stateValues.autoCondenseContext ?? true,
16121612
autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ClineMessage } from "@roo-code/types"
1010
import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage"
1111
import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
1212
import { safeJsonParse } from "@roo/safeJsonParse"
13+
import { FollowUpData, SuggestionItem } from "@roo-code/types"
1314

1415
import { useCopyToClipboard } from "@src/utils/clipboard"
1516
import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -50,6 +51,7 @@ interface ChatRowProps {
5051
onHeightChange: (isTaller: boolean) => void
5152
onSuggestionClick?: (answer: string, event?: React.MouseEvent) => void
5253
onBatchFileResponse?: (response: { [key: string]: boolean }) => void
54+
onFollowUpUnmount?: () => void
5355
}
5456

5557
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
@@ -98,6 +100,7 @@ export const ChatRowContent = ({
98100
isStreaming,
99101
onToggleExpand,
100102
onSuggestionClick,
103+
onFollowUpUnmount,
101104
onBatchFileResponse,
102105
}: ChatRowContentProps) => {
103106
const { t } = useTranslation()
@@ -279,7 +282,7 @@ export const ChatRowContent = ({
279282

280283
const followUpData = useMemo(() => {
281284
if (message.type === "ask" && message.ask === "followup" && !message.partial) {
282-
return safeJsonParse<any>(message.text)
285+
return safeJsonParse<FollowUpData>(message.text)
283286
}
284287
return null
285288
}, [message.type, message.ask, message.partial, message.text])
@@ -1215,6 +1218,7 @@ export const ChatRowContent = ({
12151218
suggestions={followUpData?.suggest}
12161219
onSuggestionClick={onSuggestionClick}
12171220
ts={message?.ts}
1221+
onUnmount={onFollowUpUnmount}
12181222
/>
12191223
</>
12201224
)

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { ClineAsk, ClineMessage } from "@roo-code/types"
1515
import { ClineSayBrowserAction, ClineSayTool, ExtensionMessage } from "@roo/ExtensionMessage"
1616
import { McpServer, McpTool } from "@roo/mcp"
1717
import { findLast } from "@roo/array"
18+
import { FollowUpData } from "@roo-code/types"
1819
import { combineApiRequests } from "@roo/combineApiRequests"
1920
import { combineCommandSequences } from "@roo/combineCommandSequences"
2021
import { getApiMetrics } from "@roo/getApiMetrics"
@@ -1215,6 +1216,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12151216
vscode.postMessage({ type: "askResponse", askResponse: "objectResponse", text: JSON.stringify(response) })
12161217
}, [])
12171218

1219+
// Handler for when FollowUpSuggest component unmounts
1220+
const handleFollowUpUnmount = useCallback(() => {
1221+
// Clear the auto-approve timeout to prevent race conditions
1222+
if (autoApproveTimeoutRef.current) {
1223+
clearTimeout(autoApproveTimeoutRef.current)
1224+
autoApproveTimeoutRef.current = null
1225+
}
1226+
}, [])
1227+
12181228
const itemContent = useCallback(
12191229
(index: number, messageOrGroup: ClineMessage | ClineMessage[]) => {
12201230
// browser session group
@@ -1250,6 +1260,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12501260
isStreaming={isStreaming}
12511261
onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
12521262
onBatchFileResponse={handleBatchFileResponse}
1263+
onFollowUpUnmount={handleFollowUpUnmount}
12531264
/>
12541265
)
12551266
},
@@ -1262,6 +1273,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12621273
isStreaming,
12631274
handleSuggestionClickInRow,
12641275
handleBatchFileResponse,
1276+
handleFollowUpUnmount,
12651277
],
12661278
)
12671279

@@ -1279,7 +1291,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12791291
if (lastMessage?.ask && isAutoApproved(lastMessage)) {
12801292
// Special handling for follow-up questions
12811293
if (lastMessage.ask === "followup") {
1282-
const followUpData = JSON.parse(lastMessage.text || "{}")
1294+
// Handle invalid JSON
1295+
let followUpData: FollowUpData = {}
1296+
try {
1297+
followUpData = JSON.parse(lastMessage.text || "{}") as FollowUpData
1298+
} catch (error) {
1299+
console.error("Failed to parse follow-up data:", error)
1300+
return
1301+
}
1302+
12831303
if (followUpData && followUpData.suggest && followUpData.suggest.length > 0) {
12841304
// Wait for the configured timeout before auto-selecting the first suggestion
12851305
await new Promise<void>((resolve) => {

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ import { vscode } from "@/utils/vscode"
66

77
import { useAppTranslation } from "@src/i18n/TranslationContext"
88
import { useExtensionState } from "@src/context/ExtensionStateContext"
9+
import { SuggestionItem } from "@roo-code/types"
910

10-
interface SuggestionItem {
11-
answer: string
12-
mode?: string
13-
}
11+
const DEFAULT_FOLLOWUP_TIMEOUT_MS = 60000
12+
const COUNTDOWN_INTERVAL_MS = 1000
1413

1514
interface FollowUpSuggestProps {
1615
suggestions?: (string | SuggestionItem)[]
1716
onSuggestionClick?: (answer: string, event?: React.MouseEvent) => void
1817
ts: number
18+
onUnmount?: () => void
1919
}
2020

21-
export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }: FollowUpSuggestProps) => {
21+
export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, onUnmount }: FollowUpSuggestProps) => {
2222
const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState()
2323
const [countdown, setCountdown] = useState<number | null>(null)
2424
const [suggestionSelected, setSuggestionSelected] = useState(false)
@@ -32,7 +32,7 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }:
3232
const timeoutMs =
3333
typeof followupAutoApproveTimeoutMs === "number" && !isNaN(followupAutoApproveTimeoutMs)
3434
? followupAutoApproveTimeoutMs
35-
: 10000
35+
: DEFAULT_FOLLOWUP_TIMEOUT_MS
3636

3737
// Convert milliseconds to seconds for the countdown
3838
setCountdown(Math.floor(timeoutMs / 1000))
@@ -46,10 +46,15 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }:
4646
}
4747
return prevCountdown - 1
4848
})
49-
}, 1000)
49+
}, COUNTDOWN_INTERVAL_MS)
5050

51-
// Clean up interval on unmount
52-
return () => clearInterval(intervalId)
51+
// Clean up interval on unmount and notify parent component
52+
return () => {
53+
clearInterval(intervalId)
54+
// Notify parent component that this component is unmounting
55+
// so it can clear any related timeouts
56+
onUnmount?.()
57+
}
5358
} else {
5459
setCountdown(null)
5560
}
@@ -76,11 +81,14 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }:
7681
// Mark a suggestion as selected if it's not a shift-click (which just copies to input)
7782
if (!event.shiftKey) {
7883
setSuggestionSelected(true)
84+
// Also notify parent component to cancel auto-approval timeout
85+
// This prevents race conditions between visual countdown and actual timeout
86+
onUnmount?.()
7987
}
8088

8189
onSuggestionClick?.(suggestionText, event)
8290
},
83-
[onSuggestionClick],
91+
[onSuggestionClick, onUnmount],
8492
)
8593

8694
// Don't render if there are no suggestions or no click handler.

webview-ui/src/components/settings/AutoApproveSettings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const AutoApproveSettings = ({
6363
alwaysAllowSubtasks,
6464
alwaysAllowExecute,
6565
alwaysAllowFollowupQuestions,
66-
followupAutoApproveTimeoutMs = 10000,
66+
followupAutoApproveTimeoutMs = 60000,
6767
allowedCommands,
6868
setCachedStateField,
6969
...props
@@ -219,7 +219,7 @@ export const AutoApproveSettings = ({
219219
<div className="flex items-center gap-2">
220220
<Slider
221221
min={1000}
222-
max={30000}
222+
max={300000}
223223
step={1000}
224224
value={[followupAutoApproveTimeoutMs]}
225225
onValueChange={([value]) =>

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
604604
alwaysAllowSubtasks={alwaysAllowSubtasks}
605605
alwaysAllowExecute={alwaysAllowExecute}
606606
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
607-
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs || 10000}
607+
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
608608
allowedCommands={allowedCommands}
609609
setCachedStateField={setCachedStateField}
610610
/>

0 commit comments

Comments
 (0)