From fef33142b3791bbde8a7cb571cb8ed8c782bd570 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 30 Sep 2026 22:46:50 +0800 Subject: [PATCH] feat(ssh-terminal): add a duplicate tab action (#1510) --- plugins/ssh-terminal/locales/en.json | 3 +- plugins/ssh-terminal/src/frontend/index.tsx | 22 ++++++++++++++ .../tests/frontend/activate.test.tsx | 30 +++++++++++++++++++ src/ui/shell/TabBar.tsx | 2 +- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/plugins/ssh-terminal/locales/en.json b/plugins/ssh-terminal/locales/en.json index 8c49f3a2d..219e511a0 100644 --- a/plugins/ssh-terminal/locales/en.json +++ b/plugins/ssh-terminal/locales/en.json @@ -79,7 +79,8 @@ "clipboardReadFailed": "Failed to read from clipboard. Make sure clipboard permissions are granted.", "clipboardWriteFailed": "Failed to copy to clipboard. Make sure the page is served over HTTPS or localhost.", "connected": "Connected", - "totpCodeLabel": "Verification Code" + "totpCodeLabel": "Verification Code", + "duplicateTab": "Duplicate tab" }, "terminalToolbar": { "detachTmux": "Detach tmux", diff --git a/plugins/ssh-terminal/src/frontend/index.tsx b/plugins/ssh-terminal/src/frontend/index.tsx index 1ef1f70e1..93a72dcb8 100644 --- a/plugins/ssh-terminal/src/frontend/index.tsx +++ b/plugins/ssh-terminal/src/frontend/index.tsx @@ -3,6 +3,7 @@ import "@xterm/xterm/css/xterm.css"; import { lazy, Suspense, type ComponentType } from "react"; import { Braces, + Copy, Hammer, History, Laptop, @@ -12,6 +13,7 @@ import { import type { PanelProps, PluginHostRecord, + PluginTabRecord, StandaloneViewProps, TabProps, TermixApp, @@ -117,6 +119,26 @@ export function activate(app: TermixApp): void { }, ); + app.registerAction( + "terminal.duplicateTab", + ((_handle: unknown, tab?: PluginTabRecord) => { + if (tab?.type === "terminal" && tab.host) { + app.tabs.openTab(tab.host, "terminal", { forceNewTab: true }); + } + }) as never, + { permission: "use" }, + ); + app.registerSlotContribution("tab.menu", { + actionId: "terminal.duplicateTab", + titleKey: "terminal.duplicateTab", + icon: Copy, + kind: "button", + when: ({ tab }) => { + const target = tab as PluginTabRecord | undefined; + return target?.type === "terminal" && !!target.host; + }, + }); + app.registerHostAction({ id: "terminal", titleKey: "nav.terminal", diff --git a/plugins/ssh-terminal/tests/frontend/activate.test.tsx b/plugins/ssh-terminal/tests/frontend/activate.test.tsx index 1f0d5ed17..12b684872 100644 --- a/plugins/ssh-terminal/tests/frontend/activate.test.tsx +++ b/plugins/ssh-terminal/tests/frontend/activate.test.tsx @@ -63,6 +63,36 @@ describe(`${manifest.id} activate`, () => { ); }); + it("duplicates only the SSH connection configuration into a fresh tab", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + const host = { id: 7, name: "server", ip: "192.0.2.1", port: 22 }; + expect(rendered.registered.slot("tab.menu")).toContain( + "terminal.duplicateTab", + ); + await rendered.app.invokeAction("terminal.duplicateTab", undefined, { + id: "old-tab", + type: "terminal", + host, + instanceId: "old-instance", + data: { initialPath: "/tmp", joinSharedSessionId: "shared-session" }, + }); + expect(rendered.shellCalls).toEqual([ + { + method: "openTab", + args: [ + { ...host, pluginSettings: {} }, + "terminal", + { forceNewTab: true }, + ], + }, + ]); + await rendered.app.invokeAction("terminal.duplicateTab", undefined, { + id: "local", + type: "local-terminal", + }); + expect(rendered.shellCalls).toHaveLength(1); + }); + it("owns the host editor's Terminal tab in the SSH group", async () => { rendered = await renderWithApp(plugin, { manifest, locales }); expect(rendered.registered.hostEditorSections()).toEqual(["terminal"]); diff --git a/src/ui/shell/TabBar.tsx b/src/ui/shell/TabBar.tsx index 6a4abb017..e27652d21 100644 --- a/src/ui/shell/TabBar.tsx +++ b/src/ui/shell/TabBar.tsx @@ -713,7 +713,7 @@ export function TabBar({ [ctxTab.terminalRef?.current]} + context={() => [ctxTab.terminalRef?.current, ctxTab]} renderItem={(contribution, invoke) => (