Skip to content

Commit 5c04139

Browse files
authored
Autocomplete (#105)
* implement autocomplete * fix * fix * remove useless history command * fix spacing * fix padding for goodbye * cleanup
1 parent df29dc1 commit 5c04139

File tree

6 files changed

+596
-51
lines changed

6 files changed

+596
-51
lines changed

cmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI,
931931
// Get user input
932932
prompt, err := cli.GetPrompt()
933933
if err == io.EOF {
934-
fmt.Println("\nGoodbye!")
934+
fmt.Println("\n Goodbye!")
935935
return nil
936936
}
937937
if err != nil {
@@ -944,7 +944,7 @@ func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI,
944944

945945
// Handle slash commands
946946
if cli.IsSlashCommand(prompt) {
947-
result := cli.HandleSlashCommand(prompt, config.ServerNames, config.ToolNames, messages)
947+
result := cli.HandleSlashCommand(prompt, config.ServerNames, config.ToolNames)
948948
if result.Handled {
949949
// If the command was to clear history, clear the messages slice and session
950950
if result.ClearHistory {

internal/ui/cli.go

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package ui
22

33
import (
4-
"errors"
54
"fmt"
65
"io"
76
"os"
87
"strings"
98
"time"
109

11-
"github.com/charmbracelet/huh"
10+
tea "github.com/charmbracelet/bubbletea"
1211
"github.com/charmbracelet/lipgloss"
1312
"github.com/cloudwego/eino/schema"
1413
"golang.org/x/term"
@@ -71,23 +70,34 @@ func (c *CLI) GetPrompt() (string, error) {
7170

7271
// No divider needed - removed for cleaner appearance
7372

74-
var prompt string
75-
err := huh.NewForm(huh.NewGroup(huh.NewText().
76-
Title("Enter your prompt (Type /help for commands, Ctrl+C to quit, ESC to cancel generation)").
77-
Value(&prompt).
78-
CharLimit(5000)),
79-
).WithWidth(c.width).
80-
WithTheme(huh.ThemeCharm()).
81-
Run()
73+
// Create our custom slash command input
74+
input := NewSlashCommandInput(c.width, "Enter your prompt (Type /help for commands, Ctrl+C to quit, ESC to cancel generation)")
75+
76+
// Run as a tea program
77+
p := tea.NewProgram(input)
78+
finalModel, err := p.Run()
8279

8380
if err != nil {
84-
if errors.Is(err, huh.ErrUserAborted) {
81+
return "", err
82+
}
83+
84+
// Get the value from the final model
85+
if finalInput, ok := finalModel.(*SlashCommandInput); ok {
86+
// Clear the input field from the display
87+
linesToClear := finalInput.RenderedLines()
88+
// We need to clear linesToClear - 1 lines because we're already on the line after the last rendered line
89+
for i := 0; i < linesToClear-1; i++ {
90+
fmt.Print("\033[1A\033[2K") // Move up one line and clear it
91+
}
92+
93+
if finalInput.Cancelled() {
8594
return "", io.EOF // Signal clean exit
8695
}
87-
return "", err
96+
value := strings.TrimSpace(finalInput.Value())
97+
return value, nil
8898
}
8999

90-
return prompt, nil
100+
return "", fmt.Errorf("unexpected model type")
91101
}
92102

93103
// ShowSpinner displays a spinner with the given message and executes the action
@@ -241,7 +251,6 @@ func (c *CLI) DisplayHelp() {
241251
- ` + "`/help`" + `: Show this help message
242252
- ` + "`/tools`" + `: List all available tools
243253
- ` + "`/servers`" + `: List configured MCP servers
244-
- ` + "`/history`" + `: Display conversation history
245254
- ` + "`/usage`" + `: Show token usage and cost statistics
246255
- ` + "`/reset-usage`" + `: Reset usage statistics
247256
- ` + "`/clear`" + `: Clear message history
@@ -295,36 +304,6 @@ func (c *CLI) DisplayServers(servers []string) {
295304
c.displayContainer()
296305
}
297306

298-
// DisplayHistory displays conversation history using the message container
299-
func (c *CLI) DisplayHistory(messages []*schema.Message) {
300-
// Create a temporary container for history
301-
historyContainer := NewMessageContainer(c.width, c.height-4, c.compactMode)
302-
303-
for _, msg := range messages {
304-
switch msg.Role {
305-
case schema.User:
306-
var uiMsg UIMessage
307-
if c.compactMode {
308-
uiMsg = c.compactRenderer.RenderUserMessage(msg.Content, time.Now())
309-
} else {
310-
uiMsg = c.messageRenderer.RenderUserMessage(msg.Content, time.Now())
311-
}
312-
historyContainer.AddMessage(uiMsg)
313-
case schema.Assistant:
314-
var uiMsg UIMessage
315-
if c.compactMode {
316-
uiMsg = c.compactRenderer.RenderAssistantMessage(msg.Content, time.Now(), c.modelName)
317-
} else {
318-
uiMsg = c.messageRenderer.RenderAssistantMessage(msg.Content, time.Now(), c.modelName)
319-
}
320-
historyContainer.AddMessage(uiMsg)
321-
}
322-
}
323-
324-
fmt.Println("\nConversation History:")
325-
fmt.Println(historyContainer.Render())
326-
}
327-
328307
// IsSlashCommand checks if the input is a slash command
329308
func (c *CLI) IsSlashCommand(input string) bool {
330309
return strings.HasPrefix(input, "/")
@@ -337,7 +316,7 @@ type SlashCommandResult struct {
337316
}
338317

339318
// HandleSlashCommand handles slash commands and returns the result
340-
func (c *CLI) HandleSlashCommand(input string, servers []string, tools []string, history []*schema.Message) SlashCommandResult {
319+
func (c *CLI) HandleSlashCommand(input string, servers []string, tools []string) SlashCommandResult {
341320
switch input {
342321
case "/help":
343322
c.DisplayHelp()
@@ -348,9 +327,7 @@ func (c *CLI) HandleSlashCommand(input string, servers []string, tools []string,
348327
case "/servers":
349328
c.DisplayServers(servers)
350329
return SlashCommandResult{Handled: true}
351-
case "/history":
352-
c.DisplayHistory(history)
353-
return SlashCommandResult{Handled: true}
330+
354331
case "/clear":
355332
c.ClearMessages()
356333
c.DisplayInfo("Conversation cleared. Starting fresh.")
@@ -362,7 +339,7 @@ func (c *CLI) HandleSlashCommand(input string, servers []string, tools []string,
362339
c.ResetUsageStats()
363340
return SlashCommandResult{Handled: true}
364341
case "/quit":
365-
fmt.Println("\nGoodbye!")
342+
fmt.Println("\n Goodbye!")
366343
os.Exit(0)
367344
return SlashCommandResult{Handled: true}
368345
default:

internal/ui/commands.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package ui
2+
3+
// SlashCommand represents a slash command with its metadata
4+
type SlashCommand struct {
5+
Name string
6+
Description string
7+
Aliases []string
8+
Category string // e.g., "Navigation", "System", "Info"
9+
}
10+
11+
// SlashCommands is the registry of all available slash commands
12+
var SlashCommands = []SlashCommand{
13+
{
14+
Name: "/help",
15+
Description: "Show available commands and usage information",
16+
Category: "Info",
17+
Aliases: []string{"/h", "/?"},
18+
},
19+
{
20+
Name: "/tools",
21+
Description: "List all available MCP tools",
22+
Category: "Info",
23+
Aliases: []string{"/t"},
24+
},
25+
{
26+
Name: "/servers",
27+
Description: "Show connected MCP servers",
28+
Category: "Info",
29+
Aliases: []string{"/s"},
30+
},
31+
32+
{
33+
Name: "/clear",
34+
Description: "Clear conversation and start fresh",
35+
Category: "System",
36+
Aliases: []string{"/c", "/cls"},
37+
},
38+
{
39+
Name: "/usage",
40+
Description: "Show token usage statistics",
41+
Category: "Info",
42+
Aliases: []string{"/u"},
43+
},
44+
{
45+
Name: "/reset-usage",
46+
Description: "Reset usage statistics",
47+
Category: "System",
48+
Aliases: []string{"/ru"},
49+
},
50+
{
51+
Name: "/quit",
52+
Description: "Exit the application",
53+
Category: "System",
54+
Aliases: []string{"/q", "/exit"},
55+
},
56+
}
57+
58+
// GetCommandByName returns a command by its name or alias
59+
func GetCommandByName(name string) *SlashCommand {
60+
for i := range SlashCommands {
61+
cmd := &SlashCommands[i]
62+
if cmd.Name == name {
63+
return cmd
64+
}
65+
for _, alias := range cmd.Aliases {
66+
if alias == name {
67+
return cmd
68+
}
69+
}
70+
}
71+
return nil
72+
}
73+
74+
// GetAllCommandNames returns all command names and aliases
75+
func GetAllCommandNames() []string {
76+
var names []string
77+
for _, cmd := range SlashCommands {
78+
names = append(names, cmd.Name)
79+
names = append(names, cmd.Aliases...)
80+
}
81+
return names
82+
}

internal/ui/fuzzy.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package ui
2+
3+
import (
4+
"strings"
5+
)
6+
7+
// FuzzyMatch represents a match result with score
8+
type FuzzyMatch struct {
9+
Command *SlashCommand
10+
Score int
11+
}
12+
13+
// FuzzyMatchCommands performs fuzzy matching on slash commands
14+
func FuzzyMatchCommands(query string, commands []SlashCommand) []FuzzyMatch {
15+
if query == "" || query == "/" {
16+
// Return all commands when query is empty or just "/"
17+
matches := make([]FuzzyMatch, len(commands))
18+
for i := range commands {
19+
matches[i] = FuzzyMatch{
20+
Command: &commands[i],
21+
Score: 0,
22+
}
23+
}
24+
return matches
25+
}
26+
27+
// Normalize query
28+
query = strings.ToLower(strings.TrimPrefix(query, "/"))
29+
30+
var matches []FuzzyMatch
31+
32+
for i := range commands {
33+
cmd := &commands[i]
34+
score := fuzzyScore(query, cmd)
35+
if score > 0 {
36+
matches = append(matches, FuzzyMatch{
37+
Command: cmd,
38+
Score: score,
39+
})
40+
}
41+
}
42+
43+
// Sort by score (highest first)
44+
for i := 0; i < len(matches)-1; i++ {
45+
for j := i + 1; j < len(matches); j++ {
46+
if matches[j].Score > matches[i].Score {
47+
matches[i], matches[j] = matches[j], matches[i]
48+
}
49+
}
50+
}
51+
52+
return matches
53+
}
54+
55+
// fuzzyScore calculates the fuzzy match score for a command
56+
func fuzzyScore(query string, cmd *SlashCommand) int {
57+
// Check exact match first
58+
cmdName := strings.ToLower(strings.TrimPrefix(cmd.Name, "/"))
59+
if cmdName == query {
60+
return 1000
61+
}
62+
63+
// Check aliases for exact match
64+
for _, alias := range cmd.Aliases {
65+
aliasName := strings.ToLower(strings.TrimPrefix(alias, "/"))
66+
if aliasName == query {
67+
return 900
68+
}
69+
}
70+
71+
// Check if command starts with query
72+
if strings.HasPrefix(cmdName, query) {
73+
return 800 - len(cmdName) + len(query)
74+
}
75+
76+
// Check if any alias starts with query
77+
for _, alias := range cmd.Aliases {
78+
aliasName := strings.ToLower(strings.TrimPrefix(alias, "/"))
79+
if strings.HasPrefix(aliasName, query) {
80+
return 700 - len(aliasName) + len(query)
81+
}
82+
}
83+
84+
// Check if command contains query
85+
if strings.Contains(cmdName, query) {
86+
return 500
87+
}
88+
89+
// Check if description contains query
90+
if strings.Contains(strings.ToLower(cmd.Description), query) {
91+
return 300
92+
}
93+
94+
// Fuzzy character matching
95+
score := fuzzyCharacterMatch(query, cmdName)
96+
if score > 0 {
97+
return score
98+
}
99+
100+
// Try fuzzy matching on aliases
101+
for _, alias := range cmd.Aliases {
102+
aliasName := strings.ToLower(strings.TrimPrefix(alias, "/"))
103+
score = fuzzyCharacterMatch(query, aliasName)
104+
if score > 0 {
105+
return score - 50 // Slightly lower score for alias matches
106+
}
107+
}
108+
109+
return 0
110+
}
111+
112+
// fuzzyCharacterMatch performs character-by-character fuzzy matching
113+
func fuzzyCharacterMatch(query, target string) int {
114+
if len(query) > len(target) {
115+
return 0
116+
}
117+
118+
queryIdx := 0
119+
score := 100
120+
consecutiveMatches := 0
121+
122+
for i := 0; i < len(target) && queryIdx < len(query); i++ {
123+
if target[i] == query[queryIdx] {
124+
queryIdx++
125+
consecutiveMatches++
126+
score += consecutiveMatches * 10
127+
} else {
128+
consecutiveMatches = 0
129+
score -= 5
130+
}
131+
}
132+
133+
// Must match all characters in query
134+
if queryIdx < len(query) {
135+
return 0
136+
}
137+
138+
return score
139+
}

0 commit comments

Comments
 (0)