From 8d874b839db169906e18e4277cd198504018e022 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Sat, 12 Apr 2025 02:01:45 +0200 Subject: add initial message handling --- internal/message/message.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'internal/message/message.go') diff --git a/internal/message/message.go b/internal/message/message.go index 13cf54048..eeeb83ed2 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -2,17 +2,20 @@ package message import ( "context" + "database/sql" "encoding/json" "fmt" "github.com/google/uuid" "github.com/kujtimiihoxha/termai/internal/db" + "github.com/kujtimiihoxha/termai/internal/llm/models" "github.com/kujtimiihoxha/termai/internal/pubsub" ) type CreateMessageParams struct { Role MessageRole Parts []ContentPart + Model models.ModelID } type Service interface { @@ -68,6 +71,7 @@ func (s *service) Create(sessionID string, params CreateMessageParams) (Message, SessionID: sessionID, Role: string(params.Role), Parts: string(partsJSON), + Model: sql.NullString{String: string(params.Model), Valid: true}, }) if err != nil { return Message{}, err @@ -101,9 +105,15 @@ func (s *service) Update(message Message) error { if err != nil { return err } + finishedAt := sql.NullInt64{} + if f := message.FinishPart(); f != nil { + finishedAt.Int64 = f.Time + finishedAt.Valid = true + } err = s.q.UpdateMessage(s.ctx, db.UpdateMessageParams{ - ID: message.ID, - Parts: string(parts), + ID: message.ID, + Parts: string(parts), + FinishedAt: finishedAt, }) if err != nil { return err -- cgit v1.2.3 From 0697dcc1d9c7330d8c9d8a2be0bb94b3d46c9345 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Sat, 12 Apr 2025 14:49:01 +0200 Subject: implement nested tool calls and initial setup for result metadata --- go.mod | 21 +- go.sum | 43 +-- internal/llm/agent/agent.go | 1 + internal/llm/tools/bash.go | 13 +- internal/llm/tools/tools.go | 23 +- internal/message/content.go | 5 +- internal/message/message.go | 1 + internal/tui/components/chat/editor.go | 15 +- internal/tui/components/chat/messages.go | 458 +++++++++++++++++++++++-------- internal/tui/components/chat/sidebar.go | 11 +- internal/tui/page/chat.go | 62 ++++- internal/tui/styles/background.go | 81 ++++++ internal/tui/styles/markdown.go | 7 +- internal/tui/styles/styles.go | 10 + 14 files changed, 584 insertions(+), 167 deletions(-) create mode 100644 internal/tui/styles/background.go (limited to 'internal/message/message.go') diff --git a/go.mod b/go.mod index 63df37fba..3b8bd99b1 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/charmbracelet/glamour v0.9.1 github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/ansi v0.8.0 github.com/fsnotify/fsnotify v1.8.0 github.com/go-logfmt/logfmt v0.6.0 github.com/golang-migrate/migrate/v4 v4.18.2 @@ -29,11 +30,11 @@ require ( github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.16.0 github.com/openai/openai-go v0.1.0-beta.2 - github.com/sergi/go-diff v1.3.1 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.0 github.com/stretchr/testify v1.10.0 - golang.org/x/net v0.34.0 + golang.org/x/net v0.39.0 google.golang.org/api v0.215.0 ) @@ -64,7 +65,6 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect @@ -76,6 +76,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect @@ -92,6 +93,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -115,20 +117,21 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.design/x/clipboard v0.7.0 // indirect - golang.org/x/crypto v0.33.0 // indirect - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/image v0.14.0 // indirect golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.8.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect google.golang.org/grpc v1.67.3 // indirect google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c4b32ef32..08e7e7c42 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg= github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -139,6 +139,7 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -189,8 +190,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= @@ -199,8 +200,9 @@ github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8 github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -261,10 +263,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 h1:bFYqOIMdeiCEdzPJkLiOoMDzW/v3tjW4AA/RmUZYsL8= golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= @@ -282,15 +284,15 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -304,8 +306,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -314,8 +316,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -323,8 +325,8 @@ 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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -343,8 +345,9 @@ google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7Qf google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 998dc1551..b01ffec3c 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -305,6 +305,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{ Role: message.Assistant, Parts: []message.ContentPart{}, + Model: c.model.ID, }) if err != nil { return err diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index 4e80ae60a..d20afb7f2 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/kujtimiihoxha/termai/internal/config" "github.com/kujtimiihoxha/termai/internal/llm/tools/shell" @@ -21,6 +22,9 @@ type BashPermissionsParams struct { Timeout int `json:"timeout"` } +type BashToolResponseMetadata struct { + Took int64 `json:"took"` +} type bashTool struct { permissions permission.Service } @@ -272,11 +276,13 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) return NewTextErrorResponse("permission denied"), nil } } + startTime := time.Now() shell := shell.GetPersistentShell(config.WorkingDirectory()) stdout, stderr, exitCode, interrupted, err := shell.Exec(ctx, params.Command, params.Timeout) if err != nil { return NewTextErrorResponse(fmt.Sprintf("error executing command: %s", err)), nil } + took := time.Since(startTime).Milliseconds() stdout = truncateOutput(stdout) stderr = truncateOutput(stderr) @@ -304,10 +310,13 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) stdout += "\n" + errorMessage } + metadata := BashToolResponseMetadata{ + Took: took, + } if stdout == "" { - return NewTextResponse("no output"), nil + return WithResponseMetadata(NewTextResponse("no output"), metadata), nil } - return NewTextResponse(stdout), nil + return WithResponseMetadata(NewTextResponse(stdout), metadata), nil } func truncateOutput(content string) string { diff --git a/internal/llm/tools/tools.go b/internal/llm/tools/tools.go index e15c1c31f..6bb528686 100644 --- a/internal/llm/tools/tools.go +++ b/internal/llm/tools/tools.go @@ -1,6 +1,9 @@ package tools -import "context" +import ( + "context" + "encoding/json" +) type ToolInfo struct { Name string @@ -17,9 +20,10 @@ const ( ) type ToolResponse struct { - Type toolResponseType `json:"type"` - Content string `json:"content"` - IsError bool `json:"is_error"` + Type toolResponseType `json:"type"` + Content string `json:"content"` + Metadata string `json:"metadata,omitempty"` + IsError bool `json:"is_error"` } func NewTextResponse(content string) ToolResponse { @@ -29,6 +33,17 @@ func NewTextResponse(content string) ToolResponse { } } +func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse { + if metadata != nil { + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return response + } + response.Metadata = string(metadataBytes) + } + return response +} + func NewTextErrorResponse(content string) ToolResponse { return ToolResponse{ Type: ToolResponseTypeText, diff --git a/internal/message/content.go b/internal/message/content.go index cd263798b..422c04f52 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -3,6 +3,8 @@ package message import ( "encoding/base64" "time" + + "github.com/kujtimiihoxha/termai/internal/llm/models" ) type MessageRole string @@ -65,7 +67,6 @@ type ToolCall struct { Name string `json:"name"` Input string `json:"input"` Type string `json:"type"` - Metadata any `json:"metadata"` Finished bool `json:"finished"` } @@ -75,6 +76,7 @@ type ToolResult struct { ToolCallID string `json:"tool_call_id"` Name string `json:"name"` Content string `json:"content"` + Metadata string `json:"metadata"` IsError bool `json:"is_error"` } @@ -92,6 +94,7 @@ type Message struct { Role MessageRole SessionID string Parts []ContentPart + Model models.ModelID CreatedAt int64 UpdatedAt int64 diff --git a/internal/message/message.go b/internal/message/message.go index eeeb83ed2..06dae13a5 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -155,6 +155,7 @@ func (s *service) fromDBItem(item db.Message) (Message, error) { SessionID: item.SessionID, Role: MessageRole(item.Role), Parts: parts, + Model: models.ModelID(item.Model.String), CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }, nil diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index df336818c..e87f1ffae 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -77,21 +77,20 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case AgentWorkingMsg: m.agentWorking = bool(msg) case tea.KeyMsg: - if key.Matches(msg, focusedKeyMaps.Send) { + // if the key does not match any binding, return + if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) { return m, m.send() } - if key.Matches(msg, bluredKeyMaps.Send) { + if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) { return m, m.send() } - if key.Matches(msg, focusedKeyMaps.Blur) { + if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Blur) { m.textarea.Blur() return m, util.CmdHandler(EditorFocusMsg(false)) } - if key.Matches(msg, bluredKeyMaps.Focus) { - if !m.textarea.Focused() { - m.textarea.Focus() - return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true))) - } + if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) { + m.textarea.Focus() + return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true))) } } m.textarea, cmd = m.textarea.Update(msg) diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index 0a7e6e2a4..b5a361392 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -1,16 +1,21 @@ package chat import ( + "encoding/json" "fmt" - "regexp" - "strconv" + "math" "strings" + "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/ansi" "github.com/kujtimiihoxha/termai/internal/app" + "github.com/kujtimiihoxha/termai/internal/llm/agent" + "github.com/kujtimiihoxha/termai/internal/llm/models" + "github.com/kujtimiihoxha/termai/internal/llm/tools" "github.com/kujtimiihoxha/termai/internal/message" "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/session" @@ -18,10 +23,20 @@ import ( "github.com/kujtimiihoxha/termai/internal/tui/util" ) +type uiMessageType int + +const ( + userMessageType uiMessageType = iota + assistantMessageType + toolMessageType +) + type uiMessage struct { - position int - height int - content string + ID string + messageType uiMessageType + position int + height int + content string } type messagesCmp struct { @@ -32,141 +47,116 @@ type messagesCmp struct { session session.Session messages []message.Message uiMessages []uiMessage - currentIndex int + currentMsgID string renderer *glamour.TermRenderer focusRenderer *glamour.TermRenderer cachedContent map[string]string + agentWorking bool + spinner spinner.Model + needsRerender bool + lastViewport string } func (m *messagesCmp) Init() tea.Cmd { - return m.viewport.Init() -} - -var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m") - -func hexToBgSGR(hex string) (string, error) { - hex = strings.TrimPrefix(hex, "#") - if len(hex) != 6 { - return "", fmt.Errorf("invalid hex color: must be 6 hexadecimal digits") - } - - // Parse RGB components in one block - rgb := make([]uint64, 3) - for i := 0; i < 3; i++ { - val, err := strconv.ParseUint(hex[i*2:i*2+2], 16, 8) - if err != nil { - return "", err - } - rgb[i] = val - } - - return fmt.Sprintf("48;2;%d;%d;%d", rgb[0], rgb[1], rgb[2]), nil -} - -func forceReplaceBackgroundColors(input string, newBg string) string { - return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string { - // Extract content between "\x1b[" and "m" - content := seq[2 : len(seq)-1] - tokens := strings.Split(content, ";") - var newTokens []string - - // Skip background color tokens - for i := 0; i < len(tokens); i++ { - if tokens[i] == "" { - continue - } - - val, err := strconv.Atoi(tokens[i]) - if err != nil { - newTokens = append(newTokens, tokens[i]) - continue - } - - // Skip background color tokens - if val == 48 { - // Skip "48;5;N" or "48;2;R;G;B" sequences - if i+1 < len(tokens) { - if nextVal, err := strconv.Atoi(tokens[i+1]); err == nil { - switch nextVal { - case 5: - i += 2 // Skip "5" and color index - case 2: - i += 4 // Skip "2" and RGB components - } - } - } - } else if (val < 40 || val > 47) && (val < 100 || val > 107) && val != 49 { - // Keep non-background tokens - newTokens = append(newTokens, tokens[i]) - } - } - - // Add new background if provided - if newBg != "" { - newTokens = append(newTokens, strings.Split(newBg, ";")...) - } - - if len(newTokens) == 0 { - return "" - } - - return "\x1b[" + strings.Join(newTokens, ";") + "m" - }) + return tea.Batch(m.viewport.Init()) } func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd switch msg := msg.(type) { + case AgentWorkingMsg: + m.agentWorking = bool(msg) + if m.agentWorking { + cmds = append(cmds, m.spinner.Tick) + } case EditorFocusMsg: m.writingMode = bool(msg) case SessionSelectedMsg: if msg.ID != m.session.ID { cmd := m.SetSession(msg) + m.needsRerender = true return m, cmd } return m, nil + case SessionClearedMsg: + m.session = session.Session{} + m.messages = make([]message.Message, 0) + m.currentMsgID = "" + m.needsRerender = true + return m, nil + + case tea.KeyMsg: + if m.writingMode { + return m, nil + } case pubsub.Event[message.Message]: if msg.Type == pubsub.CreatedEvent { if msg.Payload.SessionID == m.session.ID { // check if message exists + + messageExists := false for _, v := range m.messages { if v.ID == msg.Payload.ID { - return m, nil + messageExists = true + break } } - m.messages = append(m.messages, msg.Payload) - m.renderView() - m.viewport.GotoBottom() + if !messageExists { + m.messages = append(m.messages, msg.Payload) + delete(m.cachedContent, m.currentMsgID) + m.currentMsgID = msg.Payload.ID + m.needsRerender = true + } } for _, v := range m.messages { for _, c := range v.ToolCalls() { // the message is being added to the session of a tool called if c.ID == msg.Payload.SessionID { - m.renderView() - m.viewport.GotoBottom() + m.needsRerender = true } } } } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID { for i, v := range m.messages { if v.ID == msg.Payload.ID { + if !m.messages[i].IsFinished() && msg.Payload.IsFinished() && msg.Payload.FinishReason() == "end_turn" || msg.Payload.FinishReason() == "canceled" { + cmds = append(cmds, util.CmdHandler(AgentWorkingMsg(false))) + } m.messages[i] = msg.Payload delete(m.cachedContent, msg.Payload.ID) - m.renderView() - if i == len(m.messages)-1 { - m.viewport.GotoBottom() - } + m.needsRerender = true break } } } } + if m.agentWorking { + u, cmd := m.spinner.Update(msg) + m.spinner = u + cmds = append(cmds, cmd) + } + oldPos := m.viewport.YPosition u, cmd := m.viewport.Update(msg) m.viewport = u - return m, cmd + m.needsRerender = m.needsRerender || m.viewport.YPosition != oldPos + cmds = append(cmds, cmd) + if m.needsRerender { + m.renderView() + if len(m.messages) > 0 { + if msg, ok := msg.(pubsub.Event[message.Message]); ok { + if (msg.Type == pubsub.CreatedEvent) || + (msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) { + m.viewport.GotoBottom() + } + } + } + m.needsRerender = false + } + return m, tea.Batch(cmds...) } -func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string { +func (m *messagesCmp) renderSimpleMessage(msg message.Message, info ...string) string { if v, ok := m.cachedContent[msg.ID]; ok { return v } @@ -178,7 +168,7 @@ func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string { BorderStyle(lipgloss.ThickBorder()) renderer := m.renderer - if inx == m.currentIndex { + if msg.ID == m.currentMsgID { style = style. Foreground(styles.Forground). BorderForeground(styles.Blue). @@ -186,33 +176,269 @@ func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string { renderer = m.focusRenderer } c, _ := renderer.Render(msg.Content().String()) - col, _ := hexToBgSGR(styles.Background.Dark) - rendered := style.Render(forceReplaceBackgroundColors(c, col)) + parts := []string{ + styles.ForceReplaceBackgroundWithLipgloss(c, styles.Background), + } + // remove newline at the end + parts[0] = strings.TrimSuffix(parts[0], "\n") + if len(info) > 0 { + parts = append(parts, info...) + } + rendered := style.Render( + lipgloss.JoinVertical( + lipgloss.Left, + parts..., + ), + ) m.cachedContent[msg.ID] = rendered return rendered } +func formatTimeDifference(unixTime1, unixTime2 int64) string { + diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1))) + + if diffSeconds < 60 { + return fmt.Sprintf("%.1fs", diffSeconds) + } + + minutes := int(diffSeconds / 60) + seconds := int(diffSeconds) % 60 + return fmt.Sprintf("%dm%ds", minutes, seconds) +} + +func (m *messagesCmp) renderToolCall(toolCall message.ToolCall, isNested bool) string { + key := "" + value := "" + switch toolCall.Name { + // TODO: add result data to the tools + case agent.AgentToolName: + key = "Task" + var params agent.AgentParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + value = params.Prompt + // TODO: handle nested calls + case tools.BashToolName: + key = "Bash" + var params tools.BashParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + value = params.Command + case tools.EditToolName: + key = "Edit" + var params tools.EditParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + value = params.FilePath + case tools.FetchToolName: + key = "Fetch" + var params tools.FetchParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + value = params.URL + case tools.GlobToolName: + key = "Glob" + var params tools.GlobParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + if params.Path == "" { + params.Path = "." + } + value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path) + case tools.GrepToolName: + key = "Grep" + var params tools.GrepParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + if params.Path == "" { + params.Path = "." + } + value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path) + case tools.LSToolName: + key = "Ls" + var params tools.LSParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + if params.Path == "" { + params.Path = "." + } + value = params.Path + case tools.SourcegraphToolName: + key = "Sourcegraph" + var params tools.SourcegraphParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + value = params.Query + case tools.ViewToolName: + key = "View" + var params tools.ViewParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + value = params.FilePath + case tools.WriteToolName: + key = "Write" + var params tools.WriteParams + json.Unmarshal([]byte(toolCall.Input), ¶ms) + value = params.FilePath + default: + key = toolCall.Name + var params map[string]any + json.Unmarshal([]byte(toolCall.Input), ¶ms) + jsonData, _ := json.Marshal(params) + value = string(jsonData) + } + + style := styles.BaseStyle. + Width(m.width). + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + PaddingLeft(1). + BorderForeground(styles.Yellow) + + keyStyle := styles.BaseStyle. + Foreground(styles.ForgroundDim) + valyeStyle := styles.BaseStyle. + Foreground(styles.Forground) + + if isNested { + valyeStyle = valyeStyle.Foreground(styles.ForgroundMid) + } + keyValye := keyStyle.Render( + fmt.Sprintf("%s: ", key), + ) + if !isNested { + value = valyeStyle. + Width(m.width - lipgloss.Width(keyValye) - 2). + Render( + ansi.Truncate( + value, + m.width-lipgloss.Width(keyValye)-2, + "...", + ), + ) + } else { + keyValye = keyStyle.Render( + fmt.Sprintf(" └ %s: ", key), + ) + value = valyeStyle. + Width(m.width - lipgloss.Width(keyValye) - 2). + Render( + ansi.Truncate( + value, + m.width-lipgloss.Width(keyValye)-2, + "...", + ), + ) + } + + innerToolCalls := make([]string, 0) + if toolCall.Name == agent.AgentToolName { + messages, _ := m.app.Messages.List(toolCall.ID) + toolCalls := make([]message.ToolCall, 0) + for _, v := range messages { + toolCalls = append(toolCalls, v.ToolCalls()...) + } + for _, v := range toolCalls { + call := m.renderToolCall(v, true) + innerToolCalls = append(innerToolCalls, call) + } + } + + if isNested { + return lipgloss.JoinHorizontal( + lipgloss.Left, + keyValye, + value, + ) + } + callContent := lipgloss.JoinHorizontal( + lipgloss.Left, + keyValye, + value, + ) + callContent = strings.ReplaceAll(callContent, "\n", "") + if len(innerToolCalls) > 0 { + callContent = lipgloss.JoinVertical( + lipgloss.Left, + callContent, + lipgloss.JoinVertical( + lipgloss.Left, + innerToolCalls..., + ), + ) + } + return style.Render(callContent) +} + +func (m *messagesCmp) renderAssistantMessage(msg message.Message) []uiMessage { + // find the user message that is before this assistant message + var userMsg message.Message + for i := len(m.messages) - 1; i >= 0; i-- { + if m.messages[i].Role == message.User { + userMsg = m.messages[i] + break + } + } + messages := make([]uiMessage, 0) + if msg.Content().String() != "" { + info := make([]string, 0) + if msg.IsFinished() && msg.FinishReason() == "end_turn" { + finish := msg.FinishPart() + took := formatTimeDifference(userMsg.CreatedAt, finish.Time) + + info = append(info, styles.BaseStyle.Width(m.width-1).Foreground(styles.ForgroundDim).Render( + fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took), + )) + } + content := m.renderSimpleMessage(msg, info...) + messages = append(messages, uiMessage{ + messageType: assistantMessageType, + position: 0, // gets updated in renderView + height: lipgloss.Height(content), + content: content, + }) + } + for _, v := range msg.ToolCalls() { + content := m.renderToolCall(v, false) + messages = append(messages, + uiMessage{ + messageType: toolMessageType, + position: 0, // gets updated in renderView + height: lipgloss.Height(content), + content: content, + }, + ) + } + + return messages +} + func (m *messagesCmp) renderView() { m.uiMessages = make([]uiMessage, 0) pos := 0 for _, v := range m.messages { - content := "" switch v.Role { case message.User: - content = m.renderUserMessage(pos, v) + content := m.renderSimpleMessage(v) + m.uiMessages = append(m.uiMessages, uiMessage{ + messageType: userMessageType, + position: pos, + height: lipgloss.Height(content), + content: content, + }) + pos += lipgloss.Height(content) + 1 // + 1 for spacing + case message.Assistant: + assistantMessages := m.renderAssistantMessage(v) + for _, msg := range assistantMessages { + msg.position = pos + m.uiMessages = append(m.uiMessages, msg) + pos += msg.height + 1 // + 1 for spacing + } + } - m.uiMessages = append(m.uiMessages, uiMessage{ - position: pos, - height: lipgloss.Height(content), - content: content, - }) - pos += lipgloss.Height(content) + 1 // + 1 for spacing } messages := make([]string, 0) for _, v := range m.uiMessages { - messages = append(messages, v.content) + messages = append(messages, v.content, + styles.BaseStyle. + Width(m.width). + Render( + "", + ), + ) } m.viewport.SetContent( styles.BaseStyle. @@ -246,7 +472,6 @@ func (m *messagesCmp) View() string { ) } - m.renderView() return styles.BaseStyle. Width(m.width). Render( @@ -260,15 +485,21 @@ func (m *messagesCmp) View() string { func (m *messagesCmp) help() string { text := "" + + if m.agentWorking { + text += styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render( + fmt.Sprintf("%s %s ", m.spinner.View(), "Generating..."), + ) + } if m.writingMode { - text = lipgloss.JoinHorizontal( + text += lipgloss.JoinHorizontal( lipgloss.Left, styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "), styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"), styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"), ) } else { - text = lipgloss.JoinHorizontal( + text += lipgloss.JoinHorizontal( lipgloss.Left, styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "), styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"), @@ -306,7 +537,15 @@ func (m *messagesCmp) SetSize(width, height int) { glamour.WithWordWrap(width-1), ) m.focusRenderer = focusRenderer + // clear the cached content + for k := range m.cachedContent { + delete(m.cachedContent, k) + } m.renderer = renderer + if len(m.messages) > 0 { + m.renderView() + m.viewport.GotoBottom() + } } func (m *messagesCmp) GetSize() (int, int) { @@ -320,7 +559,8 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd { return util.ReportError(err) } m.messages = messages - m.messages = append(m.messages, m.messages[0]) + m.currentMsgID = m.messages[len(m.messages)-1].ID + m.needsRerender = true return nil } @@ -333,6 +573,9 @@ func NewMessagesCmp(app *app.App) tea.Model { glamour.WithStyles(styles.MarkdownTheme(false)), glamour.WithWordWrap(80), ) + + s := spinner.New() + s.Spinner = spinner.Pulse return &messagesCmp{ app: app, writingMode: true, @@ -340,5 +583,6 @@ func NewMessagesCmp(app *app.App) tea.Model { viewport: viewport.New(0, 0), focusRenderer: focusRenderer, renderer: renderer, + spinner: s, } } diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index 65c06f4a1..51192cf9a 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -5,6 +5,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/session" "github.com/kujtimiihoxha/termai/internal/tui/styles" ) @@ -19,6 +20,14 @@ func (m *sidebarCmp) Init() tea.Cmd { } func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case pubsub.Event[session.Session]: + if msg.Type == pubsub.UpdatedEvent { + if m.session.ID == msg.Payload.ID { + m.session = msg.Payload + } + } + } return m, nil } @@ -45,7 +54,7 @@ func (m *sidebarCmp) sessionSection() string { sessionValue := styles.BaseStyle. Foreground(styles.Forground). Width(m.width - lipgloss.Width(sessionKey)). - Render(": New Session") + Render(fmt.Sprintf(": %s", m.session.Title)) return lipgloss.JoinHorizontal( lipgloss.Left, sessionKey, diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index 7ac0d2293..a7a51bb84 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -1,9 +1,10 @@ package page import ( + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/termai/internal/llm/agent" "github.com/kujtimiihoxha/termai/internal/session" "github.com/kujtimiihoxha/termai/internal/tui/components/chat" "github.com/kujtimiihoxha/termai/internal/tui/layout" @@ -18,8 +19,32 @@ type chatPage struct { session session.Session } +type ChatKeyMap struct { + NewSession key.Binding +} + +var keyMap = ChatKeyMap{ + NewSession: key.NewBinding( + key.WithKeys("ctrl+n"), + key.WithHelp("ctrl+n", "new session"), + ), +} + func (p *chatPage) Init() tea.Cmd { - return p.layout.Init() + // TODO: remove + cmds := []tea.Cmd{ + p.layout.Init(), + } + + sessions, _ := p.app.Sessions.List() + if len(sessions) > 0 { + p.session = sessions[0] + cmd := p.setSidebar() + cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(p.session)), cmd) + } + return tea.Batch( + cmds..., + ) } func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -31,6 +56,13 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if cmd != nil { return p, cmd } + case tea.KeyMsg: + switch { + case key.Matches(msg, keyMap.NewSession): + p.session = session.Session{} + p.clearSidebar() + return p, util.CmdHandler(chat.SessionClearedMsg{}) + } } u, cmd := p.layout.Update(msg) p.layout = u.(layout.SplitPaneLayout) @@ -51,6 +83,12 @@ func (p *chatPage) setSidebar() tea.Cmd { return sidebarContainer.Init() } +func (p *chatPage) clearSidebar() { + p.layout.SetRightPanel(nil) + width, height := p.layout.GetSize() + p.layout.SetSize(width, height) +} + func (p *chatPage) sendMessage(text string) tea.Cmd { var cmds []tea.Cmd if p.session.ID == "" { @@ -66,15 +104,15 @@ func (p *chatPage) sendMessage(text string) tea.Cmd { } cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session))) } - // TODO: actually call agent - p.app.Messages.Create(p.session.ID, message.CreateMessageParams{ - Role: message.User, - Parts: []message.ContentPart{ - message.TextContent{ - Text: text, - }, - }, - }) + // TODO: move this to a service + a, err := agent.NewCoderAgent(p.app) + if err != nil { + return util.ReportError(err) + } + go func() { + a.Generate(p.app.Context, p.session.ID, text) + }() + return tea.Batch(cmds...) } @@ -85,7 +123,7 @@ func (p *chatPage) View() string { func NewChatPage(app *app.App) tea.Model { messagesContainer := layout.NewContainer( chat.NewMessagesCmp(app), - layout.WithPadding(1, 1, 1, 1), + layout.WithPadding(1, 1, 0, 1), ) editorContainer := layout.NewContainer( diff --git a/internal/tui/styles/background.go b/internal/tui/styles/background.go new file mode 100644 index 000000000..bf6cbc105 --- /dev/null +++ b/internal/tui/styles/background.go @@ -0,0 +1,81 @@ +package styles + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m") + +func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) { + r, g, b, a := c.RGBA() + + // Un-premultiply alpha if needed + if a > 0 && a < 0xffff { + r = (r * 0xffff) / a + g = (g * 0xffff) / a + b = (b * 0xffff) / a + } + + // Convert from 16-bit to 8-bit color + return uint8(r >> 8), uint8(g >> 8), uint8(b >> 8) +} + +func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string { + r, g, b := getColorRGB(newBgColor) + + newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b) + + return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string { + // Extract content between "\x1b[" and "m" + content := seq[2 : len(seq)-1] + tokens := strings.Split(content, ";") + var newTokens []string + + // Skip background color tokens + for i := 0; i < len(tokens); i++ { + if tokens[i] == "" { + continue + } + + val, err := strconv.Atoi(tokens[i]) + if err != nil { + newTokens = append(newTokens, tokens[i]) + continue + } + + // Skip background color tokens + if val == 48 { + // Skip "48;5;N" or "48;2;R;G;B" sequences + if i+1 < len(tokens) { + if nextVal, err := strconv.Atoi(tokens[i+1]); err == nil { + switch nextVal { + case 5: + i += 2 // Skip "5" and color index + case 2: + i += 4 // Skip "2" and RGB components + } + } + } + } else if (val < 40 || val > 47) && (val < 100 || val > 107) && val != 49 { + // Keep non-background tokens + newTokens = append(newTokens, tokens[i]) + } + } + + // Add new background if provided + if newBg != "" { + newTokens = append(newTokens, strings.Split(newBg, ";")...) + } + + if len(newTokens) == 0 { + return "" + } + + return "\x1b[" + strings.Join(newTokens, ";") + "m" + }) +} diff --git a/internal/tui/styles/markdown.go b/internal/tui/styles/markdown.go index b4e71c51e..52816eab3 100644 --- a/internal/tui/styles/markdown.go +++ b/internal/tui/styles/markdown.go @@ -515,6 +515,7 @@ var ASCIIStyleConfig = ansi.StyleConfig{ Document: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ BackgroundColor: stringPtr(Background.Dark), + Color: stringPtr(ForgroundDim.Dark), }, Indent: uintPtr(1), IndentToken: stringPtr(BaseStyle.Render(" ")), @@ -688,7 +689,7 @@ var DraculaStyleConfig = ansi.StyleConfig{ Heading: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ BlockSuffix: "\n", - Color: stringPtr("#bd93f9"), + Color: stringPtr(PrimaryColor.Dark), Bold: boolPtr(true), BackgroundColor: stringPtr(Background.Dark), }, @@ -740,7 +741,7 @@ var DraculaStyleConfig = ansi.StyleConfig{ }, Strong: ansi.StylePrimitive{ Bold: boolPtr(true), - Color: stringPtr("#ffb86c"), + Color: stringPtr(Blue.Dark), BackgroundColor: stringPtr(Background.Dark), }, HorizontalRule: ansi.StylePrimitive{ @@ -796,7 +797,7 @@ var DraculaStyleConfig = ansi.StyleConfig{ CodeBlock: ansi.StyleCodeBlock{ StyleBlock: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr("#ffb86c"), + Color: stringPtr(Blue.Dark), BackgroundColor: stringPtr(Background.Dark), }, Margin: uintPtr(defaultMargin), diff --git a/internal/tui/styles/styles.go b/internal/tui/styles/styles.go index 41863cf1b..476339b57 100644 --- a/internal/tui/styles/styles.go +++ b/internal/tui/styles/styles.go @@ -34,6 +34,11 @@ var ( Light: "#d3d3d3", } + ForgroundMid = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", + Light: "#a0a0a0", + } + ForgroundDim = lipgloss.AdaptiveColor{ Dark: "#737373", Light: "#737373", @@ -159,6 +164,11 @@ var ( Light: light.Peach().Hex, } + Yellow = lipgloss.AdaptiveColor{ + Dark: dark.Yellow().Hex, + Light: light.Yellow().Hex, + } + Primary = Blue Secondary = Mauve -- cgit v1.2.3 From 3ad983db0f2c08826d56cb5de274d706c95b3353 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Sun, 13 Apr 2025 13:17:17 +0200 Subject: cleanup app, config and root --- .gitignore | 2 +- .opencode.json | 11 ++ .termai.json | 11 -- cmd/git/main.go | 4 - cmd/root.go | 253 +++++++++++++++++++++++-------- internal/app/app.go | 76 ++++++++++ internal/app/lsp.go | 108 +++++++++++++ internal/app/services.go | 64 -------- internal/config/config.go | 20 +-- internal/history/file.go | 73 +++++---- internal/llm/agent/agent-tool.go | 10 +- internal/llm/agent/agent.go | 53 +++---- internal/llm/agent/coder.go | 5 +- internal/llm/agent/task.go | 3 +- internal/message/message.go | 46 +++--- internal/session/session.go | 44 +++--- internal/tui/components/chat/messages.go | 5 +- internal/tui/components/repl/editor.go | 4 +- internal/tui/components/repl/messages.go | 7 +- internal/tui/components/repl/sessions.go | 4 +- internal/tui/page/chat.go | 8 +- internal/tui/tui.go | 6 +- 22 files changed, 525 insertions(+), 292 deletions(-) create mode 100644 .opencode.json delete mode 100644 .termai.json delete mode 100644 cmd/git/main.go create mode 100644 internal/app/app.go create mode 100644 internal/app/lsp.go delete mode 100644 internal/app/services.go (limited to 'internal/message/message.go') diff --git a/.gitignore b/.gitignore index 388f8b2ca..0ef6e2aef 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,6 @@ debug.log .env .env.local -.termai +.opencode internal/assets/diff/index.mjs diff --git a/.opencode.json b/.opencode.json new file mode 100644 index 000000000..f63a63dba --- /dev/null +++ b/.opencode.json @@ -0,0 +1,11 @@ +{ + "model": { + "coder": "claude-3.7-sonnet", + "coderMaxTokens": 20000 + }, + "lsp": { + "gopls": { + "command": "gopls" + } + } +} diff --git a/.termai.json b/.termai.json deleted file mode 100644 index f63a63dba..000000000 --- a/.termai.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "model": { - "coder": "claude-3.7-sonnet", - "coderMaxTokens": 20000 - }, - "lsp": { - "gopls": { - "command": "gopls" - } - } -} diff --git a/cmd/git/main.go b/cmd/git/main.go deleted file mode 100644 index da29a2cad..000000000 --- a/cmd/git/main.go +++ /dev/null @@ -1,4 +0,0 @@ -package main - -func main() { -} diff --git a/cmd/root.go b/cmd/root.go index d846a14c2..092606de7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,9 +2,10 @@ package cmd import ( "context" - "log/slog" + "fmt" "os" "sync" + "time" tea "github.com/charmbracelet/bubbletea" "github.com/kujtimiihoxha/termai/internal/app" @@ -13,6 +14,7 @@ import ( "github.com/kujtimiihoxha/termai/internal/db" "github.com/kujtimiihoxha/termai/internal/llm/agent" "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/tui" zone "github.com/lrstanley/bubblezone" "github.com/spf13/cobra" @@ -23,111 +25,229 @@ var rootCmd = &cobra.Command{ Short: "A terminal ai assistant", Long: `A terminal ai assistant`, RunE: func(cmd *cobra.Command, args []string) error { + // If the help flag is set, show the help message if cmd.Flag("help").Changed { cmd.Help() return nil } + + // Load the config debug, _ := cmd.Flags().GetBool("debug") - err := config.Load(debug) + cwd, _ := cmd.Flags().GetString("cwd") + if cwd != "" { + err := os.Chdir(cwd) + if err != nil { + return fmt.Errorf("failed to change directory: %v", err) + } + } + if cwd == "" { + c, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %v", err) + } + cwd = c + } + _, err := config.Load(cwd, debug) if err != nil { return err } - cfg := config.Get() - defaultLevel := slog.LevelInfo - if cfg.Debug { - defaultLevel = slog.LevelDebug - } - logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{ - Level: defaultLevel, - })) - slog.SetDefault(logger) err = assets.WriteAssets() if err != nil { - return err + logging.Error("Error writing assets: %v", err) } + // Connect DB, this will also run migrations conn, err := db.Connect() if err != nil { return err } - ctx := context.Background() + + // Create main context for the application + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() app := app.New(ctx, conn) - logging.Info("Starting termai...") + + // Set up the TUI zone.NewGlobal() - tui := tea.NewProgram( + program := tea.NewProgram( tui.New(app), tea.WithAltScreen(), tea.WithMouseCellMotion(), ) - logging.Info("Setting up subscriptions...") - ch, unsub := setupSubscriptions(app) - defer unsub() + // Initialize MCP tools in the background + initMCPTools(ctx, app) + + // Setup the subscriptions, this will send services events to the TUI + ch, cancelSubs := setupSubscriptions(app) + + // Create a context for the TUI message handler + tuiCtx, tuiCancel := context.WithCancel(ctx) + var tuiWg sync.WaitGroup + tuiWg.Add(1) + + // Set up message handling for the TUI go func() { - // Set this up once - agent.GetMcpTools(ctx, app.Permissions) - for msg := range ch { - tui.Send(msg) + defer tuiWg.Done() + defer func() { + if r := recover(); r != nil { + logging.Error("Panic in TUI message handling: %v", r) + attemptTUIRecovery(program) + } + }() + + for { + select { + case <-tuiCtx.Done(): + logging.Info("TUI message handler shutting down") + return + case msg, ok := <-ch: + if !ok { + logging.Info("TUI message channel closed") + return + } + program.Send(msg) + } } }() - if _, err := tui.Run(); err != nil { - return err + + // Cleanup function for when the program exits + cleanup := func() { + // Shutdown the app + app.Shutdown() + + // Cancel subscriptions first + cancelSubs() + + // Then cancel TUI message handler + tuiCancel() + + // Wait for TUI message handler to finish + tuiWg.Wait() + + logging.Info("All goroutines cleaned up") + } + + // Run the TUI + result, err := program.Run() + cleanup() + + if err != nil { + logging.Error("TUI error: %v", err) + return fmt.Errorf("TUI error: %v", err) } + + logging.Info("TUI exited with result: %v", result) return nil }, } -func setupSubscriptions(app *app.App) (chan tea.Msg, func()) { - ch := make(chan tea.Msg) - wg := sync.WaitGroup{} - ctx, cancel := context.WithCancel(app.Context) - { - sub := logging.Subscribe(ctx) - wg.Add(1) - go func() { - for ev := range sub { - ch <- ev +// attemptTUIRecovery tries to recover the TUI after a panic +func attemptTUIRecovery(program *tea.Program) { + logging.Info("Attempting to recover TUI after panic") + + // We could try to restart the TUI or gracefully exit + // For now, we'll just quit the program to avoid further issues + program.Quit() +} + +func initMCPTools(ctx context.Context, app *app.App) { + go func() { + defer func() { + if r := recover(); r != nil { + logging.Error("Panic in MCP goroutine: %v", r) } - wg.Done() }() - } - { - sub := app.Sessions.Subscribe(ctx) - wg.Add(1) - go func() { - for ev := range sub { - ch <- ev + + // Create a context with timeout for the initial MCP tools fetch + ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + // Set this up once with proper error handling + agent.GetMcpTools(ctxWithTimeout, app.Permissions) + logging.Info("MCP message handling goroutine exiting") + }() +} + +func setupSubscriber[T any]( + ctx context.Context, + wg *sync.WaitGroup, + name string, + subscriber func(context.Context) <-chan pubsub.Event[T], + outputCh chan<- tea.Msg, +) { + wg.Add(1) + go func() { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + logging.Error("Panic in %s subscription goroutine: %v", name, r) } - wg.Done() }() - } - { - sub := app.Messages.Subscribe(ctx) - wg.Add(1) - go func() { - for ev := range sub { - ch <- ev + + for { + select { + case event, ok := <-subscriber(ctx): + if !ok { + logging.Info("%s subscription channel closed", name) + return + } + + // Convert generic event to tea.Msg if needed + var msg tea.Msg = event + + // Non-blocking send with timeout to prevent deadlocks + select { + case outputCh <- msg: + case <-time.After(500 * time.Millisecond): + logging.Warn("%s message dropped due to slow consumer", name) + case <-ctx.Done(): + logging.Info("%s subscription cancelled", name) + return + } + case <-ctx.Done(): + logging.Info("%s subscription cancelled", name) + return } - wg.Done() - }() - } - { - sub := app.Permissions.Subscribe(ctx) - wg.Add(1) + } + }() +} + +func setupSubscriptions(app *app.App) (chan tea.Msg, func()) { + ch := make(chan tea.Msg, 100) + // Add a buffer to prevent blocking + wg := sync.WaitGroup{} + ctx, cancel := context.WithCancel(context.Background()) + // Setup each subscription using the helper + setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch) + setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch) + setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch) + setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch) + + // Return channel and a cleanup function + cleanupFunc := func() { + logging.Info("Cancelling all subscriptions") + cancel() // Signal all goroutines to stop + + // Wait with a timeout for all goroutines to complete + waitCh := make(chan struct{}) go func() { - for ev := range sub { - ch <- ev - } - wg.Done() + wg.Wait() + close(waitCh) }() + + select { + case <-waitCh: + logging.Info("All subscription goroutines completed successfully") + case <-time.After(5 * time.Second): + logging.Warn("Timed out waiting for some subscription goroutines to complete") + } + + close(ch) // Safe to close after all writers are done or timed out } - return ch, func() { - cancel() - wg.Wait() - close(ch) - } + return ch, cleanupFunc } func Execute() { @@ -139,5 +259,6 @@ func Execute() { func init() { rootCmd.Flags().BoolP("help", "h", false, "Help") - rootCmd.Flags().BoolP("debug", "d", false, "Help") + rootCmd.Flags().BoolP("debug", "d", false, "Debug") + rootCmd.Flags().StringP("cwd", "c", "", "Current working directory") } diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 000000000..fa4a6ee90 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,76 @@ +package app + +import ( + "context" + "database/sql" + "maps" + "sync" + "time" + + "github.com/kujtimiihoxha/termai/internal/db" + "github.com/kujtimiihoxha/termai/internal/history" + "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/termai/internal/session" +) + +type App struct { + Sessions session.Service + Messages message.Service + Files history.Service + Permissions permission.Service + + LSPClients map[string]*lsp.Client + + clientsMutex sync.RWMutex + + watcherCancelFuncs []context.CancelFunc + cancelFuncsMutex sync.Mutex + watcherWG sync.WaitGroup +} + +func New(ctx context.Context, conn *sql.DB) *App { + q := db.New(conn) + sessions := session.NewService(q) + messages := message.NewService(q) + files := history.NewService(q) + + app := &App{ + Sessions: sessions, + Messages: messages, + Files: files, + Permissions: permission.NewPermissionService(), + LSPClients: make(map[string]*lsp.Client), + } + + app.initLSPClients(ctx) + + return app +} + +// Shutdown performs a clean shutdown of the application +func (app *App) Shutdown() { + // Cancel all watcher goroutines + app.cancelFuncsMutex.Lock() + for _, cancel := range app.watcherCancelFuncs { + cancel() + } + app.cancelFuncsMutex.Unlock() + app.watcherWG.Wait() + + // Perform additional cleanup for LSP clients + app.clientsMutex.RLock() + clients := make(map[string]*lsp.Client, len(app.LSPClients)) + maps.Copy(clients, app.LSPClients) + app.clientsMutex.RUnlock() + + for name, client := range clients { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + if err := client.Shutdown(shutdownCtx); err != nil { + logging.Error("Failed to shutdown LSP client", "name", name, "error", err) + } + cancel() + } +} diff --git a/internal/app/lsp.go b/internal/app/lsp.go new file mode 100644 index 000000000..4e0568f07 --- /dev/null +++ b/internal/app/lsp.go @@ -0,0 +1,108 @@ +package app + +import ( + "context" + "time" + + "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/termai/internal/lsp/watcher" +) + +func (app *App) initLSPClients(ctx context.Context) { + cfg := config.Get() + + // Initialize LSP clients + for name, clientConfig := range cfg.LSP { + app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...) + } +} + +// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher +func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) { + // Create a specific context for initialization with a timeout + initCtx, initCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer initCancel() + + // Create the LSP client + lspClient, err := lsp.NewClient(initCtx, command, args...) + if err != nil { + logging.Error("Failed to create LSP client for", name, err) + return + } + + // Initialize with the initialization context + _, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory()) + if err != nil { + logging.Error("Initialize failed", "name", name, "error", err) + // Clean up the client to prevent resource leaks + lspClient.Close() + return + } + + // Create a child context that can be canceled when the app is shutting down + watchCtx, cancelFunc := context.WithCancel(ctx) + workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient) + + // Store the cancel function to be called during cleanup + app.cancelFuncsMutex.Lock() + app.watcherCancelFuncs = append(app.watcherCancelFuncs, cancelFunc) + app.cancelFuncsMutex.Unlock() + + // Add the watcher to a WaitGroup to track active goroutines + app.watcherWG.Add(1) + + // Add to map with mutex protection before starting goroutine + app.clientsMutex.Lock() + app.LSPClients[name] = lspClient + app.clientsMutex.Unlock() + + go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher) +} + +// runWorkspaceWatcher executes the workspace watcher for an LSP client +func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) { + defer app.watcherWG.Done() + defer func() { + if r := recover(); r != nil { + logging.Error("LSP client crashed", "client", name, "panic", r) + + // Try to restart the client + app.restartLSPClient(ctx, name) + } + }() + + workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory()) + logging.Info("Workspace watcher stopped", "client", name) +} + +// restartLSPClient attempts to restart a crashed or failed LSP client +func (app *App) restartLSPClient(ctx context.Context, name string) { + // Get the original configuration + cfg := config.Get() + clientConfig, exists := cfg.LSP[name] + if !exists { + logging.Error("Cannot restart client, configuration not found", "client", name) + return + } + + // Clean up the old client if it exists + app.clientsMutex.Lock() + oldClient, exists := app.LSPClients[name] + if exists { + delete(app.LSPClients, name) // Remove from map before potentially slow shutdown + } + app.clientsMutex.Unlock() + + if exists && oldClient != nil { + // Try to shut it down gracefully, but don't block on errors + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _ = oldClient.Shutdown(shutdownCtx) + cancel() + } + + // Create a new client using the shared function + app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...) + logging.Info("Successfully restarted LSP client", "client", name) +} diff --git a/internal/app/services.go b/internal/app/services.go deleted file mode 100644 index 6ecdef03c..000000000 --- a/internal/app/services.go +++ /dev/null @@ -1,64 +0,0 @@ -package app - -import ( - "context" - "database/sql" - - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/watcher" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/session" -) - -type App struct { - Context context.Context - - Sessions session.Service - Messages message.Service - Files history.Service - Permissions permission.Service - - LSPClients map[string]*lsp.Client -} - -func New(ctx context.Context, conn *sql.DB) *App { - cfg := config.Get() - logging.Info("Debug mode enabled") - - q := db.New(conn) - sessions := session.NewService(ctx, q) - messages := message.NewService(ctx, q) - files := history.NewService(ctx, q) - - app := &App{ - Context: ctx, - Sessions: sessions, - Messages: messages, - Files: files, - Permissions: permission.NewPermissionService(), - LSPClients: make(map[string]*lsp.Client), - } - - for name, client := range cfg.LSP { - lspClient, err := lsp.NewClient(ctx, client.Command, client.Args...) - workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient) - if err != nil { - logging.Error("Failed to create LSP client for", name, err) - continue - } - - _, err = lspClient.InitializeLSPClient(ctx, config.WorkingDirectory()) - if err != nil { - logging.Error("Initialize failed", "error", err) - continue - } - go workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory()) - app.LSPClients[name] = lspClient - } - return app -} diff --git a/internal/config/config.go b/internal/config/config.go index 6f757b3f4..1f3091ff3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -83,9 +83,9 @@ var cfg *Config // Load initializes the configuration from environment variables and config files. // If debug is true, debug mode is enabled and log level is set to debug. // It returns an error if configuration loading fails. -func Load(workingDir string, debug bool) error { +func Load(workingDir string, debug bool) (*Config, error) { if cfg != nil { - return nil + return cfg, nil } cfg = &Config{ @@ -101,7 +101,7 @@ func Load(workingDir string, debug bool) error { // Read global config if err := readConfig(viper.ReadInConfig()); err != nil { - return err + return cfg, err } // Load and merge local config @@ -109,7 +109,7 @@ func Load(workingDir string, debug bool) error { // Apply configuration to the struct if err := viper.Unmarshal(cfg); err != nil { - return err + return cfg, fmt.Errorf("failed to unmarshal config: %w", err) } applyDefaultValues() @@ -123,7 +123,7 @@ func Load(workingDir string, debug bool) error { Level: defaultLevel, })) slog.SetDefault(logger) - return nil + return cfg, nil } // configureViper sets up viper's configuration paths and environment variables. @@ -237,7 +237,7 @@ func readConfig(err error) error { return nil } - return err + return fmt.Errorf("failed to read config: %w", err) } // mergeLocalConfig loads and merges configuration from the local directory. @@ -264,14 +264,6 @@ func applyDefaultValues() { } } -// setWorkingDirectory stores the current working directory in the configuration. -func setWorkingDirectory() { - workdir, err := os.Getwd() - if err == nil { - viper.Set("wd", workdir) - } -} - // Get returns the current configuration. // It's safe to call this function multiple times. func Get() *Config { diff --git a/internal/history/file.go b/internal/history/file.go index 25953b273..82017d4cf 100644 --- a/internal/history/file.go +++ b/internal/history/file.go @@ -27,45 +27,43 @@ type File struct { type Service interface { pubsub.Suscriber[File] - Create(sessionID, path, content string) (File, error) - CreateVersion(sessionID, path, content string) (File, error) - Get(id string) (File, error) - GetByPathAndSession(path, sessionID string) (File, error) - ListBySession(sessionID string) ([]File, error) - ListLatestSessionFiles(sessionID string) ([]File, error) - Update(file File) (File, error) - Delete(id string) error - DeleteSessionFiles(sessionID string) error + Create(ctx context.Context, sessionID, path, content string) (File, error) + CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) + Get(ctx context.Context, id string) (File, error) + GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error) + ListBySession(ctx context.Context, sessionID string) ([]File, error) + ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) + Update(ctx context.Context, file File) (File, error) + Delete(ctx context.Context, id string) error + DeleteSessionFiles(ctx context.Context, sessionID string) error } type service struct { *pubsub.Broker[File] - q db.Querier - ctx context.Context + q db.Querier } -func NewService(ctx context.Context, q db.Querier) Service { +func NewService(q db.Querier) Service { return &service{ Broker: pubsub.NewBroker[File](), q: q, - ctx: ctx, } } -func (s *service) Create(sessionID, path, content string) (File, error) { - return s.createWithVersion(sessionID, path, content, InitialVersion) +func (s *service) Create(ctx context.Context, sessionID, path, content string) (File, error) { + return s.createWithVersion(ctx, sessionID, path, content, InitialVersion) } -func (s *service) CreateVersion(sessionID, path, content string) (File, error) { +func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) { // Get the latest version for this path - files, err := s.q.ListFilesByPath(s.ctx, path) + files, err := s.q.ListFilesByPath(ctx, path) if err != nil { return File{}, err } if len(files) == 0 { // No previous versions, create initial - return s.Create(sessionID, path, content) + return s.Create(ctx, sessionID, path, content) } // Get the latest version @@ -89,11 +87,11 @@ func (s *service) CreateVersion(sessionID, path, content string) (File, error) { nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt) } - return s.createWithVersion(sessionID, path, content, nextVersion) + return s.createWithVersion(ctx, sessionID, path, content, nextVersion) } -func (s *service) createWithVersion(sessionID, path, content, version string) (File, error) { - dbFile, err := s.q.CreateFile(s.ctx, db.CreateFileParams{ +func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) { + dbFile, err := s.q.CreateFile(ctx, db.CreateFileParams{ ID: uuid.New().String(), SessionID: sessionID, Path: path, @@ -108,16 +106,16 @@ func (s *service) createWithVersion(sessionID, path, content, version string) (F return file, nil } -func (s *service) Get(id string) (File, error) { - dbFile, err := s.q.GetFile(s.ctx, id) +func (s *service) Get(ctx context.Context, id string) (File, error) { + dbFile, err := s.q.GetFile(ctx, id) if err != nil { return File{}, err } return s.fromDBItem(dbFile), nil } -func (s *service) GetByPathAndSession(path, sessionID string) (File, error) { - dbFile, err := s.q.GetFileByPathAndSession(s.ctx, db.GetFileByPathAndSessionParams{ +func (s *service) GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error) { + dbFile, err := s.q.GetFileByPathAndSession(ctx, db.GetFileByPathAndSessionParams{ Path: path, SessionID: sessionID, }) @@ -127,8 +125,8 @@ func (s *service) GetByPathAndSession(path, sessionID string) (File, error) { return s.fromDBItem(dbFile), nil } -func (s *service) ListBySession(sessionID string) ([]File, error) { - dbFiles, err := s.q.ListFilesBySession(s.ctx, sessionID) +func (s *service) ListBySession(ctx context.Context, sessionID string) ([]File, error) { + dbFiles, err := s.q.ListFilesBySession(ctx, sessionID) if err != nil { return nil, err } @@ -139,8 +137,8 @@ func (s *service) ListBySession(sessionID string) ([]File, error) { return files, nil } -func (s *service) ListLatestSessionFiles(sessionID string) ([]File, error) { - dbFiles, err := s.q.ListLatestSessionFiles(s.ctx, sessionID) +func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) { + dbFiles, err := s.q.ListLatestSessionFiles(ctx, sessionID) if err != nil { return nil, err } @@ -151,8 +149,8 @@ func (s *service) ListLatestSessionFiles(sessionID string) ([]File, error) { return files, nil } -func (s *service) Update(file File) (File, error) { - dbFile, err := s.q.UpdateFile(s.ctx, db.UpdateFileParams{ +func (s *service) Update(ctx context.Context, file File) (File, error) { + dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{ ID: file.ID, Content: file.Content, Version: file.Version, @@ -165,12 +163,12 @@ func (s *service) Update(file File) (File, error) { return updatedFile, nil } -func (s *service) Delete(id string) error { - file, err := s.Get(id) +func (s *service) Delete(ctx context.Context, id string) error { + file, err := s.Get(ctx, id) if err != nil { return err } - err = s.q.DeleteFile(s.ctx, id) + err = s.q.DeleteFile(ctx, id) if err != nil { return err } @@ -178,13 +176,13 @@ func (s *service) Delete(id string) error { return nil } -func (s *service) DeleteSessionFiles(sessionID string) error { - files, err := s.ListBySession(sessionID) +func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error { + files, err := s.ListBySession(ctx, sessionID) if err != nil { return err } for _, file := range files { - err = s.Delete(file.ID) + err = s.Delete(ctx, file.ID) if err != nil { return err } @@ -203,4 +201,3 @@ func (s *service) fromDBItem(item db.File) File { UpdatedAt: item.UpdatedAt, } } - diff --git a/internal/llm/agent/agent-tool.go b/internal/llm/agent/agent-tool.go index deb6aed60..91c46da8b 100644 --- a/internal/llm/agent/agent-tool.go +++ b/internal/llm/agent/agent-tool.go @@ -51,7 +51,7 @@ func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes return tools.NewTextErrorResponse(fmt.Sprintf("error creating agent: %s", err)), nil } - session, err := b.app.Sessions.CreateTaskSession(call.ID, b.parentSessionID, "New Agent Session") + session, err := b.app.Sessions.CreateTaskSession(ctx, call.ID, b.parentSessionID, "New Agent Session") if err != nil { return tools.NewTextErrorResponse(fmt.Sprintf("error creating session: %s", err)), nil } @@ -61,7 +61,7 @@ func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes return tools.NewTextErrorResponse(fmt.Sprintf("error generating agent: %s", err)), nil } - messages, err := b.app.Messages.List(session.ID) + messages, err := b.app.Messages.List(ctx, session.ID) if err != nil { return tools.NewTextErrorResponse(fmt.Sprintf("error listing messages: %s", err)), nil } @@ -74,11 +74,11 @@ func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes return tools.NewTextErrorResponse("no assistant message found"), nil } - updatedSession, err := b.app.Sessions.Get(session.ID) + updatedSession, err := b.app.Sessions.Get(ctx, session.ID) if err != nil { return tools.NewTextErrorResponse(fmt.Sprintf("error: %s", err)), nil } - parentSession, err := b.app.Sessions.Get(b.parentSessionID) + parentSession, err := b.app.Sessions.Get(ctx, b.parentSessionID) if err != nil { return tools.NewTextErrorResponse(fmt.Sprintf("error: %s", err)), nil } @@ -87,7 +87,7 @@ func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes parentSession.PromptTokens += updatedSession.PromptTokens parentSession.CompletionTokens += updatedSession.CompletionTokens - _, err = b.app.Sessions.Save(parentSession) + _, err = b.app.Sessions.Save(ctx, parentSession) if err != nil { return tools.NewTextErrorResponse(fmt.Sprintf("error: %s", err)), nil } diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 89de627f7..b7c736e6c 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -48,7 +48,7 @@ func (c *agent) handleTitleGeneration(ctx context.Context, sessionID, content st return } - session, err := c.Sessions.Get(sessionID) + session, err := c.Sessions.Get(ctx, sessionID) if err != nil { return } @@ -56,12 +56,12 @@ func (c *agent) handleTitleGeneration(ctx context.Context, sessionID, content st session.Title = response.Content session.Title = strings.TrimSpace(session.Title) session.Title = strings.ReplaceAll(session.Title, "\n", " ") - c.Sessions.Save(session) + c.Sessions.Save(ctx, session) } } -func (c *agent) TrackUsage(sessionID string, model models.Model, usage provider.TokenUsage) error { - session, err := c.Sessions.Get(sessionID) +func (c *agent) TrackUsage(ctx context.Context, sessionID string, model models.Model, usage provider.TokenUsage) error { + session, err := c.Sessions.Get(ctx, sessionID) if err != nil { return err } @@ -75,11 +75,12 @@ func (c *agent) TrackUsage(sessionID string, model models.Model, usage provider. session.CompletionTokens += usage.OutputTokens session.PromptTokens += usage.InputTokens - _, err = c.Sessions.Save(session) + _, err = c.Sessions.Save(ctx, session) return err } func (c *agent) processEvent( + ctx context.Context, sessionID string, assistantMsg *message.Message, event provider.ProviderEvent, @@ -87,10 +88,10 @@ func (c *agent) processEvent( switch event.Type { case provider.EventThinkingDelta: assistantMsg.AppendReasoningContent(event.Content) - return c.Messages.Update(*assistantMsg) + return c.Messages.Update(ctx, *assistantMsg) case provider.EventContentDelta: assistantMsg.AppendContent(event.Content) - return c.Messages.Update(*assistantMsg) + return c.Messages.Update(ctx, *assistantMsg) case provider.EventError: if errors.Is(event.Error, context.Canceled) { return nil @@ -105,11 +106,11 @@ func (c *agent) processEvent( case provider.EventComplete: assistantMsg.SetToolCalls(event.Response.ToolCalls) assistantMsg.AddFinish(event.Response.FinishReason) - err := c.Messages.Update(*assistantMsg) + err := c.Messages.Update(ctx, *assistantMsg) if err != nil { return err } - return c.TrackUsage(sessionID, c.model, event.Response.Usage) + return c.TrackUsage(ctx, sessionID, c.model, event.Response.Usage) } return nil @@ -237,7 +238,7 @@ func (c *agent) handleToolExecution( for _, toolResult := range toolResults { parts = append(parts, toolResult) } - msg, err := c.Messages.Create(assistantMsg.SessionID, message.CreateMessageParams{ + msg, err := c.Messages.Create(ctx, assistantMsg.SessionID, message.CreateMessageParams{ Role: message.Tool, Parts: parts, }) @@ -247,7 +248,7 @@ func (c *agent) handleToolExecution( func (c *agent) generate(ctx context.Context, sessionID string, content string) error { ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID) - messages, err := c.Messages.List(sessionID) + messages, err := c.Messages.List(ctx, sessionID) if err != nil { return err } @@ -256,7 +257,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) go c.handleTitleGeneration(ctx, sessionID, content) } - userMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{ + userMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{ Role: message.User, Parts: []message.ContentPart{ message.TextContent{ @@ -272,7 +273,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) for { select { case <-ctx.Done(): - assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{ + assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{ Role: message.Assistant, Parts: []message.ContentPart{}, }) @@ -280,7 +281,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) return err } assistantMsg.AddFinish("canceled") - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return context.Canceled default: // Continue processing @@ -289,7 +290,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) eventChan, err := c.agent.StreamResponse(ctx, messages, c.tools) if err != nil { if errors.Is(err, context.Canceled) { - assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{ + assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{ Role: message.Assistant, Parts: []message.ContentPart{}, }) @@ -297,13 +298,13 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) return err } assistantMsg.AddFinish("canceled") - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return context.Canceled } return err } - assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{ + assistantMsg, err := c.Messages.Create(ctx, sessionID, message.CreateMessageParams{ Role: message.Assistant, Parts: []message.ContentPart{}, Model: c.model.ID, @@ -314,22 +315,22 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID) for event := range eventChan { - err = c.processEvent(sessionID, &assistantMsg, event) + err = c.processEvent(ctx, sessionID, &assistantMsg, event) if err != nil { if errors.Is(err, context.Canceled) { assistantMsg.AddFinish("canceled") - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return context.Canceled } assistantMsg.AddFinish("error:" + err.Error()) - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return err } select { case <-ctx.Done(): assistantMsg.AddFinish("canceled") - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return context.Canceled default: } @@ -339,7 +340,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) select { case <-ctx.Done(): assistantMsg.AddFinish("canceled") - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return context.Canceled default: // Continue processing @@ -349,13 +350,13 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) if err != nil { if errors.Is(err, context.Canceled) { assistantMsg.AddFinish("canceled") - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return context.Canceled } return err } - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) if len(assistantMsg.ToolCalls()) == 0 { break @@ -370,7 +371,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string) select { case <-ctx.Done(): assistantMsg.AddFinish("canceled") - c.Messages.Update(assistantMsg) + c.Messages.Update(ctx, assistantMsg) return context.Canceled default: // Continue processing @@ -383,7 +384,7 @@ func getAgentProviders(ctx context.Context, model models.Model) (provider.Provid maxTokens := config.Get().Model.CoderMaxTokens providerConfig, ok := config.Get().Providers[model.Provider] - if !ok || !providerConfig.Enabled { + if !ok || providerConfig.Disabled { return nil, nil, errors.New("provider is not enabled") } var agentProvider provider.Provider diff --git a/internal/llm/agent/coder.go b/internal/llm/agent/coder.go index 5deff05a8..f8e1c40a0 100644 --- a/internal/llm/agent/coder.go +++ b/internal/llm/agent/coder.go @@ -40,12 +40,13 @@ func NewCoderAgent(app *app.App) (Agent, error) { return nil, errors.New("model not supported") } - agentProvider, titleGenerator, err := getAgentProviders(app.Context, model) + ctx := context.Background() + agentProvider, titleGenerator, err := getAgentProviders(ctx, model) if err != nil { return nil, err } - otherTools := GetMcpTools(app.Context, app.Permissions) + otherTools := GetMcpTools(ctx, app.Permissions) if len(app.LSPClients) > 0 { otherTools = append(otherTools, tools.NewDiagnosticsTool(app.LSPClients)) } diff --git a/internal/llm/agent/task.go b/internal/llm/agent/task.go index 034e93460..c196cb107 100644 --- a/internal/llm/agent/task.go +++ b/internal/llm/agent/task.go @@ -24,7 +24,8 @@ func NewTaskAgent(app *app.App) (Agent, error) { return nil, errors.New("model not supported") } - agentProvider, titleGenerator, err := getAgentProviders(app.Context, model) + ctx := context.Background() + agentProvider, titleGenerator, err := getAgentProviders(ctx, model) if err != nil { return nil, err } diff --git a/internal/message/message.go b/internal/message/message.go index 06dae13a5..2871780a7 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -20,34 +20,32 @@ type CreateMessageParams struct { type Service interface { pubsub.Suscriber[Message] - Create(sessionID string, params CreateMessageParams) (Message, error) - Update(message Message) error - Get(id string) (Message, error) - List(sessionID string) ([]Message, error) - Delete(id string) error - DeleteSessionMessages(sessionID string) error + Create(ctx context.Context, sessionID string, params CreateMessageParams) (Message, error) + Update(ctx context.Context, message Message) error + Get(ctx context.Context, id string) (Message, error) + List(ctx context.Context, sessionID string) ([]Message, error) + Delete(ctx context.Context, id string) error + DeleteSessionMessages(ctx context.Context, sessionID string) error } type service struct { *pubsub.Broker[Message] - q db.Querier - ctx context.Context + q db.Querier } -func NewService(ctx context.Context, q db.Querier) Service { +func NewService(q db.Querier) Service { return &service{ Broker: pubsub.NewBroker[Message](), q: q, - ctx: ctx, } } -func (s *service) Delete(id string) error { - message, err := s.Get(id) +func (s *service) Delete(ctx context.Context, id string) error { + message, err := s.Get(ctx, id) if err != nil { return err } - err = s.q.DeleteMessage(s.ctx, message.ID) + err = s.q.DeleteMessage(ctx, message.ID) if err != nil { return err } @@ -55,7 +53,7 @@ func (s *service) Delete(id string) error { return nil } -func (s *service) Create(sessionID string, params CreateMessageParams) (Message, error) { +func (s *service) Create(ctx context.Context, sessionID string, params CreateMessageParams) (Message, error) { if params.Role != Assistant { params.Parts = append(params.Parts, Finish{ Reason: "stop", @@ -66,7 +64,7 @@ func (s *service) Create(sessionID string, params CreateMessageParams) (Message, return Message{}, err } - dbMessage, err := s.q.CreateMessage(s.ctx, db.CreateMessageParams{ + dbMessage, err := s.q.CreateMessage(ctx, db.CreateMessageParams{ ID: uuid.New().String(), SessionID: sessionID, Role: string(params.Role), @@ -84,14 +82,14 @@ func (s *service) Create(sessionID string, params CreateMessageParams) (Message, return message, nil } -func (s *service) DeleteSessionMessages(sessionID string) error { - messages, err := s.List(sessionID) +func (s *service) DeleteSessionMessages(ctx context.Context, sessionID string) error { + messages, err := s.List(ctx, sessionID) if err != nil { return err } for _, message := range messages { if message.SessionID == sessionID { - err = s.Delete(message.ID) + err = s.Delete(ctx, message.ID) if err != nil { return err } @@ -100,7 +98,7 @@ func (s *service) DeleteSessionMessages(sessionID string) error { return nil } -func (s *service) Update(message Message) error { +func (s *service) Update(ctx context.Context, message Message) error { parts, err := marshallParts(message.Parts) if err != nil { return err @@ -110,7 +108,7 @@ func (s *service) Update(message Message) error { finishedAt.Int64 = f.Time finishedAt.Valid = true } - err = s.q.UpdateMessage(s.ctx, db.UpdateMessageParams{ + err = s.q.UpdateMessage(ctx, db.UpdateMessageParams{ ID: message.ID, Parts: string(parts), FinishedAt: finishedAt, @@ -122,16 +120,16 @@ func (s *service) Update(message Message) error { return nil } -func (s *service) Get(id string) (Message, error) { - dbMessage, err := s.q.GetMessage(s.ctx, id) +func (s *service) Get(ctx context.Context, id string) (Message, error) { + dbMessage, err := s.q.GetMessage(ctx, id) if err != nil { return Message{}, err } return s.fromDBItem(dbMessage) } -func (s *service) List(sessionID string) ([]Message, error) { - dbMessages, err := s.q.ListMessagesBySession(s.ctx, sessionID) +func (s *service) List(ctx context.Context, sessionID string) ([]Message, error) { + dbMessages, err := s.q.ListMessagesBySession(ctx, sessionID) if err != nil { return nil, err } diff --git a/internal/session/session.go b/internal/session/session.go index 13f420b7c..9a16224c3 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -23,22 +23,21 @@ type Session struct { type Service interface { pubsub.Suscriber[Session] - Create(title string) (Session, error) - CreateTaskSession(toolCallID, parentSessionID, title string) (Session, error) - Get(id string) (Session, error) - List() ([]Session, error) - Save(session Session) (Session, error) - Delete(id string) error + Create(ctx context.Context, title string) (Session, error) + CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error) + Get(ctx context.Context, id string) (Session, error) + List(ctx context.Context) ([]Session, error) + Save(ctx context.Context, session Session) (Session, error) + Delete(ctx context.Context, id string) error } type service struct { *pubsub.Broker[Session] - q db.Querier - ctx context.Context + q db.Querier } -func (s *service) Create(title string) (Session, error) { - dbSession, err := s.q.CreateSession(s.ctx, db.CreateSessionParams{ +func (s *service) Create(ctx context.Context, title string) (Session, error) { + dbSession, err := s.q.CreateSession(ctx, db.CreateSessionParams{ ID: uuid.New().String(), Title: title, }) @@ -50,8 +49,8 @@ func (s *service) Create(title string) (Session, error) { return session, nil } -func (s *service) CreateTaskSession(toolCallID, parentSessionID, title string) (Session, error) { - dbSession, err := s.q.CreateSession(s.ctx, db.CreateSessionParams{ +func (s *service) CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error) { + dbSession, err := s.q.CreateSession(ctx, db.CreateSessionParams{ ID: toolCallID, ParentSessionID: sql.NullString{String: parentSessionID, Valid: true}, Title: title, @@ -64,12 +63,12 @@ func (s *service) CreateTaskSession(toolCallID, parentSessionID, title string) ( return session, nil } -func (s *service) Delete(id string) error { - session, err := s.Get(id) +func (s *service) Delete(ctx context.Context, id string) error { + session, err := s.Get(ctx, id) if err != nil { return err } - err = s.q.DeleteSession(s.ctx, session.ID) + err = s.q.DeleteSession(ctx, session.ID) if err != nil { return err } @@ -77,16 +76,16 @@ func (s *service) Delete(id string) error { return nil } -func (s *service) Get(id string) (Session, error) { - dbSession, err := s.q.GetSessionByID(s.ctx, id) +func (s *service) Get(ctx context.Context, id string) (Session, error) { + dbSession, err := s.q.GetSessionByID(ctx, id) if err != nil { return Session{}, err } return s.fromDBItem(dbSession), nil } -func (s *service) Save(session Session) (Session, error) { - dbSession, err := s.q.UpdateSession(s.ctx, db.UpdateSessionParams{ +func (s *service) Save(ctx context.Context, session Session) (Session, error) { + dbSession, err := s.q.UpdateSession(ctx, db.UpdateSessionParams{ ID: session.ID, Title: session.Title, PromptTokens: session.PromptTokens, @@ -101,8 +100,8 @@ func (s *service) Save(session Session) (Session, error) { return session, nil } -func (s *service) List() ([]Session, error) { - dbSessions, err := s.q.ListSessions(s.ctx) +func (s *service) List(ctx context.Context) ([]Session, error) { + dbSessions, err := s.q.ListSessions(ctx) if err != nil { return nil, err } @@ -127,11 +126,10 @@ func (s service) fromDBItem(item db.Session) Session { } } -func NewService(ctx context.Context, q db.Querier) Service { +func NewService(q db.Querier) Service { broker := pubsub.NewBroker[Session]() return &service{ broker, q, - ctx, } } diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index b5a361392..dc21fca29 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -1,6 +1,7 @@ package chat import ( + "context" "encoding/json" "fmt" "math" @@ -324,7 +325,7 @@ func (m *messagesCmp) renderToolCall(toolCall message.ToolCall, isNested bool) s innerToolCalls := make([]string, 0) if toolCall.Name == agent.AgentToolName { - messages, _ := m.app.Messages.List(toolCall.ID) + messages, _ := m.app.Messages.List(context.Background(), toolCall.ID) toolCalls := make([]message.ToolCall, 0) for _, v := range messages { toolCalls = append(toolCalls, v.ToolCalls()...) @@ -554,7 +555,7 @@ func (m *messagesCmp) GetSize() (int, int) { func (m *messagesCmp) SetSession(session session.Session) tea.Cmd { m.session = session - messages, err := m.app.Messages.List(session.ID) + messages, err := m.app.Messages.List(context.Background(), session.ID) if err != nil { return util.ReportError(err) } diff --git a/internal/tui/components/repl/editor.go b/internal/tui/components/repl/editor.go index e9493129d..b1e39e655 100644 --- a/internal/tui/components/repl/editor.go +++ b/internal/tui/components/repl/editor.go @@ -160,7 +160,7 @@ func (m *editorCmp) Send() tea.Cmd { return util.ReportWarn("Assistant is still working on the previous message") } - messages, err := m.app.Messages.List(m.sessionID) + messages, err := m.app.Messages.List(context.Background(), m.sessionID) if err != nil { return util.ReportError(err) } @@ -177,7 +177,7 @@ func (m *editorCmp) Send() tea.Cmd { if len(content) == 0 { return util.ReportWarn("Message is empty") } - ctx, cancel := context.WithCancel(m.app.Context) + ctx, cancel := context.WithCancel(context.Background()) m.cancelMessage = cancel go func() { defer cancel() diff --git a/internal/tui/components/repl/messages.go b/internal/tui/components/repl/messages.go index 57a55c579..260be220e 100644 --- a/internal/tui/components/repl/messages.go +++ b/internal/tui/components/repl/messages.go @@ -1,6 +1,7 @@ package repl import ( + "context" "encoding/json" "fmt" "sort" @@ -77,8 +78,8 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.session = msg.Payload } case SelectedSessionMsg: - m.session, _ = m.app.Sessions.Get(msg.SessionID) - m.messages, _ = m.app.Messages.List(m.session.ID) + m.session, _ = m.app.Sessions.Get(context.Background(), msg.SessionID) + m.messages, _ = m.app.Messages.List(context.Background(), m.session.ID) m.renderView() m.viewport.GotoBottom() } @@ -259,7 +260,7 @@ func (m *messagesCmp) renderMessageWithToolCall(content string, tools []message. runningIndicator := runningStyle.Render(fmt.Sprintf("%s Running...", styles.SpinnerIcon)) allParts = append(allParts, leftPadding.Render(runningIndicator)) - taskSessionMessages, _ := m.app.Messages.List(toolCall.ID) + taskSessionMessages, _ := m.app.Messages.List(context.Background(), toolCall.ID) for _, msg := range taskSessionMessages { if msg.Role == message.Assistant { for _, toolCall := range msg.ToolCalls() { diff --git a/internal/tui/components/repl/sessions.go b/internal/tui/components/repl/sessions.go index 093337b18..c83c40367 100644 --- a/internal/tui/components/repl/sessions.go +++ b/internal/tui/components/repl/sessions.go @@ -1,6 +1,7 @@ package repl import ( + "context" "fmt" "strings" @@ -57,12 +58,13 @@ var sessionKeyMapValue = sessionsKeyMap{ } func (i *sessionsCmp) Init() tea.Cmd { - existing, err := i.app.Sessions.List() + existing, err := i.app.Sessions.List(context.Background()) if err != nil { return util.ReportError(err) } if len(existing) == 0 || existing[0].MessageCount > 0 { newSession, err := i.app.Sessions.Create( + context.Background(), "New Session", ) if err != nil { diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index a7a51bb84..9b9924909 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -1,6 +1,8 @@ package page import ( + "context" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/kujtimiihoxha/termai/internal/app" @@ -36,7 +38,7 @@ func (p *chatPage) Init() tea.Cmd { p.layout.Init(), } - sessions, _ := p.app.Sessions.List() + sessions, _ := p.app.Sessions.List(context.Background()) if len(sessions) > 0 { p.session = sessions[0] cmd := p.setSidebar() @@ -92,7 +94,7 @@ func (p *chatPage) clearSidebar() { func (p *chatPage) sendMessage(text string) tea.Cmd { var cmds []tea.Cmd if p.session.ID == "" { - session, err := p.app.Sessions.Create("New Session") + session, err := p.app.Sessions.Create(context.Background(), "New Session") if err != nil { return util.ReportError(err) } @@ -110,7 +112,7 @@ func (p *chatPage) sendMessage(text string) tea.Cmd { return util.ReportError(err) } go func() { - a.Generate(p.app.Context, p.session.ID, text) + a.Generate(context.Background(), p.session.ID, text) }() return tea.Batch(cmds...) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index db9ac9ff6..1b1a1ed50 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -1,6 +1,8 @@ package tui import ( + "context" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -184,7 +186,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case key.Matches(msg, replKeyMap): if a.currentPage == page.ReplPage { - sessions, err := a.app.Sessions.List() + sessions, err := a.app.Sessions.List(context.Background()) if err != nil { return a, util.CmdHandler(util.ReportError(err)) } @@ -192,7 +194,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if lastSession.MessageCount == 0 { return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: lastSession.ID}) } - s, err := a.app.Sessions.Create("New Session") + s, err := a.app.Sessions.Create(context.Background(), "New Session") if err != nil { return a, util.CmdHandler(util.ReportError(err)) } -- cgit v1.2.3 From cc07f7a186995f428436bc1adc66a264a95171a4 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 16 Apr 2025 21:48:29 +0200 Subject: rename to opencode --- .opencode.json | 11 ++++ cmd/root.go | 14 ++-- go.mod | 2 +- internal/app/app.go | 18 +++--- internal/app/lsp.go | 8 +-- internal/config/config.go | 4 +- internal/db/connect.go | 6 +- internal/diff/diff.go | 4 +- internal/history/file.go | 4 +- internal/llm/agent/agent-tool.go | 10 +-- internal/llm/agent/agent.go | 18 +++--- internal/llm/agent/mcp-tools.go | 10 +-- internal/llm/agent/tools.go | 12 ++-- internal/llm/prompt/coder.go | 97 ++++++++++++---------------- internal/llm/prompt/prompt.go | 4 +- internal/llm/prompt/task.go | 4 +- internal/llm/prompt/title.go | 2 +- internal/llm/provider/anthropic.go | 8 +-- internal/llm/provider/bedrock.go | 4 +- internal/llm/provider/gemini.go | 8 +-- internal/llm/provider/openai.go | 8 +-- internal/llm/provider/provider.go | 6 +- internal/llm/tools/bash.go | 16 ++--- internal/llm/tools/diagnostics.go | 4 +- internal/llm/tools/edit.go | 10 +-- internal/llm/tools/edit_test.go | 2 +- internal/llm/tools/fetch.go | 6 +- internal/llm/tools/glob.go | 2 +- internal/llm/tools/grep.go | 2 +- internal/llm/tools/ls.go | 2 +- internal/llm/tools/mocks_test.go | 6 +- internal/llm/tools/shell/shell.go | 8 +-- internal/llm/tools/sourcegraph.go | 2 +- internal/llm/tools/view.go | 4 +- internal/llm/tools/write.go | 10 +-- internal/llm/tools/write_test.go | 2 +- internal/logging/writer.go | 2 +- internal/lsp/client.go | 6 +- internal/lsp/handlers.go | 8 +-- internal/lsp/language.go | 2 +- internal/lsp/methods.go | 2 +- internal/lsp/transport.go | 4 +- internal/lsp/util/edit.go | 2 +- internal/lsp/watcher/watcher.go | 8 +-- internal/message/content.go | 2 +- internal/message/message.go | 6 +- internal/permission/permission.go | 2 +- internal/session/session.go | 4 +- internal/tui/components/chat/chat.go | 8 +-- internal/tui/components/chat/editor.go | 10 +-- internal/tui/components/chat/messages.go | 22 +++---- internal/tui/components/chat/sidebar.go | 12 ++-- internal/tui/components/core/status.go | 12 ++-- internal/tui/components/dialog/help.go | 2 +- internal/tui/components/dialog/permission.go | 12 ++-- internal/tui/components/dialog/quit.go | 6 +- internal/tui/components/logs/details.go | 6 +- internal/tui/components/logs/table.go | 10 +-- internal/tui/layout/border.go | 2 +- internal/tui/layout/container.go | 2 +- internal/tui/layout/overlay.go | 4 +- internal/tui/layout/split.go | 2 +- internal/tui/page/chat.go | 10 +-- internal/tui/page/logs.go | 4 +- internal/tui/tui.go | 18 +++--- main.go | 4 +- 66 files changed, 266 insertions(+), 266 deletions(-) (limited to 'internal/message/message.go') diff --git a/.opencode.json b/.opencode.json index b7fc19b52..4b2944f86 100644 --- a/.opencode.json +++ b/.opencode.json @@ -3,5 +3,16 @@ "gopls": { "command": "gopls" } + }, + "agents": { + "coder": { + "model": "gpt-4.1" + }, + "task": { + "model": "gpt-4.1" + }, + "title": { + "model": "gpt-4.1" + } } } diff --git a/cmd/root.go b/cmd/root.go index ff71747d5..f506e9940 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,13 +8,13 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/llm/agent" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/tui" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/llm/agent" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/tui" zone "github.com/lrstanley/bubblezone" "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index 16c88d3a6..822e70dbd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/kujtimiihoxha/termai +module github.com/kujtimiihoxha/opencode go 1.24.0 diff --git a/internal/app/app.go b/internal/app/app.go index 1c16ccc11..748fdaa7f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,15 +7,15 @@ import ( "sync" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/llm/agent" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/llm/agent" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/session" ) type App struct { diff --git a/internal/app/lsp.go b/internal/app/lsp.go index 4a762f1a1..d8a35c8b3 100644 --- a/internal/app/lsp.go +++ b/internal/app/lsp.go @@ -4,10 +4,10 @@ import ( "context" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/watcher" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/watcher" ) func (app *App) initLSPClients(ctx context.Context) { diff --git a/internal/config/config.go b/internal/config/config.go index 147d6c83a..20a8bac97 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,8 +7,8 @@ import ( "os" "strings" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/logging" "github.com/spf13/viper" ) diff --git a/internal/db/connect.go b/internal/db/connect.go index 8bba9cad8..e850bc8d0 100644 --- a/internal/db/connect.go +++ b/internal/db/connect.go @@ -12,8 +12,8 @@ import ( "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/mattn/go-sqlite3" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" ) func Connect() (*sql.DB, error) { @@ -24,7 +24,7 @@ func Connect() (*sql.DB, error) { if err := os.MkdirAll(dataDir, 0o700); err != nil { return nil, fmt.Errorf("failed to create data directory: %w", err) } - dbPath := filepath.Join(dataDir, "termai.db") + dbPath := filepath.Join(dataDir, "opencode.db") // Open the SQLite database db, err := sql.Open("sqlite3", dbPath) if err != nil { diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 829554c7e..f48079c9c 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -19,8 +19,8 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" "github.com/sergi/go-diff/diffmatchpatch" ) diff --git a/internal/history/file.go b/internal/history/file.go index 82017d4cf..1e8bc50bb 100644 --- a/internal/history/file.go +++ b/internal/history/file.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) const ( diff --git a/internal/llm/agent/agent-tool.go b/internal/llm/agent/agent-tool.go index 308412bde..be6e09a9b 100644 --- a/internal/llm/agent/agent-tool.go +++ b/internal/llm/agent/agent-tool.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/session" ) type agentTool struct { diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index ab2742ec1..a5dadb89d 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -7,15 +7,15 @@ import ( "strings" "sync" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/prompt" - "github.com/kujtimiihoxha/termai/internal/llm/provider" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/prompt" + "github.com/kujtimiihoxha/opencode/internal/llm/provider" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/session" ) // Common errors diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index c7ea4916c..16dddc1ba 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/version" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/version" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/mcp" diff --git a/internal/llm/agent/tools.go b/internal/llm/agent/tools.go index a37f1d65d..409d14273 100644 --- a/internal/llm/agent/tools.go +++ b/internal/llm/agent/tools.go @@ -3,12 +3,12 @@ package agent import ( "context" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/session" ) func CoderAgentTools( diff --git a/internal/llm/prompt/coder.go b/internal/llm/prompt/coder.go index 7439fd570..3a06911da 100644 --- a/internal/llm/prompt/coder.go +++ b/internal/llm/prompt/coder.go @@ -8,9 +8,9 @@ import ( "runtime" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" ) func CoderPrompt(provider models.ModelProvider) string { @@ -24,69 +24,58 @@ func CoderPrompt(provider models.ModelProvider) string { return fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation()) } -const baseOpenAICoderPrompt = `You are termAI, an autonomous CLI-based software engineer. Your job is to reduce user effort by proactively reasoning, inferring context, and solving software engineering tasks end-to-end with minimal prompting. - -# Your mindset -Act like a competent, efficient software engineer who is familiar with large codebases. You should: -- Think critically about user requests. -- Proactively search the codebase for related information. -- Infer likely commands, tools, or conventions. -- Write and edit code with minimal user input. -- Anticipate next steps (tests, lints, etc.), but never commit unless explicitly told. - -# Context awareness -- Before acting, infer the purpose of a file from its name, directory, and neighboring files. -- If a file or function appears malicious, refuse to interact with it or discuss it. -- If a termai.md file exists, auto-load it as memory. Offer to update it only if new useful info appears (commands, preferences, structure). - -# CLI communication -- Use GitHub-flavored markdown in monospace font. -- Be concise. Never add preambles or postambles unless asked. Max 4 lines per response. -- Never explain your code unless asked. Do not narrate actions. -- Avoid unnecessary questions. Infer, search, act. - -# Behavior guidelines -- Follow project conventions: naming, formatting, libraries, frameworks. -- Before using any library or framework, confirm it’s already used. -- Always look at the surrounding code to match existing style. -- Do not add comments unless the code is complex or the user asks. - -# Autonomy rules -You are allowed and expected to: -- Search for commands, tools, or config files before asking the user. -- Run multiple search tool calls concurrently to gather relevant context. -- Choose test, lint, and typecheck commands based on package files or scripts. -- Offer to store these commands in termai.md if not already present. - -# Example behavior -user: write tests for new feature -assistant: [searches for existing test patterns, finds appropriate location, generates test code using existing style, optionally asks to add test command to termai.md] +const baseOpenAICoderPrompt = ` +You are **OpenCode**, an autonomous CLI assistant for software‑engineering tasks. + +### ── INTERNAL REFLECTION ── +• Silently think step‑by‑step about the user request, directory layout, and tool calls (never reveal this). +• Formulate a plan, then execute without further approval unless a blocker triggers the Ask‑Only‑If rules. + +### ── PUBLIC RESPONSE RULES ── +• Visible reply ≤ 4 lines; no fluff, preamble, or postamble. +• Use GitHub‑flavored Markdown. +• When running a non‑trivial shell command, add ≤ 1 brief purpose sentence. + +### ── CONTEXT & MEMORY ── +• Infer file intent from directory structure before editing. +• Auto‑load 'OpenCode.md'; ask once before writing new reusable commands or style notes. -user: how do I typecheck this codebase? -assistant: [searches for known commands, infers package manager, checks for scripts or config files] -tsc --noEmit +### ── AUTONOMY PRIORITY ── +**Ask‑Only‑If Decision Tree:** +1. **Safety risk?** (e.g., destructive command, secret exposure) → ask. +2. **Critical unknown?** (no docs/tests; cannot infer) → ask. +3. **Tool failure after two self‑attempts?** → ask. +Otherwise, proceed autonomously. -user: is X function used anywhere else? -assistant: [searches repo for references, returns file paths and lines] +### ── SAFETY & STYLE ── +• Mimic existing code style; verify libraries exist before import. +• Never commit unless explicitly told. +• After edits, run lint & type‑check (ask for commands once, then offer to store in 'OpenCode.md'). +• Protect secrets; follow standard security practices :contentReference[oaicite:2]{index=2}. -# Tool usage -- Use parallel calls when possible. -- Use file search and content tools before asking the user. -- Do not ask the user for information unless it cannot be determined via tools. +### ── TOOL USAGE ── +• Batch independent Agent search/file calls in one block for efficiency :contentReference[oaicite:3]{index=3}. +• Communicate with the user only via visible text; do not expose tool output or internal reasoning. -Never commit changes unless the user explicitly asks you to.` +### ── EXAMPLES ── +user: list files +assistant: ls + +user: write tests for new feature +assistant: [searches & edits autonomously, no extra chit‑chat] +` -const baseAnthropicCoderPrompt = `You are termAI, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. +const baseAnthropicCoderPrompt = `You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. # Memory -If the current working directory contains a file called termai.md, it will be automatically added to your context. This file serves multiple purposes: +If the current working directory contains a file called OpenCode.md, it will be automatically added to your context. This file serves multiple purposes: 1. Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time 2. Recording the user's code style preferences (naming conventions, preferred libraries, etc.) 3. Maintaining useful information about the codebase structure and organization -When you spend time searching for commands to typecheck, lint, build, or test, you should ask the user if it's okay to add those commands to termai.md. Similarly, when learning about code style preferences or important codebase information, ask if it's okay to add that to termai.md so you can remember it for next time. +When you spend time searching for commands to typecheck, lint, build, or test, you should ask the user if it's okay to add those commands to OpenCode.md. Similarly, when learning about code style preferences or important codebase information, ask if it's okay to add that to OpenCode.md so you can remember it for next time. # Tone and style You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system). @@ -161,7 +150,7 @@ The user will primarily request you perform software engineering tasks. This inc 1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially. 2. Implement the solution using all tools available to you 3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. -4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to termai.md so that you will know to run it next time. +4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time. NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. diff --git a/internal/llm/prompt/prompt.go b/internal/llm/prompt/prompt.go index 63fc2df7b..cdc3560ce 100644 --- a/internal/llm/prompt/prompt.go +++ b/internal/llm/prompt/prompt.go @@ -1,8 +1,8 @@ package prompt import ( - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" ) func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string { diff --git a/internal/llm/prompt/task.go b/internal/llm/prompt/task.go index 8bf604ad9..88cd1a0f4 100644 --- a/internal/llm/prompt/task.go +++ b/internal/llm/prompt/task.go @@ -3,11 +3,11 @@ package prompt import ( "fmt" - "github.com/kujtimiihoxha/termai/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/models" ) func TaskPrompt(_ models.ModelProvider) string { - agentPrompt := `You are an agent for termAI. Given the user's prompt, you should use the tools available to you to answer the user's question. + agentPrompt := `You are an agent for OpenCode. Given the user's prompt, you should use the tools available to you to answer the user's question. Notes: 1. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is .", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". 2. When relevant, share file names and code snippets relevant to the query diff --git a/internal/llm/prompt/title.go b/internal/llm/prompt/title.go index 3023a8550..6e5289b24 100644 --- a/internal/llm/prompt/title.go +++ b/internal/llm/prompt/title.go @@ -1,6 +1,6 @@ package prompt -import "github.com/kujtimiihoxha/termai/internal/llm/models" +import "github.com/kujtimiihoxha/opencode/internal/llm/models" func TitlePrompt(_ models.ModelProvider) string { return `you will generate a short title based on the first message a user begins a conversation with diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index c3a4efc49..7bbc02103 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -12,10 +12,10 @@ import ( "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/bedrock" "github.com/anthropics/anthropic-sdk-go/option" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" ) type anthropicOptions struct { diff --git a/internal/llm/provider/bedrock.go b/internal/llm/provider/bedrock.go index d76925ad1..9415b30fe 100644 --- a/internal/llm/provider/bedrock.go +++ b/internal/llm/provider/bedrock.go @@ -7,8 +7,8 @@ import ( "os" "strings" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/message" ) type bedrockOptions struct { diff --git a/internal/llm/provider/gemini.go b/internal/llm/provider/gemini.go index 804baea28..384bff900 100644 --- a/internal/llm/provider/gemini.go +++ b/internal/llm/provider/gemini.go @@ -11,10 +11,10 @@ import ( "github.com/google/generative-ai-go/genai" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" "google.golang.org/api/iterator" "google.golang.org/api/option" ) diff --git a/internal/llm/provider/openai.go b/internal/llm/provider/openai.go index 9c2ad2012..13ce934f2 100644 --- a/internal/llm/provider/openai.go +++ b/internal/llm/provider/openai.go @@ -8,10 +8,10 @@ import ( "io" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" "github.com/openai/openai-go" "github.com/openai/openai-go/option" ) diff --git a/internal/llm/provider/provider.go b/internal/llm/provider/provider.go index 1a5b3dc8a..e04bee71b 100644 --- a/internal/llm/provider/provider.go +++ b/internal/llm/provider/provider.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/message" ) type EventType string diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index c7c970e5a..18533b761 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/tools/shell" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/tools/shell" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type BashParams struct { @@ -122,16 +122,16 @@ When the user asks you to create a new git commit, follow these steps carefully: 4. Create the commit with a message ending with: -🤖 Generated with termai -Co-Authored-By: termai +🤖 Generated with opencode +Co-Authored-By: opencode - In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example: git commit -m "$(cat <<'EOF' Commit message here. - 🤖 Generated with termai - Co-Authored-By: termai + 🤖 Generated with opencode + Co-Authored-By: opencode EOF )" @@ -193,7 +193,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF' ## Test plan [Checklist of TODOs for testing the pull request...] -🤖 Generated with termai +🤖 Generated with opencode EOF )" diff --git a/internal/llm/tools/diagnostics.go b/internal/llm/tools/diagnostics.go index b7b2bb8ba..82989c774 100644 --- a/internal/llm/tools/diagnostics.go +++ b/internal/llm/tools/diagnostics.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) type DiagnosticsParams struct { diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go index 148e7aba7..6a1616010 100644 --- a/internal/llm/tools/edit.go +++ b/internal/llm/tools/edit.go @@ -9,11 +9,11 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type EditParams struct { diff --git a/internal/llm/tools/edit_test.go b/internal/llm/tools/edit_test.go index 0971775dd..1b58a0d7d 100644 --- a/internal/llm/tools/edit_test.go +++ b/internal/llm/tools/edit_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/internal/llm/tools/fetch.go b/internal/llm/tools/fetch.go index 91bcb36a0..827755863 100644 --- a/internal/llm/tools/fetch.go +++ b/internal/llm/tools/fetch.go @@ -11,8 +11,8 @@ import ( md "github.com/JohannesKaufmann/html-to-markdown" "github.com/PuerkitoBio/goquery" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type FetchParams struct { @@ -146,7 +146,7 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error return ToolResponse{}, fmt.Errorf("failed to create request: %w", err) } - req.Header.Set("User-Agent", "termai/1.0") + req.Header.Set("User-Agent", "opencode/1.0") resp, err := client.Do(req) if err != nil { diff --git a/internal/llm/tools/glob.go b/internal/llm/tools/glob.go index 7b4fb1187..40262ce2b 100644 --- a/internal/llm/tools/glob.go +++ b/internal/llm/tools/glob.go @@ -12,7 +12,7 @@ import ( "time" "github.com/bmatcuk/doublestar/v4" - "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/opencode/internal/config" ) const ( diff --git a/internal/llm/tools/grep.go b/internal/llm/tools/grep.go index 19333f50b..3436dd7eb 100644 --- a/internal/llm/tools/grep.go +++ b/internal/llm/tools/grep.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/opencode/internal/config" ) type GrepParams struct { diff --git a/internal/llm/tools/ls.go b/internal/llm/tools/ls.go index a63bf0eeb..05f300c0e 100644 --- a/internal/llm/tools/ls.go +++ b/internal/llm/tools/ls.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/opencode/internal/config" ) type LSParams struct { diff --git a/internal/llm/tools/mocks_test.go b/internal/llm/tools/mocks_test.go index 321f09ac1..81993160c 100644 --- a/internal/llm/tools/mocks_test.go +++ b/internal/llm/tools/mocks_test.go @@ -9,9 +9,9 @@ import ( "time" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) // Mock permission service for testing diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index 4a776478a..e25bdf3ea 100644 --- a/internal/llm/tools/shell/shell.go +++ b/internal/llm/tools/shell/shell.go @@ -126,10 +126,10 @@ func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx } tempDir := os.TempDir() - stdoutFile := filepath.Join(tempDir, fmt.Sprintf("termai-stdout-%d", time.Now().UnixNano())) - stderrFile := filepath.Join(tempDir, fmt.Sprintf("termai-stderr-%d", time.Now().UnixNano())) - statusFile := filepath.Join(tempDir, fmt.Sprintf("termai-status-%d", time.Now().UnixNano())) - cwdFile := filepath.Join(tempDir, fmt.Sprintf("termai-cwd-%d", time.Now().UnixNano())) + stdoutFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stdout-%d", time.Now().UnixNano())) + stderrFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stderr-%d", time.Now().UnixNano())) + statusFile := filepath.Join(tempDir, fmt.Sprintf("opencode-status-%d", time.Now().UnixNano())) + cwdFile := filepath.Join(tempDir, fmt.Sprintf("opencode-cwd-%d", time.Now().UnixNano())) defer func() { os.Remove(stdoutFile) diff --git a/internal/llm/tools/sourcegraph.go b/internal/llm/tools/sourcegraph.go index a6f2c8afb..0d38c975f 100644 --- a/internal/llm/tools/sourcegraph.go +++ b/internal/llm/tools/sourcegraph.go @@ -218,7 +218,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse, } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "termai/1.0") + req.Header.Set("User-Agent", "opencode/1.0") resp, err := client.Do(req) if err != nil { diff --git a/internal/llm/tools/view.go b/internal/llm/tools/view.go index 7450a84bf..3fa4ca116 100644 --- a/internal/llm/tools/view.go +++ b/internal/llm/tools/view.go @@ -10,8 +10,8 @@ import ( "path/filepath" "strings" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/lsp" ) type ViewParams struct { diff --git a/internal/llm/tools/write.go b/internal/llm/tools/write.go index bb49381fd..261865c39 100644 --- a/internal/llm/tools/write.go +++ b/internal/llm/tools/write.go @@ -8,11 +8,11 @@ import ( "path/filepath" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/permission" ) type WriteParams struct { diff --git a/internal/llm/tools/write_test.go b/internal/llm/tools/write_test.go index 2264f36fb..b5ecb3fda 100644 --- a/internal/llm/tools/write_test.go +++ b/internal/llm/tools/write_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/internal/logging/writer.go b/internal/logging/writer.go index 9fe469c5e..1dc07e853 100644 --- a/internal/logging/writer.go +++ b/internal/logging/writer.go @@ -9,7 +9,7 @@ import ( "time" "github.com/go-logfmt/logfmt" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) const ( diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 0f03e7fcb..dad07f3c0 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -13,9 +13,9 @@ import ( "sync/atomic" "time" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) type Client struct { diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index c3088d685..7a11286e6 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -3,10 +3,10 @@ package lsp import ( "encoding/json" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" - "github.com/kujtimiihoxha/termai/internal/lsp/util" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/util" ) // Requests diff --git a/internal/lsp/language.go b/internal/lsp/language.go index 2e276c464..65ccd54f3 100644 --- a/internal/lsp/language.go +++ b/internal/lsp/language.go @@ -4,7 +4,7 @@ import ( "path/filepath" "strings" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) func DetectLanguageID(uri string) protocol.LanguageKind { diff --git a/internal/lsp/methods.go b/internal/lsp/methods.go index 079b3bfe3..ab33d7e1b 100644 --- a/internal/lsp/methods.go +++ b/internal/lsp/methods.go @@ -4,7 +4,7 @@ package lsp import ( "context" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) // Implementation sends a textDocument/implementation request to the LSP server. diff --git a/internal/lsp/transport.go b/internal/lsp/transport.go index 89255fd78..fe59b0fbb 100644 --- a/internal/lsp/transport.go +++ b/internal/lsp/transport.go @@ -8,8 +8,8 @@ import ( "io" "strings" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" ) // Write writes an LSP message to the given writer diff --git a/internal/lsp/util/edit.go b/internal/lsp/util/edit.go index 3b94fb39f..52f03ee77 100644 --- a/internal/lsp/util/edit.go +++ b/internal/lsp/util/edit.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error { diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index 156f38e1a..595c78db9 100644 --- a/internal/lsp/watcher/watcher.go +++ b/internal/lsp/watcher/watcher.go @@ -10,10 +10,10 @@ import ( "time" "github.com/fsnotify/fsnotify" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" ) // WorkspaceWatcher manages LSP file watching diff --git a/internal/message/content.go b/internal/message/content.go index f9e76b11c..f52449f4a 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -5,7 +5,7 @@ import ( "slices" "time" - "github.com/kujtimiihoxha/termai/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/models" ) type MessageRole string diff --git a/internal/message/message.go b/internal/message/message.go index 2871780a7..f165fcfc7 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -7,9 +7,9 @@ import ( "fmt" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) type CreateMessageParams struct { diff --git a/internal/permission/permission.go b/internal/permission/permission.go index 8aa280906..4cb379dea 100644 --- a/internal/permission/permission.go +++ b/internal/permission/permission.go @@ -6,7 +6,7 @@ import ( "time" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) var ErrorPermissionDenied = errors.New("permission denied") diff --git a/internal/session/session.go b/internal/session/session.go index 019019df4..280da1ff0 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -5,8 +5,8 @@ import ( "database/sql" "github.com/google/uuid" - "github.com/kujtimiihoxha/termai/internal/db" - "github.com/kujtimiihoxha/termai/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/db" + "github.com/kujtimiihoxha/opencode/internal/pubsub" ) type Session struct { diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index e98001efa..52ff4c8bf 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -5,10 +5,10 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/version" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/version" ) type SendMsg struct { diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index e2f4da9e2..4d6ef5ca0 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -5,11 +5,11 @@ import ( "github.com/charmbracelet/bubbles/textarea" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type editorCmp struct { diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index 26a98970e..c2ce7d88b 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -15,17 +15,17 @@ import ( "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/llm/agent" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/message" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/llm/agent" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/message" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type uiMessageType int diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index b90269d1a..54b39f4a1 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -7,12 +7,12 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/history" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/history" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type sidebarCmp struct { diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 089dffa2c..411cac1c5 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -7,12 +7,12 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/config" - "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/lsp" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/config" + "github.com/kujtimiihoxha/opencode/internal/llm/models" + "github.com/kujtimiihoxha/opencode/internal/lsp" + "github.com/kujtimiihoxha/opencode/internal/lsp/protocol" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type statusCmp struct { diff --git a/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go index 1d3c2b077..6242017f1 100644 --- a/internal/tui/components/dialog/help.go +++ b/internal/tui/components/dialog/help.go @@ -6,7 +6,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type helpCmp struct { diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index 9c55effde..200a7970d 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -9,12 +9,12 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/diff" - "github.com/kujtimiihoxha/termai/internal/llm/tools" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/diff" + "github.com/kujtimiihoxha/opencode/internal/llm/tools" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type PermissionAction string diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go index 10d9ba8a2..5bbe6696c 100644 --- a/internal/tui/components/dialog/quit.go +++ b/internal/tui/components/dialog/quit.go @@ -6,9 +6,9 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) const question = "Are you sure you want to quit?" diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go index 18eb1a526..3a8f17999 100644 --- a/internal/tui/components/logs/details.go +++ b/internal/tui/components/logs/details.go @@ -9,9 +9,9 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type DetailComponent interface { diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index 6e8eb58b1..dc6184e3d 100644 --- a/internal/tui/components/logs/table.go +++ b/internal/tui/components/logs/table.go @@ -7,11 +7,11 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type TableComponent interface { diff --git a/internal/tui/layout/border.go b/internal/tui/layout/border.go index 8fe5c430c..ea9f5e0bc 100644 --- a/internal/tui/layout/border.go +++ b/internal/tui/layout/border.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type BorderPosition int diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go index db07d49fb..603699955 100644 --- a/internal/tui/layout/container.go +++ b/internal/tui/layout/container.go @@ -4,7 +4,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type Container interface { diff --git a/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go index 4a1bcf661..4c05e8462 100644 --- a/internal/tui/layout/overlay.go +++ b/internal/tui/layout/overlay.go @@ -5,8 +5,8 @@ import ( "strings" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/util" "github.com/mattn/go-runewidth" "github.com/muesli/ansi" "github.com/muesli/reflow/truncate" diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go index 6482fc74c..bfb616a53 100644 --- a/internal/tui/layout/split.go +++ b/internal/tui/layout/split.go @@ -4,7 +4,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/opencode/internal/tui/styles" ) type SplitPaneLayout interface { diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index cebc0e461..c268e677f 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -5,11 +5,11 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/session" - "github.com/kujtimiihoxha/termai/internal/tui/components/chat" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/session" + "github.com/kujtimiihoxha/opencode/internal/tui/components/chat" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) var ChatPage PageID = "chat" diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go index d1e557eab..c77a033f4 100644 --- a/internal/tui/page/logs.go +++ b/internal/tui/page/logs.go @@ -2,8 +2,8 @@ package page import ( tea "github.com/charmbracelet/bubbletea" - "github.com/kujtimiihoxha/termai/internal/tui/components/logs" - "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/components/logs" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" ) var LogsPage PageID = "logs" diff --git a/internal/tui/tui.go b/internal/tui/tui.go index dff7ad63d..657de6b6e 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -4,15 +4,15 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/logging" - "github.com/kujtimiihoxha/termai/internal/permission" - "github.com/kujtimiihoxha/termai/internal/pubsub" - "github.com/kujtimiihoxha/termai/internal/tui/components/core" - "github.com/kujtimiihoxha/termai/internal/tui/components/dialog" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/page" - "github.com/kujtimiihoxha/termai/internal/tui/util" + "github.com/kujtimiihoxha/opencode/internal/app" + "github.com/kujtimiihoxha/opencode/internal/logging" + "github.com/kujtimiihoxha/opencode/internal/permission" + "github.com/kujtimiihoxha/opencode/internal/pubsub" + "github.com/kujtimiihoxha/opencode/internal/tui/components/core" + "github.com/kujtimiihoxha/opencode/internal/tui/components/dialog" + "github.com/kujtimiihoxha/opencode/internal/tui/layout" + "github.com/kujtimiihoxha/opencode/internal/tui/page" + "github.com/kujtimiihoxha/opencode/internal/tui/util" ) type keyMap struct { diff --git a/main.go b/main.go index 2e6954646..06578c7ef 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/kujtimiihoxha/termai/cmd" - "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/opencode/cmd" + "github.com/kujtimiihoxha/opencode/internal/logging" ) func main() { -- cgit v1.2.3 From 2de51274177432b559be3b7deb1f14b9539f2994 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Sat, 19 Apr 2025 16:35:45 +0200 Subject: initial tool call stream --- internal/llm/agent/agent.go | 22 ++++++ internal/llm/provider/anthropic.go | 60 ++++++++++++---- internal/llm/provider/openai.go | 9 +-- internal/llm/provider/provider.go | 7 +- internal/message/content.go | 43 ++++++++++++ internal/message/message.go | 2 + internal/pubsub/broker.go | 7 -- internal/tui/components/chat/list.go | 117 +++++++------------------------- internal/tui/components/chat/message.go | 92 ++++++++++++++++++++----- internal/tui/layout/split.go | 28 ++++++++ internal/tui/page/chat.go | 10 ++- 11 files changed, 261 insertions(+), 136 deletions(-) (limited to 'internal/message/message.go') diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 7542d9adf..ae5bcb231 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -380,6 +380,21 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg case provider.EventContentDelta: assistantMsg.AppendContent(event.Content) return a.messages.Update(ctx, *assistantMsg) + case provider.EventToolUseStart: + assistantMsg.AddToolCall(*event.ToolCall) + return a.messages.Update(ctx, *assistantMsg) + // TODO: see how to handle this + // case provider.EventToolUseDelta: + // tm := time.Unix(assistantMsg.UpdatedAt, 0) + // assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input) + // if time.Since(tm) > 1000*time.Millisecond { + // err := a.messages.Update(ctx, *assistantMsg) + // assistantMsg.UpdatedAt = time.Now().Unix() + // return err + // } + case provider.EventToolUseStop: + assistantMsg.FinishToolCall(event.ToolCall.ID) + return a.messages.Update(ctx, *assistantMsg) case provider.EventError: if errors.Is(event.Error, context.Canceled) { logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID)) @@ -456,6 +471,13 @@ func createAgentProvider(agentName config.AgentName) (provider.Provider, error) provider.WithReasoningEffort(agentConfig.ReasoningEffort), ), ) + } else if model.Provider == models.ProviderAnthropic && model.CanReason { + opts = append( + opts, + provider.WithAnthropicOptions( + provider.WithAnthropicShouldThinkFn(provider.DefaultShouldThinkFn), + ), + ) } agentProvider, err := provider.NewProvider( model.Provider, diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index 7bbc02103..2c16a0593 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -93,8 +93,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic } if len(blocks) == 0 { - logging.Warn("There is a message without content, investigate") - // This should never happend but we log this because we might have a bug in our cleanup method + logging.Warn("There is a message without content, investigate, this should not happen") continue } anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...)) @@ -196,8 +195,8 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message, preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools)) cfg := config.Get() if cfg.Debug { - jsonData, _ := json.Marshal(preparedMessages) - logging.Debug("Prepared messages", "messages", string(jsonData)) + // jsonData, _ := json.Marshal(preparedMessages) + // logging.Debug("Prepared messages", "messages", string(jsonData)) } attempts := 0 for { @@ -243,8 +242,8 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools)) cfg := config.Get() if cfg.Debug { - jsonData, _ := json.Marshal(preparedMessages) - logging.Debug("Prepared messages", "messages", string(jsonData)) + // jsonData, _ := json.Marshal(preparedMessages) + // logging.Debug("Prepared messages", "messages", string(jsonData)) } attempts := 0 eventChan := make(chan ProviderEvent) @@ -257,6 +256,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message ) accumulatedMessage := anthropic.Message{} + currentToolCallID := "" for anthropicStream.Next() { event := anthropicStream.Current() err := accumulatedMessage.Accumulate(event) @@ -267,7 +267,19 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message switch event := event.AsAny().(type) { case anthropic.ContentBlockStartEvent: - eventChan <- ProviderEvent{Type: EventContentStart} + if event.ContentBlock.Type == "text" { + eventChan <- ProviderEvent{Type: EventContentStart} + } else if event.ContentBlock.Type == "tool_use" { + currentToolCallID = event.ContentBlock.ID + eventChan <- ProviderEvent{ + Type: EventToolUseStart, + ToolCall: &message.ToolCall{ + ID: event.ContentBlock.ID, + Name: event.ContentBlock.Name, + Finished: false, + }, + } + } case anthropic.ContentBlockDeltaEvent: if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" { @@ -280,11 +292,30 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message Type: EventContentDelta, Content: event.Delta.Text, } + } else if event.Delta.Type == "input_json_delta" { + if currentToolCallID != "" { + eventChan <- ProviderEvent{ + Type: EventToolUseDelta, + ToolCall: &message.ToolCall{ + ID: currentToolCallID, + Finished: false, + Input: event.Delta.JSON.PartialJSON.Raw(), + }, + } + } } - // TODO: check if we can somehow stream tool calls - case anthropic.ContentBlockStopEvent: - eventChan <- ProviderEvent{Type: EventContentStop} + if currentToolCallID != "" { + eventChan <- ProviderEvent{ + Type: EventToolUseStop, + ToolCall: &message.ToolCall{ + ID: currentToolCallID, + }, + } + currentToolCallID = "" + } else { + eventChan <- ProviderEvent{Type: EventContentStop} + } case anthropic.MessageStopEvent: content := "" @@ -378,10 +409,11 @@ func (a *anthropicClient) toolCalls(msg anthropic.Message) []message.ToolCall { switch variant := block.AsAny().(type) { case anthropic.ToolUseBlock: toolCall := message.ToolCall{ - ID: variant.ID, - Name: variant.Name, - Input: string(variant.Input), - Type: string(variant.Type), + ID: variant.ID, + Name: variant.Name, + Input: string(variant.Input), + Type: string(variant.Type), + Finished: true, } toolCalls = append(toolCalls, toolCall) } diff --git a/internal/llm/provider/openai.go b/internal/llm/provider/openai.go index 6c6f74988..40d263242 100644 --- a/internal/llm/provider/openai.go +++ b/internal/llm/provider/openai.go @@ -344,10 +344,11 @@ func (o *openaiClient) toolCalls(completion openai.ChatCompletion) []message.Too if len(completion.Choices) > 0 && len(completion.Choices[0].Message.ToolCalls) > 0 { for _, call := range completion.Choices[0].Message.ToolCalls { toolCall := message.ToolCall{ - ID: call.ID, - Name: call.Function.Name, - Input: call.Function.Arguments, - Type: "function", + ID: call.ID, + Name: call.Function.Name, + Input: call.Function.Arguments, + Type: "function", + Finished: true, } toolCalls = append(toolCalls, toolCall) } diff --git a/internal/llm/provider/provider.go b/internal/llm/provider/provider.go index e04bee71b..283a0d983 100644 --- a/internal/llm/provider/provider.go +++ b/internal/llm/provider/provider.go @@ -15,6 +15,9 @@ const maxRetries = 8 const ( EventContentStart EventType = "content_start" + EventToolUseStart EventType = "tool_use_start" + EventToolUseDelta EventType = "tool_use_delta" + EventToolUseStop EventType = "tool_use_stop" EventContentDelta EventType = "content_delta" EventThinkingDelta EventType = "thinking_delta" EventContentStop EventType = "content_stop" @@ -43,8 +46,8 @@ type ProviderEvent struct { Content string Thinking string Response *ProviderResponse - - Error error + ToolCall *message.ToolCall + Error error } type Provider interface { SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) diff --git a/internal/message/content.go b/internal/message/content.go index f52449f4a..beebe354e 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -233,6 +233,40 @@ func (m *Message) AppendReasoningContent(delta string) { } } +func (m *Message) FinishToolCall(toolCallID string) { + for i, part := range m.Parts { + if c, ok := part.(ToolCall); ok { + if c.ID == toolCallID { + m.Parts[i] = ToolCall{ + ID: c.ID, + Name: c.Name, + Input: c.Input, + Type: c.Type, + Finished: true, + } + return + } + } + } +} + +func (m *Message) AppendToolCallInput(toolCallID string, inputDelta string) { + for i, part := range m.Parts { + if c, ok := part.(ToolCall); ok { + if c.ID == toolCallID { + m.Parts[i] = ToolCall{ + ID: c.ID, + Name: c.Name, + Input: c.Input + inputDelta, + Type: c.Type, + Finished: c.Finished, + } + return + } + } + } +} + func (m *Message) AddToolCall(tc ToolCall) { for i, part := range m.Parts { if c, ok := part.(ToolCall); ok { @@ -246,6 +280,15 @@ func (m *Message) AddToolCall(tc ToolCall) { } func (m *Message) SetToolCalls(tc []ToolCall) { + // remove any existing tool call part it could have multiple + parts := make([]ContentPart, 0) + for _, part := range m.Parts { + if _, ok := part.(ToolCall); ok { + continue + } + parts = append(parts, part) + } + m.Parts = parts for _, toolCall := range tc { m.Parts = append(m.Parts, toolCall) } diff --git a/internal/message/message.go b/internal/message/message.go index f165fcfc7..20ace7b41 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "time" "github.com/google/uuid" "github.com/kujtimiihoxha/opencode/internal/db" @@ -116,6 +117,7 @@ func (s *service) Update(ctx context.Context, message Message) error { if err != nil { return err } + message.UpdatedAt = time.Now().Unix() s.Publish(pubsub.UpdatedEvent, message) return nil } diff --git a/internal/pubsub/broker.go b/internal/pubsub/broker.go index 3e70ae095..d73accffb 100644 --- a/internal/pubsub/broker.go +++ b/internal/pubsub/broker.go @@ -7,13 +7,6 @@ import ( const bufferSize = 1024 -type Logger interface { - Debug(msg string, args ...any) - Info(msg string, args ...any) - Warn(msg string, args ...any) - Error(msg string, args ...any) -} - // Broker allows clients to publish events and subscribe to events type Broker[T any] struct { subs map[chan Event[T]]struct{} // subscriptions diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go index 994ddea03..b09cc4495 100644 --- a/internal/tui/components/chat/list.go +++ b/internal/tui/components/chat/list.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "math" - "sync" - "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" @@ -13,7 +11,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/kujtimiihoxha/opencode/internal/app" - "github.com/kujtimiihoxha/opencode/internal/logging" "github.com/kujtimiihoxha/opencode/internal/message" "github.com/kujtimiihoxha/opencode/internal/pubsub" "github.com/kujtimiihoxha/opencode/internal/session" @@ -35,89 +32,14 @@ type messagesCmp struct { messages []message.Message uiMessages []uiMessage currentMsgID string - mutex sync.Mutex cachedContent map[string]cacheItem spinner spinner.Model - lastUpdate time.Time rendering bool } type renderFinishedMsg struct{} func (m *messagesCmp) Init() tea.Cmd { - return tea.Batch(m.viewport.Init()) -} - -func (m *messagesCmp) preloadSessions() tea.Cmd { - return func() tea.Msg { - m.mutex.Lock() - defer m.mutex.Unlock() - sessions, err := m.app.Sessions.List(context.Background()) - if err != nil { - return util.ReportError(err)() - } - if len(sessions) == 0 { - return nil - } - if len(sessions) > 20 { - sessions = sessions[:20] - } - for _, s := range sessions { - messages, err := m.app.Messages.List(context.Background(), s.ID) - if err != nil { - return util.ReportError(err)() - } - if len(messages) == 0 { - continue - } - m.cacheSessionMessages(messages, m.width) - - } - logging.Debug("preloaded sessions") - - return func() tea.Msg { - return renderFinishedMsg{} - } - } -} - -func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int) { - pos := 0 - if m.width == 0 { - return - } - for inx, msg := range messages { - switch msg.Role { - case message.User: - userMsg := renderUserMessage( - msg, - false, - width, - pos, - ) - m.cachedContent[msg.ID] = cacheItem{ - width: width, - content: []uiMessage{userMsg}, - } - pos += userMsg.height + 1 // + 1 for spacing - case message.Assistant: - assistantMessages := renderAssistantMessage( - msg, - inx, - messages, - m.app.Messages, - "", - width, - pos, - ) - for _, msg := range assistantMessages { - pos += msg.height + 1 // + 1 for spacing - } - m.cachedContent[msg.ID] = cacheItem{ - width: width, - content: assistantMessages, - } - } - } + return tea.Batch(m.viewport.Init(), m.spinner.Tick) } func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -360,21 +282,35 @@ func hasToolsWithoutResponse(messages []message.Message) bool { break } } - if !found { + if !found && v.Finished { return true } } + return false +} +func hasUnfinishedToolCalls(messages []message.Message) bool { + toolCalls := make([]message.ToolCall, 0) + for _, m := range messages { + toolCalls = append(toolCalls, m.ToolCalls()...) + } + for _, v := range toolCalls { + if !v.Finished { + return true + } + } return false } func (m *messagesCmp) working() string { text := "" - if m.IsAgentWorking() { + if m.IsAgentWorking() && len(m.messages) > 0 { task := "Thinking..." lastMessage := m.messages[len(m.messages)-1] if hasToolsWithoutResponse(m.messages) { task = "Waiting for tool response..." + } else if hasUnfinishedToolCalls(m.messages) { + task = "Building tool call..." } else if !lastMessage.IsFinished() { task = "Generating..." } @@ -434,8 +370,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd { delete(m.cachedContent, msg.ID) } m.uiMessages = make([]uiMessage, 0) - m.renderView() - return m.preloadSessions() + return nil } func (m *messagesCmp) GetSize() (int, int) { @@ -446,16 +381,16 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd { if m.session.ID == session.ID { return nil } + m.session = session + messages, err := m.app.Messages.List(context.Background(), session.ID) + if err != nil { + return util.ReportError(err) + } + m.messages = messages + m.currentMsgID = m.messages[len(m.messages)-1].ID + delete(m.cachedContent, m.currentMsgID) m.rendering = true return func() tea.Msg { - m.session = session - messages, err := m.app.Messages.List(context.Background(), session.ID) - if err != nil { - return util.ReportError(err) - } - m.messages = messages - m.currentMsgID = m.messages[len(m.messages)-1].ID - delete(m.cachedContent, m.currentMsgID) m.renderView() return renderFinishedMsg{} } diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go index 14b9e268e..b8e450079 100644 --- a/internal/tui/components/chat/message.go +++ b/internal/tui/components/chat/message.go @@ -113,18 +113,10 @@ func renderAssistantMessage( width int, position int, ) []uiMessage { - // find the user message that is before this assistant message - var userMsg message.Message - for i := msgIndex - 1; i >= 0; i-- { - msg := allMessages[i] - if msg.Role == message.User { - userMsg = allMessages[i] - break - } - } - messages := []uiMessage{} content := msg.Content().String() + thinking := msg.IsThinking() + thinkingContent := msg.ReasoningContent().Thinking finished := msg.IsFinished() finishData := msg.FinishPart() info := []string{} @@ -133,7 +125,7 @@ func renderAssistantMessage( if finished { switch finishData.Reason { case message.FinishReasonEndTurn: - took := formatTimeDifference(userMsg.CreatedAt, finishData.Time) + took := formatTimeDifference(msg.CreatedAt, finishData.Time) info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took), )) @@ -166,6 +158,9 @@ func renderAssistantMessage( }) position += messages[0].height position++ // for the space + } else if thinking && thinkingContent != "" { + // Render the thinking content + content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width) } for i, toolCall := range msg.ToolCalls() { @@ -218,10 +213,40 @@ func toolName(name string) string { return "View" case tools.WriteToolName: return "Write" + case tools.PatchToolName: + return "Patch" } return name } +func getToolAction(name string) string { + switch name { + case agent.AgentToolName: + return "Preparing prompt..." + case tools.BashToolName: + return "Building command..." + case tools.EditToolName: + return "Preparing edit..." + case tools.FetchToolName: + return "Writing fetch..." + case tools.GlobToolName: + return "Finding files..." + case tools.GrepToolName: + return "Searching content..." + case tools.LSToolName: + return "Listing directory..." + case tools.SourcegraphToolName: + return "Searching code..." + case tools.ViewToolName: + return "Reading file..." + case tools.WriteToolName: + return "Preparing write..." + case tools.PatchToolName: + return "Preparing patch..." + } + return "Working..." +} + // renders params, params[0] (params[1]=params[2] ....) func renderParams(paramsWidth int, params ...string) string { if len(params) == 0 { @@ -490,8 +515,47 @@ func renderToolMessage( if nested { width = width - 3 } + style := styles.BaseStyle. + Width(width - 1). + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + PaddingLeft(1). + BorderForeground(styles.ForgroundDim) + response := findToolResponse(toolCall.ID, allMessages) toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name))) + + if !toolCall.Finished { + // Get a brief description of what the tool is doing + toolAction := getToolAction(toolCall.Name) + + // toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ") + // truncatedInput := toolInput + // if len(truncatedInput) > 10 { + // truncatedInput = truncatedInput[len(truncatedInput)-10:] + // } + // + // truncatedInput = styles.BaseStyle. + // Italic(true). + // Width(width - 2 - lipgloss.Width(toolName)). + // Background(styles.BackgroundDim). + // Foreground(styles.ForgroundMid). + // Render(truncatedInput) + + progressText := styles.BaseStyle. + Width(width - 2 - lipgloss.Width(toolName)). + Foreground(styles.ForgroundDim). + Render(fmt.Sprintf("%s", toolAction)) + + content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText)) + toolMsg := uiMessage{ + messageType: toolMessageType, + position: position, + height: lipgloss.Height(content), + content: content, + } + return toolMsg + } params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall) responseContent := "" if response != nil { @@ -504,12 +568,6 @@ func renderToolMessage( Foreground(styles.ForgroundDim). Render("Waiting for response...") } - style := styles.BaseStyle. - Width(width - 1). - BorderLeft(true). - BorderStyle(lipgloss.ThickBorder()). - PaddingLeft(1). - BorderForeground(styles.ForgroundDim) parts := []string{} if !nested { diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go index a41df6ab8..f3ab9247d 100644 --- a/internal/tui/layout/split.go +++ b/internal/tui/layout/split.go @@ -14,6 +14,10 @@ type SplitPaneLayout interface { SetLeftPanel(panel Container) tea.Cmd SetRightPanel(panel Container) tea.Cmd SetBottomPanel(panel Container) tea.Cmd + + ClearLeftPanel() tea.Cmd + ClearRightPanel() tea.Cmd + ClearBottomPanel() tea.Cmd } type splitPaneLayout struct { @@ -192,6 +196,30 @@ func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd { return nil } +func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd { + s.leftPanel = nil + if s.width > 0 && s.height > 0 { + return s.SetSize(s.width, s.height) + } + return nil +} + +func (s *splitPaneLayout) ClearRightPanel() tea.Cmd { + s.rightPanel = nil + if s.width > 0 && s.height > 0 { + return s.SetSize(s.width, s.height) + } + return nil +} + +func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd { + s.bottomPanel = nil + if s.width > 0 && s.height > 0 { + return s.SetSize(s.width, s.height) + } + return nil +} + func (s *splitPaneLayout) BindingKeys() []key.Binding { keys := []key.Binding{} if s.leftPanel != nil { diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index ef826e9a3..a5a656a22 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -57,6 +57,14 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if cmd != nil { return p, cmd } + case chat.SessionSelectedMsg: + if p.session.ID == "" { + cmd := p.setSidebar() + if cmd != nil { + cmds = append(cmds, cmd) + } + } + p.session = msg case chat.EditorFocusMsg: p.editingMode = bool(msg) case tea.KeyMsg: @@ -91,7 +99,7 @@ func (p *chatPage) setSidebar() tea.Cmd { } func (p *chatPage) clearSidebar() tea.Cmd { - return p.layout.SetRightPanel(nil) + return p.layout.ClearRightPanel() } func (p *chatPage) sendMessage(text string) tea.Cmd { -- cgit v1.2.3