Skip to content

feat: add docker agent board, a Kanban TUI for orchestrating agents#3451

Draft
dgageot wants to merge 53 commits into
mainfrom
feat/board-kanban-tui
Draft

feat: add docker agent board, a Kanban TUI for orchestrating agents#3451
dgageot wants to merge 53 commits into
mainfrom
feat/board-kanban-tui

Conversation

@dgageot

@dgageot dgageot commented Jul 3, 2026

Copy link
Copy Markdown
Member

Working on several agent tasks in parallel means juggling terminals, worktrees, and half-remembered session IDs. This adds docker agent board, a full-screen Kanban TUI that turns that workflow into cards on a pipeline: each card launches an agent in a tmux session on an isolated git worktree, and moving a card forward (Dev → Review → Push → Done) delivers the destination column's prompt to its agent. It is a TUI port of the experimental web-based board project, built natively on docker-agent's own primitives (--worktree, --session, --listen).

The engine (pkg/board) keeps one watcher goroutine per card tailing the agent's control-plane event stream over a per-card unix socket, mirroring title and running/waiting/paused/failed status into a JSON store, and relaunching the tmux session (resuming the same conversation and worktree) if the agent dies. Prompt delivery, busy-move rejection, and delete/relaunch races are all serialized through the store and a relaunch lock. Projects and column prompts live in the user's global config file (~/.config/cagent/config.yaml, new board: section) and are editable from the TUI, which follows the main TUI's standards: shared dialog chrome, theme, keymap (remapped quit/suspend bindings are honored), scrollbars, and mouse support. Pressing enter attaches the terminal to a card's agent; ctrl+q detaches back to the board.

The board is single-instance (flock on the state file), sanitizes agent-controlled strings before rendering, runs its tmux sessions on a private validated socket, and requires tmux (it fails with a clear message otherwise; Windows builds compile but the command is effectively unix-only). Docs live in docs/features/board/.

@aheritier aheritier added kind/feat PR adds a new feature (maps to feat:). Use on PRs only. area/cli CLI commands, flags, output formatting area/docs Documentation changes area/tui For features/issues/fixes related to the TUI labels Jul 3, 2026
@dgageot dgageot force-pushed the feat/board-kanban-tui branch from 3119838 to fdb2835 Compare July 3, 2026 15:25
@rumpl

rumpl commented Jul 3, 2026

Copy link
Copy Markdown
Member

Didn't look at the code, one note: i'm using the tasks toolset a lot, can you make sure that the board can be reused? I would want to have a board for the tasks at some point

