I have a switch entry in my launcher list, and from the GUI it did nothing at all. The reason is obvious in hindsight: the entry spawns project-switch switch, which is an inquire prompt that wants a terminal, and the launcher window has no console attached. So the process started, found nowhere to draw, and died.

Rather than special-case the spawn, I pulled the selection logic out into a PickerState model that knows nothing about how it’s rendered — a two-stage list of clients, then (for clients with nested projects) the client itself or one of its projects, with filtering, a starting cursor on the current selection, and a back() step. The terminal switch now drives that model in a loop, which collapsed two hand-rolled prompts and their duplicated cursor arithmetic into one. The GUI drives the same model directly in the launcher window, so both offer identical choices without either knowing about the other.

Detecting which list entries are switch commands is its own small problem, since the configured command could be project-switch.exe switch, an absolute path, a quoted path with spaces, or have the subcommand sitting in the args field. is_switch_command tokenises the whole thing, respects double quotes, and only matches when it’s exactly this program plus the bare switch subcommand — anything with extra tokens (switch && echo done) or a different program (git switch) gets spawned normally, as it should.

Two supporting changes came along with it. Actions dispatched off the UI thread now report failures back through a channel that the frame loop polls, and the launcher grew an error banner to show them — previously a failed action just printed to a stderr nobody was reading. And Escape in the picker steps back a stage before it closes the window, which is what you reach for when you’ve descended into the wrong client.