Smooth cursor onboarding (#1143)

* WIP cursor onboarding

Claude rebase fixes

fmt

WIP

WIP

WIP Add login button

Remove auto login check

Login needed detection

Setup script for login and install

Fix install+login case

Update task to inreview upon failure, cleanup

Feedback in next action box

Use errortype isntead of next action to communicate setup needed

* Add auto retry after install

* Fix setup needed detection

* Fix next_run_reason

* Lint and format

* i18n

* Rename to setup helper

* rename ErrorType to NormalizedEntryError

* Better setupHelpText i18n (vibe-kanban a8dd795e)

frontend/src/i18n/locales/en/tasks.json setupHelpText currently isn't very informative. We should change it to 
"{agent_name} isn't setup correctly. Click 'Run Setup' to install it and login." or similar.

* Cursor login should be exempt from the 2 execution process limit (vibe-kanban deca56cd)

frontend/src/components/NormalizedConversation/NextActionCard.tsx

Display run setup even if there are more than 2 execution processes

* Move agent setup to server
This commit is contained in:
Alex Netsch
2025-11-04 11:22:21 +00:00
committed by GitHub
parent 103f55621c
commit 6acc53e581
23 changed files with 486 additions and 166 deletions

View File

@@ -793,6 +793,7 @@ function DisplayConversationEntry({
failed={entry.entry_type.failed}
execution_processes={entry.entry_type.execution_processes}
task={task}
needsSetup={entry.entry_type.needs_setup}
/>
</div>
);

View File

@@ -8,6 +8,7 @@ import {
Copy,
Check,
GitBranch,
Settings,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import NiceModal from '@ebay/nice-modal-react';
@@ -21,6 +22,7 @@ import { getIdeName } from '@/components/ide/IdeIcon';
import { useProject } from '@/contexts/project-context';
import { useQuery } from '@tanstack/react-query';
import { attemptsApi } from '@/lib/api';
import { BaseAgentCapability, type BaseCodingAgent } from 'shared/types';
import {
Tooltip,
TooltipContent,
@@ -34,6 +36,7 @@ type NextActionCardProps = {
failed: boolean;
execution_processes: number;
task?: any;
needsSetup?: boolean;
};
export function NextActionCard({
@@ -42,6 +45,7 @@ export function NextActionCard({
failed,
execution_processes,
task,
needsSetup,
}: NextActionCardProps) {
const { t } = useTranslation('tasks');
const { config } = useUserSystem();
@@ -54,6 +58,7 @@ export function NextActionCard({
queryFn: () => attemptsApi.get(attemptId!),
enabled: !!attemptId && failed,
});
const { capabilities } = useUserSystem();
const openInEditor = useOpenInEditor(attemptId);
const { fileCount, added, deleted, error } = useDiffSummary(
@@ -116,10 +121,36 @@ export function NextActionCard({
});
}, [attemptId, task, project?.id]);
const handleRunSetup = useCallback(async () => {
if (!attemptId || !attempt) return;
try {
await attemptsApi.runAgentSetup(attemptId, {
executor_profile_id: {
executor: attempt.executor as BaseCodingAgent,
variant: null,
},
});
} catch (error) {
console.error('Failed to run setup:', error);
}
}, [attemptId, attempt]);
const canAutoSetup = !!(
attempt?.executor &&
capabilities?.[attempt.executor]?.includes(BaseAgentCapability.SETUP_HELPER)
);
const setupHelpText = canAutoSetup
? t('attempt.setupHelpText', { agent: attempt?.executor })
: null;
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) {
if (
(!failed || (execution_processes > 2 && !needsSetup)) &&
fileCount === 0
) {
return <div className="h-24"></div>;
}
@@ -133,8 +164,19 @@ export function NextActionCard({
{t('attempt.labels.summaryAndActions')}
</span>
</div>
{/* Display setup help text when setup is needed */}
{needsSetup && setupHelpText && (
<div
className={`border-x border-t ${failed ? 'border-destructive' : 'border-foreground'} px-3 py-2 flex items-start gap-2`}
>
<Settings className="h-4 w-4 mt-0.5 flex-shrink-0" />
<span className="text-sm">{setupHelpText}</span>
</div>
)}
<div
className={`border px-3 py-2 flex items-center gap-3 min-w-0 ${failed ? 'border-destructive' : 'border-foreground'}`}
className={`border px-3 py-2 flex items-center gap-3 min-w-0 ${failed ? 'border-destructive' : 'border-foreground'} ${needsSetup && setupHelpText ? 'border-t-0' : ''}`}
>
{/* Left: Diff summary */}
{!error && (
@@ -155,19 +197,33 @@ export function NextActionCard({
<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>
)}
{/* Run Setup or Try Again button */}
{failed &&
(needsSetup ? (
<Button
variant="default"
size="sm"
onClick={handleRunSetup}
disabled={!attempt}
className="text-sm"
aria-label={t('attempt.runSetup')}
>
{t('attempt.runSetup')}
</Button>
) : (
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 && (

View File

@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
import { Pencil } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useProcessRetry } from '@/hooks/useProcessRetry';
import { TaskAttempt, type BaseAgentCapability } from 'shared/types';
import { TaskAttempt, BaseAgentCapability } from 'shared/types';
import { useUserSystem } from '@/components/config-provider';
import { useDraftStream } from '@/hooks/follow-up/useDraftStream';
import { RetryEditorInline } from './RetryEditorInline';
@@ -28,7 +28,7 @@ const UserMessage = ({
const canFork = !!(
taskAttempt?.executor &&
capabilities?.[taskAttempt.executor]?.includes(
'SESSION_FORK' as BaseAgentCapability
BaseAgentCapability.SESSION_FORK
)
);

View File

@@ -65,14 +65,23 @@ const loadingPatch: PatchTypeWithKey = {
const nextActionPatch: (
failed: boolean,
execution_processes: number
) => PatchTypeWithKey = (failed, execution_processes) => ({
execution_processes: number,
needs_setup: boolean,
setup_help_text?: string
) => PatchTypeWithKey = (
failed,
execution_processes,
needs_setup,
setup_help_text
) => ({
type: 'NORMALIZED_ENTRY',
content: {
entry_type: {
type: 'next_action',
failed: failed,
execution_processes: execution_processes,
needs_setup: needs_setup,
setup_help_text: setup_help_text ?? null,
},
content: '',
timestamp: null,
@@ -240,6 +249,8 @@ export const useConversationHistory = ({
let hasPendingApproval = false;
let hasRunningProcess = false;
let lastProcessFailedOrKilled = false;
let needsSetup = false;
let setupHelpText: string | undefined;
// Create user messages + tool calls for setup/cleanup scripts
const allEntries = Object.values(executionProcessState)
@@ -317,6 +328,23 @@ export const useConversationHistory = ({
index === Object.keys(executionProcessState).length - 1
) {
lastProcessFailedOrKilled = true;
// Check if this failed process has a SetupRequired entry
const hasSetupRequired = entriesExcludingUser.some((entry) => {
if (entry.type !== 'NORMALIZED_ENTRY') return false;
if (
entry.content.entry_type.type === 'error_message' &&
entry.content.entry_type.error_type.type === 'setup_required'
) {
setupHelpText = entry.content.content;
return true;
}
return false;
});
if (hasSetupRequired) {
needsSetup = true;
}
}
if (isProcessRunning && !hasPendingApprovalEntry) {
@@ -410,7 +438,9 @@ export const useConversationHistory = ({
allEntries.push(
nextActionPatch(
lastProcessFailedOrKilled,
Object.keys(executionProcessState).length
Object.keys(executionProcessState).length,
needsSetup,
setupHelpText
)
);
}

View File

@@ -181,6 +181,8 @@
"diffs": "Diffs",
"gitActions": "Git Actions",
"tryAgain": "Try Again",
"runSetup": "Run Setup",
"setupHelpText": "{{agent}} isn't setup correctly. Click 'Run Setup' to install it and login.",
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
},
"git": {

View File

@@ -45,6 +45,8 @@
"viewDevLogs": "View dev server logs",
"viewHistory": "View attempt history",
"tryAgain": "Try Again",
"runSetup": "Ejecutar Configuración",
"setupHelpText": "{{agent}} no está configurado correctamente. Haz clic en 'Ejecutar Configuración' para instalarlo e iniciar sesión.",
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
},
"attemptHeaderActions": {

View File

@@ -45,6 +45,8 @@
"viewDevLogs": "View dev server logs",
"viewHistory": "View attempt history",
"tryAgain": "Try Again",
"runSetup": "セットアップを実行",
"setupHelpText": "{{agent}}が正しく設定されていません。「セットアップを実行」をクリックしてインストールとログインを行ってください。",
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
},
"attemptHeaderActions": {

View File

@@ -45,6 +45,8 @@
"viewDevLogs": "View dev server logs",
"viewHistory": "View attempt history",
"tryAgain": "Try Again",
"runSetup": "설정 실행",
"setupHelpText": "{{agent}}이(가) 올바르게 설정되지 않았습니다. '설정 실행'을 클릭하여 설치하고 로그인하세요.",
"devScriptMissingTooltip": "To start the dev server, add a dev script to this project"
},
"attemptHeaderActions": {

View File

@@ -47,6 +47,8 @@ import {
RebaseTaskAttemptRequest,
ChangeTargetBranchRequest,
ChangeTargetBranchResponse,
RunAgentSetupRequest,
RunAgentSetupResponse,
} from 'shared/types';
// Re-export types for convenience
@@ -386,6 +388,20 @@ export const attemptsApi = {
return handleApiResponse<void>(response);
},
runAgentSetup: async (
attemptId: string,
data: RunAgentSetupRequest
): Promise<RunAgentSetupResponse> => {
const response = await makeRequest(
`/api/task-attempts/${attemptId}/run-agent-setup`,
{
method: 'POST',
body: JSON.stringify(data),
}
);
return handleApiResponse<RunAgentSetupResponse>(response);
},
getDraft: async (
attemptId: string,
type: 'follow_up' | 'retry'