Skip to content

feat: add interactive TUI for task execution - #3015

Draft
janluke wants to merge 28 commits into
go-task:mainfrom
janluke:tui
Draft

feat: add interactive TUI for task execution#3015
janluke wants to merge 28 commits into
go-task:mainfrom
janluke:tui

Conversation

@janluke

@janluke janluke commented Sep 3, 2026

Copy link
Copy Markdown

Summary

This PR adds an interactive terminal interface through task --tui.

The TUI is both a task launcher and an execution dashboard. It makes it possible to inspect the status and output of each task and subtask independently, instead of reading combined output from multiple processes in one terminal stream.

# Open the task launcher
task --tui

# Skip the launcher and execute tasks in the dashboard
task --tui lint test

# Execute multiple requested roots concurrently
task --tui --parallel lint test

Without task arguments, task --tui opens a searchable launcher containing all non-internal tasks. When task arguments are supplied, execution starts directly in the dashboard.

Demo

Updated to: 2026/09/03

task-tui-demo-1.5x.mp4

Motivation

The existing output modes work well for conventional terminal output, but it can be difficult to follow several tasks running concurrently. Their output is either interleaved, prefixed in a single stream, or grouped and displayed only after execution.

The TUI provides a separate view for each task invocation. The task panel shows the execution state of roots and subtasks, while the output panel displays and scrolls the output of the selected task.

This is particularly useful for orchestration tasks with multiple dependencies, parallel execution, or long-running commands that produce substantial output.

User experience

The launcher presents tasks as a compact two-column list containing the task name and description.

  • Typing filters tasks by name or description.
  • The arrow keys and mouse select a task.
  • Enter runs the selected task in the TUI dashboard.
  • Ctrl+R runs the selected task using Task's normal terminal output.
  • Escape clears the current filter.
  • Ctrl+C exits the launcher.

During execution:

  • The left pane shows roots, subtasks, and their current status.
  • The right pane shows the output of the selected task.
  • The keyboard or mouse can select tasks and scroll their output.
  • f opens a fullscreen view suitable for terminal copy operations.
  • Escape or b returns to the launcher.
  • q or Ctrl+C exits the TUI.

Returning to the launcher or quitting while execution is active requests cancellation and waits for Task's processes to exit before changing views or closing the application.

Root tasks remain selectable because they may contain their own shell commands in addition to invoking dependencies.

Additional TUI options

There are currently two options for experimentation purposes, but they are not necessary by any means.

--tui-task-navigator

The task navigator is the left pane where running tasks are displayed. It can use a tree view (default) or a flat list view:

task --tui --tui-task-navigator list ...
task --tui --tui-task-navigator tree ...  # default

In tree mode, a task shared by multiple parents remains visible at each invocation location. Joined calls use a marker and resolve to the status and output of the invocation that owns the execution.

--tui-status

Task status can be represented by icons or text labels:

task --tui --tui-status icons ...   # default
task --tui --tui-status labels ...

Execution behavior

Multiple task arguments are represented as independent roots in the dashboard.
They retain Task's existing execution semantics:

  • Requested roots execute sequentially by default.
  • --parallel executes the requested roots concurrently.
  • A failure during sequential execution leaves subsequent roots skipped.
  • Fail-fast cancellation interrupts work that is already running.
  • Joined run: once calls share the status and output of their owning
    invocation.
  • Repeated run: always calls remain separate invocations.

The dashboard distinguishes the following states:

  • pending
  • running
  • successful
  • failed
  • canceled
  • skipped

Canceled tasks were actively interrupted. Skipped tasks were never attempted, for example because an earlier sequential root failed.

Implementation

The terminal interface lives in a dedicated internal/tui package and is built using Bubble Tea, Bubbles, and Lip Gloss.

The executor emits optional lifecycle events for:

  • task scheduling
  • task start
  • task completion
  • calls joining an existing execution

Each runtime call receives an invocation ID, root ID, and parent ID. These values allow the TUI to associate output and status with individual task calls and to construct either the list or tree navigator.

Task-aware output writers associate stdout and stderr with the relevant invocation. Existing output implementations continue to use the original output interface and do not need to implement lifecycle or task-aware output support.

The launcher and dashboard run in the same terminal application. A task can finish and return to the launcher without restarting Task, while cancellation is coordinated with the executor so the TUI does not exit before child processes have returned.

Current issues/limitations

