mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
feat(code): gate onboarding integrations modal behind opt-in flag (#4227)
Streamlined first-run onboarding: the integrations summary screen is now off by default (model selection handles provider installs); re-enable it with `DEEPAGENTS_CODE_ONBOARDING_INTEGRATIONS_SCREEN`. --- The onboarding "Installed Integrations" modal (`LaunchDependenciesScreen`) added a read-only step whose status glyphs (`○` / `[ ]`) read as clickable checkboxes but weren't, and it largely duplicated the model selector — which already surfaces uninstalled providers as install-required rows and installs them on select. This skips it by default (onboarding goes name → model selector) while keeping the screen in the repo, reachable via `DEEPAGENTS_CODE_ONBOARDING_INTEGRATIONS_SCREEN`. The curated model-selector description gains a one-line `/install` hint so sandboxes and other non-provider extras stay discoverable. Made by [Open SWE](https://openswe.vercel.app/agents/03c9e178-0f75-11f1-b97b-2762e103489f) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -158,6 +158,15 @@ and `/api/show`. See `_ollama_discovery_enabled` for accepted truthy/falsy
|
||||
values.
|
||||
"""
|
||||
|
||||
ONBOARDING_INTEGRATIONS_SCREEN = "DEEPAGENTS_CODE_ONBOARDING_INTEGRATIONS_SCREEN"
|
||||
"""Show the "Installed Integrations" summary screen during first-run onboarding.
|
||||
|
||||
Off by default: onboarding goes straight from the name prompt to the model
|
||||
selector, which already surfaces (and installs) uninstalled model providers.
|
||||
Set to a truthy value to bring the standalone integrations screen back into the
|
||||
flow. Parsed by `is_env_truthy`: accepts `1`, `true`, `yes`, `on` as enabled.
|
||||
"""
|
||||
|
||||
RESTARTED_AFTER_UPDATE = "DEEPAGENTS_CODE_RESTARTED_AFTER_UPDATE"
|
||||
"""Internal sentinel recording the target version immediately before the
|
||||
startup auto-update re-execs the process.
|
||||
|
||||
@@ -6414,12 +6414,23 @@ class DeepAgentsApp(App):
|
||||
def _build_launch_dependencies_prompt(
|
||||
self,
|
||||
) -> tuple[ModalScreen, asyncio.Future[tuple[bool, tuple[str, str] | None]]]:
|
||||
"""Build the dependency/model prompt screen and result future.
|
||||
"""Build the first post-name onboarding screen and its result future.
|
||||
|
||||
The integrations summary screen is disabled by default (the model
|
||||
selector already surfaces and installs uninstalled providers), so the
|
||||
model selector is normally the first screen. Setting
|
||||
`DEEPAGENTS_CODE_ONBOARDING_INTEGRATIONS_SCREEN` re-inserts the
|
||||
`LaunchDependenciesScreen` ahead of it.
|
||||
|
||||
Returns:
|
||||
The dependency screen and a future resolved when the dependency or
|
||||
model screen finishes.
|
||||
The first onboarding screen and a future resolved when the
|
||||
dependency or model screen finishes.
|
||||
"""
|
||||
from deepagents_code._env_vars import (
|
||||
ONBOARDING_INTEGRATIONS_SCREEN,
|
||||
is_env_truthy,
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
result_future: asyncio.Future[tuple[bool, tuple[str, str] | None]] = (
|
||||
loop.create_future()
|
||||
@@ -6442,6 +6453,9 @@ class DeepAgentsApp(App):
|
||||
curated=True,
|
||||
result_callback=handle_model,
|
||||
)
|
||||
if not is_env_truthy(ONBOARDING_INTEGRATIONS_SCREEN):
|
||||
return model_screen, result_future
|
||||
|
||||
dependency_screen = self._build_launch_dependencies_screen(
|
||||
continue_screen=model_screen,
|
||||
on_done=handle_dependencies,
|
||||
@@ -9661,7 +9675,8 @@ class DeepAgentsApp(App):
|
||||
description=(
|
||||
"These models have performed well in Deep Agents evals and are "
|
||||
"a solid starting set. You can explore the full model list "
|
||||
"later with /model."
|
||||
"later with /model. Sandboxes and other integrations install "
|
||||
"anytime with /install."
|
||||
if curated
|
||||
else None
|
||||
),
|
||||
|
||||
@@ -840,6 +840,14 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
|
||||
default=False,
|
||||
env_var=_env_vars.NO_TERMINAL_ESCAPE,
|
||||
),
|
||||
ConfigOption(
|
||||
key="display.onboarding_integrations_screen",
|
||||
group="Display",
|
||||
summary="Show the integrations summary screen during first-run onboarding.",
|
||||
kind=OptionKind.BOOL,
|
||||
default=False,
|
||||
env_var=_env_vars.ONBOARDING_INTEGRATIONS_SCREEN,
|
||||
),
|
||||
# --- Models --------------------------------------------------------
|
||||
ConfigOption(
|
||||
key="models.default",
|
||||
|
||||
@@ -1072,15 +1072,60 @@ class TestStartupSequence:
|
||||
app._prompt_launch_tavily.assert_awaited_once_with() # ty: ignore
|
||||
assert order == ["tavily", "switch"]
|
||||
|
||||
async def test_launch_init_wires_name_screen_to_dependency_screen(self) -> None:
|
||||
"""On mount, submitting the name switches straight into the deps screen.
|
||||
async def test_launch_init_wires_name_screen_to_model_selector(self) -> None:
|
||||
"""On mount, submitting the name switches straight into the selector.
|
||||
|
||||
Regression guard for the no-flash wiring: the mount path must set the
|
||||
name screen's `continue_screen` and pass a `dependency_result` future. If
|
||||
it regressed to the old `LaunchNameScreen()` with no continue screen,
|
||||
onboarding would fall back to the double-modal flow and the post-submit
|
||||
screen would not be the dependency summary.
|
||||
The integrations summary screen is off by default, so onboarding goes
|
||||
name -> model selector. Regression guard for the no-flash wiring: the
|
||||
mount path must set the name screen's `continue_screen` and pass a
|
||||
`dependency_result` future. If it regressed to the old
|
||||
`LaunchNameScreen()` with no continue screen, onboarding would fall back
|
||||
to the double-modal flow.
|
||||
"""
|
||||
from deepagents_code.widgets.model_selector import ModelSelectorScreen
|
||||
|
||||
app = DeepAgentsApp(launch_init=True)
|
||||
app._prewarm_deferred_imports = MagicMock() # ty: ignore
|
||||
app._resolve_git_branch_and_continue = AsyncMock() # ty: ignore
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen, LaunchNameScreen)
|
||||
# The name screen is pre-wired to continue into the model selector
|
||||
# rather than dismissing back to the base app.
|
||||
assert isinstance(
|
||||
app.screen._continue_screen, # ty: ignore
|
||||
ModelSelectorScreen,
|
||||
)
|
||||
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen, ModelSelectorScreen)
|
||||
|
||||
launch_task = app._launch_init_task
|
||||
assert launch_task is not None
|
||||
app.screen.action_cancel()
|
||||
await asyncio.wait_for(launch_task, timeout=2)
|
||||
await pilot.pause()
|
||||
|
||||
async def test_launch_init_integrations_flag_inserts_dependency_screen(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""With the opt-in flag set, the integrations summary precedes selection.
|
||||
|
||||
Exercises the full flag-on chain: name -> integrations summary ->
|
||||
model selector. Continuing past the integrations screen must switch
|
||||
into the model selector wired as its `continue_screen`; if that wiring
|
||||
regressed (e.g. `continue_screen` dropped), the user would never reach
|
||||
the selector.
|
||||
"""
|
||||
from deepagents_code._env_vars import ONBOARDING_INTEGRATIONS_SCREEN
|
||||
from deepagents_code.widgets.model_selector import ModelSelectorScreen
|
||||
|
||||
monkeypatch.setenv(ONBOARDING_INTEGRATIONS_SCREEN, "1")
|
||||
app = DeepAgentsApp(launch_init=True)
|
||||
app._prewarm_deferred_imports = MagicMock() # ty: ignore
|
||||
app._resolve_git_branch_and_continue = AsyncMock() # ty: ignore
|
||||
@@ -1089,8 +1134,6 @@ class TestStartupSequence:
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen, LaunchNameScreen)
|
||||
# The name screen is pre-wired to continue into the dependency
|
||||
# summary rather than dismissing back to the base app.
|
||||
assert isinstance(
|
||||
app.screen._continue_screen, # ty: ignore
|
||||
LaunchDependenciesScreen,
|
||||
@@ -1101,14 +1144,51 @@ class TestStartupSequence:
|
||||
|
||||
assert isinstance(app.screen, LaunchDependenciesScreen)
|
||||
|
||||
# Continuing past the integrations summary lands on the model
|
||||
# selector it was built with as `continue_screen`.
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
|
||||
assert isinstance(app.screen, ModelSelectorScreen)
|
||||
|
||||
launch_task = app._launch_init_task
|
||||
assert launch_task is not None
|
||||
app.screen.action_cancel()
|
||||
await asyncio.wait_for(launch_task, timeout=2)
|
||||
await pilot.pause()
|
||||
|
||||
async def test_launch_init_finishes_when_dependency_switch_fails(self) -> None:
|
||||
"""A failed name-to-dependencies switch should skip the rest of setup."""
|
||||
async def test_build_launch_dependencies_prompt_screen_tracks_flag(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The first onboarding screen returned tracks the opt-in flag.
|
||||
|
||||
Locks the return contract of `_build_launch_dependencies_prompt`
|
||||
directly: the model selector is first by default, the integrations
|
||||
summary is first when the flag is set, and the result future starts
|
||||
unresolved in both cases.
|
||||
"""
|
||||
from deepagents_code._env_vars import ONBOARDING_INTEGRATIONS_SCREEN
|
||||
from deepagents_code.widgets.model_selector import ModelSelectorScreen
|
||||
|
||||
app = DeepAgentsApp(launch_init=True)
|
||||
|
||||
monkeypatch.delenv(ONBOARDING_INTEGRATIONS_SCREEN, raising=False)
|
||||
screen, result_future = app._build_launch_dependencies_prompt()
|
||||
assert isinstance(screen, ModelSelectorScreen)
|
||||
assert isinstance(result_future, asyncio.Future)
|
||||
assert not result_future.done()
|
||||
|
||||
monkeypatch.setenv(ONBOARDING_INTEGRATIONS_SCREEN, "1")
|
||||
screen, result_future = app._build_launch_dependencies_prompt()
|
||||
assert isinstance(screen, LaunchDependenciesScreen)
|
||||
assert isinstance(result_future, asyncio.Future)
|
||||
assert not result_future.done()
|
||||
|
||||
async def test_launch_init_finishes_when_first_screen_switch_fails(self) -> None:
|
||||
"""A failed name-to-selector switch should skip the rest of setup."""
|
||||
from deepagents_code.widgets.model_selector import ModelSelectorScreen
|
||||
|
||||
app = DeepAgentsApp(launch_init=True)
|
||||
app._prewarm_deferred_imports = MagicMock() # ty: ignore
|
||||
app._resolve_git_branch_and_continue = AsyncMock() # ty: ignore
|
||||
@@ -1119,8 +1199,8 @@ class TestStartupSequence:
|
||||
app._switch_or_install_launch_model = switch_or_install # ty: ignore
|
||||
original_switch_screen = app.switch_screen
|
||||
|
||||
def fail_dependency_switch(screen: ModalScreen[Any] | str) -> None:
|
||||
if isinstance(screen, LaunchDependenciesScreen):
|
||||
def fail_first_screen_switch(screen: ModalScreen[Any] | str) -> None:
|
||||
if isinstance(screen, ModelSelectorScreen):
|
||||
msg = "stack torn down"
|
||||
raise ScreenStackError(msg)
|
||||
original_switch_screen(screen)
|
||||
@@ -1131,7 +1211,7 @@ class TestStartupSequence:
|
||||
assert isinstance(app.screen, LaunchNameScreen)
|
||||
launch_task = app._launch_init_task
|
||||
assert launch_task is not None
|
||||
app.switch_screen = fail_dependency_switch # ty: ignore
|
||||
app.switch_screen = fail_first_screen_switch # ty: ignore
|
||||
|
||||
await pilot.press("a", "d", "a", "enter")
|
||||
await pilot.pause()
|
||||
@@ -1604,7 +1684,8 @@ class TestStartupSequence:
|
||||
screen._description
|
||||
== "These models have performed well in Deep Agents evals and are "
|
||||
"a solid starting set. You can explore the full model list "
|
||||
"later with /model."
|
||||
"later with /model. Sandboxes and other integrations install "
|
||||
"anytime with /install."
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user