TUI view management and input handling cleanup
This commit is contained in:
parent
24b5cdbbf6
commit
463ca9ef40
@ -40,7 +40,7 @@ func ChatCmd(ctx *lmcli.Context) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if list {
|
if list {
|
||||||
opts = append(opts, tui.WithInitialView(shared.StateConversations))
|
opts = append(opts, tui.WithInitialView(shared.ViewConversations))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tui.Launch(ctx, opts...)
|
err = tui.Launch(ctx, opts...)
|
||||||
|
@ -4,13 +4,31 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Shared struct {
|
// An analogue to tea.Model with support for checking if the model has been
|
||||||
|
// initialized before
|
||||||
|
type ViewModel interface {
|
||||||
|
Init() tea.Cmd
|
||||||
|
Update(tea.Msg) (ViewModel, tea.Cmd)
|
||||||
|
View() string
|
||||||
|
Initialized() bool // Return whether this view is initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewState struct {
|
||||||
Initialized bool
|
Initialized bool
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type View int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ViewChat View = iota
|
||||||
|
ViewConversations
|
||||||
|
//StateSettings
|
||||||
|
//StateHelp
|
||||||
|
)
|
||||||
|
|
||||||
// a convenience struct for holding rendered content for indiviudal UI
|
// a convenience struct for holding rendered content for indiviudal UI
|
||||||
// elements
|
// elements
|
||||||
type Sections struct {
|
type Sections struct {
|
||||||
@ -28,6 +46,8 @@ type (
|
|||||||
MsgViewEnter struct{}
|
MsgViewEnter struct{}
|
||||||
// sent when an error occurs
|
// sent when an error occurs
|
||||||
MsgError error
|
MsgError error
|
||||||
|
// sent when the view has handled a key input
|
||||||
|
MsgKeyHandled tea.KeyMsg
|
||||||
)
|
)
|
||||||
|
|
||||||
func ViewEnter() tea.Cmd {
|
func ViewEnter() tea.Cmd {
|
||||||
@ -36,17 +56,14 @@ func ViewEnter() tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func KeyHandled(key tea.KeyMsg) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return MsgKeyHandled(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WrapError(err error) tea.Cmd {
|
func WrapError(err error) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
return MsgError(err)
|
return MsgError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type View int
|
|
||||||
|
|
||||||
const (
|
|
||||||
StateChat View = iota
|
|
||||||
StateConversations
|
|
||||||
//StateSettings
|
|
||||||
//StateHelp
|
|
||||||
)
|
|
||||||
|
114
pkg/tui/tui.go
114
pkg/tui/tui.go
@ -1,11 +1,5 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
// The terminal UI for lmcli, launched from the `lmcli chat` command
|
|
||||||
// TODO:
|
|
||||||
// - change model
|
|
||||||
// - rename conversation
|
|
||||||
// - set system prompt
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@ -24,117 +18,81 @@ type LaunchOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
App *model.AppModel
|
App *model.AppModel
|
||||||
view shared.View
|
|
||||||
|
|
||||||
// views
|
activeView shared.View
|
||||||
chat chat.Model
|
views map[shared.View]shared.ViewModel
|
||||||
conversations conversations.Model
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
|
func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
|
||||||
m := Model{
|
sharedData := shared.ViewState{}
|
||||||
App: &model.AppModel{
|
|
||||||
Ctx: ctx,
|
app := &model.AppModel{
|
||||||
Conversation: opts.InitialConversation,
|
Ctx: ctx,
|
||||||
},
|
Conversation: opts.InitialConversation,
|
||||||
view: opts.InitialView,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedData := shared.Shared{}
|
m := Model{
|
||||||
|
App: app,
|
||||||
|
activeView: opts.InitialView,
|
||||||
|
views: map[shared.View]shared.ViewModel{
|
||||||
|
shared.ViewChat: chat.Chat(app, sharedData),
|
||||||
|
shared.ViewConversations: conversations.Conversations(app, sharedData),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
m.chat = chat.Chat(m.App, sharedData)
|
|
||||||
m.conversations = conversations.Conversations(m.App, sharedData)
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
func() tea.Msg {
|
func() tea.Msg {
|
||||||
return shared.MsgViewChange(m.view)
|
return shared.MsgViewChange(m.activeView)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleGlobalInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
||||||
var cmds []tea.Cmd
|
view, cmd := m.views[m.activeView].Update(msg)
|
||||||
switch m.view {
|
m.views[m.activeView] = view
|
||||||
case shared.StateChat:
|
if cmd != nil {
|
||||||
handled, cmd := m.chat.HandleInput(msg)
|
return cmd
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
if handled {
|
|
||||||
m.chat, cmd = m.chat.Update(nil)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
return true, tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
case shared.StateConversations:
|
|
||||||
handled, cmd := m.conversations.HandleInput(msg)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
if handled {
|
|
||||||
m.conversations, cmd = m.conversations.Update(nil)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
return true, tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "ctrl+q":
|
case "ctrl+c", "ctrl+q":
|
||||||
return true, tea.Quit
|
return tea.Quit
|
||||||
}
|
}
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
handled, cmd := m.handleGlobalInput(msg)
|
cmd := m.handleGlobalInput(msg)
|
||||||
if handled {
|
if cmd != nil {
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
case shared.MsgViewChange:
|
case shared.MsgViewChange:
|
||||||
m.view = shared.View(msg)
|
m.activeView = shared.View(msg)
|
||||||
|
view := m.views[m.activeView]
|
||||||
|
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch m.view {
|
if !view.Initialized() {
|
||||||
case shared.StateConversations:
|
cmds = append(cmds, view.Init())
|
||||||
if !m.conversations.Initialized {
|
|
||||||
cmds = append(cmds, m.conversations.Init())
|
|
||||||
m.conversations.Initialized = true
|
|
||||||
}
|
|
||||||
case shared.StateChat:
|
|
||||||
if !m.chat.Initialized {
|
|
||||||
cmds = append(cmds, m.chat.Init())
|
|
||||||
m.chat.Initialized = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cmds = append(cmds, tea.WindowSize(), shared.ViewEnter())
|
cmds = append(cmds, tea.WindowSize(), shared.ViewEnter())
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
view, cmd := m.views[m.activeView].Update(msg)
|
||||||
switch m.view {
|
m.views[m.activeView] = view
|
||||||
case shared.StateConversations:
|
return m, cmd
|
||||||
m.conversations, cmd = m.conversations.Update(msg)
|
|
||||||
case shared.StateChat:
|
|
||||||
m.chat, cmd = m.chat.Update(msg)
|
|
||||||
}
|
|
||||||
if cmd != nil {
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
switch m.view {
|
return m.views[m.activeView].View()
|
||||||
case shared.StateConversations:
|
|
||||||
return m.conversations.View()
|
|
||||||
case shared.StateChat:
|
|
||||||
return m.chat.View()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LaunchOption func(*LaunchOptions)
|
type LaunchOption func(*LaunchOptions)
|
||||||
@ -153,7 +111,7 @@ func WithInitialView(view shared.View) LaunchOption {
|
|||||||
|
|
||||||
func Launch(ctx *lmcli.Context, options ...LaunchOption) error {
|
func Launch(ctx *lmcli.Context, options ...LaunchOption) error {
|
||||||
opts := &LaunchOptions{
|
opts := &LaunchOptions{
|
||||||
InitialView: shared.StateChat,
|
InitialView: shared.ViewChat,
|
||||||
}
|
}
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
opt(opts)
|
opt(opts)
|
||||||
|
@ -74,7 +74,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
shared.Shared
|
*shared.ViewState
|
||||||
shared.Sections
|
shared.Sections
|
||||||
|
|
||||||
// App state
|
// App state
|
||||||
@ -108,10 +108,14 @@ type Model struct {
|
|||||||
elapsed time.Duration
|
elapsed time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func Chat(app *model.AppModel, shared shared.Shared) Model {
|
func (m Model) Initialized() bool {
|
||||||
|
return m.ViewState.Initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
func Chat(app *model.AppModel, shared shared.ViewState) shared.ViewModel {
|
||||||
m := Model{
|
m := Model{
|
||||||
App: app,
|
App: app,
|
||||||
Shared: shared,
|
ViewState: &shared,
|
||||||
|
|
||||||
state: idle,
|
state: idle,
|
||||||
persistence: true,
|
persistence: true,
|
||||||
@ -169,6 +173,7 @@ func Chat(app *model.AppModel, shared shared.Shared) Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
|
m.ViewState.Initialized = true
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
m.waitForResponseChunk(),
|
m.waitForResponseChunk(),
|
||||||
)
|
)
|
||||||
|
@ -11,17 +11,17 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
|
||||||
switch m.focus {
|
switch m.focus {
|
||||||
case focusInput:
|
case focusInput:
|
||||||
consumed, cmd := m.handleInputKey(msg)
|
cmd := m.handleInputKey(msg)
|
||||||
if consumed {
|
if cmd != nil {
|
||||||
return true, cmd
|
return cmd
|
||||||
}
|
}
|
||||||
case focusMessages:
|
case focusMessages:
|
||||||
consumed, cmd := m.handleMessagesKey(msg)
|
cmd := m.handleMessagesKey(msg)
|
||||||
if consumed {
|
if cmd != nil {
|
||||||
return true, cmd
|
return cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,51 +29,51 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
case "esc":
|
case "esc":
|
||||||
if m.state == pendingResponse {
|
if m.state == pendingResponse {
|
||||||
m.stopSignal <- struct{}{}
|
m.stopSignal <- struct{}{}
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
}
|
}
|
||||||
return true, func() tea.Msg {
|
return func() tea.Msg {
|
||||||
return shared.MsgViewChange(shared.StateConversations)
|
return shared.MsgViewChange(shared.ViewConversations)
|
||||||
}
|
}
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
if m.state == pendingResponse {
|
if m.state == pendingResponse {
|
||||||
m.stopSignal <- struct{}{}
|
m.stopSignal <- struct{}{}
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
}
|
}
|
||||||
case "ctrl+p":
|
case "ctrl+p":
|
||||||
m.persistence = !m.persistence
|
m.persistence = !m.persistence
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "ctrl+t":
|
case "ctrl+t":
|
||||||
m.showToolResults = !m.showToolResults
|
m.showToolResults = !m.showToolResults
|
||||||
m.rebuildMessageCache()
|
m.rebuildMessageCache()
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "ctrl+w":
|
case "ctrl+w":
|
||||||
m.wrap = !m.wrap
|
m.wrap = !m.wrap
|
||||||
m.rebuildMessageCache()
|
m.rebuildMessageCache()
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
}
|
}
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleMessagesKey handles input when the messages pane is focused
|
// handleMessagesKey handles input when the messages pane is focused
|
||||||
func (m *Model) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "tab", "enter":
|
case "tab", "enter":
|
||||||
m.focus = focusInput
|
m.focus = focusInput
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.input.Focus()
|
m.input.Focus()
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "e":
|
case "e":
|
||||||
if m.selectedMessage < len(m.App.Messages) {
|
if m.selectedMessage < len(m.App.Messages) {
|
||||||
m.editorTarget = selectedMessage
|
m.editorTarget = selectedMessage
|
||||||
return true, tuiutil.OpenTempfileEditor(
|
return tuiutil.OpenTempfileEditor(
|
||||||
"message.*.md",
|
"message.*.md",
|
||||||
m.App.Messages[m.selectedMessage].Content,
|
m.App.Messages[m.selectedMessage].Content,
|
||||||
"# Edit the message below\n",
|
"# Edit the message below\n",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return false, nil
|
return nil
|
||||||
case "ctrl+k":
|
case "ctrl+k":
|
||||||
if m.selectedMessage > 0 && len(m.App.Messages) == len(m.messageOffsets) {
|
if m.selectedMessage > 0 && len(m.App.Messages) == len(m.messageOffsets) {
|
||||||
m.selectedMessage--
|
m.selectedMessage--
|
||||||
@ -81,7 +81,7 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
offset := m.messageOffsets[m.selectedMessage]
|
offset := m.messageOffsets[m.selectedMessage]
|
||||||
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
|
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
|
||||||
}
|
}
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "ctrl+j":
|
case "ctrl+j":
|
||||||
if m.selectedMessage < len(m.App.Messages)-1 && len(m.App.Messages) == len(m.messageOffsets) {
|
if m.selectedMessage < len(m.App.Messages)-1 && len(m.App.Messages) == len(m.messageOffsets) {
|
||||||
m.selectedMessage++
|
m.selectedMessage++
|
||||||
@ -89,7 +89,7 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
offset := m.messageOffsets[m.selectedMessage]
|
offset := m.messageOffsets[m.selectedMessage]
|
||||||
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
|
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
|
||||||
}
|
}
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "ctrl+h", "ctrl+l":
|
case "ctrl+h", "ctrl+l":
|
||||||
dir := model.CyclePrev
|
dir := model.CyclePrev
|
||||||
if msg.String() == "ctrl+l" {
|
if msg.String() == "ctrl+l" {
|
||||||
@ -102,8 +102,7 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
} else if m.selectedMessage > 0 {
|
} else if m.selectedMessage > 0 {
|
||||||
cmd = m.cycleSelectedReply(&m.App.Messages[m.selectedMessage-1], dir)
|
cmd = m.cycleSelectedReply(&m.App.Messages[m.selectedMessage-1], dir)
|
||||||
}
|
}
|
||||||
|
return cmd
|
||||||
return cmd != nil, cmd
|
|
||||||
case "ctrl+r":
|
case "ctrl+r":
|
||||||
// resubmit the conversation with all messages up until and including the selected message
|
// resubmit the conversation with all messages up until and including the selected message
|
||||||
if m.state == idle && m.selectedMessage < len(m.App.Messages) {
|
if m.state == idle && m.selectedMessage < len(m.App.Messages) {
|
||||||
@ -112,14 +111,14 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
cmd := m.promptLLM()
|
cmd := m.promptLLM()
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.content.GotoBottom()
|
m.content.GotoBottom()
|
||||||
return true, cmd
|
return cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleInputKey handles input when the input textarea is focused
|
// handleInputKey handles input when the input textarea is focused
|
||||||
func (m *Model) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
func (m *Model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
m.focus = focusMessages
|
m.focus = focusMessages
|
||||||
@ -132,20 +131,20 @@ func (m *Model) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.input.Blur()
|
m.input.Blur()
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "ctrl+s":
|
case "ctrl+s":
|
||||||
// TODO: call a "handleSend" function which returns a tea.Cmd
|
// TODO: call a "handleSend" function which returns a tea.Cmd
|
||||||
if m.state != idle {
|
if m.state != idle {
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
input := strings.TrimSpace(m.input.Value())
|
input := strings.TrimSpace(m.input.Value())
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.App.Messages) > 0 && m.App.Messages[len(m.App.Messages)-1].Role == api.MessageRoleUser {
|
if len(m.App.Messages) > 0 && m.App.Messages[len(m.App.Messages)-1].Role == api.MessageRoleUser {
|
||||||
return true, shared.WrapError(fmt.Errorf("Can't reply to a user message"))
|
return shared.WrapError(fmt.Errorf("Can't reply to a user message"))
|
||||||
}
|
}
|
||||||
|
|
||||||
m.addMessage(api.Message{
|
m.addMessage(api.Message{
|
||||||
@ -164,11 +163,11 @@ func (m *Model) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
|
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.content.GotoBottom()
|
m.content.GotoBottom()
|
||||||
return true, tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
case "ctrl+e":
|
case "ctrl+e":
|
||||||
cmd := tuiutil.OpenTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n")
|
cmd := tuiutil.OpenTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n")
|
||||||
m.editorTarget = input
|
m.editorTarget = input
|
||||||
return true, cmd
|
return cmd
|
||||||
}
|
}
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,17 @@ func (m *Model) updateContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||||
|
inputHandled := false
|
||||||
|
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
cmd := m.handleInput(msg)
|
||||||
|
if cmd != nil {
|
||||||
|
inputHandled = true
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.Width, m.Height = msg.Width, msg.Height
|
m.Width, m.Height = msg.Width, msg.Height
|
||||||
m.content.Width = msg.Width
|
m.content.Width = msg.Width
|
||||||
@ -167,7 +175,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
m.updateContent()
|
m.updateContent()
|
||||||
case msgChatResponseError:
|
case msgChatResponseError:
|
||||||
m.state = idle
|
m.state = idle
|
||||||
m.Shared.Err = error(msg)
|
m.ViewState.Err = error(msg)
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
case msgToolResults:
|
case msgToolResults:
|
||||||
last := len(m.App.Messages) - 1
|
last := len(m.App.Messages) - 1
|
||||||
@ -231,14 +239,16 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prevInputLineCnt := m.input.LineCount()
|
prevInputLineCnt := m.input.LineCount()
|
||||||
inputCaptured := false
|
|
||||||
m.input, cmd = m.input.Update(msg)
|
if !inputHandled {
|
||||||
if cmd != nil {
|
m.input, cmd = m.input.Update(msg)
|
||||||
inputCaptured = true
|
if cmd != nil {
|
||||||
cmds = append(cmds, cmd)
|
inputHandled = true
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !inputCaptured {
|
if !inputHandled {
|
||||||
m.content, cmd = m.content.Update(msg)
|
m.content, cmd = m.content.Update(msg)
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@ -285,5 +295,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
if len(cmds) > 0 {
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,9 @@ type (
|
|||||||
// sent when a conversation is deleted
|
// sent when a conversation is deleted
|
||||||
msgConversationDeleted struct{}
|
msgConversationDeleted struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
shared.Shared
|
*shared.ViewState
|
||||||
shared.Sections
|
shared.Sections
|
||||||
|
|
||||||
App *model.AppModel
|
App *model.AppModel
|
||||||
@ -38,21 +39,25 @@ type Model struct {
|
|||||||
confirmPrompt bubbles.ConfirmPrompt
|
confirmPrompt bubbles.ConfirmPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
func Conversations(app *model.AppModel, shared shared.Shared) Model {
|
func Conversations(app *model.AppModel, shared shared.ViewState) Model {
|
||||||
m := Model{
|
m := Model{
|
||||||
App: app,
|
App: app,
|
||||||
Shared: shared,
|
ViewState: &shared,
|
||||||
content: viewport.New(0, 0),
|
content: viewport.New(0, 0),
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
func (m Model) Initialized() bool {
|
||||||
|
return m.ViewState.Initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
|
||||||
if m.confirmPrompt.Focused() {
|
if m.confirmPrompt.Focused() {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.confirmPrompt, cmd = m.confirmPrompt.Update(msg)
|
m.confirmPrompt, cmd = m.confirmPrompt.Update(msg)
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
return true, cmd
|
return cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,8 +66,8 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
if len(m.App.Conversations) > 0 && m.cursor < len(m.App.Conversations) {
|
if len(m.App.Conversations) > 0 && m.cursor < len(m.App.Conversations) {
|
||||||
m.App.Conversation = &m.App.Conversations[m.cursor].Conv
|
m.App.Conversation = &m.App.Conversations[m.cursor].Conv
|
||||||
m.App.Messages = []api.Message{}
|
m.App.Messages = []api.Message{}
|
||||||
return true, func() tea.Msg {
|
return func() tea.Msg {
|
||||||
return shared.MsgViewChange(shared.StateChat)
|
return shared.MsgViewChange(shared.ViewChat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "j", "down":
|
case "j", "down":
|
||||||
@ -81,7 +86,7 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
m.cursor = len(m.App.Conversations) - 1
|
m.cursor = len(m.App.Conversations) - 1
|
||||||
m.content.GotoBottom()
|
m.content.GotoBottom()
|
||||||
}
|
}
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "k", "up":
|
case "k", "up":
|
||||||
if m.cursor > 0 {
|
if m.cursor > 0 {
|
||||||
m.cursor--
|
m.cursor--
|
||||||
@ -95,7 +100,7 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
m.content.GotoTop()
|
m.content.GotoTop()
|
||||||
}
|
}
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
case "n":
|
case "n":
|
||||||
// new conversation
|
// new conversation
|
||||||
case "d":
|
case "d":
|
||||||
@ -111,7 +116,7 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
m.confirmPrompt.Style = lipgloss.NewStyle().
|
m.confirmPrompt.Style = lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("3"))
|
Foreground(lipgloss.Color("3"))
|
||||||
return true, nil
|
return shared.KeyHandled(msg)
|
||||||
}
|
}
|
||||||
case "c":
|
case "c":
|
||||||
// copy/clone conversation
|
// copy/clone conversation
|
||||||
@ -120,16 +125,27 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
|||||||
case "shift+r":
|
case "shift+r":
|
||||||
// show prompt to generate name for conversation
|
// show prompt to generate name for conversation
|
||||||
}
|
}
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
|
m.ViewState.Initialized = true
|
||||||
return m.loadConversations()
|
return m.loadConversations()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||||
|
isInput := false
|
||||||
|
inputHandled := false
|
||||||
|
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
isInput = true
|
||||||
|
cmd := m.handleInput(msg)
|
||||||
|
if cmd != nil {
|
||||||
|
inputHandled = true
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
case shared.MsgViewEnter:
|
case shared.MsgViewEnter:
|
||||||
cmds = append(cmds, m.loadConversations())
|
cmds = append(cmds, m.loadConversations())
|
||||||
m.content.SetContent(m.renderConversationList())
|
m.content.SetContent(m.renderConversationList())
|
||||||
@ -153,10 +169,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
cmds = append(cmds, m.loadConversations())
|
cmds = append(cmds, m.loadConversations())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
if !isInput || !inputHandled {
|
||||||
m.content, cmd = m.content.Update(msg)
|
var cmd tea.Cmd
|
||||||
if cmd != nil {
|
m.content, cmd = m.content.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
if cmd != nil {
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Width > 0 {
|
if m.Width > 0 {
|
||||||
@ -171,7 +189,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
m.content.Height = m.Height - fixedHeight
|
m.content.Height = m.Height - fixedHeight
|
||||||
m.Content = m.content.View()
|
m.Content = m.content.View()
|
||||||
}
|
}
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
|
if len(cmds) > 0 {
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) loadConversations() tea.Cmd {
|
func (m *Model) loadConversations() tea.Cmd {
|
||||||
@ -289,7 +312,7 @@ func (m *Model) renderConversationList() string {
|
|||||||
sb.WriteRune('\n')
|
sb.WriteRune('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
tStyle := titleStyle.Copy()
|
tStyle := titleStyle
|
||||||
if c.Conv.Title == "" {
|
if c.Conv.Title == "" {
|
||||||
tStyle = tStyle.Inherit(untitledStyle).SetString("(untitled)")
|
tStyle = tStyle.Inherit(untitledStyle).SetString("(untitled)")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user