Skip to content
Open
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
6 changes: 5 additions & 1 deletion docs/configuration/user-settings/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ You rarely need to hand-edit this file. Most fields are managed from the TUI's `

## Layout Settings

`layout` customizes the TUI's sidebar. The zero value (an omitted `layout:` block, or any field left out) is the default: sidebar on the right, every section visible, normal spacing.
`layout` customizes the TUI's sidebar. The zero value (an omitted `layout:` block, or any field left out) is the default: sidebar on the right, all sections except **Plans** visible, normal spacing.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
Expand All @@ -74,6 +74,7 @@ You rarely need to hand-edit this file. Most fields are managed from the TUI's `
| `hide_agents` | boolean | `false` | Hide the Agents section. |
| `active_agents_only` | boolean | `false` | Show only agents active in the current session in the Agents section (and the top/bottom band), instead of the whole configured team. Ignored while the Agents section is hidden. |
| `hide_tools` | boolean | `false` | Hide the Tools section. |
| `show_plans` | boolean | `false` | Show the [Plans sidebar section](../../features/tui/index.md#plans-sidebar) for shared plans. Full left/right sidebars list the five most recently updated plans; compact layouts show a count and browser shortcut. |
| `hide_todos` | boolean | `false` | Hide the Todos section. |

```yaml
Expand All @@ -82,8 +83,11 @@ settings:
sidebar_position: left
section_spacing: compact
hide_usage: true
show_plans: true
```

Enable **Plans** under `/settings` → **Appearance** → **Sidebar sections**, or set `settings.layout.show_plans: true` as above. This is a global user preference, not an agent configuration field or a per-session plan. It displays the same shared plan store as `/plans` and `docker agent plans`, without classifying free-form statuses as active or completed. The section remains hidden in lean mode and with `--sidebar=false`.

## Complete Example

```yaml
Expand Down
22 changes: 21 additions & 1 deletion docs/features/tui/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@ Slash commands (both built-in and named) execute immediately when entered. Regul

Agent-defined commands (prompts, URL links, agent-switching shortcuts) are configured under `commands:` in the agent YAML — see [Custom Commands](../../configuration/commands/index.md) for the full reference, including how to hide commands with `--disable-commands`.

### Plans Sidebar

The optional **Plans** section is off by default. Enable it under `/settings` → **Appearance** → **Sidebar sections**, or in your global [user settings](../../configuration/user-settings/index.md):

```yaml
# ~/.config/cagent/config.yaml
settings:
layout:
show_plans: true
```

Plans are shared documents from the same store used by `/plans`, the [plan tools](../../tools/plan/index.md), and `docker agent plans` — not plans attached to the current session. A full left/right sidebar shows up to **five** plans, ordered by last update (newest first, unknown timestamps last, with name as the tie-breaker). Status is shown as free-form text; there is no active/completed classification or status filter.

- **Single left-click a plan row** to open its content directly in `$VISUAL`/`$EDITOR`, guarded by the displayed revision. A stale revision is rejected rather than overwriting newer content.
- **All plans** opens the shared plan browser. `/plans` and the <kbd>Ctrl</kbd>+<kbd>K</kbd> command palette remain the keyboard routes.
- Top/bottom layouts and narrow or collapsed sidebar bands show only a compact `Plans (N) - open /plans` count and browser shortcut, not individual plan rows. Lean mode and `--sidebar=false` never show the section.
- Changes from plan events in the current process and local edits refresh the shared metadata. To pick up changes from another process, use the sidebar's **Refresh plans** action or press <kbd>r</kbd> in the plan browser or detail view. There is no automatic polling or file watcher.

Editing changes only the plan document. It does not approve a plan, execute its steps, or authorize tool writes.

### Agents Panel

The sidebar's **Agents** section lists every agent in the team and has two display modes selectable via **Sidebar info mode** in `/settings`:
Expand Down Expand Up @@ -558,7 +578,7 @@ The **Appearance** tab selects the theme and customizes the layout. Layout chang
- **Sidebar position**: `Right` (default), `Left`, `Top`, or `Bottom`. Left/right keep the full vertical sidebar next to the chat; top/bottom render it as a compact horizontal band above or below the chat (session title, working directory, token usage, plus a one-line summary of the current agent and its model; in multi-agent configurations all team agents are listed by name after the current agent).
- **Sidebar info mode**: `Compact` (default) or `Detailed`. Controls how the Agents panel renders agent rows — see [Agents Panel](#agents-panel) for details. Persisted as `settings.layout.sidebar_info_mode: detailed`; compact is the default and omitted from the config.
- **Section spacing**: `Compact`, `Normal` (default), or `Relaxed`, the number of blank lines between the sidebar sections (1, 2, or 3).
- **Sidebar sections**: toggle the visibility of the **Session path** (the working directory line, including its git branch) and the **Token usage**, **Agents**, **Tools**, and **Todos** sections. The session title is always shown.
- **Sidebar sections**: toggle the visibility of the **Session path** (the working directory line, including its git branch) and the **Token usage**, **Agents**, **Tools**, **Plans**, and **Todos** sections. All are visible by default except [Plans](#plans-sidebar), which is opt-in. The session title is always shown.

Appearance also controls split-diff rendering, expanded thinking, whether tool results are hidden by default, and **Show startup banner** — the ASCII-art banner drawn on an empty conversation (persisted as `settings.show_banner: false` when turned off, and honored by the lean TUI too). Select **Theme** to open the theme picker.

Expand Down
21 changes: 18 additions & 3 deletions docs/tools/plan/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,38 @@ $ docker agent plans update release --file ./plan.md --expected-version 1

### The `/plans` browser in the TUI

Inside the full-screen TUI, the `/plans` slash command (also in the <kbd>Ctrl</kbd>+<kbd>K</kbd> command palette) opens a plan browser over the same store the agents use, so changes made by agents mid-session appear immediately. The list shows every shared plan with its scope, name, status, version, last update time, and title.
Inside the full-screen TUI, the `/plans` slash command (also in the <kbd>Ctrl</kbd>+<kbd>K</kbd> command palette) opens a plan browser over the same store the agents use, so changes made by agents in the same process appear immediately. The list shows every shared plan with its scope, name, status, version, last update time, and title.

Keybindings:

| Key | Action |
| --- | ------ |
| <kbd>↑</kbd>/<kbd>↓</kbd>, mouse | Navigate; <kbd>Enter</kbd> or double-click opens a detail view with the full metadata and scrollable markdown content |
| <kbd>/</kbd> | Filter by name, title, status, or scope (<kbd>Esc</kbd> leaves filter mode) |
| <kbd>r</kbd> | Refresh from storage |
| <kbd>r</kbd> | Refresh from storage in the browser or detail view, including changes from other processes |
| <kbd>x</kbd> | Export the selected plan to `<name>.md` in the session's working directory. An existing file is never overwritten — the export fails with a notification instead |
| <kbd>s</kbd> | Set a plan's free-form status via a small input dialog |
| <kbd>e</kbd> | Edit a plan's content in `$VISUAL`/`$EDITOR` |
| <kbd>n</kbd> | Create a new plan: pick a name, then draft the content in `$VISUAL`/`$EDITOR` (an empty draft aborts) |
| <kbd>d</kbd> | Delete a plan after a confirmation that names the plan and its version |
| <kbd>Esc</kbd> | Close the detail view / the browser |

Every mutation is guarded by the version shown on screen (the same optimistic locking as `last_known_revision`): if an agent changed the plan in the meantime, the write is rejected, a notification reports the current version, the newer content is left intact and re-read into the browser, and an edit draft is kept in a temp file so nothing is lost. The browser also refreshes live when agents in the same process write, re-status, or delete plans; in the lean TUI, which has no overlays, `/plans` is unavailable.
Every mutation is guarded by the version shown on screen (the same optimistic locking as `last_known_revision`): if an agent changed the plan in the meantime, the write is rejected, a notification reports the current version, the newer content is left intact and re-read into the browser, and an edit draft is kept in a temp file so nothing is lost. The browser also refreshes live when agents in the same process write, re-status, or delete plans. Changes from other processes require an explicit refresh; there is no automatic polling or file watcher. In the lean TUI, which has no overlays, `/plans` is unavailable.

### Optional Plans sidebar

Enable **Plans** under `/settings` → **Appearance** → **Sidebar sections** to keep shared plans visible alongside the chat. It is off by default and saved as a global user preference, separate from agent YAML:

```yaml
# ~/.config/cagent/config.yaml
settings:
layout:
show_plans: true
```

The full left/right sidebar lists up to five shared plans by last update, newest first (unknown timestamps last, ties by name), plus **All plans** to open the browser. The free-form status is displayed without an active/completed classification. Single left-click a plan row to open it directly in `$VISUAL`/`$EDITOR` at the displayed revision; stale revisions are rejected and newer content is preserved. Editing changes only the document — it never approves the plan, executes its steps, or authorizes tool writes.

Top/bottom layouts and narrow or collapsed sidebar bands show a compact `Plans (N) - open /plans` count and browser shortcut instead of individual rows. Plan events in the current process and local edits refresh the shared metadata; use the sidebar's **Refresh plans** action or <kbd>r</kbd> in the browser/detail view for cross-process changes. The section is hidden in lean mode and with `--sidebar=false`; `/plans` and the command palette remain available in the full TUI even when the section is off. See [Plans Sidebar](../../features/tui/index.md#plans-sidebar) for details.

> [!TIP]
> **Plan vs. Todo vs. Tasks**
Expand Down
9 changes: 8 additions & 1 deletion pkg/tui/components/sidebar/collapsed_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ type CollapsedViewModel struct {
UsageSummary string
// InfoLine is the compact agents/tools/todos summary shown when the
// sidebar renders as a horizontal band.
InfoLine string
InfoLine string
PlansSummary string

// Layout decisions computed from the data
TitleAndIndicatorOnOneLine bool
Expand Down Expand Up @@ -48,6 +49,9 @@ func (vm CollapsedViewModel) LineCount() int {
if vm.InfoLine != "" {
lines += linesNeeded(lipgloss.Width(vm.InfoLine), vm.ContentWidth)
}
if vm.PlansSummary != "" {
lines += linesNeeded(lipgloss.Width(vm.PlansSummary), vm.ContentWidth)
}

return lines
}
Expand Down Expand Up @@ -109,6 +113,9 @@ func RenderCollapsedView(vm CollapsedViewModel) string {
if vm.InfoLine != "" {
lines = append(lines, vm.InfoLine)
}
if vm.PlansSummary != "" {
lines = append(lines, vm.PlansSummary)
}

return strings.Join(lines, "\n")
}
Expand Down
102 changes: 102 additions & 0 deletions pkg/tui/components/sidebar/plans.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package sidebar

import (
"cmp"
"fmt"
"slices"
"strings"

tea "charm.land/bubbletea/v2"

"github.com/docker/docker-agent/pkg/plans"
"github.com/docker/docker-agent/pkg/tui/components/notification"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core"
"github.com/docker/docker-agent/pkg/tui/messages"
"github.com/docker/docker-agent/pkg/tui/styles"
)

const recentPlanLimit = 5

type planClickZone struct {
kind ClickResult
name string
}

func (m *model) setPlans(data messages.PlanSidebarDataMsg) {
m.planData = data
m.recentPlans = slices.Clone(data.Result.Plans)
slices.SortFunc(m.recentPlans, func(a, b plans.Plan) int {
if order := b.UpdatedAt.Compare(a.UpdatedAt); order != 0 {
return order
}
return cmp.Compare(a.Name, b.Name)
})
m.recentPlans = m.recentPlans[:min(len(m.recentPlans), recentPlanLimit)]
m.invalidateCache()
}

func (m *model) EditPlan(name, tabID string) tea.Cmd {
for _, p := range m.recentPlans {
if p.Name == name && p.Version != nil {
return core.CmdHandler(messages.EditSidebarPlanMsg{
TabID: tabID, Ref: plans.SharedRef(p.Name), ExpectedVersion: *p.Version,
})
}
}
return notification.WarningCmd("Plan is no longer available for editing. Refresh plans and retry.")
}

func (m *model) plansSection(width int) (string, []planClickZone) {
var lines []string
var zones []planClickZone
add := func(text string, zone planClickZone) {
text = strings.Join(strings.Fields(text), " ")
lines = append(lines, toolcommon.TruncateText(text, max(1, width)))
zones = append(zones, zone)
}
switch {
case m.planData.Err != nil:
label := "Plans unavailable; refresh to retry"
if len(m.recentPlans) > 0 {
label = "Plans unavailable; showing stale data"
}
add(styles.MutedStyle.Render(label), planClickZone{})
case m.planData.Loading:
add(styles.MutedStyle.Render("Loading plans"), planClickZone{})
case len(m.recentPlans) == 0:
add(styles.MutedStyle.Render("No plans"), planClickZone{})
}
if len(m.planData.Result.Warnings) > 0 {
add(styles.MutedStyle.Render(fmt.Sprintf("%d plan(s) could not be read", len(m.planData.Result.Warnings))), planClickZone{})
}
for _, p := range m.recentPlans {
zone := planClickZone{kind: ClickPlan, name: p.Name}
add(styles.BaseStyle.Render(p.Name), zone)
if p.Title != "" {
add(styles.MutedStyle.Render(p.Title), zone)
}
if p.Status != "" {
add(styles.MutedStyle.Render(p.Status), zone)
}
}
add(styles.MutedStyle.Render(fmt.Sprintf("All plans (%d)", len(m.planData.Result.Plans))), planClickZone{kind: ClickPlanBrowser})
add(styles.MutedStyle.Render("Refresh plans"), planClickZone{kind: ClickPlanRefresh})
return m.renderTab("Plans", strings.Join(lines, "\n"), width), zones
}

func (m *model) plansSummary() string {
if !m.sectionVisibility.ShowPlans {
return ""
}
label := fmt.Sprintf("Plans (%d) - open /plans", len(m.planData.Result.Plans))
switch {
case m.planData.Err != nil:
label = "Plans unavailable - open /plans"
case m.planData.Loading:
label = "Loading plans - open /plans"
case len(m.planData.Result.Warnings) > 0:
label += " (warnings)"
}
return styles.MutedStyle.Render(label)
}
Loading
Loading