mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: add jump-to-user-prompts navigation feature
- Add UserPromptNavigation component with up/down navigation buttons - Integrate navigation buttons near scroll-to-bottom button - Add keyboard shortcuts (Alt+Up/Down) for quick navigation - Add visual highlight animation when jumping to prompts - Add position indicator showing current prompt (e.g., "2 of 5") - Add comprehensive tests for the navigation feature - Add translation keys for new UI elements Closes #9825
This commit is contained in:
parent
29385e01d7
commit
57caeaa940
5 changed files with 565 additions and 43 deletions
|
|
@ -49,6 +49,7 @@ import { QueuedMessages } from "./QueuedMessages"
|
|||
import DismissibleUpsell from "../common/DismissibleUpsell"
|
||||
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
|
||||
import { Cloud } from "lucide-react"
|
||||
import { UserPromptNavigation } from "./UserPromptNavigation"
|
||||
|
||||
export interface ChatViewProps {
|
||||
isHidden: boolean
|
||||
|
|
@ -1244,40 +1245,44 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
return <BrowserSessionStatusRow key={messageOrGroup.ts} message={messageOrGroup} />
|
||||
}
|
||||
|
||||
// regular message
|
||||
// regular message - add data attribute for navigation
|
||||
return (
|
||||
<ChatRow
|
||||
key={messageOrGroup.ts}
|
||||
message={messageOrGroup}
|
||||
isExpanded={expandedRows[messageOrGroup.ts] || false}
|
||||
onToggleExpand={toggleRowExpansion} // This was already stabilized
|
||||
lastModifiedMessage={modifiedMessages.at(-1)} // Original direct access
|
||||
isLast={index === groupedMessages.length - 1} // Original direct access
|
||||
onHeightChange={handleRowHeightChange}
|
||||
isStreaming={isStreaming}
|
||||
onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
|
||||
onBatchFileResponse={handleBatchFileResponse}
|
||||
isFollowUpAnswered={messageOrGroup.isAnswered === true || messageOrGroup.ts === currentFollowUpTs}
|
||||
editable={
|
||||
messageOrGroup.type === "ask" &&
|
||||
messageOrGroup.ask === "tool" &&
|
||||
(() => {
|
||||
let tool: any = {}
|
||||
try {
|
||||
tool = JSON.parse(messageOrGroup.text || "{}")
|
||||
} catch (_) {
|
||||
if (messageOrGroup.text?.includes("updateTodoList")) {
|
||||
tool = { tool: "updateTodoList" }
|
||||
<div data-message-index={index}>
|
||||
<ChatRow
|
||||
key={messageOrGroup.ts}
|
||||
message={messageOrGroup}
|
||||
isExpanded={expandedRows[messageOrGroup.ts] || false}
|
||||
onToggleExpand={toggleRowExpansion} // This was already stabilized
|
||||
lastModifiedMessage={modifiedMessages.at(-1)} // Original direct access
|
||||
isLast={index === groupedMessages.length - 1} // Original direct access
|
||||
onHeightChange={handleRowHeightChange}
|
||||
isStreaming={isStreaming}
|
||||
onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
|
||||
onBatchFileResponse={handleBatchFileResponse}
|
||||
isFollowUpAnswered={
|
||||
messageOrGroup.isAnswered === true || messageOrGroup.ts === currentFollowUpTs
|
||||
}
|
||||
editable={
|
||||
messageOrGroup.type === "ask" &&
|
||||
messageOrGroup.ask === "tool" &&
|
||||
(() => {
|
||||
let tool: any = {}
|
||||
try {
|
||||
tool = JSON.parse(messageOrGroup.text || "{}")
|
||||
} catch (_) {
|
||||
if (messageOrGroup.text?.includes("updateTodoList")) {
|
||||
tool = { tool: "updateTodoList" }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tool.tool === "updateTodoList" && alwaysAllowUpdateTodoList) {
|
||||
return false
|
||||
}
|
||||
return tool.tool === "updateTodoList" && enableButtons && !!primaryButtonText
|
||||
})()
|
||||
}
|
||||
hasCheckpoint={hasCheckpoint}
|
||||
/>
|
||||
if (tool.tool === "updateTodoList" && alwaysAllowUpdateTodoList) {
|
||||
return false
|
||||
}
|
||||
return tool.tool === "updateTodoList" && enableButtons && !!primaryButtonText
|
||||
})()
|
||||
}
|
||||
hasCheckpoint={hasCheckpoint}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[
|
||||
|
|
@ -1330,6 +1335,25 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
switchToNextMode()
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Alt + Arrow keys for prompt navigation
|
||||
if (event.altKey && (event.key === "ArrowUp" || event.key === "ArrowDown")) {
|
||||
event.preventDefault() // Prevent default scrolling
|
||||
|
||||
// Find the UserPromptNavigation component and trigger navigation
|
||||
const promptNavButtons = document.querySelectorAll("[data-prompt-nav]")
|
||||
if (promptNavButtons.length > 0) {
|
||||
if (event.key === "ArrowUp") {
|
||||
// Trigger previous prompt navigation
|
||||
const prevButton = document.querySelector('[data-prompt-nav="prev"]') as HTMLButtonElement
|
||||
prevButton?.click()
|
||||
} else if (event.key === "ArrowDown") {
|
||||
// Trigger next prompt navigation
|
||||
const nextButton = document.querySelector('[data-prompt-nav="next"]') as HTMLButtonElement
|
||||
nextButton?.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[switchToNextMode, switchToPreviousMode],
|
||||
)
|
||||
|
|
@ -1474,17 +1498,25 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
: "opacity-50"
|
||||
}`}>
|
||||
{showScrollToBottom ? (
|
||||
<StandardTooltip content={t("chat:scrollToBottom")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-[2]"
|
||||
onClick={() => {
|
||||
scrollToBottomSmooth()
|
||||
disableAutoScrollRef.current = false
|
||||
}}>
|
||||
<span className="codicon codicon-chevron-down"></span>
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={virtuosoRef}
|
||||
visibleMessages={groupedMessages}
|
||||
className="mr-2"
|
||||
/>
|
||||
<StandardTooltip content={t("chat:scrollToBottom")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
scrollToBottomSmooth()
|
||||
disableAutoScrollRef.current = false
|
||||
}}>
|
||||
<span className="codicon codicon-chevron-down"></span>
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{primaryButtonText && !isStreaming && (
|
||||
|
|
|
|||
21
webview-ui/src/components/chat/UserPromptNavigation.css
Normal file
21
webview-ui/src/components/chat/UserPromptNavigation.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@keyframes prompt-highlight {
|
||||
0% {
|
||||
background-color: var(--vscode-editor-findMatchHighlightBackground);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-highlight {
|
||||
animation: prompt-highlight 1.5s ease-in-out;
|
||||
border-radius: 4px;
|
||||
}
|
||||
181
webview-ui/src/components/chat/UserPromptNavigation.tsx
Normal file
181
webview-ui/src/components/chat/UserPromptNavigation.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import React, { useMemo, useCallback, useState, useEffect } from "react"
|
||||
import { ChevronUp, ChevronDown } from "lucide-react"
|
||||
import { StandardTooltip } from "@src/components/ui"
|
||||
import { LucideIconButton } from "./LucideIconButton"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
import type { VirtuosoHandle } from "react-virtuoso"
|
||||
import "./UserPromptNavigation.css"
|
||||
|
||||
interface UserPromptNavigationProps {
|
||||
messages: ClineMessage[]
|
||||
virtuosoRef: React.RefObject<VirtuosoHandle>
|
||||
visibleMessages: ClineMessage[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const UserPromptNavigation: React.FC<UserPromptNavigationProps> = ({
|
||||
messages,
|
||||
virtuosoRef,
|
||||
visibleMessages,
|
||||
className = "",
|
||||
}) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [currentPromptIndex, setCurrentPromptIndex] = useState<number>(-1)
|
||||
const [isNavigating, setIsNavigating] = useState(false)
|
||||
|
||||
// Find all user prompts in the visible messages
|
||||
const userPromptIndices = useMemo(() => {
|
||||
const indices: number[] = []
|
||||
visibleMessages.forEach((msg, index) => {
|
||||
if (msg.say === "user_feedback" && msg.text && msg.text.trim() !== "") {
|
||||
indices.push(index)
|
||||
}
|
||||
})
|
||||
return indices
|
||||
}, [visibleMessages])
|
||||
|
||||
// Reset navigation when messages change significantly
|
||||
useEffect(() => {
|
||||
if (!isNavigating) {
|
||||
setCurrentPromptIndex(-1)
|
||||
}
|
||||
}, [messages.length, isNavigating])
|
||||
|
||||
// Navigate to previous user prompt
|
||||
const navigateToPreviousPrompt = useCallback(() => {
|
||||
if (userPromptIndices.length === 0) return
|
||||
|
||||
setIsNavigating(true)
|
||||
|
||||
let targetIndex: number
|
||||
if (currentPromptIndex === -1) {
|
||||
// If not currently navigating, jump to the last prompt
|
||||
targetIndex = userPromptIndices.length - 1
|
||||
} else if (currentPromptIndex > 0) {
|
||||
// Navigate to previous prompt
|
||||
targetIndex = currentPromptIndex - 1
|
||||
} else {
|
||||
// Wrap around to the last prompt
|
||||
targetIndex = userPromptIndices.length - 1
|
||||
}
|
||||
|
||||
setCurrentPromptIndex(targetIndex)
|
||||
const messageIndex = userPromptIndices[targetIndex]
|
||||
|
||||
// Scroll to the message with smooth animation
|
||||
virtuosoRef.current?.scrollToIndex({
|
||||
index: messageIndex,
|
||||
behavior: "smooth",
|
||||
align: "center",
|
||||
})
|
||||
|
||||
// Briefly highlight the message after scrolling
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-message-index="${messageIndex}"]`)
|
||||
if (element) {
|
||||
element.classList.add("prompt-highlight")
|
||||
setTimeout(() => {
|
||||
element.classList.remove("prompt-highlight")
|
||||
}, 1500)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
// Clear navigation state after a delay
|
||||
setTimeout(() => {
|
||||
setIsNavigating(false)
|
||||
}, 3000)
|
||||
}, [userPromptIndices, currentPromptIndex, virtuosoRef])
|
||||
|
||||
// Navigate to next user prompt
|
||||
const navigateToNextPrompt = useCallback(() => {
|
||||
if (userPromptIndices.length === 0) return
|
||||
|
||||
setIsNavigating(true)
|
||||
|
||||
let targetIndex: number
|
||||
if (currentPromptIndex === -1) {
|
||||
// If not currently navigating, jump to the first prompt
|
||||
targetIndex = 0
|
||||
} else if (currentPromptIndex < userPromptIndices.length - 1) {
|
||||
// Navigate to next prompt
|
||||
targetIndex = currentPromptIndex + 1
|
||||
} else {
|
||||
// Wrap around to the first prompt
|
||||
targetIndex = 0
|
||||
}
|
||||
|
||||
setCurrentPromptIndex(targetIndex)
|
||||
const messageIndex = userPromptIndices[targetIndex]
|
||||
|
||||
// Scroll to the message with smooth animation
|
||||
virtuosoRef.current?.scrollToIndex({
|
||||
index: messageIndex,
|
||||
behavior: "smooth",
|
||||
align: "center",
|
||||
})
|
||||
|
||||
// Briefly highlight the message after scrolling
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`[data-message-index="${messageIndex}"]`)
|
||||
if (element) {
|
||||
element.classList.add("prompt-highlight")
|
||||
setTimeout(() => {
|
||||
element.classList.remove("prompt-highlight")
|
||||
}, 1500)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
// Clear navigation state after a delay
|
||||
setTimeout(() => {
|
||||
setIsNavigating(false)
|
||||
}, 3000)
|
||||
}, [userPromptIndices, currentPromptIndex, virtuosoRef])
|
||||
|
||||
// Don't show navigation if there are no user prompts
|
||||
if (userPromptIndices.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const navigationInfo =
|
||||
currentPromptIndex !== -1
|
||||
? t("chat:promptNavigation.position", {
|
||||
current: currentPromptIndex + 1,
|
||||
total: userPromptIndices.length,
|
||||
})
|
||||
: t("chat:promptNavigation.total", { total: userPromptIndices.length })
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
<StandardTooltip content={`${t("chat:promptNavigation.previousTooltip")} (Alt+↑)`}>
|
||||
<div data-prompt-nav="prev">
|
||||
<LucideIconButton
|
||||
icon={ChevronUp}
|
||||
onClick={navigateToPreviousPrompt}
|
||||
disabled={userPromptIndices.length === 0}
|
||||
className="h-7 w-7"
|
||||
title={t("chat:promptNavigation.previous")}
|
||||
/>
|
||||
</div>
|
||||
</StandardTooltip>
|
||||
|
||||
{isNavigating && (
|
||||
<span className="text-xs text-vscode-descriptionForeground px-1 min-w-[60px] text-center">
|
||||
{navigationInfo}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<StandardTooltip content={`${t("chat:promptNavigation.nextTooltip")} (Alt+↓)`}>
|
||||
<div data-prompt-nav="next">
|
||||
<LucideIconButton
|
||||
icon={ChevronDown}
|
||||
onClick={navigateToNextPrompt}
|
||||
disabled={userPromptIndices.length === 0}
|
||||
className="h-7 w-7"
|
||||
title={t("chat:promptNavigation.next")}
|
||||
/>
|
||||
</div>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { UserPromptNavigation } from "../UserPromptNavigation"
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
import type { VirtuosoHandle } from "react-virtuoso"
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@src/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
if (key === "chat:promptNavigation.position") {
|
||||
return `${params?.current} of ${params?.total}`
|
||||
}
|
||||
if (key === "chat:promptNavigation.total") {
|
||||
return `${params?.total} prompts`
|
||||
}
|
||||
if (key === "chat:promptNavigation.previousTooltip") {
|
||||
return "Jump to previous user prompt"
|
||||
}
|
||||
if (key === "chat:promptNavigation.nextTooltip") {
|
||||
return "Jump to next user prompt"
|
||||
}
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock CSS import
|
||||
vi.mock("../UserPromptNavigation.css", () => ({}))
|
||||
|
||||
// Mock StandardTooltip to avoid TooltipProvider requirement
|
||||
vi.mock("@src/components/ui", () => ({
|
||||
StandardTooltip: ({ children, content }: { children: React.ReactNode; content: string }) => (
|
||||
<div title={content}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock LucideIconButton
|
||||
vi.mock("../LucideIconButton", () => ({
|
||||
LucideIconButton: ({ onClick, disabled, title }: any) => (
|
||||
<button onClick={onClick} disabled={disabled} title={title}>
|
||||
{title}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe("UserPromptNavigation", () => {
|
||||
const mockVirtuosoRef = {
|
||||
current: {
|
||||
scrollToIndex: vi.fn(),
|
||||
} as unknown as VirtuosoHandle,
|
||||
}
|
||||
|
||||
const createMessage = (say: string, text: string, ts: number): ClineMessage =>
|
||||
({
|
||||
type: "say",
|
||||
say,
|
||||
text,
|
||||
ts,
|
||||
}) as ClineMessage
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should not render when there are no user prompts", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
createMessage("text", "AI response", 1000),
|
||||
createMessage("api_req_started", "API request", 2000),
|
||||
]
|
||||
|
||||
const { container } = render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("should render navigation buttons when there are user prompts", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
createMessage("user_feedback", "First prompt", 1000),
|
||||
createMessage("text", "AI response", 2000),
|
||||
createMessage("user_feedback", "Second prompt", 3000),
|
||||
]
|
||||
|
||||
render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Check for navigation buttons
|
||||
expect(screen.getByTitle("chat:promptNavigation.previous")).toBeInTheDocument()
|
||||
expect(screen.getByTitle("chat:promptNavigation.next")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should navigate to the last prompt when clicking previous for the first time", async () => {
|
||||
const messages: ClineMessage[] = [
|
||||
createMessage("user_feedback", "First prompt", 1000),
|
||||
createMessage("text", "AI response", 2000),
|
||||
createMessage("user_feedback", "Second prompt", 3000),
|
||||
createMessage("text", "AI response 2", 4000),
|
||||
createMessage("user_feedback", "Third prompt", 5000),
|
||||
]
|
||||
|
||||
render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
const prevButton = screen.getByTitle("chat:promptNavigation.previous")
|
||||
fireEvent.click(prevButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVirtuosoRef.current?.scrollToIndex).toHaveBeenCalledWith({
|
||||
index: 4, // Index of the third prompt
|
||||
behavior: "smooth",
|
||||
align: "center",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should navigate to the first prompt when clicking next for the first time", async () => {
|
||||
const messages: ClineMessage[] = [
|
||||
createMessage("user_feedback", "First prompt", 1000),
|
||||
createMessage("text", "AI response", 2000),
|
||||
createMessage("user_feedback", "Second prompt", 3000),
|
||||
]
|
||||
|
||||
render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nextButton = screen.getByTitle("chat:promptNavigation.next")
|
||||
fireEvent.click(nextButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVirtuosoRef.current?.scrollToIndex).toHaveBeenCalledWith({
|
||||
index: 0, // Index of the first prompt
|
||||
behavior: "smooth",
|
||||
align: "center",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should cycle through prompts when navigating multiple times", async () => {
|
||||
const messages: ClineMessage[] = [
|
||||
createMessage("user_feedback", "First prompt", 1000),
|
||||
createMessage("user_feedback", "Second prompt", 2000),
|
||||
createMessage("user_feedback", "Third prompt", 3000),
|
||||
]
|
||||
|
||||
render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nextButton = screen.getByTitle("chat:promptNavigation.next")
|
||||
|
||||
// First click - go to first prompt
|
||||
fireEvent.click(nextButton)
|
||||
await waitFor(() => {
|
||||
expect(mockVirtuosoRef.current?.scrollToIndex).toHaveBeenCalledWith({
|
||||
index: 0,
|
||||
behavior: "smooth",
|
||||
align: "center",
|
||||
})
|
||||
})
|
||||
|
||||
// Wait a bit and click again - should go to second prompt
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
fireEvent.click(nextButton)
|
||||
await waitFor(() => {
|
||||
expect(mockVirtuosoRef.current?.scrollToIndex).toHaveBeenCalledWith({
|
||||
index: 1,
|
||||
behavior: "smooth",
|
||||
align: "center",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should filter out empty user prompts", () => {
|
||||
const messages: ClineMessage[] = [
|
||||
createMessage("user_feedback", "Valid prompt", 1000),
|
||||
createMessage("user_feedback", "", 2000), // Empty prompt
|
||||
createMessage("user_feedback", " ", 3000), // Whitespace only
|
||||
createMessage("user_feedback", "Another valid prompt", 4000),
|
||||
]
|
||||
|
||||
render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nextButton = screen.getByTitle("chat:promptNavigation.next")
|
||||
fireEvent.click(nextButton)
|
||||
|
||||
// Should navigate to first valid prompt (index 0)
|
||||
expect(mockVirtuosoRef.current?.scrollToIndex).toHaveBeenCalledWith({
|
||||
index: 0,
|
||||
behavior: "smooth",
|
||||
align: "center",
|
||||
})
|
||||
})
|
||||
|
||||
it("should show navigation info when navigating", async () => {
|
||||
const messages: ClineMessage[] = [
|
||||
createMessage("user_feedback", "First prompt", 1000),
|
||||
createMessage("user_feedback", "Second prompt", 2000),
|
||||
]
|
||||
|
||||
const { container } = render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
const nextButton = screen.getByTitle("chat:promptNavigation.next")
|
||||
fireEvent.click(nextButton)
|
||||
|
||||
// Should show position info briefly
|
||||
await waitFor(() => {
|
||||
const info = container.querySelector(".text-xs")
|
||||
expect(info?.textContent).toContain("1 of 2")
|
||||
})
|
||||
})
|
||||
|
||||
it("should have proper tooltips with keyboard shortcuts", () => {
|
||||
const messages: ClineMessage[] = [createMessage("user_feedback", "First prompt", 1000)]
|
||||
|
||||
render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Check for data attributes for keyboard navigation
|
||||
expect(document.querySelector('[data-prompt-nav="prev"]')).toBeInTheDocument()
|
||||
expect(document.querySelector('[data-prompt-nav="next"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should apply custom className when provided", () => {
|
||||
const messages: ClineMessage[] = [createMessage("user_feedback", "First prompt", 1000)]
|
||||
|
||||
const { container } = render(
|
||||
<UserPromptNavigation
|
||||
messages={messages}
|
||||
virtuosoRef={mockVirtuosoRef as React.RefObject<VirtuosoHandle>}
|
||||
visibleMessages={messages}
|
||||
className="custom-class"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toHaveClass("custom-class")
|
||||
})
|
||||
})
|
||||
|
|
@ -98,6 +98,14 @@
|
|||
"placeholder": "Edit your message..."
|
||||
},
|
||||
"scrollToBottom": "Scroll to bottom of chat",
|
||||
"promptNavigation": {
|
||||
"previous": "Previous prompt",
|
||||
"previousTooltip": "Jump to previous user prompt",
|
||||
"next": "Next prompt",
|
||||
"nextTooltip": "Jump to next user prompt",
|
||||
"position": "{{current}} of {{total}}",
|
||||
"total": "{{total}} prompts"
|
||||
},
|
||||
"about": "Roo is a whole AI dev team in your editor",
|
||||
"docs": "Check our <DocsLink>docs</DocsLink> to get started",
|
||||
"onboarding": "What would you like to do?",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue