Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ All notable changes to SyncMill are documented here.

### Fixed

- Without a terminal, bare `syncmill start` and `start` without `--task`
print their refusal and the commands to use instead on separate lines. They
used to come out as one line joined by a literal `\x0a` (#254).
- The TUI no longer exits when the board's decisions or the selected item
cannot be read during a Board-tab refresh; it shows `DEGRADED` and keeps
the last view, as it already did for the item list.
Expand Down
10 changes: 5 additions & 5 deletions src/syncmill/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,8 @@ def _start_project(

if not task:
if not interactive:
raise click.ClickException(
"start needs --task without a terminal.\n" + _PROJECT_STEPS
raise _MultilineError(
"start needs --task without a terminal.", *_PROJECT_STEPS.splitlines()
)
task = click.prompt("What should the agent do?").strip()
if not task:
Expand Down Expand Up @@ -1228,10 +1228,10 @@ def start(
if not trial and not project_flags:
if not interactive:
# Never wait for an answer nobody can give.
raise click.ClickException(
raise _MultilineError(
"start needs a terminal to ask what you want to do. Use "
"`syncmill start --trial` for the offline walkthrough.\n"
+ _PROJECT_STEPS
"`syncmill start --trial` for the offline walkthrough.",
*_PROJECT_STEPS.splitlines(),
)
choice = click.prompt(
"What would you like to do?\n"
Expand Down
20 changes: 20 additions & 0 deletions tests/test_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,17 @@ def test_dry_run_shows_the_screen_and_runs_nothing(monkeypatch, project, calls):
assert calls["run"] == []


def _assert_project_steps_as_lines(output: str, first: str) -> None:
"""The refusal and the steps it points at each sit on their own line: the
error boundary escapes a one-line message's LF to a visible ``\\x0a``."""
assert "\\x0a" not in output
assert output.splitlines() == [f"Error: {first}", *cli_module._PROJECT_STEPS.splitlines()]


def test_without_a_terminal_it_needs_task_and_yes(monkeypatch, project, calls):
no_task = _start(monkeypatch, ["-a", "codex"], ready=_ready("codex"))
assert no_task.exit_code == 1 and "start needs --task" in no_task.output
_assert_project_steps_as_lines(no_task.output, "start needs --task without a terminal.")

no_yes = _start(monkeypatch, ["--task", "t"], ready=_ready("codex"))
assert no_yes.exit_code == 1 and "start needs --yes" in no_yes.output
Expand All @@ -109,6 +117,18 @@ def test_without_a_terminal_it_needs_task_and_yes(monkeypatch, project, calls):
assert calls["diff"] == calls["apply"] == [] # nobody to ask; run printed the commands


def test_bare_start_without_a_terminal_explains_instead_of_asking(monkeypatch, project, calls):
result = _start(monkeypatch, [], ready=_ready("codex"))

assert result.exit_code == 1
_assert_project_steps_as_lines(
result.output,
"start needs a terminal to ask what you want to do. Use "
"`syncmill start --trial` for the offline walkthrough.",
)
assert calls["run"] == []


def test_no_ready_agent_points_at_the_trial(monkeypatch, project, calls):
result = _start(
monkeypatch, ["--task", "t"], ready=_ready(missing=["codex"], logged_out=["claude"])
Expand Down
Loading