Limitations and questions for discussion:

  • Watch mode is not supported. Should it be?
  • Interactive tasks and interactive variable prompting are not supported.
  • Confirmation prompts must be accepted in advance with --yes.
  • The interactive launcher starts one selected task at a time. Multiple independent roots must currently be supplied as command-line arguments. Should the launcher support selecting multiple tasks for concurrent execution?

ToDo list

  • Fix text selection in fullscreen mode. Currently, scrolling while selecting text clears the current selection.
  • Fix: when task --tui <non-existent-task>, display an error message without entering the TUI.
  • Support interactive tasks.
  • Add configuration fields related to the TUI preferences (if these are needed). TUI preferences are CLI-only at the moment.
  • Improve documentation once the PR is stable. Add images or videos.

Testing

The implementation includes tests covering:

  • launcher filtering and navigation
  • normal and TUI launch actions
  • returning from execution to the launcher
  • multiple requested roots
  • task lifecycle ordering
  • repeated and shared task invocations
  • run: once joins
  • list and tree navigation
  • success, failure, cancellation, and skipped states
  • fail-fast behavior
  • output association and scrolling
  • cancellation while processes are running

The following checks pass:

go test ./...
go test -race ./internal/tui
golangci-lint run

The launcher, dashboard, multiple-root execution, text selection, and process
cancellation were also exercised manually in a pseudo-terminal.

AI usage

The code in this PR was generated by ChatGPT 5.6 Sol. I've not yet reviewed the code in depth, but I'm aware of the general architecture, I manually tested the feature and went through multiple iterations of fixes and improvements.

At the moment, this PR serves primarily as a way to prototype and iterate the TUI.

Related issue

Closes #2077.

Checklist

  • I have read and followed the Contribution Guide.
  • I have disclosed the use of any AI-generated content in this pull request per the AI Usage Policy.
  • I fully understand the changes and have hand-written the description (No AI) of this pull request.

The TUI classifies a task as canceled when its completion error wraps
context.Canceled. A killed process does not report that the same way
everywhere: the shell interpreter surfaces the context error on Unix,
while on Windows the same kill arrives as a plain non-zero exit status.
On Windows every in-flight task was therefore marked failed with a
spurious "exit status 1" whenever a run was interrupted or fail-fast
cancellation kicked in.

Consult the context when reporting a completion so the distinction is
portable, wrapping the task error rather than replacing it so its
message survives for display.

Fixes TestTaskLifecycleReportsFailfastCancellation on Windows.
The TUI was originally conceived as a fourth output mode, so it was
built as an output.Output and installed by swapping Executor.Output at
runtime. Everything else about it was then discovered by type-asserting
that field: two lifecycle interfaces, a task-aware writer interface and
a TerminalUI marker.

That conflated two unrelated things. Output describes where bytes go;
it should not also carry execution lifecycle events or a capability
saying who owns the terminal. The launcher settles it: --output has to
stay meaningful alongside the TUI, because tasks started with ctrl+r
run with Task's normal terminal output. The two are orthogonal, so the
TUI cannot be an output mode.

Replace the four optional interfaces with one task.Listener and an
optional Executor.Listener field. A nil Listener behaves exactly as
before. internal/output goes back to describing output only.

Also drop --tui from the executor's error messages, which no longer
knows the listener is a TUI, and name invocations with t.Name() rather
than the often-empty t.Prefix.
The TUI reuses a single Executor for every launcher selection, but the
map of started executions that "run: once" and "run: when_changed"
calls join is only ever populated, never cleared. Running a task,
returning to the launcher and running it again found the first run's
finished execution and returned its result instantly: no execution, no
output, a green tick.

The per-task counter behind MaximumTaskCall accumulates the same way and
would eventually refuse to run a task at all in a long session.

Add Executor.ResetRunState to clear both, and call it for each launcher
selection. Default "run: always" tasks hash to the empty string and
never reached the map, which is why this went unnoticed.
Task existence is checked inside Executor.Run, which the TUI only
reaches after the Bubble Tea program has started. "task --tui nope"
therefore took over the terminal and reported the error inside a
dashboard the user then had to quit.

Resolve the requested calls before the alt screen opens so the error
lands on the terminal like any other.
The dashboard gives each invocation its own output pane, so --output
does not apply to tasks run inside it, but it does apply to tasks
started with ctrl+r. Say so in the CLI reference.

Also note in RunTask why a call that fails to resolve or compile emits
no lifecycle events and so stays out of a listener's task list.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a interactive TUI powered by Bubbletea

1 participant