Add button to copy all process logs (vibe-kanban) (#1429)

* 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):** "ログをコピー" / "コピーしました!"
This commit is contained in:
Louis Knight-Webb
2025-12-04 15:33:38 +00:00
committed by GitHub
parent 7989168e7a
commit 9bfaa6dde5
5 changed files with 58 additions and 17 deletions

View File

@@ -12,7 +12,8 @@ import {
import { executionProcessesApi } from '@/lib/api.ts'; import { executionProcessesApi } from '@/lib/api.ts';
import { ProfileVariantBadge } from '@/components/common/ProfileVariantBadge.tsx'; import { ProfileVariantBadge } from '@/components/common/ProfileVariantBadge.tsx';
import { useExecutionProcesses } from '@/hooks/useExecutionProcesses'; 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 type { ExecutionProcessStatus, ExecutionProcess } from 'shared/types';
import { useProcessSelection } from '@/contexts/ProcessSelectionContext'; import { useProcessSelection } from '@/contexts/ProcessSelectionContext';
@@ -36,12 +37,33 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {
const [localProcessDetails, setLocalProcessDetails] = useState< const [localProcessDetails, setLocalProcessDetails] = useState<
Record<string, ExecutionProcess> Record<string, ExecutionProcess>
>({}); >({});
const [copied, setCopied] = useState(false);
const selectedProcess = selectedProcessId
? localProcessDetails[selectedProcessId] ||
executionProcessesById[selectedProcessId]
: null;
const { logs, error: logsError } = useLogStream(selectedProcess?.id ?? '');
useEffect(() => { useEffect(() => {
setLocalProcessDetails({}); setLocalProcessDetails({});
setLoadingProcessId(null); setLoadingProcessId(null);
}, [attemptId]); }, [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) => { const getStatusIcon = (status: ExecutionProcessStatus) => {
switch (status) { switch (status) {
case 'running': case 'running':
@@ -126,10 +148,6 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {
} }
}; };
const selectedProcess = selectedProcessId
? localProcessDetails[selectedProcessId] ||
executionProcessesById[selectedProcessId]
: null;
const { isProcessGreyed } = useRetryUi(); const { isProcessGreyed } = useRetryUi();
if (!attemptId) { if (!attemptId) {
@@ -261,17 +279,32 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
{t('processes.detailsTitle')} {t('processes.detailsTitle')}
</h2> </h2>
<button <div className="flex items-center gap-2">
onClick={() => setSelectedProcessId(null)} <button
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded-md border border-border transition-colors" onClick={handleCopyLogs}
> disabled={logs.length === 0}
<ArrowLeft className="h-4 w-4" /> className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border transition-colors ${
{t('processes.backToList')} copied
</button> ? 'text-success'
: logs.length === 0
? 'text-muted-foreground opacity-50 cursor-not-allowed'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
}`}
>
{copied ? t('processes.logsCopied') : t('processes.copyLogs')}
</button>
<button
onClick={() => setSelectedProcessId(null)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded-md border border-border transition-colors"
>
<ArrowLeft className="h-4 w-4" />
{t('processes.backToList')}
</button>
</div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
{selectedProcess ? ( {selectedProcess ? (
<ProcessLogsViewer processId={selectedProcess.id} /> <ProcessLogsViewerContent logs={logs} error={logsError} />
) : loadingProcessId === selectedProcessId ? ( ) : loadingProcessId === selectedProcessId ? (
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<p>{t('processes.loadingDetails')}</p> <p>{t('processes.loadingDetails')}</p>

View File

@@ -126,7 +126,9 @@
"detailsTitle": "Process Details", "detailsTitle": "Process Details",
"backToList": "Back to list", "backToList": "Back to list",
"loadingDetails": "Loading process details...", "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": { "taskHeader": {
"editTask": "Edit task", "editTask": "Edit task",

View File

@@ -304,7 +304,9 @@
"processId": "Process ID: {{id}}", "processId": "Process ID: {{id}}",
"reconnecting": "Reconnecting...", "reconnecting": "Reconnecting...",
"selectAttempt": "Select an attempt to view execution processes.", "selectAttempt": "Select an attempt to view execution processes.",
"started": "Started: {{date}}" "started": "Started: {{date}}",
"copyLogs": "Copiar registros",
"logsCopied": "¡Copiado!"
}, },
"rebase": { "rebase": {
"common": { "common": {

View File

@@ -304,7 +304,9 @@
"processId": "Process ID: {{id}}", "processId": "Process ID: {{id}}",
"reconnecting": "Reconnecting...", "reconnecting": "Reconnecting...",
"selectAttempt": "Select an attempt to view execution processes.", "selectAttempt": "Select an attempt to view execution processes.",
"started": "Started: {{date}}" "started": "Started: {{date}}",
"copyLogs": "ログをコピー",
"logsCopied": "コピーしました!"
}, },
"rebase": { "rebase": {
"common": { "common": {

View File

@@ -304,7 +304,9 @@
"processId": "Process ID: {{id}}", "processId": "Process ID: {{id}}",
"reconnecting": "Reconnecting...", "reconnecting": "Reconnecting...",
"selectAttempt": "Select an attempt to view execution processes.", "selectAttempt": "Select an attempt to view execution processes.",
"started": "Started: {{date}}" "started": "Started: {{date}}",
"copyLogs": "로그 복사",
"logsCopied": "복사됨!"
}, },
"rebase": { "rebase": {
"common": { "common": {