feat: create bubbletea TUI with chat viewport, markdown rendering, and tool indicators
This commit is contained in:
parent
ec28399775
commit
530d6a1253
6 changed files with 986 additions and 0 deletions
34
go.mod
34
go.mod
|
|
@ -7,6 +7,10 @@ require (
|
||||||
github.com/anthropics/anthropic-sdk-go v1.22.1
|
github.com/anthropics/anthropic-sdk-go v1.22.1
|
||||||
github.com/bwmarrin/discordgo v0.29.0
|
github.com/bwmarrin/discordgo v0.29.0
|
||||||
github.com/caarlos0/env/v11 v11.3.1
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/glamour v0.10.0
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||||
github.com/chzyer/readline v1.5.1
|
github.com/chzyer/readline v1.5.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
|
@ -21,8 +25,38 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||||
|
golang.org/x/term v0.40.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
88
go.sum
88
go.sum
|
|
@ -1,10 +1,26 @@
|
||||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
|
github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
|
||||||
github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
|
github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
|
||||||
|
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||||
|
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.22.1 h1:xbsc3vJKCX/ELDZSpTNfz9wCgrFsamwFewPb1iI0Xh0=
|
github.com/anthropics/anthropic-sdk-go v1.22.1 h1:xbsc3vJKCX/ELDZSpTNfz9wCgrFsamwFewPb1iI0Xh0=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.22.1/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
github.com/anthropics/anthropic-sdk-go v1.22.1/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
|
@ -17,12 +33,38 @@ github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5m
|
||||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
|
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||||
|
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
|
@ -30,6 +72,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/github/copilot-sdk/go v0.1.23 h1:uExtO/inZQndCZMiSAA1hvXINiz9tqo/MZgQzFzurxw=
|
github.com/github/copilot-sdk/go v0.1.23 h1:uExtO/inZQndCZMiSAA1hvXINiz9tqo/MZgQzFzurxw=
|
||||||
|
|
@ -65,12 +111,16 @@ github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
|
@ -88,6 +138,25 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk=
|
github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk=
|
||||||
github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
|
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/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0=
|
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0=
|
||||||
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y=
|
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
|
@ -105,6 +174,10 @@ github.com/openai/openai-go/v3 v3.22.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbD
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
|
@ -144,11 +217,18 @@ github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZy
|
||||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||||
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
||||||
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||||
|
|
@ -161,6 +241,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
|
@ -204,10 +286,12 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
|
@ -217,6 +301,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
|
@ -224,6 +310,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
|
||||||
58
pkg/tui/messages.go
Normal file
58
pkg/tui/messages.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
// PicoClaw - Ultra-lightweight personal AI agent
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import "github.com/sipeed/picoclaw/pkg/agent"
|
||||||
|
|
||||||
|
// ThinkingStartedMsg indicates the LLM is processing.
|
||||||
|
type ThinkingStartedMsg struct{}
|
||||||
|
|
||||||
|
// ToolCallStartedMsg indicates a tool execution has begun.
|
||||||
|
type ToolCallStartedMsg struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Args string // JSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolCallCompletedMsg indicates a tool execution has finished.
|
||||||
|
type ToolCallCompletedMsg struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Result string
|
||||||
|
IsError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseMsg carries the final LLM response text.
|
||||||
|
type ResponseMsg struct {
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorMsg carries an error from the agent loop.
|
||||||
|
type ErrorMsg struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlashCommandResultMsg carries the result of a slash command.
|
||||||
|
type SlashCommandResultMsg struct {
|
||||||
|
Result string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventBridge adapts AgentEventListener to send tea.Msg via a channel.
|
||||||
|
type eventBridge struct {
|
||||||
|
events chan agent.AgentEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEventBridge() *eventBridge {
|
||||||
|
return &eventBridge{
|
||||||
|
events: make(chan agent.AgentEvent, 50),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *eventBridge) OnEvent(event agent.AgentEvent) {
|
||||||
|
select {
|
||||||
|
case eb.events <- event:
|
||||||
|
default:
|
||||||
|
// Drop event if channel is full (TUI not consuming fast enough)
|
||||||
|
}
|
||||||
|
}
|
||||||
469
pkg/tui/model.go
Normal file
469
pkg/tui/model.go
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
// PicoClaw - Ultra-lightweight personal AI agent
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/glamour"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tickInterval = 200 * time.Millisecond
|
||||||
|
thinkingFrameCount = 4
|
||||||
|
headerHeight = 1
|
||||||
|
statusBarHeight = 1
|
||||||
|
textareaHeight = 3
|
||||||
|
// chromeHeight accounts for header, status bar, textarea, and separators
|
||||||
|
chromeHeight = headerHeight + statusBarHeight + textareaHeight + 2
|
||||||
|
maxSessionKeyLen = 15
|
||||||
|
)
|
||||||
|
|
||||||
|
// thinkingFrames are the animation frames for the thinking indicator
|
||||||
|
var thinkingFrames = [thinkingFrameCount]string{"⠋", "⠙", "⠹", "⠸"}
|
||||||
|
|
||||||
|
// tickMsg drives the thinking animation and event polling
|
||||||
|
type tickMsg time.Time
|
||||||
|
|
||||||
|
// chatMessage represents a single message in the chat history
|
||||||
|
type chatMessage struct {
|
||||||
|
role string // "user", "assistant", "tool", "system"
|
||||||
|
content string
|
||||||
|
toolName string
|
||||||
|
toolID string
|
||||||
|
toolDone bool
|
||||||
|
toolErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model is the main bubbletea model for the PicoClaw TUI
|
||||||
|
type Model struct {
|
||||||
|
viewport viewport.Model
|
||||||
|
textarea textarea.Model
|
||||||
|
messages []chatMessage
|
||||||
|
agentLoop *agent.AgentLoop
|
||||||
|
sessionKey string
|
||||||
|
modelName string
|
||||||
|
thinking bool
|
||||||
|
thinkFrame int
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
ready bool
|
||||||
|
eventBridge *eventBridge
|
||||||
|
renderer *glamour.TermRenderer
|
||||||
|
quitting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewModel creates a new TUI model wired to the given agent loop
|
||||||
|
func NewModel(agentLoop *agent.AgentLoop, sessionKey, modelName string) Model {
|
||||||
|
ta := textarea.New()
|
||||||
|
ta.Placeholder = "Type a message... (Enter to send, Alt+Enter for newline)"
|
||||||
|
ta.Prompt = "│ "
|
||||||
|
ta.CharLimit = 0 // No limit
|
||||||
|
ta.SetHeight(textareaHeight)
|
||||||
|
ta.ShowLineNumbers = false
|
||||||
|
|
||||||
|
// Enter sends, Alt+Enter inserts newline
|
||||||
|
ta.KeyMap.InsertNewline = key.NewBinding(key.WithKeys("alt+enter"))
|
||||||
|
|
||||||
|
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
|
||||||
|
|
||||||
|
eb := newEventBridge()
|
||||||
|
agentLoop.SetEventListener(eb)
|
||||||
|
|
||||||
|
renderer, _ := glamour.NewTermRenderer(
|
||||||
|
glamour.WithAutoStyle(),
|
||||||
|
glamour.WithWordWrap(0), // will be updated on resize
|
||||||
|
)
|
||||||
|
|
||||||
|
return Model{
|
||||||
|
textarea: ta,
|
||||||
|
agentLoop: agentLoop,
|
||||||
|
sessionKey: sessionKey,
|
||||||
|
modelName: modelName,
|
||||||
|
eventBridge: eb,
|
||||||
|
renderer: renderer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init returns the initial command for the TUI
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return tea.Batch(textarea.Blink, tickCmd())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handles incoming messages and updates the model
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
return m.handleKeyMsg(msg)
|
||||||
|
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
return m.handleWindowSize(msg)
|
||||||
|
|
||||||
|
case tickMsg:
|
||||||
|
return m.handleTick()
|
||||||
|
|
||||||
|
case ThinkingStartedMsg:
|
||||||
|
m.thinking = true
|
||||||
|
m.updateViewport()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case ToolCallStartedMsg:
|
||||||
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: "tool",
|
||||||
|
content: fmt.Sprintf("Running %s...", msg.Name),
|
||||||
|
toolName: msg.Name,
|
||||||
|
toolID: msg.ID,
|
||||||
|
})
|
||||||
|
m.updateViewport()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case ToolCallCompletedMsg:
|
||||||
|
for i := len(m.messages) - 1; i >= 0; i-- {
|
||||||
|
if m.messages[i].role == "tool" && m.messages[i].toolID == msg.ID {
|
||||||
|
m.messages[i].toolDone = true
|
||||||
|
m.messages[i].toolErr = msg.IsError
|
||||||
|
if msg.IsError {
|
||||||
|
m.messages[i].content = fmt.Sprintf("%s failed", msg.Name)
|
||||||
|
} else {
|
||||||
|
m.messages[i].content = fmt.Sprintf("%s done", msg.Name)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.updateViewport()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case ResponseMsg:
|
||||||
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: "assistant",
|
||||||
|
content: msg.Content,
|
||||||
|
})
|
||||||
|
m.thinking = false
|
||||||
|
m.updateViewport()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case ErrorMsg:
|
||||||
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: "system",
|
||||||
|
content: fmt.Sprintf("Error: %v", msg.Err),
|
||||||
|
})
|
||||||
|
m.thinking = false
|
||||||
|
m.updateViewport()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case SlashCommandResultMsg:
|
||||||
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: "system",
|
||||||
|
content: msg.Result,
|
||||||
|
})
|
||||||
|
m.thinking = false
|
||||||
|
m.updateViewport()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass remaining messages to textarea
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textarea, cmd = m.textarea.Update(msg)
|
||||||
|
if cmd != nil {
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// View renders the TUI
|
||||||
|
func (m Model) View() string {
|
||||||
|
if m.quitting {
|
||||||
|
return "Goodbye!\n"
|
||||||
|
}
|
||||||
|
if !m.ready {
|
||||||
|
return "Initializing...\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
header := m.renderHeader()
|
||||||
|
sep := separatorStyle.Render(strings.Repeat("─", m.width))
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s\n%s\n%s\n%s\n%s",
|
||||||
|
header,
|
||||||
|
m.viewport.View(),
|
||||||
|
sep,
|
||||||
|
m.textarea.View(),
|
||||||
|
m.renderStatusBar(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleKeyMsg processes keyboard input
|
||||||
|
func (m Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.Type {
|
||||||
|
case tea.KeyCtrlC:
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case tea.KeyPgUp:
|
||||||
|
m.viewport.PageUp()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyPgDown:
|
||||||
|
m.viewport.PageDown()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyEnter:
|
||||||
|
input := strings.TrimSpace(m.textarea.Value())
|
||||||
|
if input == "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.textarea.Reset()
|
||||||
|
|
||||||
|
// Add user message to chat
|
||||||
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: "user",
|
||||||
|
content: input,
|
||||||
|
})
|
||||||
|
m.thinking = true
|
||||||
|
m.updateViewport()
|
||||||
|
|
||||||
|
return m, m.processMessage(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass to textarea for regular typing
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textarea, cmd = m.textarea.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWindowSize initializes or resizes the viewport and textarea
|
||||||
|
func (m Model) handleWindowSize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
|
||||||
|
vpHeight := msg.Height - chromeHeight
|
||||||
|
if vpHeight < 1 {
|
||||||
|
vpHeight = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.ready {
|
||||||
|
m.viewport = viewport.New(msg.Width, vpHeight)
|
||||||
|
m.ready = true
|
||||||
|
} else {
|
||||||
|
m.viewport.Width = msg.Width
|
||||||
|
m.viewport.Height = vpHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
m.textarea.SetWidth(msg.Width)
|
||||||
|
|
||||||
|
// Recreate renderer with updated word wrap
|
||||||
|
if r, err := glamour.NewTermRenderer(
|
||||||
|
glamour.WithAutoStyle(),
|
||||||
|
glamour.WithWordWrap(msg.Width-4),
|
||||||
|
); err == nil {
|
||||||
|
m.renderer = r
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateViewport()
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTick advances the thinking animation and polls agent events
|
||||||
|
func (m Model) handleTick() (tea.Model, tea.Cmd) {
|
||||||
|
cmds := []tea.Cmd{tickCmd()}
|
||||||
|
|
||||||
|
if m.thinking {
|
||||||
|
m.thinkFrame = (m.thinkFrame + 1) % thinkingFrameCount
|
||||||
|
m.updateViewport()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll events from the agent bridge
|
||||||
|
eventCmds := m.pollEvents()
|
||||||
|
cmds = append(cmds, eventCmds...)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processMessage sends user input to the agent loop in a goroutine
|
||||||
|
func (m Model) processMessage(input string) tea.Cmd {
|
||||||
|
agentLoop := m.agentLoop
|
||||||
|
sessionKey := m.sessionKey
|
||||||
|
|
||||||
|
return func() tea.Msg {
|
||||||
|
ctx := context.Background()
|
||||||
|
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorMsg{Err: err}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(input, "/") {
|
||||||
|
return SlashCommandResultMsg{Result: response}
|
||||||
|
}
|
||||||
|
return ResponseMsg{Content: response}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollEvents drains the event bridge channel, returning tea commands
|
||||||
|
func (m Model) pollEvents() []tea.Cmd {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-m.eventBridge.events:
|
||||||
|
cmd := m.convertEvent(event)
|
||||||
|
if cmd != nil {
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertEvent turns an agent event into a tea.Cmd that returns the matching message
|
||||||
|
func (m Model) convertEvent(event agent.AgentEvent) tea.Cmd {
|
||||||
|
switch event.Type {
|
||||||
|
case agent.EventThinkingStarted:
|
||||||
|
return func() tea.Msg { return ThinkingStartedMsg{} }
|
||||||
|
|
||||||
|
case agent.EventToolCallStarted:
|
||||||
|
if data, ok := event.Data.(agent.ToolCallStartedData); ok {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return ToolCallStartedMsg{
|
||||||
|
ID: data.ID,
|
||||||
|
Name: data.Name,
|
||||||
|
Args: data.Args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case agent.EventToolCallCompleted:
|
||||||
|
if data, ok := event.Data.(agent.ToolCallCompletedData); ok {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return ToolCallCompletedMsg{
|
||||||
|
ID: data.ID,
|
||||||
|
Name: data.Name,
|
||||||
|
Result: data.Result,
|
||||||
|
IsError: data.IsError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case agent.EventResponseComplete:
|
||||||
|
// The response is also returned by ProcessDirect, so we don't
|
||||||
|
// duplicate it here. The tick-based polling of EventResponseComplete
|
||||||
|
// is only used for streaming scenarios in the future.
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case agent.EventError:
|
||||||
|
if data, ok := event.Data.(agent.ErrorData); ok {
|
||||||
|
return func() tea.Msg { return ErrorMsg{Err: data.Err} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateViewport renders all messages and sets the viewport content
|
||||||
|
func (m *Model) updateViewport() {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
for _, msg := range m.messages {
|
||||||
|
switch msg.role {
|
||||||
|
case "user":
|
||||||
|
label := userLabelStyle.Render("You:")
|
||||||
|
sb.WriteString(label + " " + msg.content + "\n\n")
|
||||||
|
|
||||||
|
case "assistant":
|
||||||
|
label := assistantLabelStyle.Render("Assistant:")
|
||||||
|
rendered := msg.content
|
||||||
|
if m.renderer != nil {
|
||||||
|
if r, err := m.renderer.Render(msg.content); err == nil {
|
||||||
|
rendered = strings.TrimSpace(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(label + "\n" + rendered + "\n\n")
|
||||||
|
|
||||||
|
case "tool":
|
||||||
|
var styled string
|
||||||
|
switch {
|
||||||
|
case msg.toolErr:
|
||||||
|
styled = toolErrorStyle.Render("✗ " + msg.content)
|
||||||
|
case msg.toolDone:
|
||||||
|
styled = toolDoneStyle.Render("✓ " + msg.content)
|
||||||
|
default:
|
||||||
|
styled = toolRunningStyle.Render("⟳ " + msg.content)
|
||||||
|
}
|
||||||
|
sb.WriteString(" " + styled + "\n")
|
||||||
|
|
||||||
|
case "system":
|
||||||
|
sb.WriteString(msg.content + "\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append thinking indicator if active
|
||||||
|
if m.thinking {
|
||||||
|
frame := thinkingFrames[m.thinkFrame]
|
||||||
|
sb.WriteString(thinkingStyle.Render(frame+" Thinking...") + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.viewport.SetContent(sb.String())
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderHeader returns the header line
|
||||||
|
func (m Model) renderHeader() string {
|
||||||
|
title := lipgloss.NewStyle().
|
||||||
|
Foreground(primaryColor).
|
||||||
|
Bold(true).
|
||||||
|
Render("PicoClaw")
|
||||||
|
|
||||||
|
session := truncateSessionKey(m.sessionKey)
|
||||||
|
|
||||||
|
right := lipgloss.NewStyle().
|
||||||
|
Foreground(secondaryColor).
|
||||||
|
Render(fmt.Sprintf("[%s] %s", session, m.modelName))
|
||||||
|
|
||||||
|
gap := m.width - lipgloss.Width(title) - lipgloss.Width(right)
|
||||||
|
if gap < 1 {
|
||||||
|
gap = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return title + strings.Repeat(" ", gap) + right
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderStatusBar returns the status bar at the bottom
|
||||||
|
func (m Model) renderStatusBar() string {
|
||||||
|
left := "Ctrl+C: quit | PgUp/PgDn: scroll | Alt+Enter: newline"
|
||||||
|
pct := fmt.Sprintf("%3.0f%%", m.viewport.ScrollPercent()*100)
|
||||||
|
|
||||||
|
gap := m.width - lipgloss.Width(left) - lipgloss.Width(pct) - 2 // padding
|
||||||
|
if gap < 1 {
|
||||||
|
gap = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
bar := left + strings.Repeat(" ", gap) + pct
|
||||||
|
return statusBarStyle.Render(bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncateSessionKey shortens the session key for display
|
||||||
|
func truncateSessionKey(key string) string {
|
||||||
|
if len(key) <= maxSessionKeyLen {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return key[:maxSessionKeyLen] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
// tickCmd returns a command that sends a tickMsg after the tick interval
|
||||||
|
func tickCmd() tea.Cmd {
|
||||||
|
return tea.Tick(tickInterval, func(t time.Time) tea.Msg {
|
||||||
|
return tickMsg(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
294
pkg/tui/model_test.go
Normal file
294
pkg/tui/model_test.go
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
// PicoClaw - Ultra-lightweight personal AI agent
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTruncateSessionKey(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "short key unchanged",
|
||||||
|
input: "abc",
|
||||||
|
want: "abc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exact 15 chars unchanged",
|
||||||
|
input: "123456789012345",
|
||||||
|
want: "123456789012345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long key truncated with ellipsis",
|
||||||
|
input: "1234567890123456",
|
||||||
|
want: "123456789012345…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty key",
|
||||||
|
input: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := truncateSessionKey(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("truncateSessionKey(%q) = %q, want %q", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTestModel creates a Model for testing without requiring a real AgentLoop.
|
||||||
|
// The model is constructed directly, bypassing NewModel. We must use
|
||||||
|
// textarea.New() because the bubbles textarea panics on SetWidth if its
|
||||||
|
// internal state is nil.
|
||||||
|
func newTestModel() Model {
|
||||||
|
ta := textarea.New()
|
||||||
|
ta.SetHeight(textareaHeight)
|
||||||
|
return Model{
|
||||||
|
textarea: ta,
|
||||||
|
sessionKey: "test-session",
|
||||||
|
modelName: "test-model",
|
||||||
|
messages: []chatMessage{},
|
||||||
|
eventBridge: newEventBridge(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initTestModel creates a test model and sends a WindowSizeMsg to make it ready.
|
||||||
|
func initTestModel(t *testing.T) Model {
|
||||||
|
t.Helper()
|
||||||
|
m := newTestModel()
|
||||||
|
result, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
|
||||||
|
model, ok := result.(Model)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Update did not return a Model")
|
||||||
|
}
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_View_NotReady(t *testing.T) {
|
||||||
|
m := newTestModel()
|
||||||
|
view := m.View()
|
||||||
|
if !strings.Contains(view, "Initializing...") {
|
||||||
|
t.Errorf("expected 'Initializing...' in view, got %q", view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_View_Quitting(t *testing.T) {
|
||||||
|
m := newTestModel()
|
||||||
|
m.quitting = true
|
||||||
|
view := m.View()
|
||||||
|
if !strings.Contains(view, "Goodbye!") {
|
||||||
|
t.Errorf("expected 'Goodbye!' in view, got %q", view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_CtrlC_SetsQuitting(t *testing.T) {
|
||||||
|
m := initTestModel(t)
|
||||||
|
|
||||||
|
result, cmd := m.Update(tea.KeyMsg{Type: tea.KeyCtrlC})
|
||||||
|
model, ok := result.(Model)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Update did not return a Model")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.quitting {
|
||||||
|
t.Error("expected quitting to be true after Ctrl+C")
|
||||||
|
}
|
||||||
|
if cmd == nil {
|
||||||
|
t.Error("expected quit command, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_ResponseMsg_AddsAssistantMessage(t *testing.T) {
|
||||||
|
m := initTestModel(t)
|
||||||
|
m.thinking = true
|
||||||
|
|
||||||
|
result, _ := m.Update(ResponseMsg{Content: "Hello, world!"})
|
||||||
|
model, ok := result.(Model)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Update did not return a Model")
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.thinking {
|
||||||
|
t.Error("expected thinking to be false after ResponseMsg")
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, msg := range model.messages {
|
||||||
|
if msg.role == "assistant" && msg.content == "Hello, world!" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("expected assistant message 'Hello, world!' in messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_ToolCallStarted_AddsToolMessage(t *testing.T) {
|
||||||
|
m := initTestModel(t)
|
||||||
|
|
||||||
|
result, _ := m.Update(ToolCallStartedMsg{
|
||||||
|
ID: "tool-1",
|
||||||
|
Name: "web_search",
|
||||||
|
Args: `{"query":"test"}`,
|
||||||
|
})
|
||||||
|
model, ok := result.(Model)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Update did not return a Model")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(model.messages) != 1 {
|
||||||
|
t.Fatalf("expected 1 message, got %d", len(model.messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := model.messages[0]
|
||||||
|
if msg.role != "tool" {
|
||||||
|
t.Errorf("expected role 'tool', got %q", msg.role)
|
||||||
|
}
|
||||||
|
if msg.toolName != "web_search" {
|
||||||
|
t.Errorf("expected toolName 'web_search', got %q", msg.toolName)
|
||||||
|
}
|
||||||
|
if msg.toolID != "tool-1" {
|
||||||
|
t.Errorf("expected toolID 'tool-1', got %q", msg.toolID)
|
||||||
|
}
|
||||||
|
if msg.toolDone {
|
||||||
|
t.Error("expected toolDone to be false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_ToolCallCompleted_UpdatesToolMessage(t *testing.T) {
|
||||||
|
m := initTestModel(t)
|
||||||
|
|
||||||
|
// First start a tool
|
||||||
|
result, _ := m.Update(ToolCallStartedMsg{
|
||||||
|
ID: "tool-1",
|
||||||
|
Name: "web_search",
|
||||||
|
})
|
||||||
|
m = result.(Model)
|
||||||
|
|
||||||
|
// Then complete it
|
||||||
|
result, _ = m.Update(ToolCallCompletedMsg{
|
||||||
|
ID: "tool-1",
|
||||||
|
Name: "web_search",
|
||||||
|
})
|
||||||
|
model := result.(Model)
|
||||||
|
|
||||||
|
if len(model.messages) != 1 {
|
||||||
|
t.Fatalf("expected 1 message, got %d", len(model.messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := model.messages[0]
|
||||||
|
if !msg.toolDone {
|
||||||
|
t.Error("expected toolDone to be true after completion")
|
||||||
|
}
|
||||||
|
if msg.toolErr {
|
||||||
|
t.Error("expected toolErr to be false for successful completion")
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg.content, "done") {
|
||||||
|
t.Errorf("expected content to contain 'done', got %q", msg.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_ToolCallCompleted_Error(t *testing.T) {
|
||||||
|
m := initTestModel(t)
|
||||||
|
|
||||||
|
result, _ := m.Update(ToolCallStartedMsg{
|
||||||
|
ID: "tool-2",
|
||||||
|
Name: "exec",
|
||||||
|
})
|
||||||
|
m = result.(Model)
|
||||||
|
|
||||||
|
result, _ = m.Update(ToolCallCompletedMsg{
|
||||||
|
ID: "tool-2",
|
||||||
|
Name: "exec",
|
||||||
|
IsError: true,
|
||||||
|
})
|
||||||
|
model := result.(Model)
|
||||||
|
|
||||||
|
msg := model.messages[0]
|
||||||
|
if !msg.toolErr {
|
||||||
|
t.Error("expected toolErr to be true for error completion")
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg.content, "failed") {
|
||||||
|
t.Errorf("expected content to contain 'failed', got %q", msg.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_ErrorMsg_AddsSystemMessage(t *testing.T) {
|
||||||
|
m := initTestModel(t)
|
||||||
|
m.thinking = true
|
||||||
|
|
||||||
|
result, _ := m.Update(ErrorMsg{Err: fmt.Errorf("connection refused")})
|
||||||
|
model, ok := result.(Model)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Update did not return a Model")
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.thinking {
|
||||||
|
t.Error("expected thinking to be false after ErrorMsg")
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, msg := range model.messages {
|
||||||
|
if msg.role == "system" && strings.Contains(msg.content, "connection refused") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("expected system message containing 'connection refused'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_SlashCommandResult(t *testing.T) {
|
||||||
|
m := initTestModel(t)
|
||||||
|
|
||||||
|
result, _ := m.Update(SlashCommandResultMsg{Result: "Session: default"})
|
||||||
|
model := result.(Model)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, msg := range model.messages {
|
||||||
|
if msg.role == "system" && msg.content == "Session: default" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("expected system message with slash command result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel_Update_WindowSizeMsg_SetsReady(t *testing.T) {
|
||||||
|
m := newTestModel()
|
||||||
|
|
||||||
|
if m.ready {
|
||||||
|
t.Fatal("expected model to not be ready initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
|
||||||
|
model := result.(Model)
|
||||||
|
|
||||||
|
if !model.ready {
|
||||||
|
t.Error("expected model to be ready after WindowSizeMsg")
|
||||||
|
}
|
||||||
|
if model.width != 120 {
|
||||||
|
t.Errorf("expected width 120, got %d", model.width)
|
||||||
|
}
|
||||||
|
if model.height != 40 {
|
||||||
|
t.Errorf("expected height 40, got %d", model.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
pkg/tui/styles.go
Normal file
43
pkg/tui/styles.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// PicoClaw - Ultra-lightweight personal AI agent
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
var (
|
||||||
|
primaryColor = lipgloss.Color("#FF6B35") // Lobster orange
|
||||||
|
secondaryColor = lipgloss.Color("#7B8794")
|
||||||
|
errorColor = lipgloss.Color("#E74C3C")
|
||||||
|
successColor = lipgloss.Color("#2ECC71")
|
||||||
|
warningColor = lipgloss.Color("#F39C12")
|
||||||
|
|
||||||
|
userLabelStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(primaryColor).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
assistantLabelStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#3498DB")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
toolRunningStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(warningColor)
|
||||||
|
|
||||||
|
toolDoneStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(successColor)
|
||||||
|
|
||||||
|
toolErrorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(errorColor)
|
||||||
|
|
||||||
|
statusBarStyle = lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("#2C3E50")).
|
||||||
|
Foreground(lipgloss.Color("#ECF0F1")).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
thinkingStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(secondaryColor).
|
||||||
|
Italic(true)
|
||||||
|
|
||||||
|
separatorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#34495E"))
|
||||||
|
)
|
||||||
Loading…
Add table
Reference in a new issue