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
@@ -1,4 +1,4 @@
import { test, expect } from "./fixtures"; import { test, expect, type Page } from "./fixtures";
import { clickNewTerminal, gotoWorkspace } from "./helpers/launcher"; import { clickNewTerminal, gotoWorkspace } from "./helpers/launcher";
import { renameModalInput, renameModalSubmit } from "./helpers/rename"; import { renameModalInput, renameModalSubmit } from "./helpers/rename";
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
@@ -36,7 +36,52 @@ async function waitForCreatedTerminalId(workspace: SeededWorkspace): Promise<str
return terminal.id; return terminal.id;
} }
async function cleanupTerminal(
workspace: SeededWorkspace,
terminalId: string | null,
): Promise<void> {
if (terminalId) {
await workspace.client.killTerminal(terminalId).catch(() => undefined);
}
await workspace.cleanup();
}
async function readClipboard(page: Page): Promise<string> {
return page.evaluate(() => navigator.clipboard.readText());
}
test.describe("Workspace terminal tab rename", () => { test.describe("Workspace terminal tab rename", () => {
test("right-click copy terminal id writes the terminal id to the clipboard", async ({
context,
page,
}) => {
test.setTimeout(60_000);
const workspace = await seedWorkspace({ repoPrefix: "workspace-terminal-copy-id-" });
let terminalId: string | null = null;
try {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await gotoWorkspace(page, workspace.workspaceId);
await clickNewTerminal(page);
terminalId = await waitForCreatedTerminalId(workspace);
const tab = page.getByTestId(`workspace-tab-terminal_${terminalId}`).first();
await expect(tab).toBeVisible({ timeout: 15_000 });
await tab.click({ button: "right" });
const copyTerminalId = page.getByTestId(
`workspace-tab-context-terminal_${terminalId}-copy-terminal-id`,
);
await expect(copyTerminalId).toBeVisible({ timeout: 10_000 });
await copyTerminalId.click();
await expect.poll(() => readClipboard(page)).toBe(terminalId);
} finally {
await cleanupTerminal(workspace, terminalId);
}
});
test("right-click rename persists the terminal title and updates the tab label", async ({ test("right-click rename persists the terminal title and updates the tab label", async ({
page, page,
}) => { }) => {
@@ -74,10 +119,7 @@ test.describe("Workspace terminal tab rename", () => {
.poll(() => fetchTerminalTitle(workspace, terminalId!)) .poll(() => fetchTerminalTitle(workspace, terminalId!))
.toBe("My Renamed Terminal"); .toBe("My Renamed Terminal");
} finally { } finally {
if (terminalId) { await cleanupTerminal(workspace, terminalId);
await workspace.client.killTerminal(terminalId).catch(() => undefined);
}
await workspace.cleanup();
} }
}); });
}); });
@@ -95,6 +95,7 @@ interface SplitContainerProps {
onCloseTab: (tabId: string) => Promise<void> | void; onCloseTab: (tabId: string) => Promise<void> | void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void; onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void; onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void; onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void; onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void; onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -373,6 +374,7 @@ export function SplitContainer({
onCloseTab, onCloseTab,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -592,6 +594,7 @@ export function SplitContainer({
onCloseTab={onCloseTab} onCloseTab={onCloseTab}
onCopyResumeCommand={onCopyResumeCommand} onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId} onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent} onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab} onRenameTab={onRenameTab}
@@ -738,6 +741,7 @@ function SplitNodeView({
onCloseTab, onCloseTab,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -795,6 +799,7 @@ function SplitNodeView({
onCloseTab={onCloseTab} onCloseTab={onCloseTab}
onCopyResumeCommand={onCopyResumeCommand} onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId} onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent} onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab} onRenameTab={onRenameTab}
@@ -844,6 +849,7 @@ function SplitNodeView({
onCloseTab={onCloseTab} onCloseTab={onCloseTab}
onCopyResumeCommand={onCopyResumeCommand} onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId} onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent} onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab} onRenameTab={onRenameTab}
@@ -899,6 +905,7 @@ function SplitPaneView({
onCloseTab, onCloseTab,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -1044,6 +1051,7 @@ function SplitPaneView({
onCloseTab={onCloseTab} onCloseTab={onCloseTab}
onCopyResumeCommand={onCopyResumeCommand} onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId} onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent} onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab} onRenameTab={onRenameTab}
+2
View File
@@ -492,6 +492,7 @@ export const ar: TranslationResources = {
openFor: "فتح القائمة لـ{{label}}", openFor: "فتح القائمة لـ{{label}}",
copyResumeCommand: "نسخ أمر السيرة الذاتية", copyResumeCommand: "نسخ أمر السيرة الذاتية",
copyAgentId: "نسخ معرف الوكيل", copyAgentId: "نسخ معرف الوكيل",
copyTerminalId: "نسخ معرف المحطة",
copyFilePath: "Copy file path", copyFilePath: "Copy file path",
rename: "إعادة تسمية", rename: "إعادة تسمية",
closeAbove: "إغلاق علامات التبويب أعلاه", closeAbove: "إغلاق علامات التبويب أعلاه",
@@ -529,6 +530,7 @@ export const ar: TranslationResources = {
toasts: { toasts: {
copyFailed: "فشل النسخ", copyFailed: "فشل النسخ",
agentIdCopiedLabel: "AgentID", agentIdCopiedLabel: "AgentID",
terminalIdCopiedLabel: "معرف المحطة",
resumeCommandCopiedLabel: "أمر الاستئناف", resumeCommandCopiedLabel: "أمر الاستئناف",
filePathCopiedLabel: "File path", filePathCopiedLabel: "File path",
resumeIdUnavailable: "السيرة الذاتية ID غير متوفرة", resumeIdUnavailable: "السيرة الذاتية ID غير متوفرة",
+2
View File
@@ -491,6 +491,7 @@ export const en = {
openFor: "Open menu for {{label}}", openFor: "Open menu for {{label}}",
copyResumeCommand: "Copy resume command", copyResumeCommand: "Copy resume command",
copyAgentId: "Copy agent id", copyAgentId: "Copy agent id",
copyTerminalId: "Copy terminal id",
copyFilePath: "Copy file path", copyFilePath: "Copy file path",
rename: "Rename", rename: "Rename",
closeAbove: "Close tabs above", closeAbove: "Close tabs above",
@@ -528,6 +529,7 @@ export const en = {
toasts: { toasts: {
copyFailed: "Copy failed", copyFailed: "Copy failed",
agentIdCopiedLabel: "Agent ID", agentIdCopiedLabel: "Agent ID",
terminalIdCopiedLabel: "Terminal ID",
resumeCommandCopiedLabel: "resume command", resumeCommandCopiedLabel: "resume command",
filePathCopiedLabel: "File path", filePathCopiedLabel: "File path",
resumeIdUnavailable: "Resume ID not available", resumeIdUnavailable: "Resume ID not available",
+2
View File
@@ -496,6 +496,7 @@ export const es: TranslationResources = {
openFor: "Menú abierto para{{label}}", openFor: "Menú abierto para{{label}}",
copyResumeCommand: "Copiar comando de reanudación", copyResumeCommand: "Copiar comando de reanudación",
copyAgentId: "Copiar ID del agente", copyAgentId: "Copiar ID del agente",
copyTerminalId: "Copiar ID del terminal",
copyFilePath: "Copy file path", copyFilePath: "Copy file path",
rename: "Rebautizar", rename: "Rebautizar",
closeAbove: "Cerrar pestañas arriba", closeAbove: "Cerrar pestañas arriba",
@@ -534,6 +535,7 @@ export const es: TranslationResources = {
toasts: { toasts: {
copyFailed: "Copia fallida", copyFailed: "Copia fallida",
agentIdCopiedLabel: "AgentID", agentIdCopiedLabel: "AgentID",
terminalIdCopiedLabel: "ID del terminal",
resumeCommandCopiedLabel: "reanudar el comando", resumeCommandCopiedLabel: "reanudar el comando",
filePathCopiedLabel: "File path", filePathCopiedLabel: "File path",
resumeIdUnavailable: "ReanudarIDno disponible", resumeIdUnavailable: "ReanudarIDno disponible",
+2
View File
@@ -496,6 +496,7 @@ export const fr: TranslationResources = {
openFor: "Ouvrir le menu pour{{label}}", openFor: "Ouvrir le menu pour{{label}}",
copyResumeCommand: "Copier la commande de reprise", copyResumeCommand: "Copier la commande de reprise",
copyAgentId: "Copier l'identifiant de l'agent", copyAgentId: "Copier l'identifiant de l'agent",
copyTerminalId: "Copier l'identifiant du terminal",
copyFilePath: "Copy file path", copyFilePath: "Copy file path",
rename: "Rebaptiser", rename: "Rebaptiser",
closeAbove: "Fermer les onglets ci-dessus", closeAbove: "Fermer les onglets ci-dessus",
@@ -534,6 +535,7 @@ export const fr: TranslationResources = {
toasts: { toasts: {
copyFailed: "Échec de la copie", copyFailed: "Échec de la copie",
agentIdCopiedLabel: "AgentID", agentIdCopiedLabel: "AgentID",
terminalIdCopiedLabel: "Identifiant du terminal",
resumeCommandCopiedLabel: "reprendre la commande", resumeCommandCopiedLabel: "reprendre la commande",
filePathCopiedLabel: "File path", filePathCopiedLabel: "File path",
resumeIdUnavailable: "ReprendreIDnon disponible", resumeIdUnavailable: "ReprendreIDnon disponible",
+2
View File
@@ -496,6 +496,7 @@ export const ja: TranslationResources = {
openFor: "{{label}}のメニューを開く", openFor: "{{label}}のメニューを開く",
copyResumeCommand: "再開コマンドをコピー", copyResumeCommand: "再開コマンドをコピー",
copyAgentId: "エージェントIDをコピー", copyAgentId: "エージェントIDをコピー",
copyTerminalId: "ターミナルIDをコピー",
copyFilePath: "ファイルパスをコピー", copyFilePath: "ファイルパスをコピー",
rename: "名前を変更", rename: "名前を変更",
closeAbove: "上のタブを閉じる", closeAbove: "上のタブを閉じる",
@@ -534,6 +535,7 @@ export const ja: TranslationResources = {
toasts: { toasts: {
copyFailed: "コピーに失敗しました", copyFailed: "コピーに失敗しました",
agentIdCopiedLabel: "エージェントID", agentIdCopiedLabel: "エージェントID",
terminalIdCopiedLabel: "ターミナルID",
resumeCommandCopiedLabel: "再開コマンド", resumeCommandCopiedLabel: "再開コマンド",
filePathCopiedLabel: "ファイルパス", filePathCopiedLabel: "ファイルパス",
resumeIdUnavailable: "再開IDが利用できません", resumeIdUnavailable: "再開IDが利用できません",
+2
View File
@@ -496,6 +496,7 @@ export const ptBR: TranslationResources = {
openFor: "Abrir menu de {{label}}", openFor: "Abrir menu de {{label}}",
copyResumeCommand: "Copiar comando de retomada", copyResumeCommand: "Copiar comando de retomada",
copyAgentId: "Copiar ID do agente", copyAgentId: "Copiar ID do agente",
copyTerminalId: "Copiar ID do terminal",
copyFilePath: "Copiar caminho do arquivo", copyFilePath: "Copiar caminho do arquivo",
rename: "Renomear", rename: "Renomear",
closeAbove: "Fechar abas acima", closeAbove: "Fechar abas acima",
@@ -533,6 +534,7 @@ export const ptBR: TranslationResources = {
toasts: { toasts: {
copyFailed: "Falha ao copiar", copyFailed: "Falha ao copiar",
agentIdCopiedLabel: "ID do agente", agentIdCopiedLabel: "ID do agente",
terminalIdCopiedLabel: "ID do terminal",
resumeCommandCopiedLabel: "comando de retomada", resumeCommandCopiedLabel: "comando de retomada",
filePathCopiedLabel: "Caminho do arquivo", filePathCopiedLabel: "Caminho do arquivo",
resumeIdUnavailable: "ID de retomada indisponível", resumeIdUnavailable: "ID de retomada indisponível",
+2
View File
@@ -496,6 +496,7 @@ export const ru: TranslationResources = {
openFor: "Открыть меню для{{label}}", openFor: "Открыть меню для{{label}}",
copyResumeCommand: "Копировать команду возобновления", copyResumeCommand: "Копировать команду возобновления",
copyAgentId: "Скопировать идентификатор агента", copyAgentId: "Скопировать идентификатор агента",
copyTerminalId: "Скопировать идентификатор терминала",
copyFilePath: "Copy file path", copyFilePath: "Copy file path",
rename: "Переименовать", rename: "Переименовать",
closeAbove: "Закрыть вкладки выше", closeAbove: "Закрыть вкладки выше",
@@ -533,6 +534,7 @@ export const ru: TranslationResources = {
toasts: { toasts: {
copyFailed: "Не удалось скопировать", copyFailed: "Не удалось скопировать",
agentIdCopiedLabel: "AgentID", agentIdCopiedLabel: "AgentID",
terminalIdCopiedLabel: "Идентификатор терминала",
resumeCommandCopiedLabel: "команда возобновления", resumeCommandCopiedLabel: "команда возобновления",
filePathCopiedLabel: "File path", filePathCopiedLabel: "File path",
resumeIdUnavailable: "Резюме ID недоступно", resumeIdUnavailable: "Резюме ID недоступно",
+2
View File
@@ -492,6 +492,7 @@ export const zhCN: TranslationResources = {
openFor: "打开 {{label}} 的菜单", openFor: "打开 {{label}} 的菜单",
copyResumeCommand: "复制恢复命令", copyResumeCommand: "复制恢复命令",
copyAgentId: "复制 Agent ID", copyAgentId: "复制 Agent ID",
copyTerminalId: "复制 Terminal ID",
copyFilePath: "Copy file path", copyFilePath: "Copy file path",
rename: "重命名", rename: "重命名",
closeAbove: "关闭上方标签", closeAbove: "关闭上方标签",
@@ -529,6 +530,7 @@ export const zhCN: TranslationResources = {
toasts: { toasts: {
copyFailed: "复制失败", copyFailed: "复制失败",
agentIdCopiedLabel: "Agent ID", agentIdCopiedLabel: "Agent ID",
terminalIdCopiedLabel: "Terminal ID",
resumeCommandCopiedLabel: "恢复命令", resumeCommandCopiedLabel: "恢复命令",
filePathCopiedLabel: "File path", filePathCopiedLabel: "File path",
resumeIdUnavailable: "恢复 ID 不可用", resumeIdUnavailable: "恢复 ID 不可用",
@@ -419,6 +419,7 @@ interface WorkspaceDesktopTabsRowProps {
onCloseTab: (tabId: string) => Promise<void> | void; onCloseTab: (tabId: string) => Promise<void> | void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void; onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void; onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void; onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void; onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void; onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -764,6 +765,7 @@ export function WorkspaceDesktopTabsRow({
onCloseTab, onCloseTab,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -847,6 +849,7 @@ export function WorkspaceDesktopTabsRow({
() => ({ () => ({
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"), copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
copyAgentId: t("workspace.tabs.menu.copyAgentId"), copyAgentId: t("workspace.tabs.menu.copyAgentId"),
copyTerminalId: t("workspace.tabs.menu.copyTerminalId"),
copyFilePath: t("workspace.tabs.menu.copyFilePath"), copyFilePath: t("workspace.tabs.menu.copyFilePath"),
rename: t("workspace.tabs.menu.rename"), rename: t("workspace.tabs.menu.rename"),
closeAbove: t("workspace.tabs.menu.closeAbove"), closeAbove: t("workspace.tabs.menu.closeAbove"),
@@ -945,6 +948,7 @@ export function WorkspaceDesktopTabsRow({
normalizedWorkspaceId={normalizedWorkspaceId} normalizedWorkspaceId={normalizedWorkspaceId}
onCopyResumeCommand={onCopyResumeCommand} onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId} onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent} onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab} onRenameTab={onRenameTab}
@@ -976,6 +980,7 @@ export function WorkspaceDesktopTabsRow({
onCloseTabsToLeft, onCloseTabsToLeft,
onCloseTabsToRight, onCloseTabsToRight,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onCopyResumeCommand, onCopyResumeCommand,
onNavigateTab, onNavigateTab,
@@ -1097,6 +1102,7 @@ function ResolvedDesktopTabChip({
normalizedWorkspaceId, normalizedWorkspaceId,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -1123,6 +1129,7 @@ function ResolvedDesktopTabChip({
normalizedWorkspaceId: string; normalizedWorkspaceId: string;
onCopyResumeCommand: (agentId: string) => Promise<void> | void; onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void; onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void; onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void; onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void; onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -1149,6 +1156,7 @@ function ResolvedDesktopTabChip({
tabCount, tabCount,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -1166,6 +1174,7 @@ function ResolvedDesktopTabChip({
onCloseTabsToLeft, onCloseTabsToLeft,
onCloseTabsToRight, onCloseTabsToRight,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onCopyResumeCommand, onCopyResumeCommand,
labels, labels,
@@ -424,6 +424,7 @@ interface MobileWorkspaceTabSwitcherProps {
onSelectSwitcherTab: (key: string) => void; onSelectSwitcherTab: (key: string) => void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void; onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void; onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void; onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void; onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void; onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -612,6 +613,7 @@ function MobileWorkspaceTabOption({
onPress, onPress,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -630,6 +632,7 @@ function MobileWorkspaceTabOption({
onPress: () => void; onPress: () => void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void; onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void; onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void; onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void; onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void; onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -643,6 +646,7 @@ function MobileWorkspaceTabOption({
() => ({ () => ({
copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"), copyResumeCommand: t("workspace.tabs.menu.copyResumeCommand"),
copyAgentId: t("workspace.tabs.menu.copyAgentId"), copyAgentId: t("workspace.tabs.menu.copyAgentId"),
copyTerminalId: t("workspace.tabs.menu.copyTerminalId"),
copyFilePath: t("workspace.tabs.menu.copyFilePath"), copyFilePath: t("workspace.tabs.menu.copyFilePath"),
rename: t("workspace.tabs.menu.rename"), rename: t("workspace.tabs.menu.rename"),
closeAbove: t("workspace.tabs.menu.closeAbove"), closeAbove: t("workspace.tabs.menu.closeAbove"),
@@ -665,6 +669,7 @@ function MobileWorkspaceTabOption({
menuTestIDBase, menuTestIDBase,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -733,6 +738,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
onSelectSwitcherTab, onSelectSwitcherTab,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -789,6 +795,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
onPress={onPress} onPress={onPress}
onCopyResumeCommand={onCopyResumeCommand} onCopyResumeCommand={onCopyResumeCommand}
onCopyAgentId={onCopyAgentId} onCopyAgentId={onCopyAgentId}
onCopyTerminalId={onCopyTerminalId}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onReloadAgent={onReloadAgent} onReloadAgent={onReloadAgent}
onRenameTab={onRenameTab} onRenameTab={onRenameTab}
@@ -807,6 +814,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
normalizedWorkspaceId, normalizedWorkspaceId,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -2772,6 +2780,19 @@ function WorkspaceScreenContent({
[toast, t], [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( const handleCopyFilePath = useCallback(
async (path: string) => { async (path: string) => {
if (!path) return; if (!path) return;
@@ -3645,6 +3666,7 @@ function WorkspaceScreenContent({
onCloseTab={handleCloseTabById} onCloseTab={handleCloseTabById}
onCopyResumeCommand={handleCopyResumeCommand} onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId} onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath} onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent} onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab} onRenameTab={handleRenameTab}
@@ -3681,6 +3703,7 @@ function WorkspaceScreenContent({
handleCloseTabById, handleCloseTabById,
handleCopyResumeCommand, handleCopyResumeCommand,
handleCopyAgentId, handleCopyAgentId,
handleCopyTerminalId,
handleCopyFilePath, handleCopyFilePath,
handleReloadAgent, handleReloadAgent,
handleRenameTab, handleRenameTab,
@@ -3762,6 +3785,7 @@ function WorkspaceScreenContent({
onSelectSwitcherTab={handleSelectSwitcherTab} onSelectSwitcherTab={handleSelectSwitcherTab}
onCopyResumeCommand={handleCopyResumeCommand} onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId} onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath} onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent} onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab} onRenameTab={handleRenameTab}
@@ -3784,6 +3808,7 @@ function WorkspaceScreenContent({
onCloseTab={handleCloseTabById} onCloseTab={handleCloseTabById}
onCopyResumeCommand={handleCopyResumeCommand} onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId} onCopyAgentId={handleCopyAgentId}
onCopyTerminalId={handleCopyTerminalId}
onCopyFilePath={handleCopyFilePath} onCopyFilePath={handleCopyFilePath}
onReloadAgent={handleReloadAgent} onReloadAgent={handleReloadAgent}
onRenameTab={handleRenameTab} onRenameTab={handleRenameTab}
@@ -34,6 +34,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-agent_123", menuTestIDBase: "workspace-tab-context-agent_123",
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId: vi.fn(),
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, onRenameTab,
@@ -64,6 +65,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-menu-agent_123", menuTestIDBase: "workspace-tab-menu-agent_123",
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(), onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab: vi.fn(), onRenameTab: vi.fn(),
@@ -99,6 +101,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-menu-draft_123", menuTestIDBase: "workspace-tab-menu-draft_123",
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(), onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab: vi.fn(), onRenameTab: vi.fn(),
@@ -127,6 +130,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-agent_123", menuTestIDBase: "workspace-tab-context-agent_123",
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(), onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab: vi.fn(), onRenameTab: vi.fn(),
@@ -156,6 +160,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-agent_123", menuTestIDBase: "workspace-tab-context-agent_123",
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(), onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab, onRenameTab,
@@ -174,8 +179,9 @@ describe("buildWorkspaceTabMenuEntries", () => {
expect(onRenameTab).toHaveBeenCalledWith(tab); 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 onRenameTab = vi.fn();
const onCopyTerminalId = vi.fn();
const terminalTab: WorkspaceTabDescriptor = { const terminalTab: WorkspaceTabDescriptor = {
key: "terminal_abc", key: "terminal_abc",
tabId: "terminal_abc", tabId: "terminal_abc",
@@ -190,6 +196,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-terminal_abc", menuTestIDBase: "workspace-tab-context-terminal_abc",
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId,
onCopyFilePath: vi.fn(), onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab, onRenameTab,
@@ -200,12 +207,22 @@ describe("buildWorkspaceTabMenuEntries", () => {
}); });
const labels = entries.filter((entry) => entry.kind === "item").map((entry) => entry.label); 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 resume command");
expect(labels).not.toContain("Copy agent id"); expect(labels).not.toContain("Copy agent id");
expect(labels).not.toContain("Copy file path"); expect(labels).not.toContain("Copy file path");
expect(labels).not.toContain("Reload agent"); 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"); const renameEntry = entries.find((entry) => entry.kind === "item" && entry.label === "Rename");
if (!renameEntry || renameEntry.kind !== "item") { if (!renameEntry || renameEntry.kind !== "item") {
throw new Error("Rename entry missing"); throw new Error("Rename entry missing");
@@ -230,6 +247,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase: "workspace-tab-context-file_abc", menuTestIDBase: "workspace-tab-context-file_abc",
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath, onCopyFilePath,
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab: vi.fn(), onRenameTab: vi.fn(),
@@ -272,6 +290,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
tabCount: 1, tabCount: 1,
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(), onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab: vi.fn(), onRenameTab: vi.fn(),
@@ -302,6 +321,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
menuTestIDBase, menuTestIDBase,
onCopyResumeCommand: vi.fn(), onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(), onCopyAgentId: vi.fn(),
onCopyTerminalId: vi.fn(),
onCopyFilePath: vi.fn(), onCopyFilePath: vi.fn(),
onReloadAgent: vi.fn(), onReloadAgent: vi.fn(),
onRenameTab: vi.fn(), onRenameTab: vi.fn(),
@@ -8,6 +8,7 @@ export type WorkspaceTabMenuSurface = "desktop" | "mobile";
export interface WorkspaceTabMenuLabels { export interface WorkspaceTabMenuLabels {
copyResumeCommand: string; copyResumeCommand: string;
copyAgentId: string; copyAgentId: string;
copyTerminalId: string;
copyFilePath: string; copyFilePath: string;
rename: string; rename: string;
closeAbove: string; closeAbove: string;
@@ -23,6 +24,7 @@ export interface WorkspaceTabMenuLabels {
export const DEFAULT_WORKSPACE_TAB_MENU_LABELS: WorkspaceTabMenuLabels = { export const DEFAULT_WORKSPACE_TAB_MENU_LABELS: WorkspaceTabMenuLabels = {
copyResumeCommand: i18n.t("workspace.tabs.menu.copyResumeCommand"), copyResumeCommand: i18n.t("workspace.tabs.menu.copyResumeCommand"),
copyAgentId: i18n.t("workspace.tabs.menu.copyAgentId"), copyAgentId: i18n.t("workspace.tabs.menu.copyAgentId"),
copyTerminalId: i18n.t("workspace.tabs.menu.copyTerminalId"),
copyFilePath: i18n.t("workspace.tabs.menu.copyFilePath"), copyFilePath: i18n.t("workspace.tabs.menu.copyFilePath"),
rename: i18n.t("workspace.tabs.menu.rename"), rename: i18n.t("workspace.tabs.menu.rename"),
closeAbove: i18n.t("workspace.tabs.menu.closeAbove"), closeAbove: i18n.t("workspace.tabs.menu.closeAbove"),
@@ -68,6 +70,7 @@ interface BuildWorkspaceTabMenuEntriesInput {
menuTestIDBase: string; menuTestIDBase: string;
onCopyResumeCommand: (agentId: string) => Promise<void> | void; onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void; onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void; onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void; onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void; onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -84,6 +87,7 @@ interface BuildWorkspaceDesktopTabActionsInput {
tabCount: number; tabCount: number;
onCopyResumeCommand: (agentId: string) => Promise<void> | void; onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void; onCopyAgentId: (agentId: string) => Promise<void> | void;
onCopyTerminalId: (terminalId: string) => Promise<void> | void;
onCopyFilePath: (path: string) => Promise<void> | void; onCopyFilePath: (path: string) => Promise<void> | void;
onReloadAgent: (agentId: string) => Promise<void> | void; onReloadAgent: (agentId: string) => Promise<void> | void;
onRenameTab: (tab: WorkspaceTabDescriptor) => void; onRenameTab: (tab: WorkspaceTabDescriptor) => void;
@@ -161,6 +165,7 @@ export function buildWorkspaceTabMenuEntries(
menuTestIDBase, menuTestIDBase,
onCopyResumeCommand, onCopyResumeCommand,
onCopyAgentId, onCopyAgentId,
onCopyTerminalId,
onCopyFilePath, onCopyFilePath,
onReloadAgent, onReloadAgent,
onRenameTab, 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") { if (tab.target.kind === "file") {
const filePath = tab.target.path; const filePath = tab.target.path;
entries.push({ entries.push({
@@ -306,6 +326,7 @@ export function buildWorkspaceDesktopTabActions(
menuTestIDBase: contextMenuTestId, menuTestIDBase: contextMenuTestId,
onCopyResumeCommand: input.onCopyResumeCommand, onCopyResumeCommand: input.onCopyResumeCommand,
onCopyAgentId: input.onCopyAgentId, onCopyAgentId: input.onCopyAgentId,
onCopyTerminalId: input.onCopyTerminalId,
onCopyFilePath: input.onCopyFilePath, onCopyFilePath: input.onCopyFilePath,
onReloadAgent: input.onReloadAgent, onReloadAgent: input.onReloadAgent,
onRenameTab: input.onRenameTab, onRenameTab: input.onRenameTab,