From 50ca182e1e8c4cb366c94bcb4e5c5634fdf97668 Mon Sep 17 00:00:00 2001 From: Netcatty-Bot Date: Tue, 29 Sep 2026 19:47:06 +0800 Subject: [PATCH] fix(#3562): automated AI fix (#3564) Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com> --- application/i18n/locales/en/scripts.ts | 3 +++ application/i18n/locales/es/scripts.ts | 3 +++ application/i18n/locales/ru/scripts.ts | 3 +++ application/i18n/locales/zh-CN/scripts.ts | 3 +++ application/i18n/locales/zh-TW/scripts.ts | 3 +++ components/ScriptsSidePanel.test.ts | 13 ++++++++++ components/ScriptsSidePanel.tsx | 29 +++++++++++++++++++++++ 7 files changed, 57 insertions(+) diff --git a/application/i18n/locales/en/scripts.ts b/application/i18n/locales/en/scripts.ts index 6aa9bfcc6..453e75ec7 100644 --- a/application/i18n/locales/en/scripts.ts +++ b/application/i18n/locales/en/scripts.ts @@ -27,6 +27,9 @@ export const enScriptsMessages = { 'scripts.actions.runParallel': 'Run on all tabs (parallel)', 'scripts.actions.runSequential': 'Run on all tabs (sequential)', 'scripts.actions.runOnAllTabs': 'Run on all tabs', + 'scripts.actions.copyCommand': 'Copy command', + 'scripts.actions.copyCommand.copied': 'Command copied', + 'scripts.actions.copyCommand.failed': 'Could not copy command', 'scripts.actions.skippedConnectingSessions': '{count} tab(s) still connecting and were skipped', 'scripts.actions.skippedSensitiveSessions': '{count} tab(s) skipped (password/sensitive input)', 'scripts.actions.noRunnableHosts': 'No connectable hosts match this script\'s targets', diff --git a/application/i18n/locales/es/scripts.ts b/application/i18n/locales/es/scripts.ts index 6586b79d1..d452a6b2d 100644 --- a/application/i18n/locales/es/scripts.ts +++ b/application/i18n/locales/es/scripts.ts @@ -27,6 +27,9 @@ export const esScriptsMessages = { 'scripts.actions.runParallel': 'Ejecutar en todas las pestañas (en paralelo)', 'scripts.actions.runSequential': 'Ejecutar en todas las pestañas (en secuencia)', 'scripts.actions.runOnAllTabs': 'Ejecutar en todas las pestañas', + 'scripts.actions.copyCommand': 'Copiar comando', + 'scripts.actions.copyCommand.copied': 'Comando copiado', + 'scripts.actions.copyCommand.failed': 'No se pudo copiar el comando', 'scripts.actions.skippedConnectingSessions': '{count} pestaña(s) aún conectándose y fueron omitidas', 'scripts.actions.skippedSensitiveSessions': '{count} pestaña(s) omitidas (entrada de contraseña/datos sensibles)', 'scripts.actions.noRunnableHosts': 'Ningún host conectable coincide con los destinos de este script', diff --git a/application/i18n/locales/ru/scripts.ts b/application/i18n/locales/ru/scripts.ts index 9e6f49942..cfd2e3935 100644 --- a/application/i18n/locales/ru/scripts.ts +++ b/application/i18n/locales/ru/scripts.ts @@ -27,6 +27,9 @@ export const ruScriptsMessages = { 'scripts.actions.runParallel': 'Запустить на всех вкладках (параллельно)', 'scripts.actions.runSequential': 'Запустить на всех вкладках (последовательно)', 'scripts.actions.runOnAllTabs': 'Запустить на всех вкладках', + 'scripts.actions.copyCommand': 'Копировать команду', + 'scripts.actions.copyCommand.copied': 'Команда скопирована', + 'scripts.actions.copyCommand.failed': 'Не удалось скопировать команду', 'scripts.actions.skippedConnectingSessions': 'Пропущено вкладок, которые ещё подключаются: {count}', 'scripts.actions.skippedSensitiveSessions': 'Пропущено вкладок с паролем/чувствительным вводом: {count}', 'scripts.actions.noRunnableHosts': 'Нет доступных для подключения хостов, соответствующих целям этого скрипта', diff --git a/application/i18n/locales/zh-CN/scripts.ts b/application/i18n/locales/zh-CN/scripts.ts index 098c858ff..615c52073 100644 --- a/application/i18n/locales/zh-CN/scripts.ts +++ b/application/i18n/locales/zh-CN/scripts.ts @@ -27,6 +27,9 @@ export const zhCNScriptsMessages = { 'scripts.actions.runParallel': '在所有标签页并行运行', 'scripts.actions.runSequential': '在所有标签页顺序运行', 'scripts.actions.runOnAllTabs': '在所有标签页运行', + 'scripts.actions.copyCommand': '复制命令', + 'scripts.actions.copyCommand.copied': '命令已复制', + 'scripts.actions.copyCommand.failed': '复制命令失败', 'scripts.actions.skippedConnectingSessions': '有 {count} 个标签仍在连接中,已跳过', 'scripts.actions.skippedSensitiveSessions': '有 {count} 个标签处于密码/敏感输入,已跳过', 'scripts.actions.noRunnableHosts': '没有与脚本目标匹配的可连接主机', diff --git a/application/i18n/locales/zh-TW/scripts.ts b/application/i18n/locales/zh-TW/scripts.ts index b1f45f15f..1fde32bb8 100644 --- a/application/i18n/locales/zh-TW/scripts.ts +++ b/application/i18n/locales/zh-TW/scripts.ts @@ -27,6 +27,9 @@ export const zhTWScriptsMessages = { 'scripts.actions.runParallel': '在所有分頁並行執行', 'scripts.actions.runSequential': '在所有分頁依序執行', 'scripts.actions.runOnAllTabs': '在所有分頁執行', + 'scripts.actions.copyCommand': '複製指令', + 'scripts.actions.copyCommand.copied': '指令已複製', + 'scripts.actions.copyCommand.failed': '複製指令失敗', 'scripts.actions.skippedConnectingSessions': '有 {count} 個分頁仍在連線中,已跳過', 'scripts.actions.skippedSensitiveSessions': '有 {count} 個分頁處於密碼/敏感輸入,已跳過', 'scripts.actions.noRunnableHosts': '沒有與指令碼目標相符的可連線主機', diff --git a/components/ScriptsSidePanel.test.ts b/components/ScriptsSidePanel.test.ts index d1f9543a7..5d9931f91 100644 --- a/components/ScriptsSidePanel.test.ts +++ b/components/ScriptsSidePanel.test.ts @@ -53,6 +53,19 @@ test("scripts side panel offers run-on-all-tabs for every snippet, not only auto assert.match(source, /openPackageDialog/); }); +test("scripts side panel snippet context menu offers copy command without running", () => { + // Copy must sit between the run actions and Edit, write snippet.command to + // the clipboard, and confirm with a toast — it never executes the command. + assert.match(source, /onCopyCommand=\{\(\) => handleCopySnippetCommand\(item\.snippet\)\}/); + assert.match(source, /onCopyCommand=\{\(\) => handleCopySnippetCommand\(item\.row\.snippet\)\}/); + assert.match(source, /scripts\.actions\.copyCommand'/); + const menu = source.match(/\{onRunParallel \? \([\s\S]*?<\/ContextMenu>/); + assert.ok(menu, "snippet row context menu should exist"); + assert.match(menu[0], /onCopyCommand[\s\S]*?onClick=\{onEdit\}/); + assert.match(menu[0], / { assert.match(source, /snippets\.action\.newSnippet/); assert.match(source, /snippets\.action\.newPackage/); diff --git a/components/ScriptsSidePanel.tsx b/components/ScriptsSidePanel.tsx index 0450229cf..0a0d38724 100644 --- a/components/ScriptsSidePanel.tsx +++ b/components/ScriptsSidePanel.tsx @@ -10,6 +10,7 @@ import { CheckSquare, ChevronRight, + Copy, Edit2, Expand, FolderPlus, @@ -54,6 +55,7 @@ import { SnippetCommandTooltipContent } from './snippets/SnippetCommandTooltipCo import { TERMINAL_SIDE_PANEL_INNER_HEADER_CLASS } from './terminalLayer/terminalSidePanelChrome'; import { isNonPrimaryPointer, primaryOnlyDragHandlers } from './ui/primaryOnlyDrag'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; +import { toast } from './ui/toast'; const toolbarIconButtonClass = 'h-7 w-7 shrink-0 flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-colors disabled:opacity-40 disabled:pointer-events-none'; @@ -889,6 +891,20 @@ const ScriptsSidePanelInner: React.FC = ({ ); }, []); + const handleCopySnippetCommand = useCallback((snippet: Snippet) => { + if (!snippet.command) return; + navigator.clipboard.writeText(snippet.command).then( + () => { + toast.success(t('scripts.actions.copyCommand.copied'), { + title: snippet.label, + }); + }, + () => { + toast.error(t('scripts.actions.copyCommand.failed')); + }, + ); + }, [t]); + if (!isVisible) return null; const hasAnyContent = snippets.length > 0 || packages.length > 0; @@ -1129,6 +1145,8 @@ const ScriptsSidePanelInner: React.FC = ({ onClick={() => handleSnippetClick(item.snippet)} onEdit={() => handleEditSnippet(item.snippet)} onDelete={() => handleDeleteSnippet(item.snippet.id)} + onCopyCommand={() => handleCopySnippetCommand(item.snippet)} + copyCommandLabel={t('scripts.actions.copyCommand')} onRunParallel={onRunScriptOnWorkspace ? () => onRunScriptOnWorkspace(item.snippet, 'parallel') : undefined} @@ -1182,6 +1200,8 @@ const ScriptsSidePanelInner: React.FC = ({ onClick={() => handleSnippetClick(item.row.snippet)} onEdit={() => handleEditSnippet(item.row.snippet)} onDelete={() => handleDeleteSnippet(item.row.snippet.id)} + onCopyCommand={() => handleCopySnippetCommand(item.row.snippet)} + copyCommandLabel={t('scripts.actions.copyCommand')} onRunParallel={onRunScriptOnWorkspace ? () => onRunScriptOnWorkspace(item.row.snippet, 'parallel') : undefined} @@ -1505,6 +1525,8 @@ interface SnippetRowProps { onDelete: () => void; onRunParallel?: () => void; onRunSequential?: () => void; + onCopyCommand?: () => void; + copyCommandLabel?: string; runParallelLabel?: string; runSequentialLabel?: string; editLabel: string; @@ -1527,6 +1549,8 @@ const SnippetRow = memo(({ onDelete, onRunParallel, onRunSequential, + onCopyCommand, + copyCommandLabel, runParallelLabel, runSequentialLabel, editLabel, @@ -1604,6 +1628,11 @@ const SnippetRow = memo(({ {runSequentialLabel} ) : null} + {onCopyCommand ? ( + + {copyCommandLabel} + + ) : null} {editLabel}