diff --git a/.gitignore b/.gitignore
index 12ba99c77..abfcf698c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ vendor
/cagent
/cagent-*
/docker-mcp-*
+docker-agent
diff --git a/docs/features/tui/index.md b/docs/features/tui/index.md
index 74736983a..f49c783b2 100644
--- a/docs/features/tui/index.md
+++ b/docs/features/tui/index.md
@@ -132,6 +132,8 @@ Customize session titles to make them more meaningful and easier to find. By def
| Enter | Send message (or newline with Shift+Enter) |
| Up/Down | Navigate message history |
+Press Ctrl+H to view the complete list of all available keyboard shortcuts.
+
## History Search
Press Ctrl+R to enter incremental history search mode. Start typing to filter through your previous inputs. Press Enter to select a match, or Escape to cancel.
diff --git a/pkg/tui/dialog/help.go b/pkg/tui/dialog/help.go
new file mode 100644
index 000000000..f0c0bbc2c
--- /dev/null
+++ b/pkg/tui/dialog/help.go
@@ -0,0 +1,164 @@
+package dialog
+
+import (
+ "fmt"
+ "strings"
+
+ "charm.land/bubbles/v2/key"
+ tea "charm.land/bubbletea/v2"
+ "charm.land/lipgloss/v2"
+
+ "github.com/docker/docker-agent/pkg/tui/core/layout"
+ "github.com/docker/docker-agent/pkg/tui/styles"
+)
+
+// helpDialog displays all currently active key bindings in a scrollable dialog.
+type helpDialog struct {
+ readOnlyScrollDialog
+
+ bindings []key.Binding
+}
+
+// NewHelpDialog creates a new help dialog that displays all active key bindings.
+func NewHelpDialog(bindings []key.Binding) Dialog {
+ d := &helpDialog{
+ bindings: bindings,
+ }
+ d.readOnlyScrollDialog = newReadOnlyScrollDialog(
+ readOnlyScrollDialogSize{
+ widthPercent: 70,
+ minWidth: 60,
+ maxWidth: 100,
+ heightPercent: 80,
+ heightMax: 40,
+ },
+ d.renderContent,
+ )
+ d.helpKeys = []string{"↑↓", "scroll", "Esc", "close"}
+ return d
+}
+
+// renderContent renders the help dialog content.
+func (d *helpDialog) renderContent(contentWidth, maxHeight int) []string {
+ titleStyle := styles.DialogTitleStyle
+ separatorStyle := styles.DialogSeparatorStyle
+ keyStyle := styles.DialogHelpStyle.Foreground(styles.TextSecondary).Bold(true)
+ descStyle := styles.DialogHelpStyle
+
+ lines := []string{
+ titleStyle.Render("Active Key Bindings"),
+ separatorStyle.Render(strings.Repeat("─", contentWidth)),
+ "",
+ }
+
+ // Group bindings by category for better organization
+ // We'll do a simple categorization based on key prefixes
+ globalBindings := []key.Binding{}
+ ctrlBindings := []key.Binding{}
+ otherBindings := []key.Binding{}
+
+ for _, binding := range d.bindings {
+ if len(binding.Keys()) == 0 {
+ continue
+ }
+ keyStr := binding.Keys()[0]
+ switch {
+ case strings.HasPrefix(keyStr, "ctrl+"):
+ ctrlBindings = append(ctrlBindings, binding)
+ case keyStr == "esc" || keyStr == "enter" || keyStr == "tab":
+ globalBindings = append(globalBindings, binding)
+ default:
+ otherBindings = append(otherBindings, binding)
+ }
+ }
+
+ // Render global bindings
+ if len(globalBindings) > 0 {
+ lines = append(lines,
+ styles.DialogHelpStyle.Bold(true).Render("General"),
+ "",
+ )
+ for _, binding := range globalBindings {
+ lines = append(lines, d.formatBinding(binding, keyStyle, descStyle))
+ }
+ lines = append(lines, "")
+ }
+
+ // Render ctrl bindings
+ if len(ctrlBindings) > 0 {
+ lines = append(lines,
+ styles.DialogHelpStyle.Bold(true).Render("Control Key Shortcuts"),
+ "",
+ )
+ for _, binding := range ctrlBindings {
+ lines = append(lines, d.formatBinding(binding, keyStyle, descStyle))
+ }
+ lines = append(lines, "")
+ }
+
+ // Render other bindings
+ if len(otherBindings) > 0 {
+ lines = append(lines,
+ styles.DialogHelpStyle.Bold(true).Render("Other"),
+ "",
+ )
+ for _, binding := range otherBindings {
+ lines = append(lines, d.formatBinding(binding, keyStyle, descStyle))
+ }
+ }
+
+ return lines
+}
+
+// formatBinding formats a single key binding as " key description"
+func (d *helpDialog) formatBinding(binding key.Binding, keyStyle, descStyle lipgloss.Style) string {
+ helpInfo := binding.Help()
+ helpKey := helpInfo.Key
+ helpDesc := helpInfo.Desc
+
+ // Calculate spacing to align descriptions
+ const keyWidth = 20
+ const indent = 2
+
+ keyPart := keyStyle.Render(helpKey)
+ descPart := descStyle.Render(helpDesc)
+
+ // Pad the key part to align descriptions
+ keyPartWidth := lipgloss.Width(keyPart)
+ padding := strings.Repeat(" ", max(1, keyWidth-keyPartWidth))
+
+ return fmt.Sprintf("%s%s%s%s",
+ strings.Repeat(" ", indent),
+ keyPart,
+ padding,
+ descPart,
+ )
+}
+
+func (d *helpDialog) Init() tea.Cmd {
+ return d.readOnlyScrollDialog.Init()
+}
+
+func (d *helpDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
+ model, cmd := d.readOnlyScrollDialog.Update(msg)
+ if rod, ok := model.(*readOnlyScrollDialog); ok {
+ d.readOnlyScrollDialog = *rod
+ }
+ return d, cmd
+}
+
+func (d *helpDialog) View() string {
+ return d.readOnlyScrollDialog.View()
+}
+
+func (d *helpDialog) Position() (int, int) {
+ return d.readOnlyScrollDialog.Position()
+}
+
+func (d *helpDialog) SetSize(width, height int) tea.Cmd {
+ return d.readOnlyScrollDialog.SetSize(width, height)
+}
+
+func (d *helpDialog) Bindings() []key.Binding {
+ return []key.Binding{}
+}
diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go
index a63520bc0..2ed89fcf5 100644
--- a/pkg/tui/tui.go
+++ b/pkg/tui/tui.go
@@ -1553,8 +1553,8 @@ func (m *appModel) Help() help.KeyMap {
return core.NewSimpleHelp(m.Bindings())
}
-// Bindings returns the key bindings shown in the status bar.
-func (m *appModel) Bindings() []key.Binding {
+// AllBindings returns ALL available key bindings for the help dialog (comprehensive list).
+func (m *appModel) AllBindings() []key.Binding {
quitBinding := key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("Ctrl+c", "quit"),
@@ -1572,10 +1572,48 @@ func (m *appModel) Bindings() []key.Binding {
bindings := []key.Binding{quitBinding, tabBinding}
bindings = append(bindings, m.tabBar.Bindings()...)
- bindings = append(bindings, key.NewBinding(
- key.WithKeys("ctrl+k"),
- key.WithHelp("Ctrl+k", "commands"),
- ))
+ // Additional global shortcuts
+ bindings = append(bindings,
+ key.NewBinding(
+ key.WithKeys("ctrl+k"),
+ key.WithHelp("Ctrl+k", "commands"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+h"),
+ key.WithHelp("Ctrl+h", "help"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+y"),
+ key.WithHelp("Ctrl+y", "toggle yolo mode"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+o"),
+ key.WithHelp("Ctrl+o", "toggle hide tool results"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+s"),
+ key.WithHelp("Ctrl+s", "cycle agent"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+m"),
+ key.WithHelp("Ctrl+m", "model picker"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+x"),
+ key.WithHelp("Ctrl+x", "clear queue"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+z"),
+ key.WithHelp("Ctrl+z", "suspend"),
+ ),
+ )
+
+ if !m.leanMode {
+ bindings = append(bindings, key.NewBinding(
+ key.WithKeys("ctrl+b"),
+ key.WithHelp("Ctrl+b", "toggle sidebar"),
+ ))
+ }
// Show newline help based on keyboard enhancement support
if m.keyboardEnhancementsSupported {
@@ -1608,6 +1646,47 @@ func (m *appModel) Bindings() []key.Binding {
return bindings
}
+// Bindings returns the key bindings shown in the status bar (a curated subset).
+// This filters AllBindings() to show only the most essential commands.
+func (m *appModel) Bindings() []key.Binding {
+ all := m.AllBindings()
+
+ // Define which keys should appear in the status bar
+ statusBarKeys := map[string]bool{
+ "ctrl+c": true, // quit
+ "tab": true, // switch focus
+ "ctrl+t": true, // new tab (from tabBar)
+ "ctrl+w": true, // close tab (from tabBar)
+ "ctrl+p": true, // prev tab (from tabBar)
+ "ctrl+n": true, // next tab (from tabBar)
+ "ctrl+k": true, // commands
+ "ctrl+h": true, // help
+ "shift+enter": true, // newline
+ "ctrl+j": true, // newline fallback
+ "ctrl+g": true, // edit in external editor (editor context)
+ "ctrl+r": true, // history search (editor context)
+ // Content panel bindings (↑↓, c, e, d) are always included
+ "up": true,
+ "down": true,
+ "c": true,
+ "e": true,
+ "d": true,
+ }
+
+ // Filter to only include status bar keys
+ var filtered []key.Binding
+ for _, binding := range all {
+ if len(binding.Keys()) > 0 {
+ bindingKey := binding.Keys()[0]
+ if statusBarKeys[bindingKey] {
+ filtered = append(filtered, binding)
+ }
+ }
+ }
+
+ return filtered
+}
+
// handleKeyPress handles all keyboard input with proper priority routing.
func (m *appModel) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
// Check if we should stop transcription on Enter or Escape
@@ -1687,6 +1766,12 @@ func (m *appModel) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, key.NewBinding(key.WithKeys("ctrl+x"))):
return m, core.CmdHandler(messages.ClearQueueMsg{})
+
+ case key.Matches(msg, key.NewBinding(key.WithKeys("ctrl+h", "f1", "ctrl+?"))):
+ // Show contextual help dialog with ALL available key bindings
+ return m, core.CmdHandler(dialog.OpenDialogMsg{
+ Model: dialog.NewHelpDialog(m.AllBindings()),
+ })
}
// History search is a modal state — capture all remaining keys before normal routing