Skip to content

Commit e41d699

Browse files
committed
feat: add fetch_web_content timeline rendering in ChatRow
1 parent 4486426 commit e41d699

3 files changed

Lines changed: 163 additions & 0 deletions

File tree

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { useSelectedModel } from "../ui/hooks/useSelectedModel"
5757
import {
5858
Eye,
5959
FileDiff,
60+
Globe,
6061
ListTree,
6162
User,
6263
Edit,
@@ -1015,6 +1016,28 @@ export const ChatRowContent = ({
10151016
)}
10161017
</>
10171018
)
1019+
case "fetchWebContent":
1020+
return (
1021+
<>
1022+
<div style={headerStyle}>
1023+
<Globe className="w-4 shrink-0" aria-label="Web fetch icon" />
1024+
<span style={{ fontWeight: "bold" }}>
1025+
{message.type === "ask"
1026+
? t("chat:webFetch.wantsToFetch")
1027+
: t("chat:webFetch.didFetch")}
1028+
</span>
1029+
</div>
1030+
<div className="pl-6">
1031+
<ToolUseBlock>
1032+
<ToolUseBlockHeader className="group">
1033+
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2">
1034+
{tool.url}
1035+
</span>
1036+
</ToolUseBlockHeader>
1037+
</ToolUseBlock>
1038+
</div>
1039+
</>
1040+
)
10181041
default:
10191042
return null
10201043
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React from "react"
2+
import { render, screen } from "@/utils/test-utils"
3+
import { describe, it, expect, beforeEach, vi } from "vitest"
4+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
5+
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
6+
import { ChatRowContent } from "../ChatRow"
7+
8+
// Mock i18n
9+
vi.mock("react-i18next", () => ({
10+
useTranslation: () => ({
11+
t: (key: string) => {
12+
const translations: Record<string, string> = {
13+
"chat:webFetch.wantsToFetch": "Zoo wants to fetch web content",
14+
"chat:webFetch.didFetch": "Zoo fetched web content",
15+
}
16+
return translations[key] || key
17+
},
18+
}),
19+
Trans: ({ i18nKey, children }: { i18nKey: string; children?: React.ReactNode }) => {
20+
return <>{children || i18nKey}</>
21+
},
22+
initReactI18next: {
23+
type: "3rdParty",
24+
init: () => {},
25+
},
26+
}))
27+
28+
// Mock VSCodeBadge
29+
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
30+
VSCodeBadge: ({ children, ...props }: { children: React.ReactNode }) => <span {...props}>{children}</span>,
31+
}))
32+
33+
const queryClient = new QueryClient()
34+
35+
const mockOnToggleExpand = vi.fn()
36+
const mockOnSuggestionClick = vi.fn()
37+
const mockOnBatchFileResponse = vi.fn()
38+
const mockOnFollowUpUnmount = vi.fn()
39+
40+
const renderChatRowWithProviders = (message: any) => {
41+
return render(
42+
<ExtensionStateContextProvider>
43+
<QueryClientProvider client={queryClient}>
44+
<ChatRowContent
45+
message={message}
46+
isExpanded={false}
47+
isLast={false}
48+
isStreaming={false}
49+
onToggleExpand={mockOnToggleExpand}
50+
onSuggestionClick={mockOnSuggestionClick}
51+
onBatchFileResponse={mockOnBatchFileResponse}
52+
onFollowUpUnmount={mockOnFollowUpUnmount}
53+
isFollowUpAnswered={false}
54+
/>
55+
</QueryClientProvider>
56+
</ExtensionStateContextProvider>,
57+
)
58+
}
59+
60+
describe("ChatRow - fetchWebContent tool", () => {
61+
beforeEach(() => {
62+
vi.clearAllMocks()
63+
})
64+
65+
it("should display fetchWebContent ask message with URL", () => {
66+
const message: any = {
67+
type: "ask",
68+
ask: "tool",
69+
ts: Date.now(),
70+
text: JSON.stringify({
71+
tool: "fetchWebContent",
72+
url: "https://example.com",
73+
}),
74+
partial: false,
75+
}
76+
77+
renderChatRowWithProviders(message)
78+
79+
expect(screen.getByText("Zoo wants to fetch web content")).toBeInTheDocument()
80+
expect(screen.getByText("https://example.com")).toBeInTheDocument()
81+
})
82+
83+
it("should display the Globe icon for fetchWebContent", () => {
84+
const message: any = {
85+
type: "ask",
86+
ask: "tool",
87+
ts: Date.now(),
88+
text: JSON.stringify({
89+
tool: "fetchWebContent",
90+
url: "https://docs.example.com/api",
91+
}),
92+
partial: false,
93+
}
94+
95+
renderChatRowWithProviders(message)
96+
97+
expect(screen.getByLabelText("Web fetch icon")).toBeInTheDocument()
98+
})
99+
100+
it("should display the URL in the tool use block", () => {
101+
const message: any = {
102+
type: "ask",
103+
ask: "tool",
104+
ts: Date.now(),
105+
text: JSON.stringify({
106+
tool: "fetchWebContent",
107+
url: "https://api.github.com/repos/owner/repo",
108+
}),
109+
partial: false,
110+
}
111+
112+
renderChatRowWithProviders(message)
113+
114+
expect(screen.getByText("https://api.github.com/repos/owner/repo")).toBeInTheDocument()
115+
})
116+
117+
it("should not return null for fetchWebContent tool (regression test)", () => {
118+
const message: any = {
119+
type: "ask",
120+
ask: "tool",
121+
ts: Date.now(),
122+
text: JSON.stringify({
123+
tool: "fetchWebContent",
124+
url: "https://www.delfi.lt",
125+
}),
126+
partial: false,
127+
}
128+
129+
const { container } = renderChatRowWithProviders(message)
130+
131+
// The container should have rendered content (not null)
132+
expect(container.innerHTML).not.toBe("")
133+
expect(screen.getByText("Zoo wants to fetch web content")).toBeInTheDocument()
134+
expect(screen.getByText("https://www.delfi.lt")).toBeInTheDocument()
135+
})
136+
})

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@
266266
"didSearch_other": "Found {{count}} results",
267267
"resultTooltip": "Similarity score: {{score}} (click to open file)"
268268
},
269+
"webFetch": {
270+
"wantsToFetch": "Zoo wants to fetch web content",
271+
"didFetch": "Zoo fetched web content"
272+
},
269273
"commandOutput": "Command Output",
270274
"commandExecution": {
271275
"abort": "Abort",

0 commit comments

Comments
 (0)