mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
feat(ui): open external $EDITOR via ctrl+x e chord
- Add ctrl+x e leader key chord to open $VISUAL/$EDITOR in a temp file pre-populated with the current input text - On save & quit, replace the input textarea with the edited content - On error exit (e.g. :cq in vim), silently preserve original input - Use charmbracelet/x/editor for editor command construction - Use tea.ExecProcess to suspend/resume the TUI around the editor - Update input hint text at all width breakpoints to show the shortcut - Add ctrl+x e to /help output Closes #5
This commit is contained in:
@@ -15,6 +15,7 @@ require (
|
||||
github.com/charmbracelet/log v1.0.0
|
||||
github.com/charmbracelet/openai-go v0.0.0-20260319145158-d0740cc34266
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260414011438-8c69ec811b1e
|
||||
github.com/charmbracelet/x/editor v0.2.0
|
||||
github.com/clipperhouse/displaywidth v0.11.0
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0
|
||||
github.com/coder/acp-go-sdk v0.6.3
|
||||
|
||||
@@ -94,6 +94,8 @@ github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMx
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||
github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIRg8gGWwk=
|
||||
github.com/charmbracelet/x/editor v0.2.0/go.mod h1:p3oQ28TSL3YPd+GKJ1fHWcp+7bVGpedHpXmo0D6t1dY=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260413165052-6921c759c913 h1:6F/6bu5nBLjodsvaU5xAszTaxtHrDU5UiJarpMPZj48=
|
||||
|
||||
@@ -521,12 +521,14 @@ func (s *InputComponent) View() tea.View {
|
||||
} else {
|
||||
hint = "^X s steer"
|
||||
}
|
||||
} else if availableHintWidth >= 80 {
|
||||
hint = "enter submit • ctrl+j / shift+enter new line • ctrl+x e editor • ctrl+v paste image"
|
||||
} else if availableHintWidth >= 67 {
|
||||
hint = "enter submit • ctrl+j / shift+enter new line • ctrl+v paste image"
|
||||
hint = "enter submit • ctrl+j new line • ctrl+x e editor • ctrl+v image"
|
||||
} else if availableHintWidth >= 40 {
|
||||
hint = "↵ submit • ctrl+j newline • ctrl+v image"
|
||||
hint = "↵ submit • ctrl+j newline • ^X e editor"
|
||||
} else if availableHintWidth >= 20 {
|
||||
hint = "↵ submit • ctrl+j"
|
||||
hint = "↵ submit • ^X e editor"
|
||||
} else {
|
||||
hint = "↵ submit"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/editor"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mark3labs/kit/internal/app"
|
||||
@@ -1333,6 +1334,45 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
}
|
||||
case "e":
|
||||
// Ctrl+X e → open $EDITOR to compose/edit the prompt.
|
||||
editorApp := os.Getenv("VISUAL")
|
||||
if editorApp == "" {
|
||||
editorApp = os.Getenv("EDITOR")
|
||||
}
|
||||
if editorApp == "" {
|
||||
m.printSystemMessage("Set `$EDITOR` or `$VISUAL` to use external editor")
|
||||
} else {
|
||||
var currentText string
|
||||
if ic, ok := m.input.(*InputComponent); ok {
|
||||
currentText = ic.textarea.Value()
|
||||
}
|
||||
tmpFile, err := os.CreateTemp("", "kit_prompt_*.md")
|
||||
if err == nil {
|
||||
if currentText != "" {
|
||||
_, _ = tmpFile.WriteString(currentText)
|
||||
}
|
||||
_ = tmpFile.Close()
|
||||
editorCmd, cmdErr := editor.Command(editorApp, tmpFile.Name())
|
||||
if cmdErr != nil {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
m.printSystemMessage(fmt.Sprintf("Failed to open editor: %v", cmdErr))
|
||||
} else {
|
||||
cmds = append(cmds, tea.ExecProcess(editorCmd, func(err error) tea.Msg {
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
return externalEditorMsg{err: err}
|
||||
}
|
||||
content, readErr := os.ReadFile(tmpFile.Name())
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
if readErr != nil {
|
||||
return externalEditorMsg{err: readErr}
|
||||
}
|
||||
return externalEditorMsg{text: string(content)}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Chord consumed — don't propagate to children.
|
||||
return m, tea.Batch(cmds...)
|
||||
@@ -1973,6 +2013,19 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.printSystemMessage(msg.output)
|
||||
}
|
||||
|
||||
case externalEditorMsg:
|
||||
// User returned from $EDITOR. Replace input textarea content with
|
||||
// whatever they saved in the temp file. On error (e.g. :cq in vim)
|
||||
// the original input is silently preserved.
|
||||
if msg.err == nil {
|
||||
if ic, ok := m.input.(*InputComponent); ok {
|
||||
ic.textarea.SetValue(msg.text)
|
||||
// Move cursor to the end of the inserted text.
|
||||
ic.textarea.CursorEnd()
|
||||
}
|
||||
m.layoutDirty = true
|
||||
}
|
||||
|
||||
case extReloadResultMsg:
|
||||
if msg.err != nil {
|
||||
m.printSystemMessage(fmt.Sprintf("Extension reload failed: %v", msg.err))
|
||||
@@ -2967,6 +3020,7 @@ func (m *AppModel) printHelpMessage() {
|
||||
"- `Ctrl+C`: Exit at any time\n" +
|
||||
"- `ESC` (x2): Cancel ongoing LLM generation\n" +
|
||||
"- `Ctrl+X s`: Steer — redirect the agent mid-turn (injected between tool calls)\n" +
|
||||
"- `Ctrl+X e`: Open `$EDITOR` to compose/edit your prompt\n" +
|
||||
"- `Enter` (while working): Queue message for after the agent finishes\n\n" +
|
||||
"You can also just type your message to chat with the AI assistant."
|
||||
m.printSystemMessage(help)
|
||||
@@ -4054,6 +4108,13 @@ func cancelTimerCmd() tea.Cmd {
|
||||
// Interactive prompt support
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// externalEditorMsg is sent when the user returns from $EDITOR after
|
||||
// composing a prompt via the Ctrl+X e chord.
|
||||
type externalEditorMsg struct {
|
||||
text string
|
||||
err error
|
||||
}
|
||||
|
||||
// shareResultMsg carries the result of an async gist upload.
|
||||
type shareResultMsg struct {
|
||||
err error
|
||||
|
||||
Reference in New Issue
Block a user