fix(dashboard): wider/resizable onboarding modal + GitHub CLI-aware messaging

- ModelOnboardingModal: native CSS resize with persisted size via
  useModalResizePersist; default ~1100px wide; modal scrolls inside
  the content area instead of clipping.
- Provider cards flex-wrap so the API-key form drops to a full-width
  row instead of being squished into a fixed side column. Connected
  badge no longer stretches the full body width.
- Step changes now reset content scrollTop so each page lands at the
  top.
- GitHub step: prominent GitHub mark via ProviderIcon; when gh CLI is
  authenticated the intro reads as 'you are all set', primary CTA
  becomes 'Continue with gh CLI auth', and a secondary
  'Connect OAuth (optional)' button is offered.
- CustomModelDropdown: highlight init runs once per open session, and
  filter changes reset highlight to top + scrollTop=0, fixing the
  scroll-fight when filtering models.
- TUI dashboard: at >=150 cols, Stats panel sits to the left of Logs;
  bottom row drops to Utilities + Settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-27 20:05:45 -07:00
parent 4b9fd3dd64
commit f3d918997c
4 changed files with 249 additions and 74 deletions

View File

@@ -283,16 +283,36 @@ export function CustomModelDropdown({
setPortalRoot(document.body);
}, []);
// Reset highlighted index when opening
// Initialise the highlighted option on open. We only seed once per open
// session — re-seeding on every optionsList change (which fires on each
// keystroke into the filter) was snapping highlightedIndex back to the
// current model's position, and the scrollIntoView effect below then
// yanked the list back to that row, making filtering feel like the
// dropdown was "refreshing" or fighting the user's scroll.
const didInitHighlightRef = useRef(false);
useEffect(() => {
if (isOpen) {
const selectableIndex = optionsList.findIndex(
(opt, idx) => idx >= (currentValueIndex >= 0 ? currentValueIndex : 0) && opt.type !== "provider"
);
setHighlightedIndex(selectableIndex >= 0 ? selectableIndex : 0);
if (!isOpen) {
didInitHighlightRef.current = false;
return;
}
if (didInitHighlightRef.current) return;
if (optionsList.length === 0) return;
const selectableIndex = optionsList.findIndex(
(opt, idx) => idx >= (currentValueIndex >= 0 ? currentValueIndex : 0) && opt.type !== "provider"
);
setHighlightedIndex(selectableIndex >= 0 ? selectableIndex : 0);
didInitHighlightRef.current = true;
}, [isOpen, optionsList, currentValueIndex]);
// When the filter changes, reset to the first option and scroll the list
// back to the top instead of keeping a now-invalid highlight position.
useEffect(() => {
if (!isOpen) return;
if (!didInitHighlightRef.current) return;
setHighlightedIndex(0);
if (listRef.current) listRef.current.scrollTop = 0;
}, [localFilter, isOpen]);
// Focus search input and position dropdown when opening
useEffect(() => {
if (!isOpen) return;

View File

@@ -1,11 +1,24 @@
/* ===== Model Onboarding Modal ===== */
.model-onboarding-modal {
max-width: 880px;
width: 90vw;
max-height: 85vh;
/* Resizable via the native CSS grip. The user's chosen size is persisted
by useModalResizePersist. Min/max keep the dimensions sane so a value
saved on a 4K display still renders on a laptop. */
min-width: 640px;
min-height: 480px;
max-width: calc(100vw - 40px);
max-height: calc(100dvh - 40px);
display: flex;
flex-direction: column;
overflow: hidden;
resize: both;
}
.model-onboarding-modal:not([style*="width"]) {
width: min(1100px, calc(100vw - 40px));
}
.model-onboarding-modal:not([style*="height"]) {
height: min(85vh, calc(100dvh - 40px));
}
.model-onboarding-header {
@@ -414,7 +427,10 @@
margin: 0 0 var(--space-md) 0;
}
/* Provider cards in onboarding - enhanced card layout for AI setup step */
/* Provider cards in onboarding - enhanced card layout for AI setup step.
flex-wrap lets the API-key form drop to a full-width row beneath the
icon+body header instead of being crammed into a fixed side column,
which was the root of the squished layout at narrow modal widths. */
.onboarding-provider-card {
display: flex;
align-items: flex-start;
@@ -424,6 +440,8 @@
border-radius: var(--radius-md);
background: var(--card);
transition: background var(--transition-fast), border-color var(--transition-fast);
flex-wrap: wrap;
min-width: 0;
}
.onboarding-provider-card:hover {
@@ -442,11 +460,12 @@
}
.onboarding-provider-card__body {
flex: 1;
flex: 1 1 240px;
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-xs);
overflow-wrap: anywhere;
}
.onboarding-provider-card__name {
@@ -463,6 +482,13 @@
line-height: 1.4;
}
/* Status badge sits in a flex-column body — keep it sized to its text
instead of stretching the full row width. */
.onboarding-provider-card__body > .auth-status-badge,
.onboarding-provider-card__body > .auth-key-hint {
align-self: flex-start;
}
.onboarding-provider-card__actions {
flex-shrink: 0;
display: flex;
@@ -489,7 +515,11 @@
.onboarding-provider-card__actions--api-key {
align-items: stretch;
width: min(calc(var(--space-2xl) * 10), 100%);
/* Force the API-key form onto its own full-width row beneath the
icon+body header so the input has space to breathe. */
flex: 1 0 100%;
width: 100%;
min-width: 0;
}
.auth-provider-card--cli .auth-provider-info {
@@ -833,6 +863,34 @@
opacity: 0.5;
}
.model-onboarding-github-optional .optional-icon--github {
color: var(--text);
opacity: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* ProviderIcon caps at lg=24px — scale up so the GitHub mark reads as the
hero of this section. */
.model-onboarding-github-optional .optional-icon--github svg {
width: 64px;
height: 64px;
}
.model-onboarding-github-optional__actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--space-sm);
}
.model-onboarding-github-optional__actions .btn {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
}
.model-onboarding-github-optional p {
color: var(--text-muted);
max-width: 320px;
@@ -1142,6 +1200,13 @@
gap: 6px;
}
/* When the footer holds only the "Get Started" button (final "complete"
step), push it to the right edge instead of hugging the left under
`justify-content: space-between`. */
.model-onboarding-footer > .btn-primary:only-child {
margin-inline-start: auto;
}
.onboarding-skip-step-link {
background: none;
border: none;

View File

@@ -15,6 +15,7 @@ import {
createTask,
} from "../api";
import type { ToastType } from "../hooks/useToast";
import { useModalResizePersist } from "../hooks/useModalResizePersist";
import { CustomModelDropdown } from "./CustomModelDropdown";
import { ProviderIcon } from "./ProviderIcon";
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
@@ -594,6 +595,7 @@ export function ModelOnboardingModal({
const [apiKeySuccess, setApiKeySuccess] = useState<Record<string, string | null>>({});
const apiKeySuccessTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
const onboardingContentRef = useRef<HTMLDivElement | null>(null);
const modalRef = useRef<HTMLDivElement | null>(null);
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [loginOutcomes, setLoginOutcomes] = useState<Record<string, LoginOutcome>>({});
const [isGithubSkipped, setIsGithubSkipped] = useState<boolean>(() => {
@@ -606,6 +608,17 @@ export function ModelOnboardingModal({
const resumedFromStep = persistedState?.currentStep;
const isResumedFlow = !!persistedState && persistedState.currentStep !== "complete";
useModalResizePersist(modalRef, isOpen, "fusion:model-onboarding-modal-size");
// Scroll the content area to the top whenever the step changes so the user
// always lands at the start of the next page instead of mid-scroll from the
// previous one.
useEffect(() => {
const content = onboardingContentRef.current;
if (!content) return;
content.scrollTop = 0;
}, [step]);
// Initialize skippedProviders from persisted state
const [skippedProviders, setSkippedProviders] = useState<Record<string, boolean>>(
() => {
@@ -1743,7 +1756,7 @@ export function ModelOnboardingModal({
aria-modal="true"
aria-labelledby="onboarding-title"
>
<div className="modal model-onboarding-modal">
<div className="modal model-onboarding-modal" ref={modalRef}>
{/* Header */}
<div className="model-onboarding-header">
<h2 id="onboarding-title" className="model-onboarding-title">
@@ -2028,25 +2041,35 @@ export function ModelOnboardingModal({
{step === "github" && (
<div className="model-onboarding-github">
<p className="model-onboarding-description">
Connecting GitHub unlocks issue imports and pull request tracking. You can skip this task creation works without it.
</p>
<div className="onboarding-feature-list">
<ul>
<li className="onboarding-feature-list-heading">
<strong>Without GitHub (available now):</strong>
</li>
<li className="onboarding-helper-text">Create tasks manually</li>
<li className="onboarding-helper-text">Describe work for AI agents</li>
<li className="onboarding-helper-text">Track progress on the board</li>
<li className="onboarding-feature-list-heading">
<strong>With GitHub (after connecting):</strong>
</li>
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Import issues as tasks</li>
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Sync pull request status</li>
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Link code changes to tasks</li>
</ul>
</div>
{isGitHubReady ? (
<p className="model-onboarding-description">
{isGitHubReadyViaCli
? "GitHub CLI is already authenticated — issue imports and pull request tracking work right now. You're all set; no further action needed."
: "GitHub is connected — issue imports and pull request tracking are available. You're all set; no further action needed."}
</p>
) : (
<p className="model-onboarding-description">
Connecting GitHub unlocks issue imports and pull request tracking. You can skip this task creation works without it.
</p>
)}
{!isGitHubReady && (
<div className="onboarding-feature-list">
<ul>
<li className="onboarding-feature-list-heading">
<strong>Without GitHub (available now):</strong>
</li>
<li className="onboarding-helper-text">Create tasks manually</li>
<li className="onboarding-helper-text">Describe work for AI agents</li>
<li className="onboarding-helper-text">Track progress on the board</li>
<li className="onboarding-feature-list-heading">
<strong>With GitHub (after connecting):</strong>
</li>
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Import issues as tasks</li>
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Sync pull request status</li>
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Link code changes to tasks</li>
</ul>
</div>
)}
{/* Skip-state banner: shown when AI setup was skipped */}
{aiSetupSkipped && (
@@ -2067,11 +2090,13 @@ export function ModelOnboardingModal({
{!hasGithubProvider ? (
<div className="model-onboarding-github-optional">
<GitPullRequest size={48} className="optional-icon" />
<div className="optional-icon optional-icon--github" aria-hidden="true">
<ProviderIcon provider="github" size="lg" />
</div>
{isGitHubReadyViaCli ? (
<p>
GitHub CLI is already authenticated, so imports and PR tracking work now.
OAuth integration in Settings Authentication is optional and only controls
OAuth from the dashboard is optional and only controls
dashboard-managed connect/disconnect.
</p>
) : (
@@ -2080,12 +2105,38 @@ export function ModelOnboardingModal({
or continue now and connect later.
</p>
)}
<button
className="btn btn-primary btn-sm"
onClick={() => setStep("project-setup")}
>
Continue without GitHub
</button>
<div className="model-onboarding-github-optional__actions">
<button
className="btn btn-primary btn-sm"
onClick={() => setStep("project-setup")}
>
{isGitHubReadyViaCli
? "Continue with gh CLI auth →"
: "Continue without GitHub →"}
</button>
{isGitHubReadyViaCli && (
authActionInProgress === "github" ? (
<button className="btn btn-sm" disabled>
<Loader2 size={14} className="onboarding-spinner" />
Waiting for OAuth login
</button>
) : (
<button
className="btn btn-sm"
onClick={() => handleLogin("github")}
>
<ProviderIcon provider="github" size="sm" />
Connect OAuth (optional)
</button>
)
)}
</div>
{isGitHubReadyViaCli && authActionInProgress === "github" && loginInstructions.github && (
<LoginInstructions
instructions={loginInstructions.github}
data-testid="onboarding-login-instructions-github"
/>
)}
</div>
) : (
<>