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:
Mason Daugherty
2026-06-25 00:02:48 -04:00
committed by GitHub
parent 4600365ea0
commit 6c930c5e45
4 changed files with 132 additions and 19 deletions
+9
View File
@@ -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.
+19 -4
View File
@@ -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",
+96 -15
View File
@@ -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."
)