Compare commits

...

1 Commits

Author SHA1 Message Date
openhands
814cc4f65c feat: Add jump-to-file button in chat messages (simplified version)
- Add JumpToFileButton component that reuses existing ActionTooltip
- Update ChatMessage to show file button when filePath is available
- Add filePath to Message type and set it in chat slice
- Add translations for the jump-to-file tooltip
2025-01-17 21:17:19 +00:00
6 changed files with 62 additions and 2 deletions

View File

@@ -6,19 +6,24 @@ import { cn } from "#/utils/utils";
import { ul, ol } from "../markdown/list";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
import { anchor } from "../markdown/anchor";
import { JumpToFileButton } from "#/components/shared/buttons/jump-to-file-button";
import { useFiles } from "#/context/files";
interface ChatMessageProps {
type: "user" | "assistant";
message: string;
filePath?: string;
}
export function ChatMessage({
type,
message,
filePath,
children,
}: React.PropsWithChildren<ChatMessageProps>) {
const [isHovering, setIsHovering] = React.useState(false);
const [isCopy, setIsCopy] = React.useState(false);
const { setSelectedPath } = useFiles();
const handleCopyToClipboard = async () => {
await navigator.clipboard.writeText(message);
@@ -57,6 +62,12 @@ export function ChatMessage({
onClick={handleCopyToClipboard}
mode={isCopy ? "copied" : "copy"}
/>
{filePath && (
<JumpToFileButton
filePath={filePath}
onClick={() => setSelectedPath(filePath)}
/>
)}
<Markdown
className="text-sm overflow-auto"
components={{

View File

@@ -29,6 +29,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
key={index}
type={message.sender}
message={message.content}
filePath={message.filePath}
>
{message.imageUrls && message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />

View File

@@ -0,0 +1,32 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { VscGoToFile } from "react-icons/vsc";
import { I18nKey } from "#/i18n/declaration";
import { ActionTooltip } from "#/components/shared/action-tooltip";
import { cn } from "#/utils/utils";
interface JumpToFileButtonProps {
filePath: string;
onClick: () => void;
}
export function JumpToFileButton({ filePath, onClick }: JumpToFileButtonProps) {
const { t } = useTranslation();
return (
<ActionTooltip content={t(I18nKey.CHAT$JUMP_TO_FILE_TOOLTIP, { path: filePath })} side="top">
<button
type="button"
data-testid="jump-to-file-button"
onClick={onClick}
className={cn(
"absolute top-2 right-12 p-2 rounded-lg",
"text-neutral-400 hover:text-neutral-200 hover:bg-neutral-700",
"transition-colors duration-200"
)}
>
<VscGoToFile size={16} />
</button>
</ActionTooltip>
);
}

View File

@@ -4492,7 +4492,21 @@
"tr": "İstemcinin hazır olması bekleniyor...",
"ja": "クライアントの準備を待機中"
},
"SUGGESTIONS$WHAT_TO_BUILD": {
"CHAT$JUMP_TO_FILE_TOOLTIP": {
"en": "Jump to file: {{path}}",
"zh-CN": "跳转到文件:{{path}}",
"de": "Zur Datei springen: {{path}}",
"ko-KR": "파일로 이동: {{path}}",
"no": "Hopp til fil: {{path}}",
"zh-TW": "跳轉到文件:{{path}}",
"it": "Vai al file: {{path}}",
"pt": "Ir para o arquivo: {{path}}",
"es": "Ir al archivo: {{path}}",
"ar": "انتقل إلى الملف: {{path}}",
"fr": "Aller au fichier: {{path}}",
"tr": "Dosyaya git: {{path}}"
},
"SUGGESTIONS$WHAT_TO_BUILD": {
"en": "What do you want to build?",
"ja": "何を開発しますか?",
"zh-CN": "你想要构建什么?",

View File

@@ -8,4 +8,5 @@ type Message = {
pending?: boolean;
translationID?: string;
eventID?: number;
filePath?: string;
};

View File

@@ -166,8 +166,9 @@ export const chatSlice = createSlice({
}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
causeMessage.content = content; // Observation content includes the action
} else if (observationID === "read" || observationID === "edit") {
const { content } = observation.payload;
const { content, extras } = observation.payload;
causeMessage.content = `\`\`\`${observationID === "edit" ? "diff" : "python"}\n${content}\n\`\`\``; // Content is already truncated by the ACI
causeMessage.filePath = extras.path;
} else if (observationID === "browse") {
let content = `**URL:** ${observation.payload.extras.url}\n`;
if (observation.payload.extras.error) {