feat(onboarding): align first-agent setup flow

This commit is contained in:
gsxdsm
2026-06-22 04:39:35 -07:00
parent 0c0581471e
commit f0fbc59991
38 changed files with 450 additions and 56 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Update first-run onboarding to include an optional first-agent step and clearer temporary-agent task guidance.

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -489,6 +489,7 @@ export function AppModals({
onOpenGitHubImport={handleOpenGitHubImport}
firstCreatedTask={firstCreatedTask}
onViewTask={handleOnboardingViewTask}
agentOnboardingEnabled={agentOnboardingEnabled}
/>
)}

View File

@@ -1,6 +1,10 @@
/* ===== Model Onboarding Modal ===== */
.model-onboarding-modal {
/*
FNXC:Onboarding 2026-06-22-04:16:
First-time provider/GitHub onboarding should visually match new-project onboarding, including spacing, button treatment, contrast, and persistent Discord help access.
*/
/* Resizable via the native CSS grip. The user's chosen size is persisted
by useModalResizePersist. Min/max keep the dimensions sane so a value
saved on a 4K display still renders on a laptop. */
@@ -15,7 +19,7 @@
}
.model-onboarding-modal:not([style*="width"]) {
width: min(1100px, calc(100vw - 40px));
width: min(960px, calc(100vw - 40px));
}
.model-onboarding-modal:not([style*="height"]) {
height: min(85vh, calc(100dvh - 40px));
@@ -25,8 +29,8 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 12px;
border-bottom: 1px solid var(--border);
padding: var(--space-xl) var(--space-xl) 0;
gap: var(--space-md);
}
.model-onboarding-header .modal-close {
@@ -46,12 +50,14 @@
}
.model-onboarding-title {
font-size: 18px;
font-weight: 600;
font-size: 24px;
font-weight: 800;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
color: var(--text);
line-height: 1.1;
}
.model-onboarding-title svg {
@@ -77,7 +83,7 @@
align-items: center;
justify-content: center;
gap: 0;
padding: 16px 24px;
padding: var(--space-md) var(--space-xl);
border-bottom: 1px solid var(--border);
}
@@ -157,10 +163,10 @@
}
.model-onboarding-step-connector {
width: 40px;
width: 30px;
height: 2px;
background: var(--border);
margin: 0 12px;
margin: 0 var(--space-sm);
transition: background-color var(--transition-fast);
}
@@ -170,19 +176,19 @@
/* Content area */
.model-onboarding-content {
padding: 20px 24px;
padding: var(--space-md) var(--space-xl);
overflow-y: auto;
flex: 1;
}
.model-onboarding-description {
color: var(--text-muted);
color: var(--text);
margin-bottom: 16px;
line-height: 1.5;
}
.onboarding-helper-text {
color: var(--text-dim);
color: var(--text-muted);
font-size: 12px;
margin: 4px 0 0 0;
line-height: 1.4;
@@ -873,6 +879,19 @@
gap: calc(var(--space-xl) - var(--space-xs));
}
.model-onboarding-agent-step .setup-wizard-agent-intro {
color: var(--text);
}
.model-onboarding-agent-step .setup-wizard-agent-preset-description,
.model-onboarding-agent-step .setup-wizard-agent-preview-title-row p {
color: var(--text-muted);
}
.model-onboarding-agent-step .setup-wizard-agent-preview-list dd {
color: var(--text);
}
/* === Onboarding First Task Inline Form === */
.onboarding-first-task-form {
display: flex;
@@ -1134,22 +1153,25 @@
.model-onboarding-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
justify-content: flex-end;
gap: var(--space-sm);
padding: var(--space-md) var(--space-xl) var(--space-lg);
border-top: 1px solid var(--border);
background: var(--surface);
}
.model-onboarding-footer .btn-primary {
display: flex;
align-items: center;
gap: 6px;
gap: var(--space-sm);
padding: 10px 24px;
font-size: 14px;
font-weight: 600;
}
/* When the footer holds only the "Get Started" button (final "complete"
step), push it to the right edge instead of hugging the left under
`justify-content: space-between`. */
.model-onboarding-footer > .btn-primary:only-child {
margin-inline-start: auto;
.model-onboarding-help-link {
margin-right: auto;
text-decoration: none;
}
.onboarding-skip-step-link {
@@ -1384,6 +1406,22 @@
min-width: 0;
}
.model-onboarding-help-link {
margin-right: 0;
width: 100%;
justify-content: center;
}
.model-onboarding-agent-step .setup-wizard-agent-layout {
grid-template-columns: 1fr;
}
.model-onboarding-agent-step .setup-wizard-agent-preset-list {
max-height: none;
overflow-y: visible;
padding-right: 0;
}
.onboarding-step-wrapper {
flex-direction: row;
}

View File

@@ -1,6 +1,7 @@
import "./ModelOnboardingModal.css";
import { useState, useEffect, useCallback, useRef } from "react";
import { X, Loader2, CheckCircle, Key, Zap, GitPullRequest, Rocket, Plus } from "lucide-react";
import "./SetupWizardModal.css";
import { lazy, Suspense, useState, useEffect, useCallback, useRef, useMemo, type KeyboardEvent } from "react";
import { X, Loader2, CheckCircle, Key, Zap, GitPullRequest, Rocket, Plus, Sparkles, UserRound } from "lucide-react";
import { getErrorMessage, type Task } from "@fusion/core";
import type { AuthProvider, ManualOAuthCodeInfo, ModelInfo, CustomProvider, CustomProviderConfig, OAuthDeviceCodeInfo } from "../api";
import {
@@ -15,8 +16,10 @@ import {
fetchModels,
updateGlobalSettings,
createTask,
createAgent,
fetchCustomProviders,
createCustomProvider,
type AgentOnboardingSummary,
} from "../api";
import type { ToastType } from "../hooks/useToast";
import { useModalResizePersist } from "../hooks/useModalResizePersist";
@@ -36,6 +39,19 @@ import { filterVisibleOnboardingAndSettingsProviders } from "./providerVisibilit
import { useShellConnection } from "../hooks/useShellConnection";
import { useConfirm } from "../hooks/useConfirm";
import { useTranslation } from "react-i18next";
import { AgentAvatar } from "./AgentAvatar";
import { ErrorBoundary } from "./ErrorBoundary";
import { AGENT_PRESETS, getPresetById } from "./agent-presets";
import {
buildAgentCreatePayload,
mapOnboardingSummaryToAgentDraft,
mapPresetToAgentDraft,
type AgentDraftValues,
} from "./agent-presets/agentCreatePayload";
const ExperimentalAgentOnboardingModal = lazy(() =>
import("./ExperimentalAgentOnboardingModal").then((m) => ({ default: m.ExperimentalAgentOnboardingModal })),
);
const mapLegacyCustomProviderToConfig = (
provider: CustomProvider | CustomProviderConfig,
@@ -527,6 +543,8 @@ export interface ModelOnboardingModalProps {
firstCreatedTask?: Task | null;
/** Optional callback when user wants to open the created task detail */
onViewTask?: (task: Task) => void;
/** Enables the AI interview entry point while template creation and skip remain available. */
agentOnboardingEnabled?: boolean;
}
/** Outcome states for OAuth login attempts */
@@ -564,9 +582,19 @@ export function ModelOnboardingModal({
onOpenGitHubImport,
firstCreatedTask,
onViewTask,
agentOnboardingEnabled = false,
}: ModelOnboardingModalProps) {
const { t } = useTranslation("app");
const { confirm } = useConfirm();
/*
FNXC:Onboarding 2026-06-22-04:16:
First-time provider/GitHub setup must pass through the same optional first-agent flow before first-task creation.
Users can create or skip a persistent coordinating agent because Fusion automatically spawns temporary agents to plan, execute, review, and merge tasks.
*/
const ceoPreset = useMemo(
() => getPresetById("ceo") ?? AGENT_PRESETS[0]!,
[],
);
// Initialize from persisted state if available (allows resume from last step)
const persistedState = getOnboardingState();
const persistedStep = persistedState?.currentStep;
@@ -589,6 +617,11 @@ export function ModelOnboardingModal({
const [isCreatingFirstTask, setIsCreatingFirstTask] = useState(false);
const [taskCreationError, setTaskCreationError] = useState<string | null>(null);
const [inlineCreatedTask, setInlineCreatedTask] = useState<Task | null>(null);
const [selectedAgentPresetId, setSelectedAgentPresetId] = useState(ceoPreset.id);
const [agentDraft, setAgentDraft] = useState<AgentDraftValues>(() => mapPresetToAgentDraft(ceoPreset));
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
const [agentCreationError, setAgentCreationError] = useState<string | null>(null);
const [isAgentInterviewOpen, setIsAgentInterviewOpen] = useState(false);
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
const [ghCliStatus, setGhCliStatus] = useState<GhCliStatus | undefined>(undefined);
const [authLoading, setAuthLoading] = useState(true);
@@ -617,6 +650,7 @@ export function ModelOnboardingModal({
const onboardingContentRef = useRef<HTMLDivElement | null>(null);
const modalRef = useRef<HTMLDivElement | null>(null);
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const agentErrorRef = useRef<HTMLDivElement | null>(null);
const [loginOutcomes, setLoginOutcomes] = useState<Record<string, LoginOutcome>>({});
const lastAutoCopiedDeviceCodesRef = useRef<Record<string, string>>({});
const [isGithubSkipped, setIsGithubSkipped] = useState<boolean>(() => {
@@ -670,6 +704,7 @@ export function ModelOnboardingModal({
{ key: "ai-setup" as const, label: t("setup.stepAiSetup", "AI Setup") },
{ key: "github" as const, label: t("setup.stepGithub", "GitHub") },
{ key: "project-setup" as const, label: t("setup.stepProject", "Project") },
{ key: "agent" as const, label: t("setup.stepAgent", "Agent") },
{ key: "first-task" as const, label: t("setup.stepFirstTask", "First Task") },
];
@@ -712,6 +747,12 @@ export function ModelOnboardingModal({
previousCreatedTaskRef.current = firstCreatedTask;
}, [firstCreatedTask]);
useEffect(() => {
if (agentCreationError) {
agentErrorRef.current?.focus();
}
}, [agentCreationError]);
// Auto-mark unconnected providers as skipped when leaving ai-setup step
// Only skip if NO providers are connected (if at least one is connected, others remain "Not connected")
const prevStepRef = useRef<OnboardingStep>(initialStep);
@@ -1079,6 +1120,61 @@ export function ModelOnboardingModal({
handleSkip();
}, [handleSkip]);
const handleAgentPresetSelect = useCallback((presetId: string) => {
const preset = getPresetById(presetId);
if (!preset) return;
setSelectedAgentPresetId(preset.id);
setAgentDraft(mapPresetToAgentDraft(preset));
setAgentCreationError(null);
}, []);
const handleAgentPresetKeyDown = useCallback((event: KeyboardEvent<HTMLButtonElement>, presetId: string) => {
const currentIndex = AGENT_PRESETS.findIndex((preset) => preset.id === presetId);
if (currentIndex < 0) return;
const lastIndex = AGENT_PRESETS.length - 1;
let nextIndex: number | null = null;
if (event.key === "ArrowDown" || event.key === "ArrowRight") {
nextIndex = currentIndex === lastIndex ? 0 : currentIndex + 1;
} else if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
nextIndex = currentIndex === 0 ? lastIndex : currentIndex - 1;
} else if (event.key === "Home") {
nextIndex = 0;
} else if (event.key === "End") {
nextIndex = lastIndex;
}
if (nextIndex === null) return;
event.preventDefault();
const nextPreset = AGENT_PRESETS[nextIndex];
handleAgentPresetSelect(nextPreset.id);
requestAnimationFrame(() => {
document.querySelector<HTMLButtonElement>(`[data-model-onboarding-agent-preset-id="${nextPreset.id}"]`)?.focus();
});
}, [handleAgentPresetSelect]);
const handleApplyAgentDraft = useCallback((draft: AgentOnboardingSummary) => {
setSelectedAgentPresetId("");
setAgentDraft(mapOnboardingSummaryToAgentDraft(draft));
setAgentCreationError(null);
}, []);
const handleCreateFirstAgent = useCallback(async () => {
const targetProjectId = projectId?.trim();
if (!targetProjectId || !agentDraft.name.trim()) return;
setIsCreatingAgent(true);
setAgentCreationError(null);
try {
await createAgent(buildAgentCreatePayload(agentDraft), targetProjectId);
handleNext();
} catch (err) {
setAgentCreationError(err instanceof Error ? err.message : t("setup.firstAgentCreateError", "Failed to create agent"));
} finally {
setIsCreatingAgent(false);
}
}, [agentDraft, handleNext, projectId, t]);
// OAuth login handler
const handleLogin = useCallback(
async (providerId: string) => {
@@ -1709,6 +1805,8 @@ export function ModelOnboardingModal({
const connectedAiProviders = aiProviders.filter((provider) => provider.authenticated);
const hasAiProvider = connectedAiProviders.length > 0;
const hasProjectSelected = Boolean(projectId);
const selectedAgentPreset = selectedAgentPresetId ? getPresetById(selectedAgentPresetId) : undefined;
const isAgentActionDisabled = isCreatingAgent || !hasProjectSelected;
// True when on GitHub step but skipped AI setup (no AI provider connected)
const aiSetupSkipped = step === "github" && !hasAiProvider;
@@ -2101,6 +2199,11 @@ export function ModelOnboardingModal({
<Rocket size={24} /> {t("setup.titleSetUpProject", "Set Up Your Project")}
</>
)}
{step === "agent" && (
<>
<UserRound size={24} /> {t("setup.titleCreateFirstAgent", "Create Your First Agent")} <span className="onboarding-optional-badge">{t("setup.optionalBadge", "Optional")}</span>
</>
)}
{step === "first-task" && (
<>
<Rocket size={24} /> {t("setup.titleCreateFirstTask", "Create Your First Task")}
@@ -2124,7 +2227,7 @@ export function ModelOnboardingModal({
)}
</div>
{/* Step indicator - 4 progress steps + complete */}
{/* Step indicator - progress steps + complete */}
<div className="model-onboarding-steps">
{steps.map((s, index) => {
// A step is done/skipped only once we have progressed beyond it.
@@ -2690,6 +2793,109 @@ export function ModelOnboardingModal({
</div>
)}
{step === "agent" && (
<div className="setup-wizard-agent-step model-onboarding-agent-step">
<p className="setup-wizard-agent-intro">
{t("setup.firstAgentIntro", "You do not need to create or assign an agent for Fusion to build tasks. Fusion automatically spawns temporary agents to plan, execute, review, and merge task work. A persistent agent is optional and can coordinate work, help create tasks, and keep direction across sessions.")}
</p>
{!hasProjectSelected && (
<div className="onboarding-project-prerequisite" data-testid="onboarding-agent-project-prerequisite">
<p className="onboarding-helper-text">
{t("setup.projectMustBeSelectedForAgent", "Set up a project before creating your first agent. You can also skip this and create tasks without a persistent agent.")}
</p>
<button
type="button"
className="btn btn-primary"
onClick={onOpenSetupWizard}
data-testid="onboarding-agent-open-setup-wizard"
>
{t("setup.setUpProject", "Set Up Project")}
</button>
</div>
)}
<div className="setup-wizard-agent-layout">
<section className="setup-wizard-agent-presets" aria-labelledby="model-onboarding-first-agent-presets-heading">
<div className="setup-wizard-agent-section-heading" id="model-onboarding-first-agent-presets-heading">
{t("setup.firstAgentTemplates", "Templates")}
</div>
<div className="setup-wizard-agent-preset-list" role="radiogroup" aria-label={t("setup.firstAgentTemplates", "Templates")}>
{AGENT_PRESETS.map((preset) => {
const selected = selectedAgentPresetId === preset.id;
return (
<button
key={preset.id}
type="button"
className={`setup-wizard-agent-preset${selected ? " selected" : ""}`}
role="radio"
aria-checked={selected}
aria-label={selected ? t("setup.selectedAgentTemplate", "{{name}} selected", { name: preset.name }) : preset.name}
tabIndex={selected ? 0 : -1}
data-model-onboarding-agent-preset-id={preset.id}
disabled={isAgentActionDisabled}
onClick={() => handleAgentPresetSelect(preset.id)}
onKeyDown={(event) => handleAgentPresetKeyDown(event, preset.id)}
>
<AgentAvatar agent={{ id: preset.id, icon: preset.icon, name: preset.name }} size={28} />
<span className="setup-wizard-agent-preset-copy">
<span className="setup-wizard-agent-preset-name">
{preset.name}
{preset.id === "ceo" && <span className="wizard-option-recommended">{t("setup.recommended", "Recommended")}</span>}
</span>
<span className="setup-wizard-agent-preset-description">{preset.description}</span>
</span>
</button>
);
})}
</div>
</section>
<section className="setup-wizard-agent-preview" aria-labelledby="model-onboarding-first-agent-preview-heading">
<div className="setup-wizard-agent-section-heading" id="model-onboarding-first-agent-preview-heading">
{t("setup.firstAgentPreview", "Preview")}
</div>
<div className="setup-wizard-agent-preview-card">
<div className="setup-wizard-agent-preview-title-row">
<AgentAvatar agent={{ id: selectedAgentPresetId || "draft", icon: agentDraft.icon, name: agentDraft.name }} size={36} />
<div>
<h3>{agentDraft.name || t("setup.firstAgentDraftName", "Draft agent")}</h3>
<p>{agentDraft.title || selectedAgentPreset?.title || t("setup.firstAgentCustomDraft", "Custom agent draft")}</p>
</div>
</div>
<dl className="setup-wizard-agent-preview-list">
<div>
<dt>{t("agents.fieldRole", "Role")}</dt>
<dd>{agentDraft.role}</dd>
</div>
<div>
<dt>{t("agents.fieldInstructionsText", "Inline Instructions")}</dt>
<dd>{agentDraft.instructionsText || t("setup.firstAgentNoInstructions", "No inline instructions yet")}</dd>
</div>
</dl>
{agentOnboardingEnabled && (
<button
type="button"
className="btn setup-wizard-agent-ai-btn"
onClick={() => setIsAgentInterviewOpen(true)}
disabled={isAgentActionDisabled}
>
<Sparkles size={16} />
<span>{t("agents.aiInterview", "AI Interview")}</span>
</button>
)}
</div>
</section>
</div>
{agentCreationError && (
<div className="wizard-error" role="alert" tabIndex={-1} ref={agentErrorRef}>
{agentCreationError}
</div>
)}
</div>
)}
{step === "first-task" && (
<div className="model-onboarding-first-task">
<p className="model-onboarding-description">
@@ -2855,6 +3061,14 @@ export function ModelOnboardingModal({
{/* Footer */}
<div className="model-onboarding-footer">
<a
className="btn model-onboarding-help-link"
href="https://discord.gg/ksrfuy7WYR"
target="_blank"
rel="noopener noreferrer"
>
{t("setup.needHelp", "Need help?")}
</a>
{step === "ai-setup" && (
<>
<button
@@ -2898,6 +3112,36 @@ export function ModelOnboardingModal({
</>
)}
{step === "agent" && (
<>
<button className="btn btn-sm" onClick={handleBack}>
{t("setup.back", "← Back")}
</button>
<button
className="btn"
onClick={handleSkip}
disabled={isCreatingAgent}
>
{t("setup.skipFirstAgent", "Skip for now")}
</button>
<button
className="btn btn-primary"
onClick={() => void handleCreateFirstAgent()}
disabled={isAgentActionDisabled || !agentDraft.name.trim()}
aria-busy={isCreatingAgent}
>
{isCreatingAgent ? (
<>
<Loader2 size={16} className="animate-spin" />
<span>{t("setup.creatingFirstAgent", "Creating agent...")}</span>
</>
) : (
<span>{t("setup.createFirstAgent", "Create Agent")}</span>
)}
</button>
</>
)}
{step === "first-task" && !showTaskCreated && (
<>
<button className="btn btn-sm" onClick={handleBack}>
@@ -2928,6 +3172,30 @@ export function ModelOnboardingModal({
)}
</div>
</div>
{agentOnboardingEnabled && isAgentInterviewOpen && (
<ErrorBoundary
level="modal"
fallback={(
<div className="wizard-error setup-wizard-agent-interview-error" role="alert">
<span>{t("setup.firstAgentInterviewLoadError", "AI interview could not load. You can still create an agent from a template or skip this step.")}</span>
<button type="button" className="btn" onClick={() => setIsAgentInterviewOpen(false)}>
{t("setup.firstAgentContinueWithTemplates", "Continue with templates")}
</button>
</div>
)}
>
<Suspense fallback={null}>
<ExperimentalAgentOnboardingModal
isOpen={isAgentInterviewOpen}
onClose={() => setIsAgentInterviewOpen(false)}
onUseDraft={handleApplyAgentDraft}
projectId={projectId}
existingAgents={[]}
mode="create"
/>
</Suspense>
</ErrorBoundary>
)}
</div>
);
}

View File

@@ -65,7 +65,7 @@ export function SetupWizardModal({
agentOnboardingEnabled = false,
}: SetupWizardModalProps) {
const { t } = useTranslation("app");
const helpUrl = "https://github.com/runfusion/fusion/discussions";
const helpUrl = "https://discord.gg/ksrfuy7WYR";
/*
FNXC:Onboarding 2026-06-22-03:11:
First-run project setup must offer a first persistent agent after registration, defaulting to the CEO preset while still letting users choose another template or skip creation.
@@ -678,7 +678,7 @@ export function SetupWizardModal({
className="btn setup-wizard-help-link"
href={helpUrl}
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
{t("setup.needHelp", "Need help?")}
</a>

View File

@@ -258,7 +258,7 @@ vi.mock("../../components/model-onboarding-state", () => ({
getOnboardingCompletedAt: (...args: unknown[]) => mockGetOnboardingCompletedAt(...args),
getSkippedSteps: (...args: unknown[]) => mockGetSkippedSteps(...args),
getStepData: (...args: unknown[]) => mockGetStepData(...args),
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"],
}));
// Mock CustomModelDropdown for onboarding modal tests

View File

@@ -17,6 +17,7 @@ const mockFetchModels = vi.fn();
const mockFetchGlobalSettings = vi.fn();
const mockUpdateGlobalSettings = vi.fn();
const mockCreateTask = vi.fn();
const mockCreateAgent = vi.fn();
const mockFetchCustomProviders = vi.fn();
const mockCreateCustomProvider = vi.fn();
const mockFetchCursorCliStatus = vi.fn();
@@ -36,6 +37,7 @@ vi.mock("../../api", () => ({
fetchGlobalSettings: (...args: unknown[]) => mockFetchGlobalSettings(...args),
updateGlobalSettings: (...args: unknown[]) => mockUpdateGlobalSettings(...args),
createTask: (...args: unknown[]) => mockCreateTask(...args),
createAgent: (...args: unknown[]) => mockCreateAgent(...args),
fetchCustomProviders: (...args: unknown[]) => mockFetchCustomProviders(...args),
createCustomProvider: (...args: unknown[]) => mockCreateCustomProvider(...args),
fetchCursorCliStatus: (...args: unknown[]) => mockFetchCursorCliStatus(...args),
@@ -98,7 +100,7 @@ vi.mock("../model-onboarding-state", () => ({
markStepSkipped: (...args: unknown[]) => mockMarkStepSkipped(...args),
getSkippedSteps: (...args: unknown[]) => mockGetSkippedSteps(...args),
getStepData: (...args: unknown[]) => mockGetStepData(...args),
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"],
}));
const mockTrackOnboardingEvent = vi.fn();
@@ -138,6 +140,8 @@ vi.mock("lucide-react", async (importOriginal) => {
GitPullRequest: () => <span data-testid="icon-git-pull-request">GitPullRequest</span>,
Rocket: () => <span data-testid="icon-rocket">Rocket</span>,
Plus: () => <span data-testid="icon-plus">Plus</span>,
Sparkles: () => <span data-testid="icon-sparkles">Sparkles</span>,
UserRound: () => <span data-testid="icon-user-round">UserRound</span>,
ChevronRight: () => <span data-testid="icon-chevron-right">ChevronRight</span>,
};
});
@@ -180,6 +184,10 @@ async function navigateToProjectSetupStep() {
async function navigateToFirstTaskStep() {
await navigateToProjectSetupStep();
fireEvent.click(screen.getByText("Next →"));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("Skip for now"));
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeTruthy();
});
@@ -194,6 +202,7 @@ beforeEach(() => {
mockFetchGlobalSettings.mockResolvedValue({});
mockUpdateGlobalSettings.mockResolvedValue({});
mockCreateTask.mockResolvedValue({ id: "FN-TEST", description: "test task" });
mockCreateAgent.mockResolvedValue({ id: "agent-1" });
mockFetchCustomProviders.mockResolvedValue({ providers: [] });
mockCreateCustomProvider.mockResolvedValue({ provider: {} });
mockLoginProvider.mockResolvedValue({ url: "https://auth.example.com/login" });
@@ -1869,6 +1878,12 @@ describe("ModelOnboardingModal", () => {
fireEvent.click(screen.getByText("Next →"));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("Skip for now"));
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeTruthy();
});
@@ -2450,6 +2465,12 @@ describe("ModelOnboardingModal", () => {
fireEvent.click(screen.getByText("← Back"));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("← Back"));
await waitFor(() => {
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
});
@@ -2459,6 +2480,7 @@ describe("ModelOnboardingModal", () => {
await waitFor(() => {
expect(screen.getByText("Connect GitHub")).toBeTruthy();
});
});
});
@@ -2582,7 +2604,7 @@ describe("ModelOnboardingModal", () => {
expect(aiSetupIndicator).toHaveClass("done");
expect(githubIndicator).toHaveClass("done");
expect(firstTaskIndicator).toHaveClass("done");
expect(document.querySelectorAll(".model-onboarding-step-connector.done")).toHaveLength(3);
expect(document.querySelectorAll(".model-onboarding-step-connector.done")).toHaveLength(4);
// Click Get Started to close
fireEvent.click(screen.getByText("Get Started"));
@@ -2877,6 +2899,12 @@ describe("ModelOnboardingModal", () => {
// Navigate back to see if the model dropdown has the saved value
fireEvent.click(screen.getByText("← Back"));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("← Back"));
await waitFor(() => {
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
});
@@ -3209,6 +3237,13 @@ describe("ModelOnboardingModal", () => {
fireEvent.click(screen.getByText("Next →"));
// Then advance to optional Agent step before First Task
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("Skip for now"));
// Then advance to First Task step
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeTruthy();
@@ -3273,6 +3308,11 @@ describe("ModelOnboardingModal", () => {
});
fireEvent.click(screen.getByText("Next →"));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("Skip for now"));
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeTruthy();
});
@@ -3320,6 +3360,12 @@ describe("ModelOnboardingModal", () => {
fireEvent.click(screen.getByText("Next →"));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("Skip for now"));
// Then advance to First Task step
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeTruthy();
@@ -4572,4 +4618,3 @@ describe("Custom providers disclosure", () => {
});
});
});

View File

@@ -5,7 +5,7 @@ import { OnboardingResumeCard } from "../OnboardingResumeCard";
// Mock the model-onboarding-state module
vi.mock("../model-onboarding-state", () => ({
getOnboardingResumeStep: vi.fn(),
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"],
}));
const mockTrackOnboardingEvent = vi.fn();
@@ -184,7 +184,7 @@ describe("OnboardingResumeCard", () => {
});
render(<OnboardingResumeCard onResume={vi.fn()} />);
// Uses singular "step" for 1 completed
expect(screen.getByText(/1 of 4 step complete/)).toBeInTheDocument();
expect(screen.getByText(/1 of 5 step complete/)).toBeInTheDocument();
});
it("shows completed step count text with 2 completed steps (plural)", () => {
@@ -195,7 +195,7 @@ describe("OnboardingResumeCard", () => {
});
render(<OnboardingResumeCard onResume={vi.fn()} />);
// Uses plural "steps" for 2 completed
expect(screen.getByText(/2 of 4 steps complete/)).toBeInTheDocument();
expect(screen.getByText(/2 of 5 steps complete/)).toBeInTheDocument();
});
});

View File

@@ -18,7 +18,7 @@ vi.mock("../model-onboarding-state", () => ({
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"],
}));
vi.mock("../PluginSlot", () => ({

View File

@@ -162,7 +162,7 @@ describe("SetupWizardModal", () => {
expect(screen.getByText("Set Token & Continue")).toBeDefined();
expect(screen.getByRole("link", { name: "Need help?" })).toHaveAttribute(
"href",
"https://github.com/runfusion/fusion/discussions"
"https://discord.gg/ksrfuy7WYR"
);
});

View File

@@ -142,7 +142,7 @@ vi.mock("../../components/model-onboarding-state", () => ({
getOnboardingCompletedAt: () => null,
getSkippedSteps: () => [],
getStepData: () => null,
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"],
}));
vi.mock("../../components/Board", () => ({

View File

@@ -17,6 +17,7 @@ const mockSaveApiKey = vi.fn();
const mockClearApiKey = vi.fn();
const mockUpdateGlobalSettings = vi.fn();
const mockCreateTask = vi.fn();
const mockCreateAgent = vi.fn();
vi.mock("../../api", () => ({
fetchAuthStatus: (...args: unknown[]) => mockFetchAuthStatus(...args),
@@ -28,6 +29,7 @@ vi.mock("../../api", () => ({
clearApiKey: (...args: unknown[]) => mockClearApiKey(...args),
updateGlobalSettings: (...args: unknown[]) => mockUpdateGlobalSettings(...args),
createTask: (...args: unknown[]) => mockCreateTask(...args),
createAgent: (...args: unknown[]) => mockCreateAgent(...args),
}));
const mockGetOnboardingState = vi.fn();
@@ -56,7 +58,7 @@ vi.mock("../model-onboarding-state", () => ({
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"],
}));
vi.mock("../CustomModelDropdown", () => ({
@@ -196,7 +198,8 @@ async function advanceThroughSteps(_renderResult: RenderResult, actions: Array<"
} else {
const skipButton =
screen.queryByRole("button", { name: "Skip setup →" })
?? screen.queryByRole("button", { name: "Skip GitHub →" });
?? screen.queryByRole("button", { name: "Skip GitHub →" })
?? screen.queryByRole("button", { name: "Skip for now" });
expect(skipButton).toBeTruthy();
fireEvent.click(skipButton!);
@@ -334,6 +337,7 @@ beforeEach(() => {
mockClearApiKey.mockResolvedValue({ success: true });
mockUpdateGlobalSettings.mockResolvedValue({});
mockCreateTask.mockResolvedValue(createdTaskMock);
mockCreateAgent.mockResolvedValue({ id: "agent-1" });
});
afterEach(() => {
@@ -539,6 +543,11 @@ describe("onboarding flow integration", () => {
});
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: "Skip for now" }));
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
});
@@ -586,7 +595,7 @@ describe("onboarding flow integration", () => {
it("dismissal flow: dismissing on first-task step saves skipped GitHub state", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "skip", "next"]);
await advanceThroughSteps(renderResult, ["next", "skip", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -626,7 +635,7 @@ describe("onboarding flow integration", () => {
expect(screen.getByText("Continue Setup")).toBeInTheDocument();
expect(screen.getByText(/GitHub/)).toBeInTheDocument();
expect(screen.getByText(/1 of 4 step complete/)).toBeInTheDocument();
expect(screen.getByText(/1 of 5 step complete/)).toBeInTheDocument();
});
});
@@ -682,7 +691,7 @@ describe("onboarding flow integration", () => {
expect(screen.getByText("Connect GitHub")).toBeInTheDocument();
});
await advanceThroughSteps(renderResult, ["next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -802,7 +811,7 @@ describe("onboarding flow integration", () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -821,7 +830,7 @@ describe("onboarding flow integration", () => {
expect(screen.getByText("Anthropic")).toBeInTheDocument();
});
await advanceThroughSteps(renderResult, ["skip", "skip", "next"]);
await advanceThroughSteps(renderResult, ["skip", "skip", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -842,7 +851,7 @@ describe("onboarding flow integration", () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -869,7 +878,7 @@ describe("onboarding flow integration", () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -881,7 +890,7 @@ describe("onboarding flow integration", () => {
it("skip warnings: Import from GitHub CTA shows connection requirement note when GitHub not connected", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -900,7 +909,7 @@ describe("onboarding flow integration", () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
@@ -918,7 +927,7 @@ describe("onboarding flow integration", () => {
it("task creation flow: inline task creation shows success state and marks onboarding complete", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await simulateFirstTaskCreation(renderResult, "Ship onboarding telemetry");
await waitFor(() => {
@@ -938,7 +947,7 @@ describe("onboarding flow integration", () => {
it("task creation flow: View Task button navigates and completes onboarding", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await simulateFirstTaskCreation(renderResult, "View task flow");
await waitFor(() => {
@@ -989,6 +998,12 @@ describe("onboarding flow integration", () => {
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: "Skip for now" }));
await waitFor(() => {
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
});
@@ -1013,7 +1028,7 @@ describe("onboarding flow integration", () => {
it("task creation flow: Finish Setup button (without creating task) completes onboarding", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
fireEvent.click(screen.getByRole("button", { name: "Finish Setup" }));
@@ -1027,7 +1042,7 @@ describe("onboarding flow integration", () => {
it("task creation flow: Create a New Task CTA completes onboarding and triggers external task dialog", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
fireEvent.click(screen.getByRole("button", { name: /Create a New Task/i }));
@@ -1049,7 +1064,7 @@ describe("onboarding flow integration", () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
fireEvent.click(screen.getByRole("button", { name: /Import from GitHub/i }));
@@ -1064,7 +1079,7 @@ describe("onboarding flow integration", () => {
it("task creation flow: validation error on empty description does not call createTask", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
fireEvent.click(screen.getByTestId("onboarding-first-task-submit"));
@@ -1076,7 +1091,7 @@ describe("onboarding flow integration", () => {
mockCreateTask.mockRejectedValueOnce(new Error("Task API unavailable"));
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
await simulateFirstTaskCreation(renderResult, "Retryable task");
await waitFor(() => {
@@ -1090,7 +1105,7 @@ describe("onboarding flow integration", () => {
it("task creation flow: Get Started button on complete step closes the modal", async () => {
const renderResult = renderModal();
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
await advanceThroughSteps(renderResult, ["next", "next", "next", "skip"]);
fireEvent.click(screen.getByRole("button", { name: "Finish Setup" }));

View File

@@ -5,7 +5,7 @@
* from where they left off if they dismiss the modal without completing.
*/
export type OnboardingStep = "ai-setup" | "github" | "project-setup" | "first-task" | "complete";
export type OnboardingStep = "ai-setup" | "github" | "project-setup" | "agent" | "first-task" | "complete";
interface OnboardingState {
currentStep: OnboardingStep | string; // string allows for future unknown steps
@@ -42,7 +42,7 @@ const DEFAULT_POST_ONBOARDING_DISMISSED_AT: string | undefined = undefined;
* Ordered onboarding flow steps before completion.
* Keep this list in sync with ModelOnboardingModal's stepper rendering and navigation.
*/
export const ONBOARDING_FLOW_STEPS = ["ai-setup", "github", "project-setup", "first-task"] as const;
export const ONBOARDING_FLOW_STEPS = ["ai-setup", "github", "project-setup", "agent", "first-task"] as const;
/**
* Step labels for display in the resume card.
@@ -52,6 +52,7 @@ export const ONBOARDING_STEP_LABELS: Record<OnboardingStep, string> = {
"ai-setup": "AI Setup",
github: "GitHub",
"project-setup": "Project",
agent: "Agent",
"first-task": "First Task",
complete: "Complete",
};

View File

@@ -14,7 +14,7 @@ vi.mock("../../api", () => ({
vi.mock("../../components/model-onboarding-state", () => ({
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"],
}));
vi.mock("../../components/onboarding-events", () => ({

View File

@@ -6679,6 +6679,7 @@
"profileName": "Profile name",
"projectDirectory": "Project Directory",
"projectMustBeSelected": "A project must be selected before you can create tasks or import from GitHub.",
"projectMustBeSelectedForAgent": "Set up a project before creating your first agent. You can also skip this and create tasks without a persistent agent.",
"projectName": "Project Name",
"projectNameHintClone": "By default this follows the destination folder name unless you edit it.",
"projectNameHintExisting": "By default this follows the selected directory name unless you edit it.",
@@ -6760,6 +6761,7 @@
"statusNotConnected": "Not connected",
"statusRetry": "Retry",
"statusSkipped": "Skipped",
"stepAgent": "Agent",
"stepAiSetup": "AI Setup",
"stepFirstTask": "First Task",
"stepGithub": "GitHub",
@@ -6773,6 +6775,7 @@
"titleAiSetup": "Set Up AI",
"titleAllSet": "All Set!",
"titleConnectGitHub": "Connect GitHub",
"titleCreateFirstAgent": "Create Your First Agent",
"titleCreateFirstTask": "Create Your First Task",
"titleSetUpProject": "Set Up Your Project",
"tokenEnvVar": "The token was set via the {{env}} environment variable when starting the dashboard.",

View File

@@ -6679,6 +6679,7 @@
"profileName": "Nombre del perfil",
"projectDirectory": "Directorio del proyecto",
"projectMustBeSelected": "Debes seleccionar un proyecto antes de crear tareas o importar desde GitHub.",
"projectMustBeSelectedForAgent": "",
"projectName": "Nombre del proyecto",
"projectNameHintClone": "De forma predeterminada, esto sigue el nombre de la carpeta de destino a menos que lo edite.",
"projectNameHintExisting": "De forma predeterminada, esto sigue el nombre del directorio seleccionado a menos que lo edite.",
@@ -6760,6 +6761,7 @@
"statusNotConnected": "No conectado",
"statusRetry": "Reintentar",
"statusSkipped": "Omitido",
"stepAgent": "",
"stepAiSetup": "Configuración IA",
"stepFirstTask": "Primera tarea",
"stepGithub": "GitHub",
@@ -6773,6 +6775,7 @@
"titleAiSetup": "Configurar IA",
"titleAllSet": "¡Todo listo!",
"titleConnectGitHub": "Conectar GitHub",
"titleCreateFirstAgent": "",
"titleCreateFirstTask": "Crear tu primera tarea",
"titleSetUpProject": "Configurar tu proyecto",
"tokenEnvVar": "El token se estableció a través de la variable de entorno {{env}} al iniciar el panel.",

View File

@@ -6679,6 +6679,7 @@
"profileName": "Nom du profil",
"projectDirectory": "Répertoire du projet",
"projectMustBeSelected": "Un projet doit être sélectionné avant de pouvoir créer des tâches ou importer depuis GitHub.",
"projectMustBeSelectedForAgent": "",
"projectName": "Nom du projet",
"projectNameHintClone": "Par défaut, cela suit le nom du dossier de destination sauf si vous l'éditez.",
"projectNameHintExisting": "Par défaut, cela suit le nom du répertoire sélectionné sauf si vous l'éditez.",
@@ -6760,6 +6761,7 @@
"statusNotConnected": "Non connecté",
"statusRetry": "Réessayer",
"statusSkipped": "Ignoré",
"stepAgent": "",
"stepAiSetup": "Configuration IA",
"stepFirstTask": "Première tâche",
"stepGithub": "GitHub",
@@ -6773,6 +6775,7 @@
"titleAiSetup": "Configurer l'IA",
"titleAllSet": "C'est parti !",
"titleConnectGitHub": "Connecter GitHub",
"titleCreateFirstAgent": "",
"titleCreateFirstTask": "Créer votre première tâche",
"titleSetUpProject": "Configurer votre projet",
"tokenEnvVar": "Le jeton a été défini via la variable d'environnement {{env}} au démarrage du tableau de bord.",

View File

@@ -6679,6 +6679,7 @@
"profileName": "프로필 이름",
"projectDirectory": "프로젝트 디렉터리",
"projectMustBeSelected": "작업을 생성하거나 GitHub에서 가져오려면 먼저 프로젝트를 선택해야 합니다.",
"projectMustBeSelectedForAgent": "",
"projectName": "프로젝트 이름",
"projectNameHintClone": "기본적으로 대상 폴더 이름을 따르며, 직접 편집할 수 있습니다.",
"projectNameHintExisting": "기본적으로 선택된 디렉터리 이름을 따르며, 직접 편집할 수 있습니다.",
@@ -6760,6 +6761,7 @@
"statusNotConnected": "연결되지 않음",
"statusRetry": "재시도",
"statusSkipped": "건너뜀",
"stepAgent": "",
"stepAiSetup": "AI 설정",
"stepFirstTask": "첫 번째 작업",
"stepGithub": "GitHub",
@@ -6773,6 +6775,7 @@
"titleAiSetup": "AI 설정",
"titleAllSet": "모두 완료!",
"titleConnectGitHub": "GitHub 연결",
"titleCreateFirstAgent": "",
"titleCreateFirstTask": "첫 번째 작업 만들기",
"titleSetUpProject": "프로젝트 설정",
"tokenEnvVar": "대시보드 시작 시 {{env}} 환경 변수를 통해 토큰이 설정되었습니다.",

View File

@@ -6679,6 +6679,7 @@
"profileName": "配置文件名称",
"projectDirectory": "项目目录",
"projectMustBeSelected": "在创建任务或从 GitHub 导入之前,必须先选择一个项目。",
"projectMustBeSelectedForAgent": "",
"projectName": "项目名称",
"projectNameHintClone": "默认情况下,这遵循目标文件夹名称,除非您编辑它。",
"projectNameHintExisting": "默认情况下,这遵循选定的目录名称,除非您编辑它。",
@@ -6760,6 +6761,7 @@
"statusNotConnected": "未连接",
"statusRetry": "重试",
"statusSkipped": "已跳过",
"stepAgent": "",
"stepAiSetup": "AI 设置",
"stepFirstTask": "第一个任务",
"stepGithub": "GitHub",
@@ -6773,6 +6775,7 @@
"titleAiSetup": "设置 AI",
"titleAllSet": "一切就绪!",
"titleConnectGitHub": "连接 GitHub",
"titleCreateFirstAgent": "",
"titleCreateFirstTask": "创建您的第一个任务",
"titleSetUpProject": "设置您的项目",
"tokenEnvVar": "启动仪表板时通过 {{env}} 环境变量设置令牌。",

View File

@@ -6679,6 +6679,7 @@
"profileName": "設定檔名稱",
"projectDirectory": "專案目錄",
"projectMustBeSelected": "在建立任務或從 GitHub 匯入之前,必須先選擇一個專案。",
"projectMustBeSelectedForAgent": "",
"projectName": "專案名稱",
"projectNameHintClone": "預設情況下,除非您編輯,否則這遵循目的地資料夾名稱。",
"projectNameHintExisting": "預設情況下,除非您編輯,否則這遵循選定的目錄名稱。",
@@ -6760,6 +6761,7 @@
"statusNotConnected": "未連線",
"statusRetry": "重試",
"statusSkipped": "已略過",
"stepAgent": "",
"stepAiSetup": "AI 設定",
"stepFirstTask": "第一個任務",
"stepGithub": "GitHub",
@@ -6773,6 +6775,7 @@
"titleAiSetup": "設定 AI",
"titleAllSet": "一切就緒!",
"titleConnectGitHub": "連線 GitHub",
"titleCreateFirstAgent": "",
"titleCreateFirstTask": "建立您的第一個任務",
"titleSetUpProject": "設定您的專案",
"tokenEnvVar": "啟動儀表板時通過 {{env}} 環境變數設定令牌。",

View File

@@ -6683,6 +6683,7 @@ export default interface Resources {
"profileName": "Profile name",
"projectDirectory": "Project Directory",
"projectMustBeSelected": "A project must be selected before you can create tasks or import from GitHub.",
"projectMustBeSelectedForAgent": "Set up a project before creating your first agent. You can also skip this and create tasks without a persistent agent.",
"projectName": "Project Name",
"projectNameHintClone": "By default this follows the destination folder name unless you edit it.",
"projectNameHintExisting": "By default this follows the selected directory name unless you edit it.",
@@ -6765,6 +6766,7 @@ export default interface Resources {
"statusNotConnected": "Not connected",
"statusRetry": "Retry",
"statusSkipped": "Skipped",
"stepAgent": "Agent",
"stepAiSetup": "AI Setup",
"stepFirstTask": "First Task",
"stepGithub": "GitHub",
@@ -6778,6 +6780,7 @@ export default interface Resources {
"titleAiSetup": "Set Up AI",
"titleAllSet": "All Set!",
"titleConnectGitHub": "Connect GitHub",
"titleCreateFirstAgent": "Create Your First Agent",
"titleCreateFirstTask": "Create Your First Task",
"titleSetUpProject": "Set Up Your Project",
"tokenEnvVar": "The token was set via the {{env}} environment variable when starting the dashboard.",