Skip to content

Commit

Permalink
feat: Add jump-to-file button in chat messages (simplified version)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
openhands-agent committed Jan 17, 2025
1 parent 62fbe4c commit 814cc4f
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 2 deletions.
11 changes: 11 additions & 0 deletions frontend/src/components/features/chat/chat-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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={{
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/features/chat/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/components/shared/buttons/jump-to-file-button.tsx
Original file line number Diff line number Diff line change
@@ -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">

Check failure on line 17 in frontend/src/components/shared/buttons/jump-to-file-button.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

Replace `·content={t(I18nKey.CHAT$JUMP_TO_FILE_TOOLTIP,·{·path:·filePath·})}·side="top"` with `⏎······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"

Check failure on line 25 in frontend/src/components/shared/buttons/jump-to-file-button.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

Insert `,`
)}
>
<VscGoToFile size={16} />
</button>
</ActionTooltip>
);
}

Check failure on line 32 in frontend/src/components/shared/buttons/jump-to-file-button.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

Insert `⏎`
16 changes: 15 additions & 1 deletion frontend/src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "你想要构建什么?",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ type Message = {
pending?: boolean;
translationID?: string;
eventID?: number;
filePath?: string;
};
3 changes: 2 additions & 1 deletion frontend/src/state/chat-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 814cc4f

Please sign in to comment.