[cursor] Clarify CLI setup readiness (#2733)
* 🐛 fix(cursor): Clarify CLI setup readiness - Keep Cursor CLI and SDK configuration under one setup entry - Prioritize cursor-agent install/login readiness over API-key state - Surface actionable install and login guidance in the web picker * 📸 docs(cursor): Add setup guidance demo * 🎨 style(web): Apply locked Prettier formatting Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> * 🧪 test(web): Cover Cursor setup guidance end to end Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> --------- Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
+108
-17
@@ -10232,8 +10232,8 @@ def _prompt_install_cursor() -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _manage_cursor_harness() -> None:
|
||||
"""Run the level-2 loop for Cursor: manage its ``CURSOR_API_KEY``.
|
||||
def _manage_cursor_sdk_harness() -> None:
|
||||
"""Run the Cursor SDK loop: manage its ``CURSOR_API_KEY``.
|
||||
|
||||
Cursor runs via the ``cursor-sdk`` package and authenticates against
|
||||
Cursor's own backend with a ``CURSOR_API_KEY`` — the SDK requires one (a
|
||||
@@ -10350,6 +10350,84 @@ def _set_cursor_api_key() -> str | None:
|
||||
return "✓ Cursor API key stored"
|
||||
|
||||
|
||||
def _manage_cursor_native_harness() -> None:
|
||||
"""Configure the ``cursor-agent`` CLI used by the built-in web agent."""
|
||||
from omnigent.onboarding.harness_install import (
|
||||
CURSOR_KEY,
|
||||
harness_cli_installed,
|
||||
harness_cli_logged_in,
|
||||
harness_install_spec,
|
||||
harness_login,
|
||||
harness_logout,
|
||||
)
|
||||
from omnigent.onboarding.interactive import console, select
|
||||
|
||||
if not harness_cli_installed(CURSOR_KEY):
|
||||
spec = harness_install_spec(CURSOR_KEY)
|
||||
hint = (
|
||||
spec.install_hint
|
||||
if spec and spec.install_hint
|
||||
else "curl https://cursor.com/install -fsS | bash"
|
||||
)
|
||||
console.print(
|
||||
" Cursor CLI isn't installed. Install it with:\n"
|
||||
f" [bold]{hint}[/bold]\n"
|
||||
" then run [bold]cursor-agent login[/bold] or re-open this menu."
|
||||
)
|
||||
return
|
||||
|
||||
status: str | None = None
|
||||
while True:
|
||||
logged_in = harness_cli_logged_in(CURSOR_KEY)
|
||||
header = "Cursor CLI — logged in" if logged_in else "Cursor CLI — not logged in yet"
|
||||
rows = [_HarnessMenuRow("Sign in (cursor-agent login)", action="login")]
|
||||
if logged_in:
|
||||
rows.append(_HarnessMenuRow("Sign out (cursor-agent logout)", action="logout"))
|
||||
rows.append(_HarnessMenuRow("← Back", action="back"))
|
||||
idx = select(header, [row.label for row in rows], clear_on_exit=True, status=status)
|
||||
if idx < 0 or rows[idx].action == "back":
|
||||
return
|
||||
if rows[idx].action == "login":
|
||||
status = (
|
||||
"✓ Cursor CLI logged in" if harness_login(CURSOR_KEY) else "Login not detected"
|
||||
)
|
||||
elif rows[idx].action == "logout":
|
||||
status = "✓ Cursor CLI logged out" if harness_logout(CURSOR_KEY) else "Logout failed"
|
||||
|
||||
|
||||
def _manage_cursor_harness() -> None:
|
||||
"""Configure Cursor CLI and SDK from one consolidated setup entry."""
|
||||
from omnigent.onboarding.cursor_auth import cursor_api_key_configured
|
||||
from omnigent.onboarding.harness_install import (
|
||||
CURSOR_KEY,
|
||||
harness_cli_installed,
|
||||
harness_cli_logged_in,
|
||||
)
|
||||
from omnigent.onboarding.interactive import select
|
||||
|
||||
while True:
|
||||
cli_status = (
|
||||
"logged in"
|
||||
if harness_cli_logged_in(CURSOR_KEY)
|
||||
else "needs login"
|
||||
if harness_cli_installed(CURSOR_KEY)
|
||||
else "not installed"
|
||||
)
|
||||
sdk_status = "API key configured" if cursor_api_key_configured() else "not configured"
|
||||
rows = [
|
||||
_HarnessMenuRow(f"Cursor CLI — {cli_status}", action="cli"),
|
||||
_HarnessMenuRow(f"Cursor SDK — {sdk_status}", action="sdk"),
|
||||
_HarnessMenuRow("← Back", action="back"),
|
||||
]
|
||||
idx = select("Cursor setup", [row.label for row in rows], clear_on_exit=True)
|
||||
if idx < 0 or rows[idx].action == "back":
|
||||
return
|
||||
if rows[idx].action == "cli":
|
||||
_manage_cursor_native_harness()
|
||||
elif rows[idx].action == "sdk":
|
||||
_manage_cursor_sdk_harness()
|
||||
|
||||
|
||||
def _prompt_install_antigravity() -> str | None:
|
||||
"""Offer to install the missing ``antigravity`` extra; return a status line.
|
||||
|
||||
@@ -11825,11 +11903,7 @@ def _run_configure_harnesses_interactive() -> None:
|
||||
copilot_github_token_configured,
|
||||
copilot_sdk_installed,
|
||||
)
|
||||
from omnigent.onboarding.cursor_auth import (
|
||||
CURSOR_EXTRA,
|
||||
cursor_api_key_configured,
|
||||
cursor_sdk_installed,
|
||||
)
|
||||
from omnigent.onboarding.cursor_auth import cursor_api_key_configured
|
||||
from omnigent.onboarding.extra_install import extra_install_display
|
||||
from omnigent.onboarding.goose_auth import goose_config_summary
|
||||
from omnigent.onboarding.harness_install import (
|
||||
@@ -11842,6 +11916,7 @@ def _run_configure_harnesses_interactive() -> None:
|
||||
OPENCODE_KEY,
|
||||
QWEN_KEY,
|
||||
harness_cli_installed,
|
||||
harness_cli_logged_in,
|
||||
harness_install_command,
|
||||
harness_install_spec,
|
||||
)
|
||||
@@ -11976,29 +12051,45 @@ def _run_configure_harnesses_interactive() -> None:
|
||||
rows.append(_family_row(ANTHROPIC_FAMILY))
|
||||
rows.append(_family_row(OPENAI_FAMILY))
|
||||
|
||||
# Cursor — readiness is the CURSOR_API_KEY (the cursor-sdk extra is a
|
||||
# soft dependency; the key is independently storable, so a missing SDK
|
||||
# is surfaced as the install hint, not a hard block).
|
||||
if cursor_api_key_configured(config) or bool(os.environ.get("CURSOR_API_KEY")):
|
||||
rows.append((CURSOR_KEY, "Cursor", "API key", "ready", ""))
|
||||
elif not cursor_sdk_installed():
|
||||
# Cursor setup covers both surfaces, but readiness prioritizes the CLI
|
||||
# used by the built-in web agent. An SDK key never hides a CLI problem.
|
||||
cursor_sdk_ready = cursor_api_key_configured(config) or bool(
|
||||
os.environ.get("CURSOR_API_KEY")
|
||||
)
|
||||
if not harness_cli_installed(CURSOR_KEY):
|
||||
cursor_spec = harness_install_spec(CURSOR_KEY)
|
||||
cursor_hint = (
|
||||
cursor_spec.install_hint
|
||||
if cursor_spec and cursor_spec.install_hint
|
||||
else "curl https://cursor.com/install -fsS | bash"
|
||||
)
|
||||
rows.append(
|
||||
(
|
||||
CURSOR_KEY,
|
||||
"Cursor",
|
||||
"Not installed",
|
||||
"CLI not installed · SDK ready" if cursor_sdk_ready else "CLI not installed",
|
||||
"missing",
|
||||
_install_hint(extra_install_display(CURSOR_EXTRA)),
|
||||
_install_hint(cursor_hint),
|
||||
),
|
||||
)
|
||||
elif harness_cli_logged_in(CURSOR_KEY):
|
||||
rows.append(
|
||||
(
|
||||
CURSOR_KEY,
|
||||
"Cursor",
|
||||
"CLI + SDK ready" if cursor_sdk_ready else "CLI ready",
|
||||
"ready",
|
||||
"",
|
||||
)
|
||||
)
|
||||
else:
|
||||
rows.append(
|
||||
(
|
||||
CURSOR_KEY,
|
||||
"Cursor",
|
||||
"Not configured",
|
||||
"CLI needs login · SDK ready" if cursor_sdk_ready else "CLI needs login",
|
||||
"warn",
|
||||
"Open to add the Cursor API key.",
|
||||
"Open to run `cursor-agent login`.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -129,6 +129,10 @@ def _harnesses_installed(monkeypatch):
|
||||
"omnigent.onboarding.harness_install.harness_logout",
|
||||
lambda family: True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_logged_in",
|
||||
lambda family: True,
|
||||
)
|
||||
|
||||
|
||||
def _config_yaml(config_home) -> dict[str, object]:
|
||||
@@ -1757,6 +1761,43 @@ def test_overview_lists_kiro_row(isolated_config, monkeypatch) -> None:
|
||||
assert "kiro-cli login" in Text.from_markup(descriptions[kiro]).plain
|
||||
|
||||
|
||||
def test_overview_reports_missing_cursor_cli_despite_sdk_api_key(
|
||||
isolated_config, monkeypatch
|
||||
) -> None:
|
||||
"""A Cursor SDK key must not make the native Cursor CLI look ready."""
|
||||
from rich.text import Text
|
||||
|
||||
monkeypatch.setenv("CURSOR_API_KEY", "crsr_sdk_only")
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed",
|
||||
lambda key: key != "cursor",
|
||||
)
|
||||
|
||||
options, selectable, descriptions, _, _max_visible = _capture_setup_overview(monkeypatch)
|
||||
names = _overview_row_names(options, selectable)
|
||||
|
||||
assert "Cursor CLI" not in names
|
||||
assert "Cursor SDK" not in names
|
||||
cursor = names.index("Cursor")
|
||||
assert "CLI not installed" in Text.from_markup(options[cursor]).plain
|
||||
assert "SDK ready" in Text.from_markup(options[cursor]).plain
|
||||
assert "cursor.com/install" in Text.from_markup(descriptions[cursor]).plain
|
||||
|
||||
|
||||
def test_missing_cursor_cli_drillin_shows_install_and_login(isolated_config, monkeypatch) -> None:
|
||||
"""The consolidated Cursor setup gives both steps needed by the web agent."""
|
||||
monkeypatch.setattr(
|
||||
"omnigent.onboarding.harness_install.harness_cli_installed",
|
||||
lambda key: key != "cursor",
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input="3\n1\nq\nq\n")
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "curl https://cursor.com/install -fsS | bash" in result.output
|
||||
assert "cursor-agent login" in result.output
|
||||
|
||||
|
||||
def test_overview_hermes_row_reflects_configured_model(isolated_config, monkeypatch) -> None:
|
||||
"""Hermes reads ready (with its picked model) once ``hermes model`` has run.
|
||||
|
||||
@@ -1986,7 +2027,7 @@ def test_overview_descriptions_map_to_their_rows(isolated_config, monkeypatch) -
|
||||
}
|
||||
assert desc_by_name["Claude"] == "Open to add a credential."
|
||||
assert desc_by_name["Codex"] == "Open to add a credential."
|
||||
assert desc_by_name["Cursor"] == "Open to add the Cursor API key."
|
||||
assert desc_by_name["Cursor"] == ""
|
||||
assert desc_by_name["OpenCode"] == "Open to sign in (opencode auth login)."
|
||||
assert desc_by_name["Hermes"] == "Open to configure with `hermes model`."
|
||||
assert desc_by_name["Pi"] == "Open to add a credential."
|
||||
@@ -2422,7 +2463,8 @@ def test_add_menu_readds_dismissed_cli_config_credential(isolated_config) -> Non
|
||||
|
||||
# ── Cursor API-key flow ─────────────────────────────────────────────────────
|
||||
# Cursor runs via the ``cursor-sdk`` package and authenticates with a
|
||||
# ``CURSOR_API_KEY``; it has no provider/gateway family. Its drill-in (L1 row 4)
|
||||
# ``CURSOR_API_KEY``; it has no provider/gateway family. Its drill-in is under
|
||||
# the consolidated Cursor row (L1 row 3, then Cursor SDK).
|
||||
# stores the key in the secret store + a dedicated ``cursor:`` config block,
|
||||
# mirroring the other harnesses' api-key persistence. The menu is API-key-only
|
||||
# (Set/Replace/Remove), so it touches neither the ``cursor-agent`` binary nor a
|
||||
@@ -2456,9 +2498,8 @@ def test_cursor_set_api_key_paste_writes_block_and_secret(
|
||||
Proves the api-key path: the secret lands in the store (never plaintext in
|
||||
config) and the config references it via ``keychain:cursor``.
|
||||
"""
|
||||
# L1 3=Cursor → cursor menu 1=Set API key → paste key (crsr_ → no warn) →
|
||||
# cursor menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["3", "1", "crsr_test_key_123", "q", "q"]) + "\n"
|
||||
# L1 Cursor → Cursor SDK → Set API key → paste key → back through both menus.
|
||||
stdin = "\n".join(["3", "2", "1", "crsr_test_key_123", "q", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2478,9 +2519,8 @@ def test_cursor_adopt_env_api_key_writes_env_ref(
|
||||
at the live environment variable so the key never leaves the user's shell.
|
||||
"""
|
||||
monkeypatch.setenv("CURSOR_API_KEY", "crsr_env_key_456")
|
||||
# L1 3=Cursor → 1=Set API key → "y" adopt detected $CURSOR_API_KEY →
|
||||
# q back → q quit.
|
||||
stdin = "\n".join(["3", "1", "y", "q", "q"]) + "\n"
|
||||
# L1 Cursor → Cursor SDK → Set API key → adopt detected env key → back.
|
||||
stdin = "\n".join(["3", "2", "1", "y", "q", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2499,9 +2539,8 @@ def test_cursor_remove_api_key_drops_block_and_secret(
|
||||
with open(config_path, "w") as f:
|
||||
yaml.safe_dump({"cursor": {"api_key_ref": "keychain:cursor"}}, f)
|
||||
|
||||
# L1 3=Cursor → cursor menu (key set: 1=Replace 2=Remove 3=Back) → 2=Remove
|
||||
# → q back → q quit.
|
||||
stdin = "\n".join(["3", "2", "q", "q"]) + "\n"
|
||||
# L1 Cursor → Cursor SDK → Remove → back through both menus.
|
||||
stdin = "\n".join(["3", "2", "2", "q", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2518,9 +2557,8 @@ def test_cursor_set_api_key_non_crsr_declined_is_not_stored(
|
||||
The soft prefix check warns and asks to store anyway; declining must leave
|
||||
both the secret store and the config untouched.
|
||||
"""
|
||||
# L1 3=Cursor → 1=Set API key → paste non-crsr_ key → "n" decline warning →
|
||||
# q back → q quit.
|
||||
stdin = "\n".join(["3", "1", "sk-not-a-cursor-key", "n", "q", "q"]) + "\n"
|
||||
# L1 Cursor → Cursor SDK → Set → decline the non-crsr_ warning → back.
|
||||
stdin = "\n".join(["3", "2", "1", "sk-not-a-cursor-key", "n", "q", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2550,23 +2588,18 @@ def _cursor_sdk_absent(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
def test_cursor_overview_install_command_is_selection_only(
|
||||
def test_cursor_overview_stays_cli_ready_when_sdk_missing(
|
||||
isolated_config, _cursor_sdk_absent, monkeypatch
|
||||
) -> None:
|
||||
"""With the cursor extra absent, the Cursor row's install command is its description.
|
||||
|
||||
The install command (dynamically computed) is the selection-only hint —
|
||||
the selector's per-row description, shown when the row is highlighted —
|
||||
and is NOT baked into the always-visible row label.
|
||||
"""
|
||||
"""A missing SDK does not duplicate or downgrade a ready Cursor CLI row."""
|
||||
from rich.text import Text
|
||||
|
||||
options, selectable, descriptions, _, _max_visible = _capture_setup_overview(monkeypatch)
|
||||
names = _overview_row_names(options, selectable)
|
||||
assert names.count("Cursor") == 1
|
||||
cursor = names.index("Cursor")
|
||||
assert "omnigent[cursor]" in Text.from_markup(descriptions[cursor]).plain
|
||||
# The command lives in the description only — never the always-visible row.
|
||||
assert "omnigent[cursor]" not in Text.from_markup(options[cursor]).plain
|
||||
assert "CLI ready" in Text.from_markup(options[cursor]).plain
|
||||
assert Text.from_markup(descriptions[cursor]).plain == ""
|
||||
|
||||
|
||||
def test_cursor_drillin_offers_install_when_sdk_missing(
|
||||
@@ -2577,8 +2610,8 @@ def test_cursor_drillin_offers_install_when_sdk_missing(
|
||||
Here the user picks "show the command" (choice 3), which prints it and falls
|
||||
through to the key menu, then backs out.
|
||||
"""
|
||||
# L1 3=Cursor → install offer 3=show command → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["3", "3", "q", "q"]) + "\n"
|
||||
# Cursor → Cursor SDK → show command → back through both menus.
|
||||
stdin = "\n".join(["3", "2", "3", "q", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
out = result.output
|
||||
@@ -2593,9 +2626,8 @@ def test_cursor_key_settable_when_sdk_missing(isolated_config, _cursor_sdk_absen
|
||||
NOT gate key management on it. Here the user declines ("set the key anyway" =
|
||||
choice 2), then sets the key — which must persist as it does with the SDK.
|
||||
"""
|
||||
# L1 3=Cursor → install offer 2=set key anyway → key menu 1=Set →
|
||||
# paste crsr_ key → key menu q=back → L1 q=quit.
|
||||
stdin = "\n".join(["3", "2", "1", "crsr_key_no_sdk", "q", "q"]) + "\n"
|
||||
# Cursor → Cursor SDK → set key anyway → Set → paste key → back.
|
||||
stdin = "\n".join(["3", "2", "2", "1", "crsr_key_no_sdk", "q", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2624,8 +2656,8 @@ def test_cursor_install_now_invokes_runner_without_index(
|
||||
monkeypatch.setattr("omnigent.onboarding.extra_install.shutil.which", lambda name: None)
|
||||
monkeypatch.setattr("omnigent.onboarding.cursor_auth.subprocess.run", _run)
|
||||
|
||||
# L1 3=Cursor → install offer 1=install now → key menu q=back → L1 q.
|
||||
stdin = "\n".join(["3", "1", "q", "q"]) + "\n"
|
||||
# Cursor → Cursor SDK → install now → back through both menus.
|
||||
stdin = "\n".join(["3", "2", "1", "q", "q", "q"]) + "\n"
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=stdin)
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
@@ -2851,7 +2883,11 @@ def test_copilot_overview_install_command_is_selection_only(
|
||||
@pytest.mark.parametrize(
|
||||
"choice,sdk_probe,unexpected_header",
|
||||
[
|
||||
("3", "omnigent.onboarding.cursor_auth.cursor_sdk_installed", "Cursor — no API key yet"),
|
||||
(
|
||||
"3\n2",
|
||||
"omnigent.onboarding.cursor_auth.cursor_sdk_installed",
|
||||
"Cursor — no API key yet",
|
||||
),
|
||||
(
|
||||
"7",
|
||||
"omnigent.onboarding.antigravity_auth.antigravity_sdk_installed",
|
||||
@@ -2876,7 +2912,7 @@ def test_soft_sdk_install_prompt_abort_returns_to_overview(
|
||||
"""
|
||||
monkeypatch.setattr(sdk_probe, lambda: False)
|
||||
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=f"{choice}\nq\nq\n")
|
||||
result = CliRunner().invoke(cli, ["setup", "--no-internal-beta"], input=f"{choice}\nq\nq\nq\n")
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert unexpected_header not in result.output
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""E2E: Cursor setup guidance in the New Chat agent picker."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from playwright.sync_api import Page, Route, expect
|
||||
|
||||
_HOST_ID = "host_cursor_e2e"
|
||||
_HOST_NAME = "cursor-e2e-host"
|
||||
_AGENT_ID = "ag_cursor_e2e"
|
||||
|
||||
|
||||
def _fulfill_hosts(route: Route) -> None:
|
||||
route.fulfill(
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
body=json.dumps(
|
||||
{
|
||||
"hosts": [
|
||||
{
|
||||
"host_id": _HOST_ID,
|
||||
"name": _HOST_NAME,
|
||||
"owner": "e2e",
|
||||
"status": "online",
|
||||
"configured_harnesses": {"cursor-native": False},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _fulfill_agents(route: Route) -> None:
|
||||
route.fulfill(
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
body=json.dumps(
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": _AGENT_ID,
|
||||
"name": "cursor-native-ui",
|
||||
"display_name": "Cursor",
|
||||
"description": "Cursor's coding agent",
|
||||
"harness": "cursor-native",
|
||||
"skills": [],
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _fulfill_empty_agent_scan(route: Route) -> None:
|
||||
route.fulfill(
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
body=json.dumps({"data": []}),
|
||||
)
|
||||
|
||||
|
||||
def test_cursor_missing_cli_shows_install_and_login_guidance(
|
||||
page: Page,
|
||||
seeded_session: tuple[str, str],
|
||||
) -> None:
|
||||
"""A missing Cursor CLI is badged and explained before session launch."""
|
||||
base_url, session_id = seeded_session
|
||||
del session_id
|
||||
|
||||
page.route("**/v1/hosts", _fulfill_hosts)
|
||||
page.route("**/v1/agents", _fulfill_agents)
|
||||
page.route(re.compile(r"/v1/sessions\?.*kind=any"), _fulfill_empty_agent_scan)
|
||||
page.add_init_script(
|
||||
f"""window.localStorage.setItem(
|
||||
"omnigent:recent-workspaces",
|
||||
JSON.stringify({{ {_HOST_ID!r}: ["/work/repo"] }})
|
||||
);"""
|
||||
)
|
||||
|
||||
page.goto(f"{base_url}/")
|
||||
composer = page.get_by_test_id("new-chat-landing-input")
|
||||
expect(composer).to_be_visible(timeout=30_000)
|
||||
|
||||
warning = page.get_by_test_id("new-chat-landing-harness-warning")
|
||||
expect(warning).to_be_visible(timeout=30_000)
|
||||
expect(warning).to_contain_text(f"Cursor needs cursor-agent on {_HOST_NAME}")
|
||||
expect(warning).to_contain_text("curl https://cursor.com/install -fsS | bash")
|
||||
expect(warning).to_contain_text("cursor-agent login")
|
||||
expect(warning.locator("code")).to_have_count(2)
|
||||
|
||||
# The guidance is visible before launch and remains warning-only.
|
||||
composer.fill("help me inspect this repository")
|
||||
expect(page.get_by_test_id("new-chat-landing-submit")).to_be_enabled()
|
||||
expect(warning).to_be_visible()
|
||||
|
||||
page.get_by_test_id("new-chat-landing-agent-select").click()
|
||||
badge = page.get_by_test_id(f"new-chat-landing-agent-warning-{_AGENT_ID}")
|
||||
expect(badge).to_be_visible()
|
||||
expect(badge).to_have_text("install & login")
|
||||
@@ -459,6 +459,16 @@ describe("harnessUnconfiguredOnHost", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows native Cursor's install and login steps before launch", () => {
|
||||
const testHost = hostWith({ "cursor-native": false });
|
||||
const reason = harnessUnavailableReasonOnHost("cursor-native", testHost);
|
||||
expect(reason).toBe("cursor-cli-missing");
|
||||
expect(harnessWarningBadgeText(reason)).toBe("install & login");
|
||||
expect(harnessWarningMessageText("Cursor", "laptop", reason)).toBe(
|
||||
"Cursor needs cursor-agent on laptop — install it with `curl https://cursor.com/install -fsS | bash`, then run `cursor-agent login`.",
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces structured codex unavailable reasons", () => {
|
||||
const testHost = hostWith({ codex: "needs-auth", "codex-native": "binary-missing" });
|
||||
expect(harnessUnconfiguredOnHost("codex", testHost)).toBe(true);
|
||||
|
||||
@@ -544,13 +544,21 @@ function isCodexHarness(harness: string): boolean {
|
||||
return harness === "codex" || harness === "codex-native" || harness === "native-codex";
|
||||
}
|
||||
|
||||
function isNativeCursorHarness(harness: string): boolean {
|
||||
return harness === "cursor-native" || harness === "native-cursor";
|
||||
}
|
||||
|
||||
export function harnessUnavailableReasonOnHost(
|
||||
harness: string | null | undefined,
|
||||
host: Host | undefined | null,
|
||||
): string | null {
|
||||
if (!harness || !host?.configured_harnesses) return null;
|
||||
const availability = host.configured_harnesses[harness];
|
||||
if (availability === false) return isCodexHarness(harness) ? "binary-missing" : "unconfigured";
|
||||
if (availability === false) {
|
||||
if (isCodexHarness(harness)) return "binary-missing";
|
||||
if (isNativeCursorHarness(harness)) return "cursor-cli-missing";
|
||||
return "unconfigured";
|
||||
}
|
||||
if (
|
||||
isCodexHarness(harness) &&
|
||||
(availability === "binary-missing" || availability === "needs-auth")
|
||||
@@ -564,6 +572,7 @@ export function harnessUnavailableReasonOnHost(
|
||||
export function harnessWarningBadgeText(reason: string | null): string {
|
||||
if (reason === "binary-missing") return "binary missing";
|
||||
if (reason === "needs-auth") return "needs auth";
|
||||
if (reason === "cursor-cli-missing") return "install & login";
|
||||
return "needs setup";
|
||||
}
|
||||
|
||||
@@ -572,6 +581,9 @@ export function harnessWarningMessageText(
|
||||
hostName: string | undefined,
|
||||
reason: string | null,
|
||||
): string {
|
||||
if (reason === "cursor-cli-missing") {
|
||||
return `${agentName} needs cursor-agent on ${hostName} — install it with \`curl https://cursor.com/install -fsS | bash\`, then run \`cursor-agent login\`.`;
|
||||
}
|
||||
if (reason === "needs-auth") {
|
||||
return `${agentName} needs Codex authentication on ${hostName} — run codex login on that machine.`;
|
||||
}
|
||||
@@ -586,6 +598,15 @@ function harnessWarningMessage(
|
||||
hostName: string | undefined,
|
||||
reason: string | null,
|
||||
): ReactNode {
|
||||
if (reason === "cursor-cli-missing") {
|
||||
return (
|
||||
<>
|
||||
{agentName} needs cursor-agent on {hostName} — install it with{" "}
|
||||
<code>curl https://cursor.com/install -fsS | bash</code>, then run{" "}
|
||||
<code>cursor-agent login</code>.
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (reason === "needs-auth") {
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user