dgageot added 24 commits July 3, 2026 18:52
Assisted-By: claude-opus-4-5
Any other stat failure (permissions, corrupt path) was silently hidden
as "no changes"; surface it instead.
An empty path silently validated against the board's working directory
and a relative one depended on it. Reject blank paths, expand a leading
~, and store absolute paths.
A card whose column was dropped from the configured pipeline silently
disappeared from the TUI while its agent kept running: impossible to
attach, move, or delete. Bucket such cards into the first column.
A watcher's background resume could race a prompt-bearing relaunch and
kill the session it had just created, dropping the prompt. Relaunches
now run under a lock, and a plain resume skips sessions a concurrent
relaunch already resurrected.
A predictable socket path directly under /tmp let other local users
pre-create or interfere with it. Follow tmux's own /tmp/tmux-<uid>
convention.
Removes the concrete-type assertions at dialog open sites: every dialog
now implements Init and a single openDialog helper installs it.
First-run users saw six empty columns with no guidance; the overlay
explains the model and points at the n/p/? keys.
Makes it easy to find the branch to check out or push without opening
the agent session.
The dialog was rebuilt from scratch on every change, resetting the
cursor to the first entry.
Session titles arrive from agent-controlled control planes and diffs
are repository content: both could embed ANSI/OSC sequences (clipboard
writes, title changes, screen manipulation). Strip terminal controls
from titles, project names, flash messages, and diff content before
rendering.
SendPrompt runs outside the watcher, so a delete racing an in-flight
prompt delivery could resurrect the tmux session (and worktree) of a
card already gone from the store. relaunch now aborts when the card no
longer exists, and DeleteCard kills the session under the same lock.
The per-user socket dir under /tmp was created best-effort: a path
pre-created by another local user would have been used silently. The
directory is now validated once per process (real directory, owned by
the current user, 0700) and tmux is never started when the checks
fail.
Drop the Simplify and Fix columns. Also stop freezing the built-in
columns into the config file on every save: the columns section is only
persisted when it differs from the defaults, so future default changes
reach users who never customized their pipeline.
All of a project's cards share an accent color (border and project
badge), matching the project chip in the new-card dialog, so each
project's work is recognizable at a glance. Cards whose project was
removed hash to a stable color.
Enter still creates the card; the newline binding moves from
shift+enter to cmd+enter (super+enter in CSI-u terms), with ctrl+j as
a fallback for terminals without the Kitty keyboard protocol.
The prompt textarea now spans most of the screen (up to 24 rows, width
100): long task descriptions are the norm, not the exception. When only
one project is configured the selector row disappears entirely; the
dialog title still names the target project.
Adding a project now starts in a small directory picker (type to
filter, enter descends, backspace walks up, git repositories are
marked) instead of a bare text field. The picked directory pre-fills
the form's name and path; ctrl+o re-opens the browser from the form.
Board dialogs now build their titles and help-key lines through the
shared dialog content builder (pkg/tui/dialog), so they render exactly
like every other docker-agent dialog: centered title, highlighted keys
with muted descriptions, centered help row.
lipgloss Layer.Draw ignores a layer's X/Y/Z — positioning lives in the
Compositor — so each Compose painted the layer's content at the origin
over the whole canvas: dialogs erased the board behind them. Route the
layers through a Compositor and pin the regression with a test.
Enter now inserts a newline (multiline prompts are the norm) and
cmd+enter submits the card, with ctrl+s as a fallback for terminals
without the Kitty keyboard protocol.
View called App.Projects() — a config lock plus slice rebuild — on
every render frame (120ms while any card is busy). The model now keeps
a snapshot, refreshed on reload and after project add/delete, which
also keeps project colors and the header count fresh without waiting
for the next card change.
dgageot added 29 commits July 3, 2026 18:52
Same submit binding as the new-card dialog (ctrl+s still works).
Wheel events move the selection through the column under the cursor;
the scroll window already follows the selection. The diff viewport
keeps its native wheel scrolling.
Columns with starting or running agents get an animated spinner and
count next to the card count, so activity is visible even when the
busy cards are scrolled out of view.
Runs $BOARD_EDITOR (default: code) on the selected card's worktree,
matching the web board's editor button.
Project names, paths, and agent refs come from the config file (or are
typed into dialogs); strip terminal controls before rendering them in
chips, titles, project rows, and the footer, like every other
untrusted string.
Scrolling over the header or footer changed the selection. Column
hit-testing now bounds Y to the board area and is shared between
cardAt and handleWheel so the math cannot drift apart.
upstreamBase blindly assumed <remote>/main when nothing resolved, so
git merge-base failed and the diff view errored in local-only repos.
Fall back to the local default branch, then HEAD (+ tests).
The projects dialog now shows each project in the same color its cards
carry on the board.
The diff is a snapshot and the agent may still be working; r reloads
it in place, preserving the scroll position.
Listing (hidden dirs and files excluded, git detection), descend and
walk-up navigation, filtering, picking, and cancellation.
The quit binding now merges the user's remapped global quit (from the
config file, resolved through the main TUI's keymap) instead of
hard-coding ctrl+c — inside dialogs too. Help additionally answers to
the main TUI's f1/ctrl+h.
Matches the main TUI's viewer dialogs: a scrollbar column next to the
viewport (reserved even when the content fits, so refreshes don't
shift the layout) and the scroll percentage in the help row.
The delete confirmation now runs on dialog.DefaultConfirmKeyMap (Y/N,
case-insensitive) like every other docker-agent confirmation dialog;
enter still confirms and esc still cancels.
ctrl+z (or the user's remapped suspend binding) suspends the board to
the shell; fg resumes it.
Keys in bold secondary, descriptions in the shared dialog help style,
instead of ad-hoc badge colors.
Pressing enter repeatedly stacked tea.ExecProcess commands, replaying
an attach after every detach. A guard now holds from the readiness
probe until the session detaches or the probe fails.
Colorizing and holding an unbounded diff in the viewport could freeze
the UI; larger diffs are cut at a line boundary with a notice.
A restored scroll offset (diff refresh) was applied while the viewport
height was still zero, so it clamped against the wrong bound; a
shrunken diff or a taller terminal could then render blank content
past the real bottom. Re-clamp once the real dimensions are known.
The per-card unix socket file lingered in ~/.cagent/run forever.
Two boards over the same state file would each run per-card watchers
and race one another relaunching agent sessions. NewApp now takes an
exclusive flock on the state file; a second instance fails fast with a
clear message. The OS drops the lock on exit, so a crash never leaves
a stale lock.
The textarea was a fixed 10 rows; it now adapts between 4 and 16 rows
like the new-card dialog.
User-defined columns may omit the emoji; the header and prompt-editor
title no longer show a leading double space (and column names/emojis
are sanitized like other config strings).
Typing 'exec docker-agent …' into the user's interactive shell was
fragile: it depends on the shell supporting exec, pollutes shell
history, and a slow shell startup could swallow or garble the input.
respawn-pane -k replaces the pane process directly (tmux runs the
command via /bin/sh), keeping the same remain-on-exit dead-pane
semantics. Verified live: create card, agent starts, relaunch works.
Columns that did not fit were clipped off screen and unreachable by
mouse. The board now windows the pipeline around the selected column,
with ◀/▶ hidden-column counts in the header; hit-testing follows the
window (+ tests). Verified live on a 55-column terminal.
@dgageot dgageot force-pushed the feat/board-kanban-tui branch from fdb2835 to 6938695 Compare July 3, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli CLI commands, flags, output formatting area/docs Documentation changes area/tui For features/issues/fixes related to the TUI kind/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants