fix(dashboard): polish first-run setup wizard (AI step, GitHub step, project step)
- AI Setup: connected providers now render first; 'Skip for now' hides once a provider is connected; footer buttons share one size. - GitHub step: state-driven (no install/pitch content when connected), one type scale, real <code> literals, per-OS gh instructions behind a disclosure, single skip affordance; gh/OAuth status revalidates on window focus and OAuth relogin so later steps stop showing stale 'not connected'. - Project step: onboarding-driven opens keep 'Step 3 of 5' context; runtime-node picker no longer lists 'Local node' and 'local (local)' duplicates and hides itself when only the local node exists; isolation-mode cards no longer render a stretched native radio or uppercased descriptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/setup-wizard-polish.md
Normal file
7
.changeset/setup-wizard-polish.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Overhaul first-run setup: connected providers on top, state-driven GitHub step, fixed isolation-mode radios, deduped runtime-node picker.
|
||||
category: fix
|
||||
dev: New setupWizardNodes.ts (getSelectableRuntimeNodes/shouldShowRuntimeNodeSelector) shared by SetupWizardModal and SetupProjectForm; GitHub status revalidates on window focus and OAUTH_RELOGIN_SUCCESS_EVENT; 4 new i18n keys.
|
||||
@@ -667,20 +667,31 @@
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:45:
|
||||
First-run review: the GitHub step mixed tiny bold micro-headers with helper-text bullets in one flat
|
||||
list. The with/without comparison now renders as two labeled groups on a single consistent type scale
|
||||
(font-size-sm headings + font-size-sm items), side by side on desktop and stacked on mobile.
|
||||
*/
|
||||
.onboarding-feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-lg);
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
font-size: calc(var(--space-md) + (var(--space-xs) / 4));
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.onboarding-feature-list ul {
|
||||
.onboarding-feature-list__group h4 {
|
||||
margin: 0 0 var(--space-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.onboarding-feature-list__group ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -689,33 +700,19 @@
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.onboarding-feature-list .onboarding-feature-list-heading strong {
|
||||
display: block;
|
||||
font-size: var(--space-md);
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.onboarding-feature-list .onboarding-feature-list-heading:first-child strong {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.onboarding-feature-list li:not(.onboarding-feature-list-heading) {
|
||||
.onboarding-feature-list__group li {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
padding-left: var(--space-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.onboarding-feature-list li:not(.onboarding-feature-list-heading)::before {
|
||||
.onboarding-feature-list__group li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.onboarding-feature-list .onboarding-feature-list-item--with-github {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* === Onboarding GitHub Step === */
|
||||
.onboarding-github-git-prerequisite {
|
||||
display: flex;
|
||||
@@ -875,6 +872,24 @@
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:50:
|
||||
Inline command fragments (`gh`, `brew install gh`, `sudo apt install git`) in git/gh install guidance
|
||||
render as real <code> elements (see renderWithInlineCode) instead of raw backtick literals.
|
||||
*/
|
||||
.onboarding-github-setup-card p code,
|
||||
.onboarding-github-setup-card li code,
|
||||
.onboarding-github-git-prerequisite p code,
|
||||
.onboarding-github-git-prerequisite li code {
|
||||
padding: 1px var(--space-xs);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-raised);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* === GitHub Connection Status Feedback (FN-1934) === */
|
||||
.onboarding-github-feedback {
|
||||
margin-top: var(--space-sm);
|
||||
@@ -1271,12 +1286,25 @@
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.model-onboarding-footer .btn-primary {
|
||||
display: flex;
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:55:
|
||||
First-run review: footer buttons had wildly different sizes ("Next →" much larger than the skip
|
||||
buttons). Every footer control shares the same 38px height and 14px type; the primary action keeps
|
||||
its filled emphasis and wider padding only.
|
||||
*/
|
||||
.model-onboarding-footer .btn,
|
||||
.model-onboarding-footer .onboarding-skip-step-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
padding: 10px 24px;
|
||||
min-height: 38px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.model-onboarding-footer .btn-primary {
|
||||
padding: 8px 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1526,6 +1554,12 @@
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
/* FNXC:Onboarding 2026-07-10-10:45: stack the with/without GitHub comparison on phones. */
|
||||
.onboarding-feature-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.model-onboarding-footer .btn-primary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./ModelOnboardingModal.css";
|
||||
import "./SetupWizardModal.css";
|
||||
import { lazy, Suspense, useState, useEffect, useCallback, useRef, useMemo, type KeyboardEvent } from "react";
|
||||
import { lazy, Suspense, useState, useEffect, useCallback, useRef, useMemo, type KeyboardEvent, type ReactNode } from "react";
|
||||
import { X, Loader2, CheckCircle, Key, Zap, GitPullRequest, Rocket, Plus, Sparkles, UserRound } from "lucide-react";
|
||||
import { getErrorMessage, type Task } from "@fusion/core";
|
||||
import type { AuthProvider, ManualOAuthCodeInfo, ModelInfo, CustomProvider, CustomProviderConfig, OAuthDeviceCodeInfo } from "../api";
|
||||
@@ -228,6 +228,23 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||
const getManualCodeLoginWarningMessage = (providerName: string) =>
|
||||
`After you sign in with ${providerName}, the browser will try to redirect to a localhost address that this dashboard can't reach. The redirect tab will look like it failed. Before that happens, copy the full URL from the browser address bar — you'll paste it back here to finish login. Continue?`;
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:05:
|
||||
Install-guidance i18n strings (git/gh CLI) contain backtick-quoted commands like `gh` and `brew install gh`.
|
||||
Previously those backticks rendered as literal characters in plain text (first-run review called this out as slop).
|
||||
Render backtick spans as real <code> elements so commands are visually distinct, without splitting every
|
||||
translated string into prefix/suffix keys (all existing locale catalogs keep working unchanged).
|
||||
*/
|
||||
function renderWithInlineCode(text: string): ReactNode {
|
||||
const parts = text.split("`");
|
||||
if (parts.length < 3) {
|
||||
return text;
|
||||
}
|
||||
return parts.map((part, index) =>
|
||||
index % 2 === 1 ? <code key={index}>{part}</code> : <span key={index}>{part}</span>,
|
||||
);
|
||||
}
|
||||
|
||||
function getProviderDisplayName(providerId: string): string {
|
||||
if (PROVIDER_DISPLAY_NAMES[providerId]) {
|
||||
return PROVIDER_DISPLAY_NAMES[providerId];
|
||||
@@ -1006,7 +1023,6 @@ export function ModelOnboardingModal({
|
||||
const hasGithubProvider = githubActionState.oauth.providerAvailable;
|
||||
const isGithubAuthenticated = githubActionState.oauth.authenticated;
|
||||
const isGithubLoginInProgress = githubActionState.oauth.loginInProgress;
|
||||
const isGithubCliAuthenticated = githubActionState.ghCli.authenticated;
|
||||
const gitInstallUrl = gitCliStatus?.installUrl ?? GIT_INSTALL_URL;
|
||||
const gitVersionLabel = gitCliStatus?.version
|
||||
? t("setup.gitPrerequisiteInstalledVersion", "Git is installed on the Fusion host ({{version}}).", { version: gitCliStatus.version })
|
||||
@@ -1121,6 +1137,27 @@ export function ModelOnboardingModal({
|
||||
);
|
||||
}, [loadAuthStatus, loadModels, loadGlobalSettings]);
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:10:
|
||||
Bug from first-run review: connecting GitHub mid-session (e.g. running `gh auth login` in a terminal,
|
||||
or completing an OAuth login from another surface) left later wizard steps (Agent / First Task readiness
|
||||
summary) showing GitHub as "not connected" because provider/gh-CLI status was only fetched once and only
|
||||
re-fetched when returning to the AI Setup step. Revalidate the cached auth/gh/git status whenever the
|
||||
window regains focus (the user just came back from a terminal or an OAuth tab) and whenever any surface
|
||||
broadcasts a successful OAuth re-login, so every step reflects the current connection state.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const refreshAuthStatus = () => {
|
||||
void loadAuthStatus();
|
||||
};
|
||||
window.addEventListener("focus", refreshAuthStatus);
|
||||
window.addEventListener(OAUTH_RELOGIN_SUCCESS_EVENT, refreshAuthStatus);
|
||||
return () => {
|
||||
window.removeEventListener("focus", refreshAuthStatus);
|
||||
window.removeEventListener(OAUTH_RELOGIN_SUCCESS_EVENT, refreshAuthStatus);
|
||||
};
|
||||
}, [loadAuthStatus]);
|
||||
|
||||
// Restore login outcomes from persisted state on mount
|
||||
useEffect(() => {
|
||||
const persistedStepData = getStepData("ai-setup");
|
||||
@@ -2147,8 +2184,19 @@ export function ModelOnboardingModal({
|
||||
const visibleOrderedAiProviders = orderedAiProviders.filter(
|
||||
(provider) => !(provider.id === "anthropic" && hasSeparatedAnthropicProvider),
|
||||
);
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:15:
|
||||
First-run review: after connecting a provider the "Connected providers" section rendered BELOW the
|
||||
quick-start cards and the long provider list, so users could not see what was already connected without
|
||||
scrolling. ALL connected providers (quick-start and advanced alike) now render in a single "Connected
|
||||
providers" section at the TOP of the AI Setup step, and the quick-start list only offers providers that
|
||||
still need connecting. The "N of M providers connected" summary above is unchanged.
|
||||
*/
|
||||
const connectedVisibleProviders = visibleOrderedAiProviders.filter(
|
||||
(provider) => provider.authenticated,
|
||||
);
|
||||
const quickStartProviders = visibleOrderedAiProviders
|
||||
.filter((provider) => quickStartSet.has(provider.id))
|
||||
.filter((provider) => quickStartSet.has(provider.id) && !provider.authenticated)
|
||||
.sort((a, b) => {
|
||||
const rankA = QUICK_START_PROVIDER_IDS.indexOf(a.id as (typeof QUICK_START_PROVIDER_IDS)[number]);
|
||||
const rankB = QUICK_START_PROVIDER_IDS.indexOf(b.id as (typeof QUICK_START_PROVIDER_IDS)[number]);
|
||||
@@ -2157,9 +2205,6 @@ export function ModelOnboardingModal({
|
||||
}
|
||||
return compareOnboardingProviders(a, b);
|
||||
});
|
||||
const connectedNonQuickStartProviders = visibleOrderedAiProviders.filter(
|
||||
(provider) => provider.authenticated && !quickStartSet.has(provider.id),
|
||||
);
|
||||
const advancedProviders = visibleOrderedAiProviders.filter(
|
||||
(provider) => !provider.authenticated && !quickStartSet.has(provider.id),
|
||||
);
|
||||
@@ -2625,12 +2670,30 @@ export function ModelOnboardingModal({
|
||||
actions={{ refreshAuthProviders: () => { void loadAuthStatus(); } }}
|
||||
/>
|
||||
|
||||
{/*
|
||||
FNXC:Onboarding 2026-07-10-10:15:
|
||||
Connected providers render FIRST so that reopening the step (or returning after an
|
||||
OAuth login) immediately shows what is already connected without scrolling.
|
||||
*/}
|
||||
{connectedVisibleProviders.length > 0 && (
|
||||
<section className="onboarding-provider-section" data-testid="onboarding-connected-providers">
|
||||
<h3 className="onboarding-section-title">{t("setup.connectedProviders", "Connected providers")}</h3>
|
||||
<div className="model-onboarding-providers">
|
||||
{connectedVisibleProviders.map((provider) => renderAiProviderCard(provider))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="onboarding-provider-section" data-testid="onboarding-quick-start-providers">
|
||||
<h3 className="onboarding-section-title">{t("setup.quickStartProviders", "Quick start providers")}</h3>
|
||||
{quickStartProviders.length > 0 ? (
|
||||
<div className="model-onboarding-providers">
|
||||
{quickStartProviders.map((provider) => renderAiProviderCard(provider))}
|
||||
</div>
|
||||
) : connectedVisibleProviders.length > 0 ? (
|
||||
<p className="onboarding-helper-text">
|
||||
{t("setup.quickStartAllConnected", "All quick-start providers are already connected.")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="onboarding-helper-text">
|
||||
{t("setup.noQuickStartProviders", "No quick-start providers are available in this environment.")}
|
||||
@@ -2680,16 +2743,7 @@ export function ModelOnboardingModal({
|
||||
</OnboardingDisclosure>
|
||||
</section>
|
||||
|
||||
{connectedNonQuickStartProviders.length > 0 && (
|
||||
<section className="onboarding-provider-section" data-testid="onboarding-connected-providers">
|
||||
<h3 className="onboarding-section-title">{t("setup.connectedProviders", "Connected providers")}</h3>
|
||||
<div className="model-onboarding-providers">
|
||||
{connectedNonQuickStartProviders.map((provider) => renderAiProviderCard(provider))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Model Selection — placed directly after the authenticated provider list */}
|
||||
{/* Model Selection — placed directly after the provider sections */}
|
||||
<div className="onboarding-model-section">
|
||||
<h3 className="onboarding-section-title">
|
||||
{t("setup.defaultModelOptional", "Default Model (Optional)")}
|
||||
@@ -2793,13 +2847,20 @@ export function ModelOnboardingModal({
|
||||
<p>{gitVersionLabel}</p>
|
||||
) : (
|
||||
<>
|
||||
{/*
|
||||
FNXC:Onboarding 2026-07-10-10:20:
|
||||
First-run review flagged raw backtick literals rendering as plain text.
|
||||
Commands in these strings now render as real <code> elements via renderWithInlineCode.
|
||||
Missing git is a hard blocker for project setup, so its per-OS instructions stay
|
||||
expanded (unlike the optional gh CLI guidance which is collapsed behind a disclosure).
|
||||
*/}
|
||||
<p>
|
||||
{t("setup.gitPrerequisiteMissingBody", "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces.")}
|
||||
{renderWithInlineCode(t("setup.gitPrerequisiteMissingBody", "Fusion could not find `git` on the server host running Fusion. Install Git there before cloning repositories, initializing projects, or registering Git-backed workspaces."))}
|
||||
</p>
|
||||
<ul className="onboarding-github-git-install-list">
|
||||
<li>{t("setup.gitPrerequisiteMac", "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer.")}</li>
|
||||
<li>{t("setup.gitPrerequisiteWindows", "Windows: install Git for Windows and restart the Fusion host shell or service.")}</li>
|
||||
<li>{t("setup.gitPrerequisiteLinux", "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`.")}</li>
|
||||
<li>{renderWithInlineCode(t("setup.gitPrerequisiteMac", "macOS: install Xcode Command Line Tools with `xcode-select --install`, Homebrew with `brew install git`, or the Git installer."))}</li>
|
||||
<li>{renderWithInlineCode(t("setup.gitPrerequisiteWindows", "Windows: install Git for Windows and restart the Fusion host shell or service."))}</li>
|
||||
<li>{renderWithInlineCode(t("setup.gitPrerequisiteLinux", "Linux: install with your package manager, for example `sudo apt install git`, `sudo dnf install git`, or `sudo pacman -S git`."))}</li>
|
||||
</ul>
|
||||
<a href={gitInstallUrl} target="_blank" rel="noreferrer">
|
||||
{t("setup.gitPrerequisiteInstallLink", "Open Git install downloads")}
|
||||
@@ -2809,22 +2870,31 @@ export function ModelOnboardingModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
FNXC:Onboarding 2026-07-10-10:25:
|
||||
First-run review called the GitHub step "slop-coded": it mixed tiny bold micro-headers,
|
||||
bullet items, and helper-text sizes inside one flat list. The with/without comparison is
|
||||
now two labeled groups on one consistent type scale (heading + plain list items), rendered
|
||||
only while GitHub is NOT connected — a connected user does not need the sales pitch.
|
||||
*/}
|
||||
{!isGitHubReady && (
|
||||
<div className="onboarding-feature-list">
|
||||
<ul>
|
||||
<li className="onboarding-feature-list-heading">
|
||||
<strong>{t("setup.withoutGitHubHeading", "Without GitHub (available now):")}</strong>
|
||||
</li>
|
||||
<li className="onboarding-helper-text">{t("setup.withoutGitHub1", "Create tasks manually")}</li>
|
||||
<li className="onboarding-helper-text">{t("setup.withoutGitHub2", "Describe work for AI agents")}</li>
|
||||
<li className="onboarding-helper-text">{t("setup.withoutGitHub3", "Track progress on the board")}</li>
|
||||
<li className="onboarding-feature-list-heading">
|
||||
<strong>{t("setup.withGitHubHeading", "With GitHub (after connecting):")}</strong>
|
||||
</li>
|
||||
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">{t("setup.withGitHub1", "Import issues as tasks")}</li>
|
||||
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">{t("setup.withGitHub2", "Sync pull request status")}</li>
|
||||
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">{t("setup.withGitHub3", "Link code changes to tasks")}</li>
|
||||
</ul>
|
||||
<div className="onboarding-feature-list__group">
|
||||
<h4>{t("setup.withoutGitHubHeading", "Without GitHub (available now)")}</h4>
|
||||
<ul>
|
||||
<li>{t("setup.withoutGitHub1", "Create tasks manually")}</li>
|
||||
<li>{t("setup.withoutGitHub2", "Describe work for AI agents")}</li>
|
||||
<li>{t("setup.withoutGitHub3", "Track progress on the board")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="onboarding-feature-list__group">
|
||||
<h4>{t("setup.withGitHubHeading", "With GitHub (after connecting)")}</h4>
|
||||
<ul>
|
||||
<li>{t("setup.withGitHub1", "Import issues as tasks")}</li>
|
||||
<li>{t("setup.withGitHub2", "Sync pull request status")}</li>
|
||||
<li>{t("setup.withGitHub3", "Link code changes to tasks")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2849,27 +2919,36 @@ export function ModelOnboardingModal({
|
||||
GitHub onboarding must give users in-flow paths for both supported auth modes instead of Settings-only explanatory copy.
|
||||
The dashboard can start OAuth when the GitHub provider exists; `gh` CLI setup remains explicit host-side guidance because Fusion does not install third-party binaries automatically.
|
||||
*/}
|
||||
{!isGithubCliAuthenticated && githubActionState.ghCli.state === "missing" && (
|
||||
{/*
|
||||
FNXC:Onboarding 2026-07-10-10:30:
|
||||
State-driven GitHub step (first-run review): the gh CLI cards only render while GitHub is
|
||||
NOT ready overall — an OAuth-connected user must never be told to install the GitHub CLI.
|
||||
The per-OS install wall of text is collapsed behind a single disclosure; the card leads
|
||||
with one sentence and commands render as <code> instead of raw backtick literals.
|
||||
*/}
|
||||
{!isGitHubReady && githubActionState.ghCli.state === "missing" && (
|
||||
<div className="onboarding-github-setup-card onboarding-github-setup-card--warning" data-testid="onboarding-gh-cli-install-card" role="status">
|
||||
<div className="onboarding-github-setup-card__heading">
|
||||
<ProviderIcon provider="github" size="sm" />
|
||||
<strong>{t("setup.githubCliInstallTitle", "Install GitHub CLI")}</strong>
|
||||
</div>
|
||||
<p>
|
||||
{t("setup.githubCliInstallBody", "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication.")}
|
||||
{renderWithInlineCode(t("setup.githubCliInstallBody", "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication."))}
|
||||
</p>
|
||||
<ul className="onboarding-github-setup-list">
|
||||
<li>{t("setup.githubCliInstallMac", "macOS: `brew install gh` or download the installer from GitHub CLI releases.")}</li>
|
||||
<li>{t("setup.githubCliInstallWindows", "Windows: install GitHub CLI with WinGet, Chocolatey, or the GitHub CLI installer, then restart the Fusion host shell or service.")}</li>
|
||||
<li>{t("setup.githubCliInstallLinux", "Linux: install `gh` with your distribution package manager or the packages from cli.github.com.")}</li>
|
||||
</ul>
|
||||
<a className="btn btn-sm" href={GH_CLI_INSTALL_URL} target="_blank" rel="noreferrer">
|
||||
{t("setup.githubCliInstallLink", "Open GitHub CLI releases")}
|
||||
</a>
|
||||
<OnboardingDisclosure summary={t("setup.githubCliInstallShowInstructions", "Show install instructions")}>
|
||||
<ul className="onboarding-github-setup-list">
|
||||
<li>{renderWithInlineCode(t("setup.githubCliInstallMac", "macOS: `brew install gh` or download the installer from GitHub CLI releases."))}</li>
|
||||
<li>{renderWithInlineCode(t("setup.githubCliInstallWindows", "Windows: install GitHub CLI with WinGet, Chocolatey, or the GitHub CLI installer, then restart the Fusion host shell or service."))}</li>
|
||||
<li>{renderWithInlineCode(t("setup.githubCliInstallLinux", "Linux: install `gh` with your distribution package manager or the packages from cli.github.com."))}</li>
|
||||
</ul>
|
||||
<a className="btn btn-sm" href={GH_CLI_INSTALL_URL} target="_blank" rel="noreferrer">
|
||||
{t("setup.githubCliInstallLink", "Open GitHub CLI releases")}
|
||||
</a>
|
||||
</OnboardingDisclosure>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isGithubCliAuthenticated && githubActionState.ghCli.state === "unauthenticated" && (
|
||||
{!isGitHubReady && githubActionState.ghCli.state === "unauthenticated" && (
|
||||
<div className="onboarding-github-setup-card onboarding-github-setup-card--info" data-testid="onboarding-gh-cli-auth-card" role="status">
|
||||
<div className="onboarding-github-setup-card__heading">
|
||||
<ProviderIcon provider="github" size="sm" />
|
||||
@@ -2898,32 +2977,40 @@ export function ModelOnboardingModal({
|
||||
<ProviderIcon provider="github" size="lg" />
|
||||
</div>
|
||||
{isGitHubReadyViaCli ? (
|
||||
<p>
|
||||
{t("setup.githubCliAuthNote", "GitHub CLI is already authenticated, so imports and PR tracking work now. OAuth from the dashboard is optional and only controls dashboard-managed connect/disconnect.")}
|
||||
</p>
|
||||
<>
|
||||
<p>
|
||||
{t("setup.githubCliAuthNote", "GitHub CLI is already authenticated, so imports and PR tracking work now. OAuth from the dashboard is optional and only controls dashboard-managed connect/disconnect.")}
|
||||
</p>
|
||||
<div className="model-onboarding-github-optional__actions">
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
/*
|
||||
* FNXC:Onboarding 2026-07-03-08:00:
|
||||
* Advance via handleNext (not a raw setStep) so the GitHub step's status is
|
||||
* recorded. When GitHub is ready via the gh CLI, "Continue with gh CLI auth"
|
||||
* must COMPLETE the step (handleNext) — previously it jumped to project-setup
|
||||
* without adding "github" to completedSteps, so step 2 never checked off.
|
||||
*/
|
||||
onClick={handleNext}
|
||||
>
|
||||
{t("setup.continueWithGhCli", "Continue with gh CLI auth →")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/*
|
||||
* FNXC:Onboarding 2026-07-10-10:35:
|
||||
* First-run review: the step offered THREE overlapping escape hatches at once —
|
||||
* an in-body "Continue without GitHub →" CTA plus footer "Skip GitHub →" plus
|
||||
* "Next →". The in-body button is removed entirely; the footer "Skip GitHub →"
|
||||
* is the single skip affordance. The explanatory sentence (and its wrapper) stays,
|
||||
* and its former actions <div> is dropped with it so no empty button shell remains
|
||||
* on desktop or mobile.
|
||||
*/
|
||||
<p>
|
||||
{t("setup.githubOauthUnavailable", "Dashboard GitHub OAuth is not configured on this Fusion host. You can still continue without GitHub, or use the GitHub CLI setup guidance above when available.")}
|
||||
{t("setup.githubOauthUnavailable", "Dashboard GitHub OAuth is not configured on this Fusion host. You can still skip GitHub below, or use the GitHub CLI setup guidance above when available.")}
|
||||
</p>
|
||||
)}
|
||||
<div className="model-onboarding-github-optional__actions">
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
/*
|
||||
* FNXC:Onboarding 2026-07-03-08:00:
|
||||
* Advance via handleNext/handleSkip (not a raw setStep) so the GitHub step's
|
||||
* status is recorded. When GitHub is ready via the gh CLI, "Continue with gh CLI
|
||||
* auth" must COMPLETE the step (handleNext) — previously it jumped to project-setup
|
||||
* without adding "github" to completedSteps, so step 2 never checked off. When
|
||||
* GitHub isn't connected, "Continue without GitHub" is a SKIP (handleSkip).
|
||||
*/
|
||||
onClick={isGitHubReadyViaCli ? handleNext : handleSkip}
|
||||
>
|
||||
{isGitHubReadyViaCli
|
||||
? t("setup.continueWithGhCli", "Continue with gh CLI auth →")
|
||||
: t("setup.continueWithoutGitHub", "Continue without GitHub →")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -3358,13 +3445,23 @@ export function ModelOnboardingModal({
|
||||
</a>
|
||||
{step === "ai-setup" && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={handleDismiss}
|
||||
disabled={saving}
|
||||
>
|
||||
{t("setup.skipForNow", "Skip for now")}
|
||||
</button>
|
||||
{/*
|
||||
FNXC:Onboarding 2026-07-10-10:40:
|
||||
First-run review: the AI Setup footer showed BOTH "Skip for now" (dismiss onboarding) and
|
||||
"Skip setup →" (skip this step) next to "Next →". Once at least one AI provider is
|
||||
connected the user has effectively completed this step, so "Skip for now" is redundant
|
||||
noise and is hidden; "Skip setup →" and "Next →" remain. No wrapper/aria-label is left
|
||||
behind — the button subtree is omitted entirely (desktop and mobile share this markup).
|
||||
*/}
|
||||
{!hasAiProvider && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={handleDismiss}
|
||||
disabled={saving}
|
||||
>
|
||||
{t("setup.skipForNow", "Skip for now")}
|
||||
</button>
|
||||
)}
|
||||
<button className="onboarding-skip-step-link" onClick={handleSkip}>
|
||||
{t("setup.skipSetup", "Skip setup →")}
|
||||
</button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Check, Loader2 } from "lucide-react";
|
||||
import { validateProjectPath, validateProjectName, suggestProjectName } from "../utils/projectDetection";
|
||||
import type { ProjectCreateInput, NodeInfo } from "../api";
|
||||
import { DirectoryPicker } from "./DirectoryPicker";
|
||||
import { getSelectableRuntimeNodes, shouldShowRuntimeNodeSelector } from "./setupWizardNodes";
|
||||
|
||||
export interface SetupProjectFormProps {
|
||||
/** Called when the form is submitted with valid data */
|
||||
@@ -119,24 +120,34 @@ export function SetupProjectForm({
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="setup-project-form">
|
||||
{/* Node selector */}
|
||||
<div className="form-group">
|
||||
<div className="project-node-selector">
|
||||
<span className="project-node-selector__label">{t("setup.runtimeNode", "Runtime Node")}</span>
|
||||
<select
|
||||
value={nodeId}
|
||||
onChange={(e) => setNodeId(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<option value="">{t("setup.localNode", "Local node")}</option>
|
||||
{nodes.map((node) => (
|
||||
<option key={node.id} value={node.id}>
|
||||
{node.name} ({node.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/*
|
||||
FNXC:SetupWizard 2026-07-10-11:00:
|
||||
Same Runtime Node dedupe as SetupWizardModal (Surface Enumeration for the duplicate
|
||||
"Local node" / "local (local)" option bug): local-type node records never render as
|
||||
options and the selector is hidden when only the local machine exists.
|
||||
*/}
|
||||
{shouldShowRuntimeNodeSelector(nodes) && (
|
||||
<div className="form-group">
|
||||
<div className="project-node-selector">
|
||||
<span className="project-node-selector__label">{t("setup.runtimeNode", "Runtime Node")}</span>
|
||||
<select
|
||||
value={nodeId}
|
||||
onChange={(e) => setNodeId(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<option value="">{t("setup.localNode", "Local node")}</option>
|
||||
{getSelectableRuntimeNodes(nodes).map((node) => (
|
||||
<option key={node.id} value={node.id}>
|
||||
{node.name} ({node.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<p className="form-hint">
|
||||
{t("setup.runtimeNodeHint", "A runtime node is the machine where this project's tasks run. \"Local node\" is this computer; pick a remote node to run tasks elsewhere.")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Path input */}
|
||||
<div className="form-group">
|
||||
|
||||
@@ -106,6 +106,19 @@
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:SetupWizard 2026-07-10-11:05:
|
||||
Eyebrow shown when this wizard runs as the onboarding "Project" step sub-flow, so the user keeps the
|
||||
"Step 3 of 5" context instead of feeling ejected into an unrelated modal.
|
||||
*/
|
||||
.setup-wizard-step-context {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.setup-wizard-content {
|
||||
padding: var(--space-md) var(--space-xl);
|
||||
flex: 1 1 auto;
|
||||
@@ -250,7 +263,13 @@
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.setup-wizard-manual .form-group input,
|
||||
/*
|
||||
FNXC:SetupWizard 2026-07-10-11:10:
|
||||
Exclude radios/checkboxes from the full-width text-input styling. This rule previously matched the
|
||||
isolation-mode radio inputs too, stretching the native radio into a full-width accent-colored bar
|
||||
across the option card on click/focus (first-run review bug).
|
||||
*/
|
||||
.setup-wizard-manual .form-group input:not([type="radio"]):not([type="checkbox"]),
|
||||
.setup-wizard-manual .form-group select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
@@ -262,7 +281,7 @@
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.setup-wizard-manual .form-group input:focus,
|
||||
.setup-wizard-manual .form-group input:not([type="radio"]):not([type="checkbox"]):focus,
|
||||
.setup-wizard-manual .form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--todo);
|
||||
@@ -560,7 +579,17 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:SetupWizard 2026-07-10-11:10:
|
||||
Isolation-mode option cards ARE the selectable control (first-run review): the native radio used to
|
||||
float centered in the card and stretched into a full-width orange bar on focus/click. The radio is now
|
||||
visually hidden (kept in the DOM for form semantics and keyboard selection), the card shows the
|
||||
selected state via border/ring, and focus-visible on the hidden radio draws the focus ring on the card.
|
||||
Descriptions render in sentence case — the global ".form-group label" uppercase transform is
|
||||
explicitly neutralized here because each option card is itself a <label> inside a .form-group.
|
||||
*/
|
||||
.setup-wizard-isolation-option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-md);
|
||||
@@ -569,6 +598,8 @@
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
border-color var(--transition-fast);
|
||||
@@ -586,8 +617,20 @@
|
||||
}
|
||||
|
||||
.setup-wizard-isolation-option input[type="radio"] {
|
||||
margin-top: 2px;
|
||||
accent-color: var(--todo);
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.setup-wizard-isolation-option:has(input[type="radio"]:focus-visible) {
|
||||
border-color: var(--todo);
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.setup-wizard-isolation-option-content {
|
||||
@@ -649,12 +692,23 @@
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.setup-wizard-footer .btn-primary {
|
||||
/*
|
||||
FNXC:SetupWizard 2026-07-10-10:55:
|
||||
Match ModelOnboarding footer sizing: all footer controls share the same height/type scale; the
|
||||
primary action keeps its filled emphasis and wider padding only.
|
||||
*/
|
||||
.setup-wizard-footer .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
padding: 10px 24px;
|
||||
min-height: 38px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.setup-wizard-footer .btn-primary {
|
||||
padding: 8px 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ function suggestTaskPrefixFromName(name: string): string {
|
||||
return "FN";
|
||||
}
|
||||
import { useNodes } from "../hooks/useNodes";
|
||||
import { getSelectableRuntimeNodes, shouldShowRuntimeNodeSelector } from "./setupWizardNodes";
|
||||
import { AgentAvatar } from "./AgentAvatar";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { AGENT_PRESETS, getPresetById } from "./agent-presets";
|
||||
@@ -404,8 +405,24 @@ export function SetupWizardModal({
|
||||
</svg>
|
||||
<span className="setup-wizard-brand-name">{t("setup.brandName", "Fusion")}</span>
|
||||
</div>
|
||||
{/*
|
||||
FNXC:SetupWizard 2026-07-10-11:05:
|
||||
First-run review: opening project registration from the 5-step onboarding wizard (its
|
||||
Project step) replaced the stepper chrome with a bare "Welcome to Fusion" modal, which read
|
||||
as being thrown out of setup. When this wizard runs as that sub-flow (includeAgentStep is
|
||||
false — see AppModals: it is only false while ModelOnboarding is driving), the header keeps
|
||||
the onboarding context explicit: a "Step 3 of 5 — Project" eyebrow plus the onboarding
|
||||
step's own title instead of the standalone welcome title.
|
||||
*/}
|
||||
{!includeAgentStep && state.step === "manual" && (
|
||||
<span className="setup-wizard-step-context">
|
||||
{t("setup.projectStepContext", "Step 3 of 5 — Project · Fusion setup")}
|
||||
</span>
|
||||
)}
|
||||
<h2 id="wizard-title" className="setup-wizard-title">
|
||||
{state.step === "manual" && t("setup.welcomeToFusion", "Welcome to Fusion")}
|
||||
{state.step === "manual" && (includeAgentStep
|
||||
? t("setup.welcomeToFusion", "Welcome to Fusion")
|
||||
: t("setup.titleSetUpProject", "Set Up Your Project"))}
|
||||
{state.step === "agent" && t("setup.firstAgentTitle", "Create your first agent")}
|
||||
{state.step === "complete" && t("setup.setupCompleteTitle", "Setup Complete!")}
|
||||
</h2>
|
||||
@@ -614,26 +631,49 @@ export function SetupWizardModal({
|
||||
</button>
|
||||
{showAdvancedSettings && (
|
||||
<div className="setup-wizard-advanced-panel">
|
||||
<div className="form-group">
|
||||
<div className="project-node-selector">
|
||||
<span className="project-node-selector__label">{t("setup.runtimeNode", "Runtime Node")}</span>
|
||||
<select
|
||||
value={state.manualNodeId}
|
||||
onChange={(e) => setState((prev) => ({ ...prev, manualNodeId: e.target.value }))}
|
||||
disabled={nodesLoading || state.isRegistering}
|
||||
>
|
||||
<option value="">{t("setup.localNode", "Local node")}</option>
|
||||
{nodes.map((node) => (
|
||||
<option key={node.id} value={node.id}>
|
||||
{node.name} ({node.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/*
|
||||
FNXC:SetupWizard 2026-07-10-11:00:
|
||||
Runtime Node dedupe (first-run review): registered local-type node records used to
|
||||
render as a second "local (local)" option next to the built-in "Local node" default.
|
||||
Local-type records are filtered out (see setupWizardNodes.ts) and the whole selector
|
||||
is hidden when only the local machine is available, with a plain-language description
|
||||
of what a runtime node is when the choice does exist.
|
||||
*/}
|
||||
{shouldShowRuntimeNodeSelector(nodes) && (
|
||||
<div className="form-group">
|
||||
<div className="project-node-selector">
|
||||
<span className="project-node-selector__label">{t("setup.runtimeNode", "Runtime Node")}</span>
|
||||
<select
|
||||
value={state.manualNodeId}
|
||||
onChange={(e) => setState((prev) => ({ ...prev, manualNodeId: e.target.value }))}
|
||||
disabled={nodesLoading || state.isRegistering}
|
||||
>
|
||||
<option value="">{t("setup.localNode", "Local node")}</option>
|
||||
{getSelectableRuntimeNodes(nodes).map((node) => (
|
||||
<option key={node.id} value={node.id}>
|
||||
{node.name} ({node.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<p className="form-hint">
|
||||
{t("setup.runtimeNodeHint", "A runtime node is the machine where this project's tasks run. \"Local node\" is this computer; pick a remote node to run tasks elsewhere.")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label>{t("setup.isolationMode", "Isolation Mode")}</label>
|
||||
{/*
|
||||
FNXC:SetupWizard 2026-07-10-11:10:
|
||||
First-run review: the isolation-mode cards rendered a raw radio dot floating in the
|
||||
card, and clicking/focusing stretched the native radio into a full-width accent bar
|
||||
(a global ".form-group input { width: 100% }" rule hit the radio). The whole card is
|
||||
the selectable control: the radio input is visually hidden (kept for semantics and
|
||||
keyboard toggling), selection/focus is shown via the card's border/ring, and the
|
||||
descriptions must read in sentence case — the global ".form-group label" uppercase
|
||||
transform is neutralized for these option cards. See SetupWizardModal.css.
|
||||
*/}
|
||||
<div className="setup-wizard-isolation-options">
|
||||
<label
|
||||
className={`setup-wizard-isolation-option${state.manualIsolationMode === "in-process" ? " selected" : ""}`}
|
||||
|
||||
@@ -338,6 +338,27 @@ describe("ModelOnboardingModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hides the redundant Skip for now on AI Setup once a provider is connected", async () => {
|
||||
// FNXC:Onboarding 2026-07-10-10:40: with a connected provider the user has effectively
|
||||
// completed the step, so only "Skip setup →" and "Next →" remain in the footer.
|
||||
mockFetchAuthStatus.mockResolvedValue({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: true, type: "oauth" },
|
||||
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Next →" })).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Skip for now" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Skip setup →" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Next →" })).toBeTruthy();
|
||||
});
|
||||
|
||||
// FNXC:Onboarding 2026-07-03: the "Research runs require provider credentials and an enabled
|
||||
// Research View" note was intentionally removed from onboarding at the operator's request (it
|
||||
// belongs in Settings, not first-run), so the assertion that it renders no longer applies.
|
||||
@@ -688,6 +709,34 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(screen.queryByTestId("onboarding-advanced-provider-settings")).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces connected quick-start providers in the Connected section at the top of the step", async () => {
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:15:
|
||||
After connecting a provider the user must immediately see it on step open, without scrolling:
|
||||
ALL connected providers render in "Connected providers" ABOVE quick start, and the connected
|
||||
provider leaves the quick-start list.
|
||||
*/
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: true, type: "oauth" },
|
||||
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
const connectedSection = await screen.findByTestId("onboarding-connected-providers");
|
||||
expect(getProviderOrderInSection(connectedSection)).toEqual(["anthropic"]);
|
||||
|
||||
const quickStartSection = screen.getByTestId("onboarding-quick-start-providers");
|
||||
expect(getProviderOrderInSection(quickStartSection)).toEqual(["openai"]);
|
||||
expect(
|
||||
connectedSection.compareDocumentPosition(quickStartSection) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
// The "N of M providers connected" summary keeps working with the new layout.
|
||||
expect(screen.getByTestId("provider-summary")).toHaveTextContent("1 of 2 providers connected");
|
||||
});
|
||||
|
||||
it("renders onboarding provider icon wrapper for connected advanced providers", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
@@ -1705,7 +1754,10 @@ describe("ModelOnboardingModal", () => {
|
||||
|
||||
expect(screen.getByText(/Dashboard GitHub OAuth is not configured/)).toBeTruthy();
|
||||
expect(screen.getByText(/GitHub CLI setup guidance above/)).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
||||
// FNXC:Onboarding 2026-07-10-10:35: the in-body "Continue without GitHub →" CTA was
|
||||
// removed — footer "Skip GitHub →" is the single skip affordance for this step.
|
||||
expect(screen.queryByRole("button", { name: "Continue without GitHub →" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Skip GitHub →" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows feature availability callout on GitHub step", async () => {
|
||||
@@ -1812,7 +1864,8 @@ describe("ModelOnboardingModal", () => {
|
||||
|
||||
expect(screen.getByTestId("onboarding-git-prerequisite")).toHaveTextContent("Git prerequisite ready");
|
||||
expect(screen.queryByTestId("github-status-badge")).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: "Continue without GitHub →" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Skip GitHub →" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not treat GitHub Copilot provider auth as GitHub integration readiness", async () => {
|
||||
@@ -1829,7 +1882,8 @@ describe("ModelOnboardingModal", () => {
|
||||
|
||||
expect(screen.queryByTestId("github-status-badge")).toBeNull();
|
||||
expect(screen.getByText(/Dashboard GitHub OAuth is not configured/)).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: "Continue without GitHub →" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Skip GitHub →" })).toBeTruthy();
|
||||
expect(screen.queryByText(/GitHub is connected — issue imports/)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1869,9 +1923,16 @@ describe("ModelOnboardingModal", () => {
|
||||
const installCard = screen.getByTestId("onboarding-gh-cli-install-card");
|
||||
expect(installCard).toHaveTextContent("Install GitHub CLI");
|
||||
expect(installCard).toHaveTextContent("host running Fusion");
|
||||
// FNXC:Onboarding 2026-07-10-10:30: the per-OS install wall of text is collapsed
|
||||
// behind a single disclosure; expand it to see the platform instructions, and the
|
||||
// `gh` / `brew install gh` commands render as real <code> elements.
|
||||
expect(installCard).not.toHaveTextContent("macOS");
|
||||
fireEvent.click(within(installCard).getByRole("button", { name: /Show install instructions/ }));
|
||||
expect(installCard).toHaveTextContent("macOS");
|
||||
expect(installCard).toHaveTextContent("Windows");
|
||||
expect(installCard).toHaveTextContent("Linux");
|
||||
expect(installCard.querySelector("code")).toBeTruthy();
|
||||
expect(installCard).not.toHaveTextContent("`gh`");
|
||||
expect(screen.getByRole("link", { name: "Open GitHub CLI releases" })).toHaveAttribute("href", "https://github.com/cli/cli/releases/latest");
|
||||
expect(screen.getByRole("button", { name: /Connect GitHub OAuth/ })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Skip GitHub →" })).toBeTruthy();
|
||||
@@ -1890,7 +1951,8 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(screen.getByTestId("onboarding-gh-cli-install-card")).toHaveTextContent("Install GitHub CLI");
|
||||
expect(screen.getByText(/Dashboard GitHub OAuth is not configured/)).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /Connect GitHub OAuth/ })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: "Continue without GitHub →" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Skip GitHub →" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("preserves missing-Git guidance when GitHub OAuth provider is absent and gh CLI is ready", async () => {
|
||||
@@ -1947,7 +2009,8 @@ describe("ModelOnboardingModal", () => {
|
||||
await navigateToGitHubStep();
|
||||
|
||||
expect(screen.queryByTestId("onboarding-git-prerequisite")).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: "Continue without GitHub →" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Skip GitHub →" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("GitHub step shows connected state when already authenticated", async () => {
|
||||
@@ -1975,6 +2038,86 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
|
||||
});
|
||||
|
||||
it("state-driven GitHub step: a ready connection suppresses install/auth guidance and the feature pitch", async () => {
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:30:
|
||||
Surface Enumeration for the state-driven step: when GitHub is ready via EITHER path (OAuth or
|
||||
gh CLI), neither gh CLI setup card nor the with/without feature list may render — one primary
|
||||
path per state. Previously an OAuth-connected user was still told to install the GitHub CLI.
|
||||
*/
|
||||
const readyScenarios = [
|
||||
{
|
||||
label: "OAuth connected, gh missing",
|
||||
providers: [{ id: "github", name: "GitHub", authenticated: true, type: "oauth" }],
|
||||
ghCli: { available: false, authenticated: false },
|
||||
},
|
||||
{
|
||||
label: "OAuth connected, gh unauthenticated",
|
||||
providers: [{ id: "github", name: "GitHub", authenticated: true, type: "oauth" }],
|
||||
ghCli: { available: true, authenticated: false },
|
||||
},
|
||||
{
|
||||
label: "gh authenticated, no OAuth provider",
|
||||
providers: [],
|
||||
ghCli: { available: true, authenticated: true },
|
||||
},
|
||||
];
|
||||
|
||||
for (const scenario of readyScenarios) {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: scenario.providers,
|
||||
ghCli: scenario.ghCli,
|
||||
});
|
||||
|
||||
const { unmount } = render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
expect(screen.queryByTestId("onboarding-gh-cli-install-card")).toBeNull();
|
||||
expect(screen.queryByTestId("onboarding-gh-cli-auth-card")).toBeNull();
|
||||
expect(document.querySelector(".onboarding-feature-list")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Continue without GitHub →" })).toBeNull();
|
||||
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("revalidates GitHub connection status when the window regains focus (mid-session gh auth)", async () => {
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:10:
|
||||
Regression test for the stale-status bug: the user authenticated the gh CLI in a terminal while
|
||||
the wizard stayed open, and later steps still showed GitHub as not connected. Regaining window
|
||||
focus must refetch auth/gh status and flip the step to connected.
|
||||
*/
|
||||
mockFetchAuthStatus.mockImplementation(() => Promise.resolve({
|
||||
providers: [
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
ghCli: { available: true, authenticated: false },
|
||||
}));
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
expect(screen.getByTestId("github-status-badge")).toHaveTextContent("Not connected");
|
||||
|
||||
// The user runs `gh auth login` in a terminal, then switches back to the dashboard.
|
||||
mockFetchAuthStatus.mockImplementation(() => Promise.resolve({
|
||||
providers: [
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
ghCli: { available: true, authenticated: true },
|
||||
}));
|
||||
|
||||
fireEvent(window, new Event("focus"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("github-status-badge")).toHaveTextContent("✓ Connected");
|
||||
});
|
||||
expect(screen.queryByTestId("onboarding-gh-cli-auth-card")).toBeNull();
|
||||
});
|
||||
|
||||
it("GitHub step explains gh CLI auth when OAuth provider is absent", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [],
|
||||
@@ -2211,12 +2354,14 @@ describe("ModelOnboardingModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("allows navigating to First Task step via Continue without GitHub", async () => {
|
||||
it("allows navigating to First Task step via footer Skip GitHub", async () => {
|
||||
// FNXC:Onboarding 2026-07-10-10:35: the in-body "Continue without GitHub →" CTA was
|
||||
// removed; footer "Skip GitHub →" is the single skip affordance for this step.
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
await navigateToGitHubStep();
|
||||
|
||||
fireEvent.click(screen.getByText("Continue without GitHub →"));
|
||||
fireEvent.click(screen.getByText("Skip GitHub →"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||
|
||||
@@ -147,21 +147,21 @@ describe("SetupProjectForm", () => {
|
||||
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
||||
expect(select.value).toBe("");
|
||||
|
||||
// Check options directly by querying the select's children
|
||||
// FNXC:SetupWizard 2026-07-10-11:00: registered local-type node records must NOT
|
||||
// render as a second near-identical option next to the built-in "Local node" default.
|
||||
const options = Array.from(select.querySelectorAll("option"));
|
||||
const optionValues = options.map((opt) => opt.value);
|
||||
expect(optionValues).toContain("local-1");
|
||||
expect(optionValues).not.toContain("local-1");
|
||||
expect(optionValues).toContain("remote-1");
|
||||
expect(optionValues.filter((value) => value === "")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not render extra node options when only local node is in list", () => {
|
||||
it("hides the node selector when only a local node is in the list", () => {
|
||||
render(<SetupProjectForm onSubmit={vi.fn()} nodes={[localNode]} />);
|
||||
// When only the local node is provided, it appears as the default option
|
||||
// The extra options from nodes.map should include localNode's entry
|
||||
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
||||
const options = Array.from(select.querySelectorAll("option"));
|
||||
// Should have at least the local node option
|
||||
expect(options.some((opt) => opt.value === "local-1")).toBe(true);
|
||||
// FNXC:SetupWizard 2026-07-10-11:00: a single-local-node install has no runtime
|
||||
// choice to make — the selector (and its duplicate local option) is gone entirely.
|
||||
expect(screen.queryByText("Runtime Node")).toBeNull();
|
||||
expect(screen.queryByRole("combobox")).toBeNull();
|
||||
});
|
||||
|
||||
it("uses selectedNodeId as default value", () => {
|
||||
|
||||
@@ -170,6 +170,23 @@ describe("SetupWizardModal", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps onboarding step context when opened as the onboarding Project sub-flow", () => {
|
||||
// FNXC:SetupWizard 2026-07-10-11:05: opened from the 5-step onboarding wizard
|
||||
// (includeAgentStep=false), the header must keep the "Step 3 of 5" context instead
|
||||
// of the standalone "Welcome to Fusion" chrome, so users don't feel ejected from setup.
|
||||
render(
|
||||
<SetupWizardModal
|
||||
onProjectRegistered={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
includeAgentStep={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Step 3 of 5 — Project · Fusion setup")).toBeDefined();
|
||||
expect(screen.getByText("Set Up Your Project")).toBeDefined();
|
||||
expect(screen.queryByText("Welcome to Fusion")).toBeNull();
|
||||
});
|
||||
|
||||
it("has DirectoryPicker for path selection", () => {
|
||||
render(
|
||||
<SetupWizardModal
|
||||
@@ -872,7 +889,9 @@ describe("SetupWizardModal", () => {
|
||||
|
||||
fireEvent.click(screen.getByText("Advanced settings"));
|
||||
|
||||
expect(screen.getByText("Runtime Node")).toBeDefined();
|
||||
// FNXC:SetupWizard 2026-07-10-11:00: with no non-local node registered there is
|
||||
// nothing to choose, so the Runtime Node selector stays hidden in advanced settings.
|
||||
expect(screen.queryByText("Runtime Node")).toBeNull();
|
||||
expect(screen.getByText("In-Process")).toBeDefined();
|
||||
expect(screen.getByText("Child-Process")).toBeDefined();
|
||||
expect(screen.getByText("Recommended")).toBeDefined();
|
||||
@@ -945,12 +964,16 @@ describe("SetupWizardModal", () => {
|
||||
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
||||
expect(select.value).toBe("");
|
||||
|
||||
// Check options directly by querying the select's children
|
||||
// FNXC:SetupWizard 2026-07-10-11:00: registered local-type node records must NOT
|
||||
// render as a second near-identical "local (local)" option — the built-in
|
||||
// "Local node" default option already represents the local machine.
|
||||
const options = Array.from(select.querySelectorAll("option"));
|
||||
const optionValues = options.map((opt) => opt.value);
|
||||
const optionLabels = options.map((opt) => opt.label);
|
||||
expect(optionValues).toContain("local-1");
|
||||
expect(optionValues).not.toContain("local-1");
|
||||
expect(optionValues).toContain("remote-1");
|
||||
expect(optionValues.filter((value) => value === "")).toHaveLength(1);
|
||||
// Plain-language explanation of what a runtime node is.
|
||||
expect(screen.getByText(/A runtime node is the machine where this project's tasks run/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("registration includes selected nodeId", async () => {
|
||||
@@ -1004,7 +1027,7 @@ describe("SetupWizardModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("registration includes undefined nodeId when local node selected", async () => {
|
||||
it("registration includes undefined nodeId when only a local node exists (selector hidden)", async () => {
|
||||
mockUseNodes.mockImplementation(() => ({
|
||||
nodes: [localNode],
|
||||
loading: false,
|
||||
@@ -1028,9 +1051,10 @@ describe("SetupWizardModal", () => {
|
||||
|
||||
fireEvent.click(screen.getByText("Advanced settings"));
|
||||
|
||||
// Local node is selected by default (empty value)
|
||||
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
||||
expect(select.value).toBe("");
|
||||
// FNXC:SetupWizard 2026-07-10-11:00: a single-local-node install has no runtime
|
||||
// choice to make, so the selector is hidden and registration runs locally.
|
||||
expect(screen.queryByText("Runtime Node")).toBeNull();
|
||||
expect(screen.queryByRole("combobox")).toBeNull();
|
||||
|
||||
// Fill path and name
|
||||
fireEvent.change(screen.getByPlaceholderText("/path/to/your/project"), {
|
||||
|
||||
@@ -448,11 +448,19 @@ describe("onboarding flow integration", () => {
|
||||
expect(screen.getByText("You only need one provider to get started.")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /Advanced provider settings/ })).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-07-10-10:15:
|
||||
ALL connected providers (quick-start and advanced alike) render in the "Connected providers"
|
||||
section at the TOP of the step, above quick start; quick start only offers unconnected providers.
|
||||
*/
|
||||
const quickStartSection = screen.getByTestId("onboarding-quick-start-providers");
|
||||
expect(getProviderOrderInSection(quickStartSection)).toEqual(["anthropic", "openai", "google", "openrouter"]);
|
||||
expect(getProviderOrderInSection(quickStartSection)).toEqual(["openai", "google", "openrouter"]);
|
||||
|
||||
const connectedSection = screen.getByTestId("onboarding-connected-providers");
|
||||
expect(getProviderOrderInSection(connectedSection)).toEqual(["zai"]);
|
||||
expect(getProviderOrderInSection(connectedSection)).toEqual(["anthropic", "zai"]);
|
||||
expect(
|
||||
connectedSection.compareDocumentPosition(quickStartSection) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId("onboarding-provider-card-minimax")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("onboarding-provider-card-moonshot")).not.toBeInTheDocument();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { NodeInfo } from "../../api";
|
||||
import { getSelectableRuntimeNodes, shouldShowRuntimeNodeSelector } from "../setupWizardNodes";
|
||||
|
||||
/*
|
||||
FNXC:SetupWizard 2026-07-10-11:00:
|
||||
First-run review: the wizard's Runtime Node dropdown showed both the built-in "Local node" default and
|
||||
a registered local node record as "local (local)". These tests pin the dedupe rule: local-type node
|
||||
records never appear as options, and the selector only exists when a non-local node is registered.
|
||||
*/
|
||||
|
||||
function buildNode(overrides: Partial<NodeInfo> & Pick<NodeInfo, "id" | "type">): NodeInfo {
|
||||
return {
|
||||
name: overrides.id,
|
||||
status: "online",
|
||||
maxConcurrent: 2,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
} as NodeInfo;
|
||||
}
|
||||
|
||||
describe("setupWizardNodes", () => {
|
||||
it("filters registered local-type nodes out of the selectable list", () => {
|
||||
const nodes = [
|
||||
buildNode({ id: "local-1", type: "local" }),
|
||||
buildNode({ id: "remote-1", type: "remote" }),
|
||||
];
|
||||
|
||||
expect(getSelectableRuntimeNodes(nodes).map((node) => node.id)).toEqual(["remote-1"]);
|
||||
});
|
||||
|
||||
it("hides the selector when only local nodes exist (single-local-node install)", () => {
|
||||
expect(shouldShowRuntimeNodeSelector([buildNode({ id: "local-1", type: "local" })])).toBe(false);
|
||||
expect(shouldShowRuntimeNodeSelector([])).toBe(false);
|
||||
});
|
||||
|
||||
it("shows the selector when at least one non-local node exists", () => {
|
||||
const nodes = [
|
||||
buildNode({ id: "local-1", type: "local" }),
|
||||
buildNode({ id: "remote-1", type: "remote" }),
|
||||
];
|
||||
expect(shouldShowRuntimeNodeSelector(nodes)).toBe(true);
|
||||
});
|
||||
});
|
||||
24
packages/dashboard/app/components/setupWizardNodes.ts
Normal file
24
packages/dashboard/app/components/setupWizardNodes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { NodeInfo } from "../api";
|
||||
|
||||
/*
|
||||
FNXC:SetupWizard 2026-07-10-11:00:
|
||||
First-run review (project registration, Advanced settings): the Runtime Node dropdown showed two
|
||||
near-identical options — the built-in "Local node" default option (empty value = run on this machine)
|
||||
PLUS a registered local node record rendered as "local (local)". A single-local-node install must show
|
||||
exactly one local choice, so:
|
||||
- registered nodes of type "local" are filtered out of the option list (the built-in default option
|
||||
already represents them; registering with an empty nodeId runs locally), and
|
||||
- the selector is hidden entirely when there is no non-local node to pick — a choice with one option
|
||||
is noise during first-run setup.
|
||||
Kept as a pure module so the dedupe rule is unit-testable without rendering the wizard.
|
||||
*/
|
||||
|
||||
/** Nodes that are meaningful to offer in the Runtime Node dropdown (everything except local-type records). */
|
||||
export function getSelectableRuntimeNodes(nodes: NodeInfo[]): NodeInfo[] {
|
||||
return nodes.filter((node) => node.type !== "local");
|
||||
}
|
||||
|
||||
/** The Runtime Node selector is only shown when there is at least one non-local node to choose. */
|
||||
export function shouldShowRuntimeNodeSelector(nodes: NodeInfo[]): boolean {
|
||||
return getSelectableRuntimeNodes(nodes).length > 0;
|
||||
}
|
||||
@@ -6849,11 +6849,12 @@
|
||||
"githubCliAuthTitle": "Authenticate GitHub CLI",
|
||||
"githubCliInstallBody": "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication.",
|
||||
"githubCliInstallLink": "Open GitHub CLI releases",
|
||||
"githubCliInstallShowInstructions": "Show install instructions",
|
||||
"githubCliInstallLinux": "Linux: install `gh` with your distribution package manager or the packages from cli.github.com.",
|
||||
"githubCliInstallMac": "macOS: `brew install gh` or download the installer from GitHub CLI releases.",
|
||||
"githubCliInstallTitle": "Install GitHub CLI",
|
||||
"githubCliInstallWindows": "Windows: install GitHub CLI with WinGet, Chocolatey, or the GitHub CLI installer, then restart the Fusion host shell or service.",
|
||||
"githubOauthUnavailable": "Dashboard GitHub OAuth is not configured on this Fusion host. You can still continue without GitHub, or use the GitHub CLI setup guidance above when available.",
|
||||
"githubOauthUnavailable": "Dashboard GitHub OAuth is not configured on this Fusion host. You can still skip GitHub below, or use the GitHub CLI setup guidance above when available.",
|
||||
"githubCliAuthNote": "GitHub CLI is already authenticated, so imports and PR tracking work now. OAuth from the dashboard is optional and only controls dashboard-managed connect/disconnect.",
|
||||
"githubCliAuthSuccess": "GitHub CLI is authenticated. Imports and pull request tracking are available. Connect OAuth in Settings → Authentication if you want dashboard-managed sign-in controls.",
|
||||
"githubConnected": "GitHub is connected — issue imports and pull request tracking are available. You're all set; no further action needed.",
|
||||
@@ -6927,12 +6928,14 @@
|
||||
"projectRegisteredSuccess": "Your project has been registered successfully.",
|
||||
"projectRequired": "A project is required before first-task actions are available.",
|
||||
"projectSelected": "Project selected — task creation and imports are available.",
|
||||
"projectStepContext": "Step 3 of 5 — Project · Fusion setup",
|
||||
"projectSetupDescription": "Choose your first project before creating or importing tasks. You can register an existing local directory or clone a GitHub repository URL through the setup wizard.",
|
||||
"providersConnectedSummary": "✓ {{connected}} of {{total}} provider(s) connected",
|
||||
"providersSkippedSummary_one_one": "{{count}} provider skipped",
|
||||
"providersSkippedSummary_one_other": "{{count}} provider skipped",
|
||||
"providersSkippedSummary_other_one": "{{count}} providers skipped",
|
||||
"providersSkippedSummary_other_other": "{{count}} providers skipped",
|
||||
"quickStartAllConnected": "All quick-start providers are already connected.",
|
||||
"quickStartProviders": "Quick start providers",
|
||||
"readinessAiProviderConnected": "{{name}} connected — AI agents can work on tasks",
|
||||
"readinessAiProviderLabel": "AI Provider",
|
||||
@@ -6964,6 +6967,7 @@
|
||||
"retry": "Retry",
|
||||
"reviewStep": "Review {{label}}",
|
||||
"runtimeNode": "Runtime Node",
|
||||
"runtimeNodeHint": "A runtime node is the machine where this project's tasks run. \"Local node\" is this computer; pick a remote node to run tasks elsewhere.",
|
||||
"savedProfileButFailedToActivate": "Saved profile but failed to activate it",
|
||||
"saveKey": "Save",
|
||||
"saveRemoteServer": "Save remote server",
|
||||
@@ -7030,11 +7034,11 @@
|
||||
"withGitHub1": "Import issues as tasks",
|
||||
"withGitHub2": "Sync pull request status",
|
||||
"withGitHub3": "Link code changes to tasks",
|
||||
"withGitHubHeading": "With GitHub (after connecting):",
|
||||
"withGitHubHeading": "With GitHub (after connecting)",
|
||||
"withoutGitHub1": "Create tasks manually",
|
||||
"withoutGitHub2": "Describe work for AI agents",
|
||||
"withoutGitHub3": "Track progress on the board",
|
||||
"withoutGitHubHeading": "Without GitHub (available now):",
|
||||
"withoutGitHubHeading": "Without GitHub (available now)",
|
||||
"brandLogo": "Fusion logo",
|
||||
"brandName": "Fusion",
|
||||
"setupCompleteTitle": "Setup Complete!",
|
||||
|
||||
@@ -1386,7 +1386,8 @@
|
||||
"untitledSession": "Sin título",
|
||||
"workingStatus": "Trabajando…",
|
||||
"you": "Tú",
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens",
|
||||
"noMessage": ""
|
||||
},
|
||||
"chatRooms": {
|
||||
"error": {
|
||||
@@ -6837,6 +6838,7 @@
|
||||
"githubCliAuthBody": "GitHub CLI is installed but not authenticated on the Fusion host. Run this command in the environment where Fusion is running, then return here and continue.",
|
||||
"githubCliAuthTitle": "Authenticate GitHub CLI",
|
||||
"githubCliInstallBody": "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication.",
|
||||
"githubCliInstallShowInstructions": "Mostrar instrucciones de instalación",
|
||||
"githubCliInstallLink": "Open GitHub CLI releases",
|
||||
"githubCliInstallLinux": "Linux: install `gh` with your distribution package manager or the packages from cli.github.com.",
|
||||
"githubCliInstallMac": "macOS: `brew install gh` or download the installer from GitHub CLI releases.",
|
||||
@@ -6916,12 +6918,14 @@
|
||||
"projectRegisteredSuccess": "Su proyecto se ha registrado exitosamente.",
|
||||
"projectRequired": "Se requiere un proyecto antes de que las acciones de primera tarea estén disponibles.",
|
||||
"projectSelected": "Proyecto seleccionado — la creación de tareas y las importaciones están disponibles.",
|
||||
"projectStepContext": "Paso 3 de 5 — Proyecto · Configuración de Fusion",
|
||||
"projectSetupDescription": "Elige tu primer proyecto antes de crear o importar tareas. Puedes registrar un directorio local existente o clonar una URL de repositorio de GitHub a través del asistente de configuración.",
|
||||
"providersConnectedSummary": "✓ {{connected}} de {{total}} proveedor(es) conectado(s)",
|
||||
"providersSkippedSummary_one_one": "",
|
||||
"providersSkippedSummary_one_other": "",
|
||||
"providersSkippedSummary_other_one": "",
|
||||
"providersSkippedSummary_other_other": "",
|
||||
"quickStartAllConnected": "Todos los proveedores de inicio rápido ya están conectados.",
|
||||
"quickStartProviders": "Proveedores de inicio rápido",
|
||||
"readinessAiProviderConnected": "{{name}} conectado — los agentes de IA pueden trabajar en las tareas",
|
||||
"readinessAiProviderLabel": "Proveedor de IA",
|
||||
@@ -6953,6 +6957,7 @@
|
||||
"retry": "Reintentar",
|
||||
"reviewStep": "Revisar {{label}}",
|
||||
"runtimeNode": "Nodo en tiempo de ejecución",
|
||||
"runtimeNodeHint": "Un nodo de ejecución es la máquina donde se ejecutan las tareas de este proyecto. \"Nodo local\" es este equipo; elige un nodo remoto para ejecutar tareas en otro lugar.",
|
||||
"savedProfileButFailedToActivate": "Se guardó el perfil pero no se pudo activar",
|
||||
"saveKey": "Guardar",
|
||||
"saveRemoteServer": "Guardar servidor remoto",
|
||||
|
||||
@@ -1386,7 +1386,8 @@
|
||||
"untitledSession": "Sans titre",
|
||||
"workingStatus": "Traitement en cours…",
|
||||
"you": "Vous",
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens",
|
||||
"noMessage": ""
|
||||
},
|
||||
"chatRooms": {
|
||||
"error": {
|
||||
@@ -6837,6 +6838,7 @@
|
||||
"githubCliAuthBody": "GitHub CLI is installed but not authenticated on the Fusion host. Run this command in the environment where Fusion is running, then return here and continue.",
|
||||
"githubCliAuthTitle": "Authenticate GitHub CLI",
|
||||
"githubCliInstallBody": "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication.",
|
||||
"githubCliInstallShowInstructions": "Afficher les instructions d'installation",
|
||||
"githubCliInstallLink": "Open GitHub CLI releases",
|
||||
"githubCliInstallLinux": "Linux: install `gh` with your distribution package manager or the packages from cli.github.com.",
|
||||
"githubCliInstallMac": "macOS: `brew install gh` or download the installer from GitHub CLI releases.",
|
||||
@@ -6916,12 +6918,14 @@
|
||||
"projectRegisteredSuccess": "Votre projet a été enregistré avec succès.",
|
||||
"projectRequired": "Un projet est requis avant que les actions de première tâche soient disponibles.",
|
||||
"projectSelected": "Projet sélectionné — la création de tâches et les imports sont disponibles.",
|
||||
"projectStepContext": "Étape 3 sur 5 — Projet · Configuration de Fusion",
|
||||
"projectSetupDescription": "Choisissez votre premier projet avant de créer ou d'importer des tâches. Vous pouvez enregistrer un répertoire local existant ou cloner une URL de dépôt GitHub via l'assistant de configuration.",
|
||||
"providersConnectedSummary": "✓ {{connected}} fournisseur(s) sur {{total}} connecté(s)",
|
||||
"providersSkippedSummary_one_one": "",
|
||||
"providersSkippedSummary_one_other": "",
|
||||
"providersSkippedSummary_other_one": "",
|
||||
"providersSkippedSummary_other_other": "",
|
||||
"quickStartAllConnected": "Tous les fournisseurs de démarrage rapide sont déjà connectés.",
|
||||
"quickStartProviders": "Fournisseurs de démarrage rapide",
|
||||
"readinessAiProviderConnected": "{{name}} connecté — les agents IA peuvent travailler sur les tâches",
|
||||
"readinessAiProviderLabel": "Fournisseur IA",
|
||||
@@ -6953,6 +6957,7 @@
|
||||
"retry": "Réessayer",
|
||||
"reviewStep": "Revoir {{label}}",
|
||||
"runtimeNode": "Nœud d'exécution",
|
||||
"runtimeNodeHint": "Un nœud d'exécution est la machine où s'exécutent les tâches de ce projet. « Nœud local » désigne cet ordinateur ; choisissez un nœud distant pour exécuter les tâches ailleurs.",
|
||||
"savedProfileButFailedToActivate": "Profil enregistré mais échec de l'activation",
|
||||
"saveKey": "Enregistrer",
|
||||
"saveRemoteServer": "Enregistrer le serveur distant",
|
||||
|
||||
@@ -1386,7 +1386,8 @@
|
||||
"untitledSession": "제목 없음",
|
||||
"workingStatus": "작업 중…",
|
||||
"you": "나",
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens",
|
||||
"noMessage": ""
|
||||
},
|
||||
"chatRooms": {
|
||||
"error": {
|
||||
@@ -6837,6 +6838,7 @@
|
||||
"githubCliAuthBody": "GitHub CLI is installed but not authenticated on the Fusion host. Run this command in the environment where Fusion is running, then return here and continue.",
|
||||
"githubCliAuthTitle": "Authenticate GitHub CLI",
|
||||
"githubCliInstallBody": "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication.",
|
||||
"githubCliInstallShowInstructions": "설치 안내 보기",
|
||||
"githubCliInstallLink": "Open GitHub CLI releases",
|
||||
"githubCliInstallLinux": "Linux: install `gh` with your distribution package manager or the packages from cli.github.com.",
|
||||
"githubCliInstallMac": "macOS: `brew install gh` or download the installer from GitHub CLI releases.",
|
||||
@@ -6916,12 +6918,14 @@
|
||||
"projectRegisteredSuccess": "프로젝트가 성공적으로 등록되었습니다.",
|
||||
"projectRequired": "첫 번째 작업 동작을 사용하려면 프로젝트가 필요합니다.",
|
||||
"projectSelected": "프로젝트 선택됨 — 작업 생성 및 가져오기를 사용할 수 있습니다.",
|
||||
"projectStepContext": "5단계 중 3단계 — 프로젝트 · Fusion 설정",
|
||||
"projectSetupDescription": "작업을 생성하거나 가져오기 전에 첫 번째 프로젝트를 선택하세요. 기존 로컬 디렉터리를 등록하거나, 설정 마법사를 통해 GitHub 저장소 URL을 클론할 수 있습니다.",
|
||||
"providersConnectedSummary": "✓ {{total}}개 제공자 중 {{connected}}개 연결됨",
|
||||
"providersSkippedSummary_one_one": "",
|
||||
"providersSkippedSummary_one_other": "",
|
||||
"providersSkippedSummary_other_one": "",
|
||||
"providersSkippedSummary_other_other": "",
|
||||
"quickStartAllConnected": "빠른 시작 제공자가 모두 이미 연결되어 있습니다.",
|
||||
"quickStartProviders": "빠른 시작 제공자",
|
||||
"readinessAiProviderConnected": "{{name}} 연결됨 — AI 에이전트가 작업을 처리할 수 있습니다",
|
||||
"readinessAiProviderLabel": "AI 제공자",
|
||||
@@ -6953,6 +6957,7 @@
|
||||
"retry": "재시도",
|
||||
"reviewStep": "{{label}} 검토",
|
||||
"runtimeNode": "런타임 노드",
|
||||
"runtimeNodeHint": "런타임 노드는 이 프로젝트의 작업이 실행되는 머신입니다. \"로컬 노드\"는 이 컴퓨터이며, 다른 곳에서 작업을 실행하려면 원격 노드를 선택하세요.",
|
||||
"savedProfileButFailedToActivate": "프로필을 저장했지만 활성화에 실패했습니다",
|
||||
"saveKey": "저장",
|
||||
"saveRemoteServer": "원격 서버 저장",
|
||||
|
||||
@@ -1386,7 +1386,8 @@
|
||||
"untitledSession": "无标题",
|
||||
"workingStatus": "处理中……",
|
||||
"you": "你",
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens",
|
||||
"noMessage": ""
|
||||
},
|
||||
"chatRooms": {
|
||||
"error": {
|
||||
@@ -6837,6 +6838,7 @@
|
||||
"githubCliAuthBody": "GitHub CLI is installed but not authenticated on the Fusion host. Run this command in the environment where Fusion is running, then return here and continue.",
|
||||
"githubCliAuthTitle": "Authenticate GitHub CLI",
|
||||
"githubCliInstallBody": "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication.",
|
||||
"githubCliInstallShowInstructions": "显示安装说明",
|
||||
"githubCliInstallLink": "Open GitHub CLI releases",
|
||||
"githubCliInstallLinux": "Linux: install `gh` with your distribution package manager or the packages from cli.github.com.",
|
||||
"githubCliInstallMac": "macOS: `brew install gh` or download the installer from GitHub CLI releases.",
|
||||
@@ -6916,12 +6918,14 @@
|
||||
"projectRegisteredSuccess": "您的项目已成功注册。",
|
||||
"projectRequired": "在可以使用第一个任务操作之前,需要先有一个项目。",
|
||||
"projectSelected": "已选择项目——任务创建和导入功能已可用。",
|
||||
"projectStepContext": "第 3 步(共 5 步)— 项目 · Fusion 设置",
|
||||
"projectSetupDescription": "在创建或导入任务之前,请选择您的第一个项目。您可以注册现有的本地目录,或通过设置向导克隆 GitHub 仓库 URL。",
|
||||
"providersConnectedSummary": "✓ {{total}} 个提供商中已连接 {{connected}} 个",
|
||||
"providersSkippedSummary_one_one": "",
|
||||
"providersSkippedSummary_one_other": "",
|
||||
"providersSkippedSummary_other_one": "",
|
||||
"providersSkippedSummary_other_other": "",
|
||||
"quickStartAllConnected": "所有快速开始提供商均已连接。",
|
||||
"quickStartProviders": "快速启动提供商",
|
||||
"readinessAiProviderConnected": "{{name}} 已连接——AI 代理可以处理任务",
|
||||
"readinessAiProviderLabel": "AI 提供商",
|
||||
@@ -6953,6 +6957,7 @@
|
||||
"retry": "重试",
|
||||
"reviewStep": "查看{{label}}",
|
||||
"runtimeNode": "运行时节点",
|
||||
"runtimeNodeHint": "运行时节点是运行此项目任务的机器。\"本地节点\"即这台电脑;选择远程节点可在其他地方运行任务。",
|
||||
"savedProfileButFailedToActivate": "配置文件已保存但激活失败",
|
||||
"saveKey": "保存",
|
||||
"saveRemoteServer": "保存远程服务器",
|
||||
|
||||
@@ -1386,7 +1386,8 @@
|
||||
"untitledSession": "未命名",
|
||||
"workingStatus": "處理中……",
|
||||
"you": "你",
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
|
||||
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens",
|
||||
"noMessage": ""
|
||||
},
|
||||
"chatRooms": {
|
||||
"error": {
|
||||
@@ -6837,6 +6838,7 @@
|
||||
"githubCliAuthBody": "GitHub CLI is installed but not authenticated on the Fusion host. Run this command in the environment where Fusion is running, then return here and continue.",
|
||||
"githubCliAuthTitle": "Authenticate GitHub CLI",
|
||||
"githubCliInstallBody": "Fusion could not find `gh` on the host running Fusion. Install GitHub CLI there to import issues and track pull requests with CLI authentication.",
|
||||
"githubCliInstallShowInstructions": "顯示安裝說明",
|
||||
"githubCliInstallLink": "Open GitHub CLI releases",
|
||||
"githubCliInstallLinux": "Linux: install `gh` with your distribution package manager or the packages from cli.github.com.",
|
||||
"githubCliInstallMac": "macOS: `brew install gh` or download the installer from GitHub CLI releases.",
|
||||
@@ -6916,12 +6918,14 @@
|
||||
"projectRegisteredSuccess": "您的專案已成功註冊。",
|
||||
"projectRequired": "在可以使用第一個任務操作之前,需要先有一個專案。",
|
||||
"projectSelected": "已選擇專案——任務建立和匯入功能已可用。",
|
||||
"projectStepContext": "第 3 步(共 5 步)— 專案 · Fusion 設定",
|
||||
"projectSetupDescription": "在建立或匯入任務之前,請選擇您的第一個專案。您可以註冊現有的本機目錄,或透過設定精靈複製 GitHub 儲存庫 URL。",
|
||||
"providersConnectedSummary": "✓ {{total}} 個提供商中已連接 {{connected}} 個",
|
||||
"providersSkippedSummary_one_one": "",
|
||||
"providersSkippedSummary_one_other": "",
|
||||
"providersSkippedSummary_other_one": "",
|
||||
"providersSkippedSummary_other_other": "",
|
||||
"quickStartAllConnected": "所有快速開始供應商均已連線。",
|
||||
"quickStartProviders": "快速啟動提供商",
|
||||
"readinessAiProviderConnected": "{{name}} 已連接——AI 代理可以處理任務",
|
||||
"readinessAiProviderLabel": "AI 提供商",
|
||||
@@ -6953,6 +6957,7 @@
|
||||
"retry": "重試",
|
||||
"reviewStep": "查看{{label}}",
|
||||
"runtimeNode": "執行時節點",
|
||||
"runtimeNodeHint": "執行節點是執行此專案任務的機器。「本機節點」即這台電腦;選擇遠端節點可在其他地方執行任務。",
|
||||
"savedProfileButFailedToActivate": "設定檔已儲存但啟用失敗",
|
||||
"saveKey": "儲存",
|
||||
"saveRemoteServer": "儲存遠端伺服器",
|
||||
|
||||
Reference in New Issue
Block a user