Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,23 @@ def _policy_summary_lines(managed: dict) -> list[str]:
return lines


def _print_managed_summary(managed: dict, state: dict, tool: str | None) -> None:
def _print_managed_summary(
managed: dict, state: dict, tool: str | None, *, abridged: bool = False
) -> None:
"""Show which of the admin's settings are in force.

With ``tool`` set (launch path) the per-agent Agent/Provider/Model lines are included;
with ``tool=None`` (e.g. ``ucode configure`` under a managed config) they are skipped
since no single agent has been chosen yet.

``abridged`` prints only what changes launch-to-launch — the agent and model this run will use,
and the policy in force — with a pointer to ``ucode status`` for the rest. Bare ``ucode`` runs
every session, so re-enumerating the workspace's full MCP/skills/tier list each time is noise;
the full box stays for ``status`` and ``configure``, where the reader asked to see it.
"""
if abridged:
_print_managed_summary_abridged(managed, state, tool)
return
lines = [f"[bold]Workspace:[/bold] [cyan]{state.get('workspace', '?')}[/cyan]"]
if tool is not None:
lines.append(f"[bold]Agent:[/bold] [green]{TOOL_SPECS[tool]['display']}[/green]")
Expand Down Expand Up @@ -220,6 +230,27 @@ def _print_managed_summary(managed: dict, state: dict, tool: str | None) -> None
)


def _print_managed_summary_abridged(managed: dict, state: dict, tool: str | None) -> None:
"""One-line launch banner: which agent (and model) this managed run is launching.

Bare ``ucode`` runs every session, so the full box's MCP/skills/policy enumeration is noise
each time; ``ucode status`` still shows all of it. See ``_print_managed_summary``'s ``abridged``
note. ``tool`` is always set on the launch path, but is guarded for callers that pass None."""
if tool is None:
print_note("Using managed config.")
return
agent = TOOL_SPECS[tool]["display"]
model = managed_default_model(managed, tool)
model_suffix = f" with [magenta]{model}[/magenta]" if model else ""
# "as the default agent" only when this really is the config's default: a budget tier can
# override the default and launch a different agent, and the tier note in `_launch_tool` already
# explains that case — so claiming "default" here would contradict it.
role = " as the default agent" if tool == managed.get("default_agent") else ""
console.print(
f"[dim]•[/dim] Using managed config — launching [green]{agent}[/green]{role}{model_suffix}"
)


def _resolve_workspace_then_maybe_reject(
workspace_entries: list[tuple[str, str | None]] | None,
) -> list[tuple[str, str | None]] | None:
Expand Down Expand Up @@ -926,6 +957,16 @@ def status() -> int:
if profile:
print_kv("CLI profile", profile)

# When the workspace publishes a managed config and this run has the feature switched on, that
# admin-authored config is what launches actually apply — so surface the whole setup as one box
# here too, rather than leaving a developer to infer it from the per-agent rows below. Read from
# the local cache (no network): status is a quick, offline-safe glance, and the cache is what the
# last launch persisted for this workspace.
if workspace and managed_agent_config_enabled():
managed = load_managed_state(workspace)
if managed:
_print_managed_summary(managed, state, None)

print_heading("Coding Agents")
for tool, spec in TOOL_SPECS.items():
configured = tool in configured_tools
Expand Down Expand Up @@ -2008,7 +2049,7 @@ def _launch_managed_default(
"Your workspace's managed config names no agent to launch. Ask an admin to set a "
"default agent, or run `ucode <agent>` directly."
)
_print_managed_summary(managed, state, tool)
_print_managed_summary(managed, state, tool, abridged=True)
_launch_tool(
tool,
ctx,
Expand Down
74 changes: 74 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,50 @@ def test_status_treats_available_tools_as_configured_agents(self):
assert "https://example.databricks.com/ai-gateway/anthropic" not in result.output
assert "https://example.databricks.com/ai-gateway/gemini" not in result.output

def test_status_shows_managed_config_box_when_present_and_enabled(self, monkeypatch):
monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1")
managed = {
"enabled_agents": {"claude": {}, "codex": {}},
"mcp_servers": [{"name": "github-mcp", "type": "external"}],
"skills": {"names": ["debug-ci"]},
}
with (
patch("ucode.cli.load_state", return_value=MINIMAL_STATE),
patch("ucode.cli.load_managed_state", return_value=managed),
):
result = runner.invoke(app, ["status"])

assert result.exit_code == 0, result.output
assert "Workspace-managed config" in result.output
assert "Enabled agents:" in result.output
assert "github-mcp" in result.output
assert "debug-ci" in result.output

def test_status_hides_managed_config_box_when_feature_disabled(self, monkeypatch):
monkeypatch.delenv("ENABLE_MANAGED_AGENT_CONFIG", raising=False)
managed = {"enabled_agents": {"claude": {}}}
with (
patch("ucode.cli.load_state", return_value=MINIMAL_STATE),
patch("ucode.cli.load_managed_state", return_value=managed) as load_managed,
):
result = runner.invoke(app, ["status"])

assert result.exit_code == 0, result.output
assert "Workspace-managed config" not in result.output
# Feature off: the managed cache is never consulted.
load_managed.assert_not_called()

def test_status_hides_managed_config_box_when_none_present(self, monkeypatch):
monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1")
with (
patch("ucode.cli.load_state", return_value=MINIMAL_STATE),
patch("ucode.cli.load_managed_state", return_value=None),
):
result = runner.invoke(app, ["status"])

assert result.exit_code == 0, result.output
assert "Workspace-managed config" not in result.output


class TestConfigureSkillsCommand:
def test_mcp_flag_dispatches_location_set(self):
Expand Down Expand Up @@ -2801,6 +2845,36 @@ def test_falls_back_to_the_first_enabled_agent(self, monkeypatch):
assert result.exit_code == 0, result.output
assert launched[0][0] == "opencode"

def test_launch_banner_is_abridged_not_the_full_box(self, monkeypatch):
managed = {
"default_agent": "claude",
"enabled_agents": {"claude": {"model_config": {"default_model": "system.ai.opus"}}},
"mcp_servers": [{"name": "system.ai.slack", "type": "mcp-service"}],
"skills": {"names": ["main.default.my_skill"]},
}
result, _ = self._run(monkeypatch, managed=managed)
assert result.exit_code == 0, result.output
# One-line banner: the agent it launches, and the model.
assert "launching Claude Code as the default agent" in result.output
assert "system.ai.opus" in result.output
# The full box's per-config enumeration is left to `ucode status`.
assert "Enabled agents:" not in result.output
assert "system.ai.slack" not in result.output
assert "main.default.my_skill" not in result.output

def test_launch_banner_omits_default_agent_when_a_tier_overrides(self, monkeypatch):
# A budget tier can launch a different agent than the config's default; the banner must not
# then call it "the default agent" (the tier note in _launch_tool explains the swap).
managed = {"default_agent": "claude", "enabled_agents": {"claude": {}, "opencode": {}}}
monkeypatch.setattr(
"ucode.cli._fetch_budget_recommendation", lambda state, m: {"agent": "opencode"}
)
result, launched = self._run(monkeypatch, managed=managed)
assert result.exit_code == 0, result.output
assert launched[0][0] == "opencode"
assert "launching OpenCode" in result.output
assert "as the default agent" not in result.output

def test_admin_without_a_config_is_pointed_at_setup(self, monkeypatch):
result, launched = self._run(monkeypatch, managed=None, is_admin=True)
assert result.exit_code == 0, result.output
Expand Down
Loading