Skip to content
Draft
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
25 changes: 16 additions & 9 deletions pkg/skills/skills.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ const skillFile = "SKILL.md"

// Skill represents a loaded skill with its metadata and content location.
type Skill struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
FilePath string `yaml:"-"`
BaseDir string `yaml:"-"`
Files []string `yaml:"-"`
License string `yaml:"license"`
Compatibility string `yaml:"compatibility"`
Metadata map[string]string `yaml:"metadata"`
AllowedTools []string `yaml:"allowed-tools"`
Name string `yaml:"name"`
Description string `yaml:"description"`
ArgsDescription string `yaml:"args"`
FilePath string `yaml:"-"`
BaseDir string `yaml:"-"`
Files []string `yaml:"-"`
License string `yaml:"license"`
Compatibility string `yaml:"compatibility"`
Metadata map[string]string `yaml:"metadata"`
AllowedTools []string `yaml:"allowed-tools"`
}

// HasArgs returns true if the skill declares an args description,
// indicating it accepts arguments when invoked as a slash command.
func (s Skill) HasArgs() bool {
return s.ArgsDescription != ""
}

// Load discovers and loads skills from the given sources.
Expand Down
6 changes: 5 additions & 1 deletion pkg/tui/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,11 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor
skillCommands := make([]Item, 0, len(skillsList))
for _, skill := range skillsList {
skillName := skill.Name
description := toolcommon.TruncateText(skill.Description, 55)
description := skill.Description
if skill.HasArgs() {
description += " (args: " + skill.ArgsDescription + ")"
}
description = toolcommon.TruncateText(description, 55)

skillCommands = append(skillCommands, Item{
ID: "skill." + skillName,
Expand Down
24 changes: 22 additions & 2 deletions pkg/tui/components/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type QueryMsg struct {
type SelectedMsg struct {
Value string
Execute func() tea.Cmd
Tab bool // Tab selects without submitting, allowing the user to continue typing.
}

// SelectionChangedMsg is sent when the selected item changes (for preview in editor)
Expand Down Expand Up @@ -88,6 +89,7 @@ type completionKeyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Tab key.Binding
Escape key.Binding
}

Expand All @@ -103,8 +105,12 @@ func defaultCompletionKeyMap() completionKeyMap {
key.WithHelp("↓", "down"),
),
Enter: key.NewBinding(
key.WithKeys("enter", "tab"),
key.WithHelp("enter/tab", "select"),
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "complete"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
Expand Down Expand Up @@ -260,6 +266,20 @@ func (c *manager) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
}),
core.CmdHandler(ClosedMsg{}),
)

case key.Matches(msg, c.keyMap.Tab):
c.visible = false
if len(c.filteredItems) == 0 || c.selected >= len(c.filteredItems) {
return c, core.CmdHandler(ClosedMsg{})
}
selectedItem := c.filteredItems[c.selected]
return c, tea.Sequence(
core.CmdHandler(SelectedMsg{
Value: selectedItem.Value,
Tab: true,
}),
core.CmdHandler(ClosedMsg{}),
)
case key.Matches(msg, c.keyMap.Escape):
c.visible = false
return c, core.CmdHandler(ClosedMsg{})
Expand Down
9 changes: 9 additions & 0 deletions pkg/tui/components/editor/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,15 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
return e, msg.Execute()
}
if e.currentCompletion.AutoSubmit() {
if msg.Tab {
// Tab inserts the value without submitting, allowing the user
// to continue typing (e.g., adding arguments to a skill).
e.textarea.SetValue(msg.Value + " ")
e.textarea.MoveToEnd()
e.userTyped = true
e.clearSuggestion()
return e, nil
}
// For auto-submit completions (like commands), use the selected
// command value (e.g., "/exit") instead of what the user typed
// (e.g., "/e"). Append any extra text after the trigger word
Expand Down
33 changes: 28 additions & 5 deletions pkg/tui/dialog/command_palette.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ type CommandExecuteMsg struct {
Command commands.Item
}

// CommandAutoCompleteMsg is sent when a command is auto-completed via tab.
// The dialog closes and the slash command is inserted into the editor for further typing.
type CommandAutoCompleteMsg struct {
Command commands.Item
}

// commandPaletteDialog implements Dialog for the command palette
type commandPaletteDialog struct {
BaseDialog
Expand All @@ -40,6 +46,7 @@ type commandPaletteKeyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Tab key.Binding
Escape key.Binding
}

Expand All @@ -58,6 +65,10 @@ func defaultCommandPaletteKeyMap() commandPaletteKeyMap {
key.WithKeys("enter"),
key.WithHelp("enter", "execute"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "complete"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
Expand Down Expand Up @@ -120,7 +131,7 @@ func (d *commandPaletteDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
if cmdIdx == d.lastClickIndex && now.Sub(d.lastClickTime) < styles.DoubleClickThreshold {
d.selected = cmdIdx
d.lastClickTime = time.Time{}
cmd := d.executeSelected()
cmd := d.selectCommand(false)
return d, cmd
}
d.selected = cmdIdx
Expand Down Expand Up @@ -154,7 +165,11 @@ func (d *commandPaletteDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
return d, nil

case key.Matches(msg, d.keyMap.Enter):
cmd := d.executeSelected()
cmd := d.selectCommand(false)
return d, cmd

case key.Matches(msg, d.keyMap.Tab):
cmd := d.selectCommand(true)
return d, cmd

default:
Expand All @@ -168,12 +183,20 @@ func (d *commandPaletteDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
return d, nil
}

// executeSelected executes the currently selected command and closes the dialog.
func (d *commandPaletteDialog) executeSelected() tea.Cmd {
// selectCommand handles both enter (execute) and tab (auto-complete) on the
// selected command. When autoComplete is true, the slash command is inserted
// into the editor for further typing instead of being executed.
func (d *commandPaletteDialog) selectCommand(autoComplete bool) tea.Cmd {
if d.selected < 0 || d.selected >= len(d.filtered) {
return nil
}
selectedCmd := d.filtered[d.selected]
if autoComplete {
return tea.Sequence(
core.CmdHandler(CloseDialogMsg{}),
core.CmdHandler(CommandAutoCompleteMsg{Command: selectedCmd}),
)
}
cmds := []tea.Cmd{core.CmdHandler(CloseDialogMsg{})}
if selectedCmd.Execute != nil {
cmds = append(cmds, selectedCmd.Execute(""))
Expand Down Expand Up @@ -338,7 +361,7 @@ func (d *commandPaletteDialog) View() string {
AddSeparator().
AddContent(scrollableContent).
AddSpace().
AddHelpKeys("↑/↓", "navigate", "enter", "execute", "esc", "close").
AddHelpKeys("↑/↓", "navigate", "enter", "execute", "tab", "complete", "esc", "close").
Build()

return styles.DialogStyle.Width(dialogWidth).Render(content)
Expand Down
2 changes: 1 addition & 1 deletion pkg/tui/dialog/file_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func (d *filePickerDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
}
return d, nil

case key.Matches(msg, d.keyMap.Enter):
case key.Matches(msg, d.keyMap.Enter) || key.Matches(msg, d.keyMap.Tab):
if d.selected >= 0 && d.selected < len(d.filtered) {
entry := d.filtered[d.selected]
if entry.isDir {
Expand Down
2 changes: 1 addition & 1 deletion pkg/tui/dialog/model_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (d *modelPickerDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
}
return d, nil

case key.Matches(msg, d.keyMap.Enter):
case key.Matches(msg, d.keyMap.Enter) || key.Matches(msg, d.keyMap.Tab):
cmd := d.handleSelection()
return d, cmd

Expand Down
2 changes: 1 addition & 1 deletion pkg/tui/dialog/theme_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func (d *themePickerDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
}
return d, nil

case key.Matches(msg, d.keyMap.Enter):
case key.Matches(msg, d.keyMap.Enter) || key.Matches(msg, d.keyMap.Tab):
cmd := d.handleSelection()
return d, cmd

Expand Down
8 changes: 8 additions & 0 deletions pkg/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,14 @@ func (m *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cleanupAll()
return m, tea.Quit

case dialog.CommandAutoCompleteMsg:
// Tab was pressed in the command palette: insert the slash command into the editor
// so the user can continue typing (e.g., adding arguments).
if msg.Command.SlashCommand != "" {
m.editor.SetValue(msg.Command.SlashCommand + " ")
}
return m, m.editor.Focus()

case dialog.RuntimeResumeMsg:
m.application.Resume(msg.Request)
return m, nil
Expand Down