From cd8411691d65347df262da09ce541c154051eabe Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Sat, 5 Jul 2025 11:11:46 +0300 Subject: [PATCH] fix: preserve textarea content when external editor fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only clear textarea when external editor exits successfully, rather than clearing immediately and restoring on failure. This simpler approach prevents content loss when the editor exits with non-zero status. 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode --- .../tui/internal/components/chat/editor.go | 5 --- packages/tui/internal/tui/tui.go | 41 ++++++++++--------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 3213f44b..9466d541 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -27,7 +27,6 @@ type EditorComponent interface { Content(width int) string Lines() int Value() string - SetValue(value string) Focused() bool Focus() (tea.Model, tea.Cmd) Blur() @@ -231,10 +230,6 @@ func (m *editorComponent) Value() string { return m.textarea.Value() } -func (m *editorComponent) SetValue(value string) { - m.textarea.SetValue(value) -} - func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { value := strings.TrimSpace(m.Value()) if value == "" { diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 38a24891..b57f8a72 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -32,9 +32,10 @@ import ( // InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires type InterruptDebounceTimeoutMsg struct{} -// EditorRestoreMsg is sent when the external editor fails and we need to restore the original content -type EditorRestoreMsg struct { - Content string +// EditorSuccessMsg is sent when the external editor succeeds and we should clear textarea and send the message +type EditorSuccessMsg struct { + Text string + Attachments []opencode.FilePartParam } // InterruptKeyState tracks the state of interrupt key presses for debouncing @@ -491,9 +492,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Reset interrupt key state after timeout a.interruptKeyState = InterruptKeyIdle a.editor.SetInterruptKeyInDebounce(false) - case EditorRestoreMsg: - // Restore the original content to the textarea when editor fails - a.editor.SetValue(msg.Content) + case EditorSuccessMsg: + // Clear the textarea and send the message when editor succeeds + updated, cmd := a.editor.Clear() + a.editor = updated.(chat.EditorComponent) + cmds = append(cmds, cmd) + cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: msg.Text, Attachments: msg.Attachments})) case dialog.FindSelectedMsg: return a.openFile(msg.FilePath) } @@ -781,9 +785,6 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) } value := a.editor.Value() - updated, cmd := a.editor.Clear() - a.editor = updated.(chat.EditorComponent) - cmds = append(cmds, cmd) tmpfile, err := os.CreateTemp("", "msg_*.md") tmpfile.WriteString(value) @@ -803,28 +804,30 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) if err != nil { if exitError, ok := err.(*exec.ExitError); ok { slog.Warn("Editor exited with non-zero status", "exitCode", exitError.ExitCode()) - // Restore original content when editor exits unsuccessfully - return EditorRestoreMsg{Content: value} + // Don't clear textarea on failure + return nil } else { slog.Error("Failed to open editor", "error", err) - // Restore original content when editor fails to start - return EditorRestoreMsg{Content: value} + // Don't clear textarea on failure + return nil } } content, err := os.ReadFile(tmpfile.Name()) if err != nil { slog.Error("Failed to read file", "error", err) - // Restore original content when file read fails - return EditorRestoreMsg{Content: value} + // Don't clear textarea on failure + return nil } if len(content) == 0 { slog.Warn("Message is empty") - // Restore original content when message is empty - return EditorRestoreMsg{Content: value} + // Don't clear textarea on failure + return nil } - return app.SendMsg{ - Text: string(content), + // Only clear and send if everything succeeded + return EditorSuccessMsg{ + Text: string(content), + Attachments: []opencode.FilePartParam{}, // attachments, } }) cmds = append(cmds, cmd)