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:
Louis Knight-Webb
2025-10-23 17:43:37 +01:00
committed by GitHub
parent f88daa4826
commit 6fc7410b28
18 changed files with 693 additions and 41 deletions

View File

@@ -29,6 +29,7 @@ import {
import RawLogText from '../common/RawLogText';
import UserMessage from './UserMessage';
import PendingApprovalEntry from './PendingApprovalEntry';
import { NextActionCard } from './NextActionCard';
import { cn } from '@/lib/utils';
import { useRetryUi } from '@/contexts/RetryUiContext';
@@ -38,6 +39,7 @@ type Props = {
diffDeletable?: boolean;
executionProcessId?: string;
taskAttempt?: TaskAttempt;
task?: any;
};
type FileEditAction = Extract<ActionType, { action: 'file_edit' }>;
@@ -603,6 +605,7 @@ function DisplayConversationEntry({
expansionKey,
executionProcessId,
taskAttempt,
task,
}: Props) {
const { t } = useTranslation('common');
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 (
<div className="px-4 py-2 text-sm">
<div className={getContentClassName(entryType)}>

View File

@@ -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>
);
}

View File

@@ -61,6 +61,10 @@ export {
ViewProcessesDialog,
type ViewProcessesDialogProps,
} from './tasks/ViewProcessesDialog';
export {
GitActionsDialog,
type GitActionsDialogProps,
} from './tasks/GitActionsDialog';
// Settings dialogs
export {

View 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>
);
}
);

View File

