From 9c875e40231b400e3cd366e511f70d67d432fdb9 Mon Sep 17 00:00:00 2001 From: tauraamui Date: Wed, 15 Oct 2025 14:46:48 +0100 Subject: [PATCH] tidy: remove unwanted old content --- .../tui/components/chat/editor/editor2.go2 | 610 ------------------ internal/tui/components/chat/editor/keys.go2 | 81 --- 2 files changed, 691 deletions(-) delete mode 100644 internal/tui/components/chat/editor/editor2.go2 delete mode 100644 internal/tui/components/chat/editor/keys.go2 diff --git a/internal/tui/components/chat/editor/editor2.go2 b/internal/tui/components/chat/editor/editor2.go2 deleted file mode 100644 index 9866597dc2916ba458ea5570956e43dda479c381..0000000000000000000000000000000000000000 --- a/internal/tui/components/chat/editor/editor2.go2 +++ /dev/null @@ -1,610 +0,0 @@ -package editor - -import ( - "context" - "fmt" - "math/rand" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "slices" - "strings" - "unicode" - - "github.com/charmbracelet/bubbles/v2/key" - "github.com/charmbracelet/bubbles/v2/textarea" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/app" - "github.com/charmbracelet/crush/internal/fsext" - "github.com/charmbracelet/crush/internal/message" - "github.com/charmbracelet/crush/internal/session" - "github.com/charmbracelet/crush/internal/tui/components/chat" - "github.com/charmbracelet/crush/internal/tui/components/completions" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" - "github.com/charmbracelet/crush/internal/tui/components/dialogs" - "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" - "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker" - "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit" - "github.com/charmbracelet/crush/internal/tui/styles" - "github.com/charmbracelet/crush/internal/tui/util" - "github.com/charmbracelet/lipgloss/v2" -) - -type Editor interface { - util.Model - layout.Sizeable - layout.Focusable - layout.Help - layout.Positional - - SetSession(session session.Session) tea.Cmd - IsCompletionsOpen() bool - HasAttachments() bool - Cursor() *tea.Cursor -} - -type FileCompletionItem struct { - Path string // The file path -} - -type editorCmp struct { - width int - height int - x, y int - app *app.App - session session.Session - textarea textarea.Model - attachments []message.Attachment - deleteMode bool - readyPlaceholder string - workingPlaceholder string - - keyMap EditorKeyMap - - // File path completions - currentQuery string - completionsStartIndex int - isCompletionsOpen bool - - // History - promptHistoryIndex int -} - -var DeleteKeyMaps = DeleteAttachmentKeyMaps{ - AttachmentDeleteMode: key.NewBinding( - key.WithKeys("ctrl+r"), - key.WithHelp("ctrl+r+{i}", "delete attachment at index i"), - ), - Escape: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "cancel delete mode"), - ), - DeleteAllAttachments: key.NewBinding( - key.WithKeys("r"), - key.WithHelp("ctrl+r+r", "delete all attachments"), - ), -} - -const ( - maxAttachments = 5 -) - -type OpenEditorMsg struct { - Text string -} - -func (m *editorCmp) openEditor(value string) tea.Cmd { - editor := os.Getenv("EDITOR") - if editor == "" { - // Use platform-appropriate default editor - if runtime.GOOS == "windows" { - editor = "notepad" - } else { - editor = "nvim" - } - } - - tmpfile, err := os.CreateTemp("", "msg_*.md") - if err != nil { - return util.ReportError(err) - } - defer tmpfile.Close() //nolint:errcheck - if _, err := tmpfile.WriteString(value); err != nil { - return util.ReportError(err) - } - c := exec.CommandContext(context.TODO(), editor, tmpfile.Name()) - c.Stdin = os.Stdin - c.Stdout = os.Stdout - c.Stderr = os.Stderr - return tea.ExecProcess(c, func(err error) tea.Msg { - if err != nil { - return util.ReportError(err) - } - content, err := os.ReadFile(tmpfile.Name()) - if err != nil { - return util.ReportError(err) - } - if len(content) == 0 { - return util.ReportWarn("Message is empty") - } - os.Remove(tmpfile.Name()) - return OpenEditorMsg{ - Text: strings.TrimSpace(string(content)), - } - }) -} - -func (m *editorCmp) Init() tea.Cmd { - return nil -} - -func (m *editorCmp) send() tea.Cmd { - if m.app.CoderAgent == nil { - return util.ReportError(fmt.Errorf("coder agent is not initialized")) - } - if m.app.CoderAgent.IsSessionBusy(m.session.ID) { - return util.ReportWarn("Agent is working, please wait...") - } - - value := m.textarea.Value() - value = strings.TrimSpace(value) - - switch value { - case "exit", "quit": - m.textarea.Reset() - return util.CmdHandler(dialogs.OpenDialogMsg{Model: quit.NewQuitDialog()}) - } - - m.textarea.Reset() - attachments := m.attachments - - m.attachments = nil - if value == "" { - return nil - } - - // Change the placeholder when sending a new message. - m.randomizePlaceholders() - - return tea.Batch( - util.CmdHandler(chat.SendMsg{ - Text: value, - Attachments: attachments, - }), - ) -} - -func (m *editorCmp) repositionCompletions() tea.Msg { - x, y := m.completionsPosition() - return completions.RepositionCompletionsMsg{X: x, Y: y} -} - -func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - var cmds []tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - return m, m.repositionCompletions - case filepicker.FilePickedMsg: - if len(m.attachments) >= maxAttachments { - return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments)) - } - m.attachments = append(m.attachments, msg.Attachment) - return m, nil - case completions.CompletionsOpenedMsg: - m.isCompletionsOpen = true - case completions.CompletionsClosedMsg: - m.isCompletionsOpen = false - m.currentQuery = "" - m.completionsStartIndex = 0 - case completions.SelectCompletionMsg: - if !m.isCompletionsOpen { - return m, nil - } - if item, ok := msg.Value.(FileCompletionItem); ok { - word := m.textarea.Word() - // If the selected item is a file, insert its path into the textarea - value := m.textarea.Value() - value = value[:m.completionsStartIndex] + // Remove the current query - item.Path + // Insert the file path - value[m.completionsStartIndex+len(word):] // Append the rest of the value - // XXX: This will always move the cursor to the end of the textarea. - m.textarea.SetValue(value) - m.textarea.MoveToEnd() - if !msg.Insert { - m.isCompletionsOpen = false - m.currentQuery = "" - m.completionsStartIndex = 0 - } - } - - case commands.OpenExternalEditorMsg: - if m.app.CoderAgent.IsSessionBusy(m.session.ID) { - return m, util.ReportWarn("Agent is working, please wait...") - } - return m, m.openEditor(m.textarea.Value()) - case OpenEditorMsg: - m.textarea.SetValue(msg.Text) - m.textarea.MoveToEnd() - case tea.PasteMsg: - path := strings.ReplaceAll(string(msg), "\\ ", " ") - // try to get an image - path, err := filepath.Abs(path) - if err != nil { - m.textarea, cmd = m.textarea.Update(msg) - return m, cmd - } - isAllowedType := false - for _, ext := range filepicker.AllowedTypes { - if strings.HasSuffix(path, ext) { - isAllowedType = true - break - } - } - if !isAllowedType { - m.textarea, cmd = m.textarea.Update(msg) - return m, cmd - } - tooBig, _ := filepicker.IsFileTooBig(path, filepicker.MaxAttachmentSize) - if tooBig { - m.textarea, cmd = m.textarea.Update(msg) - return m, cmd - } - - content, err := os.ReadFile(path) - if err != nil { - m.textarea, cmd = m.textarea.Update(msg) - return m, cmd - } - mimeBufferSize := min(512, len(content)) - mimeType := http.DetectContentType(content[:mimeBufferSize]) - fileName := filepath.Base(path) - attachment := message.Attachment{FilePath: path, FileName: fileName, MimeType: mimeType, Content: content} - return m, util.CmdHandler(filepicker.FilePickedMsg{ - Attachment: attachment, - }) - - case tea.KeyPressMsg: - cur := m.textarea.Cursor() - curIdx := m.textarea.Width()*cur.Y + cur.X - switch { - // Completions - case msg.String() == "/" && !m.isCompletionsOpen && - // only show if beginning of prompt, or if previous char is a space or newline: - (len(m.textarea.Value()) == 0 || unicode.IsSpace(rune(m.textarea.Value()[len(m.textarea.Value())-1]))): - m.isCompletionsOpen = true - m.currentQuery = "" - m.completionsStartIndex = curIdx - cmds = append(cmds, m.startCompletions) - case m.isCompletionsOpen && curIdx <= m.completionsStartIndex: - cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) - } - if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) { - m.deleteMode = true - return m, nil - } - if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode { - m.deleteMode = false - m.attachments = nil - return m, nil - } - rune := msg.Code - if m.deleteMode && unicode.IsDigit(rune) { - num := int(rune - '0') - m.deleteMode = false - if num < 10 && len(m.attachments) > num { - if num == 0 { - m.attachments = m.attachments[num+1:] - } else { - m.attachments = slices.Delete(m.attachments, num, num+1) - } - return m, nil - } - } - if key.Matches(msg, m.keyMap.OpenEditor) { - if m.app.CoderAgent.IsSessionBusy(m.session.ID) { - return m, util.ReportWarn("Agent is working, please wait...") - } - return m, m.openEditor(m.textarea.Value()) - } - if key.Matches(msg, DeleteKeyMaps.Escape) { - m.deleteMode = false - return m, nil - } - if key.Matches(msg, m.keyMap.Newline) { - m.textarea.InsertRune('\n') - cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) - } - // History - if key.Matches(msg, m.keyMap.Previous) || key.Matches(msg, m.keyMap.Next) { - m.textarea.SetValue(m.handleMessageHistory(msg)) - } - // Handle Enter key - if m.textarea.Focused() && key.Matches(msg, m.keyMap.SendMessage) { - value := m.textarea.Value() - if len(value) > 0 && value[len(value)-1] == '\\' { - // If the last character is a backslash, remove it and add a newline - m.textarea.SetValue(value[:len(value)-1]) - } else { - // Otherwise, send the message - return m, m.send() - } - } - } - - m.textarea, cmd = m.textarea.Update(msg) - cmds = append(cmds, cmd) - - if m.textarea.Focused() { - kp, ok := msg.(tea.KeyPressMsg) - if ok { - if kp.String() == "space" || m.textarea.Value() == "" { - m.isCompletionsOpen = false - m.currentQuery = "" - m.completionsStartIndex = 0 - cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) - } else { - word := m.textarea.Word() - if strings.HasPrefix(word, "/") { - // XXX: wont' work if editing in the middle of the field. - m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word) - m.currentQuery = word[1:] - x, y := m.completionsPosition() - x -= len(m.currentQuery) - m.isCompletionsOpen = true - cmds = append(cmds, - util.CmdHandler(completions.FilterCompletionsMsg{ - Query: m.currentQuery, - Reopen: m.isCompletionsOpen, - X: x, - Y: y, - }), - ) - } else if m.isCompletionsOpen { - m.isCompletionsOpen = false - m.currentQuery = "" - m.completionsStartIndex = 0 - cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) - } - } - } - } - - return m, tea.Batch(cmds...) -} - -func (m *editorCmp) completionsPosition() (int, int) { - cur := m.textarea.Cursor() - if cur == nil { - return m.x, m.y + 1 // adjust for padding - } - x := cur.X + m.x - y := cur.Y + m.y + 1 // adjust for padding - return x, y -} - -func (m *editorCmp) Cursor() *tea.Cursor { - cursor := m.textarea.Cursor() - if cursor != nil { - cursor.X = cursor.X + m.x + 1 - cursor.Y = cursor.Y + m.y + 1 // adjust for padding - } - return cursor -} - -var readyPlaceholders = [...]string{ - "Ready!", - "Ready...", - "Ready?", - "Ready for instructions", -} - -var workingPlaceholders = [...]string{ - "Working!", - "Working...", - "Brrrrr...", - "Prrrrrrrr...", - "Processing...", - "Thinking...", -} - -func (m *editorCmp) randomizePlaceholders() { - m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))] - m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))] -} - -func (m *editorCmp) View() string { - t := styles.CurrentTheme() - // Update placeholder - if m.app.CoderAgent != nil && m.app.CoderAgent.IsBusy() { - m.textarea.Placeholder = m.workingPlaceholder - } else { - m.textarea.Placeholder = m.readyPlaceholder - } - if len(m.attachments) == 0 { - content := t.S().Base.Padding(1).Render( - m.textarea.View(), - ) - return content - } - content := t.S().Base.Padding(0, 1, 1, 1).Render( - lipgloss.JoinVertical(lipgloss.Top, - m.attachmentsContent(), - m.textarea.View(), - ), - ) - return content -} - -func (m *editorCmp) SetSize(width, height int) tea.Cmd { - m.width = width - m.height = height - m.textarea.SetWidth(width - 2) // adjust for padding - m.textarea.SetHeight(height - 2) // adjust for padding - return nil -} - -func (m *editorCmp) GetSize() (int, int) { - return m.textarea.Width(), m.textarea.Height() -} - -func (m *editorCmp) attachmentsContent() string { - var styledAttachments []string - t := styles.CurrentTheme() - attachmentStyles := t.S().Base. - MarginLeft(1). - Background(t.FgMuted). - Foreground(t.FgBase) - for i, attachment := range m.attachments { - var filename string - if len(attachment.FileName) > 10 { - filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7]) - } else { - filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName) - } - if m.deleteMode { - filename = fmt.Sprintf("%d%s", i, filename) - } - styledAttachments = append(styledAttachments, attachmentStyles.Render(filename)) - } - content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...) - return content -} - -func (m *editorCmp) SetPosition(x, y int) tea.Cmd { - m.x = x - m.y = y - return nil -} - -func (m *editorCmp) startCompletions() tea.Msg { - files, _, _ := fsext.ListDirectory(".", []string{}, 0) - completionItems := make([]completions.Completion, 0, len(files)) - for _, file := range files { - file = strings.TrimPrefix(file, "./") - completionItems = append(completionItems, completions.Completion{ - Title: file, - Value: FileCompletionItem{ - Path: file, - }, - }) - } - - x, y := m.completionsPosition() - return completions.OpenCompletionsMsg{ - Completions: completionItems, - X: x, - Y: y, - } -} - -// Blur implements Container. -func (c *editorCmp) Blur() tea.Cmd { - c.textarea.Blur() - return nil -} - -// Focus implements Container. -func (c *editorCmp) Focus() tea.Cmd { - return c.textarea.Focus() -} - -// IsFocused implements Container. -func (c *editorCmp) IsFocused() bool { - return c.textarea.Focused() -} - -// Bindings implements Container. -func (c *editorCmp) Bindings() []key.Binding { - return c.keyMap.KeyBindings() -} - -// TODO: most likely we do not need to have the session here -// we need to move some functionality to the page level -func (c *editorCmp) SetSession(session session.Session) tea.Cmd { - c.session = session - return nil -} - -func (c *editorCmp) IsCompletionsOpen() bool { - return c.isCompletionsOpen -} - -func (c *editorCmp) HasAttachments() bool { - return len(c.attachments) > 0 -} - -func New(app *app.App) Editor { - t := styles.CurrentTheme() - ta := textarea.New() - ta.SetStyles(t.S().TextArea) - ta.SetPromptFunc(4, func(info textarea.PromptInfo) string { - if info.LineNumber == 0 { - return " > " - } - if info.Focused { - return t.S().Base.Foreground(t.GreenDark).Render("::: ") - } else { - return t.S().Muted.Render("::: ") - } - }) - ta.ShowLineNumbers = false - ta.CharLimit = -1 - ta.SetVirtualCursor(false) - ta.Focus() - - e := &editorCmp{ - // TODO: remove the app instance from here - app: app, - textarea: ta, - keyMap: DefaultEditorKeyMap(), - } - - e.randomizePlaceholders() - e.textarea.Placeholder = e.readyPlaceholder - - return e -} - -func (m *editorCmp) getUserMessagesAsText(ctx context.Context) ([]string, error) { - allMessages, err := m.app.Messages.List(ctx, m.session.ID) - if err != nil { - return nil, err - } - - var userMessages []string - for _, msg := range allMessages { - if msg.Role == message.User { - userMessages = append(userMessages, msg.Content().Text) - } - } - return userMessages, nil -} - -func (m *editorCmp) handleMessageHistory(msg tea.KeyMsg) string { - ctx := context.Background() - userMessages, err := m.getUserMessagesAsText(ctx) - if err != nil { - return "" // Do nothing. - } - userMessages = append(userMessages, "") // Give the user a reset option. - if len(userMessages) > 0 { - if key.Matches(msg, m.keyMap.Previous) { - if m.promptHistoryIndex == 0 { - m.promptHistoryIndex = len(userMessages) - 1 - } else { - m.promptHistoryIndex -= 1 - } - } - if key.Matches(msg, m.keyMap.Next) { - if m.promptHistoryIndex == len(userMessages)-1 { - m.promptHistoryIndex = 0 - } else { - m.promptHistoryIndex += 1 - } - } - } - return userMessages[m.promptHistoryIndex] -} diff --git a/internal/tui/components/chat/editor/keys.go2 b/internal/tui/components/chat/editor/keys.go2 deleted file mode 100644 index a2f506d716ff71b8967fa9f67ec00f8ad9ff50c1..0000000000000000000000000000000000000000 --- a/internal/tui/components/chat/editor/keys.go2 +++ /dev/null @@ -1,81 +0,0 @@ -package editor - -import ( - "github.com/charmbracelet/bubbles/v2/key" -) - -type EditorKeyMap struct { - AddFile key.Binding - SendMessage key.Binding - OpenEditor key.Binding - Newline key.Binding - Next key.Binding - Previous key.Binding -} - -func DefaultEditorKeyMap() EditorKeyMap { - return EditorKeyMap{ - AddFile: key.NewBinding( - key.WithKeys("/"), - key.WithHelp("/", "add file"), - ), - SendMessage: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "send"), - ), - OpenEditor: key.NewBinding( - key.WithKeys("ctrl+o"), - key.WithHelp("ctrl+o", "open editor"), - ), - Newline: key.NewBinding( - key.WithKeys("shift+enter", "ctrl+j"), - // "ctrl+j" is a common keybinding for newline in many editors. If - // the terminal supports "shift+enter", we substitute the help text - // to reflect that. - key.WithHelp("ctrl+j", "newline"), - ), - Next: key.NewBinding( - key.WithKeys("shift+down"), - key.WithHelp("shift+↓", "down"), - ), - Previous: key.NewBinding( - key.WithKeys("shift+up"), - key.WithHelp("shift+↑", "up"), - ), - } -} - -// KeyBindings implements layout.KeyMapProvider -func (k EditorKeyMap) KeyBindings() []key.Binding { - return []key.Binding{ - k.AddFile, - k.SendMessage, - k.OpenEditor, - k.Newline, - AttachmentsKeyMaps.AttachmentDeleteMode, - AttachmentsKeyMaps.DeleteAllAttachments, - AttachmentsKeyMaps.Escape, - } -} - -type DeleteAttachmentKeyMaps struct { - AttachmentDeleteMode key.Binding - Escape key.Binding - DeleteAllAttachments key.Binding -} - -// TODO: update this to use the new keymap concepts -var AttachmentsKeyMaps = DeleteAttachmentKeyMaps{ - AttachmentDeleteMode: key.NewBinding( - key.WithKeys("ctrl+r"), - key.WithHelp("ctrl+r+{i}", "delete attachment at index i"), - ), - Escape: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "cancel delete mode"), - ), - DeleteAllAttachments: key.NewBinding( - key.WithKeys("r"), - key.WithHelp("ctrl+r+r", "delete all attachments"), - ), -}