fix(code): editable-install guidance for adding extras (#3610)

Correct misleading recovery hints for users on an editable `uv tool
install -e …` setup who try to add an optional extra. The old text told
them to `uv sync --extra <name>` from the source directory, which only
touches the source `.venv` — never the tool environment that `dcode`
actually runs from. Also fixes a Rich markup bug where the literal
`[quickjs]` in the hint was parsed as a style tag and stripped from
output.
This commit is contained in:
Mason Daugherty
2026-05-26 22:01:03 -04:00
committed by GitHub
parent 7d46357c54
commit 771e55f171
7 changed files with 80 additions and 22 deletions
+3 -3
View File
@@ -3180,6 +3180,7 @@ class DeepAgentsApp(App):
)
from deepagents_code.update_check import (
create_update_log_path,
editable_extra_hint,
install_extra_command,
is_valid_extra_name,
perform_install_extra,
@@ -3203,9 +3204,8 @@ class DeepAgentsApp(App):
if await asyncio.to_thread(_is_editable_install):
await self._mount_message(
AppMessage(
"Cannot install extras on editable installs. "
f"Run `uv sync --extra {extra}` from the "
"deepagents-code source directory instead.",
"Editable install detected — cannot install extras.\n"
+ editable_extra_hint(extra),
),
)
return
+14 -4
View File
@@ -290,10 +290,20 @@ def verify_interpreter_deps() -> None:
found = False
if not found:
msg = (
"Missing dependencies for --interpreter. "
"Install with: dcode --install quickjs"
)
from deepagents_code.config import _is_editable_install
if _is_editable_install():
from deepagents_code.update_check import editable_extra_hint
msg = (
"Missing dependencies for --interpreter. Editable install "
f"detected — {editable_extra_hint('quickjs')}"
)
else:
msg = (
"Missing dependencies for --interpreter. "
"Install with: dcode --install quickjs"
)
raise ImportError(msg)
+4 -3
View File
@@ -1988,6 +1988,7 @@ def cli_main() -> None:
from deepagents_code.extras_info import KNOWN_EXTRAS
from deepagents_code.update_check import (
create_update_log_path,
editable_extra_hint,
install_extra_command,
is_valid_extra_name,
perform_install_extra,
@@ -2011,9 +2012,9 @@ def cli_main() -> None:
if _is_editable_install():
console.print(
"[bold yellow]Warning:[/bold yellow] "
"--install is not supported on editable installs. "
f"Run [cyan]uv sync --extra {extra}[/cyan] from the "
"deepagents-code source directory instead."
"--install is not supported on editable installs.\n"
+ escape(editable_extra_hint(extra)),
highlight=False,
)
sys.exit(1)
+20 -6
View File
@@ -972,6 +972,21 @@ def install_extra_command(extra: str) -> str:
return f"uv tool install -U 'deepagents-code[{extra}]'"
def editable_extra_hint(extra: str) -> str:
"""Return the canonical action hint for editable installs missing an extra.
Shared by every site that detects an editable install and points the user
at the correct `uv tool install --editable` invocation, so wording stays
consistent and the literal `[<extra>]` bracket fragment is centrally
defined (callers that print through Rich markup must still escape it).
"""
return (
"Rerun your `uv tool install --editable` command with "
f"`--with 'deepagents-code[{extra}]'` added so the extra is "
"resolved against the editable source."
)
async def perform_install_extra(
extra: str,
*,
@@ -981,9 +996,9 @@ async def perform_install_extra(
"""Add `extra` to the installed dcode tool environment.
Runs `uv tool install -U 'deepagents-code[<extra>]'`. Editable installs
are refused — the caller should instead `uv sync --extra <extra>` from
the package directory, which we cannot do without knowing the source
location.
are refused — the caller should rerun their `uv tool install --editable`
command with `--with 'deepagents-code[<extra>]'` added so the extra is
resolved against the editable source.
Args:
extra: The extra name to install. Must satisfy `is_valid_extra_name`;
@@ -1004,9 +1019,8 @@ async def perform_install_extra(
method = detect_install_method()
if method == "unknown":
return False, (
"Editable install detected — cannot add extras automatically. "
f"Run `uv sync --extra {extra}` from the deepagents-code source "
"directory instead."
"Editable install detected — cannot add extras automatically.\n"
+ editable_extra_hint(extra)
)
if method == "brew":
# Homebrew formula doesn't expose extras; uv tool install is the
+20 -1
View File
@@ -269,16 +269,35 @@ def test_extras_categories_are_disjoint() -> None:
assert not overlap, f"Extras classified twice in {label}: {sorted(overlap)}"
def test_verify_interpreter_deps_raises_when_module_missing() -> None:
# `verify_interpreter_deps` does a lazy `from deepagents_code.config import
# _is_editable_install` each call, so the symbol is resolved against
# `deepagents_code.config` at call time. Patch the source module — patching
# `deepagents_code.extras_info._is_editable_install` would not work (it isn't
# bound there as a module-level attribute).
def test_verify_interpreter_deps_raises_with_dcode_hint_for_tool_install() -> None:
with (
patch(
"deepagents_code.extras_info.importlib.util.find_spec", return_value=None
),
patch("deepagents_code.config._is_editable_install", return_value=False),
pytest.raises(ImportError, match="dcode --install quickjs"),
):
verify_interpreter_deps()
def test_verify_interpreter_deps_raises_with_uv_hint_for_editable_install() -> None:
with (
patch(
"deepagents_code.extras_info.importlib.util.find_spec", return_value=None
),
patch("deepagents_code.config._is_editable_install", return_value=True),
pytest.raises(
ImportError, match=r"uv tool install --editable.*deepagents-code\[quickjs\]"
),
):
verify_interpreter_deps()
def test_verify_interpreter_deps_passes_when_module_present() -> None:
fake_spec = MagicMock()
with patch(
@@ -236,7 +236,4 @@ async def test_install_slash_editable_install_refuses() -> None:
await pilot.pause()
perform_mock.assert_not_awaited()
app_msgs = [m for m in app.query(AppMessage) if not m._is_markdown]
assert any(
"Cannot install extras on editable installs" in str(m._content)
for m in app_msgs
)
assert any("Editable install detected" in str(m._content) for m in app_msgs)
@@ -21,6 +21,7 @@ from deepagents_code.update_check import (
clear_update_notified,
create_update_log_path,
detect_install_method,
editable_extra_hint,
format_age_suffix,
format_installed_age_suffix,
format_release_age,
@@ -906,6 +907,21 @@ class TestInstallExtraCommand:
install_extra_command("quickjs']; touch /tmp/pwned; '")
class TestEditableExtraHint:
"""`editable_extra_hint` is the shared editable-install action hint."""
def test_contains_uv_command_and_bracketed_extra(self) -> None:
hint = editable_extra_hint("quickjs")
assert "uv tool install --editable" in hint
assert "--with 'deepagents-code[quickjs]'" in hint
def test_extra_is_interpolated_into_brackets(self) -> None:
# The bracket fragment is load-bearing — Rich-markup call sites
# must `escape()` this output, so the bracketed extra must always
# be present in the hint (callers rely on this contract).
assert "[fireworks]" in editable_extra_hint("fireworks")
class TestInstallPackageCommand:
"""`install_package_command` builds a uv tool package install string."""
@@ -938,7 +954,8 @@ class TestPerformInstallExtra:
success, output = await perform_install_extra("quickjs")
assert success is False
assert "Editable install" in output
assert "uv sync --extra quickjs" in output
assert "uv tool install --editable" in output
assert "--with 'deepagents-code[quickjs]'" in output
async def test_brew_install_refuses(self) -> None:
"""Homebrew formula doesn't expose extras."""