@@ -11,10 +11,11 @@ import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext';
export interface ViewProcessesDialogProps {
attemptId: string;
initialProcessId?: string | null;
}
export const ViewProcessesDialog = NiceModal.create<ViewProcessesDialogProps>(
({ attemptId }) => {
({ attemptId, initialProcessId }) => {
const { t } = useTranslation('tasks');
const modal = useModal();
@@ -43,7 +44,7 @@ export const ViewProcessesDialog = NiceModal.create<ViewProcessesDialogProps>(
<DialogTitle>{t('viewProcessesDialog.title')}</DialogTitle>
</DialogHeader>
<div className="h-[75vh] flex flex-col min-h-0 min-w-0">
<ProcessSelectionProvider>
<ProcessSelectionProvider initialProcessId={initialProcessId}>
<ProcessesTab attemptId={attemptId} />
</ProcessSelectionProvider>
</div>

View File

@@ -16,15 +16,17 @@ import {
useConversationHistory,
} from '@/hooks/useConversationHistory';
import { Loader2 } from 'lucide-react';
import { TaskAttempt } from 'shared/types';
import { TaskAttempt, TaskWithAttemptStatus } from 'shared/types';
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
interface VirtualizedListProps {
attempt: TaskAttempt;
task?: TaskWithAttemptStatus;
}
interface MessageListContext {
attempt: TaskAttempt;
task?: TaskWithAttemptStatus;
}
const INITIAL_TOP_ITEM = { index: 'LAST' as const, align: 'end' as const };
@@ -45,6 +47,7 @@ const ItemContent: VirtuosoMessageListProps<
MessageListContext
>['ItemContent'] = ({ data, context }) => {
const attempt = context?.attempt;
const task = context?.task;
if (data.type === 'STDOUT') {
return <p>{data.content}</p>;
@@ -59,6 +62,7 @@ const ItemContent: VirtuosoMessageListProps<
entry={data.content}
executionProcessId={data.executionProcessId}
taskAttempt={attempt}
task={task}
/>
);
}
@@ -71,7 +75,7 @@ const computeItemKey: VirtuosoMessageListProps<
MessageListContext
>['computeItemKey'] = ({ data }) => `l-${data.patchKey}`;
const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
const VirtualizedList = ({ attempt, task }: VirtualizedListProps) => {
const [channelData, setChannelData] =
useState<DataWithScrollModifier<PatchTypeWithKey> | null>(null);
const [loading, setLoading] = useState(true);
@@ -105,7 +109,10 @@ const VirtualizedList = ({ attempt }: VirtualizedListProps) => {
useConversationHistory({ attempt, onEntriesUpdated });
const messageListRef = useRef<VirtuosoMessageListMethods | null>(null);
const messageListContext = useMemo(() => ({ attempt }), [attempt]);
const messageListContext = useMemo(
() => ({ attempt, task }),
[attempt, task]
);
return (
<ApprovalFormProvider>

View File

@@ -28,7 +28,9 @@ const TaskAttemptPanel = ({
<EntriesProvider key={attempt.id}>
<RetryUiProvider attemptId={attempt.id}>
{children({
logs: <VirtualizedList key={attempt.id} attempt={attempt} />,
logs: (
<VirtualizedList key={attempt.id} attempt={attempt} task={task} />
),
followUp: (
<TaskFollowUpSection
task={task}

View File

@@ -41,6 +41,7 @@ interface GitOperationsProps {
isAttemptRunning: boolean;
setError: (error: string | null) => void;
selectedBranch: string | null;
layout?: 'horizontal' | 'vertical';
}
export type GitOperationsInputs = Omit<GitOperationsProps, 'selectedAttempt'>;
@@ -54,6 +55,7 @@ function GitOperations({
isAttemptRunning,
setError,
selectedBranch,
layout = 'horizontal',
}: GitOperationsProps) {
const { t } = useTranslation('tasks');
@@ -268,9 +270,23 @@ function GitOperations({
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 (
<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 */}
<div className="flex items-center gap-2 min-w-0 shrink-0 overflow-hidden">
{/* Task branch chip */}
@@ -319,7 +335,7 @@ function GitOperations({
size="xs"
onClick={handleChangeTargetBranchDialogOpen}
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')}
>
<Settings className="h-3.5 w-3.5" />
@@ -421,9 +437,9 @@ function GitOperations({
})()}
</div>
{/* Right: Actions (compact, right-aligned) */}
{/* Right: Actions */}
{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
onClick={handleMergeClick}
disabled={

View File

@@ -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 (
<>
<DropdownMenu>
@@ -118,6 +128,12 @@ export function ActionsDropdown({ task, attempt }: ActionsDropdownProps) {
>
{t('actionsMenu.createSubtask')}
</DropdownMenuItem>
<DropdownMenuItem
disabled={!attempt?.id || !task}
onClick={handleGitActions}
>
{t('actionsMenu.gitActions')}
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}

View File

@@ -10,13 +10,15 @@ const ProcessSelectionContext =
interface ProcessSelectionProviderProps {
children: ReactNode;
initialProcessId?: string | null;
}
export function ProcessSelectionProvider({
children,
initialProcessId = null,
}: ProcessSelectionProviderProps) {
const [selectedProcessId, setSelectedProcessId] = useState<string | null>(
null
initialProcessId
);
const value = useMemo(

View File

@@ -50,6 +50,37 @@ interface UseConversationHistoryResult {}
const MIN_INITIAL_ENTRIES = 10;
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 = ({
attempt,
onEntriesUpdated,
@@ -197,22 +228,14 @@ export const useConversationHistory = ({
.flatMap((p) => p.entries);
};
const loadingPatch: PatchTypeWithKey = {
type: 'NORMALIZED_ENTRY',
content: {
entry_type: {
type: 'loading',
},
content: '',
timestamp: null,
},
patchKey: 'loading',
executionProcessId: '',
};
const flattenEntriesForEmit = (
executionProcessState: ExecutionProcessStateStore
): 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
const allEntries = Object.values(executionProcessState)
.sort(
@@ -222,7 +245,7 @@ export const useConversationHistory = ({
).getTime() -
new Date(b.executionProcess.created_at as unknown as string).getTime()
)
.flatMap((p) => {
.flatMap((p, index) => {
const entries: PatchTypeWithKey[] = [];
if (
p.executionProcess.executor_action.typ.type ===
@@ -265,10 +288,31 @@ export const useConversationHistory = ({
);
});
if (hasPendingApprovalEntry) {
hasPendingApproval = true;
}
entries.push(...entriesExcludingUser);
const liveProcessStatus = getLiveExecutionProcess(
p.executionProcess.id
)?.status;
const isProcessRunning =
getLiveExecutionProcess(p.executionProcess.id)?.status ===
ExecutionProcessStatus.running;
liveProcessStatus === 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) {
entries.push(loadingPatch);
@@ -293,6 +337,18 @@ export const useConversationHistory = ({
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 exit_status: CommandExitStatus | null =
executionProcess?.status === 'running'
@@ -344,6 +400,16 @@ export const useConversationHistory = ({
return entries;
});
// Emit the next action bar if no process running
if (!hasRunningProcess && !hasPendingApproval) {
allEntries.push(
nextActionPatch(
lastProcessFailedOrKilled,
Object.keys(executionProcessState).length
)
);
}
return allEntries;
};

View File

@@ -159,14 +159,16 @@
"attempt": "Attempt",
"agent": "Agent",
"branch": "Branch",
"diffs": "Diffs"
"diffs": "Diffs",
"summaryAndActions": "Summary & Actions"
},
"agent": "Agent",
"path": "Path",
"openInEditor": "Open in {{editor}}",
"openInEditor": "See changes in {{editor}}",
"copied": "Copied!",
"clickToCopy": "Click to copy worktree path",
"clickToCopy": "Copy worktree path",
"stopDev": "Stop Dev",
"pauseDev": "Pause Dev",
"startDev": "Start Dev",
"viewDevLogs": "View dev server logs",
"stopping": "Stopping...",
@@ -175,7 +177,10 @@
"viewHistory": "View attempt history",
"createSubtask": "Create Subtask",
"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": {
"labels": {
@@ -213,6 +218,10 @@
"pr": {
"open": "Open PR #{{number}}",
"number": "PR #{{number}}"
},
"actions": {
"title": "Git Actions",
"prMerged": "PR #{{number}} is already merged"
}
},
"createAttemptDialog": {
@@ -238,6 +247,7 @@
"viewProcesses": "View processes",
"createNewAttempt": "Create new attempt",
"createSubtask": "Create subtask",
"gitActions": "Git actions",
"task": "Task",
"duplicate": "Duplicate"
},

View File

@@ -7,6 +7,7 @@
"createNewAttempt": "Create new attempt",
"createSubtask": "Create subtask",
"duplicate": "Duplicate",
"gitActions": "Acciones de Git",
"openInIde": "Open attempt in IDE",
"task": "Task",
"viewProcesses": "View processes"
@@ -19,26 +20,31 @@
"stopDevServer": "Detener servidor de desarrollo"
},
"agent": "Agent",
"clickToCopy": "Click to copy worktree path",
"clickToCopy": "Copiar ruta del worktree",
"copied": "Copied!",
"createSubtask": "Create Subtask",
"diffs": "Diffs",
"gitActions": "Acciones de Git",
"labels": {
"agent": "Agente",
"attempt": "Intento",
"branch": "Rama",
"diffs": "Diferencias"
"diffs": "Diferencias",
"summaryAndActions": "Resumen y Acciones"
},
"newAttempt": "New Attempt",
"openInEditor": "Open in {{editor}}",
"path": "Path",
"pauseDev": "Pausar Dev",
"preview": "Preview",
"startDev": "Start Dev",
"stopAttempt": "Stop Attempt",
"stopDev": "Stop Dev",
"stopping": "Stopping...",
"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": {
"diffs": "Diffs",
@@ -109,6 +115,10 @@
"number": "PR #{{number}}",
"open": "Open PR #{{number}}"
},
"actions": {
"title": "Acciones de Git",
"prMerged": "PR #{{number}} ya está fusionado"
},
"states": {
"createPr": "Crear PR",
"creating": "Creando...",

View File

@@ -7,6 +7,7 @@
"createNewAttempt": "Create new attempt",
"createSubtask": "Create subtask",
"duplicate": "Duplicate",
"gitActions": "Gitアクション",
"openInIde": "Open attempt in IDE",
"task": "Task",
"viewProcesses": "View processes"
@@ -19,26 +20,31 @@
"stopDevServer": "開発サーバーを停止"
},
"agent": "Agent",
"clickToCopy": "Click to copy worktree path",
"clickToCopy": "ワークツリーパスをコピー",
"copied": "Copied!",
"createSubtask": "Create Subtask",
"diffs": "Diffs",
"gitActions": "Gitアクション",
"labels": {
"agent": "エージェント",
"attempt": "試行",
"branch": "ブランチ",
"diffs": "差分"
"diffs": "差分",
"summaryAndActions": "概要とアクション"
},
"newAttempt": "New Attempt",
"openInEditor": "Open in {{editor}}",
"path": "Path",
"pauseDev": "Dev を一時停止",
"preview": "Preview",
"startDev": "Start Dev",
"stopAttempt": "Stop Attempt",
"stopDev": "Stop Dev",
"stopping": "Stopping...",
"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": {
"diffs": "Diffs",
@@ -109,6 +115,10 @@
"number": "PR #{{number}}",
"open": "Open PR #{{number}}"
},
"actions": {
"title": "Gitアクション",
"prMerged": "PR #{{number}} は既にマージされています"
},
"states": {
"createPr": "PRを作成",
"creating": "作成中...",

View File

@@ -7,6 +7,7 @@
"createNewAttempt": "Create new attempt",
"createSubtask": "Create subtask",
"duplicate": "Duplicate",
"gitActions": "Git 작업",
"openInIde": "Open attempt in IDE",
"task": "Task",
"viewProcesses": "View processes"
@@ -19,26 +20,31 @@
"stopDevServer": "개발 서버 중지"
},
"agent": "Agent",
"clickToCopy": "Click to copy worktree path",
"clickToCopy": "작업 트리 경로 복사",
"copied": "Copied!",
"createSubtask": "Create Subtask",
"diffs": "Diffs",
"gitActions": "Git 작업",
"labels": {
"agent": "에이전트",
"attempt": "시도",
"branch": "브랜치",
"diffs": "변경사항"
"diffs": "변경사항",
"summaryAndActions": "요약 및 작업"
},
"newAttempt": "New Attempt",
"openInEditor": "Open in {{editor}}",
"path": "Path",
"pauseDev": "Dev 일시정지",
"preview": "Preview",
"startDev": "Start Dev",
"stopAttempt": "Stop Attempt",
"stopDev": "Stop Dev",
"stopping": "Stopping...",
"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": {
"diffs": "Diffs",
@@ -109,6 +115,10 @@
"number": "PR #{{number}}",
"open": "Open PR #{{number}}"
},
"actions": {
"title": "Git 작업",
"prMerged": "PR #{{number}}은(는) 이미 병합되었습니다"
},
"states": {
"createPr": "PR 생성",
"creating": "생성 중...",

View File

@@ -35,6 +35,7 @@ import {
ProjectEditorSelectionDialog,
RestoreLogsDialog,
ViewProcessesDialog,
GitActionsDialog,
} from './components/dialogs';
import { CreateAttemptDialog } from './components/dialogs/tasks/CreateAttemptDialog';
@@ -61,6 +62,7 @@ NiceModal.register('project-editor-selection', ProjectEditorSelectionDialog);
NiceModal.register('restore-logs', RestoreLogsDialog);
NiceModal.register('view-processes', ViewProcessesDialog);
NiceModal.register('create-attempt', CreateAttemptDialog);
NiceModal.register('git-actions', GitActionsDialog);
import {
useLocation,