Run setup or cleanup scripts (vibe-kanban) (#1428)

* I've successfully implemented the two new API endpoints for running setup and cleanup scripts on task attempts. Here's a summary of the changes:

## Summary

### Files Modified

1. **`crates/server/src/routes/task_attempts.rs`**
   - Added `RunScriptError` enum type for error handling when no script is configured
   - Added `run_setup_script` handler - POST endpoint to run a project's setup script
   - Added `run_cleanup_script` handler - POST endpoint to run a project's cleanup script
   - Registered both routes: `/run-setup-script` and `/run-cleanup-script`

2. **`crates/server/src/bin/generate_types.rs`**
   - Added `RunScriptError` to the list of types to export to TypeScript

3. **`shared/types.ts`** (auto-generated)
   - Now includes the `RunScriptError` TypeScript type

### New Endpoints

- **`POST /api/task-attempts/{id}/run-setup-script`** - Runs the project's setup script in the task attempt's worktree
- **`POST /api/task-attempts/{id}/run-cleanup-script`** - Runs the project's cleanup script in the task attempt's worktree

Both endpoints:
- Return `ExecutionProcess` on success with the created execution process
- Return `RunScriptError::NoScriptConfigured` if the project doesn't have the respective script configured
- Track analytics events (`setup_script_executed` / `cleanup_script_executed`)
- Use the existing `ScriptContext::SetupScript` / `ScriptContext::CleanupScript` and `ExecutionProcessRunReason::SetupScript` / `ExecutionProcessRunReason::CleanupScript` patterns

* Done! I've added the running process check to both `run_setup_script` and `run_cleanup_script` endpoints. Here's a summary of the changes:

## Summary

### Files Modified

1. **`crates/db/src/models/execution_process.rs`**
   - Added `has_running_non_dev_server_processes` method that checks if any running processes (excluding dev servers) exist for a task attempt

2. **`crates/server/src/routes/task_attempts.rs`**
   - Added `ProcessAlreadyRunning` variant to `RunScriptError` enum
   - Added check at the start of both `run_setup_script` and `run_cleanup_script` handlers to return an error if non-dev-server processes are already running

3. **`shared/types.ts`** (auto-generated)
   - `RunScriptError` now includes both `no_script_configured` and `process_already_running` variants

### Behavior

Both endpoints now:
1. First check if any non-dev-server processes (coding agents, setup/cleanup scripts) are running on the task attempt
2. If running processes exist, return `RunScriptError::ProcessAlreadyRunning`
3. Otherwise proceed with script execution as before

* Cleanup script changes for task attempt cdb9cbd9-a6e7-4936-8e75-6dd837c634f3

* Done! I've implemented the frontend changes for the setup/cleanup script dropdown. Here's a summary:

## Summary of Frontend Changes

### Files Modified

1. **`frontend/src/lib/api.ts`**
   - Added `RunScriptError` to imports from shared types
   - Added `runSetupScript` and `runCleanupScript` methods to `attemptsApi`

2. **`frontend/src/components/tasks/TaskFollowUpSection.tsx`**
   - Added imports for `Terminal` icon, `DropdownMenu`, and `Tooltip` components
   - Added `attemptsApi` import
   - Updated `useProject()` to also get `project` data
   - Added computed values: `hasSetupScript`, `hasCleanupScript`, `hasAnyScript`
   - Added handlers: `handleRunSetupScript`, `handleRunCleanupScript`
   - Added dropdown button with Terminal icon after the Attach button
   - Dropdown only shows if project has at least one script configured
   - Menu items are disabled with tooltips when scripts are not configured
   - Button is disabled when `isAttemptRunning` is true

3. **`frontend/src/i18n/locales/en/tasks.json`**
   - Added translation strings for the dropdown menu items and tooltips

### Behavior

- **Dropdown visibility**: Only shown if project has `setup_script` or `cleanup_script` configured
- **Button disabled state**: When `isAttemptRunning` is true (tooltip explains why)
- **Menu item disabled states**:
  - "Run setup script" disabled if no `setup_script` (tooltip explains)
  - "Run cleanup script" disabled if no `cleanup_script` (tooltip explains)
- **After clicking**: Script starts executing and `isAttemptRunning` becomes true automatically, preventing further interactions

* Done! I've added translations for all languages:

- **Spanish (es)**:
  - "Ejecutar script de configuración" / "Ejecutar script de limpieza"
  - Tooltips in Spanish

- **Japanese (ja)**:
  - "セットアップスクリプトを実行" / "クリーンアップスクリプトを実行"
  - Tooltips in Japanese

- **Korean (ko)**:
  - "설정 스크립트 실행" / "정리 스크립트 실행"
  - Tooltips in Korean

* Done! I've removed all the fallback strings from the `t()` calls. All 5 translation calls now use just the key:

- `{t('followUp.scriptsDisabledWhileRunning')}`
- `{t('followUp.runSetupScript')}`
- `{t('followUp.noSetupScript')}`
- `{t('followUp.runCleanupScript')}`
- `{t('followUp.noCleanupScript')}`
This commit is contained in:
Louis Knight-Webb
2025-12-04 17:57:43 +00:00
committed by GitHub
parent 0d2e77dceb
commit 32c689dfc5
10 changed files with 331 additions and 6 deletions

View File

@@ -6,9 +6,22 @@ import {
Clock,
X,
Paperclip,
Terminal,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
//
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { ScratchType, type TaskWithAttemptStatus } from 'shared/types';
@@ -42,7 +55,7 @@ import { useTranslation } from 'react-i18next';
import { useScratch } from '@/hooks/useScratch';
import { useDebouncedCallback } from '@/hooks/useDebouncedCallback';
import { useQueueStatus } from '@/hooks/useQueueStatus';
import { imagesApi } from '@/lib/api';
import { imagesApi, attemptsApi } from '@/lib/api';
interface TaskFollowUpSectionProps {
task: TaskWithAttemptStatus;
@@ -54,7 +67,7 @@ export function TaskFollowUpSection({
selectedAttemptId,
}: TaskFollowUpSectionProps) {
const { t } = useTranslation('tasks');
const { projectId } = useProject();
const { projectId, project } = useProject();
const { isAttemptRunning, stopExecution, isStopping, processes } =
useAttemptExecution(selectedAttemptId, task.id);
@@ -352,6 +365,29 @@ export function TaskFollowUpSection({
]);
const isEditable = !isRetryActive && !hasPendingApproval;
// Script availability
const hasSetupScript = Boolean(project?.setup_script);
const hasCleanupScript = Boolean(project?.cleanup_script);
const hasAnyScript = hasSetupScript || hasCleanupScript;
const handleRunSetupScript = useCallback(async () => {
if (!selectedAttemptId || isAttemptRunning || !hasSetupScript) return;
try {
await attemptsApi.runSetupScript(selectedAttemptId);
} catch (error) {
console.error('Failed to run setup script:', error);
}
}, [selectedAttemptId, isAttemptRunning, hasSetupScript]);
const handleRunCleanupScript = useCallback(async () => {
if (!selectedAttemptId || isAttemptRunning || !hasCleanupScript) return;
try {
await attemptsApi.runCleanupScript(selectedAttemptId);
} catch (error) {
console.error('Failed to run cleanup script:', error);
}
}, [selectedAttemptId, isAttemptRunning, hasCleanupScript]);
// Handler to queue the current message for execution after agent finishes
const handleQueueMessage = useCallback(async () => {
if (
@@ -685,6 +721,73 @@ export function TaskFollowUpSection({
<Paperclip className="h-4 w-4" />
</Button>
{/* Scripts dropdown - only show if project has any scripts */}
{hasAnyScript && (
<DropdownMenu>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
size="sm"
variant="outline"
disabled={isAttemptRunning}
aria-label="Run scripts"
>
<Terminal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
{isAttemptRunning && (
<TooltipContent side="bottom">
{t('followUp.scriptsDisabledWhileRunning')}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<DropdownMenuItem
disabled={!hasSetupScript}
onClick={handleRunSetupScript}
>
{t('followUp.runSetupScript')}
</DropdownMenuItem>
</span>
</TooltipTrigger>
{!hasSetupScript && (
<TooltipContent side="left">
{t('followUp.noSetupScript')}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<DropdownMenuItem
disabled={!hasCleanupScript}
onClick={handleRunCleanupScript}
>
{t('followUp.runCleanupScript')}
</DropdownMenuItem>
</span>
</TooltipTrigger>
{!hasCleanupScript && (
<TooltipContent side="left">
{t('followUp.noCleanupScript')}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</DropdownMenuContent>
</DropdownMenu>
)}
{isAttemptRunning ? (
<div className="flex items-center gap-2">
{/* Queue/Cancel Queue button when running */}

View File

@@ -153,7 +153,12 @@
"queueForNextTurn": "Queue for next turn",
"queue": "Queue",
"cancelQueue": "Cancel Queue",
"queuedMessage": "Message queued - will execute when current run finishes"
"queuedMessage": "Message queued - will execute when current run finishes",
"runSetupScript": "Run setup script",
"runCleanupScript": "Run cleanup script",
"noSetupScript": "No setup script configured for this project",
"noCleanupScript": "No cleanup script configured for this project",
"scriptsDisabledWhileRunning": "Cannot run scripts while a process is running"
},
"todos": {
"title_one": "Todos ({{count}})",

View File

@@ -183,7 +183,12 @@
"unqueuing": "Unqueuing…",
"queue": "Encolar",
"cancelQueue": "Cancelar cola",
"queuedMessage": "Mensaje en cola - se ejecutará cuando finalice la ejecución actual"
"queuedMessage": "Mensaje en cola - se ejecutará cuando finalice la ejecución actual",
"runSetupScript": "Ejecutar script de configuración",
"runCleanupScript": "Ejecutar script de limpieza",
"noSetupScript": "No hay script de configuración configurado para este proyecto",
"noCleanupScript": "No hay script de limpieza configurado para este proyecto",
"scriptsDisabledWhileRunning": "No se pueden ejecutar scripts mientras un proceso está en ejecución"
},
"git": {
"branch": {

View File

@@ -183,7 +183,12 @@
"unqueuing": "Unqueuing…",
"queue": "キューに追加",
"cancelQueue": "キューをキャンセル",
"queuedMessage": "メッセージがキューに追加されました - 現在の実行が完了すると実行されます"
"queuedMessage": "メッセージがキューに追加されました - 現在の実行が完了すると実行されます",
"runSetupScript": "セットアップスクリプトを実行",
"runCleanupScript": "クリーンアップスクリプトを実行",
"noSetupScript": "このプロジェクトにセットアップスクリプトが設定されていません",
"noCleanupScript": "このプロジェクトにクリーンアップスクリプトが設定されていません",
"scriptsDisabledWhileRunning": "プロセス実行中はスクリプトを実行できません"
},
"git": {
"branch": {

View File

@@ -183,7 +183,12 @@
"unqueuing": "Unqueuing…",
"queue": "대기열에 추가",
"cancelQueue": "대기열 취소",
"queuedMessage": "메시지가 대기열에 추가됨 - 현재 실행이 완료되면 실행됩니다"
"queuedMessage": "메시지가 대기열에 추가됨 - 현재 실행이 완료되면 실행됩니다",
"runSetupScript": "설정 스크립트 실행",
"runCleanupScript": "정리 스크립트 실행",
"noSetupScript": "이 프로젝트에 설정 스크립트가 구성되어 있지 않습니다",
"noCleanupScript": "이 프로젝트에 정리 스크립트가 구성되어 있지 않습니다",
"scriptsDisabledWhileRunning": "프로세스가 실행 중일 때는 스크립트를 실행할 수 없습니다"
},
"git": {
"branch": {

View File

@@ -47,6 +47,7 @@ import {
RunAgentSetupRequest,
RunAgentSetupResponse,
GhCliSetupError,
RunScriptError,
StatusResponse,
ListOrganizationsResponse,
OrganizationMemberWithProfile,
@@ -605,6 +606,34 @@ export const attemptsApi = {
);
return handleApiResponse<ExecutionProcess, GhCliSetupError>(response);
},
runSetupScript: async (
attemptId: string
): Promise<Result<ExecutionProcess, RunScriptError>> => {
const response = await makeRequest(
`/api/task-attempts/${attemptId}/run-setup-script`,
{
method: 'POST',
}
);
return handleApiResponseAsResult<ExecutionProcess, RunScriptError>(
response
);
},
runCleanupScript: async (
attemptId: string
): Promise<Result<ExecutionProcess, RunScriptError>> => {
const response = await makeRequest(
`/api/task-attempts/${attemptId}/run-cleanup-script`,
{
method: 'POST',
}
);
return handleApiResponseAsResult<ExecutionProcess, RunScriptError>(
response
);
},
};
// Extra helpers