feat(onboarding): ask for optional first agent in setup
This commit is contained in:
5
.changeset/first-run-agent-onboarding.md
Normal file
5
.changeset/first-run-agent-onboarding.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
Ask first-run users whether to create an optional first persistent agent after project registration, with CEO as the default template, skip support, and no duplicate GitHub star prompt.
|
||||
@@ -693,6 +693,14 @@ Before clicking **Create**, the final review step remains editable for identity/
|
||||
|
||||
The final `createAgent(...)` call always uses the latest values from these step-2 controls.
|
||||
|
||||
### First-run setup first agent
|
||||
|
||||
After first-project registration, first-run setup asks whether to create a first persistent agent before entering the dashboard. The CEO preset is selected by default because this first agent is framed as an optional coordinator that can help create tasks and keep direction across sessions.
|
||||
|
||||
Users can choose any existing preset, create the selected preset-scoped agent for the newly registered project, or skip the step and create agents later from the Agents view. Creating or assigning a persistent agent is not required for task work: Fusion still creates task-scoped temporary agents to plan, execute, review, and merge tasks.
|
||||
|
||||
When `experimentalFeatures.agentOnboarding` is enabled, first-run setup also offers the same draft-first **AI Interview** path used by the New Agent dialog. Applying the interview draft updates the setup preview, but persistence remains explicit through **Create Agent**.
|
||||
|
||||
### Experimental planning-style onboarding
|
||||
|
||||
The **New Agent** dialog is the canonical launch point for agent creation.
|
||||
|
||||
@@ -357,7 +357,7 @@ Mailbox view shows inbox/outbox communication threads and unread state.
|
||||
- mailbox entry points now show unread/pending indicators: the desktop/tablet Header mailbox toggle shows a pending-approval dot first or an unread dot when unread mail exists without pending approvals, the mobile bottom-nav Mailbox tab carries the mobile badges/dots, and the compact Header actions overflow keeps a Mailbox entry only when the mobile bottom nav is disabled
|
||||
- approval lifecycle SSE events (`approval:requested`, `approval:updated`, `approval:decided`) trigger mailbox approvals refresh without manual reload
|
||||
- when a task newly enters `awaiting-approval`, the app shows a persistent approval banner above project content with an **Open Mailbox** CTA; dismissals are remembered per approval item until that item advances or a different one arrives
|
||||
- when a task first transitions into `done`, the dashboard shows a one-time **Enjoying Fusion?** GitHub star prompt in the project view; clicking **Star on GitHub** or dismissing the card marks it shown in browser `localStorage`, so it does not reappear on reload or later task completions
|
||||
- when a task first transitions into `done`, the dashboard shows a one-time **Enjoying Fusion?** GitHub star prompt in the project view after first-run setup is closed; clicking **Star on GitHub** or dismissing the card marks it shown in browser `localStorage`, so it does not reappear on reload or later task completions. The setup wizard does not add a second star prompt.
|
||||
- Visible message history/threading is driven by explicit `message.metadata.replyTo.messageId` links
|
||||
- Separate top-level messages from the same sender remain independent in the inbox and detail pane
|
||||
|
||||
@@ -595,6 +595,7 @@ Navigation:
|
||||
Features:
|
||||
- Switch between **List**, **Board**, and **Org chart** layouts
|
||||
- Filter by role/state, include/exclude system agents, and inspect health/status
|
||||
- First-run setup asks whether to create a first persistent agent after project registration. The default template is **CEO**, users can choose another preset, use the AI interview when `experimentalFeatures.agentOnboarding` is enabled, or skip the step. Task creation and execution do not require creating or assigning a persistent agent: Fusion automatically spawns temporary agents to plan, execute, review, and merge task work.
|
||||
- Start, pause, stop, and trigger agent runs from the view and from detail panels
|
||||
- In **Agent detail**, use the kebab **Bulk agent actions** button in the header utility cluster (next to **Refresh** and **Close**) to run project-wide lifecycle transitions for non-ephemeral agents in the current project — **Pause All Agents** targets agents in the `active` or `running` state, while **Resume All Agents** targets agents in the `paused` state only
|
||||
- Bulk menu items stay disabled when nothing is eligible and show an inline hint (`Loading eligible agents...`, `No active agents eligible`, `No paused agents eligible`, or the current eligible count such as `Pause 2 active/running agents`)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
title: First-Run Agent Onboarding
|
||||
type: feat
|
||||
date: 2026-06-22
|
||||
---
|
||||
|
||||
# First-Run Agent Onboarding
|
||||
|
||||
## Summary
|
||||
|
||||
Extend first-run onboarding so, after registering the first project through any setup mode, Fusion invites the user to create their first persistent agent. The default path should create a CEO-style coordinating agent from the existing preset library, while still letting users choose another template, use AI interview generation when the existing agent-onboarding feature flag is enabled, or skip agent creation and finish setup.
|
||||
|
||||
---
|
||||
|
||||
## Problem Frame
|
||||
|
||||
First-run setup currently ends after project registration, leaving the agent system as something users discover later. The requested change makes the first persistent agent part of onboarding and explains the distinction between Fusion's temporary task agents and a user-created coordinating agent that can help create, coordinate, and manage work.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- R1. The setup wizard must ask the user to create their first agent after any successful first-project registration path, including existing-directory and clone setup modes.
|
||||
- R2. The default creation choice must be the existing CEO preset (`id: "ceo"`) so a new user gets a coordination-oriented agent without extra decisions.
|
||||
- R3. The wizard must let users choose from the existing agent preset/template library before creating the agent.
|
||||
- R4. When `experimentalFeatures.agentOnboarding` is enabled, the wizard must offer the existing AI interview/generation path as an alternate way to draft the first agent; when disabled, preset creation and skip remain available without an unavailable AI affordance.
|
||||
- R5. The wizard copy must explain that Fusion creates temporary agents to work on tasks, while the user's persistent agent can coordinate, create tasks, and help manage the work.
|
||||
- R6. The agent step must be skippable, and skipping must complete onboarding without creating an agent.
|
||||
- R7. Users who skip must still have a clear path to create agents later from the Agents view.
|
||||
- R8. Agent creation during setup must use the same validation and persistence behavior as the existing New Agent flow.
|
||||
- R9. The onboarding update must remain usable on desktop and mobile modal layouts, including keyboard preset selection, selected-preset screen-reader labeling, reachable Create/Skip/AI actions, inline error focus, and supported mobile breakpoint behavior.
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
- **Reuse shared creation logic, not copied mappings:** Build the new onboarding step from `AGENT_PRESETS`, `createAgent(...)`, and the current AI interview draft flow, but extract shared preset/draft/payload helpers or a shared creation component from `NewAgentDialog` so setup does not mirror `handlePresetSelect` / `handleCreate` by hand. This keeps first-run creation aligned with the canonical New Agent dialog as presets, runtime fields, and validation evolve.
|
||||
- **Keep setup wizard as the orchestrator:** Add an `"agent"` wizard step to `SetupWizardModal` after project registration succeeds and before `"complete"`. Registration must store the returned `ProjectInfo` in wizard state and must not invoke the current parent setup-complete callback until agent creation succeeds or the user skips.
|
||||
- **Default CEO by selected preset, not hidden auto-create:** Preselect the CEO preset by stable `id: "ceo"` and make the action explicit. CEO is the default because the first persistent agent is framed as a coordinator for task creation and cross-task direction, while role-specific templates remain available for users who want a narrower first agent.
|
||||
- **Skip is terminal for setup:** A skip action should advance to the completion step and should not mark an error, create a placeholder agent, or require the user to visit the Agents view immediately.
|
||||
- **AI interview remains draft-first and feature-flagged:** Reuse `ExperimentalAgentOnboardingModal` semantics where AI produces a draft for review before persistence, and expose the AI entry only when `experimentalFeatures.agentOnboarding === true` is passed into setup. The setup wizard can apply the draft to the agent step, but the final Create action remains explicit.
|
||||
- **Project scope follows the registered project:** The first agent should be created in the project context returned by `registerProject(...)`, matching the workspace the user just registered. Implementation must verify the exact `createAgent(...)` scoping contract, including where the project ID is passed and how tests prove the created agent belongs to the registered project.
|
||||
|
||||
---
|
||||
|
||||
## High-Level Technical Design
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Auth[Auth step] --> Project[Project registration step]
|
||||
Project -->|any first-project registration succeeds| Agent[First agent step]
|
||||
Agent -->|Create CEO/default preset| CreateAgent[POST /api/agents scoped to project]
|
||||
Agent -->|Choose preset| CreateAgent
|
||||
Agent -->|AI interview draft| ReviewDraft[Review generated draft]
|
||||
ReviewDraft --> CreateAgent
|
||||
Agent -->|Skip| Complete[Setup complete]
|
||||
CreateAgent --> Complete
|
||||
```
|
||||
|
||||
The agent step should be an onboarding-specific entry surface backed by shared New Agent creation logic rather than a copied second implementation. It needs preset selection, a concise preview of the selected agent, an AI interview entry point when enabled, a pending state for Create, and a skip action.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Units
|
||||
|
||||
### U1. Add First-Agent State to Setup Wizard
|
||||
|
||||
- **Goal:** Extend `SetupWizardModal` with an agent step that appears after any successful first-project registration path and owns the newly registered project ID for scoped agent creation.
|
||||
- **Files:** `packages/dashboard/app/components/SetupWizardModal.tsx`, `packages/dashboard/app/components/SetupWizardModal.css`, `packages/dashboard/app/components/AppModals.tsx`, `packages/dashboard/app/hooks/useProjectActions.ts`
|
||||
- **Patterns:** Follow the existing `WizardState` step model, footer action branching, and modal layout rules in `SetupWizardModal`.
|
||||
- **Test Scenarios:** In `packages/dashboard/app/components/__tests__/SetupWizardModal.test.tsx`, assert that existing-directory and clone registration each advance to the first-agent step before completion, store the returned project ID, and preserve their existing registration payloads.
|
||||
- **Verification:** The project registration payload tests stay unchanged, the parent setup-complete callback is not invoked at registration time, and completion is reached only after agent creation or skip.
|
||||
|
||||
### U2. Reuse Preset-Based Agent Creation
|
||||
|
||||
- **Goal:** Render the existing preset list with CEO preselected and create an agent from the selected preset using shared New Agent creation mapping and `createAgent(...)`.
|
||||
- **Files:** `packages/dashboard/app/components/SetupWizardModal.tsx`, `packages/dashboard/app/components/NewAgentDialog.tsx`, `packages/dashboard/app/components/agent-presets/index.ts`, `packages/dashboard/app/api/legacy.ts`
|
||||
- **Patterns:** Extract or reuse a shared preset-to-form/payload helper so onboarding and `NewAgentDialog` produce the same create payload for the same preset.
|
||||
- **Test Scenarios:** In `SetupWizardModal.test.tsx`, assert that CEO is selected by default, selecting a different preset changes the preview, and clicking Create sends a scoped `createAgent` payload with name, role, title, icon, soul, and instructions text. Add a parity test that compares the onboarding-created payload with the canonical preset-created payload for the same preset.
|
||||
- **Verification:** Agent creation enters a pending state, disables duplicate create/skip/template changes as appropriate, exposes accessible busy feedback, displays errors inline with focus returned to the error/agent step, and transitions to completion only after persistence succeeds.
|
||||
|
||||
### U3. Add Skip Path and Later-Creation Copy
|
||||
|
||||
- **Goal:** Make skipping the first-agent step explicit and safe, with copy that says users can create agents later from the Agents view.
|
||||
- **Files:** `packages/dashboard/app/components/SetupWizardModal.tsx`, `packages/dashboard/app/components/SetupWizardModal.css`, `packages/i18n/locales/en/app.json`, `packages/i18n/locales/es/app.json`, `packages/i18n/locales/fr/app.json`, `packages/i18n/locales/ko/app.json`, `packages/i18n/locales/zh-CN/app.json`, `packages/i18n/locales/zh-TW/app.json`
|
||||
- **Patterns:** Follow existing setup wizard skip-button behavior from the auth step, but route to `"complete"` rather than a prior setup step.
|
||||
- **Test Scenarios:** In `SetupWizardModal.test.tsx`, assert that clicking Skip on the first-agent step does not call `createAgent`, advances to completion, and renders completion copy that still makes sense when no agent was created.
|
||||
- **Verification:** The skip button remains keyboard reachable, has an accessible name on desktop and mobile, and routes to setup completion without calling `createAgent(...)`.
|
||||
|
||||
### U4. Integrate AI Interview as an Optional Draft Path
|
||||
|
||||
- **Goal:** Offer AI-generated first-agent drafting from the agent step using the existing `ExperimentalAgentOnboardingModal` and draft application behavior when the existing experimental feature flag is enabled.
|
||||
- **Files:** `packages/dashboard/app/App.tsx`, `packages/dashboard/app/components/AppModals.tsx`, `packages/dashboard/app/components/SetupWizardModal.tsx`, `packages/dashboard/app/components/ExperimentalAgentOnboardingModal.tsx`, `packages/dashboard/app/api/legacy.ts`
|
||||
- **Patterns:** Pass `agentOnboardingEnabled={experimentalFeatures.agentOnboarding === true}` through `AppModals` into `SetupWizardModal`. Reuse the create-mode data contract from `NewAgentDialog`; first-run setup should pass an explicit empty existing-agent context unless implementation fetches the registered project's agents before opening the interview.
|
||||
- **Test Scenarios:** In `SetupWizardModal.test.tsx`, assert that the AI entry point is hidden when disabled, opens the interview when enabled, applying a draft updates the preview/create payload, and the user still must confirm creation.
|
||||
- **Verification:** Cancel, close, escape/backdrop behavior, draft application, error handling, and focus restoration return the user to the triggering AI button or updated draft preview. If the AI interview errors, preset creation and skip remain available.
|
||||
|
||||
### U5. Update User-Facing Copy and Documentation
|
||||
|
||||
- **Goal:** Add concise onboarding text explaining temporary task agents versus the user's persistent coordinating agent.
|
||||
- **Files:** `packages/i18n/locales/en/app.json`, `packages/i18n/locales/es/app.json`, `packages/i18n/locales/fr/app.json`, `packages/i18n/locales/ko/app.json`, `packages/i18n/locales/zh-CN/app.json`, `packages/i18n/locales/zh-TW/app.json`, `docs/dashboard-guide.md`, `docs/agents.md`
|
||||
- **Patterns:** Keep `docs/agents.md` as the deeper conceptual reference for agent behavior and `docs/dashboard-guide.md` as the first-run UI guide.
|
||||
- **Test Scenarios:** In `packages/i18n/src/__tests__/i18n-gate-coverage.test.ts`, update any key coverage expectations if new setup keys require inclusion. In docs-adjacent tests, update lazy/setup references only if they assert specific onboarding inventories.
|
||||
- **Verification:** Run the existing i18n extraction/sync/types flow (`pnpm i18n:extract`, `pnpm i18n:sync`, `pnpm i18n:types`) so `packages/i18n/src/resources.d.ts` is regenerated instead of manually edited, and confirm UI copy avoids implying the persistent agent replaces Fusion's temporary executor/reviewer agents.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Examples
|
||||
|
||||
- AE1. Given a new user registers their first project through existing-directory or clone setup, when registration succeeds, then the wizard shows a first-agent step with CEO selected by default.
|
||||
- AE2. Given the user wants a different coordinating role, when they select another preset and create it, then Fusion creates that preset-scoped agent for the registered project and completes setup.
|
||||
- AE3. Given the experimental agent-onboarding flag is enabled and the user wants help designing the agent, when they use AI interview and apply the generated draft, then the wizard previews the draft and waits for explicit Create before saving.
|
||||
- AE4. Given the user does not want to create an agent during onboarding, when they click Skip, then setup completes and no agent creation request is sent.
|
||||
- AE5. Given agent creation fails, when the API returns an error, then the wizard keeps the user on the first-agent step with an inline error and preserves their selected preset or draft.
|
||||
- AE6. Given the user is on a mobile viewport, when the first-agent step renders, then preset selection, preview, AI entry point when enabled, Create, Skip, inline errors, and completion copy remain visible or reachable without incoherent overflow.
|
||||
|
||||
---
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
- The plan does not replace `NewAgentDialog` or the Agents view creation workflow; setup must share its creation mapping or component rather than fork behavior.
|
||||
- The plan does not auto-create an agent without an explicit user action.
|
||||
- The plan does not change temporary task-agent provisioning or execution behavior.
|
||||
- The plan does not require AI interview to be enabled for first-run setup to work.
|
||||
- The plan does not introduce new agent templates beyond reusing the existing preset library.
|
||||
|
||||
---
|
||||
|
||||
## System-Wide Impact
|
||||
|
||||
The change affects first-run onboarding, project registration completion, and agent creation from a new entry point. It should not alter task execution, ephemeral agent provisioning, model onboarding, or the existing Agents view. Because `SetupWizardModal` is lazy-loaded, the lazy-loaded views inventory should not change.
|
||||
|
||||
---
|
||||
|
||||
## Risks & Dependencies
|
||||
|
||||
- **Nested modal risk:** Opening the AI interview from setup can stack modals. Keep the interview optional, gated by `agentOnboardingEnabled`, and verify launch, cancel, close, escape/backdrop behavior, draft application, errors, and focus restoration.
|
||||
- **Project ID timing:** Agent creation needs the project ID returned by registration. Preserve that value in wizard state before advancing, and call the parent setup-complete callback only after agent creation or skip.
|
||||
- **Agent API scoping:** Verify whether `createAgent(...)` uses an explicit `projectId` argument, implicit active project context, or global storage. If explicit project scoping is missing, add the API contract work before UI wiring.
|
||||
- **Mobile density:** Preset selection inside setup can become tall on small screens. Use a compact list or responsive grid that fits the modal's existing mobile height constraints, then verify desktop and mobile viewports for preset selection, preview, AI entry point, Create, Skip, inline error, and completion states.
|
||||
|
||||
---
|
||||
|
||||
## Sources / Research
|
||||
|
||||
- `packages/dashboard/app/components/SetupWizardModal.tsx` currently owns auth, project registration, clone/existing project modes, advanced settings, and completion.
|
||||
- `packages/dashboard/app/components/NewAgentDialog.tsx` is the canonical manual agent creation flow and already maps presets, AI drafts, runtime fields, and `createAgent(...)`.
|
||||
- `packages/dashboard/app/App.tsx` computes `agentOnboardingEnabled` from `experimentalFeatures.agentOnboarding`; `packages/dashboard/app/components/AppModals.tsx` is the modal boundary that must pass the flag into setup.
|
||||
- `packages/dashboard/app/components/ExperimentalAgentOnboardingModal.tsx` provides the draft-first AI interview behavior for agent creation.
|
||||
- `packages/dashboard/app/components/agent-presets/index.ts` defines the CEO preset and the rest of the reusable preset library.
|
||||
- `packages/i18n/locales/*/app.json` are the source catalogs for setup UI copy; `packages/i18n/src/resources.d.ts` is generated by the i18n scripts.
|
||||
- `docs/agents.md` documents the canonical New Agent dialog, experimental planning-style onboarding, and preset library.
|
||||
@@ -2369,7 +2369,8 @@ function AppInner() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{viewMode === "project" && currentProject && showGitHubStarPrompt && !gitHubStarPromptShown && (
|
||||
{/* FNXC:Onboarding 2026-06-22-03:11: The one-time GitHub star prompt stays tied to first completed task, but first-run setup must finish the optional persistent-agent create/skip step before any star ask can surface. Do not add a second setup-specific star prompt. */}
|
||||
{viewMode === "project" && currentProject && showGitHubStarPrompt && !gitHubStarPromptShown && !modalManager.setupWizardOpen && (
|
||||
<GitHubStarPrompt
|
||||
onDismiss={() => {
|
||||
markGitHubStarPromptShown();
|
||||
@@ -2521,6 +2522,7 @@ function AppInner() {
|
||||
onSettingsClose={handleSettingsCloseWithNav}
|
||||
onReopenOnboarding={reopenOnboardingWithNav}
|
||||
onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")}
|
||||
agentOnboardingEnabled={agentOnboardingEnabled}
|
||||
/>
|
||||
<AuthTokenRecoveryDialog open={authTokenRecoveryOpen} />
|
||||
{shellApi && (
|
||||
|
||||
@@ -89,6 +89,8 @@ interface AppModalsProps {
|
||||
onReopenOnboarding?: () => void;
|
||||
/** Optional callback to open mailbox approvals from Settings. */
|
||||
onOpenApprovals?: (approvalId?: string) => void;
|
||||
/** Enables planning-style agent onboarding entry points inside setup. */
|
||||
agentOnboardingEnabled?: boolean;
|
||||
}
|
||||
|
||||
export function AppModals({
|
||||
@@ -110,6 +112,7 @@ export function AppModals({
|
||||
onSettingsClose,
|
||||
onReopenOnboarding,
|
||||
onOpenApprovals,
|
||||
agentOnboardingEnabled = false,
|
||||
}: AppModalsProps) {
|
||||
const { pushNav, removeNav } = useNavigationHistoryContext();
|
||||
const [firstCreatedTask, setFirstCreatedTask] = useState<Task | null>(null);
|
||||
@@ -471,6 +474,7 @@ export function AppModals({
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={projectActions.handleSetupComplete}
|
||||
onClose={closeSetupWizardWithNav}
|
||||
agentOnboardingEnabled={agentOnboardingEnabled}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Agent, AgentOnboardingSummary, ConversationHistoryEntry, ExistingAgentOnboardingConfig, OnboardingMode } from "../api";
|
||||
import {
|
||||
@@ -43,6 +43,7 @@ export function ExperimentalAgentOnboardingModal({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [history, setHistory] = useState<ConversationHistoryEntry[]>([]);
|
||||
const isEditMode = mode === "edit";
|
||||
const activeRef = useRef(isOpen);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setViewState("initial");
|
||||
@@ -62,9 +63,11 @@ export function ExperimentalAgentOnboardingModal({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
activeRef.current = isOpen;
|
||||
if (!sessionId) return;
|
||||
const stream = connectAgentOnboardingStream(sessionId, projectId, {
|
||||
onThinking: (data) => {
|
||||
if (!activeRef.current) return;
|
||||
setHistory((current) => {
|
||||
const next = [...current];
|
||||
const last = next[next.length - 1];
|
||||
@@ -76,23 +79,30 @@ export function ExperimentalAgentOnboardingModal({
|
||||
});
|
||||
},
|
||||
onQuestion: (q) => {
|
||||
if (!activeRef.current) return;
|
||||
setCurrentQuestion(q.question);
|
||||
setCurrentQuestionId(q.id);
|
||||
setViewState("question");
|
||||
},
|
||||
onSummary: (nextSummary) => {
|
||||
if (!activeRef.current) return;
|
||||
setSummary(nextSummary);
|
||||
setViewState("summary");
|
||||
},
|
||||
onError: (message) => {
|
||||
if (!activeRef.current) return;
|
||||
setError(message);
|
||||
setViewState("error");
|
||||
},
|
||||
});
|
||||
return () => stream.close();
|
||||
}, [sessionId, projectId]);
|
||||
return () => {
|
||||
activeRef.current = false;
|
||||
stream.close();
|
||||
};
|
||||
}, [isOpen, sessionId, projectId]);
|
||||
|
||||
const handleClose = async () => {
|
||||
activeRef.current = false;
|
||||
try {
|
||||
if (sessionId) {
|
||||
await cancelAgentOnboarding(sessionId, projectId);
|
||||
@@ -106,6 +116,7 @@ export function ExperimentalAgentOnboardingModal({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
activeRef.current = isOpen;
|
||||
if (!isOpen) {
|
||||
resetState();
|
||||
}
|
||||
@@ -134,8 +145,10 @@ export function ExperimentalAgentOnboardingModal({
|
||||
},
|
||||
projectId,
|
||||
);
|
||||
if (!activeRef.current) return;
|
||||
setSessionId(result.sessionId);
|
||||
} catch (err) {
|
||||
if (!activeRef.current) return;
|
||||
setError((err as Error).message);
|
||||
setViewState("error");
|
||||
}
|
||||
@@ -155,8 +168,10 @@ export function ExperimentalAgentOnboardingModal({
|
||||
},
|
||||
]);
|
||||
await respondToAgentOnboarding(sessionId, responsePayload, projectId);
|
||||
if (!activeRef.current) return;
|
||||
setAnswer("");
|
||||
} catch (err) {
|
||||
if (!activeRef.current) return;
|
||||
setError((err as Error).message);
|
||||
setViewState("error");
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ import { LoadingSpinner } from "./LoadingSpinner";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { AgentGenerationModal } from "./AgentGenerationModal";
|
||||
import { AGENT_PRESETS, type AgentPreset } from "./agent-presets";
|
||||
import {
|
||||
buildAgentCreatePayload,
|
||||
mapOnboardingSummaryToAgentDraft,
|
||||
mapPresetToAgentDraft,
|
||||
VALID_AGENT_CAPABILITIES,
|
||||
type ThinkingLevel,
|
||||
} from "./agent-presets/agentCreatePayload";
|
||||
import { SkillMultiselect } from "./SkillMultiselect";
|
||||
import { AgentAvatar } from "./AgentAvatar";
|
||||
import { ExperimentalAgentOnboardingModal } from "./ExperimentalAgentOnboardingModal";
|
||||
@@ -36,11 +43,6 @@ const AGENT_ROLES: { value: AgentCapability; icon: string }[] = [
|
||||
{ value: "custom", icon: "✦" },
|
||||
];
|
||||
|
||||
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
|
||||
/** Set of valid AgentCapability values for mapping generated roles */
|
||||
const VALID_CAPABILITIES = new Set<string>(["triage", "executor", "reviewer", "merger", "scheduler", "engineer", "custom"]);
|
||||
|
||||
interface RuntimeConfig {
|
||||
model: string;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
@@ -171,7 +173,7 @@ export function NewAgentDialog({
|
||||
|
||||
const handleGenerated = useCallback((spec: AgentGenerationSpec) => {
|
||||
// Map generated role to AgentCapability, default to "custom" if unrecognized
|
||||
const mappedRole = VALID_CAPABILITIES.has(spec.role)
|
||||
const mappedRole = VALID_AGENT_CAPABILITIES.has(spec.role)
|
||||
? (spec.role as AgentCapability)
|
||||
: "custom";
|
||||
|
||||
@@ -205,42 +207,42 @@ export function NewAgentDialog({
|
||||
|
||||
|
||||
const handlePresetSelect = useCallback((preset: AgentPreset) => {
|
||||
const draft = mapPresetToAgentDraft(preset);
|
||||
setSelectedPresetId(preset.id);
|
||||
setName(preset.name);
|
||||
setIcon(preset.icon);
|
||||
setTitle(preset.description ?? preset.title);
|
||||
setRole(preset.role);
|
||||
setSoul(preset.soul ?? "");
|
||||
setInstructionsText(preset.instructionsText ?? "");
|
||||
setName(draft.name);
|
||||
setIcon(draft.icon ?? "");
|
||||
setTitle(draft.title ?? "");
|
||||
setRole(draft.role);
|
||||
setSoul(draft.soul ?? "");
|
||||
setInstructionsText(draft.instructionsText ?? "");
|
||||
// Advance to Step 1 so user can review model selection
|
||||
setStep(1);
|
||||
}, []);
|
||||
|
||||
const applyDraftToForm = useCallback((draft: AgentOnboardingSummary) => {
|
||||
const runtimeHint = draft.runtimeHint?.trim() ?? "";
|
||||
const modelSelection = draft.model?.trim() || draft.modelHint?.trim() || "";
|
||||
const values = mapOnboardingSummaryToAgentDraft(draft);
|
||||
|
||||
setStep(1);
|
||||
setStepZeroTab("custom");
|
||||
setName(draft.name ?? "");
|
||||
setTitle(draft.title ?? "");
|
||||
setIcon(draft.icon ?? "");
|
||||
setRole((VALID_CAPABILITIES.has(draft.role) ? draft.role : "custom") as AgentCapability);
|
||||
setReportsTo(draft.reportsTo ?? "");
|
||||
setInstructionsText(draft.instructionsText ?? "");
|
||||
setHeartbeatProcedurePath(draft.heartbeatProcedurePath ?? "");
|
||||
setSoul(draft.soul ?? "");
|
||||
setMemory(draft.memory ?? "");
|
||||
setSelectedSkills(Array.isArray(draft.skills) ? draft.skills : []);
|
||||
setName(values.name);
|
||||
setTitle(values.title ?? "");
|
||||
setIcon(values.icon ?? "");
|
||||
setRole(values.role);
|
||||
setReportsTo(values.reportsTo ?? "");
|
||||
setInstructionsText(values.instructionsText ?? "");
|
||||
setHeartbeatProcedurePath(values.heartbeatProcedurePath ?? "");
|
||||
setSoul(values.soul ?? "");
|
||||
setMemory(values.memory ?? "");
|
||||
setSelectedSkills(values.skills ?? []);
|
||||
setRuntimeConfig((current) => ({
|
||||
...current,
|
||||
model: runtimeHint ? "" : modelSelection,
|
||||
thinkingLevel: draft.thinkingLevel ?? current.thinkingLevel,
|
||||
maxTurns: draft.maxTurns ?? current.maxTurns,
|
||||
model: values.model ?? "",
|
||||
thinkingLevel: values.thinkingLevel ?? current.thinkingLevel,
|
||||
maxTurns: values.maxTurns ?? current.maxTurns,
|
||||
}));
|
||||
if (runtimeHint) {
|
||||
if (values.runtimeHint) {
|
||||
setRuntimeMode("runtime");
|
||||
setSelectedRuntimeId(runtimeHint);
|
||||
setSelectedRuntimeId(values.runtimeHint);
|
||||
} else {
|
||||
setRuntimeMode("model");
|
||||
setSelectedRuntimeId("");
|
||||
@@ -283,28 +285,23 @@ export function NewAgentDialog({
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const runtimeCfg: Record<string, unknown> = {};
|
||||
if (runtimeMode === "runtime") {
|
||||
if (selectedRuntimeId.trim()) runtimeCfg.runtimeHint = selectedRuntimeId.trim();
|
||||
} else if (runtimeConfig.model.trim()) {
|
||||
runtimeCfg.model = runtimeConfig.model.trim();
|
||||
}
|
||||
if (runtimeConfig.thinkingLevel !== "off") runtimeCfg.thinkingLevel = runtimeConfig.thinkingLevel;
|
||||
if (runtimeConfig.maxTurns !== 1000) runtimeCfg.maxTurns = runtimeConfig.maxTurns;
|
||||
await createAgent({
|
||||
name: name.trim(),
|
||||
await createAgent(buildAgentCreatePayload({
|
||||
name,
|
||||
role,
|
||||
...(title.trim() ? { title: title.trim() } : {}),
|
||||
...(icon.trim() ? { icon: icon.trim() } : {}),
|
||||
...(reportsTo.trim() ? { reportsTo: reportsTo.trim() } : {}),
|
||||
...(instructionsPath.trim() ? { instructionsPath: instructionsPath.trim() } : {}),
|
||||
...(instructionsText.trim() ? { instructionsText: instructionsText.trim() } : {}),
|
||||
...(heartbeatProcedurePath.trim() ? { heartbeatProcedurePath: heartbeatProcedurePath.trim() } : {}),
|
||||
...(soul.trim() ? { soul: soul.trim() } : {}),
|
||||
...(memory.trim() ? { memory: memory.trim() } : {}),
|
||||
...(Object.keys(runtimeCfg).length > 0 ? { runtimeConfig: runtimeCfg } : {}),
|
||||
...(selectedSkills.length > 0 ? { metadata: { skills: selectedSkills } } : {}),
|
||||
}, projectId);
|
||||
title,
|
||||
icon,
|
||||
reportsTo,
|
||||
instructionsPath,
|
||||
instructionsText,
|
||||
heartbeatProcedurePath,
|
||||
soul,
|
||||
memory,
|
||||
model: runtimeMode === "model" ? runtimeConfig.model : "",
|
||||
runtimeHint: runtimeMode === "runtime" ? selectedRuntimeId : "",
|
||||
thinkingLevel: runtimeConfig.thinkingLevel,
|
||||
maxTurns: runtimeConfig.maxTurns,
|
||||
skills: selectedSkills,
|
||||
}), projectId);
|
||||
handleClose();
|
||||
onCreated();
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -314,6 +314,186 @@
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-intro {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(280px, 0.95fr);
|
||||
gap: var(--space-lg);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-section-heading {
|
||||
margin-bottom: var(--space-sm);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset-list {
|
||||
display: grid;
|
||||
gap: var(--space-sm);
|
||||
max-height: min(360px, 48dvh);
|
||||
overflow-y: auto;
|
||||
padding-right: var(--space-xs);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: var(--space-sm);
|
||||
width: 100%;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset:hover {
|
||||
background: var(--card-hover);
|
||||
border-color: var(--text-dim);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--todo);
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset.selected {
|
||||
background: color-mix(in srgb, var(--todo) 8%, transparent);
|
||||
border-color: var(--todo);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--todo) 25%, transparent);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset-description {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-title-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-title-row h3,
|
||||
.setup-wizard-agent-preview-title-row p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-title-row h3 {
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-title-row p {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-list {
|
||||
display: grid;
|
||||
gap: var(--space-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-list div {
|
||||
display: grid;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-list dt {
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preview-list dd {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
max-height: 96px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-ai-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-interview-error {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: var(--space-xl);
|
||||
z-index: var(--z-modal, 1000);
|
||||
width: min(520px, calc(100vw - (var(--space-lg) * 2)));
|
||||
transform: translateX(-50%);
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setup-wizard-isolation-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -465,6 +645,16 @@
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.setup-wizard-agent-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.setup-wizard-agent-preset-list {
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.setup-wizard-isolation-option {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
}
|
||||
@@ -506,4 +696,3 @@
|
||||
/* =========================================================================
|
||||
Agent Manager UI — Global CSS Classes
|
||||
========================================================================= */
|
||||
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
import "./SetupWizardModal.css";
|
||||
import { useState, useCallback } from "react";
|
||||
import { X, Loader2, CheckCircle, ChevronRight } from "lucide-react";
|
||||
import { lazy, Suspense, useState, useCallback, useMemo, useRef, useEffect, type KeyboardEvent } from "react";
|
||||
import { X, Loader2, CheckCircle, ChevronRight, Sparkles } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ProjectInfo, ProjectCreateInput } from "../api";
|
||||
import { registerProject } from "../api";
|
||||
import type { AgentOnboardingSummary, ProjectInfo, ProjectCreateInput } from "../api";
|
||||
import { createAgent, registerProject } from "../api";
|
||||
import { getAuthToken, setAuthToken, clearAuthToken } from "../auth";
|
||||
import { DirectoryPicker } from "./DirectoryPicker";
|
||||
import { suggestProjectName } from "../utils/projectDetection";
|
||||
import { useNodes } from "../hooks/useNodes";
|
||||
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 })),
|
||||
);
|
||||
|
||||
export interface SetupWizardModalProps {
|
||||
/** Called when a single project is registered */
|
||||
/** Called when first-run setup should enter the registered project. */
|
||||
onProjectRegistered: (project: ProjectInfo) => void;
|
||||
/** Called when wizard is closed (completed or cancelled) */
|
||||
onClose?: () => void;
|
||||
/** Enables the existing AI interview entry point for first-agent drafting. */
|
||||
agentOnboardingEnabled?: boolean;
|
||||
}
|
||||
|
||||
type WizardStep = "auth" | "manual" | "complete";
|
||||
type WizardStep = "auth" | "manual" | "agent" | "complete";
|
||||
type ManualSetupMode = "existing" | "clone";
|
||||
type AgentOutcome = "created" | "skipped" | null;
|
||||
|
||||
interface WizardState {
|
||||
step: WizardStep;
|
||||
@@ -27,8 +43,14 @@ interface WizardState {
|
||||
manualName: string;
|
||||
manualIsolationMode: "in-process" | "child-process";
|
||||
manualNodeId: string;
|
||||
registeredProject: ProjectInfo | null;
|
||||
selectedPresetId: string;
|
||||
agentDraft: AgentDraftValues;
|
||||
isCreatingAgent: boolean;
|
||||
isRegistering: boolean;
|
||||
error: string | null;
|
||||
agentError: string | null;
|
||||
agentOutcome: AgentOutcome;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,9 +62,19 @@ interface WizardState {
|
||||
export function SetupWizardModal({
|
||||
onProjectRegistered,
|
||||
onClose,
|
||||
agentOnboardingEnabled = false,
|
||||
}: SetupWizardModalProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const helpUrl = "https://github.com/runfusion/fusion/discussions";
|
||||
/*
|
||||
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.
|
||||
The AI interview entry point is feature-flagged by `agentOnboardingEnabled`; preset creation and skip remain available without it.
|
||||
*/
|
||||
const ceoPreset = useMemo(
|
||||
() => getPresetById("ceo") ?? AGENT_PRESETS[0]!,
|
||||
[],
|
||||
);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [state, setState] = useState<WizardState>(() => ({
|
||||
step: getAuthToken() ? "manual" : "auth",
|
||||
@@ -52,12 +84,20 @@ export function SetupWizardModal({
|
||||
manualName: "",
|
||||
manualIsolationMode: "in-process",
|
||||
manualNodeId: "",
|
||||
registeredProject: null,
|
||||
selectedPresetId: ceoPreset.id,
|
||||
agentDraft: mapPresetToAgentDraft(ceoPreset),
|
||||
isCreatingAgent: false,
|
||||
isRegistering: false,
|
||||
error: null,
|
||||
agentError: null,
|
||||
agentOutcome: null,
|
||||
}));
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [authTokenInput, setAuthTokenInput] = useState("");
|
||||
const [storedAuthToken, setStoredAuthToken] = useState(() => getAuthToken());
|
||||
const [isInterviewOpen, setIsInterviewOpen] = useState(false);
|
||||
const agentErrorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { nodes, loading: nodesLoading } = useNodes();
|
||||
const localNodeId = nodes.find((n) => n.type === "local")?.id;
|
||||
@@ -67,6 +107,20 @@ export function SetupWizardModal({
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
const handleFinish = useCallback(() => {
|
||||
if (state.registeredProject) {
|
||||
onProjectRegistered(state.registeredProject);
|
||||
return;
|
||||
}
|
||||
handleClose();
|
||||
}, [handleClose, onProjectRegistered, state.registeredProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.agentError) {
|
||||
agentErrorRef.current?.focus();
|
||||
}
|
||||
}, [state.agentError]);
|
||||
|
||||
const handlePathChange = useCallback((path: string) => {
|
||||
setState((prev) => {
|
||||
const updates: Partial<WizardState> = { manualPath: path };
|
||||
@@ -98,11 +152,11 @@ export function SetupWizardModal({
|
||||
};
|
||||
|
||||
const result = await registerProject(input);
|
||||
onProjectRegistered(result);
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
step: "complete",
|
||||
step: "agent",
|
||||
registeredProject: result,
|
||||
isRegistering: false,
|
||||
}));
|
||||
} catch (err) {
|
||||
@@ -112,7 +166,7 @@ export function SetupWizardModal({
|
||||
error: err instanceof Error ? err.message : "Failed to register project",
|
||||
}));
|
||||
}
|
||||
}, [state.manualPath, state.manualName, state.manualCloneUrl, state.manualMode, state.manualIsolationMode, state.manualNodeId, onProjectRegistered]);
|
||||
}, [state.manualPath, state.manualName, state.manualCloneUrl, state.manualMode, state.manualIsolationMode, state.manualNodeId]);
|
||||
|
||||
const handleSetAuthToken = useCallback(() => {
|
||||
const token = authTokenInput.trim();
|
||||
@@ -134,6 +188,81 @@ export function SetupWizardModal({
|
||||
setState((prev) => ({ ...prev, step: "manual" }));
|
||||
}, []);
|
||||
|
||||
const handlePresetSelect = useCallback((presetId: string) => {
|
||||
const preset = getPresetById(presetId);
|
||||
if (!preset) return;
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selectedPresetId: preset.id,
|
||||
agentDraft: mapPresetToAgentDraft(preset),
|
||||
agentError: null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handlePresetKeyDown = 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];
|
||||
handlePresetSelect(nextPreset.id);
|
||||
requestAnimationFrame(() => {
|
||||
document.querySelector<HTMLButtonElement>(`[data-agent-preset-id="${nextPreset.id}"]`)?.focus();
|
||||
});
|
||||
}, [handlePresetSelect]);
|
||||
|
||||
|
||||
const handleApplyAgentDraft = useCallback((draft: AgentOnboardingSummary) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
selectedPresetId: "",
|
||||
agentDraft: mapOnboardingSummaryToAgentDraft(draft),
|
||||
agentError: null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleCreateFirstAgent = useCallback(async () => {
|
||||
if (!state.registeredProject || !state.agentDraft.name.trim()) return;
|
||||
setState((prev) => ({ ...prev, isCreatingAgent: true, agentError: null }));
|
||||
try {
|
||||
await createAgent(buildAgentCreatePayload(state.agentDraft), state.registeredProject.id);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
step: "complete",
|
||||
isCreatingAgent: false,
|
||||
agentOutcome: "created",
|
||||
}));
|
||||
} catch (err) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isCreatingAgent: false,
|
||||
agentError: err instanceof Error ? err.message : t("setup.firstAgentCreateError", "Failed to create agent"),
|
||||
}));
|
||||
}
|
||||
}, [state.agentDraft, state.registeredProject, t]);
|
||||
|
||||
const handleSkipAgent = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
step: "complete",
|
||||
agentError: null,
|
||||
agentOutcome: "skipped",
|
||||
}));
|
||||
}, []);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const isExistingMode = state.manualMode === "existing";
|
||||
@@ -145,6 +274,10 @@ export function SetupWizardModal({
|
||||
|| !hasPath
|
||||
|| !hasName
|
||||
|| (isCloneMode && !hasCloneUrl);
|
||||
const selectedPreset = state.selectedPresetId
|
||||
? getPresetById(state.selectedPresetId)
|
||||
: undefined;
|
||||
const isAgentActionDisabled = state.isCreatingAgent;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open setup-wizard-overlay" role="dialog" aria-modal="true" aria-labelledby="wizard-title">
|
||||
@@ -179,10 +312,11 @@ export function SetupWizardModal({
|
||||
<h2 id="wizard-title" className="setup-wizard-title">
|
||||
{state.step === "auth" && t("setup.setAuthToken", "Set Auth Token")}
|
||||
{state.step === "manual" && t("setup.welcomeToFusion", "Welcome to Fusion")}
|
||||
{state.step === "agent" && t("setup.firstAgentTitle", "Create your first agent")}
|
||||
{state.step === "complete" && t("setup.setupCompleteTitle", "Setup Complete!")}
|
||||
</h2>
|
||||
</div>
|
||||
{state.step !== "complete" && (
|
||||
{state.step !== "complete" && state.step !== "agent" && (
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={handleClose}
|
||||
@@ -427,6 +561,94 @@ export function SetupWizardModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* FNXC:Onboarding 2026-06-22-03:11: First-run setup asks for an optional persistent coordinating agent after project registration. Users can skip it because task creation and task execution do not require an assigned persistent agent; Fusion automatically spawns temporary planning, execution, review, and merge agents for task work. */}
|
||||
{state.step === "agent" && (
|
||||
<div className="setup-wizard-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>
|
||||
|
||||
<div className="setup-wizard-agent-layout">
|
||||
<section className="setup-wizard-agent-presets" aria-labelledby="setup-first-agent-presets-heading">
|
||||
<div className="setup-wizard-agent-section-heading" id="setup-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 = state.selectedPresetId === 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-agent-preset-id={preset.id}
|
||||
disabled={isAgentActionDisabled}
|
||||
onClick={() => handlePresetSelect(preset.id)}
|
||||
onKeyDown={(event) => handlePresetKeyDown(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="setup-first-agent-preview-heading">
|
||||
<div className="setup-wizard-agent-section-heading" id="setup-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: state.selectedPresetId || "draft", icon: state.agentDraft.icon, name: state.agentDraft.name }} size={36} />
|
||||
<div>
|
||||
<h3>{state.agentDraft.name || t("setup.firstAgentDraftName", "Draft agent")}</h3>
|
||||
<p>{state.agentDraft.title || selectedPreset?.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>{state.agentDraft.role}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("agents.fieldInstructionsText", "Inline Instructions")}</dt>
|
||||
<dd>{state.agentDraft.instructionsText || t("setup.firstAgentNoInstructions", "No inline instructions yet")}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{agentOnboardingEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn setup-wizard-agent-ai-btn"
|
||||
onClick={() => setIsInterviewOpen(true)}
|
||||
disabled={isAgentActionDisabled}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
<span>{t("agents.aiInterview", "AI Interview")}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{state.agentError && (
|
||||
<div className="wizard-error" role="alert" tabIndex={-1} ref={agentErrorRef}>
|
||||
{state.agentError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Complete Step */}
|
||||
{state.step === "complete" && (
|
||||
<div className="setup-wizard-complete">
|
||||
@@ -436,8 +658,16 @@ export function SetupWizardModal({
|
||||
</div>
|
||||
<CheckCircle size={64} className="success-icon" />
|
||||
<h3>{t("setup.allSet", "All Set!")}</h3>
|
||||
<p>{t("setup.projectRegisteredSuccess", "Your project has been registered successfully.")}</p>
|
||||
<p>{t("setup.addMoreProjectsHint", "You can add more projects anytime from the project overview.")}</p>
|
||||
<p>
|
||||
{state.agentOutcome === "created"
|
||||
? t("setup.firstAgentCreatedSuccess", "Your project is registered and your first agent is ready.")
|
||||
: t("setup.projectRegisteredSuccess", "Your project has been registered successfully.")}
|
||||
</p>
|
||||
<p>
|
||||
{state.agentOutcome === "skipped"
|
||||
? t("setup.firstAgentSkippedHint", "You can create agents later from the Agents view.")
|
||||
: t("setup.addMoreProjectsHint", "You can add more projects anytime from the project overview.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -486,14 +716,65 @@ export function SetupWizardModal({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{state.step === "agent" && (
|
||||
<>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleSkipAgent}
|
||||
disabled={isAgentActionDisabled}
|
||||
>
|
||||
{t("setup.skipFirstAgent", "Skip for now")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => void handleCreateFirstAgent()}
|
||||
disabled={isAgentActionDisabled || !state.agentDraft.name.trim()}
|
||||
aria-busy={state.isCreatingAgent}
|
||||
>
|
||||
{state.isCreatingAgent ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>{t("setup.creatingFirstAgent", "Creating agent...")}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t("setup.createFirstAgent", "Create Agent")}</span>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.step === "complete" && (
|
||||
<button className="btn btn-primary" onClick={handleClose}>
|
||||
<button className="btn btn-primary" onClick={handleFinish}>
|
||||
<CheckCircle size={16} />
|
||||
<span>{t("setup.getStarted", "Get Started")}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{agentOnboardingEnabled && isInterviewOpen && (
|
||||
<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={() => setIsInterviewOpen(false)}>
|
||||
{t("setup.firstAgentContinueWithTemplates", "Continue with templates")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<ExperimentalAgentOnboardingModal
|
||||
isOpen={isInterviewOpen}
|
||||
onClose={() => setIsInterviewOpen(false)}
|
||||
onUseDraft={handleApplyAgentDraft}
|
||||
projectId={state.registeredProject?.id}
|
||||
existingAgents={[]}
|
||||
mode="create"
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { SetupWizardModal } from "../SetupWizardModal";
|
||||
import { AGENT_PRESETS } from "../agent-presets";
|
||||
import { buildAgentCreatePayload, mapPresetToAgentDraft } from "../agent-presets/agentCreatePayload";
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock("lucide-react", async () => {
|
||||
@@ -38,6 +40,7 @@ vi.mock("../../hooks/useNodes", () => ({
|
||||
// Mock api module
|
||||
vi.mock("../../api", () => ({
|
||||
registerProject: vi.fn(),
|
||||
createAgent: vi.fn(),
|
||||
browseDirectory: vi.fn().mockResolvedValue({
|
||||
currentPath: "/home/user",
|
||||
parentPath: "/home",
|
||||
@@ -45,27 +48,97 @@ vi.mock("../../api", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../ExperimentalAgentOnboardingModal", () => ({
|
||||
ExperimentalAgentOnboardingModal: ({ isOpen, onClose, onUseDraft }: { isOpen: boolean; onClose: () => void; onUseDraft: (draft: any) => void }) => (
|
||||
isOpen ? (
|
||||
<div data-testid="agent-interview-modal">
|
||||
AI Interview Modal
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onUseDraft({
|
||||
name: "Launch Coordinator",
|
||||
title: "Launch Planning Agent",
|
||||
icon: "◇",
|
||||
role: "not-a-real-role",
|
||||
instructionsText: "Coordinate launch tasks.",
|
||||
soul: "Strategic launch planner.",
|
||||
skills: ["planning", "review"],
|
||||
runtimeHint: "codex-local",
|
||||
maxTurns: 24,
|
||||
thinkingLevel: "medium",
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Use Draft
|
||||
</button>
|
||||
</div>
|
||||
) : null
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../../auth", () => ({
|
||||
getAuthToken: vi.fn(() => undefined),
|
||||
setAuthToken: vi.fn(),
|
||||
clearAuthToken: vi.fn(),
|
||||
}));
|
||||
|
||||
import { registerProject } from "../../api";
|
||||
import { createAgent, registerProject } from "../../api";
|
||||
import { getAuthToken, setAuthToken, clearAuthToken } from "../../auth";
|
||||
import { useNodes } from "../../hooks/useNodes";
|
||||
|
||||
const mockRegisterProject = vi.mocked(registerProject);
|
||||
const mockCreateAgent = vi.mocked(createAgent);
|
||||
const mockGetAuthToken = vi.mocked(getAuthToken);
|
||||
const mockSetAuthToken = vi.mocked(setAuthToken);
|
||||
const mockClearAuthToken = vi.mocked(clearAuthToken);
|
||||
const mockUseNodes = vi.mocked(useNodes);
|
||||
|
||||
function buildMockProject(overrides = {}) {
|
||||
return {
|
||||
id: "proj_123",
|
||||
name: "test-project",
|
||||
path: "/home/user/project",
|
||||
status: "active" as const,
|
||||
isolationMode: "in-process" as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockAgent(overrides = {}) {
|
||||
return {
|
||||
id: "agent_123",
|
||||
projectId: "proj_123",
|
||||
name: "Agent",
|
||||
role: "custom",
|
||||
status: "active",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function registerProjectFromWizard() {
|
||||
fireEvent.change(screen.getByPlaceholderText("/path/to/your/project"), {
|
||||
target: { value: "/home/user/project" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("my-project"), {
|
||||
target: { value: "test-project" },
|
||||
});
|
||||
fireEvent.click(screen.getByText("Register Project"));
|
||||
return screen.findByText("Create your first agent");
|
||||
}
|
||||
|
||||
describe("SetupWizardModal", () => {
|
||||
let reloadMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockRegisterProject.mockReset();
|
||||
mockCreateAgent.mockReset();
|
||||
// Default: auth token is stored so wizard starts on the project form step.
|
||||
// Tests for the auth step explicitly unset this.
|
||||
mockGetAuthToken.mockReturnValue("stored-token");
|
||||
@@ -261,6 +334,7 @@ describe("SetupWizardModal", () => {
|
||||
});
|
||||
|
||||
it("clone mode submit sends cloneUrl payload", async () => {
|
||||
const onProjectRegistered = vi.fn();
|
||||
mockRegisterProject.mockResolvedValueOnce({
|
||||
id: "proj_clone",
|
||||
name: "fusion",
|
||||
@@ -273,7 +347,7 @@ describe("SetupWizardModal", () => {
|
||||
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={vi.fn()}
|
||||
onProjectRegistered={onProjectRegistered}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
@@ -298,6 +372,9 @@ describe("SetupWizardModal", () => {
|
||||
cloneUrl: "https://github.com/runfusion/fusion.git",
|
||||
});
|
||||
});
|
||||
expect(await screen.findByText("Create your first agent")).toBeDefined();
|
||||
expect(screen.getByRole("radio", { name: "CEO selected" })).toHaveAttribute("aria-checked", "true");
|
||||
expect(onProjectRegistered).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("register button disabled/enabled logic is mode-aware", () => {
|
||||
@@ -377,16 +454,8 @@ describe("SetupWizardModal", () => {
|
||||
expect(await screen.findByText("Path does not exist")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows completion state after successful registration", async () => {
|
||||
const mockProject = {
|
||||
id: "proj_123",
|
||||
name: "test-project",
|
||||
path: "/home/user/project",
|
||||
status: "active" as const,
|
||||
isolationMode: "in-process" as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
it("shows optional first-agent step after successful registration", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
|
||||
const onProjectRegistered = vi.fn();
|
||||
@@ -397,24 +466,245 @@ describe("SetupWizardModal", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("/path/to/your/project"), {
|
||||
target: { value: "/home/user/project" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("my-project"), {
|
||||
target: { value: "test-project" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Register Project"));
|
||||
await registerProjectFromWizard();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRegisterProject).toHaveBeenCalled();
|
||||
});
|
||||
expect(await screen.findByText("All Set!")).toBeDefined();
|
||||
expect(await screen.findByText("Get Started")).toBeDefined();
|
||||
expect(screen.getByText(/You do not need to create or assign an agent/)).toBeDefined();
|
||||
expect(screen.getByRole("radio", { name: "CEO selected" })).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.getByText("Create Agent")).toBeDefined();
|
||||
expect(screen.getByText("Skip for now")).toBeDefined();
|
||||
expect(screen.queryByText("AI Interview")).toBeNull();
|
||||
|
||||
expect(onProjectRegistered).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can skip first-agent creation and finish setup", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
|
||||
const onProjectRegistered = vi.fn();
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={onProjectRegistered}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByText("Skip for now"));
|
||||
expect(await screen.findByText("All Set!")).toBeDefined();
|
||||
expect(screen.getByText("You can create agents later from the Agents view.")).toBeDefined();
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByText("Get Started"));
|
||||
expect(onProjectRegistered).toHaveBeenCalledWith(mockProject);
|
||||
});
|
||||
|
||||
it("does not show an ambiguous close button on the first-agent step", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
|
||||
const onProjectRegistered = vi.fn();
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={onProjectRegistered}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
|
||||
expect(screen.queryByLabelText("Close wizard")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByText("Skip for now"));
|
||||
expect(await screen.findByText("All Set!")).toBeDefined();
|
||||
expect(screen.getByText("You can create agents later from the Agents view.")).toBeDefined();
|
||||
expect(onProjectRegistered).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByText("Get Started"));
|
||||
expect(onProjectRegistered).toHaveBeenCalledWith(mockProject);
|
||||
});
|
||||
|
||||
it("creates the default CEO agent before finishing setup", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
mockCreateAgent.mockResolvedValueOnce(buildMockAgent({
|
||||
id: "agent_ceo",
|
||||
projectId: mockProject.id,
|
||||
name: "CEO",
|
||||
}) as any);
|
||||
|
||||
const onProjectRegistered = vi.fn();
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={onProjectRegistered}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByText("Create Agent"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "CEO",
|
||||
role: "custom",
|
||||
title: "Oversees project strategy, sets priorities, and coordinates between departments to ensure alignment with business goals.",
|
||||
}),
|
||||
mockProject.id,
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText("Your project is registered and your first agent is ready.")).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByText("Get Started"));
|
||||
expect(onProjectRegistered).toHaveBeenCalledWith(mockProject);
|
||||
});
|
||||
|
||||
it("creates the selected non-CEO preset with the shared payload mapping", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
const engineerPreset = AGENT_PRESETS.find((preset) => preset.id === "engineer")!;
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
mockCreateAgent.mockResolvedValueOnce(buildMockAgent({
|
||||
id: "agent_engineer",
|
||||
projectId: mockProject.id,
|
||||
name: engineerPreset.name,
|
||||
role: engineerPreset.role,
|
||||
}) as any);
|
||||
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
fireEvent.click(screen.getByRole("radio", { name: engineerPreset.name }));
|
||||
|
||||
expect(screen.getByRole("radio", { name: `${engineerPreset.name} selected` })).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.getAllByText(engineerPreset.description!).length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
fireEvent.click(screen.getByText("Create Agent"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledWith(
|
||||
buildAgentCreatePayload(mapPresetToAgentDraft(engineerPreset)),
|
||||
mockProject.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("supports arrow-key navigation in the first-agent template radio group", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
const ceoOption = screen.getByRole("radio", { name: "CEO selected" });
|
||||
ceoOption.focus();
|
||||
|
||||
fireEvent.keyDown(ceoOption, { key: "ArrowDown" });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("radio", { name: "CTO selected" })).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
fireEvent.keyDown(screen.getByRole("radio", { name: "CTO selected" }), { key: "End" });
|
||||
const lastPreset = AGENT_PRESETS[AGENT_PRESETS.length - 1]!;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("radio", { name: `${lastPreset.name} selected` })).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps first-agent step available when agent creation fails", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
const onProjectRegistered = vi.fn();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
mockCreateAgent.mockRejectedValueOnce(new Error("Agent quota reached"));
|
||||
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={onProjectRegistered}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
fireEvent.click(screen.getByText("Create Agent"));
|
||||
|
||||
const alert = await screen.findByRole("alert");
|
||||
expect(alert).toHaveTextContent("Agent quota reached");
|
||||
expect(document.activeElement).toBe(alert);
|
||||
expect(screen.getByText("Create Agent")).toBeDefined();
|
||||
expect(screen.getByText("Skip for now")).toBeDefined();
|
||||
expect(screen.getByRole("radio", { name: "CEO selected" })).toHaveAttribute("aria-checked", "true");
|
||||
expect(onProjectRegistered).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByText("Skip for now"));
|
||||
expect(await screen.findByText("You can create agents later from the Agents view.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("applies an AI interview draft and waits for explicit creation", async () => {
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
mockCreateAgent.mockResolvedValueOnce(buildMockAgent({
|
||||
id: "agent_launch",
|
||||
projectId: mockProject.id,
|
||||
name: "Launch Coordinator",
|
||||
}) as any);
|
||||
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
agentOnboardingEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await registerProjectFromWizard()).toBeDefined();
|
||||
expect(screen.getByText("AI Interview")).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByText("AI Interview"));
|
||||
expect(await screen.findByTestId("agent-interview-modal")).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByText("Use Draft"));
|
||||
|
||||
expect(await screen.findByText("Launch Coordinator")).toBeDefined();
|
||||
expect(screen.getByText("Launch Planning Agent")).toBeDefined();
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByText("Create Agent"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "Launch Coordinator",
|
||||
role: "custom",
|
||||
title: "Launch Planning Agent",
|
||||
instructionsText: "Coordinate launch tasks.",
|
||||
soul: "Strategic launch planner.",
|
||||
metadata: { skills: ["planning", "review"] },
|
||||
runtimeConfig: {
|
||||
runtimeHint: "codex-local",
|
||||
maxTurns: 24,
|
||||
thinkingLevel: "medium",
|
||||
},
|
||||
}),
|
||||
mockProject.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("close button calls onClose", () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
@@ -605,15 +895,7 @@ describe("SetupWizardModal", () => {
|
||||
healthCheck: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockProject = {
|
||||
id: "proj_123",
|
||||
name: "test-project",
|
||||
path: "/home/user/project",
|
||||
status: "active" as const,
|
||||
isolationMode: "in-process" as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
|
||||
render(
|
||||
@@ -664,15 +946,7 @@ describe("SetupWizardModal", () => {
|
||||
healthCheck: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockProject = {
|
||||
id: "proj_123",
|
||||
name: "test-project",
|
||||
path: "/home/user/project",
|
||||
status: "active" as const,
|
||||
isolationMode: "in-process" as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const mockProject = buildMockProject();
|
||||
mockRegisterProject.mockResolvedValueOnce(mockProject);
|
||||
|
||||
render(
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { AgentCapability, AgentCreateInput, AgentOnboardingSummary } from "../../api";
|
||||
import type { AgentPreset } from "./index";
|
||||
|
||||
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
|
||||
export const VALID_AGENT_CAPABILITIES = new Set<string>(["triage", "executor", "reviewer", "merger", "scheduler", "engineer", "custom"]);
|
||||
|
||||
export interface AgentDraftValues {
|
||||
name: string;
|
||||
role: AgentCapability;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
reportsTo?: string;
|
||||
instructionsPath?: string;
|
||||
instructionsText?: string;
|
||||
heartbeatProcedurePath?: string;
|
||||
soul?: string;
|
||||
memory?: string;
|
||||
model?: string;
|
||||
runtimeHint?: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
maxTurns?: number;
|
||||
skills?: string[];
|
||||
}
|
||||
|
||||
export function mapPresetToAgentDraft(preset: AgentPreset): AgentDraftValues {
|
||||
return {
|
||||
name: preset.name,
|
||||
role: preset.role,
|
||||
title: preset.description ?? preset.title,
|
||||
icon: preset.icon,
|
||||
soul: preset.soul ?? "",
|
||||
instructionsText: preset.instructionsText ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export function mapOnboardingSummaryToAgentDraft(draft: AgentOnboardingSummary): AgentDraftValues {
|
||||
const runtimeHint = draft.runtimeHint?.trim() ?? "";
|
||||
const modelSelection = draft.model?.trim() || draft.modelHint?.trim() || "";
|
||||
|
||||
return {
|
||||
name: draft.name ?? "",
|
||||
title: draft.title ?? "",
|
||||
icon: draft.icon ?? "",
|
||||
role: (VALID_AGENT_CAPABILITIES.has(draft.role) ? draft.role : "custom") as AgentCapability,
|
||||
reportsTo: draft.reportsTo ?? "",
|
||||
instructionsText: draft.instructionsText ?? "",
|
||||
heartbeatProcedurePath: draft.heartbeatProcedurePath ?? "",
|
||||
soul: draft.soul ?? "",
|
||||
memory: draft.memory ?? "",
|
||||
skills: Array.isArray(draft.skills) ? draft.skills : [],
|
||||
model: runtimeHint ? "" : modelSelection,
|
||||
runtimeHint,
|
||||
thinkingLevel: draft.thinkingLevel ?? undefined,
|
||||
maxTurns: draft.maxTurns ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAgentCreatePayload(values: AgentDraftValues): AgentCreateInput {
|
||||
const runtimeCfg: Record<string, unknown> = {};
|
||||
if (values.runtimeHint?.trim()) {
|
||||
runtimeCfg.runtimeHint = values.runtimeHint.trim();
|
||||
} else if (values.model?.trim()) {
|
||||
runtimeCfg.model = values.model.trim();
|
||||
}
|
||||
if (values.thinkingLevel && values.thinkingLevel !== "off") runtimeCfg.thinkingLevel = values.thinkingLevel;
|
||||
if (values.maxTurns !== undefined && values.maxTurns !== 1000) runtimeCfg.maxTurns = values.maxTurns;
|
||||
|
||||
return {
|
||||
name: values.name.trim(),
|
||||
role: values.role,
|
||||
...(values.title?.trim() ? { title: values.title.trim() } : {}),
|
||||
...(values.icon?.trim() ? { icon: values.icon.trim() } : {}),
|
||||
...(values.reportsTo?.trim() ? { reportsTo: values.reportsTo.trim() } : {}),
|
||||
...(values.instructionsPath?.trim() ? { instructionsPath: values.instructionsPath.trim() } : {}),
|
||||
...(values.instructionsText?.trim() ? { instructionsText: values.instructionsText.trim() } : {}),
|
||||
...(values.heartbeatProcedurePath?.trim() ? { heartbeatProcedurePath: values.heartbeatProcedurePath.trim() } : {}),
|
||||
...(values.soul?.trim() ? { soul: values.soul.trim() } : {}),
|
||||
...(values.memory?.trim() ? { memory: values.memory.trim() } : {}),
|
||||
...(Object.keys(runtimeCfg).length > 0 ? { runtimeConfig: runtimeCfg } : {}),
|
||||
...(values.skills && values.skills.length > 0 ? { metadata: { skills: values.skills } } : {}),
|
||||
};
|
||||
}
|
||||
@@ -6550,6 +6550,7 @@
|
||||
"copiedCodeToClipboard": "Copied code to clipboard",
|
||||
"copyCode": "Copy code",
|
||||
"couldNotReachServer": "Could not reach the server. Check your connection and try again.",
|
||||
"createFirstAgent": "Create Agent",
|
||||
"createFirstTask": "Create First Task",
|
||||
"createNewTask": "Create a New Task",
|
||||
"createNewTaskSubtitle": "Describe what you need built and AI will work on it",
|
||||
@@ -6557,6 +6558,7 @@
|
||||
"createTasksAnytimeNote": "You can create tasks anytime from the board, or use",
|
||||
"createTasksAnytimeNoteTerminal": "in the terminal.",
|
||||
"creating": "Creating...",
|
||||
"creatingFirstAgent": "Creating agent...",
|
||||
"creatingTask": "Creating task…",
|
||||
"cursorCli": {
|
||||
"active": "✓ Active",
|
||||
@@ -6593,6 +6595,18 @@
|
||||
"failedToSaveShellConnection": "Failed to save shell connection",
|
||||
"failedToSubmitAuthCode": "Failed to submit authorization code",
|
||||
"finishSetup": "Finish Setup",
|
||||
"firstAgentCreateError": "Failed to create agent",
|
||||
"firstAgentCreatedSuccess": "Your project is registered and your first agent is ready.",
|
||||
"firstAgentCustomDraft": "Custom agent draft",
|
||||
"firstAgentDraftName": "Draft agent",
|
||||
"firstAgentContinueWithTemplates": "Continue with templates",
|
||||
"firstAgentInterviewLoadError": "AI interview could not load. You can still create an agent from a template or skip this step.",
|
||||
"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.",
|
||||
"firstAgentNoInstructions": "No inline instructions yet",
|
||||
"firstAgentPreview": "Preview",
|
||||
"firstAgentSkippedHint": "You can create agents later from the Agents view.",
|
||||
"firstAgentTemplates": "Templates",
|
||||
"firstAgentTitle": "Create your first agent",
|
||||
"firstTaskDescription": "Create your first task to start the board and launch AI execution.",
|
||||
"firstTaskPlaceholder": "Example: Build a login page with email and password",
|
||||
"firstTaskReady": "Your first task is ready!",
|
||||
@@ -6721,6 +6735,7 @@
|
||||
"savingRemoteServer": "Saving…",
|
||||
"selectDefaultModel": "Select a default model…",
|
||||
"selectDefaultModelDesc": "Choose a default AI model for task execution",
|
||||
"selectedAgentTemplate": "{{name}} selected",
|
||||
"selectedModel": "Selected:",
|
||||
"serverUrl": "Server URL",
|
||||
"serverUrlPlaceholder": "https://your-fusion-host",
|
||||
@@ -6733,6 +6748,7 @@
|
||||
"setUpProject": "Set Up Project",
|
||||
"setupWizardHint": "In the setup wizard, pick an existing directory or paste a GitHub clone URL.",
|
||||
"skip": "Skip",
|
||||
"skipFirstAgent": "Skip for now",
|
||||
"skipForNow": "Skip for now",
|
||||
"skipGitHub": "Skip GitHub →",
|
||||
"skipOnboardingAriaLabel": "Skip onboarding",
|
||||
|
||||
@@ -6550,6 +6550,7 @@
|
||||
"copiedCodeToClipboard": "Código copiado al portapapeles",
|
||||
"copyCode": "Copiar código",
|
||||
"couldNotReachServer": "No se pudo conectar al servidor. Comprueba tu conexión e inténtalo de nuevo.",
|
||||
"createFirstAgent": "",
|
||||
"createFirstTask": "Crear primera tarea",
|
||||
"createNewTask": "Crear una nueva tarea",
|
||||
"createNewTaskSubtitle": "Describe lo que necesitas construir y la IA trabajará en ello",
|
||||
@@ -6557,6 +6558,7 @@
|
||||
"createTasksAnytimeNote": "Puedes crear tareas en cualquier momento desde el tablero, o usar",
|
||||
"createTasksAnytimeNoteTerminal": "en la terminal.",
|
||||
"creating": "Creando...",
|
||||
"creatingFirstAgent": "",
|
||||
"creatingTask": "Creando tarea…",
|
||||
"cursorCli": {
|
||||
"active": "✓ Activo",
|
||||
@@ -6593,6 +6595,18 @@
|
||||
"failedToSaveShellConnection": "No se pudo guardar la conexión de shell",
|
||||
"failedToSubmitAuthCode": "No se pudo enviar el código de autorización",
|
||||
"finishSetup": "Finalizar configuración",
|
||||
"firstAgentCreateError": "",
|
||||
"firstAgentCreatedSuccess": "",
|
||||
"firstAgentCustomDraft": "",
|
||||
"firstAgentDraftName": "",
|
||||
"firstAgentContinueWithTemplates": "",
|
||||
"firstAgentInterviewLoadError": "",
|
||||
"firstAgentIntro": "",
|
||||
"firstAgentNoInstructions": "",
|
||||
"firstAgentPreview": "",
|
||||
"firstAgentSkippedHint": "",
|
||||
"firstAgentTemplates": "",
|
||||
"firstAgentTitle": "",
|
||||
"firstTaskDescription": "Crea tu primera tarea para iniciar el tablero y lanzar la ejecución de IA.",
|
||||
"firstTaskPlaceholder": "Ejemplo: Crear una página de inicio de sesión con email y contraseña",
|
||||
"firstTaskReady": "¡Tu primera tarea está lista!",
|
||||
@@ -6721,6 +6735,7 @@
|
||||
"savingRemoteServer": "Guardando…",
|
||||
"selectDefaultModel": "Seleccionar modelo predeterminado",
|
||||
"selectDefaultModelDesc": "Elija un modelo de IA predeterminado para la ejecución de tareas",
|
||||
"selectedAgentTemplate": "",
|
||||
"selectedModel": "Seleccionado:",
|
||||
"serverUrl": "URL del servidor",
|
||||
"serverUrlPlaceholder": "https://your-fusion-host",
|
||||
@@ -6733,6 +6748,7 @@
|
||||
"setUpProject": "Configurar proyecto",
|
||||
"setupWizardHint": "En el asistente de configuración, elige un directorio existente o pega una URL de clon de GitHub.",
|
||||
"skip": "Omitir",
|
||||
"skipFirstAgent": "",
|
||||
"skipForNow": "Omitir por ahora",
|
||||
"skipGitHub": "Omitir GitHub →",
|
||||
"skipOnboardingAriaLabel": "Omitir la introducción",
|
||||
|
||||
@@ -6550,6 +6550,7 @@
|
||||
"copiedCodeToClipboard": "Code copié dans le presse-papiers",
|
||||
"copyCode": "Copier le code",
|
||||
"couldNotReachServer": "Impossible de joindre le serveur. Vérifiez votre connexion et réessayez.",
|
||||
"createFirstAgent": "",
|
||||
"createFirstTask": "Créer la première tâche",
|
||||
"createNewTask": "Créer une nouvelle tâche",
|
||||
"createNewTaskSubtitle": "Décrivez ce que vous souhaitez construire et l'IA s'en occupera",
|
||||
@@ -6557,6 +6558,7 @@
|
||||
"createTasksAnytimeNote": "Vous pouvez créer des tâches à tout moment depuis le tableau, ou utiliser",
|
||||
"createTasksAnytimeNoteTerminal": "dans le terminal.",
|
||||
"creating": "Création...",
|
||||
"creatingFirstAgent": "",
|
||||
"creatingTask": "Création de la tâche…",
|
||||
"cursorCli": {
|
||||
"active": "✓ Actif",
|
||||
@@ -6593,6 +6595,18 @@
|
||||
"failedToSaveShellConnection": "Échec de l'enregistrement de la connexion shell",
|
||||
"failedToSubmitAuthCode": "Échec de la soumission du code d'autorisation",
|
||||
"finishSetup": "Terminer la configuration",
|
||||
"firstAgentCreateError": "",
|
||||
"firstAgentCreatedSuccess": "",
|
||||
"firstAgentCustomDraft": "",
|
||||
"firstAgentDraftName": "",
|
||||
"firstAgentContinueWithTemplates": "",
|
||||
"firstAgentInterviewLoadError": "",
|
||||
"firstAgentIntro": "",
|
||||
"firstAgentNoInstructions": "",
|
||||
"firstAgentPreview": "",
|
||||
"firstAgentSkippedHint": "",
|
||||
"firstAgentTemplates": "",
|
||||
"firstAgentTitle": "",
|
||||
"firstTaskDescription": "Créez votre première tâche pour démarrer le tableau et lancer l'exécution IA.",
|
||||
"firstTaskPlaceholder": "Exemple : Créer une page de connexion avec email et mot de passe",
|
||||
"firstTaskReady": "Votre première tâche est prête !",
|
||||
@@ -6721,6 +6735,7 @@
|
||||
"savingRemoteServer": "Enregistrement…",
|
||||
"selectDefaultModel": "Sélectionner un modèle par défaut",
|
||||
"selectDefaultModelDesc": "Choisissez un modèle IA par défaut pour l'exécution des tâches",
|
||||
"selectedAgentTemplate": "",
|
||||
"selectedModel": "Sélectionné :",
|
||||
"serverUrl": "URL du serveur",
|
||||
"serverUrlPlaceholder": "https://your-fusion-host",
|
||||
@@ -6733,6 +6748,7 @@
|
||||
"setUpProject": "Configurer le projet",
|
||||
"setupWizardHint": "Dans l'assistant de configuration, choisissez un répertoire existant ou collez une URL de clone GitHub.",
|
||||
"skip": "Sauter",
|
||||
"skipFirstAgent": "",
|
||||
"skipForNow": "Ignorer pour l'instant",
|
||||
"skipGitHub": "Ignorer GitHub →",
|
||||
"skipOnboardingAriaLabel": "Ignorer l'intégration",
|
||||
|
||||
@@ -6550,6 +6550,7 @@
|
||||
"copiedCodeToClipboard": "코드가 클립보드에 복사되었습니다",
|
||||
"copyCode": "코드 복사",
|
||||
"couldNotReachServer": "서버에 연결할 수 없습니다. 연결을 확인하고 다시 시도하세요.",
|
||||
"createFirstAgent": "",
|
||||
"createFirstTask": "첫 번째 작업 생성",
|
||||
"createNewTask": "새 작업 생성",
|
||||
"createNewTaskSubtitle": "필요한 것을 설명하면 AI가 작업합니다",
|
||||
@@ -6557,6 +6558,7 @@
|
||||
"createTasksAnytimeNote": "보드에서 언제든지 작업을 생성하거나",
|
||||
"createTasksAnytimeNoteTerminal": "터미널에서 사용하세요.",
|
||||
"creating": "생성 중...",
|
||||
"creatingFirstAgent": "",
|
||||
"creatingTask": "작업을 생성하는 중…",
|
||||
"cursorCli": {
|
||||
"active": "✓ 활성",
|
||||
@@ -6593,6 +6595,18 @@
|
||||
"failedToSaveShellConnection": "셸 연결 저장에 실패했습니다",
|
||||
"failedToSubmitAuthCode": "인증 코드 제출에 실패했습니다",
|
||||
"finishSetup": "설정 완료",
|
||||
"firstAgentCreateError": "",
|
||||
"firstAgentCreatedSuccess": "",
|
||||
"firstAgentCustomDraft": "",
|
||||
"firstAgentDraftName": "",
|
||||
"firstAgentContinueWithTemplates": "",
|
||||
"firstAgentInterviewLoadError": "",
|
||||
"firstAgentIntro": "",
|
||||
"firstAgentNoInstructions": "",
|
||||
"firstAgentPreview": "",
|
||||
"firstAgentSkippedHint": "",
|
||||
"firstAgentTemplates": "",
|
||||
"firstAgentTitle": "",
|
||||
"firstTaskDescription": "첫 번째 작업을 생성하여 보드를 시작하고 AI 실행을 시작하세요.",
|
||||
"firstTaskPlaceholder": "예: 이메일과 비밀번호를 사용하는 로그인 페이지 만들기",
|
||||
"firstTaskReady": "첫 번째 작업이 준비되었습니다!",
|
||||
@@ -6721,6 +6735,7 @@
|
||||
"savingRemoteServer": "저장 중…",
|
||||
"selectDefaultModel": "기본 모델 선택…",
|
||||
"selectDefaultModelDesc": "작업 실행에 사용할 기본 AI 모델을 선택하세요",
|
||||
"selectedAgentTemplate": "",
|
||||
"selectedModel": "선택됨:",
|
||||
"serverUrl": "서버 URL",
|
||||
"serverUrlPlaceholder": "https://your-fusion-host",
|
||||
@@ -6733,6 +6748,7 @@
|
||||
"setUpProject": "프로젝트 설정",
|
||||
"setupWizardHint": "설정 마법사에서 기존 디렉터리를 선택하거나 GitHub 클론 URL을 붙여넣으세요.",
|
||||
"skip": "건너뛰기",
|
||||
"skipFirstAgent": "",
|
||||
"skipForNow": "지금은 건너뛰기",
|
||||
"skipGitHub": "GitHub 건너뛰기 →",
|
||||
"skipOnboardingAriaLabel": "온보딩 건너뛰기",
|
||||
|
||||
@@ -6550,6 +6550,7 @@
|
||||
"copiedCodeToClipboard": "代码已复制到剪贴板",
|
||||
"copyCode": "复制代码",
|
||||
"couldNotReachServer": "无法连接到服务器,请检查网络后重试。",
|
||||
"createFirstAgent": "",
|
||||
"createFirstTask": "创建第一个任务",
|
||||
"createNewTask": "创建新任务",
|
||||
"createNewTaskSubtitle": "描述您需要构建的内容,AI 将为您完成",
|
||||
@@ -6557,6 +6558,7 @@
|
||||
"createTasksAnytimeNote": "您可以随时从看板创建任务,或使用",
|
||||
"createTasksAnytimeNoteTerminal": "在终端中。",
|
||||
"creating": "创建中...",
|
||||
"creatingFirstAgent": "",
|
||||
"creatingTask": "正在创建任务…",
|
||||
"cursorCli": {
|
||||
"active": "✓ 已启用",
|
||||
@@ -6593,6 +6595,18 @@
|
||||
"failedToSaveShellConnection": "保存 Shell 连接失败",
|
||||
"failedToSubmitAuthCode": "提交授权码失败",
|
||||
"finishSetup": "完成设置",
|
||||
"firstAgentCreateError": "",
|
||||
"firstAgentCreatedSuccess": "",
|
||||
"firstAgentCustomDraft": "",
|
||||
"firstAgentDraftName": "",
|
||||
"firstAgentContinueWithTemplates": "",
|
||||
"firstAgentInterviewLoadError": "",
|
||||
"firstAgentIntro": "",
|
||||
"firstAgentNoInstructions": "",
|
||||
"firstAgentPreview": "",
|
||||
"firstAgentSkippedHint": "",
|
||||
"firstAgentTemplates": "",
|
||||
"firstAgentTitle": "",
|
||||
"firstTaskDescription": "创建您的第一个任务以启动看板并启动 AI 执行。",
|
||||
"firstTaskPlaceholder": "示例:构建一个包含邮箱和密码的登录页面",
|
||||
"firstTaskReady": "您的第一个任务已就绪!",
|
||||
@@ -6721,6 +6735,7 @@
|
||||
"savingRemoteServer": "正在保存…",
|
||||
"selectDefaultModel": "选择默认模型",
|
||||
"selectDefaultModelDesc": "为任务执行选择默认 AI 模型",
|
||||
"selectedAgentTemplate": "",
|
||||
"selectedModel": "已选:",
|
||||
"serverUrl": "服务器 URL",
|
||||
"serverUrlPlaceholder": "https://your-fusion-host",
|
||||
@@ -6733,6 +6748,7 @@
|
||||
"setUpProject": "设置项目",
|
||||
"setupWizardHint": "在设置向导中,选择现有目录或粘贴 GitHub 克隆 URL。",
|
||||
"skip": "跳过",
|
||||
"skipFirstAgent": "",
|
||||
"skipForNow": "暂时跳过",
|
||||
"skipGitHub": "跳过 GitHub →",
|
||||
"skipOnboardingAriaLabel": "跳过引导",
|
||||
|
||||
@@ -6550,6 +6550,7 @@
|
||||
"copiedCodeToClipboard": "代碼已複製到剪貼簿",
|
||||
"copyCode": "複製代碼",
|
||||
"couldNotReachServer": "無法連線至伺服器,請檢查網路後再試。",
|
||||
"createFirstAgent": "",
|
||||
"createFirstTask": "建立第一個任務",
|
||||
"createNewTask": "建立新任務",
|
||||
"createNewTaskSubtitle": "描述您需要建構的內容,AI 將為您完成",
|
||||
@@ -6557,6 +6558,7 @@
|
||||
"createTasksAnytimeNote": "您可以隨時從看板建立任務,或使用",
|
||||
"createTasksAnytimeNoteTerminal": "在終端機中。",
|
||||
"creating": "建立中...",
|
||||
"creatingFirstAgent": "",
|
||||
"creatingTask": "正在建立任務…",
|
||||
"cursorCli": {
|
||||
"active": "✓ 已啟用",
|
||||
@@ -6593,6 +6595,18 @@
|
||||
"failedToSaveShellConnection": "儲存 Shell 連線失敗",
|
||||
"failedToSubmitAuthCode": "提交授權碼失敗",
|
||||
"finishSetup": "完成設定",
|
||||
"firstAgentCreateError": "",
|
||||
"firstAgentCreatedSuccess": "",
|
||||
"firstAgentCustomDraft": "",
|
||||
"firstAgentDraftName": "",
|
||||
"firstAgentContinueWithTemplates": "",
|
||||
"firstAgentInterviewLoadError": "",
|
||||
"firstAgentIntro": "",
|
||||
"firstAgentNoInstructions": "",
|
||||
"firstAgentPreview": "",
|
||||
"firstAgentSkippedHint": "",
|
||||
"firstAgentTemplates": "",
|
||||
"firstAgentTitle": "",
|
||||
"firstTaskDescription": "建立您的第一個任務以啟動看板並啟動 AI 執行。",
|
||||
"firstTaskPlaceholder": "範例:建立一個包含電子郵件和密碼的登入頁面",
|
||||
"firstTaskReady": "您的第一個任務已就緒!",
|
||||
@@ -6721,6 +6735,7 @@
|
||||
"savingRemoteServer": "正在儲存…",
|
||||
"selectDefaultModel": "選擇預設模型",
|
||||
"selectDefaultModelDesc": "為任務執行選擇預設 AI 模型",
|
||||
"selectedAgentTemplate": "",
|
||||
"selectedModel": "已選:",
|
||||
"serverUrl": "伺服器 URL",
|
||||
"serverUrlPlaceholder": "https://your-fusion-host",
|
||||
@@ -6733,6 +6748,7 @@
|
||||
"setUpProject": "設定專案",
|
||||
"setupWizardHint": "在設定精靈中,選擇現有目錄或貼上 GitHub 複製 URL。",
|
||||
"skip": "跳過",
|
||||
"skipFirstAgent": "",
|
||||
"skipForNow": "暫時略過",
|
||||
"skipGitHub": "略過 GitHub →",
|
||||
"skipOnboardingAriaLabel": "略過引導",
|
||||
|
||||
43
packages/i18n/src/resources.d.ts
vendored
43
packages/i18n/src/resources.d.ts
vendored
@@ -6550,6 +6550,7 @@ export default interface Resources {
|
||||
"copiedCodeToClipboard": "Copied code to clipboard",
|
||||
"copyCode": "Copy code",
|
||||
"couldNotReachServer": "Could not reach the server. Check your connection and try again.",
|
||||
"createFirstAgent": "Create Agent",
|
||||
"createFirstTask": "Create First Task",
|
||||
"createNewTask": "Create a New Task",
|
||||
"createNewTaskSubtitle": "Describe what you need built and AI will work on it",
|
||||
@@ -6557,6 +6558,7 @@ export default interface Resources {
|
||||
"createTasksAnytimeNote": "You can create tasks anytime from the board, or use",
|
||||
"createTasksAnytimeNoteTerminal": "in the terminal.",
|
||||
"creating": "Creating...",
|
||||
"creatingFirstAgent": "Creating agent...",
|
||||
"creatingTask": "Creating task…",
|
||||
"cursorCli": {
|
||||
"active": "✓ Active",
|
||||
@@ -6593,6 +6595,18 @@ export default interface Resources {
|
||||
"failedToSaveShellConnection": "Failed to save shell connection",
|
||||
"failedToSubmitAuthCode": "Failed to submit authorization code",
|
||||
"finishSetup": "Finish Setup",
|
||||
"firstAgentContinueWithTemplates": "Continue with templates",
|
||||
"firstAgentCreateError": "Failed to create agent",
|
||||
"firstAgentCreatedSuccess": "Your project is registered and your first agent is ready.",
|
||||
"firstAgentCustomDraft": "Custom agent draft",
|
||||
"firstAgentDraftName": "Draft agent",
|
||||
"firstAgentInterviewLoadError": "AI interview could not load. You can still create an agent from a template or skip this step.",
|
||||
"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.",
|
||||
"firstAgentNoInstructions": "No inline instructions yet",
|
||||
"firstAgentPreview": "Preview",
|
||||
"firstAgentSkippedHint": "You can create agents later from the Agents view.",
|
||||
"firstAgentTemplates": "Templates",
|
||||
"firstAgentTitle": "Create your first agent",
|
||||
"firstTaskDescription": "Create your first task to start the board and launch AI execution.",
|
||||
"firstTaskPlaceholder": "Example: Build a login page with email and password",
|
||||
"firstTaskReady": "Your first task is ready!",
|
||||
@@ -6721,6 +6735,7 @@ export default interface Resources {
|
||||
"savingRemoteServer": "Saving…",
|
||||
"selectDefaultModel": "Select a default model…",
|
||||
"selectDefaultModelDesc": "Choose a default AI model for task execution",
|
||||
"selectedAgentTemplate": "{{name}} selected",
|
||||
"selectedModel": "Selected:",
|
||||
"serverUrl": "Server URL",
|
||||
"serverUrlPlaceholder": "https://your-fusion-host",
|
||||
@@ -6734,6 +6749,7 @@ export default interface Resources {
|
||||
"setupMode": "Setup Mode",
|
||||
"setupWizardHint": "In the setup wizard, pick an existing directory or paste a GitHub clone URL.",
|
||||
"skip": "Skip",
|
||||
"skipFirstAgent": "Skip for now",
|
||||
"skipForNow": "Skip for now",
|
||||
"skipGitHub": "Skip GitHub →",
|
||||
"skipOnboardingAriaLabel": "Skip onboarding",
|
||||
@@ -7508,7 +7524,7 @@ export default interface Resources {
|
||||
"chat": "Chat",
|
||||
"comments": "Comments",
|
||||
"definition": "Definition",
|
||||
"documents": "Documents",
|
||||
"documents": "Artifacts",
|
||||
"logs": "Logs",
|
||||
"model": "Model",
|
||||
"pullRequest": "Pull Request",
|
||||
@@ -7550,6 +7566,8 @@ export default interface Resources {
|
||||
"yes": "Yes"
|
||||
},
|
||||
"taskDocuments": {
|
||||
"artifactCount": "{{count}} artifact{{plural}}",
|
||||
"artifactsSubheading": "Media artifacts",
|
||||
"cancel": "Cancel",
|
||||
"collapse": "Collapse",
|
||||
"contentLabel": "Content",
|
||||
@@ -7560,29 +7578,33 @@ export default interface Resources {
|
||||
"creating": "Creating…",
|
||||
"deleteConfirm": "Delete?",
|
||||
"deleted": "Document deleted",
|
||||
"documentCount": "{{count}} document{{plural}}",
|
||||
"documentsSubheading": "Task documents",
|
||||
"edit": "Edit",
|
||||
"expand": "Expand",
|
||||
"failedToCreate": "Failed to create document",
|
||||
"failedToDelete": "Failed to delete document",
|
||||
"failedToLoad": "Failed to load documents",
|
||||
"failedToLoadArtifacts": "Failed to load artifacts",
|
||||
"failedToLoadRevisions": "Failed to load revisions",
|
||||
"failedToSave": "Failed to save document",
|
||||
"heading": "Documents",
|
||||
"heading": "Artifacts",
|
||||
"history": "History",
|
||||
"invalidKeyFormat": "Invalid key format. Use 1-64 alphanumeric characters, hyphens, or underscores.",
|
||||
"keyHint": "Alphanumeric, hyphens, underscores (1-64 chars)",
|
||||
"keyLabel": "Key",
|
||||
"keyPlaceholder": "e.g., plan, notes, research",
|
||||
"keyRequired": "Document key is required",
|
||||
"loading": "Loading documents…",
|
||||
"loading": "Loading documents and artifacts…",
|
||||
"loadingRevisions": "Loading…",
|
||||
"modeMarkdown": "Markdown",
|
||||
"modePlain": "Plain",
|
||||
"newDocumentButton": "New Document",
|
||||
"newDocumentTitle": "New Document",
|
||||
"no": "No",
|
||||
"noDocuments": "No documents yet.",
|
||||
"noDocuments": "No documents or artifacts yet.",
|
||||
"noPreviousRevisions": "No previous revisions.",
|
||||
"noTaskDocuments": "No task documents yet.",
|
||||
"revisionHistory": "Revision History",
|
||||
"save": "Save",
|
||||
"saved": "Document saved",
|
||||
@@ -8247,6 +8269,14 @@ export default interface Resources {
|
||||
"namedScript": "Named script",
|
||||
"namedScriptNote": "Named script from project settings. The node prompt is passed via FUSION_NODE_PROMPT.",
|
||||
"prompt": "Prompt",
|
||||
"promptOverridden": "Overridden",
|
||||
"promptOverrideReset": "Prompt reset to default",
|
||||
"promptOverrideResetFailed": "Failed to reset prompt",
|
||||
"promptOverrideSaveFailed": "Failed to save prompt override",
|
||||
"promptOverrideSaved": "Prompt override saved",
|
||||
"promptOverridesLoadFailed": "Failed to load prompt overrides",
|
||||
"promptSaving": "Saving…",
|
||||
"resetPromptDefault": "Reset to default",
|
||||
"scriptName": "Script name",
|
||||
"selectAgent": "— select agent —",
|
||||
"selectSkill": "— select skill —",
|
||||
@@ -8365,7 +8395,7 @@ export default interface Resources {
|
||||
"parseArtifact": "Artifact",
|
||||
"parseParser": "Parser",
|
||||
"quorumN": "Quorum count (n)",
|
||||
"readOnlyDuplicateToEdit": "Read-only built-in — duplicate the workflow to edit nodes.",
|
||||
"readOnlyDuplicateToEdit": "Built-in structure is read-only — prompts on prompt and gate nodes can be edited here.",
|
||||
"releaseCapacity": "Downstream capacity",
|
||||
"releaseCondition": "Release condition",
|
||||
"releaseDependency": "Dependency complete",
|
||||
@@ -8524,7 +8554,7 @@ export default interface Resources {
|
||||
"nameLabel": "Workflow name",
|
||||
"newWorkflow": "New workflow",
|
||||
"noneYet": "No workflows yet.",
|
||||
"readOnlyBuiltin": "Read-only built-in workflow",
|
||||
"readOnlyBuiltin": "Built-in workflow: structure is read-only, prompts are editable.",
|
||||
"saveFailed": "Failed to save workflow",
|
||||
"saved": "Workflow saved",
|
||||
"savedNotCompilable": "Workflow saved but cannot be compiled",
|
||||
@@ -9008,6 +9038,7 @@ export default interface Resources {
|
||||
"workflowNodes": {
|
||||
"summaryAwaitInput": "Waits for user input",
|
||||
"summaryCodeDefault": "TypeScript",
|
||||
"summaryDefaultModel": "Default model",
|
||||
"summaryGateAdvisory": "Advisory",
|
||||
"summaryGateBlocks": "Gate (blocks)",
|
||||
"summaryHoldRelease": "Release: {{release}}",
|
||||
|
||||
Reference in New Issue
Block a user