|
|
@@ -3,10 +3,10 @@ package tui
|
|
|
import (
|
|
|
"context"
|
|
|
"log/slog"
|
|
|
+ "os"
|
|
|
+ "os/exec"
|
|
|
|
|
|
- "github.com/charmbracelet/bubbles/v2/cursor"
|
|
|
"github.com/charmbracelet/bubbles/v2/key"
|
|
|
- "github.com/charmbracelet/bubbles/v2/spinner"
|
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
|
"github.com/charmbracelet/lipgloss/v2"
|
|
|
|
|
|
@@ -19,57 +19,34 @@ import (
|
|
|
"github.com/sst/opencode/internal/components/status"
|
|
|
"github.com/sst/opencode/internal/layout"
|
|
|
"github.com/sst/opencode/internal/styles"
|
|
|
- "github.com/sst/opencode/internal/theme"
|
|
|
"github.com/sst/opencode/internal/util"
|
|
|
"github.com/sst/opencode/pkg/client"
|
|
|
)
|
|
|
|
|
|
type appModel struct {
|
|
|
width, height int
|
|
|
- status status.StatusComponent
|
|
|
app *app.App
|
|
|
modal layout.Modal
|
|
|
- editorContainer layout.Container
|
|
|
+ status status.StatusComponent
|
|
|
editor chat.EditorComponent
|
|
|
- messagesContainer layout.Container
|
|
|
+ messages chat.MessagesComponent
|
|
|
+ editorContainer layout.Container
|
|
|
layout layout.FlexLayout
|
|
|
- completionDialog dialog.CompletionDialog
|
|
|
+ completions dialog.CompletionDialog
|
|
|
completionManager *completions.CompletionManager
|
|
|
showCompletionDialog bool
|
|
|
-}
|
|
|
-
|
|
|
-type ChatKeyMap struct {
|
|
|
- Cancel key.Binding
|
|
|
- ToggleTools key.Binding
|
|
|
- ShowCompletionDialog key.Binding
|
|
|
-}
|
|
|
-
|
|
|
-var keyMap = ChatKeyMap{
|
|
|
- Cancel: key.NewBinding(
|
|
|
- key.WithKeys("esc"),
|
|
|
- key.WithHelp("esc", "cancel"),
|
|
|
- ),
|
|
|
- ToggleTools: key.NewBinding(
|
|
|
- key.WithKeys("ctrl+h"),
|
|
|
- key.WithHelp("ctrl+h", "toggle tools"),
|
|
|
- ),
|
|
|
- ShowCompletionDialog: key.NewBinding(
|
|
|
- key.WithKeys("/"),
|
|
|
- key.WithHelp("/", "Complete"),
|
|
|
- ),
|
|
|
+ leaderBinding *key.Binding
|
|
|
+ isLeaderSequence bool
|
|
|
}
|
|
|
|
|
|
func (a appModel) Init() tea.Cmd {
|
|
|
- t := theme.CurrentTheme()
|
|
|
var cmds []tea.Cmd
|
|
|
- cmds = append(cmds, a.app.InitializeProvider())
|
|
|
-
|
|
|
- cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
|
|
|
cmds = append(cmds, tea.RequestBackgroundColor)
|
|
|
-
|
|
|
- cmds = append(cmds, a.layout.Init())
|
|
|
- cmds = append(cmds, a.completionDialog.Init())
|
|
|
+ cmds = append(cmds, a.app.InitializeProvider())
|
|
|
+ cmds = append(cmds, a.editor.Init())
|
|
|
+ cmds = append(cmds, a.messages.Init())
|
|
|
cmds = append(cmds, a.status.Init())
|
|
|
+ cmds = append(cmds, a.completions.Init())
|
|
|
|
|
|
// Check if we should show the init dialog
|
|
|
cmds = append(cmds, func() tea.Msg {
|
|
|
@@ -82,115 +59,124 @@ func (a appModel) Init() tea.Cmd {
|
|
|
|
|
|
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
var cmds []tea.Cmd
|
|
|
- var cmd tea.Cmd
|
|
|
|
|
|
- if a.modal != nil {
|
|
|
- bypassModal := false
|
|
|
-
|
|
|
- if _, ok := msg.(modal.CloseModalMsg); ok {
|
|
|
- a.modal = nil
|
|
|
- return a, nil
|
|
|
- }
|
|
|
-
|
|
|
- if msg, ok := msg.(tea.KeyMsg); ok {
|
|
|
+ switch msg := msg.(type) {
|
|
|
+ case tea.KeyPressMsg:
|
|
|
+ // 1. Handle active modal
|
|
|
+ if a.modal != nil {
|
|
|
switch msg.String() {
|
|
|
- case "esc":
|
|
|
+ // Escape always closes current modal
|
|
|
+ case "esc", "ctrl+c":
|
|
|
a.modal = nil
|
|
|
return a, nil
|
|
|
- case "ctrl+c":
|
|
|
- return a, tea.Quit
|
|
|
}
|
|
|
|
|
|
- // TODO: do we need this?
|
|
|
- // don't send commands to the modal
|
|
|
- for _, cmdDef := range a.app.Commands {
|
|
|
- if key.Matches(msg, cmdDef.KeyBinding) {
|
|
|
- bypassModal = true
|
|
|
- break
|
|
|
- }
|
|
|
+ // Pass all other key presses to the modal
|
|
|
+ updatedModal, cmd := a.modal.Update(msg)
|
|
|
+ a.modal = updatedModal.(layout.Modal)
|
|
|
+ return a, cmd
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Check for commands that require leader
|
|
|
+ if a.isLeaderSequence {
|
|
|
+ matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
|
|
|
+ // Reset leader state
|
|
|
+ a.isLeaderSequence = false
|
|
|
+ if len(matches) > 0 {
|
|
|
+ return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // thanks i hate this
|
|
|
- switch msg.(type) {
|
|
|
- case tea.WindowSizeMsg:
|
|
|
- bypassModal = true
|
|
|
- case client.EventSessionUpdated:
|
|
|
- bypassModal = true
|
|
|
- case client.EventMessageUpdated:
|
|
|
- bypassModal = true
|
|
|
- case cursor.BlinkMsg:
|
|
|
- bypassModal = true
|
|
|
- case spinner.TickMsg:
|
|
|
- bypassModal = true
|
|
|
+ // 3. Handle completions trigger
|
|
|
+ switch msg.String() {
|
|
|
+ case "/":
|
|
|
+ a.showCompletionDialog = true
|
|
|
}
|
|
|
|
|
|
- if !bypassModal {
|
|
|
- updatedModal, cmd := a.modal.Update(msg)
|
|
|
- a.modal = updatedModal.(layout.Modal)
|
|
|
- return a, cmd
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ updated, cmd := a.editor.Update(msg)
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+
|
|
|
+ currentInput := a.editor.Value()
|
|
|
+ provider := a.completionManager.GetProvider(currentInput)
|
|
|
+ a.completions.SetProvider(provider)
|
|
|
+
|
|
|
+ context, contextCmd := a.completions.Update(msg)
|
|
|
+ a.completions = context.(dialog.CompletionDialog)
|
|
|
+ cmds = append(cmds, contextCmd)
|
|
|
+ return a, tea.Batch(cmds...)
|
|
|
+
|
|
|
+ // Doesn't forward event if enter key is pressed
|
|
|
+ // if msg.String() == "enter" {
|
|
|
+ // return a, tea.Batch(cmds...)
|
|
|
+ // }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- switch msg := msg.(type) {
|
|
|
- case chat.SendMsg:
|
|
|
- a.showCompletionDialog = false
|
|
|
- cmd := a.sendMessage(msg.Text, msg.Attachments)
|
|
|
- if cmd != nil {
|
|
|
- return a, cmd
|
|
|
+ // 4. Maximize editor responsiveness for printable characters
|
|
|
+ if msg.Text != "" {
|
|
|
+ updated, cmd := a.editor.Update(msg)
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ return a, tea.Batch(cmds...)
|
|
|
}
|
|
|
- case dialog.CompletionDialogCloseMsg:
|
|
|
- a.showCompletionDialog = false
|
|
|
- case commands.ExecuteCommandMsg:
|
|
|
- switch msg.Name {
|
|
|
- case "quit":
|
|
|
- return a, tea.Quit
|
|
|
- case "new":
|
|
|
- a.app.Session = &client.SessionInfo{}
|
|
|
- a.app.Messages = []client.MessageInfo{}
|
|
|
- cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
|
|
|
- case "sessions":
|
|
|
- sessionDialog := dialog.NewSessionDialog(a.app)
|
|
|
- a.modal = sessionDialog
|
|
|
- case "model":
|
|
|
- modelDialog := dialog.NewModelDialog(a.app)
|
|
|
- a.modal = modelDialog
|
|
|
- case "theme":
|
|
|
- themeDialog := dialog.NewThemeDialog()
|
|
|
- a.modal = themeDialog
|
|
|
- case "share":
|
|
|
- a.app.Client.PostSessionShareWithResponse(context.Background(), client.PostSessionShareJSONRequestBody{
|
|
|
- SessionID: a.app.Session.Id,
|
|
|
- })
|
|
|
- case "init":
|
|
|
- return a, a.app.InitializeProject(context.Background())
|
|
|
- // case "compact":
|
|
|
- // return a, a.app.CompactSession(context.Background())
|
|
|
- case "help":
|
|
|
- var helpBindings []key.Binding
|
|
|
- for _, cmd := range a.app.Commands {
|
|
|
- // Create a new binding for help display
|
|
|
- helpBindings = append(helpBindings, key.NewBinding(
|
|
|
- key.WithKeys(cmd.KeyBinding.Keys()...),
|
|
|
- key.WithHelp("/"+cmd.Name, cmd.Description),
|
|
|
- ))
|
|
|
- }
|
|
|
- helpDialog := dialog.NewHelpDialog(helpBindings...)
|
|
|
- a.modal = helpDialog
|
|
|
+
|
|
|
+ // 5. Check for leader key activation
|
|
|
+ if a.leaderBinding != nil &&
|
|
|
+ !a.isLeaderSequence &&
|
|
|
+ key.Matches(msg, *a.leaderBinding) {
|
|
|
+ a.isLeaderSequence = true
|
|
|
+ return a, nil
|
|
|
}
|
|
|
- slog.Info("Execute command", "cmds", cmds)
|
|
|
- return a, tea.Batch(cmds...)
|
|
|
|
|
|
+ // 6. Check again for commands that don't require leader
|
|
|
+ matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
|
|
|
+ if len(matches) > 0 {
|
|
|
+ return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. Fallback to editor. This shouldn't happen?
|
|
|
+ // All printable characters were already sent, and
|
|
|
+ // any other keypress that didn't match a command
|
|
|
+ // is likely a noop.
|
|
|
+ updatedEditor, cmd := a.editor.Update(msg)
|
|
|
+ a.editor = updatedEditor.(chat.EditorComponent)
|
|
|
+ return a, cmd
|
|
|
+ case tea.MouseWheelMsg:
|
|
|
+ if a.modal != nil {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.messages.Update(msg)
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
case tea.BackgroundColorMsg:
|
|
|
styles.Terminal = &styles.TerminalInfo{
|
|
|
BackgroundIsDark: msg.IsDark(),
|
|
|
}
|
|
|
-
|
|
|
+ slog.Debug("Background color", "isDark", msg.IsDark())
|
|
|
+ case modal.CloseModalMsg:
|
|
|
+ a.modal = nil
|
|
|
+ return a, nil
|
|
|
+ case commands.ExecuteCommandMsg:
|
|
|
+ updated, cmd := a.executeCommand(commands.Command(msg))
|
|
|
+ return updated, cmd
|
|
|
+ case commands.ExecuteCommandsMsg:
|
|
|
+ for _, command := range msg {
|
|
|
+ updated, cmd := a.executeCommand(command)
|
|
|
+ if cmd != nil {
|
|
|
+ return updated, cmd
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case app.SendMsg:
|
|
|
+ a.showCompletionDialog = false
|
|
|
+ cmd := a.app.SendChatMessage(context.Background(), msg.Text, msg.Attachments)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case dialog.CompletionDialogCloseMsg:
|
|
|
+ a.showCompletionDialog = false
|
|
|
case client.EventSessionUpdated:
|
|
|
if msg.Properties.Info.Id == a.app.Session.Id {
|
|
|
a.app.Session = &msg.Properties.Info
|
|
|
}
|
|
|
-
|
|
|
case client.EventMessageUpdated:
|
|
|
if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
|
|
|
exists := false
|
|
|
@@ -204,12 +190,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
case tea.WindowSizeMsg:
|
|
|
msg.Height -= 2 // Make space for the status bar
|
|
|
a.width, a.height = msg.Width, msg.Height
|
|
|
-
|
|
|
- // TODO: move away from global state
|
|
|
layout.Current = &layout.LayoutInfo{
|
|
|
Viewport: layout.Dimensions{
|
|
|
Width: a.width,
|
|
|
@@ -219,115 +202,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
Width: min(a.width, 80),
|
|
|
},
|
|
|
}
|
|
|
-
|
|
|
- // Update status
|
|
|
- s, cmd := a.status.Update(msg)
|
|
|
- a.status = s.(status.StatusComponent)
|
|
|
- if cmd != nil {
|
|
|
- cmds = append(cmds, cmd)
|
|
|
- }
|
|
|
-
|
|
|
- // Update chat layout
|
|
|
- cmd = a.layout.SetSize(msg.Width, msg.Height)
|
|
|
- if cmd != nil {
|
|
|
- cmds = append(cmds, cmd)
|
|
|
- }
|
|
|
-
|
|
|
- // Update modal if present
|
|
|
- if a.modal != nil {
|
|
|
- s, cmd := a.modal.Update(msg)
|
|
|
- a.modal = s.(layout.Modal)
|
|
|
- if cmd != nil {
|
|
|
- cmds = append(cmds, cmd)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return a, tea.Batch(cmds...)
|
|
|
-
|
|
|
+ a.layout.SetSize(a.width, a.height)
|
|
|
case app.SessionSelectedMsg:
|
|
|
a.app.Session = msg
|
|
|
a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id)
|
|
|
-
|
|
|
case app.ModelSelectedMsg:
|
|
|
a.app.Provider = &msg.Provider
|
|
|
a.app.Model = &msg.Model
|
|
|
a.app.Config.Provider = msg.Provider.Id
|
|
|
a.app.Config.Model = msg.Model.Id
|
|
|
- a.app.SaveConfig()
|
|
|
-
|
|
|
+ a.app.SaveState()
|
|
|
case dialog.ThemeSelectedMsg:
|
|
|
a.app.Config.Theme = msg.ThemeName
|
|
|
- a.app.SaveConfig()
|
|
|
-
|
|
|
- // Update layout
|
|
|
- u, cmd := a.layout.Update(msg)
|
|
|
- a.layout = u.(layout.FlexLayout)
|
|
|
- if cmd != nil {
|
|
|
- cmds = append(cmds, cmd)
|
|
|
- }
|
|
|
-
|
|
|
- // Update status
|
|
|
- s, cmd := a.status.Update(msg)
|
|
|
- cmds = append(cmds, cmd)
|
|
|
- a.status = s.(status.StatusComponent)
|
|
|
-
|
|
|
- t := theme.CurrentTheme()
|
|
|
- cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
|
|
|
- return a, tea.Batch(cmds...)
|
|
|
-
|
|
|
- case tea.KeyMsg:
|
|
|
- switch msg.String() {
|
|
|
- // give the editor a chance to clear input
|
|
|
- case "ctrl+c":
|
|
|
- _, cmd := a.editorContainer.Update(msg)
|
|
|
- if cmd != nil {
|
|
|
- return a, cmd
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Handle chat-specific keys
|
|
|
- switch {
|
|
|
- case key.Matches(msg, keyMap.ShowCompletionDialog):
|
|
|
- a.showCompletionDialog = true
|
|
|
- // Continue sending keys to layout->chat
|
|
|
- case key.Matches(msg, keyMap.Cancel):
|
|
|
- if a.app.Session.Id != "" {
|
|
|
- // Cancel the current session's generation process
|
|
|
- // This allows users to interrupt long-running operations
|
|
|
- a.app.Cancel(context.Background(), a.app.Session.Id)
|
|
|
- return a, nil
|
|
|
- }
|
|
|
- case key.Matches(msg, keyMap.ToggleTools):
|
|
|
- return a, util.CmdHandler(chat.ToggleToolMessagesMsg{})
|
|
|
- }
|
|
|
-
|
|
|
- // First, check for modal triggers from the command registry
|
|
|
- if a.modal == nil {
|
|
|
- for _, cmdDef := range a.app.Commands {
|
|
|
- if key.Matches(msg, cmdDef.KeyBinding) {
|
|
|
- // If a key matches, send an ExecuteCommandMsg to self.
|
|
|
- // This unifies keybinding and slash command handling.
|
|
|
- return a, util.CmdHandler(commands.ExecuteCommandMsg{Name: cmdDef.Name})
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if a.showCompletionDialog {
|
|
|
- currentInput := a.editor.Value()
|
|
|
- provider := a.completionManager.GetProvider(currentInput)
|
|
|
- a.completionDialog.SetProvider(provider)
|
|
|
-
|
|
|
- context, contextCmd := a.completionDialog.Update(msg)
|
|
|
- a.completionDialog = context.(dialog.CompletionDialog)
|
|
|
- cmds = append(cmds, contextCmd)
|
|
|
-
|
|
|
- // Doesn't forward event if enter key is pressed
|
|
|
- if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
|
|
- if keyMsg.String() == "enter" {
|
|
|
- return a, tea.Batch(cmds...)
|
|
|
- }
|
|
|
- }
|
|
|
+ a.app.SaveState()
|
|
|
}
|
|
|
|
|
|
// update status bar
|
|
|
@@ -335,18 +222,30 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
cmds = append(cmds, cmd)
|
|
|
a.status = s.(status.StatusComponent)
|
|
|
|
|
|
- // update chat layout
|
|
|
- u, cmd := a.layout.Update(msg)
|
|
|
- a.layout = u.(layout.FlexLayout)
|
|
|
+ // update editor
|
|
|
+ u, cmd := a.editor.Update(msg)
|
|
|
+ a.editor = u.(chat.EditorComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
|
- return a, tea.Batch(cmds...)
|
|
|
-}
|
|
|
|
|
|
-func (a *appModel) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
|
|
|
- var cmds []tea.Cmd
|
|
|
- cmd := a.app.SendChatMessage(context.Background(), text, attachments)
|
|
|
+ // update messages
|
|
|
+ u, cmd = a.messages.Update(msg)
|
|
|
+ a.messages = u.(chat.MessagesComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
|
- return tea.Batch(cmds...)
|
|
|
+
|
|
|
+ // update modal
|
|
|
+ if a.modal != nil {
|
|
|
+ u, cmd := a.modal.Update(msg)
|
|
|
+ a.modal = u.(layout.Modal)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ }
|
|
|
+
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ u, cmd := a.completions.Update(msg)
|
|
|
+ a.completions = u.(dialog.CompletionDialog)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ }
|
|
|
+
|
|
|
+ return a, tea.Batch(cmds...)
|
|
|
}
|
|
|
|
|
|
func (a appModel) View() string {
|
|
|
@@ -356,8 +255,8 @@ func (a appModel) View() string {
|
|
|
editorWidth, _ := a.editorContainer.GetSize()
|
|
|
editorX, editorY := a.editorContainer.GetPosition()
|
|
|
|
|
|
- a.completionDialog.SetWidth(editorWidth)
|
|
|
- overlay := a.completionDialog.View()
|
|
|
+ a.completions.SetWidth(editorWidth)
|
|
|
+ overlay := a.completions.View()
|
|
|
|
|
|
layoutView = layout.PlaceOverlay(
|
|
|
editorX,
|
|
|
@@ -372,7 +271,6 @@ func (a appModel) View() string {
|
|
|
a.status.View(),
|
|
|
}
|
|
|
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
|
|
|
-
|
|
|
if a.modal != nil {
|
|
|
appView = a.modal.Render(appView)
|
|
|
}
|
|
|
@@ -380,36 +278,219 @@ func (a appModel) View() string {
|
|
|
return appView
|
|
|
}
|
|
|
|
|
|
+func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|
|
+ cmds := []tea.Cmd{
|
|
|
+ util.CmdHandler(commands.CommandExecutedMsg(command)),
|
|
|
+ }
|
|
|
+ switch command.Name {
|
|
|
+ case commands.AppHelpCommand:
|
|
|
+ helpDialog := dialog.NewHelpDialog(a.app.Commands)
|
|
|
+ a.modal = helpDialog
|
|
|
+ case commands.EditorOpenCommand:
|
|
|
+ if a.app.IsBusy() {
|
|
|
+ // status.Warn("Agent is working, please wait...")
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ editor := os.Getenv("EDITOR")
|
|
|
+ if editor == "" {
|
|
|
+ // TODO: let the user know there's no EDITOR set
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+ if err != nil {
|
|
|
+ slog.Error("Failed to create temp file", "error", err)
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ tmpfile.Close()
|
|
|
+ c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
|
|
|
+ c.Stdin = os.Stdin
|
|
|
+ c.Stdout = os.Stdout
|
|
|
+ c.Stderr = os.Stderr
|
|
|
+ cmd = tea.ExecProcess(c, func(err error) tea.Msg {
|
|
|
+ if err != nil {
|
|
|
+ slog.Error("Failed to open editor", "error", err)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ content, err := os.ReadFile(tmpfile.Name())
|
|
|
+ if err != nil {
|
|
|
+ slog.Error("Failed to read file", "error", err)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ if len(content) == 0 {
|
|
|
+ slog.Warn("Message is empty")
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ os.Remove(tmpfile.Name())
|
|
|
+ // attachments := m.attachments
|
|
|
+ // m.attachments = nil
|
|
|
+ return app.SendMsg{
|
|
|
+ Text: string(content),
|
|
|
+ Attachments: []app.Attachment{}, // attachments,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.SessionNewCommand:
|
|
|
+ if a.app.Session.Id == "" {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ a.app.Session = &client.SessionInfo{}
|
|
|
+ a.app.Messages = []client.MessageInfo{}
|
|
|
+ cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
|
|
|
+ case commands.SessionListCommand:
|
|
|
+ sessionDialog := dialog.NewSessionDialog(a.app)
|
|
|
+ a.modal = sessionDialog
|
|
|
+ case commands.SessionShareCommand:
|
|
|
+ if a.app.Session.Id == "" {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ a.app.Client.PostSessionShareWithResponse(
|
|
|
+ context.Background(),
|
|
|
+ client.PostSessionShareJSONRequestBody{
|
|
|
+ SessionID: a.app.Session.Id,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ case commands.SessionInterruptCommand:
|
|
|
+ if a.app.Session.Id == "" {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ a.app.Cancel(context.Background(), a.app.Session.Id)
|
|
|
+ return a, nil
|
|
|
+ case commands.SessionCompactCommand:
|
|
|
+ if a.app.Session.Id == "" {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ // TODO: block until compaction is complete
|
|
|
+ a.app.CompactSession(context.Background())
|
|
|
+ case commands.ToolDetailsCommand:
|
|
|
+ cmds = append(cmds, util.CmdHandler(chat.ToggleToolDetailsMsg{}))
|
|
|
+ case commands.ModelListCommand:
|
|
|
+ modelDialog := dialog.NewModelDialog(a.app)
|
|
|
+ a.modal = modelDialog
|
|
|
+ case commands.ThemeListCommand:
|
|
|
+ themeDialog := dialog.NewThemeDialog()
|
|
|
+ a.modal = themeDialog
|
|
|
+ case commands.ProjectInitCommand:
|
|
|
+ cmds = append(cmds, a.app.InitializeProject(context.Background()))
|
|
|
+ case commands.InputClearCommand:
|
|
|
+ if a.editor.Value() == "" {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.editor.Clear()
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.InputPasteCommand:
|
|
|
+ updated, cmd := a.editor.Paste()
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.InputSubmitCommand:
|
|
|
+ updated, cmd := a.editor.Submit()
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.InputNewlineCommand:
|
|
|
+ updated, cmd := a.editor.Newline()
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.HistoryPreviousCommand:
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.editor.Previous()
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.HistoryNextCommand:
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.editor.Next()
|
|
|
+ a.editor = updated.(chat.EditorComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.MessagesFirstCommand:
|
|
|
+ updated, cmd := a.messages.First()
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.MessagesLastCommand:
|
|
|
+ updated, cmd := a.messages.Last()
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.MessagesPageUpCommand:
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.messages.PageUp()
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.MessagesPageDownCommand:
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.messages.PageDown()
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.MessagesHalfPageUpCommand:
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.messages.HalfPageUp()
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.MessagesHalfPageDownCommand:
|
|
|
+ if a.showCompletionDialog {
|
|
|
+ return a, nil
|
|
|
+ }
|
|
|
+ updated, cmd := a.messages.HalfPageDown()
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
+ case commands.AppExitCommand:
|
|
|
+ return a, tea.Quit
|
|
|
+ }
|
|
|
+ return a, tea.Batch(cmds...)
|
|
|
+}
|
|
|
+
|
|
|
func NewModel(app *app.App) tea.Model {
|
|
|
completionManager := completions.NewCompletionManager(app)
|
|
|
initialProvider := completionManager.GetProvider("")
|
|
|
- completionDialog := dialog.NewCompletionDialogComponent(initialProvider)
|
|
|
|
|
|
- messagesContainer := layout.NewContainer(
|
|
|
- chat.NewMessagesComponent(app),
|
|
|
- )
|
|
|
+ messages := chat.NewMessagesComponent(app)
|
|
|
editor := chat.NewEditorComponent(app)
|
|
|
+ completions := dialog.NewCompletionDialogComponent(initialProvider)
|
|
|
+
|
|
|
editorContainer := layout.NewContainer(
|
|
|
editor,
|
|
|
layout.WithMaxWidth(layout.Current.Container.Width),
|
|
|
layout.WithAlignCenter(),
|
|
|
)
|
|
|
+ messagesContainer := layout.NewContainer(messages)
|
|
|
+
|
|
|
+ var leaderBinding *key.Binding
|
|
|
+ if leader, ok := app.Config.Keybinds["leader"]; ok {
|
|
|
+ binding := key.NewBinding(key.WithKeys(leader))
|
|
|
+ leaderBinding = &binding
|
|
|
+ }
|
|
|
|
|
|
model := &appModel{
|
|
|
status: status.NewStatusCmp(app),
|
|
|
app: app,
|
|
|
- editorContainer: editorContainer,
|
|
|
editor: editor,
|
|
|
- messagesContainer: messagesContainer,
|
|
|
- completionDialog: completionDialog,
|
|
|
+ messages: messages,
|
|
|
+ completions: completions,
|
|
|
completionManager: completionManager,
|
|
|
+ leaderBinding: leaderBinding,
|
|
|
+ isLeaderSequence: false,
|
|
|
showCompletionDialog: false,
|
|
|
+ editorContainer: editorContainer,
|
|
|
layout: layout.NewFlexLayout(
|
|
|
- layout.WithPanes(messagesContainer, editorContainer),
|
|
|
+ []tea.ViewModel{messagesContainer, editorContainer},
|
|
|
layout.WithDirection(layout.FlexDirectionVertical),
|
|
|
- layout.WithPaneSizes(
|
|
|
- layout.FlexPaneSizeGrow,
|
|
|
- layout.FlexPaneSizeFixed(6),
|
|
|
+ layout.WithSizes(
|
|
|
+ layout.FlexChildSizeGrow,
|
|
|
+ layout.FlexChildSizeFixed(6),
|
|
|
),
|
|
|
),
|
|
|
}
|