Compare commits
3 Commits
8ca044b6af
...
e59ce973b6
Author | SHA1 | Date | |
---|---|---|---|
e59ce973b6 | |||
4ef841e945 | |||
c68084f8a5 |
1
TODO.md
1
TODO.md
@ -14,6 +14,7 @@
|
|||||||
system prompt, rather than having them in the conversation messages)
|
system prompt, rather than having them in the conversation messages)
|
||||||
- [ ] Agents may have some form of long term memory management (key-value?
|
- [ ] Agents may have some form of long term memory management (key-value?
|
||||||
natural lang?).
|
natural lang?).
|
||||||
|
- [ ] Sandboxed python, js interpreter (both useful for different reasons)
|
||||||
- [ ] Support for arbitrary external script tools
|
- [ ] Support for arbitrary external script tools
|
||||||
- [ ] Search - RAG driven search of existing conversation "hey, remind me of
|
- [ ] Search - RAG driven search of existing conversation "hey, remind me of
|
||||||
the conversation we had six months ago about X")
|
the conversation we had six months ago about X")
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
package toolbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const FILE_REPLACE_LINES_DESCRIPTION = `Replace or remove a range of lines within a file, must specify path.
|
|
||||||
|
|
||||||
Useful for re-writing snippets/blocks of code or entire functions.
|
|
||||||
|
|
||||||
Plan your edits carefully and ensure any new content matches the flow and indentation of surrounding text.`
|
|
||||||
|
|
||||||
var FileReplaceLinesTool = api.ToolSpec{
|
|
||||||
Name: "file_replace_lines",
|
|
||||||
Description: FILE_REPLACE_LINES_DESCRIPTION,
|
|
||||||
Parameters: []api.ToolParameter{
|
|
||||||
{
|
|
||||||
Name: "path",
|
|
||||||
Type: "string",
|
|
||||||
Description: "Path of the file to be modified, relative to the current working directory.",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "start_line",
|
|
||||||
Type: "integer",
|
|
||||||
Description: `Line number which specifies the start of the replacement range (inclusive).`,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "end_line",
|
|
||||||
Type: "integer",
|
|
||||||
Description: `Line number which specifies the end of the replacement range (inclusive). If unset, range extends to end of file.`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "content",
|
|
||||||
Type: "string",
|
|
||||||
Description: `Content to replace specified range. Omit to remove the specified range.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
|
|
||||||
tmp, ok := args["path"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("path parameter to write_file was not included.")
|
|
||||||
}
|
|
||||||
path, ok := tmp.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
|
||||||
}
|
|
||||||
var start_line int
|
|
||||||
tmp, ok = args["start_line"]
|
|
||||||
if ok {
|
|
||||||
tmp, ok := tmp.(float64)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("Invalid start_line in function arguments: %v", tmp)
|
|
||||||
}
|
|
||||||
start_line = int(tmp)
|
|
||||||
}
|
|
||||||
var end_line int
|
|
||||||
tmp, ok = args["end_line"]
|
|
||||||
if ok {
|
|
||||||
tmp, ok := tmp.(float64)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("Invalid end_line in function arguments: %v", tmp)
|
|
||||||
}
|
|
||||||
end_line = int(tmp)
|
|
||||||
}
|
|
||||||
var content string
|
|
||||||
tmp, ok = args["content"]
|
|
||||||
if ok {
|
|
||||||
content, ok = tmp.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("Invalid content in function arguments: %v", tmp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fileReplaceLines(path, start_line, end_line, content)
|
|
||||||
ret, err := result.ToJson()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("Could not serialize result: %v", err)
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileReplaceLines(path string, startLine int, endLine int, content string) api.CallResult {
|
|
||||||
ok, reason := toolutil.IsPathWithinCWD(path)
|
|
||||||
if !ok {
|
|
||||||
return api.CallResult{Message: reason}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the existing file's content
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}
|
|
||||||
}
|
|
||||||
_, err = os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
return api.CallResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())}
|
|
||||||
}
|
|
||||||
data = []byte{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if startLine < 1 {
|
|
||||||
return api.CallResult{Message: "start_line cannot be less than 1"}
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
contentLines := strings.Split(strings.Trim(content, "\n"), "\n")
|
|
||||||
|
|
||||||
if endLine == 0 || endLine > len(lines) {
|
|
||||||
endLine = len(lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
before := lines[:startLine-1]
|
|
||||||
after := lines[endLine:]
|
|
||||||
|
|
||||||
lines = append(before, append(contentLines, after...)...)
|
|
||||||
newContent := strings.Join(lines, "\n")
|
|
||||||
|
|
||||||
// Join the lines and write back to the file
|
|
||||||
err = os.WriteFile(path, []byte(newContent), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return api.CallResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())}
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.CallResult{Result: newContent}
|
|
||||||
}
|
|
178
pkg/agents/toolbox/modify_file.go
Normal file
178
pkg/agents/toolbox/modify_file.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package toolbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||||
|
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||||
|
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MODIFY_FILE_DESCRIPTION = []string{
|
||||||
|
"Modify a file. If the file does not exist, it will be created.",
|
||||||
|
"",
|
||||||
|
"Content can be either inserted, replaced, or removed through a combination of the start, stop, and content parameters.",
|
||||||
|
"Use the start and stop line numbers to limit the range of modification to the file.",
|
||||||
|
"If both `start` and `stop` are left unset (or set to 0), the entire file's contents will be updated.",
|
||||||
|
"If `start` is set to n and `stop` to n+1, content will be inserted at line n (the content that was at line n will be shifted below the newly inserted content).",
|
||||||
|
"If only `start` is set, content from the given line and onwards will be updated.",
|
||||||
|
"If only `stop` is set, content up to but not including the given line will be updated.",
|
||||||
|
"",
|
||||||
|
"Examples:",
|
||||||
|
"1. Append to a file:",
|
||||||
|
" {\"path\": \"example.txt\", \"start\": <last_line_number + 1>, \"content\": \"New content to append\"}",
|
||||||
|
"",
|
||||||
|
"2. Insert at a specific line:",
|
||||||
|
" {\"path\": \"example.txt\", \"start\": 5, \"stop\": 5, \"content\": \"New line inserted above the previous line 5\"}",
|
||||||
|
"",
|
||||||
|
"3. Replace a range of lines:",
|
||||||
|
" {\"path\": \"example.txt\", \"start\": 3, \"stop\": 7, \"content\": \"New content replacing lines 3-7\"}",
|
||||||
|
"",
|
||||||
|
"4. Remove a range of lines:",
|
||||||
|
" {\"path\": \"example.txt\", \"start\": 2, \"stop\": 5}",
|
||||||
|
"",
|
||||||
|
"5. Replace entire file contents:",
|
||||||
|
" {\"path\": \"example.txt\", \"content\": \"New file contents\"}",
|
||||||
|
"",
|
||||||
|
"6. Update from a specific line to the end of the file:",
|
||||||
|
" {\"path\": \"example.txt\", \"start\": 10, \"content\": \"New content from line 10 onwards\"}",
|
||||||
|
"",
|
||||||
|
"7. Update from the beginning of the file to a specific line:",
|
||||||
|
" {\"path\": \"example.txt\", \"stop\": 6, \"content\": \"New content for first 5 lines\"}",
|
||||||
|
"",
|
||||||
|
"Note: Always use specific line numbers based on the current file content. Avoid using arbitrarily large numbers for start or stop.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var ModifyFile = api.ToolSpec{
|
||||||
|
Name: "modify_file",
|
||||||
|
Description: strings.Join(MODIFY_FILE_DESCRIPTION, "\n"),
|
||||||
|
Parameters: []api.ToolParameter{
|
||||||
|
{
|
||||||
|
Name: "path",
|
||||||
|
Type: "string",
|
||||||
|
Description: "Path of the file to be modified, relative to the current working directory.",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "start",
|
||||||
|
Type: "integer",
|
||||||
|
Description: `Start line of the range to modify (inclusive). If omitted, the beginning of the file is implied.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "stop",
|
||||||
|
Type: "integer",
|
||||||
|
Description: `End line of the range to modify (inclusive). If omitted, the end of the file is implied.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "content",
|
||||||
|
Type: "string",
|
||||||
|
Description: "Content to insert/replace at the range defined by `start` and `stop`. If omitted, the range is removed.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Impl: func(tool *api.ToolSpec, args map[string]interface{}) (string, error) {
|
||||||
|
tmp, ok := args["path"]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("path parameter to modify_file was not included.")
|
||||||
|
}
|
||||||
|
path, ok := tmp.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("Invalid path in function arguments: %v", tmp)
|
||||||
|
}
|
||||||
|
var start int
|
||||||
|
tmp, ok = args["start"]
|
||||||
|
if ok {
|
||||||
|
tmp, ok := tmp.(float64)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("Invalid start in function arguments: %v", tmp)
|
||||||
|
}
|
||||||
|
start = int(tmp)
|
||||||
|
}
|
||||||
|
var stop int
|
||||||
|
tmp, ok = args["stop"]
|
||||||
|
if ok {
|
||||||
|
tmp, ok := tmp.(float64)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("Invalid stop in function arguments: %v", tmp)
|
||||||
|
}
|
||||||
|
stop = int(tmp)
|
||||||
|
}
|
||||||
|
var content string
|
||||||
|
tmp, ok = args["content"]
|
||||||
|
if ok {
|
||||||
|
content, ok = tmp.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("Invalid content in function arguments: %v", tmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fileModifyContents(path, start, stop, content)
|
||||||
|
ret, err := result.ToJson()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Could not serialize result: %v", err)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func fileModifyContents(path string, startLine int, stopLine int, content string) api.CallResult {
|
||||||
|
ok, reason := toolutil.IsPathWithinCWD(path)
|
||||||
|
if !ok {
|
||||||
|
return api.CallResult{Message: reason}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the existing file's content
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}
|
||||||
|
}
|
||||||
|
_, err = os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return api.CallResult{Message: fmt.Sprintf("Could not create new file: %s", err.Error())}
|
||||||
|
}
|
||||||
|
data = []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
contentLines := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
|
||||||
|
|
||||||
|
// If both start and stop are unset, update the entire file
|
||||||
|
if startLine == 0 && stopLine == 0 {
|
||||||
|
lines = contentLines
|
||||||
|
} else {
|
||||||
|
if startLine < 1 {
|
||||||
|
startLine = 1
|
||||||
|
}
|
||||||
|
if stopLine == 0 || stopLine > len(lines) {
|
||||||
|
stopLine = len(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
before := lines[:startLine-1]
|
||||||
|
after := lines[stopLine:]
|
||||||
|
|
||||||
|
// Handle insertion case
|
||||||
|
if startLine == stopLine {
|
||||||
|
lines = append(before, append(contentLines, lines[startLine-1:]...)...)
|
||||||
|
} else {
|
||||||
|
// If content is omitted, remove the specified range
|
||||||
|
if content == "" {
|
||||||
|
lines = append(before, after...)
|
||||||
|
} else {
|
||||||
|
lines = append(before, append(contentLines, after...)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent := strings.Join(lines, "\n")
|
||||||
|
|
||||||
|
// Write back to the file
|
||||||
|
err = os.WriteFile(path, []byte(newContent), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return api.CallResult{Message: fmt.Sprintf("Could not write to path: %s", err.Error())}
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.CallResult{Result: util.AddLineNumbers(newContent)}
|
||||||
|
}
|
@ -3,7 +3,6 @@ package toolbox
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
toolutil "git.mlow.ca/mlow/lmcli/pkg/agents/toolbox/util"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||||
@ -60,14 +59,7 @@ func readFile(path string) api.CallResult {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}
|
return api.CallResult{Message: fmt.Sprintf("Could not read path: %s", err.Error())}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
content := strings.Builder{}
|
|
||||||
for i, line := range lines {
|
|
||||||
content.WriteString(fmt.Sprintf("%d\t%s\n", i+1, line))
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.CallResult{
|
return api.CallResult{
|
||||||
Result: content.String(),
|
Result: toolutil.AddLineNumbers(string(data)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,3 +65,14 @@ func IsPathWithinCWD(path string) (bool, string) {
|
|||||||
}
|
}
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddLineNumbers takes a string of content and returns a new string with line
|
||||||
|
// numbers prefixed
|
||||||
|
func AddLineNumbers(content string) string {
|
||||||
|
lines := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
|
||||||
|
result := strings.Builder{}
|
||||||
|
for i, line := range lines {
|
||||||
|
result.WriteString(fmt.Sprintf("%d\t%s\n", i+1, line))
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
@ -11,9 +11,8 @@ var AvailableTools map[string]api.ToolSpec = map[string]api.ToolSpec{
|
|||||||
"dir_tree": toolbox.DirTreeTool,
|
"dir_tree": toolbox.DirTreeTool,
|
||||||
"read_dir": toolbox.ReadDirTool,
|
"read_dir": toolbox.ReadDirTool,
|
||||||
"read_file": toolbox.ReadFileTool,
|
"read_file": toolbox.ReadFileTool,
|
||||||
|
"modify_file": toolbox.ModifyFile,
|
||||||
"write_file": toolbox.WriteFileTool,
|
"write_file": toolbox.WriteFileTool,
|
||||||
"file_insert_lines": toolbox.FileInsertLinesTool,
|
|
||||||
"file_replace_lines": toolbox.FileReplaceLinesTool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExecuteToolCalls(calls []api.ToolCall, available []api.ToolSpec) ([]api.ToolResult, error) {
|
func ExecuteToolCalls(calls []api.ToolCall, available []api.ToolSpec) ([]api.ToolResult, error) {
|
||||||
|
67
pkg/tui/bubbles/confirmprompt.go
Normal file
67
pkg/tui/bubbles/confirmprompt.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package bubbles
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfirmPrompt struct {
|
||||||
|
Question string
|
||||||
|
Style lipgloss.Style
|
||||||
|
Payload interface{}
|
||||||
|
value bool
|
||||||
|
answered bool
|
||||||
|
focused bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfirmPrompt(question string, payload interface{}) ConfirmPrompt {
|
||||||
|
return ConfirmPrompt{
|
||||||
|
Question: question,
|
||||||
|
Style: lipgloss.NewStyle(),
|
||||||
|
Payload: payload,
|
||||||
|
focused: true, // focus by default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MsgConfirmPromptAnswered struct {
|
||||||
|
Value bool
|
||||||
|
Payload interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b ConfirmPrompt) Update(msg tea.Msg) (ConfirmPrompt, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
if !b.focused || b.answered {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
switch msg.String() {
|
||||||
|
case "y", "Y":
|
||||||
|
b.value = true
|
||||||
|
b.answered = true
|
||||||
|
b.focused = false
|
||||||
|
return b, func() tea.Msg { return MsgConfirmPromptAnswered{true, b.Payload} }
|
||||||
|
case "n", "N", "esc":
|
||||||
|
b.value = false
|
||||||
|
b.answered = true
|
||||||
|
b.focused = false
|
||||||
|
return b, func() tea.Msg { return MsgConfirmPromptAnswered{false, b.Payload} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b ConfirmPrompt) View() string {
|
||||||
|
return b.Style.Render(b.Question) + lipgloss.NewStyle().Faint(true).Render(" (y/n)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ConfirmPrompt) Focus() {
|
||||||
|
b.focused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ConfirmPrompt) Blur() {
|
||||||
|
b.focused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b ConfirmPrompt) Focused() bool {
|
||||||
|
return b.focused
|
||||||
|
}
|
@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||||
|
"git.mlow.ca/mlow/lmcli/pkg/tui/bubbles"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/styles"
|
"git.mlow.ca/mlow/lmcli/pkg/tui/styles"
|
||||||
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
|
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
|
||||||
@ -25,6 +26,13 @@ type (
|
|||||||
msgConversationsLoaded ([]loadedConversation)
|
msgConversationsLoaded ([]loadedConversation)
|
||||||
// sent when a conversation is selected
|
// sent when a conversation is selected
|
||||||
msgConversationSelected api.Conversation
|
msgConversationSelected api.Conversation
|
||||||
|
// sent when a conversation is deleted
|
||||||
|
msgConversationDeleted struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prompt payloads
|
||||||
|
type (
|
||||||
|
deleteConversationPayload api.Conversation
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
@ -36,6 +44,8 @@ type Model struct {
|
|||||||
itemOffsets []int // keeps track of the viewport y offset of each rendered item
|
itemOffsets []int // keeps track of the viewport y offset of each rendered item
|
||||||
|
|
||||||
content viewport.Model
|
content viewport.Model
|
||||||
|
|
||||||
|
confirmPrompt bubbles.ConfirmPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
func Conversations(shared shared.Shared) Model {
|
func Conversations(shared shared.Shared) Model {
|
||||||
@ -47,6 +57,14 @@ func Conversations(shared shared.Shared) Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||||
|
if m.confirmPrompt.Focused() {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.confirmPrompt, cmd = m.confirmPrompt.Update(msg)
|
||||||
|
if cmd != nil {
|
||||||
|
return true, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
if len(m.conversations) > 0 && m.cursor < len(m.conversations) {
|
if len(m.conversations) > 0 && m.cursor < len(m.conversations) {
|
||||||
@ -89,7 +107,20 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
case "n":
|
case "n":
|
||||||
// new conversation
|
// new conversation
|
||||||
case "d":
|
case "d":
|
||||||
// show prompt to delete conversation
|
if !m.confirmPrompt.Focused() && len(m.conversations) > 0 && m.cursor < len(m.conversations) {
|
||||||
|
title := m.conversations[m.cursor].conv.Title
|
||||||
|
if title == "" {
|
||||||
|
title = "(untitled)"
|
||||||
|
}
|
||||||
|
m.confirmPrompt = bubbles.NewConfirmPrompt(
|
||||||
|
fmt.Sprintf("Delete '%s'?", title),
|
||||||
|
deleteConversationPayload(m.conversations[m.cursor].conv),
|
||||||
|
)
|
||||||
|
m.confirmPrompt.Style = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("3"))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
case "c":
|
case "c":
|
||||||
// copy/clone conversation
|
// copy/clone conversation
|
||||||
case "r":
|
case "r":
|
||||||
@ -120,12 +151,23 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
m.content.SetContent(m.renderConversationList())
|
m.content.SetContent(m.renderConversationList())
|
||||||
case msgConversationsLoaded:
|
case msgConversationsLoaded:
|
||||||
m.conversations = msg
|
m.conversations = msg
|
||||||
|
m.cursor = max(0, min(len(m.conversations), m.cursor))
|
||||||
m.content.SetContent(m.renderConversationList())
|
m.content.SetContent(m.renderConversationList())
|
||||||
case msgConversationSelected:
|
case msgConversationSelected:
|
||||||
m.Values.ConvShortname = msg.ShortName.String
|
m.Values.ConvShortname = msg.ShortName.String
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
return shared.MsgViewChange(shared.StateChat)
|
return shared.MsgViewChange(shared.StateChat)
|
||||||
})
|
})
|
||||||
|
case bubbles.MsgConfirmPromptAnswered:
|
||||||
|
m.confirmPrompt.Blur()
|
||||||
|
if msg.Value {
|
||||||
|
switch payload := msg.Payload.(type) {
|
||||||
|
case deleteConversationPayload:
|
||||||
|
cmds = append(cmds, m.deleteConversation(api.Conversation(payload)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case msgConversationDeleted:
|
||||||
|
cmds = append(cmds, m.loadConversations())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
@ -135,8 +177,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.Width > 0 {
|
if m.Width > 0 {
|
||||||
|
wrap := lipgloss.NewStyle().Width(m.Width)
|
||||||
m.Header = m.headerView()
|
m.Header = m.headerView()
|
||||||
m.Footer = "" // TODO: show /something/
|
m.Footer = "" // TODO: "Press ? for help"
|
||||||
|
if m.confirmPrompt.Focused() {
|
||||||
|
m.Footer = wrap.Render(m.confirmPrompt.View())
|
||||||
|
}
|
||||||
m.Error = tuiutil.ErrorBanner(m.Err, m.Width)
|
m.Error = tuiutil.ErrorBanner(m.Err, m.Width)
|
||||||
fixedHeight := tuiutil.Height(m.Header) + tuiutil.Height(m.Error) + tuiutil.Height(m.Footer)
|
fixedHeight := tuiutil.Height(m.Header) + tuiutil.Height(m.Error) + tuiutil.Height(m.Footer)
|
||||||
m.content.Height = m.Height - fixedHeight
|
m.content.Height = m.Height - fixedHeight
|
||||||
@ -162,6 +208,16 @@ func (m *Model) loadConversations() tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) deleteConversation(conv api.Conversation) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
err := m.Ctx.Store.DeleteConversation(&conv)
|
||||||
|
if err != nil {
|
||||||
|
return shared.MsgError(fmt.Errorf("Could not delete conversation: %v", err))
|
||||||
|
}
|
||||||
|
return msgConversationDeleted{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
if m.Width == 0 {
|
if m.Width == 0 {
|
||||||
return ""
|
return ""
|
||||||
|
Loading…
Reference in New Issue
Block a user