Skip to content
Merged
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
11 changes: 11 additions & 0 deletions pkg/tui/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,17 @@ func builtInSessionCommands() []Item {
return core.CmdHandler(messages.ShowToolsDialogMsg{})
},
},
{
ID: "session.skills",
Label: "Skills",
SlashCommand: "/skills",
Description: "List skills available to the current agent",
Category: "Session",
Immediate: true,
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ShowSkillsDialogMsg{})
},
},
{
ID: "session.toolset.restart",
Label: "Restart Toolset",
Expand Down
9 changes: 9 additions & 0 deletions pkg/tui/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ func TestParseSlashCommand_OtherCommands(t *testing.T) {
assert.True(t, ok)
})

t.Run("skills command", func(t *testing.T) {
t.Parallel()
cmd := parser.Parse("/skills")
require.NotNil(t, cmd)
msg := cmd()
_, ok := msg.(messages.ShowSkillsDialogMsg)
assert.True(t, ok)
})

t.Run("unknown command returns nil", func(t *testing.T) {
t.Parallel()
cmd := parser.Parse("/unknown")
Expand Down
78 changes: 78 additions & 0 deletions pkg/tui/dialog/skills.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package dialog

import (
"fmt"
"strings"

"charm.land/lipgloss/v2"

"github.com/docker/docker-agent/pkg/skills"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/styles"
)

type skillsDialog struct {
readOnlyScrollDialog

skills []skills.Skill
}

// NewSkillsDialog creates the /skills dialog showing every skill exposed
// to the current agent.
func NewSkillsDialog(skillList []skills.Skill) Dialog {
d := &skillsDialog{
skills: skillList,
}
d.readOnlyScrollDialog = newReadOnlyScrollDialog(
readOnlyScrollDialogSize{widthPercent: 70, minWidth: 60, maxWidth: 100, heightPercent: 80, heightMax: 40},
d.renderLines,
)
return d
}

func (d *skillsDialog) renderLines(contentWidth, _ int) []string {
title := fmt.Sprintf("Skills (%d)", len(d.skills))
lines := []string{
RenderTitle(title, contentWidth, styles.DialogTitleStyle),
RenderSeparator(contentWidth),
"",
}

if len(d.skills) == 0 {
lines = append(lines, " "+styles.MutedStyle.Render("No skills available."), "")
return lines
}

for i := range d.skills {
lines = append(lines, formatSkill(&d.skills[i], contentWidth)...)
}

return lines
}

func formatSkill(s *skills.Skill, contentWidth int) []string {
name := lipgloss.NewStyle().Foreground(styles.Highlight).Render(" " + s.Name)
name += " " + skillSourceBadge(s)
if s.IsFork() {
name += " " + styles.MutedStyle.Render("[fork]")
}

out := []string{name}

if desc, _, _ := strings.Cut(s.Description, "\n"); desc != "" {
indent := " "
availableWidth := contentWidth - lipgloss.Width(indent)
if availableWidth > 0 {
out = append(out, indent+styles.MutedStyle.Render(toolcommon.TruncateText(desc, availableWidth)))
}
}
out = append(out, "")
return out
}

func skillSourceBadge(s *skills.Skill) string {
if s.Local {
return styles.SuccessStyle.Render("[local]")
}
return styles.WarningStyle.Render("[remote]")
}
45 changes: 45 additions & 0 deletions pkg/tui/dialog/skills_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dialog

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

"github.com/docker/docker-agent/pkg/skills"
)

func TestNewSkillsDialog_EmptyShowsPlaceholder(t *testing.T) {
t.Parallel()
d := NewSkillsDialog(nil).(*skillsDialog)
out := strings.Join(d.renderLines(80, 24), "\n")
assert.Contains(t, out, "Skills (0)")
assert.Contains(t, out, "No skills available")
}

func TestNewSkillsDialog_RendersSkills(t *testing.T) {
t.Parallel()
skillList := []skills.Skill{
{
Name: "commit",
Description: "Commit local changes",
Local: true,
Context: "fork",
},
{
Name: "poem",
Description: "Prints a poem",
Local: false,
},
}
d := NewSkillsDialog(skillList).(*skillsDialog)
out := strings.Join(d.renderLines(80, 24), "\n")
assert.Contains(t, out, "Skills (2)")
assert.Contains(t, out, "commit")
assert.Contains(t, out, "Commit local changes")
assert.Contains(t, out, "local")
assert.Contains(t, out, "fork")
assert.Contains(t, out, "poem")
assert.Contains(t, out, "Prints a poem")
assert.Contains(t, out, "remote")
}
6 changes: 6 additions & 0 deletions pkg/tui/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ func (m *appModel) handleShowToolsDialog() (tea.Model, tea.Cmd) {
})
}

func (m *appModel) handleShowSkillsDialog() (tea.Model, tea.Cmd) {
return m, core.CmdHandler(dialog.OpenDialogMsg{
Model: dialog.NewSkillsDialog(m.application.CurrentAgentSkills()),
})
}

// handleRestartToolset asks the runtime to restart the named toolset.
// The actual call can block for up to ~35s (the supervisor's
// reconnect timeout), so we run it inside a tea.Cmd goroutine and
Expand Down
4 changes: 4 additions & 0 deletions pkg/tui/messages/toggle.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type (
// the tool catalogue grouped by category.
ShowToolsDialogMsg struct{}

// ShowSkillsDialogMsg shows the skills dialog: the list of skills
// available to the current agent.
ShowSkillsDialogMsg struct{}

// RestartToolsetMsg asks the runtime to restart the named toolset by
// triggering its supervisor's RestartAndWait.
RestartToolsetMsg struct{ Name string }
Expand Down
3 changes: 3 additions & 0 deletions pkg/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,9 @@ func (m *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case messages.ShowToolsDialogMsg:
return m.handleShowToolsDialog()

case messages.ShowSkillsDialogMsg:
return m.handleShowSkillsDialog()

case messages.RestartToolsetMsg:
return m.handleRestartToolset(msg.Name)

Expand Down
Loading