Skip to content

Commit a073904

Browse files
committed
Fix follow-up suggestion mode rendering crash
1 parent 92cf4e9 commit a073904

5 files changed

Lines changed: 80 additions & 6 deletions

File tree

src/core/tools/AskFollowupQuestionTool.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { BaseTool, ToolCallbacks } from "./BaseTool"
66

77
interface Suggestion {
88
text: string
9-
mode?: string
9+
mode?: unknown
1010
}
1111

1212
interface AskFollowupQuestionParams {
@@ -17,6 +17,19 @@ interface AskFollowupQuestionParams {
1717
export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
1818
readonly name = "ask_followup_question" as const
1919

20+
private getSuggestionMode(mode: unknown): string | undefined {
21+
if (typeof mode === "string" && mode.trim().length > 0) {
22+
return mode
23+
}
24+
25+
if (mode && typeof mode === "object" && "mode_slug" in mode) {
26+
const modeSlug = (mode as { mode_slug?: unknown }).mode_slug
27+
return typeof modeSlug === "string" && modeSlug.trim().length > 0 ? modeSlug : undefined
28+
}
29+
30+
return undefined
31+
}
32+
2033
async execute(params: AskFollowupQuestionParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
2134
const { question, follow_up } = params
2235
const { handleError, pushToolResult } = callbacks
@@ -42,7 +55,7 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
4255
// Transform follow_up suggestions to the format expected by task.ask
4356
const follow_up_json = {
4457
question,
45-
suggest: follow_up.map((s) => ({ answer: s.text, mode: s.mode })),
58+
suggest: follow_up.map((s) => ({ answer: s.text, mode: this.getSuggestionMode(s.mode) })),
4659
}
4760

4861
task.consecutiveMistakeCount = 0

src/core/tools/__tests__/askFollowupQuestionTool.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ describe("AskFollowupQuestionTool", () => {
137137
expect(mockTask.ask).toHaveBeenCalledWith("followup", expectedJson, false)
138138
})
139139

140+
it("should normalize malformed object mode values", async () => {
141+
const params = {
142+
question: "Switch mode?",
143+
follow_up: [{ text: "Use code mode", mode: { mode_slug: "code" } }],
144+
} as any
145+
146+
await tool.execute(params, mockTask, mockCallbacks)
147+
148+
const expectedJson = JSON.stringify({
149+
question: "Switch mode?",
150+
suggest: [{ answer: "Use code mode", mode: "code" }],
151+
})
152+
153+
expect(mockTask.ask).toHaveBeenCalledWith("followup", expectedJson, false)
154+
})
155+
140156
it("should say user_feedback and push tool result after user answers", async () => {
141157
const params = {
142158
question: "Which approach?",

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ export const MAX_IMAGES_PER_MESSAGE = 20 // This is the Anthropic limit.
5959

6060
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
6161

62+
const getSuggestionMode = (mode: unknown): string | undefined => {
63+
if (typeof mode === "string" && mode.trim().length > 0) {
64+
return mode
65+
}
66+
67+
if (mode && typeof mode === "object" && "mode_slug" in mode) {
68+
const modeSlug = (mode as { mode_slug?: unknown }).mode_slug
69+
return typeof modeSlug === "string" && modeSlug.trim().length > 0 ? modeSlug : undefined
70+
}
71+
72+
return undefined
73+
}
74+
6275
const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewProps> = (
6376
{ isHidden, showAnnouncement, hideAnnouncement },
6477
ref,
@@ -1358,12 +1371,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
13581371
}
13591372

13601373
// Check if we need to switch modes
1361-
if (suggestion.mode) {
1374+
const suggestionMode = getSuggestionMode(suggestion.mode)
1375+
if (suggestionMode) {
13621376
// Only switch modes if it's a manual click (event exists) or auto-approval is allowed
13631377
const isManualClick = !!event
13641378
if (isManualClick || alwaysAllowModeSwitch) {
13651379
// Switch mode without waiting
1366-
switchToMode(suggestion.mode)
1380+
switchToMode(suggestionMode)
13671381
}
13681382
}
13691383

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ import { cn } from "@/lib/utils"
1111
const DEFAULT_FOLLOWUP_TIMEOUT_MS = 60000
1212
const COUNTDOWN_INTERVAL_MS = 1000
1313

14+
const getSuggestionMode = (mode: unknown): string | undefined => {
15+
if (typeof mode === "string" && mode.trim().length > 0) {
16+
return mode
17+
}
18+
19+
if (mode && typeof mode === "object" && "mode_slug" in mode) {
20+
const modeSlug = (mode as { mode_slug?: unknown }).mode_slug
21+
return typeof modeSlug === "string" && modeSlug.trim().length > 0 ? modeSlug : undefined
22+
}
23+
24+
return undefined
25+
}
26+
1427
interface FollowUpSuggestProps {
1528
suggestions?: SuggestionItem[]
1629
onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void
@@ -111,6 +124,7 @@ export const FollowUpSuggest = ({
111124
<div className="flex mb-2 flex-col h-full gap-2">
112125
{suggestions.map((suggestion, index) => {
113126
const isFirstSuggestion = index === 0
127+
const suggestionMode = getSuggestionMode(suggestion.mode)
114128

115129
return (
116130
<div key={`${suggestion.answer}-${ts}`} className="w-full relative group">
@@ -134,10 +148,10 @@ export const FollowUpSuggest = ({
134148
{t("chat:followUpSuggest.timerPrefix", { seconds: countdown })}
135149
</p>
136150
)}
137-
{suggestion.mode && (
151+
{suggestionMode && (
138152
<div className="absolute bottom-0 right-0 text-[10px] text-vscode-badge-foreground pl-1 pr-2.5 pt-0.5 pb-1.5 flex items-center gap-0.5 bg-transparent rounded-xl">
139153
<span className="codicon codicon-arrow-right" style={{ fontSize: "8px" }} />
140-
{suggestion.mode}
154+
{suggestionMode}
141155
</div>
142156
)}
143157
<StandardTooltip content={t("chat:followUpSuggest.copyToInput")}>

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,23 @@ describe("FollowUpSuggest", () => {
243243
expect(container.firstChild).toBeNull()
244244
})
245245

246+
it("should render malformed object mode values without crashing", () => {
247+
const suggestions = [{ answer: "Use code mode", mode: { mode_slug: "code" } }] as any
248+
249+
renderWithTestProviders(
250+
<FollowUpSuggest
251+
suggestions={suggestions}
252+
onSuggestionClick={mockOnSuggestionClick}
253+
ts={123}
254+
onCancelAutoApproval={mockOnCancelAutoApproval}
255+
/>,
256+
defaultTestState,
257+
)
258+
259+
expect(screen.getByText("Use code mode")).toBeInTheDocument()
260+
expect(screen.getByText("code")).toBeInTheDocument()
261+
})
262+
246263
it("should stop countdown when user manually responds (isAnswered becomes true)", () => {
247264
const { rerender } = renderWithTestProviders(
248265
<FollowUpSuggest

0 commit comments

Comments
 (0)