Skip to content

fleet TUI: switch the working directory from a footer command #477

Description

Problem

The Fleet Manager renders a Directory column for every run (screens/runs.py::_directory_cell, fed by RunSummary.cwd ← the system.cwd the engine stamps into workflow_started), so the fleet is already understood to span directories. The TUI itself has no such flexibility: it is nailed to whatever directory conductor fleet was started in, cannot show you which one that is, and offers no way to change it. Working on a second project means quitting, cd, and relaunching.

That cwd is not cosmetic — it silently decides two things:

  1. Relative workflow references on the New Run screen resolve against it. resolve_ref returns the reference verbatim as ResolvedRef(kind="file", path=Path(ref)) (registry/resolver.py:80) and resolve_workflow then calls workflow_path.exists() (fleet/launch.py). So ./examples/simple-qa.yaml resolves only if the TUI happened to be started in the repo root; from anywhere else it is a flat "Workflow file not found".
  2. Launched runs inherit it. cli/bg_runner.py never passes cwd= to _spawn_detached_posix / _spawn_detached_windows, so the detached child gets the launcher's directory. That directory becomes the run's recorded system.cwd (engine/workflow.py:805) — the Directory column — and is the cwd a type: script step with no explicit working_dir executes in (executor/script.py:172). A TUI-launched run is therefore stamped with the TUI's directory whether or not it has anything to do with the workflow.

Proposal

A footer command — d — that switches the Fleet Manager's launch directory, on the Runs and New Run screens.

  • Opens a modal picker (the TUI already has the ModalScreen pattern: actions.py::ConfirmKillModal, GateOptionsModal). Either a path Input with ~ expansion, or Textual's DirectoryTree — the latter is more discoverable, the former faster for someone who knows the path; a tree with an editable path field is the obvious both.
  • The chosen directory becomes the base for relative workflow references on New Run, and the cwd the next launched run inherits.
  • Not persisted. It resets to the process's launch cwd on the next conductor fleet. No config.toml key, no state file.
  • Not a view filter. The Runs and History tables keep showing the whole fleet; the Directory column is already there for the eye. Scoping the views to a directory is a separate question and deliberately out of scope here.

Design notes

The footer has no room left — and is already one column over

Measured against the real app at 100×30 with a gated run selected:

Detail 0–10  Dash 10–18  Kill 18–26  Gate 26–34  New 35–43
Providers 43–56  Registries 56–70  History 70–81  Kill all 81–93  Quit 93–101
                                                (command palette occupies 88–100)

q Quit already ends at 101 in a 100-wide footer — clipped, by the exact criterion test_footer_fits_without_truncation applies (tests/test_fleet/test_tui_runs.py:1768). That test passes only because its _write_record helper writes a plain record, so check_action hides g Gate and the tail lands at 93 instead.

Consequences for this feature:

  • The description must be terse — d Dir, not d Directory. The comment on RunsScreen.BINDINGS already explains that an overflowing footer truncates mid-word rather than wrapping, which is how h History vanished once before.
  • The footer test's fixture should be extended to cover the gated case regardless of this feature — it is guarding a budget it cannot currently see the worst case of.
  • If d Dir still does not fit, columns have to be reclaimed before the binding lands (shortening Registries/Providers, or moving a fleet-scoped action into the command palette).

os.chdir vs. threading the directory explicitly

os.chdir gets both effects for free (relative resolution and the inherited child cwd) with no signature changes, but it mutates process-global state that the TUI's asyncio.to_thread workers (issue #437) share. In practice the blast radius looks small — everything those workers read is already absolute (rundir.runs_dir(), $TMPDIR-rooted event logs) — but it is invisible coupling.

The explicit alternative is resolve_workflow(ref, *, base_dir=...) plus a new cwd= parameter on launch_background forwarded to subprocess.Popen. It costs a change to bg_runner.py (a heavily load-bearing module) but keeps the state in signatures where it can be seen and tested. Preference is the explicit form; worth settling before implementation.

Trap: the workflow path must be absolutized before launch

Today a relative reference works by coincidencelaunch_background puts str(workflow_path) straight into argv and the child inherits the parent's cwd, so parent and child agree on what it means. The moment those two directories can differ, a relative ResolvedWorkflow.path resolves against the wrong base (or nothing). resolve_workflow should return an absolute path as part of this change, independent of which mechanism above is chosen.

Switching must be visible

The Runs screen displays every run's directory and never its own. A d command that changes state nothing on screen reflects is a trap — the current launch directory needs a home in the UI (Runs header/subtitle, and the New Run screen near the reference field, where it explains what a relative path is relative to).

Explicitly unaffected

runtime.working_dir / agent.working_dir resolve against the workflow file's directory, not the process cwd (engine/workflow.py:638), as do sub-workflow references when workflow_path is set (engine/workflow.py:1802, :1929). Switching the fleet's launch directory must not change any of them.

Acceptance criteria

  • d appears in the footer on the Runs and New Run screens and opens a directory picker.
  • A non-existent or non-directory path is rejected in-place with a message, never a traceback (matching NewRunScreen's existing error handling).
  • After switching, a relative reference on New Run resolves against the chosen directory.
  • After switching, a run launched from the TUI records the chosen directory as its system.cwd and shows it in the Runs table's Directory column.
  • The current launch directory is visible on both screens.
  • The directory resets to the process launch cwd on the next conductor fleet (no persistence).
  • test_footer_fits_without_truncation covers the gated (g Gate visible) worst case and passes with the new binding present.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:bgBackground process (--web-bg) lifecyclearea:cliCLI commands and entry pointsenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions