Compare commits
6 Commits
f05e2e30f7
...
02228d65ac
Author | SHA1 | Date | |
---|---|---|---|
02228d65ac | |||
d9d1b02ef3 | |||
8c7864fdf6 | |||
d2ce8edad8 | |||
c261fdadf5 | |||
1996300c40 |
18
go.mod
18
go.mod
@ -5,21 +5,21 @@ go 1.21
|
|||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0
|
github.com/alecthomas/chroma/v2 v2.14.0
|
||||||
github.com/charmbracelet/bubbles v0.20.0
|
github.com/charmbracelet/bubbles v0.20.0
|
||||||
github.com/charmbracelet/bubbletea v1.1.1
|
github.com/charmbracelet/bubbletea v1.2.4
|
||||||
github.com/charmbracelet/lipgloss v0.13.0
|
github.com/charmbracelet/lipgloss v1.0.0
|
||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/sqids/sqids-go v0.4.1
|
github.com/sqids/sqids-go v0.4.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/sqlite v1.5.7
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.3.1 // indirect
|
github.com/charmbracelet/x/ansi v0.6.0 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
@ -30,14 +30,14 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 // indirect
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.15.2 // indirect
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
)
|
)
|
||||||
|
36
go.sum
36
go.sum
@ -12,14 +12,14 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||||
github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY=
|
github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
|
||||||
github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
|
github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
|
||||||
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
|
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||||
github.com/charmbracelet/x/ansi v0.3.1 h1:CRO6lc/6HCx2/D6S/GZ87jDvRvk6GtPyFP+IljkNtqI=
|
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
|
||||||
github.com/charmbracelet/x/ansi v0.3.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
@ -47,8 +47,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
|
|||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
@ -71,20 +71,20 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw=
|
github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw=
|
||||||
github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8=
|
github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
@ -298,39 +298,36 @@ func (s *repo) CloneBranch(messageToClone Message) (*Message, uint, error) {
|
|||||||
|
|
||||||
func fetchMessages(db *gorm.DB) ([]Message, error) {
|
func fetchMessages(db *gorm.DB) ([]Message, error) {
|
||||||
var messages []Message
|
var messages []Message
|
||||||
if err := db.Preload("Conversation").Find(&messages).Error; err != nil {
|
if err := db.Preload("Conversation").Order("id ASC").Find(&messages).Error; err != nil {
|
||||||
return nil, fmt.Errorf("Could not fetch messages: %v", err)
|
return nil, fmt.Errorf("Could not fetch messages: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
messageMap := make(map[uint]Message)
|
messageMap := make(map[uint]*Message)
|
||||||
for i, message := range messages {
|
for i, message := range messages {
|
||||||
messageMap[messages[i].ID] = message
|
messageMap[message.ID] = &messages[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a map to store replies by their parent ID
|
// Create a map to store replies by their parent ID
|
||||||
repliesMap := make(map[uint][]Message)
|
repliesMap := make(map[uint][]Message)
|
||||||
for i, message := range messages {
|
for _, message := range messages {
|
||||||
if messages[i].ParentID != nil {
|
if message.ParentID != nil {
|
||||||
repliesMap[*messages[i].ParentID] = append(repliesMap[*messages[i].ParentID], message)
|
repliesMap[*message.ParentID] = append(repliesMap[*message.ParentID], message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign replies, parent, and selected reply to each message
|
// Assign replies, parent, and selected reply to each message
|
||||||
for i := range messages {
|
for i := range messages {
|
||||||
if replies, exists := repliesMap[messages[i].ID]; exists {
|
if replies, exists := repliesMap[messages[i].ID]; exists {
|
||||||
messages[i].Replies = make([]Message, len(replies))
|
messages[i].Replies = replies
|
||||||
for j, m := range replies {
|
|
||||||
messages[i].Replies[j] = m
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if messages[i].ParentID != nil {
|
if messages[i].ParentID != nil {
|
||||||
if parent, exists := messageMap[*messages[i].ParentID]; exists {
|
if parent, exists := messageMap[*messages[i].ParentID]; exists {
|
||||||
messages[i].Parent = &parent
|
messages[i].Parent = parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if messages[i].SelectedReplyID != nil {
|
if messages[i].SelectedReplyID != nil {
|
||||||
if selectedReply, exists := messageMap[*messages[i].SelectedReplyID]; exists {
|
if selectedReply, exists := messageMap[*messages[i].SelectedReplyID]; exists {
|
||||||
messages[i].SelectedReply = &selectedReply
|
messages[i].SelectedReply = selectedReply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -359,27 +356,6 @@ func (s *repo) buildPath(message *Message, getNext func(*Message) *uint) ([]Mess
|
|||||||
messageMap[messages[i].ID] = &messages[i]
|
messageMap[messages[i].ID] = &messages[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct Replies
|
|
||||||
repliesMap := make(map[uint][]*Message, len(messages))
|
|
||||||
for _, m := range messageMap {
|
|
||||||
if m.ParentID == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p, ok := messageMap[*m.ParentID]; ok {
|
|
||||||
repliesMap[p.ID] = append(repliesMap[p.ID], m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add replies to messages
|
|
||||||
for _, m := range messageMap {
|
|
||||||
if replies, ok := repliesMap[m.ID]; ok {
|
|
||||||
m.Replies = make([]Message, len(replies))
|
|
||||||
for idx, reply := range replies {
|
|
||||||
m.Replies[idx] = *reply
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the path
|
// Build the path
|
||||||
var path []Message
|
var path []Message
|
||||||
nextID := &message.ID
|
nextID := &message.ID
|
||||||
|
@ -13,7 +13,7 @@ type ViewModel interface {
|
|||||||
// View methods
|
// View methods
|
||||||
Header(width int) string
|
Header(width int) string
|
||||||
// Render the view's main content into a container of the given dimensions
|
// Render the view's main content into a container of the given dimensions
|
||||||
Content(width, height int) string
|
Content(width, height int, errors string) string
|
||||||
Footer(width int) string
|
Footer(width int) string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +60,14 @@ func (m *Model) Init() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
if len(m.errs) > 0 {
|
||||||
|
m.errs = m.errs[1:]
|
||||||
|
return shared.KeyHandled(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view, cmd := m.views[m.activeView].Update(msg)
|
view, cmd := m.views[m.activeView].Update(msg)
|
||||||
m.views[m.activeView] = view
|
m.views[m.activeView] = view
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
@ -108,10 +116,16 @@ func (m *Model) View() string {
|
|||||||
errBanners := make([]string, len(m.errs))
|
errBanners := make([]string, len(m.errs))
|
||||||
for idx, err := range m.errs {
|
for idx, err := range m.errs {
|
||||||
errBanners[idx] = tuiutil.ErrorBanner(err, m.width)
|
errBanners[idx] = tuiutil.ErrorBanner(err, m.width)
|
||||||
fixedUIHeight += tuiutil.Height(errBanners[idx])
|
|
||||||
}
|
}
|
||||||
|
var errors string
|
||||||
|
if len(errBanners) > 0 {
|
||||||
|
errors = lipgloss.JoinVertical(lipgloss.Left, errBanners...)
|
||||||
|
} else {
|
||||||
|
errors = ""
|
||||||
|
}
|
||||||
|
fixedUIHeight += tuiutil.Height(errors)
|
||||||
|
|
||||||
content := m.views[m.activeView].Content(m.width, m.height-fixedUIHeight)
|
content := m.views[m.activeView].Content(m.width, m.height-fixedUIHeight, errors)
|
||||||
|
|
||||||
sections := make([]string, 0, 4)
|
sections := make([]string, 0, 4)
|
||||||
if header != "" {
|
if header != "" {
|
||||||
@ -123,9 +137,6 @@ func (m *Model) View() string {
|
|||||||
if footer != "" {
|
if footer != "" {
|
||||||
sections = append(sections, footer)
|
sections = append(sections, footer)
|
||||||
}
|
}
|
||||||
for _, errBanner := range errBanners {
|
|
||||||
sections = append(sections, errBanner)
|
|
||||||
}
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,9 +81,9 @@ type Model struct {
|
|||||||
selectedMessage int
|
selectedMessage int
|
||||||
editorTarget editorTarget
|
editorTarget editorTarget
|
||||||
stopSignal chan struct{}
|
stopSignal chan struct{}
|
||||||
replyChan chan conversation.Message
|
|
||||||
chatReplyChunks chan provider.Chunk
|
chatReplyChunks chan provider.Chunk
|
||||||
persistence bool // whether we will save new messages in the conversation
|
persistence bool // whether we will save new messages in the conversation
|
||||||
|
promptCaching bool // whether prompt caching is enabled
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
focus focusState
|
focus focusState
|
||||||
@ -134,8 +134,7 @@ func Chat(app *model.AppModel) *Model {
|
|||||||
persistence: true,
|
persistence: true,
|
||||||
|
|
||||||
stopSignal: make(chan struct{}),
|
stopSignal: make(chan struct{}),
|
||||||
replyChan: make(chan conversation.Message),
|
chatReplyChunks: make(chan provider.Chunk, 1),
|
||||||
chatReplyChunks: make(chan provider.Chunk),
|
|
||||||
|
|
||||||
wrap: true,
|
wrap: true,
|
||||||
selectedMessage: -1,
|
selectedMessage: -1,
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/conversation"
|
"git.mlow.ca/mlow/lmcli/pkg/conversation"
|
||||||
|
"git.mlow.ca/mlow/lmcli/pkg/provider"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
||||||
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@ -127,6 +128,15 @@ func (m *Model) promptLLM() tea.Cmd {
|
|||||||
m.spinner.Tick,
|
m.spinner.Tick,
|
||||||
func() tea.Msg {
|
func() tea.Msg {
|
||||||
resp, err := m.App.Prompt(m.App.Messages, m.chatReplyChunks, m.stopSignal)
|
resp, err := m.App.Prompt(m.App.Messages, m.chatReplyChunks, m.stopSignal)
|
||||||
|
// These empty chunk sends prevent a race condition where a final
|
||||||
|
// chunk may be received on m.chatReplyChunks after the
|
||||||
|
// msgChatResponse message is handled, resulting in that chunk
|
||||||
|
// appearing twice at the end of the final output
|
||||||
|
// One send reduces the frequency of the race, two seems to
|
||||||
|
// eliminate it
|
||||||
|
m.chatReplyChunks <- provider.Chunk{}
|
||||||
|
m.chatReplyChunks <- provider.Chunk{}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return msgChatResponseError{Err: err}
|
return msgChatResponseError{Err: err}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,13 @@ func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
|
|||||||
return shared.KeyHandled(msg)
|
return shared.KeyHandled(msg)
|
||||||
case "ctrl+t":
|
case "ctrl+t":
|
||||||
m.showDetails = !m.showDetails
|
m.showDetails = !m.showDetails
|
||||||
|
if !m.showDetails && m.selectedMessage == 0 {
|
||||||
|
if len(m.App.Messages) > 1 {
|
||||||
|
m.selectedMessage = 1
|
||||||
|
} else {
|
||||||
|
m.selectedMessage = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
m.rebuildMessageCache()
|
m.rebuildMessageCache()
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
return shared.KeyHandled(msg)
|
return shared.KeyHandled(msg)
|
||||||
@ -157,6 +164,9 @@ func (m *Model) handleInputKey(msg tea.KeyMsg) 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)
|
||||||
|
} else {
|
||||||
|
m.selectedMessage = -1
|
||||||
|
m.content.GotoTop()
|
||||||
}
|
}
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.input.Blur()
|
m.input.Blur()
|
||||||
|
@ -78,6 +78,12 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
|||||||
if m.App.Conversation.ID > 0 {
|
if m.App.Conversation.ID > 0 {
|
||||||
// (re)load conversation contents
|
// (re)load conversation contents
|
||||||
cmds = append(cmds, m.loadConversationMessages())
|
cmds = append(cmds, m.loadConversationMessages())
|
||||||
|
} else {
|
||||||
|
if len(m.App.Messages) > 0 && m.showDetails {
|
||||||
|
m.selectedMessage = 0
|
||||||
|
} else {
|
||||||
|
m.selectedMessage = -1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case tuiutil.MsgTempfileEditorClosed:
|
case tuiutil.MsgTempfileEditorClosed:
|
||||||
contents := string(msg)
|
contents := string(msg)
|
||||||
@ -89,6 +95,7 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
|||||||
if toEdit.Content != contents {
|
if toEdit.Content != contents {
|
||||||
toEdit.Content = contents
|
toEdit.Content = contents
|
||||||
m.setMessage(m.selectedMessage, toEdit)
|
m.setMessage(m.selectedMessage, toEdit)
|
||||||
|
m.updateContent()
|
||||||
if m.persistence && toEdit.ID > 0 {
|
if m.persistence && toEdit.ID > 0 {
|
||||||
// create clone of message with its new contents
|
// create clone of message with its new contents
|
||||||
cmds = append(cmds, m.cloneMessage(toEdit, true))
|
cmds = append(cmds, m.cloneMessage(toEdit, true))
|
||||||
@ -97,17 +104,14 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case msgConversationMessagesLoaded:
|
case msgConversationMessagesLoaded:
|
||||||
m.App.Messages = msg.messages
|
m.App.Messages = msg.messages
|
||||||
if m.selectedMessage == -1 {
|
m.selectedMessage = len(msg.messages) - 1
|
||||||
m.selectedMessage = len(msg.messages) - 1
|
|
||||||
} else {
|
|
||||||
m.selectedMessage = min(m.selectedMessage, len(m.App.Messages))
|
|
||||||
}
|
|
||||||
m.rebuildMessageCache()
|
m.rebuildMessageCache()
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
case msgChatResponseChunk:
|
case msgChatResponseChunk:
|
||||||
cmds = append(cmds, m.waitForResponseChunk()) // wait for the next chunk
|
cmds = append(cmds, m.waitForResponseChunk()) // wait for the next chunk
|
||||||
|
|
||||||
if msg.Content == "" {
|
if msg.Content == "" {
|
||||||
|
// skip empty chunks
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +245,7 @@ func (m *Model) conversationMessagesView() string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Content(width, height int) string {
|
func (m *Model) Content(width, height int, errors string) string {
|
||||||
// calculate clamped input height to accomodate input text
|
// calculate clamped input height to accomodate input text
|
||||||
// minimum 4 lines, maximum half of content area
|
// minimum 4 lines, maximum half of content area
|
||||||
inputHeight := max(4, min(height/2, m.input.LineCount()))
|
inputHeight := max(4, min(height/2, m.input.LineCount()))
|
||||||
@ -255,7 +255,15 @@ func (m *Model) Content(width, height int) string {
|
|||||||
// remaining height towards content
|
// remaining height towards content
|
||||||
m.content.Width, m.content.Height = width, height-tuiutil.Height(input)
|
m.content.Width, m.content.Height = width, height-tuiutil.Height(input)
|
||||||
content := m.content.View()
|
content := m.content.View()
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, content, input)
|
|
||||||
|
var sections []string
|
||||||
|
if errors != "" {
|
||||||
|
sections = []string{content, errors, input}
|
||||||
|
} else {
|
||||||
|
sections = []string{content, input}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Header(width int) string {
|
func (m *Model) Header(width int) string {
|
||||||
|
@ -218,9 +218,13 @@ func (m *Model) Header(width int) string {
|
|||||||
return styles.Header.Width(width).Render(header)
|
return styles.Header.Width(width).Render(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Content(width int, height int) string {
|
func (m *Model) Content(width int, height int, errors string) string {
|
||||||
m.content.Width, m.content.Height = width, height
|
m.content.Width, m.content.Height = width, height
|
||||||
return m.content.View()
|
content := m.content.View()
|
||||||
|
if errors != "" {
|
||||||
|
content += errors
|
||||||
|
}
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Footer(width int) string {
|
func (m *Model) Footer(width int) string {
|
||||||
|
@ -121,11 +121,15 @@ func (m *Model) Header(width int) string {
|
|||||||
return styles.Header.Width(width).Render(header)
|
return styles.Header.Width(width).Render(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Content(width, height int) string {
|
func (m *Model) Content(width, height int, errors string) string {
|
||||||
// TODO: see Header()
|
// TODO: see Header()
|
||||||
currentModel := " Active model: " + m.App.ActiveModel(lipgloss.NewStyle())
|
currentModel := " Active model: " + m.App.ActiveModel(lipgloss.NewStyle())
|
||||||
m.modelList.Width, m.modelList.Height = width, height - 2
|
m.modelList.Width, m.modelList.Height = width, height - 2
|
||||||
return "\n" + currentModel + "\n" + m.modelList.View()
|
content := "\n" + currentModel + "\n" + m.modelList.View()
|
||||||
|
if errors != "" {
|
||||||
|
content += errors
|
||||||
|
}
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Footer(width int) string {
|
func (m *Model) Footer(width int) string {
|
||||||
|
Loading…
Reference in New Issue
Block a user