From 14c40c96151c74ceeecd1251a8fb5947280172ef Mon Sep 17 00:00:00 2001 From: Roberto Berto <463349+robertoberto@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:32:33 +0000 Subject: [PATCH] feat(chat): add quick-open file icon in tool card header Add a small external-link icon next to the tool display name in the collapsed tool card header (Write/Edit/MultiEdit/ApplyPatch), so the target file can be opened in the side panel (web/desktop) or editor (VS Code) without expanding the card. On web/desktop (no runtime.editor), the icon falls back to useUIStore.openContextFile{AtLine} + mobileActions.openFiles(), matching the existing openEntryFile pattern. The existing handleMainClick only opens the file when runtime.editor is available, so this icon is the first way to open a file from the tool header in the browser. Path resolution reuses getPrimaryToolPath + toAbsoluteFilePath; the diff tools also resolve the first changed line and primary diff via the existing getFirstChangedLineFromMetadata / getPrimaryDiffFromMetadata helpers. The icon stops click propagation so the card-toggle-on-click behavior is preserved. Adds the chat.toolPart.openFile i18n key across all 11 locales. --- .../chat/message/parts/ToolPart.tsx | 74 ++++++++++++++++++- packages/ui/src/lib/i18n/messages/de.ts | 1 + packages/ui/src/lib/i18n/messages/en.ts | 1 + packages/ui/src/lib/i18n/messages/es.ts | 1 + packages/ui/src/lib/i18n/messages/fr.ts | 1 + packages/ui/src/lib/i18n/messages/ja.ts | 1 + packages/ui/src/lib/i18n/messages/ko.ts | 1 + packages/ui/src/lib/i18n/messages/pl.ts | 1 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 1 + packages/ui/src/lib/i18n/messages/uk.ts | 1 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 1 + packages/ui/src/lib/i18n/messages/zh-TW.ts | 1 + 12 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx index 18e6fa5d..f4f5436e 100644 --- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx @@ -1996,6 +1996,7 @@ const ToolPartContent: React.FC = ({ return null; }, [descriptionPath, normalizedPartTool, stateWithData, input]); const runtime = React.useContext(RuntimeAPIContext); + const mobileActions = useMobileAppActions(); const openApplyPatchFile = (file: Record, event: React.MouseEvent) => { if (!runtime?.editor) { @@ -2068,6 +2069,61 @@ const ToolPartContent: React.FC = ({ handleMainClick(event); }; + // Quick-open target for the file-link icon in the tool header. Resolves the + // primary file path (and, for diff tools, the first changed line + diff) so + // the user can open the file in the side panel (web/desktop) or editor + // (VS Code) without expanding the tool card. Reuses the same path helpers as + // handleMainClick above; the difference is the web fallback — handleMainClick + // only opens when runtime.editor is available, this icon also falls back to + // useUIStore.openContextFile{AtLine} so the file opens in the right pane. + const quickOpenTarget = React.useMemo<{ absolutePath: string; line?: number; toolDiff?: string; toolName: string } | null>(() => { + if (isTaskTool) return null; + const toolName = normalizedPartTool || part.tool; + const filePath = getPrimaryToolPath(toolName, input, metadata); + if (typeof filePath !== 'string') return null; + const absolutePath = toAbsoluteFilePath(currentDirectory, filePath); + let line: number | undefined; + let toolDiff: string | undefined; + if (toolName === 'edit' || toolName === 'multiedit' || toolName === 'apply_patch') { + line = getFirstChangedLineFromMetadata(toolName, metadata, filePath); + toolDiff = getPrimaryDiffFromMetadata(toolName, metadata, filePath); + } + return { absolutePath, line, toolDiff, toolName }; + }, [isTaskTool, normalizedPartTool, part.tool, input, metadata, currentDirectory]); + + const openQuickTarget = () => { + if (!quickOpenTarget) return; + const { absolutePath, line, toolDiff, toolName } = quickOpenTarget; + if (runtime?.editor) { + if (runtime.runtime.isVSCode && toolDiff && (toolName === 'edit' || toolName === 'multiedit' || toolName === 'apply_patch')) { + const label = `${getRelativePath(absolutePath, currentDirectory)} (changes)`; + void runtime.editor.openDiff('', absolutePath, label, { line, patch: toolDiff }); + return; + } + runtime.editor.openFile(absolutePath, line); + return; + } + const uiStore = useUIStore.getState(); + if (typeof line === 'number' && Number.isFinite(line)) { + uiStore.openContextFileAtLine(currentDirectory, absolutePath, Math.max(1, Math.trunc(line)), 1); + } else { + uiStore.openContextFile(currentDirectory, absolutePath); + } + mobileActions?.openFiles(); + }; + + const handleQuickOpen = (event: React.MouseEvent) => { + event.stopPropagation(); + openQuickTarget(); + }; + + const handleQuickOpenKeyDown = (event: React.KeyboardEvent) => { + if (event.key !== 'Enter' && event.key !== ' ') return; + event.preventDefault(); + event.stopPropagation(); + openQuickTarget(); + }; + const iconStyle = !isTaskTool && isError ? TOOL_ERROR_ICON_STYLE : TOOL_NORMAL_ICON_STYLE; const titleStyle = !isTaskTool && isError ? TOOL_ERROR_TITLE_STYLE : TOOL_NORMAL_TITLE_STYLE; const shouldRenderTaskSummary = useDeferredExpandedContent(isTaskTool && (taskSummaryEntries.length > 0 || isActive || shouldTreatAsFinalized || !!taskSessionId)); @@ -2161,7 +2217,7 @@ const ToolPartContent: React.FC = ({ {isExpanded ? : } -
+
= ({ > {displayName} + {quickOpenTarget ? ( + + ) : null}
{normalizedPartTool === 'bash' && typeof effectiveTimeStart === 'number' ? ( diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index dc69740e..ea2e0881 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -2157,6 +2157,7 @@ export const dict = { 'chat.toolPart.showRawJson': 'Rohe JSON anzeigen', 'chat.toolPart.showFormattedJson': 'Formatierte JSON anzeigen', 'chat.toolPart.showNavigableJson': 'Navigierbare JSON anzeigen', + 'chat.toolPart.openFile': 'Datei öffnen', 'chat.toolPart.openFileAtFirstChange': 'Datei bei erster Änderung öffnen', 'chat.toolPart.openFileDiff': 'Datei-Unterschied öffnen', 'chat.toolPart.copyOutput': 'Ausgabe kopieren', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 3848730f..068fe31e 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2334,6 +2334,7 @@ export const dict = { 'chat.toolPart.showRawJson': 'Show raw JSON', 'chat.toolPart.showFormattedJson': 'Show formatted JSON', 'chat.toolPart.showNavigableJson': 'Show navigable JSON', + 'chat.toolPart.openFile': 'Open file', 'chat.toolPart.openFileAtFirstChange': 'Open file at first change', 'chat.toolPart.openFileDiff': 'Open file diff', 'chat.toolPart.copyOutput': 'Copy output', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 925f0972..fd4ffaa8 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -2300,6 +2300,7 @@ export const dict: Record = { "chat.toolPart.showRawJson": "Mostrar JSON sin formato", "chat.toolPart.showFormattedJson": "Mostrar JSON formateado", "chat.toolPart.showNavigableJson": "Mostrar JSON navegable", + "chat.toolPart.openFile": "Abrir archivo", "chat.toolPart.openFileAtFirstChange": "Abrir archivo en el primer cambio", "chat.toolPart.openFileDiff": "Abrir diferencias del archivo", "chat.toolPart.copyOutput": "Copiar salida", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index af006030..c5c04626 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -3063,6 +3063,7 @@ export const dict = { 'chat.toolPart.showRawJson': 'Afficher le JSON brut', 'chat.toolPart.showFormattedJson': 'Afficher le JSON formaté', 'chat.toolPart.showNavigableJson': 'Afficher le JSON navigable', + 'chat.toolPart.openFile': 'Ouvrir le fichier', 'chat.toolPart.openFileAtFirstChange': 'Ouvrir le fichier à la première modification', 'chat.toolPart.openFileDiff': 'Ouvrir les différences du fichier', 'chat.toolPart.copyOutput': 'Copier la sortie', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index ebe9b889..8e5b3f33 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -2333,6 +2333,7 @@ export const dict: Record = { 'chat.toolPart.showRawJson': '生JSONを表示', 'chat.toolPart.showFormattedJson': '整形JSONを表示', 'chat.toolPart.showNavigableJson': 'ナビゲーション可能なJSONを表示', + 'chat.toolPart.openFile': 'ファイルを開く', 'chat.toolPart.openFileAtFirstChange': '最初の変更箇所でファイルを開く', 'chat.toolPart.openFileDiff': 'ファイル差分を開く', 'chat.toolPart.copyOutput': '出力をコピー', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 6300cd13..de3ff5d3 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2334,6 +2334,7 @@ export const dict: Record = { 'chat.toolPart.showRawJson': '원시 JSON 표시', 'chat.toolPart.showFormattedJson': '형식화된 JSON 표시', 'chat.toolPart.showNavigableJson': '탐색 가능한 JSON 표시', + 'chat.toolPart.openFile': '파일 열기', 'chat.toolPart.openFileAtFirstChange': '첫 번째 변경 위치에서 파일 열기', 'chat.toolPart.openFileDiff': '파일 diff 열기', 'chat.toolPart.copyOutput': '출력 복사', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 666c1d73..e7401647 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1416,6 +1416,7 @@ export const dict: Record = { 'chat.toolPart.showRawJson': 'Pokaż surowy JSON', 'chat.toolPart.showFormattedJson': 'Pokaż sformatowany JSON', 'chat.toolPart.showNavigableJson': 'Pokaż nawigowalny JSON', + 'chat.toolPart.openFile': 'Otwórz plik', 'chat.toolPart.openFileAtFirstChange': 'Otwórz plik przy pierwszej zmianie', 'chat.toolPart.openFileDiff': 'Otwórz różnice pliku', 'chat.toolPart.copyOutput': 'Kopiuj wyjście', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 59c2ff9e..bd969d54 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2300,6 +2300,7 @@ export const dict: Record = { "chat.toolPart.showRawJson": "Mostrar JSON bruto", "chat.toolPart.showFormattedJson": "Mostrar JSON formatado", "chat.toolPart.showNavigableJson": "Mostrar JSON navegável", + "chat.toolPart.openFile": "Abrir arquivo", "chat.toolPart.openFileAtFirstChange": "Abrir arquivo na primeira alteração", "chat.toolPart.openFileDiff": "Abrir diferenças do arquivo", "chat.toolPart.copyOutput": "Copiar saída", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index a90cab4c..a5ac5edf 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2300,6 +2300,7 @@ export const dict: Record = { "chat.toolPart.showRawJson": "Показати сирий JSON", "chat.toolPart.showFormattedJson": "Показати форматований JSON", "chat.toolPart.showNavigableJson": "Показати навігаційний JSON", + "chat.toolPart.openFile": "Відкрити файл", "chat.toolPart.openFileAtFirstChange": "Відкрити файл на першій зміні", "chat.toolPart.openFileDiff": "Відкрити diff файлу", "chat.toolPart.copyOutput": "Скопіювати вивід", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 175b755b..fc9cfc72 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2300,6 +2300,7 @@ export const dict: Record = { 'chat.toolPart.showRawJson': '显示原始 JSON', 'chat.toolPart.showFormattedJson': '显示格式化 JSON', 'chat.toolPart.showNavigableJson': '显示可导航 JSON', + 'chat.toolPart.openFile': '打开文件', 'chat.toolPart.openFileAtFirstChange': '在首次更改处打开文件', 'chat.toolPart.openFileDiff': '打开文件差异', 'chat.toolPart.copyOutput': '复制输出', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index ae1d8a49..487d496b 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2304,6 +2304,7 @@ export const dict: Record = { 'chat.toolPart.showRawJson': '顯示原始 JSON', 'chat.toolPart.showFormattedJson': '顯示格式化 JSON', 'chat.toolPart.showNavigableJson': '顯示可導覽 JSON', + 'chat.toolPart.openFile': '開啟檔案', 'chat.toolPart.openFileAtFirstChange': '在首次變更處開啟檔案', 'chat.toolPart.openFileDiff': '開啟檔案差異', 'chat.toolPart.copyOutput': '複製輸出',