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:
@@ -718,7 +718,11 @@ function StatusModeGrid({
|
|||||||
const logsShare = Math.max(1, middleHeight - SYSTEM_HEIGHT - bottomShare);
|
const logsShare = Math.max(1, middleHeight - SYSTEM_HEIGHT - bottomShare);
|
||||||
// LogsPanel chrome: border 2 + title 1 + filter 1 = 4.
|
// LogsPanel chrome: border 2 + title 1 + filter 1 = 4.
|
||||||
const logsAvailableRows = Math.max(1, logsShare - 4);
|
const logsAvailableRows = Math.max(1, logsShare - 4);
|
||||||
tuiDebug("StatusModeGrid", { cols, rows, middleHeight, logsShare, bottomShare, focused });
|
// On very wide terminals, lift Stats up next to Logs so the bottom row only
|
||||||
|
// holds Utilities + Settings. Logs gets the row budget that Stats vacated.
|
||||||
|
const wideLayout = cols >= 150;
|
||||||
|
const wideLogsAvailableRows = Math.max(1, logsShare + bottomShare - 4);
|
||||||
|
tuiDebug("StatusModeGrid", { cols, rows, middleHeight, logsShare, bottomShare, focused, wideLayout });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" flexGrow={1}>
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
@@ -732,28 +736,56 @@ function StatusModeGrid({
|
|||||||
<Box height={4} flexShrink={0} overflow="hidden">
|
<Box height={4} flexShrink={0} overflow="hidden">
|
||||||
<SystemPanel state={state} isFocused={focused === "system"} />
|
<SystemPanel state={state} isFocused={focused === "system"} />
|
||||||
</Box>
|
</Box>
|
||||||
{/* Logs: fills remaining vertical space. flexShrink=0 so System and
|
{wideLayout ? (
|
||||||
the bottom row collapse first — Logs keeps its space. */}
|
<>
|
||||||
<Box flexGrow={1} flexShrink={0} flexDirection="column" overflow="hidden">
|
{/* Wide: Stats sits left of Logs and absorbs the bottom-row height. */}
|
||||||
<LogsPanel
|
<Box flexGrow={1} flexShrink={0} flexDirection="row" overflow="hidden">
|
||||||
state={state}
|
<Box flexDirection="column" width="30%" flexShrink={0} overflow="hidden">
|
||||||
isFocused={focused === "logs"}
|
<StatsPanel state={state} isFocused={focused === "stats"} />
|
||||||
availableRows={logsAvailableRows}
|
</Box>
|
||||||
/>
|
<Box flexGrow={1} flexDirection="column" overflow="hidden">
|
||||||
</Box>
|
<LogsPanel
|
||||||
{/* Bottom row: Stats + Utilities + Settings, equal-width. flexShrink=2
|
state={state}
|
||||||
so when terminal height is small they collapse before Logs does. */}
|
isFocused={focused === "logs"}
|
||||||
<Box flexDirection="row" flexShrink={2} overflow="hidden">
|
availableRows={wideLogsAvailableRows}
|
||||||
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
/>
|
||||||
<StatsPanel state={state} isFocused={focused === "stats"} />
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
<Box flexDirection="row" flexShrink={2} overflow="hidden">
|
||||||
<UtilitiesPanel state={state} isFocused={focused === "utilities"} />
|
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
||||||
</Box>
|
<UtilitiesPanel state={state} isFocused={focused === "utilities"} />
|
||||||
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
</Box>
|
||||||
<SettingsPanel state={state} isFocused={focused === "settings"} />
|
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
||||||
</Box>
|
<SettingsPanel state={state} isFocused={focused === "settings"} />
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Logs: fills remaining vertical space. flexShrink=0 so System and
|
||||||
|
the bottom row collapse first — Logs keeps its space. */}
|
||||||
|
<Box flexGrow={1} flexShrink={0} flexDirection="column" overflow="hidden">
|
||||||
|
<LogsPanel
|
||||||
|
state={state}
|
||||||
|
isFocused={focused === "logs"}
|
||||||
|
availableRows={logsAvailableRows}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{/* Bottom row: Stats + Utilities + Settings, equal-width. flexShrink=2
|
||||||
|
so when terminal height is small they collapse before Logs does. */}
|
||||||
|
<Box flexDirection="row" flexShrink={2} overflow="hidden">
|
||||||
|
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
||||||
|
<StatsPanel state={state} isFocused={focused === "stats"} />
|
||||||
|
</Box>
|
||||||
|
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
||||||
|
<UtilitiesPanel state={state} isFocused={focused === "utilities"} />
|
||||||
|
</Box>
|
||||||
|
<Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
||||||
|
<SettingsPanel state={state} isFocused={focused === "settings"} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box flexShrink={0}>
|
<Box flexShrink={0}>
|
||||||
@@ -829,20 +861,27 @@ function StatusBar({ state, controller: _controller }: { state: DashboardState;
|
|||||||
const uptimePart = systemInfo ? formatUptime(Date.now() - systemInfo.startTimeMs) : null;
|
const uptimePart = systemInfo ? formatUptime(Date.now() - systemInfo.startTimeMs) : null;
|
||||||
const versionPart = systemInfo ? `${systemInfo.baseUrl} v${FUSION_VERSION}` : null;
|
const versionPart = systemInfo ? `${systemInfo.baseUrl} v${FUSION_VERSION}` : null;
|
||||||
|
|
||||||
// Truncate both halves so the StatusBar always fits in a single row.
|
// Both halves must collapse into a single row regardless of width.
|
||||||
// Without this, default wrap="wrap" lets long hotkey strings or URLs
|
// The right side is rendered as a single Text (not a Box of separate
|
||||||
// wrap to 2+ rows, throwing the layout's row budget off by 1-2 rows
|
// Texts joined with gap+flexShrink=0) so Yoga can truncate it when
|
||||||
// and pushing the header off the top of the alt-screen.
|
// the natural width of hotkeys + version exceeds `cols`. With the
|
||||||
|
// previous structure, the row's natural width could exceed `cols`
|
||||||
|
// (~137 in the worst case: utilities hotkeys + localhost URL + uptime
|
||||||
|
// + version + ●), causing the row to wrap to 2 lines; height={1} +
|
||||||
|
// overflow=hidden then clipped the top line, making the hotkeys row
|
||||||
|
// disappear and only the version row show through.
|
||||||
|
const rightParts: string[] = [];
|
||||||
|
if (uptimePart) rightParts.push(uptimePart, "|");
|
||||||
|
if (versionPart) rightParts.push(versionPart);
|
||||||
|
const rightText = rightParts.join(" ");
|
||||||
return (
|
return (
|
||||||
<Box height={1} justifyContent="space-between" paddingX={1} flexShrink={0} overflow="hidden">
|
<Box height={1} justifyContent="space-between" paddingX={1} flexShrink={0} overflow="hidden">
|
||||||
<Text dimColor wrap="truncate-end">{hotkeys.join(" · ")}</Text>
|
<Text dimColor wrap="truncate-end">{hotkeys.join(" · ")}</Text>
|
||||||
{versionPart && (
|
{rightText && (
|
||||||
<Box flexDirection="row" gap={1} flexShrink={0}>
|
<Text wrap="truncate-end">
|
||||||
{uptimePart && <Text dimColor wrap="truncate-end">{uptimePart}</Text>}
|
<Text dimColor>{rightText}</Text>
|
||||||
{uptimePart && <Text dimColor wrap="truncate-end">|</Text>}
|
{hasUpdate && <Text color="yellow">{" ●"}</Text>}
|
||||||
<Text dimColor wrap="truncate-end">{versionPart}</Text>
|
</Text>
|
||||||
{hasUpdate && <Text color="yellow" wrap="truncate-end">●</Text>}
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -283,16 +283,36 @@ export function CustomModelDropdown({
|
|||||||
setPortalRoot(document.body);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (!isOpen) {
|
||||||
const selectableIndex = optionsList.findIndex(
|
didInitHighlightRef.current = false;
|
||||||
(opt, idx) => idx >= (currentValueIndex >= 0 ? currentValueIndex : 0) && opt.type !== "provider"
|
return;
|
||||||
);
|
|
||||||
setHighlightedIndex(selectableIndex >= 0 ? selectableIndex : 0);
|
|
||||||
}
|
}
|
||||||
|
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]);
|
}, [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
|
// Focus search input and position dropdown when opening
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
/* ===== Model Onboarding Modal ===== */
|
/* ===== Model Onboarding Modal ===== */
|
||||||
|
|
||||||
.model-onboarding-modal {
|
.model-onboarding-modal {
|
||||||
max-width: 880px;
|
/* Resizable via the native CSS grip. The user's chosen size is persisted
|
||||||
width: 90vw;
|
by useModalResizePersist. Min/max keep the dimensions sane so a value
|
||||||
max-height: 85vh;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.model-onboarding-header {
|
||||||
@@ -414,7 +427,10 @@
|
|||||||
margin: 0 0 var(--space-md) 0;
|
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 {
|
.onboarding-provider-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -424,6 +440,8 @@
|
|||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-provider-card:hover {
|
.onboarding-provider-card:hover {
|
||||||
@@ -442,11 +460,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-provider-card__body {
|
.onboarding-provider-card__body {
|
||||||
flex: 1;
|
flex: 1 1 240px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-provider-card__name {
|
.onboarding-provider-card__name {
|
||||||
@@ -463,6 +482,13 @@
|
|||||||
line-height: 1.4;
|
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 {
|
.onboarding-provider-card__actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -489,7 +515,11 @@
|
|||||||
|
|
||||||
.onboarding-provider-card__actions--api-key {
|
.onboarding-provider-card__actions--api-key {
|
||||||
align-items: stretch;
|
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 {
|
.auth-provider-card--cli .auth-provider-info {
|
||||||
@@ -833,6 +863,34 @@
|
|||||||
opacity: 0.5;
|
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 {
|
.model-onboarding-github-optional p {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
@@ -1142,6 +1200,13 @@
|
|||||||
gap: 6px;
|
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 {
|
.onboarding-skip-step-link {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
createTask,
|
createTask,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import type { ToastType } from "../hooks/useToast";
|
import type { ToastType } from "../hooks/useToast";
|
||||||
|
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||||
import { ProviderIcon } from "./ProviderIcon";
|
import { ProviderIcon } from "./ProviderIcon";
|
||||||
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
|
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
|
||||||
@@ -594,6 +595,7 @@ export function ModelOnboardingModal({
|
|||||||
const [apiKeySuccess, setApiKeySuccess] = useState<Record<string, string | null>>({});
|
const [apiKeySuccess, setApiKeySuccess] = useState<Record<string, string | null>>({});
|
||||||
const apiKeySuccessTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
|
const apiKeySuccessTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
|
||||||
const onboardingContentRef = useRef<HTMLDivElement | null>(null);
|
const onboardingContentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const [loginOutcomes, setLoginOutcomes] = useState<Record<string, LoginOutcome>>({});
|
const [loginOutcomes, setLoginOutcomes] = useState<Record<string, LoginOutcome>>({});
|
||||||
const [isGithubSkipped, setIsGithubSkipped] = useState<boolean>(() => {
|
const [isGithubSkipped, setIsGithubSkipped] = useState<boolean>(() => {
|
||||||
@@ -606,6 +608,17 @@ export function ModelOnboardingModal({
|
|||||||
const resumedFromStep = persistedState?.currentStep;
|
const resumedFromStep = persistedState?.currentStep;
|
||||||
const isResumedFlow = !!persistedState && persistedState.currentStep !== "complete";
|
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
|
// Initialize skippedProviders from persisted state
|
||||||
const [skippedProviders, setSkippedProviders] = useState<Record<string, boolean>>(
|
const [skippedProviders, setSkippedProviders] = useState<Record<string, boolean>>(
|
||||||
() => {
|
() => {
|
||||||
@@ -1743,7 +1756,7 @@ export function ModelOnboardingModal({
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="onboarding-title"
|
aria-labelledby="onboarding-title"
|
||||||
>
|
>
|
||||||
<div className="modal model-onboarding-modal">
|
<div className="modal model-onboarding-modal" ref={modalRef}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="model-onboarding-header">
|
<div className="model-onboarding-header">
|
||||||
<h2 id="onboarding-title" className="model-onboarding-title">
|
<h2 id="onboarding-title" className="model-onboarding-title">
|
||||||
@@ -2028,25 +2041,35 @@ export function ModelOnboardingModal({
|
|||||||
|
|
||||||
{step === "github" && (
|
{step === "github" && (
|
||||||
<div className="model-onboarding-github">
|
<div className="model-onboarding-github">
|
||||||
<p className="model-onboarding-description">
|
{isGitHubReady ? (
|
||||||
Connecting GitHub unlocks issue imports and pull request tracking. You can skip this — task creation works without it.
|
<p className="model-onboarding-description">
|
||||||
</p>
|
{isGitHubReadyViaCli
|
||||||
<div className="onboarding-feature-list">
|
? "GitHub CLI is already authenticated — issue imports and pull request tracking work right now. You're all set; no further action needed."
|
||||||
<ul>
|
: "GitHub is connected — issue imports and pull request tracking are available. You're all set; no further action needed."}
|
||||||
<li className="onboarding-feature-list-heading">
|
</p>
|
||||||
<strong>Without GitHub (available now):</strong>
|
) : (
|
||||||
</li>
|
<p className="model-onboarding-description">
|
||||||
<li className="onboarding-helper-text">Create tasks manually</li>
|
Connecting GitHub unlocks issue imports and pull request tracking. You can skip this — task creation works without it.
|
||||||
<li className="onboarding-helper-text">Describe work for AI agents</li>
|
</p>
|
||||||
<li className="onboarding-helper-text">Track progress on the board</li>
|
)}
|
||||||
<li className="onboarding-feature-list-heading">
|
{!isGitHubReady && (
|
||||||
<strong>With GitHub (after connecting):</strong>
|
<div className="onboarding-feature-list">
|
||||||
</li>
|
<ul>
|
||||||
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Import issues as tasks</li>
|
<li className="onboarding-feature-list-heading">
|
||||||
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Sync pull request status</li>
|
<strong>Without GitHub (available now):</strong>
|
||||||
<li className="onboarding-helper-text onboarding-feature-list-item--with-github">Link code changes to tasks</li>
|
</li>
|
||||||
</ul>
|
<li className="onboarding-helper-text">Create tasks manually</li>
|
||||||
</div>
|
<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 */}
|
{/* Skip-state banner: shown when AI setup was skipped */}
|
||||||
{aiSetupSkipped && (
|
{aiSetupSkipped && (
|
||||||
@@ -2067,11 +2090,13 @@ export function ModelOnboardingModal({
|
|||||||
|
|
||||||
{!hasGithubProvider ? (
|
{!hasGithubProvider ? (
|
||||||
<div className="model-onboarding-github-optional">
|
<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 ? (
|
{isGitHubReadyViaCli ? (
|
||||||
<p>
|
<p>
|
||||||
GitHub CLI is already authenticated, so imports and PR tracking work now.
|
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.
|
dashboard-managed connect/disconnect.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -2080,12 +2105,38 @@ export function ModelOnboardingModal({
|
|||||||
or continue now and connect later.
|
or continue now and connect later.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<div className="model-onboarding-github-optional__actions">
|
||||||
className="btn btn-primary btn-sm"
|
<button
|
||||||
onClick={() => setStep("project-setup")}
|
className="btn btn-primary btn-sm"
|
||||||
>
|
onClick={() => setStep("project-setup")}
|
||||||
Continue without GitHub →
|
>
|
||||||
</button>
|
{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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user