diff --git a/src/deepscientist/daemon/api/handlers.py b/src/deepscientist/daemon/api/handlers.py index 197e24f0..8d1637bd 100644 --- a/src/deepscientist/daemon/api/handlers.py +++ b/src/deepscientist/daemon/api/handlers.py @@ -2533,6 +2533,17 @@ def config_show(self, name: str) -> dict: } def config_save(self, name: str, body: dict) -> dict: + if name == "config" and isinstance(body.get("structured"), dict): + default_runner = self.app.config_manager._normalize_runtime_runner_name( + body["structured"].get("default_runner") + ) + if default_runner in {"codex", "claude", "kimi", "opencode"}: + os.environ["DEEPSCIENTIST_DEFAULT_RUNNER"] = default_runner + os.environ["DEEPSCIENTIST_ENABLE_RUNNER"] = default_runner + os.environ.pop("DS_DEFAULT_RUNNER", None) + os.environ.pop("DS_ENABLE_RUNNER", None) + os.environ.pop("DEEPSCIENTIST_ENABLE_RUNNERS", None) + os.environ.pop("DS_ENABLE_RUNNERS", None) if isinstance(body.get("structured"), dict): result = self.app.config_manager.save_named_payload(name, body["structured"]) else: diff --git a/src/ui/src/components/landing/Hero.tsx b/src/ui/src/components/landing/Hero.tsx index 80d0b51b..658fbab2 100644 --- a/src/ui/src/components/landing/Hero.tsx +++ b/src/ui/src/components/landing/Hero.tsx @@ -602,7 +602,7 @@ export default function Hero(props: { className="h-12 rounded-full bg-[#C7AD96] px-7 text-[#2D2A26] shadow-[0_12px_28px_-14px_rgba(45,42,38,0.55)] transition-all duration-200 hover:-translate-y-0.5 hover:bg-[#D7C6AE]" onClick={() => { window.setTimeout(() => { - setActiveDialog('autonomous') + setActiveDialog('launch') }, 120) }} data-onboarding-id="landing-start-research" diff --git a/src/ui/src/components/landing/HeroNav.tsx b/src/ui/src/components/landing/HeroNav.tsx index 8bf4d419..9c4cb44a 100644 --- a/src/ui/src/components/landing/HeroNav.tsx +++ b/src/ui/src/components/landing/HeroNav.tsx @@ -25,7 +25,7 @@ export default function HeroNav(props: { 'supports-[backdrop-filter]:bg-white/40' )} > -
+
- + DeepScientist -
+
{props.onOpenBenchStore ? ( @@ -95,12 +96,12 @@ export default function HeroNav(props: { ) : null}
diff --git a/src/ui/src/components/onboarding/OnboardingOverlay.tsx b/src/ui/src/components/onboarding/OnboardingOverlay.tsx index 9292601a..115f7833 100644 --- a/src/ui/src/components/onboarding/OnboardingOverlay.tsx +++ b/src/ui/src/components/onboarding/OnboardingOverlay.tsx @@ -651,6 +651,25 @@ function queryTarget(id?: string | null) { return document.querySelector(`[data-onboarding-id="${id}"]`) } +function waitForTarget(id: string, timeoutMs = 2400) { + const startedAt = Date.now() + return new Promise((resolve) => { + const tick = () => { + const target = queryTarget(id) + if (target) { + resolve(target) + return + } + if (Date.now() - startedAt >= timeoutMs) { + resolve(null) + return + } + window.setTimeout(tick, 80) + } + tick() + }) +} + function stepNeedsCopilotPanel(step: OnboardingStep | null) { if (!step) return false const ids = [step.targetId, step.actionTargetId, step.waitForElementId].filter(Boolean) @@ -724,9 +743,15 @@ function scrollTargetIntoViewVerticalOnly(target: HTMLElement) { function triggerTargetAction(id?: string | null) { const target = queryTarget(id) if (!target) return false + const clickableTarget = + target instanceof HTMLElement && + !target.matches('button, a, [role="button"], input, select, textarea') + ? target.querySelector('button, a, [role="button"], input, select, textarea') + : null + const actionTarget = clickableTarget || target const isFileTreeNode = Boolean(target.closest('[data-node-id]')) if (isFileTreeNode) { - target.dispatchEvent( + actionTarget.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true, @@ -734,7 +759,7 @@ function triggerTargetAction(id?: string | null) { detail: 1, }) ) - target.dispatchEvent( + actionTarget.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true, @@ -742,7 +767,7 @@ function triggerTargetAction(id?: string | null) { detail: 2, }) ) - target.dispatchEvent( + actionTarget.dispatchEvent( new MouseEvent('dblclick', { bubbles: true, cancelable: true, @@ -752,11 +777,11 @@ function triggerTargetAction(id?: string | null) { ) return true } - if (typeof target.click === 'function') { - target.click() + if (typeof actionTarget.click === 'function') { + actionTarget.click() return true } - target.dispatchEvent( + actionTarget.dispatchEvent( new MouseEvent('click', { bubbles: true, cancelable: true, @@ -784,6 +809,51 @@ function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max) } +function cardTargetOverlap(args: { + top: number + left: number + cardWidth: number + cardHeight: number + targetRect: DOMRect +}) { + const right = args.left + args.cardWidth + const bottom = args.top + args.cardHeight + const overlapWidth = Math.max(0, Math.min(right, args.targetRect.right) - Math.max(args.left, args.targetRect.left)) + const overlapHeight = Math.max(0, Math.min(bottom, args.targetRect.bottom) - Math.max(args.top, args.targetRect.top)) + return overlapWidth * overlapHeight +} + +function bestFallbackCardPosition(args: { + targetRect: DOMRect + cardWidth: number + cardHeight: number + margin: number + viewportWidth: number + viewportHeight: number +}) { + const maxTop = Math.max(args.margin, args.viewportHeight - args.margin - args.cardHeight) + const maxLeft = Math.max(args.margin, args.viewportWidth - args.margin - args.cardWidth) + const candidates = [ + { top: args.margin, left: args.margin }, + { top: args.margin, left: maxLeft }, + { top: maxTop, left: args.margin }, + { top: maxTop, left: maxLeft }, + ] + + return candidates + .map((candidate, index) => ({ + ...candidate, + index, + overlap: cardTargetOverlap({ + ...candidate, + cardWidth: args.cardWidth, + cardHeight: args.cardHeight, + targetRect: args.targetRect, + }), + })) + .sort((a, b) => a.overlap - b.overlap || b.index - a.index)[0] +} + function resolveCardPosition(args: { targetRect: DOMRect | null cardWidth: number @@ -809,12 +879,29 @@ function resolveCardPosition(args: { const canPlaceLeft = rect.left - gap - args.cardWidth >= margin let placement = args.placement || 'auto' + if ( + (placement === 'bottom' && !canPlaceBottom) || + (placement === 'top' && !canPlaceTop) || + (placement === 'right' && !canPlaceRight) || + (placement === 'left' && !canPlaceLeft) + ) { + placement = 'auto' + } if (placement === 'auto') { if (canPlaceBottom) placement = 'bottom' else if (canPlaceTop) placement = 'top' else if (canPlaceRight) placement = 'right' else if (canPlaceLeft) placement = 'left' - else placement = 'bottom' + else { + return bestFallbackCardPosition({ + targetRect: rect, + cardWidth: args.cardWidth, + cardHeight: args.cardHeight, + margin, + viewportWidth, + viewportHeight, + }) + } } let top = rect.bottom + gap @@ -921,6 +1008,7 @@ export function OnboardingOverlay() { neverShowAgain, close, completeTutorial, + goToStep, } = useOnboardingStore((state) => ({ hydrated: state.hydrated, status: state.status, @@ -935,6 +1023,7 @@ export function OnboardingOverlay() { neverShowAgain: state.neverShowAgain, close: state.close, completeTutorial: state.completeTutorial, + goToStep: state.goToStep, })) const [targetRect, setTargetRect] = React.useState(null) @@ -990,6 +1079,40 @@ export function OnboardingOverlay() { advance() }, [advance, navigate, status, step]) + const restoreBenchstoreStartStep = React.useCallback(async () => { + triggerTargetAction('experiment-launch-close') + if (!queryTarget('benchstore-dialog')) { + triggerTargetAction('landing-benchstore') + await waitForTarget('benchstore-dialog') + } + if (!queryTarget('benchstore-detail-action-strip')) { + const featuredCard = await waitForTarget('benchstore-featured-card') + if (featuredCard) { + triggerTargetAction('benchstore-featured-card') + await waitForTarget('benchstore-detail-action-strip') + } + } + goToStep(7) + }, [goToStep]) + + const handlePrevious = React.useCallback(() => { + if (step?.id === 'benchstore-overview') { + triggerTargetAction('benchstore-close') + goToStep(2) + return + } + if (step?.id === 'landing-open-dialog') { + void restoreBenchstoreStartStep() + return + } + if (step?.id === 'launch-mode-overview') { + triggerTargetAction('experiment-launch-close') + goToStep(8) + return + } + previousStep() + }, [goToStep, previousStep, restoreBenchstoreStartStep, step?.id]) + React.useEffect(() => { hydrate() }, [hydrate]) @@ -1331,14 +1454,15 @@ export function OnboardingOverlay() {
-
+
+
{step.route === 'landing' ? : }
@@ -1397,18 +1521,19 @@ export function OnboardingOverlay() {
) : null}
+
-
+
{stepIndex > 0 ? ( - ) : ( )} -
+