From f242519623ab5945eeae2dabbc2c0343e24db215 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 12 Jun 2026 11:09:22 +0800 Subject: [PATCH] Add Copy file path to file preview tab menus (#1473) * feat(app): add Copy file path to file preview tab menus * fix(app): restore copyAgentId translations accidentally replaced by placeholder --- .../app/src/components/split-container.tsx | 8 +++ packages/app/src/i18n/resources/ar.ts | 2 + packages/app/src/i18n/resources/en.ts | 2 + packages/app/src/i18n/resources/es.ts | 2 + packages/app/src/i18n/resources/fr.ts | 2 + packages/app/src/i18n/resources/ru.ts | 2 + packages/app/src/i18n/resources/zh-CN.ts | 2 + .../workspace/workspace-desktop-tabs-row.tsx | 9 ++++ .../screens/workspace/workspace-screen.tsx | 25 +++++++++ .../workspace/workspace-tab-menu.test.ts | 51 +++++++++++++++++++ .../screens/workspace/workspace-tab-menu.ts | 20 ++++++++ 11 files changed, 125 insertions(+) diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 2b9cac093..fe300b13d 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -86,6 +86,7 @@ interface SplitContainerProps { onCloseTab: (tabId: string) => Promise | void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTabsToLeft: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; @@ -364,6 +365,7 @@ export function SplitContainer({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTabsToLeft, @@ -580,6 +582,7 @@ export function SplitContainer({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onCopyFilePath={onCopyFilePath} onReloadAgent={onReloadAgent} onRenameTab={onRenameTab} onCloseTabsToLeft={onCloseTabsToLeft} @@ -722,6 +725,7 @@ function SplitNodeView({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTabsToLeft, @@ -774,6 +778,7 @@ function SplitNodeView({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onCopyFilePath={onCopyFilePath} onReloadAgent={onReloadAgent} onRenameTab={onRenameTab} onCloseTabsToLeft={onCloseTabsToLeft} @@ -819,6 +824,7 @@ function SplitNodeView({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onCopyFilePath={onCopyFilePath} onReloadAgent={onReloadAgent} onRenameTab={onRenameTab} onCloseTabsToLeft={onCloseTabsToLeft} @@ -870,6 +876,7 @@ function SplitPaneView({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTabsToLeft, @@ -1011,6 +1018,7 @@ function SplitPaneView({ onCloseTab={onCloseTab} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onCopyFilePath={onCopyFilePath} onReloadAgent={onReloadAgent} onRenameTab={onRenameTab} onCloseTabsToLeft={handleCloseTabsToLeft} diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 0480b043f..eb693b3ac 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -433,6 +433,7 @@ export const ar: TranslationResources = { openFor: "فتح القائمة لـ{{label}}", copyResumeCommand: "نسخ أمر السيرة الذاتية", copyAgentId: "نسخ معرف الوكيل", + copyFilePath: "Copy file path", rename: "إعادة تسمية", closeAbove: "إغلاق علامات التبويب أعلاه", closeBelow: "إغلاق علامات التبويب أدناه", @@ -465,6 +466,7 @@ export const ar: TranslationResources = { copyFailed: "فشل النسخ", agentIdCopiedLabel: "AgentID", resumeCommandCopiedLabel: "أمر الاستئناف", + filePathCopiedLabel: "File path", resumeIdUnavailable: "السيرة الذاتية ID غير متوفرة", resumeCommandUnavailable: "أمر الاستئناف غير متوفر", reloadingAgent: "وكيل إعادة التحميل...", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index bcbf724a0..14c7e0d63 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -432,6 +432,7 @@ export const en = { openFor: "Open menu for {{label}}", copyResumeCommand: "Copy resume command", copyAgentId: "Copy agent id", + copyFilePath: "Copy file path", rename: "Rename", closeAbove: "Close tabs above", closeBelow: "Close tabs below", @@ -464,6 +465,7 @@ export const en = { copyFailed: "Copy failed", agentIdCopiedLabel: "Agent ID", resumeCommandCopiedLabel: "resume command", + filePathCopiedLabel: "File path", resumeIdUnavailable: "Resume ID not available", resumeCommandUnavailable: "Resume command not available", reloadingAgent: "Reloading agent...", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 6f7b09aa2..68a536f17 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -436,6 +436,7 @@ export const es: TranslationResources = { openFor: "Menú abierto para{{label}}", copyResumeCommand: "Copiar comando de reanudación", copyAgentId: "Copiar ID del agente", + copyFilePath: "Copy file path", rename: "Rebautizar", closeAbove: "Cerrar pestañas arriba", closeBelow: "Cerrar pestañas a continuación", @@ -469,6 +470,7 @@ export const es: TranslationResources = { copyFailed: "Copia fallida", agentIdCopiedLabel: "AgentID", resumeCommandCopiedLabel: "reanudar el comando", + filePathCopiedLabel: "File path", resumeIdUnavailable: "ReanudarIDno disponible", resumeCommandUnavailable: "Comando de reanudación no disponible", reloadingAgent: "Agente de recarga...", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index de23eaae3..e16ca8637 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -436,6 +436,7 @@ export const fr: TranslationResources = { openFor: "Ouvrir le menu pour{{label}}", copyResumeCommand: "Copier la commande de reprise", copyAgentId: "Copier l'identifiant de l'agent", + copyFilePath: "Copy file path", rename: "Rebaptiser", closeAbove: "Fermer les onglets ci-dessus", closeBelow: "Fermer les onglets ci-dessous", @@ -469,6 +470,7 @@ export const fr: TranslationResources = { copyFailed: "Échec de la copie", agentIdCopiedLabel: "AgentID", resumeCommandCopiedLabel: "reprendre la commande", + filePathCopiedLabel: "File path", resumeIdUnavailable: "ReprendreIDnon disponible", resumeCommandUnavailable: "Commande de reprise non disponible", reloadingAgent: "Agent de rechargement...", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 61487037e..88c460942 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -436,6 +436,7 @@ export const ru: TranslationResources = { openFor: "Открыть меню для{{label}}", copyResumeCommand: "Копировать команду возобновления", copyAgentId: "Скопировать идентификатор агента", + copyFilePath: "Copy file path", rename: "Переименовать", closeAbove: "Закрыть вкладки выше", closeBelow: "Закройте вкладки ниже", @@ -468,6 +469,7 @@ export const ru: TranslationResources = { copyFailed: "Не удалось скопировать", agentIdCopiedLabel: "AgentID", resumeCommandCopiedLabel: "команда возобновления", + filePathCopiedLabel: "File path", resumeIdUnavailable: "Резюме ID недоступно", resumeCommandUnavailable: "Команда возобновления недоступна", reloadingAgent: "Перезагрузка агента...", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index f1c131e5c..c2687b79a 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -433,6 +433,7 @@ export const zhCN: TranslationResources = { openFor: "打开 {{label}} 的菜单", copyResumeCommand: "复制恢复命令", copyAgentId: "复制 Agent ID", + copyFilePath: "Copy file path", rename: "重命名", closeAbove: "关闭上方标签", closeBelow: "关闭下方标签", @@ -465,6 +466,7 @@ export const zhCN: TranslationResources = { copyFailed: "复制失败", agentIdCopiedLabel: "Agent ID", resumeCommandCopiedLabel: "恢复命令", + filePathCopiedLabel: "File path", resumeIdUnavailable: "恢复 ID 不可用", resumeCommandUnavailable: "恢复命令不可用", reloadingAgent: "正在重新加载 Agent...", diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index 77a692592..043e4033d 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -205,6 +205,7 @@ interface WorkspaceDesktopTabsRowProps { onCloseTab: (tabId: string) => Promise | void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTabsToLeft: (tabId: string) => Promise | void; @@ -519,6 +520,7 @@ export function WorkspaceDesktopTabsRow({ onCloseTab, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTabsToLeft, @@ -590,6 +592,7 @@ export function WorkspaceDesktopTabsRow({ () => ({ copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"), copyAgentId: t("workspace.tabs.menu.copyAgentId"), + copyFilePath: t("workspace.tabs.menu.copyFilePath"), rename: t("workspace.tabs.menu.rename"), closeAbove: t("workspace.tabs.menu.closeAbove"), closeBelow: t("workspace.tabs.menu.closeBelow"), @@ -684,6 +687,7 @@ export function WorkspaceDesktopTabsRow({ normalizedWorkspaceId={normalizedWorkspaceId} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onCopyFilePath={onCopyFilePath} onReloadAgent={onReloadAgent} onRenameTab={onRenameTab} onCloseTabsToLeft={onCloseTabsToLeft} @@ -714,6 +718,7 @@ export function WorkspaceDesktopTabsRow({ onCloseTabsToLeft, onCloseTabsToRight, onCopyAgentId, + onCopyFilePath, onCopyResumeCommand, onNavigateTab, onReloadAgent, @@ -894,6 +899,7 @@ function ResolvedDesktopTabChip({ normalizedWorkspaceId, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTabsToLeft, @@ -919,6 +925,7 @@ function ResolvedDesktopTabChip({ normalizedWorkspaceId: string; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTabsToLeft: (tabId: string) => Promise | void; @@ -944,6 +951,7 @@ function ResolvedDesktopTabChip({ tabCount, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, @@ -960,6 +968,7 @@ function ResolvedDesktopTabChip({ onCloseTabsToLeft, onCloseTabsToRight, onCopyAgentId, + onCopyFilePath, onCopyResumeCommand, labels, onReloadAgent, diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index fc6344b29..34a2258af 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -365,6 +365,7 @@ interface MobileWorkspaceTabSwitcherProps { onSelectSwitcherTab: (key: string) => void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTab: (tabId: string) => Promise | void; @@ -552,6 +553,7 @@ function MobileWorkspaceTabOption({ onPress, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, @@ -569,6 +571,7 @@ function MobileWorkspaceTabOption({ onPress: () => void; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTab: (tabId: string) => Promise | void; @@ -581,6 +584,7 @@ function MobileWorkspaceTabOption({ () => ({ copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"), copyAgentId: t("workspace.tabs.menu.copyAgentId"), + copyFilePath: t("workspace.tabs.menu.copyFilePath"), rename: t("workspace.tabs.menu.rename"), closeAbove: t("workspace.tabs.menu.closeAbove"), closeBelow: t("workspace.tabs.menu.closeBelow"), @@ -602,6 +606,7 @@ function MobileWorkspaceTabOption({ menuTestIDBase, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, @@ -668,6 +673,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ onSelectSwitcherTab, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, @@ -723,6 +729,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ onPress={onPress} onCopyResumeCommand={onCopyResumeCommand} onCopyAgentId={onCopyAgentId} + onCopyFilePath={onCopyFilePath} onReloadAgent={onReloadAgent} onRenameTab={onRenameTab} onCloseTab={onCloseTab} @@ -740,6 +747,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({ normalizedWorkspaceId, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, @@ -2600,6 +2608,19 @@ function WorkspaceScreenContent({ [toast, t], ); + const handleCopyFilePath = useCallback( + async (path: string) => { + if (!path) return; + try { + await Clipboard.setStringAsync(path); + toast.copied(t("workspace.tabs.toasts.filePathCopiedLabel")); + } catch { + toast.error(t("workspace.tabs.toasts.copyFailed")); + } + }, + [toast, t], + ); + const handleCopyResumeCommand = useCallback( async (agentId: string) => { if (!agentId) return; @@ -3422,6 +3443,7 @@ function WorkspaceScreenContent({ onCloseTab={handleCloseTabById} onCopyResumeCommand={handleCopyResumeCommand} onCopyAgentId={handleCopyAgentId} + onCopyFilePath={handleCopyFilePath} onReloadAgent={handleReloadAgent} onRenameTab={handleRenameTab} onCloseTabsToLeft={handleCloseTabsToLeftInPane} @@ -3456,6 +3478,7 @@ function WorkspaceScreenContent({ handleCloseTabById, handleCopyResumeCommand, handleCopyAgentId, + handleCopyFilePath, handleReloadAgent, handleRenameTab, handleCloseTabsToLeftInPane, @@ -3535,6 +3558,7 @@ function WorkspaceScreenContent({ onSelectSwitcherTab={handleSelectSwitcherTab} onCopyResumeCommand={handleCopyResumeCommand} onCopyAgentId={handleCopyAgentId} + onCopyFilePath={handleCopyFilePath} onReloadAgent={handleReloadAgent} onRenameTab={handleRenameTab} onCloseTab={handleCloseTabById} @@ -3556,6 +3580,7 @@ function WorkspaceScreenContent({ onCloseTab={handleCloseTabById} onCopyResumeCommand={handleCopyResumeCommand} onCopyAgentId={handleCopyAgentId} + onCopyFilePath={handleCopyFilePath} onReloadAgent={handleReloadAgent} onRenameTab={handleRenameTab} onCloseTabsToLeft={handleCloseTabsToLeft} diff --git a/packages/app/src/screens/workspace/workspace-tab-menu.test.ts b/packages/app/src/screens/workspace/workspace-tab-menu.test.ts index 6c1c2e108..68325ee74 100644 --- a/packages/app/src/screens/workspace/workspace-tab-menu.test.ts +++ b/packages/app/src/screens/workspace/workspace-tab-menu.test.ts @@ -15,6 +15,7 @@ describe("buildWorkspaceTabMenuEntries", () => { it("uses desktop tab ordering labels for desktop menus", () => { const onCopyResumeCommand = vi.fn(); const onCopyAgentId = vi.fn(); + const onCopyFilePath = vi.fn(); const onReloadAgent = vi.fn(); const onRenameTab = vi.fn(); const onCloseTab = vi.fn(); @@ -30,6 +31,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-context-agent_123", onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, @@ -59,6 +61,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-menu-agent_123", onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onCopyFilePath: vi.fn(), onReloadAgent: vi.fn(), onRenameTab: vi.fn(), onCloseTab: vi.fn(), @@ -93,6 +96,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-menu-draft_123", onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onCopyFilePath: vi.fn(), onReloadAgent: vi.fn(), onRenameTab: vi.fn(), onCloseTab: vi.fn(), @@ -120,6 +124,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-context-agent_123", onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onCopyFilePath: vi.fn(), onReloadAgent: vi.fn(), onRenameTab: vi.fn(), onCloseTab: vi.fn(), @@ -148,6 +153,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-context-agent_123", onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onCopyFilePath: vi.fn(), onReloadAgent: vi.fn(), onRenameTab, onCloseTab: vi.fn(), @@ -181,6 +187,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase: "workspace-tab-context-terminal_abc", onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onCopyFilePath: vi.fn(), onReloadAgent: vi.fn(), onRenameTab, onCloseTab: vi.fn(), @@ -193,6 +200,7 @@ describe("buildWorkspaceTabMenuEntries", () => { expect(labels[0]).toBe("Rename"); expect(labels).not.toContain("Copy resume command"); expect(labels).not.toContain("Copy agent id"); + expect(labels).not.toContain("Copy file path"); expect(labels).not.toContain("Reload agent"); const renameEntry = entries.find((entry) => entry.kind === "item" && entry.label === "Rename"); @@ -203,6 +211,48 @@ describe("buildWorkspaceTabMenuEntries", () => { expect(onRenameTab).toHaveBeenCalledWith(terminalTab); }); + it("includes copy file path for file tabs", () => { + const onCopyFilePath = vi.fn(); + const fileTab: WorkspaceTabDescriptor = { + key: "file_abc", + tabId: "file_abc", + kind: "file", + target: { kind: "file", path: "/some/path.ts", lineStart: 1, lineEnd: 10 }, + }; + const entries = buildWorkspaceTabMenuEntries({ + surface: "desktop", + tab: fileTab, + index: 0, + tabCount: 1, + menuTestIDBase: "workspace-tab-context-file_abc", + onCopyResumeCommand: vi.fn(), + onCopyAgentId: vi.fn(), + onCopyFilePath, + onReloadAgent: vi.fn(), + onRenameTab: vi.fn(), + onCloseTab: vi.fn(), + onCloseTabsBefore: vi.fn(), + onCloseTabsAfter: vi.fn(), + onCloseOtherTabs: vi.fn(), + }); + + const labels = entries.filter((entry) => entry.kind === "item").map((entry) => entry.label); + expect(labels[0]).toBe("Copy file path"); + expect(labels).not.toContain("Copy resume command"); + expect(labels).not.toContain("Copy agent id"); + expect(labels).not.toContain("Rename"); + expect(labels).not.toContain("Reload agent"); + + const copyFilePathEntry = entries.find( + (entry) => entry.kind === "item" && entry.key === "copy-file-path", + ); + if (!copyFilePathEntry || copyFilePathEntry.kind !== "item") { + throw new Error("Copy file path entry missing"); + } + copyFilePathEntry.onSelect(); + expect(onCopyFilePath).toHaveBeenCalledWith("/some/path.ts"); + }); + it("uses the same rename entry shape for agent and terminal tabs", () => { const terminalTab: WorkspaceTabDescriptor = { key: "terminal_abc", @@ -218,6 +268,7 @@ describe("buildWorkspaceTabMenuEntries", () => { menuTestIDBase, onCopyResumeCommand: vi.fn(), onCopyAgentId: vi.fn(), + onCopyFilePath: vi.fn(), onReloadAgent: vi.fn(), onRenameTab: vi.fn(), onCloseTab: vi.fn(), diff --git a/packages/app/src/screens/workspace/workspace-tab-menu.ts b/packages/app/src/screens/workspace/workspace-tab-menu.ts index 5f7587452..9375582fb 100644 --- a/packages/app/src/screens/workspace/workspace-tab-menu.ts +++ b/packages/app/src/screens/workspace/workspace-tab-menu.ts @@ -8,6 +8,7 @@ export type WorkspaceTabMenuSurface = "desktop" | "mobile"; export interface WorkspaceTabMenuLabels { copyResumeCommand: string; copyAgentId: string; + copyFilePath: string; rename: string; closeAbove: string; closeBelow: string; @@ -22,6 +23,7 @@ export interface WorkspaceTabMenuLabels { export const DEFAULT_WORKSPACE_TAB_MENU_LABELS: WorkspaceTabMenuLabels = { copyResumeCommand: i18n.t("workspace.tabs.menu.copyResumeCommand"), copyAgentId: i18n.t("workspace.tabs.menu.copyAgentId"), + copyFilePath: i18n.t("workspace.tabs.menu.copyFilePath"), rename: i18n.t("workspace.tabs.menu.rename"), closeAbove: i18n.t("workspace.tabs.menu.closeAbove"), closeBelow: i18n.t("workspace.tabs.menu.closeBelow"), @@ -66,6 +68,7 @@ interface BuildWorkspaceTabMenuEntriesInput { menuTestIDBase: string; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTab: (tabId: string) => Promise | void; @@ -81,6 +84,7 @@ interface BuildWorkspaceDesktopTabActionsInput { tabCount: number; onCopyResumeCommand: (agentId: string) => Promise | void; onCopyAgentId: (agentId: string) => Promise | void; + onCopyFilePath: (path: string) => Promise | void; onReloadAgent: (agentId: string) => Promise | void; onRenameTab: (tab: WorkspaceTabDescriptor) => void; onCloseTab: (tabId: string) => Promise | void; @@ -148,6 +152,7 @@ export function buildWorkspaceTabMenuEntries( menuTestIDBase, onCopyResumeCommand, onCopyAgentId, + onCopyFilePath, onReloadAgent, onRenameTab, onCloseTab, @@ -186,6 +191,20 @@ export function buildWorkspaceTabMenuEntries( }); } + if (tab.target.kind === "file") { + const filePath = tab.target.path; + entries.push({ + kind: "item", + key: "copy-file-path", + label: labels.copyFilePath, + icon: "copy", + testID: `${menuTestIDBase}-copy-file-path`, + onSelect: () => { + void onCopyFilePath(filePath); + }, + }); + } + if (tab.target.kind === "agent" || tab.target.kind === "terminal") { entries.push({ kind: "item", @@ -278,6 +297,7 @@ export function buildWorkspaceDesktopTabActions( menuTestIDBase: contextMenuTestId, onCopyResumeCommand: input.onCopyResumeCommand, onCopyAgentId: input.onCopyAgentId, + onCopyFilePath: input.onCopyFilePath, onReloadAgent: input.onReloadAgent, onRenameTab: input.onRenameTab, onCloseTab: input.onCloseTab,