feat: add file path tooltips with centralized PathTooltip component (#9030)

- Add PathTooltip component that wraps StandardTooltip with proper styling
- Use maxWidth='min(300px,100vw)' via inline style to override defaults
- Apply '[text-wrap:wrap]' class to override text-balance behavior
- Add formatPathTooltip helper for consistent path content formatting
- Update CodeAccordian, ChatRow, and BatchFilePermission to use PathTooltip
- Centralizes tooltip behavior and reduces duplication

Supersedes PR #8797.
This commit is contained in:
Daniel 2025-11-04 21:59:19 -05:00 • committed by GitHub
parent e25b4e62d1
commit f1caf3f36e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 113 additions and 15 deletions

View file

@ -2,7 +2,8 @@ import { memo } from "react"
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import { vscode } from "@src/utils/vscode"
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
import { formatPathTooltip } from "@src/utils/formatPathTooltip"
import { PathTooltip } from "../ui/PathTooltip"
interface FilePermissionItem {
path: string
@ -35,10 +36,18 @@ export const BatchFilePermission = memo(({ files = [], onPermissionResponse, ts
<ToolUseBlockHeader
onClick={() => vscode.postMessage({ type: "openFile", text: file.content })}>
{file.path?.startsWith(".") && <span>.</span>}
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{removeLeadingNonAlphanumeric(file.path ?? "") + "\u200E"}
{file.lineSnippet && ` ${file.lineSnippet}`}
</span>
<PathTooltip
content={formatPathTooltip(
file.path,
file.lineSnippet ? ` ${file.lineSnippet}` : undefined,
)}>
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{formatPathTooltip(
file.path,
file.lineSnippet ? ` ${file.lineSnippet}` : undefined,
)}
</span>
</PathTooltip>
<div className="flex-grow"></div>
<span className="codicon codicon-link-external text-[13.5px] my-[1px]" />
</ToolUseBlockHeader>

View file

@ -14,7 +14,7 @@ import { safeJsonParse } from "@roo/safeJsonParse"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
import { vscode } from "@src/utils/vscode"
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
import { formatPathTooltip } from "@src/utils/formatPathTooltip"
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
@ -61,6 +61,7 @@ import {
MessageCircle,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { PathTooltip } from "../ui/PathTooltip"
interface ChatRowProps {
message: ClineMessage
@ -552,10 +553,11 @@ export const ChatRowContent = ({
className="group"
onClick={() => vscode.postMessage({ type: "openFile", text: tool.content })}>
{tool.path?.startsWith(".") && <span>.</span>}
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
{tool.reason}
</span>
<PathTooltip content={formatPathTooltip(tool.path, tool.reason)}>
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{formatPathTooltip(tool.path, tool.reason)}
</span>
</PathTooltip>
<div style={{ flexGrow: 1 }}></div>
<SquareArrowOutUpRight
className="w-4 shrink-0 codicon codicon-link-external opacity-0 group-hover:opacity-100 transition-opacity"

View file

@ -2,10 +2,11 @@ import { memo, useMemo } from "react"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { type ToolProgressStatus } from "@roo-code/types"
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
import { formatPathTooltip } from "@src/utils/formatPathTooltip"
import { ToolUseBlock, ToolUseBlockHeader } from "./ToolUseBlock"
import CodeBlock from "./CodeBlock"
import { PathTooltip } from "../ui/PathTooltip"
interface CodeAccordianProps {
path?: string
@ -44,7 +45,9 @@ const CodeAccordian = ({
{header ? (
<div className="flex items-center">
<span className="codicon codicon-server mr-1.5"></span>
<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2">{header}</span>
<PathTooltip content={header}>
<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2">{header}</span>
</PathTooltip>
</div>
) : isFeedback ? (
<div className="flex items-center">
@ -56,9 +59,11 @@ const CodeAccordian = ({
) : (
<>
{path?.startsWith(".") && <span>.</span>}
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{removeLeadingNonAlphanumeric(path ?? "") + "\u200E"}
</span>
<PathTooltip content={formatPathTooltip(path)}>
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{formatPathTooltip(path)}
</span>
</PathTooltip>
</>
)}
<div className="flex-grow-1" />

View file

@ -0,0 +1,54 @@
import { ReactNode } from "react"
import { StandardTooltip } from "./standard-tooltip"
interface PathTooltipProps {
/** The file path or content to display in the tooltip */
content: string
/** The element(s) that trigger the tooltip */
children: ReactNode
/** The preferred side of the trigger to render the tooltip */
side?: "top" | "right" | "bottom" | "left"
/** The preferred alignment against the trigger */
align?: "start" | "center" | "end"
/** Distance in pixels from the trigger */
sideOffset?: number
/** Whether the trigger should be rendered as a child */
asChild?: boolean
}
/**
* PathTooltip component specifically designed for displaying file paths with appropriate
* wrapping and responsive width behavior. Use this for truncated file paths that need
* tooltips to show the full path.
*
* Features:
* - Responsive max-width that adapts to panel size (max 300px on wide panels, 100vw on narrow)
* - Uses text-wrap instead of text-balance to minimize whitespace
* - Leverages existing break-words CSS for natural line breaking at path separators
*
* @example
* <PathTooltip content="/very/long/file/path.tsx">
* <span className="truncate">...path.tsx</span>
* </PathTooltip>
*/
export function PathTooltip({
content,
children,
side = "top",
align = "start",
sideOffset = 4,
asChild = true,
}: PathTooltipProps) {
return (
<StandardTooltip
content={content}
side={side}
align={align}
sideOffset={sideOffset}
className="[text-wrap:wrap]"
maxWidth="min(300px,100vw)"
asChild={asChild}>
{children}
</StandardTooltip>
)
}

View file

@ -0,0 +1,28 @@
import { removeLeadingNonAlphanumeric } from "./removeLeadingNonAlphanumeric"
/**
* Formats a file path for display in tooltips with consistent formatting.
*
* @param path - The file path to format
* @param additionalContent - Optional additional content to append (e.g., line snippets, reasons)
* @returns Formatted string ready for tooltip display
*
* @example
* formatPathTooltip("/src/components/MyComponent.tsx")
* // Returns: "src/components/MyComponent.tsx" + U+200E
*
* @example
* formatPathTooltip("/src/utils/helper.ts", ":42-45")
* // Returns: "src/utils/helper.ts:42-45" + U+200E
*/
export function formatPathTooltip(path?: string, additionalContent?: string): string {
if (!path) return ""
const formattedPath = removeLeadingNonAlphanumeric(path) + "\u200E"
if (additionalContent) {
return formattedPath + additionalContent
}
return formattedPath
}