Cleanup scripts (#288)

* Cleanup scripts (vibe-kanban 9244f51c)

We want the user to be able to specify a cleanup script, which will run after coding agent execution is complete.

This is similar to how setup scripts work, but obviously the main difference is that these run after the coding agent rather than before.

Things to ensure:
- New executor for cleanup scripts
- Project settings (FE + BE) updated with cleanup script
- Auto run cleanup script (if specified) after coding agent runs
- Task activity

* Add tab to display execution processes (vibe-kanban 444e3cf9)

Add a new tab in the TabNavigation component called "Processes"

This will be a list of all the Execution Processes associated with the selected task attempt.

Clicking on a process will open the raw logs for that execution process.

The list of execution processes is already being fetched in the frontend.

* fmt

* clippy

* format

* The processes tab info has UI glitch (vibe-kanban) (#289)

* Perfect! Fixed both UI glitches:

1. **Overlay issue**: Added `pb-20` (padding-bottom: 5rem) to both the process list view and process details view to ensure the "continue working on this task..." follow-up section doesn't overlap the process content

2. **Scrolling issue**: Changed `max-h-64` to `h-64` for both stdout/stderr log containers to ensure they have a fixed height and proper scrolling behavior

The build completed successfully, confirming the TypeScript changes are valid.

* Fixed! The issue was with the flex container setup. I made these key changes:

1. **Added `min-h-0`** to the process details container - this prevents flex items from having an implicit minimum height
2. **Added `flex-shrink-0`** to the header - this prevents the header from shrinking and ensures it stays fixed
3. **Changed `overflow-auto` to `overflow-y-auto`** - this specifically enables vertical scrolling only

Now the parent tab should properly scroll to show all process sections while the individual stdio boxes maintain their own internal scrolling.

* Execute Plan: Auto commit changes after cleanup script (vibe-kanban) (#290)

* Perfect! I've successfully implemented auto-commit functionality after cleanup script execution. Here's what was accomplished:

## Summary

Successfully implemented auto-commit functionality in [`handle_cleanup_completion()`](file:///private/var/folders/kr/jdxkcn7129j376nrg0stj9zm0000gn/T/vibe-kanban/vk-6cc2-execute-pl/backend/src/execution_monitor.rs#L1024-L1095) that:

1. **Follows existing patterns** - Reuses the same `commit_execution_changes()` function used by coding agent completion
2. **Only commits on success** - Auto-commit only triggers when cleanup script succeeds
3. **Proper error handling** - Logs errors without failing the entire process
4. **Descriptive commit messages** - Uses "Task attempt {id} - Post-cleanup changes" format
5. **Comprehensive logging** - Provides clear success/failure feedback

The implementation leverages the existing Git infrastructure and maintains consistency with the codebase patterns. The project compiles and builds successfully with the new feature.

* fmt

* Done! The commit message is now simply "Cleanup script".

* Execute Plan: Move tasks to in-review after cleanup script (vibe-kanban 78f2eb17)

# Plan: Move Tasks to In-Review After Cleanup Script

## Current Problem
- Tasks are moved to 'in-review' status immediately when coding agent completes
- Sound notifications play immediately after coding agent completion
- Cleanup scripts run after the task is already marked as complete
- This creates inconsistent UX where users think task is done before cleanup finishes

## Proposed Solution

### 1. Modify Coding Agent Completion Handler (`execution_monitor.rs:873-933`)
- **Remove** immediate task status update to `InReview`
- **Remove** immediate sound notification
- Keep cleanup script triggering logic
- Add intermediate status or flag to track "coding complete, waiting for cleanup"

### 2. Enhance Cleanup Completion Handler (`execution_monitor.rs:1024-1097`)
- **Add** task status update to `InReview` after successful cleanup
- **Add** sound notification after successful cleanup completion
- Handle cleanup failure cases (still move to `InReview` with appropriate messaging)
- Preserve existing auto-commit functionality

### 3. Handle Edge Cases
- **No cleanup script configured**: Move to `InReview` immediately after coding agent (maintain current behavior)
- **Cleanup script fails**: Still move to `InReview` but with failure notification
- **Cleanup script timeout**: Move to `InReview` with timeout notification

### 4. Files to Modify
- `backend/src/execution_monitor.rs` - Main logic changes
- Potentially update notification messages to reflect cleanup completion

## Expected Outcome
- Tasks only move to 'in-review' after ALL processing (including cleanup) is complete
- Sound notifications align with actual task completion
- Better user experience with accurate status representation

* Execute Plan: Show 'stop attempt' if cleanup script running (vibe-kanban 8fbcfe55)

## Implementation Plan: Show 'Stop Attempt' for Cleanup Scripts

### Current State Analysis
- 'Stop Attempt' button shows when `isAttemptRunning` is true
- `isAttemptRunning` only checks for `codingagent` and `setupscript` process types
- `ExecutionProcessType` enum currently only includes: `"setupscript" | "codingagent" | "devserver"`
- Types are auto-generated from backend via `generate_types.rs`

### Required Changes

#### 1. Backend Type Updates (High Priority)
- Find and update the Rust `ExecutionProcessType` enum to include `cleanupscript`
- Run `backend/src/bin/generate_types.rs` to regenerate `shared/types.ts`

#### 2. Frontend Logic Updates (High Priority)  
- Modify `isAttemptRunning` in `TaskDetailsContextProvider.tsx:278-289`:
  ```typescript
  return attemptData.processes.some(
    (process: ExecutionProcessSummary) =>
      (process.process_type === 'codingagent' ||
       process.process_type === 'setupscript' ||
       process.process_type === 'cleanupscript') &&
      process.status === 'running'
  );
  ```

#### 3. Verification (Medium Priority)
- Verify backend creates cleanup script processes with correct `process_type`
- Test that stop functionality works with cleanup scripts (should work automatically via existing `stopAllExecutions` API)

### Expected Outcome
When cleanup scripts are running, the 'Stop Attempt' button will appear and clicking it will stop the cleanup script, maintaining consistency with setup scripts and coding agents.

* Format
This commit is contained in:
Louis Knight-Webb
2025-07-20 16:07:48 +01:00
committed by GitHub
parent f7c01daa08
commit 5170844c76
24 changed files with 945 additions and 134 deletions

View File

@@ -283,7 +283,8 @@ const TaskDetailsProvider: FC<{
return attemptData.processes.some(
(process: ExecutionProcessSummary) =>
(process.process_type === 'codingagent' ||
process.process_type === 'setupscript') &&
process.process_type === 'setupscript' ||
process.process_type === 'cleanupscript') &&
process.status === 'running'
);
}, [selectedAttempt, attemptData.processes, isStopping]);

View File

@@ -21,6 +21,8 @@ interface ProjectFormFieldsProps {
setSetupScript: (script: string) => void;
devScript: string;
setDevScript: (script: string) => void;
cleanupScript: string;
setCleanupScript: (script: string) => void;
error: string;
}
@@ -41,6 +43,8 @@ export function ProjectFormFields({
setSetupScript,
devScript,
setDevScript,
cleanupScript,
setCleanupScript,
error,
}: ProjectFormFieldsProps) {
return (
@@ -206,6 +210,23 @@ export function ProjectFormFields({
</p>
</div>
<div className="space-y-2">
<Label htmlFor="cleanup-script">Cleanup Script (Optional)</Label>
<textarea
id="cleanup-script"
value={cleanupScript}
onChange={(e) => setCleanupScript(e.target.value)}
placeholder="#!/bin/bash&#10;# Add cleanup commands here...&#10;# This runs after coding agent execution"
rows={4}
className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring"
/>
<p className="text-sm text-muted-foreground">
This script will run after coding agent execution is complete. Use it
for cleanup tasks like stopping processes, clearing caches, or other
post-execution cleanup.
</p>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />

View File

@@ -32,6 +32,9 @@ export function ProjectForm({
const [gitRepoPath, setGitRepoPath] = useState(project?.git_repo_path || '');
const [setupScript, setSetupScript] = useState(project?.setup_script ?? '');
const [devScript, setDevScript] = useState(project?.dev_script ?? '');
const [cleanupScript, setCleanupScript] = useState(
project?.cleanup_script ?? ''
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showFolderPicker, setShowFolderPicker] = useState(false);
@@ -48,11 +51,13 @@ export function ProjectForm({
setGitRepoPath(project.git_repo_path || '');
setSetupScript(project.setup_script ?? '');
setDevScript(project.dev_script ?? '');
setCleanupScript(project.cleanup_script ?? '');
} else {
setName('');
setGitRepoPath('');
setSetupScript('');
setDevScript('');
setCleanupScript('');
}
}, [project]);
@@ -93,6 +98,7 @@ export function ProjectForm({
git_repo_path: finalGitRepoPath,
setup_script: setupScript.trim() || null,
dev_script: devScript.trim() || null,
cleanup_script: cleanupScript.trim() || null,
};
try {
@@ -108,6 +114,7 @@ export function ProjectForm({
use_existing_repo: repoMode === 'existing',
setup_script: setupScript.trim() || null,
dev_script: devScript.trim() || null,
cleanup_script: cleanupScript.trim() || null,
};
try {
@@ -122,6 +129,7 @@ export function ProjectForm({
setName('');
setGitRepoPath('');
setSetupScript('');
setCleanupScript('');
setParentPath('');
setFolderName('');
} catch (error) {
@@ -190,6 +198,8 @@ export function ProjectForm({
setSetupScript={setSetupScript}
devScript={devScript}
setDevScript={setDevScript}
cleanupScript={cleanupScript}
setCleanupScript={setCleanupScript}
error={error}
/>
<DialogFooter>
@@ -233,6 +243,8 @@ export function ProjectForm({
setSetupScript={setSetupScript}
devScript={devScript}
setDevScript={setDevScript}
cleanupScript={cleanupScript}
setCleanupScript={setCleanupScript}
error={error}
/>
<DialogFooter>

View File

@@ -0,0 +1,288 @@
import { useContext, useState } from 'react';
import {
Play,
Square,
AlertCircle,
CheckCircle,
Clock,
Cog,
ArrowLeft,
} from 'lucide-react';
import { TaskAttemptDataContext } from '@/components/context/taskDetailsContext.ts';
import { executionProcessesApi } from '@/lib/api.ts';
import type {
ExecutionProcessStatus,
ExecutionProcessSummary,
} from 'shared/types.ts';
function ProcessesTab() {
const { attemptData, setAttemptData } = useContext(TaskAttemptDataContext);
const [selectedProcessId, setSelectedProcessId] = useState<string | null>(
null
);
const [loadingProcessId, setLoadingProcessId] = useState<string | null>(null);
const getStatusIcon = (status: ExecutionProcessStatus) => {
switch (status) {
case 'running':
return <Play className="h-4 w-4 text-blue-500" />;
case 'completed':
return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'failed':
return <AlertCircle className="h-4 w-4 text-red-500" />;
case 'killed':
return <Square className="h-4 w-4 text-gray-500" />;
default:
return <Clock className="h-4 w-4 text-gray-400" />;
}
};
const getStatusColor = (status: ExecutionProcessStatus) => {
switch (status) {
case 'running':
return 'bg-blue-50 border-blue-200 text-blue-800';
case 'completed':
return 'bg-green-50 border-green-200 text-green-800';
case 'failed':
return 'bg-red-50 border-red-200 text-red-800';
case 'killed':
return 'bg-gray-50 border-gray-200 text-gray-800';
default:
return 'bg-gray-50 border-gray-200 text-gray-800';
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString();
};
const fetchProcessDetails = async (processId: string) => {
try {
setLoadingProcessId(processId);
const result = await executionProcessesApi.getDetails(processId);
if (result !== undefined) {
setAttemptData((prev) => ({
...prev,
runningProcessDetails: {
...prev.runningProcessDetails,
[processId]: result,
},
}));
}
} catch (err) {
console.error('Failed to fetch process details:', err);
} finally {
setLoadingProcessId(null);
}
};
const handleProcessClick = async (process: ExecutionProcessSummary) => {
setSelectedProcessId(process.id);
// If we don't have details for this process, fetch them
if (!attemptData.runningProcessDetails[process.id]) {
await fetchProcessDetails(process.id);
}
};
const selectedProcess = selectedProcessId
? attemptData.runningProcessDetails[selectedProcessId]
: null;
if (!attemptData.processes || attemptData.processes.length === 0) {
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<Cog className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No execution processes found for this attempt.</p>
</div>
</div>
);
}
return (
<div className="flex-1 flex flex-col min-h-0">
{!selectedProcessId ? (
<div className="flex-1 overflow-auto px-4 pb-20">
<div className="space-y-3">
{attemptData.processes.map((process) => (
<div
key={process.id}
className={`border rounded-lg p-4 hover:bg-muted/30 cursor-pointer transition-colors ${
loadingProcessId === process.id
? 'opacity-50 cursor-wait'
: ''
}`}
onClick={() => handleProcessClick(process)}
>
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
{getStatusIcon(process.status)}
<div>
<h3 className="font-medium text-sm">
{process.process_type}
{process.executor_type && (
<span className="text-muted-foreground">
{' '}
({process.executor_type})
</span>
)}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{process.command}
</p>
{process.args && (
<p className="text-xs text-muted-foreground mt-1">
Args: {process.args}
</p>
)}
</div>
</div>
<div className="text-right">
<span
className={`inline-block px-2 py-1 text-xs font-medium border rounded-full ${getStatusColor(
process.status
)}`}
>
{process.status}
</span>
{process.exit_code !== null && (
<p className="text-xs text-muted-foreground mt-1">
Exit: {process.exit_code.toString()}
</p>
)}
</div>
</div>
<div className="mt-3 text-xs text-muted-foreground">
<div className="flex justify-between">
<span>Started: {formatDate(process.started_at)}</span>
{process.completed_at && (
<span>Completed: {formatDate(process.completed_at)}</span>
)}
</div>
<div className="mt-1">
Working directory: {process.working_directory}
</div>
</div>
</div>
))}
</div>
</div>
) : (
<div className="flex-1 flex flex-col min-h-0">
<div className="flex items-center justify-between p-4 border-b flex-shrink-0">
<h2 className="text-lg font-semibold">Process Details</h2>
<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" />
Back to list
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 pb-20">
{selectedProcess ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="font-medium text-sm mb-2">Process Info</h3>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Type:</span>{' '}
{selectedProcess.process_type}
</p>
<p>
<span className="font-medium">Status:</span>{' '}
{selectedProcess.status}
</p>
{selectedProcess.executor_type && (
<p>
<span className="font-medium">Executor:</span>{' '}
{selectedProcess.executor_type}
</p>
)}
<p>
<span className="font-medium">Exit Code:</span>{' '}
{selectedProcess.exit_code?.toString() ?? 'N/A'}
</p>
</div>
</div>
<div>
<h3 className="font-medium text-sm mb-2">Timing</h3>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Started:</span>{' '}
{formatDate(selectedProcess.started_at)}
</p>
{selectedProcess.completed_at && (
<p>
<span className="font-medium">Completed:</span>{' '}
{formatDate(selectedProcess.completed_at)}
</p>
)}
</div>
</div>
</div>
<div>
<h3 className="font-medium text-sm mb-2">Command</h3>
<div className="bg-muted/50 p-3 rounded-md font-mono text-sm">
{selectedProcess.command}
{selectedProcess.args && (
<div className="mt-1 text-muted-foreground">
Args: {selectedProcess.args}
</div>
)}
</div>
</div>
<div>
<h3 className="font-medium text-sm mb-2">
Working Directory
</h3>
<div className="bg-muted/50 p-3 rounded-md font-mono text-sm">
{selectedProcess.working_directory}
</div>
</div>
{selectedProcess.stdout && (
<div>
<h3 className="font-medium text-sm mb-2">Stdout</h3>
<div className="bg-black text-green-400 p-3 rounded-md font-mono text-sm h-64 overflow-auto">
<pre className="whitespace-pre-wrap">
{selectedProcess.stdout}
</pre>
</div>
</div>
)}
{selectedProcess.stderr && (
<div>
<h3 className="font-medium text-sm mb-2">Stderr</h3>
<div className="bg-black text-red-400 p-3 rounded-md font-mono text-sm h-64 overflow-auto">
<pre className="whitespace-pre-wrap">
{selectedProcess.stderr}
</pre>
</div>
</div>
)}
</div>
) : loadingProcessId === selectedProcessId ? (
<div className="text-center text-muted-foreground">
<p>Loading process details...</p>
</div>
) : (
<div className="text-center text-muted-foreground">
<p>Failed to load process details. Please try again.</p>
</div>
)}
</div>
</div>
)}
</div>
);
}
export default ProcessesTab;

View File

@@ -1,18 +1,20 @@
import { GitCompare, MessageSquare, Network } from 'lucide-react';
import { GitCompare, MessageSquare, Network, Cog } from 'lucide-react';
import { useContext } from 'react';
import {
TaskAttemptDataContext,
TaskDiffContext,
TaskRelatedTasksContext,
} from '@/components/context/taskDetailsContext.ts';
type Props = {
activeTab: 'logs' | 'diffs' | 'related';
setActiveTab: (tab: 'logs' | 'diffs' | 'related') => void;
activeTab: 'logs' | 'diffs' | 'related' | 'processes';
setActiveTab: (tab: 'logs' | 'diffs' | 'related' | 'processes') => void;
};
function TabNavigation({ activeTab, setActiveTab }: Props) {
const { diff } = useContext(TaskDiffContext);
const { totalRelatedCount } = useContext(TaskRelatedTasksContext);
const { attemptData } = useContext(TaskAttemptDataContext);
return (
<div className="border-b bg-muted/30">
<div className="flex px-4">
@@ -65,6 +67,24 @@ function TabNavigation({ activeTab, setActiveTab }: Props) {
</span>
)}
</button>
<button
onClick={() => {
setActiveTab('processes');
}}
className={`flex items-center px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'processes'
? 'border-primary text-primary bg-background'
: 'border-transparent text-muted-foreground hover:text-foreground hover:bg-muted/50'
}`}
>
<Cog className="h-4 w-4 mr-2" />
Processes
{attemptData.processes && attemptData.processes.length > 0 && (
<span className="ml-2 px-1.5 py-0.5 text-xs bg-primary/10 text-primary rounded-full">
{attemptData.processes.length}
</span>
)}
</button>
</div>
</div>
);

View File

@@ -10,6 +10,7 @@ import type { TaskWithAttemptStatus } from 'shared/types';
import DiffTab from '@/components/tasks/TaskDetails/DiffTab.tsx';
import LogsTab from '@/components/tasks/TaskDetails/LogsTab.tsx';
import RelatedTasksTab from '@/components/tasks/TaskDetails/RelatedTasksTab.tsx';
import ProcessesTab from '@/components/tasks/TaskDetails/ProcessesTab.tsx';
import DeleteFileConfirmationDialog from '@/components/tasks/DeleteFileConfirmationDialog.tsx';
import TabNavigation from '@/components/tasks/TaskDetails/TabNavigation.tsx';
import CollapsibleToolbar from '@/components/tasks/TaskDetails/CollapsibleToolbar.tsx';
@@ -37,9 +38,9 @@ export function TaskDetailsPanel({
const [showEditorDialog, setShowEditorDialog] = useState(false);
// Tab and collapsible state
const [activeTab, setActiveTab] = useState<'logs' | 'diffs' | 'related'>(
'logs'
);
const [activeTab, setActiveTab] = useState<
'logs' | 'diffs' | 'related' | 'processes'
>('logs');
// Reset to logs tab when task changes
useEffect(() => {
@@ -101,6 +102,8 @@ export function TaskDetailsPanel({
<DiffTab />
) : activeTab === 'related' ? (
<RelatedTasksTab />
) : activeTab === 'processes' ? (
<ProcessesTab />
) : (
<LogsTab />
)}