Next actions (#1082)
* Scaffold * Create next action bar (vibe-kanban 1fd0bc9a) There's a placeholder NextActionCard in frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx - We should check for the diff summary frontend/src/hooks/useDiffSummary.ts - If there is a diff, then render a summary box - The summary box should contain: - The diff summary - Whether dev server is running for this task attempt - Controls to start, stop and view logs (in processes popup) for dev server frontend/src/hooks/useDevServer.ts - Button to open task attempt in IDE frontend/src/components/ide/OpenInIdeButton.tsx * simplify error * styles * i18n * hide dev server controls if no dev server configured * tooltips * fmt * Feedback on next actions (vibe-kanban 7ff2f1b0) frontend/src/components/NormalizedConversation/NextActionCard.tsx - File changed and the +/- should be clickable and take you to diffs - Tooltip for editor should say "See changes in VS Code" (or something that make it clearer that this opens the worktree) * WIP failed variant for next action * fail styling * Create new attempt button (vibe-kanban 4ee265a2) Please add a "create new attempt" button to frontend/src/components/NormalizedConversation/NextActionCard.tsx This should be a text button "Try Again" and only show when failed = true * Git actions dialog (vibe-kanban 328ec790) frontend/src/components/tasks/Toolbar/GitOperations.tsx I want these actions to be available in a dialog that's triggerable from: - Dropdown menu in attempt header frontend/src/pages/project-tasks.tsx - a new icon in frontend/src/components/NormalizedConversation/NextActionCard.tsx * Change dev server (vibe-kanban 08df620f) Instead of hiding if no dev script, show as disabled and change the tooltip to "To start the dev server, add a dev script to this project" frontend/src/components/NormalizedConversation/NextActionCard.tsx * i18n (vibe-kanban 0e07797b) Look for any missing i18n strings in frontend/src/components/NormalizedConversation/NextActionCard.tsx and frontend/src/components/dialogs/tasks/GitActionsDialog.tsx * Done! I've successfully fixed the i18n issues. The script `scripts/check-i18n.sh` was running correctly, but it was failing because there were missing translation keys in the non-English locales (Spanish, Japanese, and Korean). (#1093) ## What was fixed: The script checks that all translation keys in the English locale file exist in all other locale files. There were 4 missing keys related to the new Git Actions feature: 1. `actionsMenu.gitActions` 2. `attempt.gitActions` 3. `git.actions.title` 4. `git.actions.prMerged` I added appropriate translations for these keys to all three locale files: - **Spanish (es)**: "Acciones de Git" and "PR #{{number}} ya está fusionado" - **Japanese (ja)**: "Gitアクション" and "PR #{{number}} は既にマージされています" - **Korean (ko)**: "Git 작업" and "PR #{{number}}은(는) 이미 병합되었습니다" The i18n check now passes all three validation steps: - ✅ No new literal strings introduced - ✅ No duplicate keys found in JSON files - ✅ Translation keys are consistent across locales * hide try again if more than 2 execution processes --------- Co-authored-by: Alex Netsch <alex@bloop.ai>
This commit is contained in:
committed by
GitHub
parent
f88daa4826
commit
6fc7410b28
@@ -65,6 +65,10 @@ pub enum NormalizedEntryType {
|
|||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
Thinking,
|
Thinking,
|
||||||
Loading,
|
Loading,
|
||||||
|
NextAction {
|
||||||
|
failed: bool,
|
||||||
|
execution_processes: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import RawLogText from '../common/RawLogText';
|
import RawLogText from '../common/RawLogText';
|
||||||
import UserMessage from './UserMessage';
|
import UserMessage from './UserMessage';
|
||||||
import PendingApprovalEntry from './PendingApprovalEntry';
|
import PendingApprovalEntry from './PendingApprovalEntry';
|
||||||
|
import { NextActionCard } from './NextActionCard';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useRetryUi } from '@/contexts/RetryUiContext';
|
import { useRetryUi } from '@/contexts/RetryUiContext';
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ type Props = {
|
|||||||
diffDeletable?: boolean;
|
diffDeletable?: boolean;
|
||||||
executionProcessId?: string;
|
executionProcessId?: string;
|
||||||
taskAttempt?: TaskAttempt;
|
taskAttempt?: TaskAttempt;
|
||||||
|
task?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileEditAction = Extract<ActionType, { action: 'file_edit' }>;
|
type FileEditAction = Extract<ActionType, { action: 'file_edit' }>;
|
||||||
@@ -603,6 +605,7 @@ function DisplayConversationEntry({
|
|||||||
expansionKey,
|
expansionKey,
|
||||||
executionProcessId,
|
executionProcessId,
|
||||||
taskAttempt,
|
taskAttempt,
|
||||||
|
task,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const isNormalizedEntry = (
|
const isNormalizedEntry = (
|
||||||
@@ -779,6 +782,20 @@ function DisplayConversationEntry({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.entry_type.type === 'next_action') {
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-2 text-sm">
|
||||||
|
<NextActionCard
|
||||||
|
attemptId={taskAttempt?.id}
|
||||||
|
containerRef={taskAttempt?.container_ref}
|
||||||
|
failed={entry.entry_type.failed}
|
||||||
|
execution_processes={entry.entry_type.execution_processes}
|
||||||
|
task={task}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-2 text-sm">
|
<div className="px-4 py-2 text-sm">
|
||||||
<div className={getContentClassName(entryType)}>
|
<div className={getContentClassName(entryType)}>
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
Terminal,
|
||||||
|
FileDiff,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
GitBranch,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
|
import { useOpenInEditor } from '@/hooks/useOpenInEditor';
|
||||||
|
import { useDiffSummary } from '@/hooks/useDiffSummary';
|
||||||
|
import { useDevServer } from '@/hooks/useDevServer';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { IdeIcon } from '@/components/ide/IdeIcon';
|
||||||
|
import { useUserSystem } from '@/components/config-provider';
|
||||||
|
import { getIdeName } from '@/components/ide/IdeIcon';
|
||||||
|
import { useProject } from '@/contexts/project-context';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { attemptsApi } from '@/lib/api';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
type NextActionCardProps = {
|
||||||
|
attemptId?: string;
|
||||||
|
containerRef?: string | null;
|
||||||
|
failed: boolean;
|
||||||
|
execution_processes: number;
|
||||||
|
task?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NextActionCard({
|
||||||
|
attemptId,
|
||||||
|
containerRef,
|
||||||
|
failed,
|
||||||
|
execution_processes,
|
||||||
|
task,
|
||||||
|
}: NextActionCardProps) {
|
||||||
|
const { t } = useTranslation('tasks');
|
||||||
|
const { config } = useUserSystem();
|
||||||
|
const { project } = useProject();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const { data: attempt } = useQuery({
|
||||||
|
queryKey: ['attempt', attemptId],
|
||||||
|
queryFn: () => attemptsApi.get(attemptId!),
|
||||||
|
enabled: !!attemptId && failed,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openInEditor = useOpenInEditor(attemptId);
|
||||||
|
const { fileCount, added, deleted, error } = useDiffSummary(
|
||||||
|
attemptId ?? null
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
isStarting,
|
||||||
|
isStopping,
|
||||||
|
runningDevServer,
|
||||||
|
latestDevServerProcess,
|
||||||
|
} = useDevServer(attemptId);
|
||||||
|
|
||||||
|
const projectHasDevScript = Boolean(project?.dev_script);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(async () => {
|
||||||
|
if (!containerRef) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(containerRef);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Copy to clipboard failed:', err);
|
||||||
|
}
|
||||||
|
}, [containerRef]);
|
||||||
|
|
||||||
|
const handleOpenInEditor = useCallback(() => {
|
||||||
|
openInEditor();
|
||||||
|
}, [openInEditor]);
|
||||||
|
|
||||||
|
const handleViewLogs = useCallback(() => {
|
||||||
|
if (attemptId) {
|
||||||
|
NiceModal.show('view-processes', {
|
||||||
|
attemptId,
|
||||||
|
initialProcessId: latestDevServerProcess?.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [attemptId, latestDevServerProcess?.id]);
|
||||||
|
|
||||||
|
const handleOpenDiffs = useCallback(() => {
|
||||||
|
navigate({ search: '?view=diffs' });
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleTryAgain = useCallback(() => {
|
||||||
|
if (!attempt?.task_id) return;
|
||||||
|
NiceModal.show('create-attempt', {
|
||||||
|
taskId: attempt.task_id,
|
||||||
|
latestAttempt: attemptId,
|
||||||
|
});
|
||||||
|
}, [attempt?.task_id, attemptId]);
|
||||||
|
|
||||||
|
const handleGitActions = useCallback(() => {
|
||||||
|
if (!attemptId) return;
|
||||||
|
NiceModal.show('git-actions', {
|
||||||
|
attemptId,
|
||||||
|
task,
|
||||||
|
projectId: project?.id,
|
||||||
|
});
|
||||||
|
}, [attemptId, task, project?.id]);
|
||||||
|
|
||||||
|
const editorName = getIdeName(config?.editor?.editor_type);
|
||||||
|
|
||||||
|
// Necessary to prevent this component being displayed beyond fold within Virtualised List
|
||||||
|
if ((!failed || execution_processes > 2) && fileCount === 0) {
|
||||||
|
return <div className="h-24"></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="pt-4 pb-8">
|
||||||
|
<div
|
||||||
|
className={`px-3 py-1 text-background flex ${failed ? 'bg-destructive' : 'bg-foreground'}`}
|
||||||
|
>
|
||||||
|
<span className="font-semibold flex-1">
|
||||||
|
{t('attempt.labels.summaryAndActions')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`border px-3 py-2 flex items-center gap-3 min-w-0 ${failed ? 'border-destructive' : 'border-foreground'}`}
|
||||||
|
>
|
||||||
|
{/* Left: Diff summary */}
|
||||||
|
{!error && (
|
||||||
|
<button
|
||||||
|
onClick={handleOpenDiffs}
|
||||||
|
className="flex items-center gap-1.5 text-sm shrink-0 cursor-pointer hover:underline transition-all"
|
||||||
|
aria-label={t('attempt.diffs')}
|
||||||
|
>
|
||||||
|
<span>{t('diff.filesChanged', { count: fileCount })}</span>
|
||||||
|
<span className="opacity-50">•</span>
|
||||||
|
<span className="text-green-600 dark:text-green-400">
|
||||||
|
+{added}
|
||||||
|
</span>
|
||||||
|
<span className="opacity-50">•</span>
|
||||||
|
<span className="text-red-600 dark:text-red-400">-{deleted}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* Try Again button */}
|
||||||
|
{failed && execution_processes <= 2 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleTryAgain}
|
||||||
|
disabled={!attempt?.task_id}
|
||||||
|
className="text-sm"
|
||||||
|
aria-label={t('attempt.tryAgain')}
|
||||||
|
>
|
||||||
|
{t('attempt.tryAgain')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right: Icon buttons */}
|
||||||
|
{fileCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={handleOpenDiffs}
|
||||||
|
aria-label={t('attempt.diffs')}
|
||||||
|
>
|
||||||
|
<FileDiff className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t('attempt.diffs')}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{containerRef && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={handleCopy}
|
||||||
|
aria-label={t('attempt.clickToCopy')}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-3.5 w-3.5 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{copied ? t('attempt.copied') : t('attempt.clickToCopy')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={handleOpenInEditor}
|
||||||
|
disabled={!attemptId}
|
||||||
|
aria-label={t('attempt.openInEditor', {
|
||||||
|
editor: editorName,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<IdeIcon
|
||||||
|
editorType={config?.editor?.editor_type}
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t('attempt.openInEditor', { editor: editorName })}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="inline-block">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={runningDevServer ? () => stop() : () => start()}
|
||||||
|
disabled={
|
||||||
|
(runningDevServer ? isStopping : isStarting) ||
|
||||||
|
!attemptId ||
|
||||||
|
!projectHasDevScript
|
||||||
|
}
|
||||||
|
aria-label={
|
||||||
|
runningDevServer
|
||||||
|
? t('attempt.pauseDev')
|
||||||
|
: t('attempt.startDev')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{runningDevServer ? (
|
||||||
|
<Pause className="h-3.5 w-3.5 text-destructive" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{!projectHasDevScript
|
||||||
|
? t('attempt.devScriptMissingTooltip')
|
||||||
|
: runningDevServer
|
||||||
|
? t('attempt.pauseDev')
|
||||||
|
: t('attempt.startDev')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{latestDevServerProcess && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={handleViewLogs}
|
||||||
|
disabled={!attemptId}
|
||||||
|
aria-label={t('attempt.viewDevLogs')}
|
||||||
|
>
|
||||||
|
<Terminal className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t('attempt.viewDevLogs')}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={handleGitActions}
|
||||||
|
disabled={!attemptId}
|
||||||
|
aria-label={t('attempt.gitActions')}
|
||||||
|
>
|
||||||
|
<GitBranch className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t('attempt.gitActions')}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -61,6 +61,10 @@ export {
|
|||||||
ViewProcessesDialog,
|
ViewProcessesDialog,
|
||||||
type ViewProcessesDialogProps,
|
type ViewProcessesDialogProps,
|
||||||
} from './tasks/ViewProcessesDialog';
|
} from './tasks/ViewProcessesDialog';
|
||||||
|
export {
|
||||||
|
GitActionsDialog,
|
||||||
|
type GitActionsDialogProps,
|
||||||
|
} from './tasks/GitActionsDialog';
|
||||||
|
|
||||||
// Settings dialogs
|
// Settings dialogs
|
||||||
export {
|
export {
|
||||||
|
|||||||
164
frontend/src/components/dialogs/tasks/GitActionsDialog.tsx
Normal file
164
frontend/src/components/dialogs/tasks/GitActionsDialog.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ExternalLink, GitPullRequest } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Loader } from '@/components/ui/loader';
|
||||||
|
import GitOperations from '@/components/tasks/Toolbar/GitOperations';
|
||||||
|
import { useTaskAttempt } from '@/hooks/useTaskAttempt';
|
||||||
|
import { useBranchStatus, useAttemptExecution } from '@/hooks';
|
||||||
|
import { useProject } from '@/contexts/project-context';
|
||||||
|
import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext';
|
||||||
|
import { projectsApi } from '@/lib/api';
|
||||||
|
import type {
|
||||||
|
GitBranch,
|
||||||
|
TaskAttempt,
|
||||||
|
TaskWithAttemptStatus,
|
||||||
|
} from 'shared/types';
|
||||||
|
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||||
|
|
||||||
|
export interface GitActionsDialogProps {
|
||||||
|
attemptId: string;
|
||||||
|
task?: TaskWithAttemptStatus;
|
||||||
|
projectId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GitActionsDialogContentProps {
|
||||||
|
attempt: TaskAttempt;
|
||||||
|
task: TaskWithAttemptStatus;
|
||||||
|
projectId: string;
|
||||||
|
branches: GitBranch[];
|
||||||
|
gitError: string | null;
|
||||||
|
setGitError: (error: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GitActionsDialogContent({
|
||||||
|
attempt,
|
||||||
|
task,
|
||||||
|
projectId,
|
||||||
|
branches,
|
||||||
|
gitError,
|
||||||
|
setGitError,
|
||||||
|
}: GitActionsDialogContentProps) {
|
||||||
|
const { t } = useTranslation('tasks');
|
||||||
|
const { data: branchStatus } = useBranchStatus(attempt.id);
|
||||||
|
const { isAttemptRunning } = useAttemptExecution(attempt.id);
|
||||||
|
|
||||||
|
const mergedPR = branchStatus?.merges?.find(
|
||||||
|
(m) => m.type === 'pr' && m.pr_info?.status === 'merged'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mergedPR && mergedPR.type === 'pr') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{t('git.actions.prMerged', {
|
||||||
|
number: mergedPR.pr_info.number || '',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
{mergedPR.pr_info.url && (
|
||||||
|
<a
|
||||||
|
href={mergedPR.pr_info.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-primary hover:underline"
|
||||||
|
>
|
||||||
|
<GitPullRequest className="h-3.5 w-3.5" />
|
||||||
|
{t('git.pr.number', {
|
||||||
|
number: Number(mergedPR.pr_info.number),
|
||||||
|
})}
|
||||||
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{gitError && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded text-destructive text-sm">
|
||||||
|
{gitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<GitOperations
|
||||||
|
selectedAttempt={attempt}
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
branchStatus={branchStatus ?? null}
|
||||||
|
branches={branches}
|
||||||
|
isAttemptRunning={isAttemptRunning}
|
||||||
|
setError={setGitError}
|
||||||
|
selectedBranch={branchStatus?.target_branch_name ?? null}
|
||||||
|
layout="vertical"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GitActionsDialog = NiceModal.create<GitActionsDialogProps>(
|
||||||
|
({ attemptId, task, projectId: providedProjectId }) => {
|
||||||
|
const modal = useModal();
|
||||||
|
const { t } = useTranslation('tasks');
|
||||||
|
const { project } = useProject();
|
||||||
|
|
||||||
|
const effectiveProjectId = providedProjectId ?? project?.id;
|
||||||
|
const { data: attempt } = useTaskAttempt(attemptId);
|
||||||
|
|
||||||
|
const [branches, setBranches] = useState<GitBranch[]>([]);
|
||||||
|
const [gitError, setGitError] = useState<string | null>(null);
|
||||||
|
const [loadingBranches, setLoadingBranches] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!effectiveProjectId) return;
|
||||||
|
setLoadingBranches(true);
|
||||||
|
projectsApi
|
||||||
|
.getBranches(effectiveProjectId)
|
||||||
|
.then(setBranches)
|
||||||
|
.catch(() => setBranches([]))
|
||||||
|
.finally(() => setLoadingBranches(false));
|
||||||
|
}, [effectiveProjectId]);
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
!attempt || !effectiveProjectId || loadingBranches || !task;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={modal.visible} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('git.actions.title')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="py-8">
|
||||||
|
<Loader size={24} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ExecutionProcessesProvider key={attempt.id} attemptId={attempt.id}>
|
||||||
|
<GitActionsDialogContent
|
||||||
|
attempt={attempt}
|
||||||
|
task={task}
|
||||||
|
projectId={effectiveProjectId}
|
||||||
|
branches={branches}
|
||||||
|
gitError={gitError}
|
||||||
|
setGitError={setGitError}
|
||||||
|
/>
|
||||||
|
</ExecutionProcessesProvider>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -11,10 +11,11 @@ import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext';
|
|||||||
|
|
||||||
export interface ViewProcessesDialogProps {
|
export interface ViewProcessesDialogProps {
|
||||||
attemptId: string;
|
attemptId: string;
|
||||||
|
initialProcessId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ViewProcessesDialog = NiceModal.create<ViewProcessesDialogProps>(
|
export const ViewProcessesDialog = NiceModal.create<ViewProcessesDialogProps>(
|
||||||
({ attemptId }) => {
|
({ attemptId, initialProcessId }) => {
|
||||||
const { t } = useTranslation('tasks');
|
const { t } = useTranslation('tasks');
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export const ViewProcessesDialog = NiceModal.create<ViewProcessesDialogProps>(
|
|||||||
<DialogTitle>{t('viewProcessesDialog.title')}</DialogTitle>
|
<DialogTitle>{t('viewProcessesDialog.title')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="h-[75vh] flex flex-col min-h-0 min-w-0">
|
<div className="h-[75vh] flex flex-col min-h-0 min-w-0">
|
||||||
<ProcessSelectionProvider>
|
<ProcessSelectionProvider initialProcessId={initialProcessId}>
|
||||||
<ProcessesTab attemptId={attemptId} />
|
<ProcessesTab attemptId={attemptId} />
|
||||||
</ProcessSelectionProvider>
|
</ProcessSelectionProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ import {
|
|||||||
useConversationHistory,
|
useConversationHistory,
|
||||||
} from '@/hooks/useConversationHistory';
|
} from '@/hooks/useConversationHistory';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { TaskAttempt } from 'shared/types';
|
import { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
|
||||||
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
|
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
|
||||||
|
|
||||||
interface VirtualizedListProps {
|
interface VirtualizedListProps {
|
||||||
attempt: TaskAttempt;
|
attempt: TaskAttempt;
|
||||||
|
task?: TaskWithAttemptStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageListContext {
|
interface MessageListContext {
|
||||||
attempt: TaskAttempt;
|
attempt: TaskAttempt;
|
||||||
|
task?: TaskWithAttemptStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_TOP_ITEM = { index: 'LAST' as const, align: 'end' as const };
|
const INITIAL_TOP_ITEM = { index: 'LAST' as const, align: 'end' as const };
|
||||||
@@ -45,6 +47,7 @@ const ItemContent: VirtuosoMessageListProps<
|
|||||||
MessageListContext
|
MessageListContext
|
||||||
>['ItemContent'] = ({ data, context }) => {
|
>['ItemContent'] = ({ data, context }) => {
|
||||||
const attempt = context?.attempt;
|
const attempt = context?.attempt;
|
||||||
|
const task = context?.task;
|
||||||
|
|
||||||
if (data.type === 'STDOUT') {
|
if (data.type === 'STDOUT') {
|
||||||
return <p>{data.content}</p>;
|
return <p>{data.content}</p>;
|
||||||
@@ -59,6 +62,7 @@ const ItemContent: VirtuosoMessageListProps<
|
|||||||
entry={data.content}
|
entry={data.content}
|
||||||
executionProcessId={data.executionProcessId}
|
executionProcessId={data.executionProcessId}
|
||||||
taskAttempt={attempt}
|
taskAttempt={attempt}
|
||||||
|
task={task}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,7 +75,7 @@ const computeItemKey: VirtuosoMessageListProps<
|
|||||||
MessageListContext
|
MessageListContext
|
||||||
>['computeItemKey'] = ({ data }) => `l-${data.patchKey}`;
|
>['computeItemKey'] = ({ data }) => `l-${data.patchKey}`;
|
||||||
|
|
||||||
const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
|
const VirtualizedList = ({ attempt, task }: VirtualizedListProps) => {
|
||||||
const [channelData, setChannelData] =
|
const [channelData, setChannelData] =
|
||||||
useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null);
|
useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -105,7 +109,10 @@ const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
|
|||||||
useConversationHistory({ attempt, onEntriesUpdated });
|
useConversationHistory({ attempt, onEntriesUpdated });
|
||||||
|
|
||||||
const messageListRef = useRef<VirtuosoMessageListMethods | null>(null);
|
const messageListRef = useRef<VirtuosoMessageListMethods | null>(null);
|
||||||
const messageListContext = useMemo(() => ({ attempt }), [attempt]);
|
const messageListContext = useMemo(
|
||||||
|
() => ({ attempt, task }),
|
||||||
|
[attempt, task]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApprovalFormProvider>
|
<ApprovalFormProvider>
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ const TaskAttemptPanel = ({
|
|||||||
<EntriesProvider key={attempt.id}>
|
<EntriesProvider key={attempt.id}>
|
||||||
<RetryUiProvider attemptId={attempt.id}>
|
<RetryUiProvider attemptId={attempt.id}>
|
||||||
{children({
|
{children({
|
||||||
logs: <VirtualizedList key={attempt.id} attempt={attempt} />,
|
logs: (
|
||||||
|
<VirtualizedList key={attempt.id} attempt={attempt} task={task} />
|
||||||
|
),
|
||||||
followUp: (
|
followUp: (
|
||||||
<TaskFollowUpSection
|
<TaskFollowUpSection
|
||||||
task={task}
|
task={task}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface GitOperationsProps {
|
|||||||
isAttemptRunning: boolean;
|
isAttemptRunning: boolean;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
selectedBranch: string | null;
|
selectedBranch: string | null;
|
||||||
|
layout?: 'horizontal' | 'vertical';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GitOperationsInputs = Omit<GitOperationsProps, 'selectedAttempt'>;
|
export type GitOperationsInputs = Omit<GitOperationsProps, 'selectedAttempt'>;
|
||||||
@@ -54,6 +55,7 @@ function GitOperations({
|
|||||||
isAttemptRunning,
|
isAttemptRunning,
|
||||||
setError,
|
setError,
|
||||||
selectedBranch,
|
selectedBranch,
|
||||||
|
layout = 'horizontal',
|
||||||
}: GitOperationsProps) {
|
}: GitOperationsProps) {
|
||||||
const { t } = useTranslation('tasks');
|
const { t } = useTranslation('tasks');
|
||||||
|
|
||||||
@@ -268,9 +270,23 @@ function GitOperations({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isVertical = layout === 'vertical';
|
||||||
|
|
||||||
|
const containerClasses = isVertical
|
||||||
|
? 'grid grid-cols-1 items-start gap-3 overflow-hidden'
|
||||||
|
: 'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 overflow-hidden';
|
||||||
|
|
||||||
|
const settingsBtnClasses = isVertical
|
||||||
|
? 'inline-flex h-5 w-5 p-0 hover:bg-muted'
|
||||||
|
: 'hidden md:inline-flex h-5 w-5 p-0 hover:bg-muted';
|
||||||
|
|
||||||
|
const actionsClasses = isVertical
|
||||||
|
? 'flex flex-wrap items-center gap-2'
|
||||||
|
: 'shrink-0 flex flex-wrap items-center gap-2 overflow-y-hidden overflow-x-visible max-h-8';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full border-b py-2">
|
<div className="w-full border-b py-2">
|
||||||
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 overflow-hidden">
|
<div className={containerClasses}>
|
||||||
{/* Left: Branch flow */}
|
{/* Left: Branch flow */}
|
||||||
<div className="flex items-center gap-2 min-w-0 shrink-0 overflow-hidden">
|
<div className="flex items-center gap-2 min-w-0 shrink-0 overflow-hidden">
|
||||||
{/* Task branch chip */}
|
{/* Task branch chip */}
|
||||||
@@ -319,7 +335,7 @@ function GitOperations({
|
|||||||
size="xs"
|
size="xs"
|
||||||
onClick={handleChangeTargetBranchDialogOpen}
|
onClick={handleChangeTargetBranchDialogOpen}
|
||||||
disabled={isAttemptRunning || hasConflictsCalculated}
|
disabled={isAttemptRunning || hasConflictsCalculated}
|
||||||
className="hidden md:inline-flex h-5 w-5 p-0 hover:bg-muted"
|
className={settingsBtnClasses}
|
||||||
aria-label={t('branches.changeTarget.dialog.title')}
|
aria-label={t('branches.changeTarget.dialog.title')}
|
||||||
>
|
>
|
||||||
<Settings className="h-3.5 w-3.5" />
|
<Settings className="h-3.5 w-3.5" />
|
||||||
@@ -421,9 +437,9 @@ function GitOperations({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Actions (compact, right-aligned) */}
|
{/* Right: Actions */}
|
||||||
{branchStatus && (
|
{branchStatus && (
|
||||||
<div className="shrink-0 flex flex-wrap items-center gap-2 overflow-y-hidden overflow-x-visible max-h-8">
|
<div className={actionsClasses}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleMergeClick}
|
onClick={handleMergeClick}
|
||||||
disabled={
|
disabled={
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ export function ActionsDropdown({ task, attempt }: ActionsDropdownProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGitActions = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!attempt?.id || !task) return;
|
||||||
|
NiceModal.show('git-actions', {
|
||||||
|
attemptId: attempt.id,
|
||||||
|
task,
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -118,6 +128,12 @@ export function ActionsDropdown({ task, attempt }: ActionsDropdownProps) {
|
|||||||
>
|
>
|
||||||
{t('actionsMenu.createSubtask')}
|
{t('actionsMenu.createSubtask')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!attempt?.id || !task}
|
||||||
|
onClick={handleGitActions}
|
||||||
|
>
|
||||||
|
{t('actionsMenu.gitActions')}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ const ProcessSelectionContext =
|
|||||||
|
|
||||||
interface ProcessSelectionProviderProps {
|
interface ProcessSelectionProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
initialProcessId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProcessSelectionProvider({
|
export function ProcessSelectionProvider({
|
||||||
children,
|
children,
|
||||||
|
initialProcessId = null,
|
||||||
}: ProcessSelectionProviderProps) {
|
}: ProcessSelectionProviderProps) {
|
||||||
const [selectedProcessId, setSelectedProcessId] = useState<string | null>(
|
const [selectedProcessId, setSelectedProcessId] = useState<string | null>(
|
||||||
null
|
initialProcessId
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
|
|||||||
@@ -50,6 +50,37 @@ interface UseConversationHistoryResult {}
|
|||||||
const MIN_INITIAL_ENTRIES = 10;
|
const MIN_INITIAL_ENTRIES = 10;
|
||||||
const REMAINING_BATCH_SIZE = 50;
|
const REMAINING_BATCH_SIZE = 50;
|
||||||
|
|
||||||
|
const loadingPatch: PatchTypeWithKey = {
|
||||||
|
type: 'NORMALIZED_ENTRY',
|
||||||
|
content: {
|
||||||
|
entry_type: {
|
||||||
|
type: 'loading',
|
||||||
|
},
|
||||||
|
content: '',
|
||||||
|
timestamp: null,
|
||||||
|
},
|
||||||
|
patchKey: 'loading',
|
||||||
|
executionProcessId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextActionPatch: (
|
||||||
|
failed: boolean,
|
||||||
|
execution_processes: number
|
||||||
|
) => PatchTypeWithKey = (failed, execution_processes) => ({
|
||||||
|
type: 'NORMALIZED_ENTRY',
|
||||||
|
content: {
|
||||||
|
entry_type: {
|
||||||
|
type: 'next_action',
|
||||||
|
failed: failed,
|
||||||
|
execution_processes: execution_processes,
|
||||||
|
},
|
||||||
|
content: '',
|
||||||
|
timestamp: null,
|
||||||
|
},
|
||||||
|
patchKey: 'next_action',
|
||||||
|
executionProcessId: '',
|
||||||
|
});
|
||||||
|
|
||||||
export const useConversationHistory = ({
|
export const useConversationHistory = ({
|
||||||
attempt,
|
attempt,
|
||||||
onEntriesUpdated,
|
onEntriesUpdated,
|
||||||
@@ -197,22 +228,14 @@ export const useConversationHistory = ({
|
|||||||
.flatMap((p) => p.entries);
|
.flatMap((p) => p.entries);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadingPatch: PatchTypeWithKey = {
|
|
||||||
type: 'NORMALIZED_ENTRY',
|
|
||||||
content: {
|
|
||||||
entry_type: {
|
|
||||||
type: 'loading',
|
|
||||||
},
|
|
||||||
content: '',
|
|
||||||
timestamp: null,
|
|
||||||
},
|
|
||||||
patchKey: 'loading',
|
|
||||||
executionProcessId: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const flattenEntriesForEmit = (
|
const flattenEntriesForEmit = (
|
||||||
executionProcessState: ExecutionProcessStateStore
|
executionProcessState: ExecutionProcessStateStore
|
||||||
): PatchTypeWithKey[] => {
|
): PatchTypeWithKey[] => {
|
||||||
|
// Flags to control Next Action bar emit
|
||||||
|
let hasPendingApproval = false;
|
||||||
|
let hasRunningProcess = false;
|
||||||
|
let lastProcessFailedOrKilled = false;
|
||||||
|
|
||||||
// Create user messages + tool calls for setup/cleanup scripts
|
// Create user messages + tool calls for setup/cleanup scripts
|
||||||
const allEntries = Object.values(executionProcessState)
|
const allEntries = Object.values(executionProcessState)
|
||||||
.sort(
|
.sort(
|
||||||
@@ -222,7 +245,7 @@ export const useConversationHistory = ({
|
|||||||
).getTime() -
|
).getTime() -
|
||||||
new Date(b.executionProcess.created_at as unknown as string).getTime()
|
new Date(b.executionProcess.created_at as unknown as string).getTime()
|
||||||
)
|
)
|
||||||
.flatMap((p) => {
|
.flatMap((p, index) => {
|
||||||
const entries: PatchTypeWithKey[] = [];
|
const entries: PatchTypeWithKey[] = [];
|
||||||
if (
|
if (
|
||||||
p.executionProcess.executor_action.typ.type ===
|
p.executionProcess.executor_action.typ.type ===
|
||||||
@@ -265,10 +288,31 @@ export const useConversationHistory = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasPendingApprovalEntry) {
|
||||||
|
hasPendingApproval = true;
|
||||||
|
}
|
||||||
|
|
||||||
entries.push(...entriesExcludingUser);
|
entries.push(...entriesExcludingUser);
|
||||||
|
|
||||||
|
const liveProcessStatus = getLiveExecutionProcess(
|
||||||
|
p.executionProcess.id
|
||||||
|
)?.status;
|
||||||
const isProcessRunning =
|
const isProcessRunning =
|
||||||
getLiveExecutionProcess(p.executionProcess.id)?.status ===
|
liveProcessStatus === ExecutionProcessStatus.running;
|
||||||
ExecutionProcessStatus.running;
|
const processFailedOrKilled =
|
||||||
|
liveProcessStatus === ExecutionProcessStatus.failed ||
|
||||||
|
liveProcessStatus === ExecutionProcessStatus.killed;
|
||||||
|
|
||||||
|
if (isProcessRunning) {
|
||||||
|
hasRunningProcess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
processFailedOrKilled &&
|
||||||
|
index === Object.keys(executionProcessState).length - 1
|
||||||
|
) {
|
||||||
|
lastProcessFailedOrKilled = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (isProcessRunning && !hasPendingApprovalEntry) {
|
if (isProcessRunning && !hasPendingApprovalEntry) {
|
||||||
entries.push(loadingPatch);
|
entries.push(loadingPatch);
|
||||||
@@ -293,6 +337,18 @@ export const useConversationHistory = ({
|
|||||||
p.executionProcess.id
|
p.executionProcess.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (executionProcess?.status === ExecutionProcessStatus.running) {
|
||||||
|
hasRunningProcess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(executionProcess?.status === ExecutionProcessStatus.failed ||
|
||||||
|
executionProcess?.status === ExecutionProcessStatus.killed) &&
|
||||||
|
index === Object.keys(executionProcessState).length - 1
|
||||||
|
) {
|
||||||
|
lastProcessFailedOrKilled = true;
|
||||||
|
}
|
||||||
|
|
||||||
const exitCode = Number(executionProcess?.exit_code) || 0;
|
const exitCode = Number(executionProcess?.exit_code) || 0;
|
||||||
const exit_status: CommandExitStatus | null =
|
const exit_status: CommandExitStatus | null =
|
||||||
executionProcess?.status === 'running'
|
executionProcess?.status === 'running'
|
||||||
@@ -344,6 +400,16 @@ export const useConversationHistory = ({
|
|||||||
return entries;
|
return entries;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Emit the next action bar if no process running
|
||||||
|
if (!hasRunningProcess && !hasPendingApproval) {
|
||||||
|
allEntries.push(
|
||||||
|
nextActionPatch(
|
||||||
|
lastProcessFailedOrKilled,
|
||||||
|
Object.keys(executionProcessState).length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return allEntries;
|
return allEntries;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -159,14 +159,16 @@
|
|||||||
"attempt": "Attempt",
|
"attempt": "Attempt",
|
||||||
"agent": "Agent",
|
"agent": "Agent",
|
||||||
"branch": "Branch",
|
"branch": "Branch",
|
||||||
"diffs": "Diffs"
|
"diffs": "Diffs",
|
||||||
|
"summaryAndActions": "Summary & Actions"
|
||||||
},
|
},
|
||||||
"agent": "Agent",
|
"agent": "Agent",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
"openInEditor": "Open in {{editor}}",
|
"openInEditor": "See changes in {{editor}}",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"clickToCopy": "Click to copy worktree path",
|
"clickToCopy": "Copy worktree path",
|
||||||
"stopDev": "Stop Dev",
|
"stopDev": "Stop Dev",
|
||||||
|
"pauseDev": "Pause Dev",
|
||||||
"startDev": "Start Dev",
|
"startDev": "Start Dev",
|
||||||
"viewDevLogs": "View dev server logs",
|
"viewDevLogs": "View dev server logs",
|
||||||
"stopping": "Stopping...",
|
"stopping": "Stopping...",
|
||||||
@@ -175,7 +177,10 @@
|
|||||||
"viewHistory": "View attempt history",
|
"viewHistory": "View attempt history",
|
||||||
"createSubtask": "Create Subtask",
|
"createSubtask": "Create Subtask",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"diffs": "Diffs"
|
"diffs": "Diffs",
|
||||||
|
"gitActions": "Git Actions",
|
||||||
|
"tryAgain": "Try Again",
|
||||||
|
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -213,6 +218,10 @@
|
|||||||
"pr": {
|
"pr": {
|
||||||
"open": "Open PR #{{number}}",
|
"open": "Open PR #{{number}}",
|
||||||
"number": "PR #{{number}}"
|
"number": "PR #{{number}}"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"title": "Git Actions",
|
||||||
|
"prMerged": "PR #{{number}} is already merged"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createAttemptDialog": {
|
"createAttemptDialog": {
|
||||||
@@ -238,6 +247,7 @@
|
|||||||
"viewProcesses": "View processes",
|
"viewProcesses": "View processes",
|
||||||
"createNewAttempt": "Create new attempt",
|
"createNewAttempt": "Create new attempt",
|
||||||
"createSubtask": "Create subtask",
|
"createSubtask": "Create subtask",
|
||||||
|
"gitActions": "Git actions",
|
||||||
"task": "Task",
|
"task": "Task",
|
||||||
"duplicate": "Duplicate"
|
"duplicate": "Duplicate"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"createNewAttempt": "Create new attempt",
|
"createNewAttempt": "Create new attempt",
|
||||||
"createSubtask": "Create subtask",
|
"createSubtask": "Create subtask",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
|
"gitActions": "Acciones de Git",
|
||||||
"openInIde": "Open attempt in IDE",
|
"openInIde": "Open attempt in IDE",
|
||||||
"task": "Task",
|
"task": "Task",
|
||||||
"viewProcesses": "View processes"
|
"viewProcesses": "View processes"
|
||||||
@@ -19,26 +20,31 @@
|
|||||||
"stopDevServer": "Detener servidor de desarrollo"
|
"stopDevServer": "Detener servidor de desarrollo"
|
||||||
},
|
},
|
||||||
"agent": "Agent",
|
"agent": "Agent",
|
||||||
"clickToCopy": "Click to copy worktree path",
|
"clickToCopy": "Copiar ruta del worktree",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"createSubtask": "Create Subtask",
|
"createSubtask": "Create Subtask",
|
||||||
"diffs": "Diffs",
|
"diffs": "Diffs",
|
||||||
|
"gitActions": "Acciones de Git",
|
||||||
"labels": {
|
"labels": {
|
||||||
"agent": "Agente",
|
"agent": "Agente",
|
||||||
"attempt": "Intento",
|
"attempt": "Intento",
|
||||||
"branch": "Rama",
|
"branch": "Rama",
|
||||||
"diffs": "Diferencias"
|
"diffs": "Diferencias",
|
||||||
|
"summaryAndActions": "Resumen y Acciones"
|
||||||
},
|
},
|
||||||
"newAttempt": "New Attempt",
|
"newAttempt": "New Attempt",
|
||||||
"openInEditor": "Open in {{editor}}",
|
"openInEditor": "Open in {{editor}}",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
"pauseDev": "Pausar Dev",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"startDev": "Start Dev",
|
"startDev": "Start Dev",
|
||||||
"stopAttempt": "Stop Attempt",
|
"stopAttempt": "Stop Attempt",
|
||||||
"stopDev": "Stop Dev",
|
"stopDev": "Stop Dev",
|
||||||
"stopping": "Stopping...",
|
"stopping": "Stopping...",
|
||||||
"viewDevLogs": "View dev server logs",
|
"viewDevLogs": "View dev server logs",
|
||||||
"viewHistory": "View attempt history"
|
"viewHistory": "View attempt history",
|
||||||
|
"tryAgain": "Try Again",
|
||||||
|
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
||||||
},
|
},
|
||||||
"attemptHeaderActions": {
|
"attemptHeaderActions": {
|
||||||
"diffs": "Diffs",
|
"diffs": "Diffs",
|
||||||
@@ -109,6 +115,10 @@
|
|||||||
"number": "PR #{{number}}",
|
"number": "PR #{{number}}",
|
||||||
"open": "Open PR #{{number}}"
|
"open": "Open PR #{{number}}"
|
||||||
},
|
},
|
||||||
|
"actions": {
|
||||||
|
"title": "Acciones de Git",
|
||||||
|
"prMerged": "PR #{{number}} ya está fusionado"
|
||||||
|
},
|
||||||
"states": {
|
"states": {
|
||||||
"createPr": "Crear PR",
|
"createPr": "Crear PR",
|
||||||
"creating": "Creando...",
|
"creating": "Creando...",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"createNewAttempt": "Create new attempt",
|
"createNewAttempt": "Create new attempt",
|
||||||
"createSubtask": "Create subtask",
|
"createSubtask": "Create subtask",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
|
"gitActions": "Gitアクション",
|
||||||
"openInIde": "Open attempt in IDE",
|
"openInIde": "Open attempt in IDE",
|
||||||
"task": "Task",
|
"task": "Task",
|
||||||
"viewProcesses": "View processes"
|
"viewProcesses": "View processes"
|
||||||
@@ -19,26 +20,31 @@
|
|||||||
"stopDevServer": "開発サーバーを停止"
|
"stopDevServer": "開発サーバーを停止"
|
||||||
},
|
},
|
||||||
"agent": "Agent",
|
"agent": "Agent",
|
||||||
"clickToCopy": "Click to copy worktree path",
|
"clickToCopy": "ワークツリーパスをコピー",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"createSubtask": "Create Subtask",
|
"createSubtask": "Create Subtask",
|
||||||
"diffs": "Diffs",
|
"diffs": "Diffs",
|
||||||
|
"gitActions": "Gitアクション",
|
||||||
"labels": {
|
"labels": {
|
||||||
"agent": "エージェント",
|
"agent": "エージェント",
|
||||||
"attempt": "試行",
|
"attempt": "試行",
|
||||||
"branch": "ブランチ",
|
"branch": "ブランチ",
|
||||||
"diffs": "差分"
|
"diffs": "差分",
|
||||||
|
"summaryAndActions": "概要とアクション"
|
||||||
},
|
},
|
||||||
"newAttempt": "New Attempt",
|
"newAttempt": "New Attempt",
|
||||||
"openInEditor": "Open in {{editor}}",
|
"openInEditor": "Open in {{editor}}",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
"pauseDev": "Dev を一時停止",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"startDev": "Start Dev",
|
"startDev": "Start Dev",
|
||||||
"stopAttempt": "Stop Attempt",
|
"stopAttempt": "Stop Attempt",
|
||||||
"stopDev": "Stop Dev",
|
"stopDev": "Stop Dev",
|
||||||
"stopping": "Stopping...",
|
"stopping": "Stopping...",
|
||||||
"viewDevLogs": "View dev server logs",
|
"viewDevLogs": "View dev server logs",
|
||||||
"viewHistory": "View attempt history"
|
"viewHistory": "View attempt history",
|
||||||
|
"tryAgain": "Try Again",
|
||||||
|
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
||||||
},
|
},
|
||||||
"attemptHeaderActions": {
|
"attemptHeaderActions": {
|
||||||
"diffs": "Diffs",
|
"diffs": "Diffs",
|
||||||
@@ -109,6 +115,10 @@
|
|||||||
"number": "PR #{{number}}",
|
"number": "PR #{{number}}",
|
||||||
"open": "Open PR #{{number}}"
|
"open": "Open PR #{{number}}"
|
||||||
},
|
},
|
||||||
|
"actions": {
|
||||||
|
"title": "Gitアクション",
|
||||||
|
"prMerged": "PR #{{number}} は既にマージされています"
|
||||||
|
},
|
||||||
"states": {
|
"states": {
|
||||||
"createPr": "PRを作成",
|
"createPr": "PRを作成",
|
||||||
"creating": "作成中...",
|
"creating": "作成中...",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"createNewAttempt": "Create new attempt",
|
"createNewAttempt": "Create new attempt",
|
||||||
"createSubtask": "Create subtask",
|
"createSubtask": "Create subtask",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
|
"gitActions": "Git 작업",
|
||||||
"openInIde": "Open attempt in IDE",
|
"openInIde": "Open attempt in IDE",
|
||||||
"task": "Task",
|
"task": "Task",
|
||||||
"viewProcesses": "View processes"
|
"viewProcesses": "View processes"
|
||||||
@@ -19,26 +20,31 @@
|
|||||||
"stopDevServer": "개발 서버 중지"
|
"stopDevServer": "개발 서버 중지"
|
||||||
},
|
},
|
||||||
"agent": "Agent",
|
"agent": "Agent",
|
||||||
"clickToCopy": "Click to copy worktree path",
|
"clickToCopy": "작업 트리 경로 복사",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"createSubtask": "Create Subtask",
|
"createSubtask": "Create Subtask",
|
||||||
"diffs": "Diffs",
|
"diffs": "Diffs",
|
||||||
|
"gitActions": "Git 작업",
|
||||||
"labels": {
|
"labels": {
|
||||||
"agent": "에이전트",
|
"agent": "에이전트",
|
||||||
"attempt": "시도",
|
"attempt": "시도",
|
||||||
"branch": "브랜치",
|
"branch": "브랜치",
|
||||||
"diffs": "변경사항"
|
"diffs": "변경사항",
|
||||||
|
"summaryAndActions": "요약 및 작업"
|
||||||
},
|
},
|
||||||
"newAttempt": "New Attempt",
|
"newAttempt": "New Attempt",
|
||||||
"openInEditor": "Open in {{editor}}",
|
"openInEditor": "Open in {{editor}}",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
"pauseDev": "Dev 일시정지",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"startDev": "Start Dev",
|
"startDev": "Start Dev",
|
||||||
"stopAttempt": "Stop Attempt",
|
"stopAttempt": "Stop Attempt",
|
||||||
"stopDev": "Stop Dev",
|
"stopDev": "Stop Dev",
|
||||||
"stopping": "Stopping...",
|
"stopping": "Stopping...",
|
||||||
"viewDevLogs": "View dev server logs",
|
"viewDevLogs": "View dev server logs",
|
||||||
"viewHistory": "View attempt history"
|
"viewHistory": "View attempt history",
|
||||||
|
"tryAgain": "Try Again",
|
||||||
|
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
|
||||||
},
|
},
|
||||||
"attemptHeaderActions": {
|
"attemptHeaderActions": {
|
||||||
"diffs": "Diffs",
|
"diffs": "Diffs",
|
||||||
@@ -109,6 +115,10 @@
|
|||||||
"number": "PR #{{number}}",
|
"number": "PR #{{number}}",
|
||||||
"open": "Open PR #{{number}}"
|
"open": "Open PR #{{number}}"
|
||||||
},
|
},
|
||||||
|
"actions": {
|
||||||
|
"title": "Git 작업",
|
||||||
|
"prMerged": "PR #{{number}}은(는) 이미 병합되었습니다"
|
||||||
|
},
|
||||||
"states": {
|
"states": {
|
||||||
"createPr": "PR 생성",
|
"createPr": "PR 생성",
|
||||||
"creating": "생성 중...",
|
"creating": "생성 중...",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
ProjectEditorSelectionDialog,
|
ProjectEditorSelectionDialog,
|
||||||
RestoreLogsDialog,
|
RestoreLogsDialog,
|
||||||
ViewProcessesDialog,
|
ViewProcessesDialog,
|
||||||
|
GitActionsDialog,
|
||||||
} from './components/dialogs';
|
} from './components/dialogs';
|
||||||
import { CreateAttemptDialog } from './components/dialogs/tasks/CreateAttemptDialog';
|
import { CreateAttemptDialog } from './components/dialogs/tasks/CreateAttemptDialog';
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ NiceModal.register('project-editor-selection', ProjectEditorSelectionDialog);
|
|||||||
NiceModal.register('restore-logs', RestoreLogsDialog);
|
NiceModal.register('restore-logs', RestoreLogsDialog);
|
||||||
NiceModal.register('view-processes', ViewProcessesDialog);
|
NiceModal.register('view-processes', ViewProcessesDialog);
|
||||||
NiceModal.register('create-attempt', CreateAttemptDialog);
|
NiceModal.register('create-attempt', CreateAttemptDialog);
|
||||||
|
NiceModal.register('git-actions', GitActionsDialog);
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useLocation,
|
useLocation,
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ export type CommandRunResult = { exit_status: CommandExitStatus | null, output:
|
|||||||
|
|
||||||
export type NormalizedEntry = { timestamp: string | null, entry_type: NormalizedEntryType, content: string, };
|
export type NormalizedEntry = { timestamp: string | null, entry_type: NormalizedEntryType, content: string, };
|
||||||
|
|
||||||
export type NormalizedEntryType = { "type": "user_message" } | { "type": "user_feedback", denied_tool: string, } | { "type": "assistant_message" } | { "type": "tool_use", tool_name: string, action_type: ActionType, status: ToolStatus, } | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" } | { "type": "loading" };
|
export type NormalizedEntryType = { "type": "user_message" } | { "type": "user_feedback", denied_tool: string, } | { "type": "assistant_message" } | { "type": "tool_use", tool_name: string, action_type: ActionType, status: ToolStatus, } | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" } | { "type": "loading" } | { "type": "next_action", failed: boolean, execution_processes: number, };
|
||||||
|
|
||||||
export type FileChange = { "action": "write", content: string, } | { "action": "delete" } | { "action": "rename", new_path: string, } | { "action": "edit",
|
export type FileChange = { "action": "write", content: string, } | { "action": "delete" } | { "action": "rename", new_path: string, } | { "action": "edit",
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user