diff --git a/pkg/tui/components/statusbar/statusbar.go b/pkg/tui/components/statusbar/statusbar.go index e7c4deba8..5c43094ef 100644 --- a/pkg/tui/components/statusbar/statusbar.go +++ b/pkg/tui/components/statusbar/statusbar.go @@ -8,7 +8,6 @@ import ( "github.com/docker/docker-agent/pkg/tui/core" "github.com/docker/docker-agent/pkg/tui/styles" - "github.com/docker/docker-agent/pkg/version" ) // StatusBar displays key-binding help on the left and version info on the right. @@ -16,6 +15,7 @@ import ( type StatusBar struct { width int help core.KeyMapHelp + title string showNewTab bool newTabStartX int @@ -25,12 +25,31 @@ type StatusBar struct { cacheDirty bool } +// Option is a functional option for configuring a StatusBar. +type Option func(*StatusBar) + +// WithTitle sets a custom title for the status bar. +// +// If not provided, defaults to "docker agent". +func WithTitle(title string) Option { + return func(s *StatusBar) { + s.title = title + } +} + // New creates a new StatusBar instance -func New(help core.KeyMapHelp) StatusBar { - return StatusBar{ +func New(help core.KeyMapHelp, opts ...Option) StatusBar { + s := StatusBar{ help: help, + title: "docker agent", cacheDirty: true, } + + for _, opt := range opts { + opt(&s) + } + + return s } // SetWidth sets the width of the status bar @@ -76,19 +95,18 @@ func (s *StatusBar) rebuild() { s.newTabStartX = 0 s.newTabEndX = 0 - // Build the styled right side: optional new-tab button + version. - var right string + // Build the styled right side: optional new-tab button + title. var rightW, newTabW int - ver := styles.MutedStyle.Render("docker agent " + version.Version) + right := styles.MutedStyle.Render(s.title) + if s.showNewTab { newTab := styles.MutedStyle.Render(" \u2502 ") + styles.HighlightWhiteStyle.Render("+") + styles.SecondaryStyle.Render(" new tab") newTabW = lipgloss.Width(newTab) - right = newTab + " " + ver + right = newTab + " " + right rightW = lipgloss.Width(right) } else { - right = ver rightW = lipgloss.Width(right) } diff --git a/pkg/tui/handlers.go b/pkg/tui/handlers.go index 6571abb9c..5cf3bfcab 100644 --- a/pkg/tui/handlers.go +++ b/pkg/tui/handlers.go @@ -595,23 +595,25 @@ func (m *appModel) handleElicitationResponse(action tools.ElicitationAction, con } func (m *appModel) startShell() (tea.Model, tea.Cmd) { + exitMsg := "Type 'exit' to return to " + m.appName + var cmd *exec.Cmd if goruntime.GOOS == "windows" { if path, err := exec.LookPath("pwsh.exe"); err == nil { cmd = exec.Command(path, "-NoLogo", "-NoExit", "-Command", - `Write-Host ""; Write-Host "Type 'exit' to return to docker agent 🐳"`) + `Write-Host ""; Write-Host "`+exitMsg+`"`) } else if path, err := exec.LookPath("powershell.exe"); err == nil { cmd = exec.Command(path, "-NoLogo", "-NoExit", "-Command", - `Write-Host ""; Write-Host "Type 'exit' to return to docker agent 🐳"`) + `Write-Host ""; Write-Host "`+exitMsg+`"`) } else { // Use absolute path to cmd.exe to prevent PATH hijacking (CWE-426). shell := shellpath.WindowsCmdExe() - cmd = exec.Command(shell, "/K", `echo. & echo Type 'exit' to return to docker agent`) + cmd = exec.Command(shell, "/K", "echo. & echo "+exitMsg) } } else { shell := shellpath.DetectUnixShell() cmd = exec.Command(shell, "-i", "-c", - `echo -e "\nType 'exit' to return to docker agent 🐳"; exec `+shell) + `echo -e "\n`+exitMsg+`"; exec `+shell) } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 3c95ed23a..24f6e1feb 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -42,6 +42,7 @@ import ( "github.com/docker/docker-agent/pkg/tui/service/tuistate" "github.com/docker/docker-agent/pkg/tui/styles" "github.com/docker/docker-agent/pkg/userconfig" + "github.com/docker/docker-agent/pkg/version" ) // SessionSpawner creates new sessions with their own runtime. @@ -163,6 +164,9 @@ type appModel struct { // buildCommandCategories is a function that returns the list of command categories. buildCommandCategories func(context.Context, tea.Model) []commands.Category + + appName string + appVersion string } // Option configures the TUI. @@ -176,6 +180,24 @@ func WithLeanMode() Option { } } +// WithAppName sets the application name. +// +// If not provided, defaults to "docker agent". +func WithAppName(name string) Option { + return func(m *appModel) { + m.appName = name + } +} + +// WithVersion sets the application version. +// +// If not provided, defaults to version.Version. +func WithVersion(v string) Option { + return func(m *appModel) { + m.appVersion = v + } +} + // WithCommandBuilder builds the command categories shown in the command // palette from the given function. It overrides the default command category // builder. To include the default commands, the given function should call @@ -242,6 +264,8 @@ func New(ctx context.Context, spawner SessionSpawner, initialApp *app.App, initi focusedPanel: PanelEditor, editorLines: 3, dockerDesktop: os.Getenv("TERM_PROGRAM") == "docker_desktop", + appName: "docker agent", + appVersion: version.Version, } // Apply options @@ -260,7 +284,7 @@ func New(ctx context.Context, spawner SessionSpawner, initialApp *app.App, initi m.chatPage = initialChatPage // Initialize status bar (pass m as help provider) - m.statusBar = statusbar.New(m) + m.statusBar = statusbar.New(m, statusbar.WithTitle(m.appName+" "+m.appVersion)) // Add the initial session to the supervisor sv.AddSession(ctx, initialApp, initialApp.Session(), initialWorkingDir, cleanup) @@ -2320,9 +2344,9 @@ func (m *appModel) View() tea.View { // When the agent is working, a rotating spinner character is prepended so that // terminal multiplexers (tmux) can detect activity in the pane. func (m *appModel) windowTitle() string { - title := "docker agent" + title := m.appName if sessionTitle := m.sessionState.SessionTitle(); sessionTitle != "" { - title = sessionTitle + " - docker agent" + title = sessionTitle + " - " + m.appName } if m.chatPage.IsWorking() { title = spinner.Frame(m.animFrame) + " " + title