From 9bfaa6dde52fd74fe11af0376cfb9465707bdd6d Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Thu, 4 Dec 2025 15:33:38 +0000 Subject: [PATCH] Add button to copy all process logs (vibe-kanban) (#1429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Done! I've implemented the "Copy all process logs" button for the `ViewProcessesDialog`. Here's a summary of the changes: **Files modified:** 1. **`ProcessLogsViewer.tsx`** - Added `onLogsChange` callback prop and exported the `LogEntry` type 2. **`ProcessesTab.tsx`** - Added `onLogsChange` prop that passes through to ProcessLogsViewer, and clears logs when no process is selected 3. **`ViewProcessesDialog.tsx`** - Added the copy button with: - State management for logs and copied feedback - Copy button in the dialog header (right-aligned) - Tooltip showing "Copy logs" / "Copied!" - Button disabled when no logs are available - Uses `Copy`/`Check` icons from lucide-react 4. **i18n files** (en, es, ko, ja) - Added translation keys: - `copyLogs`: "Copy logs" - `logsCopied`: "Copied!" **Behavior:** - The copy button appears in the dialog header next to the title - When clicked, it copies all log content (joined by newlines) to clipboard - Shows a checkmark icon for 2 seconds after successful copy - Button is disabled when viewing the process list (no logs selected) * Cleanup script changes for task attempt 2060681f-e4c3-46a7-aca3-3e9705b53248 * Fixed! I wrapped the `Tooltip` component in `TooltipProvider` as required by shadcn/ui. The error should now be resolved - the copy button should work properly. * Done! I've made the following changes: 1. Removed the `Tooltip`, `TooltipProvider`, `TooltipContent`, and `TooltipTrigger` components 2. Added `mr-8` margin to position the button left of the dialog's native close button 3. Added `text-green-500` to the `Check` icon for green success feedback The button should now be properly positioned and show a green checkmark when logs are copied. * Done! Changed `text-green-500` to `text-success` so the check icon uses the theme's success color instead of a hardcoded green. * Done! I've fixed the vertical alignment by: 1. Moving the button outside the `DialogHeader` and positioning it absolutely 2. Using `absolute right-12 top-4` to place it exactly to the left of the close button (which is at `right-4 top-4`) 3. Using `h-4 w-4` to match the close button's icon size 4. Added `rounded-sm opacity-70 hover:opacity-100` to match the close button's styling The copy button should now be perfectly aligned with the close button. * Done! I've moved the copy button to `ProcessesTab` next to the "Back to list" button. The changes: 1. **Removed** the absolute-positioned icon button from `ViewProcessesDialog` 2. **Added** a text button "Copy logs" / "Copied!" in `ProcessesTab` next to "Back to list" 3. **Moved** the i18n keys from `viewProcessesDialog` to `processes` section 4. The button uses the same styling as the "Back to list" button for consistency 5. Shows green "Copied!" text for 2 seconds after successful copy * Done! I've made the following changes to address the review comment: 1. **`ProcessLogsViewer.tsx`**: Removed the `export` keyword from `LogEntry` type - it's now internal to the file 2. **`ProcessesTab.tsx`**: Changed import from `LogEntry` to `PatchType` from `shared/types`, and updated the state and callback types accordingly The type check passes successfully. * Done! I've addressed the review comment by: 1. **Lifted `useLogStream` to `ProcessesTab`** - Now `ProcessesTab` calls `useLogStream(selectedProcess?.id ?? '')` directly, giving it direct access to the logs for the copy button 2. **Removed the hacky `onLogsChange` callback** - No more `useEffect` to propagate logs up via callback 3. **Using `ProcessLogsViewerContent` directly** - `ProcessesTab` now imports and uses `ProcessLogsViewerContent` with the logs from `useLogStream` 4. **Kept `ProcessLogsViewer` as a simpler component** - The default export still exists for other potential consumers, but it's now a simple wrapper without the callback prop The data flow is now cleaner: - `ProcessesTab` owns the log data via `useLogStream` - `ProcessLogsViewerContent` is a pure presentation component - No callback prop threading needed * Done! I've added proper translations for the i18n keys: - **Spanish (es):** "Copiar registros" / "¡Copiado!" - **Korean (ko):** "로그 복사" / "복사됨!" - **Japanese (ja):** "ログをコピー" / "コピーしました!" --- .../tasks/TaskDetails/ProcessesTab.tsx | 59 +++++++++++++++---- frontend/src/i18n/locales/en/tasks.json | 4 +- frontend/src/i18n/locales/es/tasks.json | 4 +- frontend/src/i18n/locales/ja/tasks.json | 4 +- frontend/src/i18n/locales/ko/tasks.json | 4 +- 5 files changed, 58 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx b/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx index d8814dfa..a85b7188 100644 --- a/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx @@ -12,7 +12,8 @@ import { import { executionProcessesApi } from '@/lib/api.ts'; import { ProfileVariantBadge } from '@/components/common/ProfileVariantBadge.tsx'; import { useExecutionProcesses } from '@/hooks/useExecutionProcesses'; -import ProcessLogsViewer from './ProcessLogsViewer'; +import { useLogStream } from '@/hooks/useLogStream'; +import { ProcessLogsViewerContent } from './ProcessLogsViewer'; import type { ExecutionProcessStatus, ExecutionProcess } from 'shared/types'; import { useProcessSelection } from '@/contexts/ProcessSelectionContext'; @@ -36,12 +37,33 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) { const [localProcessDetails, setLocalProcessDetails] = useState< Record >({}); + const [copied, setCopied] = useState(false); + + const selectedProcess = selectedProcessId + ? localProcessDetails[selectedProcessId] || + executionProcessesById[selectedProcessId] + : null; + + const { logs, error: logsError } = useLogStream(selectedProcess?.id ?? ''); useEffect(() => { setLocalProcessDetails({}); setLoadingProcessId(null); }, [attemptId]); + const handleCopyLogs = useCallback(async () => { + if (logs.length === 0) return; + + const text = logs.map((entry) => entry.content).join('\n'); + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.warn('Copy to clipboard failed:', err); + } + }, [logs]); + const getStatusIcon = (status: ExecutionProcessStatus) => { switch (status) { case 'running': @@ -126,10 +148,6 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) { } }; - const selectedProcess = selectedProcessId - ? localProcessDetails[selectedProcessId] || - executionProcessesById[selectedProcessId] - : null; const { isProcessGreyed } = useRetryUi(); if (!attemptId) { @@ -261,17 +279,32 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {

{t('processes.detailsTitle')}

- +
+ + +
{selectedProcess ? ( - + ) : loadingProcessId === selectedProcessId ? (

{t('processes.loadingDetails')}

diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index ce25a9d8..4627f725 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -126,7 +126,9 @@ "detailsTitle": "Process Details", "backToList": "Back to list", "loadingDetails": "Loading process details...", - "errorLoadingDetails": "Failed to load process details. Please try again." + "errorLoadingDetails": "Failed to load process details. Please try again.", + "copyLogs": "Copy logs", + "logsCopied": "Copied!" }, "taskHeader": { "editTask": "Edit task", diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index 7d6f7f66..7f448013 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -304,7 +304,9 @@ "processId": "Process ID: {{id}}", "reconnecting": "Reconnecting...", "selectAttempt": "Select an attempt to view execution processes.", - "started": "Started: {{date}}" + "started": "Started: {{date}}", + "copyLogs": "Copiar registros", + "logsCopied": "¡Copiado!" }, "rebase": { "common": { diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index fd2484f2..62de801c 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -304,7 +304,9 @@ "processId": "Process ID: {{id}}", "reconnecting": "Reconnecting...", "selectAttempt": "Select an attempt to view execution processes.", - "started": "Started: {{date}}" + "started": "Started: {{date}}", + "copyLogs": "ログをコピー", + "logsCopied": "コピーしました!" }, "rebase": { "common": { diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index 23f92593..c3962ee9 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -304,7 +304,9 @@ "processId": "Process ID: {{id}}", "reconnecting": "Reconnecting...", "selectAttempt": "Select an attempt to view execution processes.", - "started": "Started: {{date}}" + "started": "Started: {{date}}", + "copyLogs": "로그 복사", + "logsCopied": "복사됨!" }, "rebase": { "common": {