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 3031d05a68
commit c94e9be31c
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>
) : (
<>