Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "followup":
setTextAreaDisabled(isPartial)
setClineAsk("followup")
setEnableButtons(isPartial)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
// setting enable buttons to `false` would trigger a focus grab when
// the text area is enabled which is undesirable.
// We have no buttons for this tool, so no problem having them "enabled"
// to workaround this issue. See #1358.
setEnableButtons(true)
setPrimaryButtonText(undefined)
setSecondaryButtonText(undefined)
break
case "tool":
if (!isAutoApproved(lastMessage)) {
Expand Down
115 changes: 112 additions & 3 deletions webview-ui/src/components/chat/__tests__/ChatView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react"
import { render, waitFor } from "@testing-library/react"
import { render, waitFor, act } from "@testing-library/react"
import ChatView from "../ChatView"
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
Expand Down Expand Up @@ -60,17 +60,25 @@ interface ChatTextAreaProps {
shouldDisableImages?: boolean
}

const mockInputRef = React.createRef<HTMLInputElement>()
const mockFocus = jest.fn()

jest.mock("../ChatTextArea", () => {
const mockReact = require("react")
return {
__esModule: true,
default: mockReact.forwardRef(function MockChatTextArea(
props: ChatTextAreaProps,
ref: React.ForwardedRef<HTMLInputElement>,
ref: React.ForwardedRef<{ focus: () => void }>,
) {
// Use useImperativeHandle to expose the mock focus method
React.useImperativeHandle(ref, () => ({
focus: mockFocus,
}))

return (
<div data-testid="chat-textarea">
<input ref={ref} type="text" onChange={(e) => props.onSend(e.target.value)} />
<input ref={mockInputRef} type="text" onChange={(e) => props.onSend(e.target.value)} />
</div>
)
}),
Expand Down Expand Up @@ -1087,3 +1095,104 @@ describe("ChatView - Sound Playing Tests", () => {
})
})
})

describe("ChatView - Focus Grabbing Tests", () => {
beforeEach(() => {
jest.clearAllMocks()
})

it("does not grab focus when follow-up question presented", async () => {
const sleep = async (timeout: number) => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, timeout))
})
}

render(
<ExtensionStateContextProvider>
<ChatView
isHidden={false}
showAnnouncement={false}
hideAnnouncement={() => {}}
showHistoryView={() => {}}
/>
</ExtensionStateContextProvider>,
)

// First hydrate state with initial task and streaming
mockPostMessage({
autoApprovalEnabled: true,
alwaysAllowBrowser: true,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now(),
text: "Initial task",
},
{
type: "say",
say: "api_req_started",
ts: Date.now(),
text: JSON.stringify({}),
partial: true,
},
],
})

// process messages
await sleep(0)
// wait for focus updates (can take 50msecs)
await sleep(100)

const FOCUS_CALLS_ON_INIT = 2
expect(mockFocus).toHaveBeenCalledTimes(FOCUS_CALLS_ON_INIT)

// Finish task, and send the followup ask message (streaming unfinished)
mockPostMessage({
autoApprovalEnabled: true,
alwaysAllowBrowser: true,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now(),
text: "Initial task",
},
{
type: "ask",
ask: "followup",
ts: Date.now(),
text: JSON.stringify({}),
partial: true,
},
],
})

// allow messages to be processed
await sleep(0)

// Finish the followup ask message (streaming finished)
mockPostMessage({
autoApprovalEnabled: true,
alwaysAllowBrowser: true,
clineMessages: [
{
type: "ask",
ask: "followup",
ts: Date.now(),
text: JSON.stringify({}),
},
],
})

// allow messages to be processed
await sleep(0)

// wait for focus updates (can take 50msecs)
await sleep(100)

// focus() should not have been called again
expect(mockFocus).toHaveBeenCalledTimes(FOCUS_CALLS_ON_INIT)
})
})