feat(app): copy terminal IDs from tab menus (#2371)

This commit is contained in:
Mohamed Boudra
2026-07-24 15:12:29 +02:00
committed by GitHub
parent 21404fbdec
commit 7250009ab7
14 changed files with 148 additions and 7 deletions
@@ -419,6 +419,7 @@ interface WorkspaceDesktopTabsRowProps {
onCloseTab: (tabId: string) => Promise<void> | void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -764,6 +765,7 @@ export function WorkspaceDesktopTabsRow({
onCloseTab,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -847,6 +849,7 @@ export function WorkspaceDesktopTabsRow({
() => ({
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
copyAgentId: t("workspace.tabs.menu.copyAgentId"),
copyTerminalId: t("workspace.tabs.menu.copyTerminalId"),
copyFilePath: t("workspace.tabs.menu.copyFilePath"),
rename: t("workspace.tabs.menu.rename"),
closeAbove: t("workspace.tabs.menu.closeAbove"),
@@ -945,6 +948,7 @@ export function WorkspaceDesktopTabsRow({
normalizedWorkspaceId={normalizedWorkspaceId}
onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab}
@@ -976,6 +980,7 @@ export function WorkspaceDesktopTabsRow({
onCloseTabsToLeft,
onCloseTabsToRight,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onCopyResumeCommand,
onNavigateTab,
@@ -1097,6 +1102,7 @@ function ResolvedDesktopTabChip({
normalizedWorkspaceId,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -1123,6 +1129,7 @@ function ResolvedDesktopTabChip({
normalizedWorkspaceId: string;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -1149,6 +1156,7 @@ function ResolvedDesktopTabChip({
tabCount,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -1166,6 +1174,7 @@ function ResolvedDesktopTabChip({
onCloseTabsToLeft,
onCloseTabsToRight,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onCopyResumeCommand,
labels,
@@ -424,6 +424,7 @@ interface MobileWorkspaceTabSwitcherProps {
onSelectSwitcherTab: (key: string) => void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -612,6 +613,7 @@ function MobileWorkspaceTabOption({
onPress,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -630,6 +632,7 @@ function MobileWorkspaceTabOption({
onPress: () => void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -643,6 +646,7 @@ function MobileWorkspaceTabOption({
() => ({
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
copyAgentId: t("workspace.tabs.menu.copyAgentId"),
copyTerminalId: t("workspace.tabs.menu.copyTerminalId"),
copyFilePath: t("workspace.tabs.menu.copyFilePath"),
rename: t("workspace.tabs.menu.rename"),
closeAbove: t("workspace.tabs.menu.closeAbove"),
@@ -665,6 +669,7 @@ function MobileWorkspaceTabOption({
menuTestIDBase,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -733,6 +738,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
onSelectSwitcherTab,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -789,6 +795,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
onPress={onPress}
onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab}
@@ -807,6 +814,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
normalizedWorkspaceId,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -2772,6 +2780,19 @@ function WorkspaceScreenContent({
[toast, t],
);
const handleCopyTerminalId = useCallback(
async (terminalId: string) => {
if (!terminalId) return;
try {
await Clipboard.setStringAsync(terminalId);
toast.copied(t("workspace.tabs.toasts.terminalIdCopiedLabel"));
} catch {
toast.error(t("workspace.tabs.toasts.copyFailed"));
}
},
[toast, t],
);
const handleCopyFilePath = useCallback(
async (path: string) => {
if (!path) return;
@@ -3645,6 +3666,7 @@ function WorkspaceScreenContent({
onCloseTab={handleCloseTabById}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab}
@@ -3681,6 +3703,7 @@ function WorkspaceScreenContent({
handleCloseTabById,
handleCopyResumeCommand,
handleCopyAgentId,
handleCopyTerminalId,
handleCopyFilePath,
handleReloadAgent,
handleRenameTab,
@@ -3762,6 +3785,7 @@ function WorkspaceScreenContent({
onSelectSwitcherTab={handleSelectSwitcherTab}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab}
@@ -3784,6 +3808,7 @@ function WorkspaceScreenContent({
onCloseTab={handleCloseTabById}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab}
@@ -34,6 +34,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-agent_123",
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId: vi.fn(),
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -64,6 +65,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-menu-agent_123",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(),
onRenameTab: vi.fn(),
@@ -99,6 +101,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-menu-draft_123",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(),
onRenameTab: vi.fn(),
@@ -127,6 +130,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-agent_123",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(),
onRenameTab: vi.fn(),
@@ -156,6 +160,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-agent_123",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(),
onRenameTab,
@@ -174,8 +179,9 @@ describe("buildWorkspaceTabMenuEntries", () => {
expect(onRenameTab).toHaveBeenCalledWith(tab);
});
it("includes rename as the first entry for terminal tabs", () => {
it("includes copy id and rename for terminal tabs", () => {
const onRenameTab = vi.fn();
const onCopyTerminalId = vi.fn();
const terminalTab: WorkspaceTabDescriptor = {
key: "terminal_abc",
tabId: "terminal_abc",
@@ -190,6 +196,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-terminal_abc",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId,
onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(),
onRenameTab,
@@ -200,12 +207,22 @@ describe("buildWorkspaceTabMenuEntries", () => {
});
const labels = entries.filter((entry) => entry.kind === "item").map((entry) => entry.label);
expect(labels[0]).toBe("Rename");
expect(labels[0]).toBe("Copy terminal id");
expect(labels[1]).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 copyTerminalIdEntry = entries.find(
(entry) => entry.kind === "item" && entry.key === "copy-terminal-id",
);
if (!copyTerminalIdEntry || copyTerminalIdEntry.kind !== "item") {
throw new Error("Copy terminal id entry missing");
}
copyTerminalIdEntry.onSelect();
expect(onCopyTerminalId).toHaveBeenCalledWith("terminal-abc");
const renameEntry = entries.find((entry) => entry.kind === "item" && entry.label === "Rename");
if (!renameEntry || renameEntry.kind !== "item") {
throw new Error("Rename entry missing");
@@ -230,6 +247,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-file_abc",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath,
onReloadAgent: vi.fn(),
onRenameTab: vi.fn(),
@@ -272,6 +290,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
tabCount: 1,
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(),
onRenameTab: vi.fn(),
@@ -302,6 +321,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase,
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(),
onRenameTab: vi.fn(),
@@ -8,6 +8,7 @@ export type WorkspaceTabMenuSurface = "desktop" | "mobile";
export interface WorkspaceTabMenuLabels {
copyResumeCommand: string;
copyAgentId: string;
copyTerminalId: string;
copyFilePath: string;
rename: string;
closeAbove: string;
@@ -23,6 +24,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"),
copyTerminalId: i18n.t("workspace.tabs.menu.copyTerminalId"),
copyFilePath: i18n.t("workspace.tabs.menu.copyFilePath"),
rename: i18n.t("workspace.tabs.menu.rename"),
closeAbove: i18n.t("workspace.tabs.menu.closeAbove"),
@@ -68,6 +70,7 @@ interface BuildWorkspaceTabMenuEntriesInput {
menuTestIDBase: string;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -84,6 +87,7 @@ interface BuildWorkspaceDesktopTabActionsInput {
tabCount: number;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -161,6 +165,7 @@ export function buildWorkspaceTabMenuEntries(
menuTestIDBase,
onCopyResumeCommand,
onCopyAgentId,
onCopyTerminalId,
onCopyFilePath,
onReloadAgent,
onRenameTab,
@@ -200,6 +205,21 @@ export function buildWorkspaceTabMenuEntries(
});
}
if (tab.target.kind === "terminal") {
const { terminalId } = tab.target;
entries.push({
kind: "item",
key: "copy-terminal-id",
label: labels.copyTerminalId,
icon: "copy",
hint: terminalId.slice(0, 7),
testID: `${menuTestIDBase}-copy-terminal-id`,
onSelect: () => {
void onCopyTerminalId(terminalId);
},
});
}
if (tab.target.kind === "file") {
const filePath = tab.target.path;
entries.push({
@@ -306,6 +326,7 @@ export function buildWorkspaceDesktopTabActions(
menuTestIDBase: contextMenuTestId,
onCopyResumeCommand: input.onCopyResumeCommand,
onCopyAgentId: input.onCopyAgentId,
onCopyTerminalId: input.onCopyTerminalId,
onCopyFilePath: input.onCopyFilePath,
onReloadAgent: input.onReloadAgent,
onRenameTab: input.onRenameTab,