Files
vibe-kanban/frontend/src/components/tasks/TaskDetailsToolbar.tsx
Gabriel Gordon-Hall 459f93b751 refactor: explicit Opencode Executor (#188)
* use display and fromStr implementations in ExecutorConfig

(cherry picked from commit 115a6a447d9195d28b9c29004fa6301fb60b1b89)
(cherry picked from commit 25d589d54a3fc89f8868f5c409f25bdb162f1326)

* rename opencode to charm/opencode

(cherry picked from commit 41fe88a46cc6c7a1cbf5ecbc3599639351c415c8)

* rename opencode on the frontend

* resuse executor types on the frontend

* put back missing types
2025-07-15 12:59:28 +01:00

278 lines
9.4 KiB
TypeScript

import { useCallback, useContext, useEffect, useState } from 'react';
import { Play } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useConfig } from '@/components/config-provider';
import { attemptsApi, projectsApi } from '@/lib/api';
import type { GitBranch, TaskAttempt } from 'shared/types';
import { EXECUTOR_TYPES, EXECUTOR_LABELS } from 'shared/types';
import {
TaskAttemptDataContext,
TaskAttemptLoadingContext,
TaskAttemptStoppingContext,
TaskDetailsContext,
TaskExecutionStateContext,
TaskSelectedAttemptContext,
} from '@/components/context/taskDetailsContext.ts';
import CreatePRDialog from '@/components/tasks/Toolbar/CreatePRDialog.tsx';
import CreateAttempt from '@/components/tasks/Toolbar/CreateAttempt.tsx';
import CurrentAttempt from '@/components/tasks/Toolbar/CurrentAttempt.tsx';
const availableExecutors = EXECUTOR_TYPES.map((id) => ({
id,
name: EXECUTOR_LABELS[id] || id,
}));
function TaskDetailsToolbar() {
const { task, projectId } = useContext(TaskDetailsContext);
const { setLoading } = useContext(TaskAttemptLoadingContext);
const { selectedAttempt, setSelectedAttempt } = useContext(
TaskSelectedAttemptContext
);
const { isStopping } = useContext(TaskAttemptStoppingContext);
const { fetchAttemptData, setAttemptData, isAttemptRunning } = useContext(
TaskAttemptDataContext
);
const { fetchExecutionState } = useContext(TaskExecutionStateContext);
const [taskAttempts, setTaskAttempts] = useState<TaskAttempt[]>([]);
const { config } = useConfig();
const [branches, setBranches] = useState<GitBranch[]>([]);
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
const [selectedExecutor, setSelectedExecutor] = useState<string>(
config?.executor.type || 'claude'
);
// State for create attempt mode
const [isInCreateAttemptMode, setIsInCreateAttemptMode] = useState(false);
const [createAttemptBranch, setCreateAttemptBranch] = useState<string | null>(
selectedBranch
);
const [createAttemptExecutor, setCreateAttemptExecutor] =
useState<string>(selectedExecutor);
// Branch status and git operations state
const [creatingPR, setCreatingPR] = useState(false);
const [showCreatePRDialog, setShowCreatePRDialog] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchProjectBranches = useCallback(async () => {
const result = await projectsApi.getBranches(projectId);
setBranches(result);
// Set current branch as default
const currentBranch = result.find((b) => b.is_current);
if (currentBranch && !selectedBranch) {
setSelectedBranch(currentBranch.name);
}
}, [projectId, selectedBranch]);
useEffect(() => {
fetchProjectBranches();
}, [fetchProjectBranches]);
// Set default executor from config
useEffect(() => {
if (config && config.executor.type !== selectedExecutor) {
setSelectedExecutor(config.executor.type);
}
}, [config, selectedExecutor]);
// Set create attempt mode when there are no attempts
useEffect(() => {
setIsInCreateAttemptMode(taskAttempts.length === 0);
}, [taskAttempts.length]);
// Update default values from latest attempt when taskAttempts change
useEffect(() => {
if (taskAttempts.length > 0) {
const latestAttempt = taskAttempts.reduce((latest, current) =>
new Date(current.created_at) > new Date(latest.created_at)
? current
: latest
);
// Only update if branch still exists in available branches
if (
latestAttempt.base_branch &&
branches.some((b: GitBranch) => b.name === latestAttempt.base_branch)
) {
setCreateAttemptBranch(latestAttempt.base_branch);
}
// Only update executor if it's different from default and exists in available executors
if (
latestAttempt.executor &&
availableExecutors.some((e) => e.id === latestAttempt.executor)
) {
setCreateAttemptExecutor(latestAttempt.executor);
}
}
}, [taskAttempts, branches, availableExecutors]);
const fetchTaskAttempts = useCallback(async () => {
if (!task) return;
try {
setLoading(true);
const result = await attemptsApi.getAll(projectId, task.id);
setTaskAttempts((prev) => {
if (JSON.stringify(prev) === JSON.stringify(result)) return prev;
return result || prev;
});
if (result.length > 0) {
const latestAttempt = result.reduce((latest, current) =>
new Date(current.created_at) > new Date(latest.created_at)
? current
: latest
);
setSelectedAttempt((prev) => {
if (JSON.stringify(prev) === JSON.stringify(latestAttempt))
return prev;
return latestAttempt;
});
fetchAttemptData(latestAttempt.id, latestAttempt.task_id);
fetchExecutionState(latestAttempt.id, latestAttempt.task_id);
} else {
setSelectedAttempt(null);
setAttemptData({
activities: [],
processes: [],
runningProcessDetails: {},
});
}
} catch (error) {
// we already logged error
} finally {
setLoading(false);
}
}, [task, projectId, fetchAttemptData, fetchExecutionState]);
useEffect(() => {
fetchTaskAttempts();
}, [fetchTaskAttempts]);
// Handle entering create attempt mode
const handleEnterCreateAttemptMode = useCallback(() => {
setIsInCreateAttemptMode(true);
// Use latest attempt's settings as defaults if available
if (taskAttempts.length > 0) {
const latestAttempt = taskAttempts.reduce((latest, current) =>
new Date(current.created_at) > new Date(latest.created_at)
? current
: latest
);
// Use latest attempt's branch if it still exists, otherwise use current selected branch
if (
latestAttempt.base_branch &&
branches.some((b: GitBranch) => b.name === latestAttempt.base_branch)
) {
setCreateAttemptBranch(latestAttempt.base_branch);
} else {
setCreateAttemptBranch(selectedBranch);
}
// Use latest attempt's executor if it exists, otherwise use current selected executor
if (
latestAttempt.executor &&
availableExecutors.some((e) => e.id === latestAttempt.executor)
) {
setCreateAttemptExecutor(latestAttempt.executor);
} else {
setCreateAttemptExecutor(selectedExecutor);
}
} else {
// Fallback to current selected values if no attempts exist
setCreateAttemptBranch(selectedBranch);
setCreateAttemptExecutor(selectedExecutor);
}
}, [taskAttempts, branches, selectedBranch, selectedExecutor]);
return (
<>
<div className="px-6 pb-4 border-b">
{/* Error Display */}
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<div className="text-red-600 text-sm">{error}</div>
</div>
)}
{isInCreateAttemptMode ? (
<CreateAttempt
fetchTaskAttempts={fetchTaskAttempts}
createAttemptBranch={createAttemptBranch}
selectedBranch={selectedBranch}
createAttemptExecutor={createAttemptExecutor}
selectedExecutor={selectedExecutor}
taskAttempts={taskAttempts}
branches={branches}
setCreateAttemptBranch={setCreateAttemptBranch}
setIsInCreateAttemptMode={setIsInCreateAttemptMode}
setCreateAttemptExecutor={setCreateAttemptExecutor}
availableExecutors={availableExecutors}
/>
) : (
<div className="space-y-3 p-3 bg-muted/20 rounded-lg border">
{/* Current Attempt Info */}
<div className="space-y-2">
{selectedAttempt ? (
<CurrentAttempt
selectedAttempt={selectedAttempt}
taskAttempts={taskAttempts}
selectedBranch={selectedBranch}
setError={setError}
setShowCreatePRDialog={setShowCreatePRDialog}
creatingPR={creatingPR}
handleEnterCreateAttemptMode={handleEnterCreateAttemptMode}
availableExecutors={availableExecutors}
/>
) : (
<div className="text-center py-8 flex-1">
<div className="text-lg font-medium text-muted-foreground">
No attempts yet
</div>
<div className="text-sm text-muted-foreground mt-1">
Start your first attempt to begin working on this task
</div>
</div>
)}
</div>
{/* Special Actions */}
{!selectedAttempt && !isAttemptRunning && !isStopping && (
<div className="space-y-2 pt-3 border-t">
<Button
onClick={handleEnterCreateAttemptMode}
size="sm"
className="w-full gap-2"
>
<Play className="h-4 w-4" />
Start Attempt
</Button>
</div>
)}
</div>
)}
</div>
<CreatePRDialog
creatingPR={creatingPR}
setShowCreatePRDialog={setShowCreatePRDialog}
showCreatePRDialog={showCreatePRDialog}
setCreatingPR={setCreatingPR}
setError={setError}
branches={branches}
/>
</>
);
}
export default TaskDetailsToolbar;