on main help sign in
implement nested tool calls and initial setup for result metadata
0697dccKujtim Hoxha
go.mod+12−9
@@ -15,6 +15,7 @@ require (
15 github.com/charmbracelet/glamour v0.9.1
15 github.com/charmbracelet/glamour v0.9.1
16 github.com/charmbracelet/huh v0.6.0
16 github.com/charmbracelet/huh v0.6.0
17 github.com/charmbracelet/lipgloss v1.1.0
17 github.com/charmbracelet/lipgloss v1.1.0
18+ github.com/charmbracelet/x/ansi v0.8.0
18 github.com/fsnotify/fsnotify v1.8.0
19 github.com/fsnotify/fsnotify v1.8.0
19 github.com/go-logfmt/logfmt v0.6.0
20 github.com/go-logfmt/logfmt v0.6.0
20 github.com/golang-migrate/migrate/v4 v4.18.2
21 github.com/golang-migrate/migrate/v4 v4.18.2
@@ -29,11 +30,11 @@ require (
29 github.com/muesli/reflow v0.3.0
30 github.com/muesli/reflow v0.3.0
30 github.com/muesli/termenv v0.16.0
31 github.com/muesli/termenv v0.16.0
31 github.com/openai/openai-go v0.1.0-beta.2
32 github.com/openai/openai-go v0.1.0-beta.2
32− github.com/sergi/go-diff v1.3.1
33+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
33 github.com/spf13/cobra v1.9.1
34 github.com/spf13/cobra v1.9.1
34 github.com/spf13/viper v1.20.0
35 github.com/spf13/viper v1.20.0
35 github.com/stretchr/testify v1.10.0
36 github.com/stretchr/testify v1.10.0
36− golang.org/x/net v0.34.0
37+ golang.org/x/net v0.39.0
37 google.golang.org/api v0.215.0
38 google.golang.org/api v0.215.0
38)
39)
39
40
@@ -64,7 +65,6 @@ require (
64 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
65 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
65 github.com/aymerick/douceur v0.2.0 // indirect
66 github.com/aymerick/douceur v0.2.0 // indirect
66 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
67 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
67− github.com/charmbracelet/x/ansi v0.8.0 // indirect
68 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
68 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
69 github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
69 github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
70 github.com/charmbracelet/x/term v0.2.1 // indirect
70 github.com/charmbracelet/x/term v0.2.1 // indirect
@@ -76,6 +76,7 @@ require (
76 github.com/go-logr/logr v1.4.2 // indirect
76 github.com/go-logr/logr v1.4.2 // indirect
77 github.com/go-logr/stdr v1.2.2 // indirect
77 github.com/go-logr/stdr v1.2.2 // indirect
78 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
78 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
79+ github.com/google/go-cmp v0.7.0 // indirect
79 github.com/google/s2a-go v0.1.8 // indirect
80 github.com/google/s2a-go v0.1.8 // indirect
80 github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
81 github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
81 github.com/googleapis/gax-go/v2 v2.14.1 // indirect
82 github.com/googleapis/gax-go/v2 v2.14.1 // indirect
@@ -92,6 +93,7 @@ require (
92 github.com/pelletier/go-toml/v2 v2.2.3 // indirect
93 github.com/pelletier/go-toml/v2 v2.2.3 // indirect
93 github.com/pmezard/go-difflib v1.0.0 // indirect
94 github.com/pmezard/go-difflib v1.0.0 // indirect
94 github.com/rivo/uniseg v0.4.7 // indirect
95 github.com/rivo/uniseg v0.4.7 // indirect
96+ github.com/rogpeppe/go-internal v1.14.1 // indirect
95 github.com/sagikazarmark/locafero v0.7.0 // indirect
97 github.com/sagikazarmark/locafero v0.7.0 // indirect
96 github.com/sahilm/fuzzy v0.1.1 // indirect
98 github.com/sahilm/fuzzy v0.1.1 // indirect
97 github.com/sourcegraph/conc v0.3.0 // indirect
99 github.com/sourcegraph/conc v0.3.0 // indirect
@@ -115,20 +117,21 @@ require (
115 go.uber.org/atomic v1.9.0 // indirect
117 go.uber.org/atomic v1.9.0 // indirect
116 go.uber.org/multierr v1.9.0 // indirect
118 go.uber.org/multierr v1.9.0 // indirect
117 golang.design/x/clipboard v0.7.0 // indirect
119 golang.design/x/clipboard v0.7.0 // indirect
118− golang.org/x/crypto v0.33.0 // indirect
120+ golang.org/x/crypto v0.37.0 // indirect
119− golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
121+ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
120 golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect
122 golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect
121 golang.org/x/image v0.14.0 // indirect
123 golang.org/x/image v0.14.0 // indirect
122 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
124 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
123 golang.org/x/oauth2 v0.25.0 // indirect
125 golang.org/x/oauth2 v0.25.0 // indirect
124− golang.org/x/sync v0.12.0 // indirect
126+ golang.org/x/sync v0.13.0 // indirect
125− golang.org/x/sys v0.31.0 // indirect
127+ golang.org/x/sys v0.32.0 // indirect
126− golang.org/x/term v0.30.0 // indirect
128+ golang.org/x/term v0.31.0 // indirect
127− golang.org/x/text v0.23.0 // indirect
129+ golang.org/x/text v0.24.0 // indirect
128 golang.org/x/time v0.8.0 // indirect
130 golang.org/x/time v0.8.0 // indirect
129 google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
131 google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
130 google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
132 google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
131 google.golang.org/grpc v1.67.3 // indirect
133 google.golang.org/grpc v1.67.3 // indirect
132 google.golang.org/protobuf v1.36.1 // indirect
134 google.golang.org/protobuf v1.36.1 // indirect
135+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
133 gopkg.in/yaml.v3 v3.0.1 // indirect
136 gopkg.in/yaml.v3 v3.0.1 // indirect
134)
137)
go.sum+23−20
@@ -117,8 +117,8 @@ github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK
117github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
117github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
118github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
118github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
119github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
119github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
120−github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
120+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
121−github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
121+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
122github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
122github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
123github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
123github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
124github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
124github.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
139github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
139github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
140github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
140github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
141github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
141github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
142+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
142github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
143github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
143github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
144github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
144github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
145github.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
189github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
190github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
190github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
191github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
191github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
192github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
192−github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
193+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
193−github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
194+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
194github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
195github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
195github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
196github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
196github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
197github.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
199github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
200github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
200github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
201github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
201github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
202github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
202−github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
203github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
203github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
204+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
205+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
204github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
206github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
205github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
207github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
206github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
208github.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
261golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
263golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
262golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
264golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
263golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
265golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
264−golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
266+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
265−golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
267+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
266−golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
268+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
267−golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
269+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
268golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 h1:bFYqOIMdeiCEdzPJkLiOoMDzW/v3tjW4AA/RmUZYsL8=
270golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 h1:bFYqOIMdeiCEdzPJkLiOoMDzW/v3tjW4AA/RmUZYsL8=
269golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
271golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
270golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
272golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
@@ -282,15 +284,15 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
282golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
284golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
283golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
285golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
284golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
286golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
285−golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
287+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
286−golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
288+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
287golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
289golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
288golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
290golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
289golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
291golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
290golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
292golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
291golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
293golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
292−golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
294+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
293−golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
295+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
294golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
296golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
295golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
297golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
296golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
298golang.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=
304golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
306golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
305golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
307golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
306golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
308golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
307−golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
309+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
308−golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
310+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
309golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
311golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
310golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
312golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
311golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
313golang.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=
314golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
316golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
315golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
317golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
316golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
318golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
317−golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
319+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
318−golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
320+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
319golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
321golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
320golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
322golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
321golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
323golang.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=
323golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
325golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
324golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
326golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
325golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
327golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
326−golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
328+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
327−golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
329+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
328golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
330golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
329golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
331golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
330golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
332golang.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
343google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
345google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
344google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
346google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
345gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
347gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
346−gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
347gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
348gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
349+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
350+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
348gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
351gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
349gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
352gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
350gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
353gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
internal/llm/agent/agent.go+1
@@ -305,6 +305,7 @@ func (c *agent) generate(ctx context.Context, sessionID string, content string)
305 assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{
305 assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{
306 Role: message.Assistant,
306 Role: message.Assistant,
307 Parts: []message.ContentPart{},
307 Parts: []message.ContentPart{},
308+ Model: c.model.ID,
308 })
309 })
309 if err != nil {
310 if err != nil {
310 return err
311 return err
internal/llm/tools/bash.go+11−2
@@ -5,6 +5,7 @@ import (
5 "encoding/json"
5 "encoding/json"
6 "fmt"
6 "fmt"
7 "strings"
7 "strings"
8+ "time"
8
9
9 "github.com/kujtimiihoxha/termai/internal/config"
10 "github.com/kujtimiihoxha/termai/internal/config"
10 "github.com/kujtimiihoxha/termai/internal/llm/tools/shell"
11 "github.com/kujtimiihoxha/termai/internal/llm/tools/shell"
@@ -21,6 +22,9 @@ type BashPermissionsParams struct {
21 Timeout int `json:"timeout"`
22 Timeout int `json:"timeout"`
22}
23}
23
24
25+type BashToolResponseMetadata struct {
26+ Took int64 `json:"took"`
27+}
24type bashTool struct {
28type bashTool struct {
25 permissions permission.Service
29 permissions permission.Service
26}
30}
@@ -272,11 +276,13 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
272 return NewTextErrorResponse("permission denied"), nil
276 return NewTextErrorResponse("permission denied"), nil
273 }
277 }
274 }
278 }
279+ startTime := time.Now()
275 shell := shell.GetPersistentShell(config.WorkingDirectory())
280 shell := shell.GetPersistentShell(config.WorkingDirectory())
276 stdout, stderr, exitCode, interrupted, err := shell.Exec(ctx, params.Command, params.Timeout)
281 stdout, stderr, exitCode, interrupted, err := shell.Exec(ctx, params.Command, params.Timeout)
277 if err != nil {
282 if err != nil {
278 return NewTextErrorResponse(fmt.Sprintf("error executing command: %s", err)), nil
283 return NewTextErrorResponse(fmt.Sprintf("error executing command: %s", err)), nil
279 }
284 }
285+ took := time.Since(startTime).Milliseconds()
280
286
281 stdout = truncateOutput(stdout)
287 stdout = truncateOutput(stdout)
282 stderr = truncateOutput(stderr)
288 stderr = truncateOutput(stderr)
@@ -304,10 +310,13 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
304 stdout += "\n" + errorMessage
310 stdout += "\n" + errorMessage
305 }
311 }
306
312
313+ metadata := BashToolResponseMetadata{
314+ Took: took,
315+ }
307 if stdout == "" {
316 if stdout == "" {
308− return NewTextResponse("no output"), nil
317+ return WithResponseMetadata(NewTextResponse("no output"), metadata), nil
309 }
318 }
310− return NewTextResponse(stdout), nil
319+ return WithResponseMetadata(NewTextResponse(stdout), metadata), nil
311}
320}
312
321
313func truncateOutput(content string) string {
322func truncateOutput(content string) string {
internal/llm/tools/tools.go+19−4
@@ -1,6 +1,9 @@
1package tools
1package tools
2
2
3+import (
3−import "context"
4+ "context"
5+ "encoding/json"
6+)
4
7
5type ToolInfo struct {
8type ToolInfo struct {
6 Name string
9 Name string
@@ -17,9 +20,10 @@ const (
17)
20)
18
21
19type ToolResponse struct {
22type ToolResponse struct {
20− Type toolResponseType `json:"type"`
23+ Type toolResponseType `json:"type"`
21− Content string `json:"content"`
24+ Content string `json:"content"`
25+ Metadata string `json:"metadata,omitempty"`
22− IsError bool `json:"is_error"`
26+ IsError bool `json:"is_error"`
23}
27}
24
28
25func NewTextResponse(content string) ToolResponse {
29func NewTextResponse(content string) ToolResponse {
@@ -29,6 +33,17 @@ func NewTextResponse(content string) ToolResponse {
29 }
33 }
30}
34}
31
35
36+func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse {
37+ if metadata != nil {
38+ metadataBytes, err := json.Marshal(metadata)
39+ if err != nil {
40+ return response
41+ }
42+ response.Metadata = string(metadataBytes)
43+ }
44+ return response
45+}
46+
32func NewTextErrorResponse(content string) ToolResponse {
47func NewTextErrorResponse(content string) ToolResponse {
33 return ToolResponse{
48 return ToolResponse{
34 Type: ToolResponseTypeText,
49 Type: ToolResponseTypeText,
internal/message/content.go+4−1
@@ -3,6 +3,8 @@ package message
3import (
3import (
4 "encoding/base64"
4 "encoding/base64"
5 "time"
5 "time"
6+
7+ "github.com/kujtimiihoxha/termai/internal/llm/models"
6)
8)
7
9
8type MessageRole string
10type MessageRole string
@@ -65,7 +67,6 @@ type ToolCall struct {
65 Name string `json:"name"`
67 Name string `json:"name"`
66 Input string `json:"input"`
68 Input string `json:"input"`
67 Type string `json:"type"`
69 Type string `json:"type"`
68− Metadata any `json:"metadata"`
69 Finished bool `json:"finished"`
70 Finished bool `json:"finished"`
70}
71}
71
72
@@ -75,6 +76,7 @@ type ToolResult struct {
75 ToolCallID string `json:"tool_call_id"`
76 ToolCallID string `json:"tool_call_id"`
76 Name string `json:"name"`
77 Name string `json:"name"`
77 Content string `json:"content"`
78 Content string `json:"content"`
79+ Metadata string `json:"metadata"`
78 IsError bool `json:"is_error"`
80 IsError bool `json:"is_error"`
79}
81}
80
82
@@ -92,6 +94,7 @@ type Message struct {
92 Role MessageRole
94 Role MessageRole
93 SessionID string
95 SessionID string
94 Parts []ContentPart
96 Parts []ContentPart
97+ Model models.ModelID
95
98
96 CreatedAt int64
99 CreatedAt int64
97 UpdatedAt int64
100 UpdatedAt int64
internal/message/message.go+1
@@ -155,6 +155,7 @@ func (s *service) fromDBItem(item db.Message) (Message, error) {
155 SessionID: item.SessionID,
155 SessionID: item.SessionID,
156 Role: MessageRole(item.Role),
156 Role: MessageRole(item.Role),
157 Parts: parts,
157 Parts: parts,
158+ Model: models.ModelID(item.Model.String),
158 CreatedAt: item.CreatedAt,
159 CreatedAt: item.CreatedAt,
159 UpdatedAt: item.UpdatedAt,
160 UpdatedAt: item.UpdatedAt,
160 }, nil
161 }, nil
internal/tui/components/chat/editor.go+7−8
@@ -77,21 +77,20 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77 case AgentWorkingMsg:
77 case AgentWorkingMsg:
78 m.agentWorking = bool(msg)
78 m.agentWorking = bool(msg)
79 case tea.KeyMsg:
79 case tea.KeyMsg:
80+ // if the key does not match any binding, return
80− if key.Matches(msg, focusedKeyMaps.Send) {
81+ if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) {
81 return m, m.send()
82 return m, m.send()
82 }
83 }
83− if key.Matches(msg, bluredKeyMaps.Send) {
84+ if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) {
84 return m, m.send()
85 return m, m.send()
85 }
86 }
86− if key.Matches(msg, focusedKeyMaps.Blur) {
87+ if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Blur) {
87 m.textarea.Blur()
88 m.textarea.Blur()
88 return m, util.CmdHandler(EditorFocusMsg(false))
89 return m, util.CmdHandler(EditorFocusMsg(false))
89 }
90 }
90− if key.Matches(msg, bluredKeyMaps.Focus) {
91+ if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) {
91− if !m.textarea.Focused() {
92− m.textarea.Focus()
92+ m.textarea.Focus()
93− return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
93+ return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
94− }
95 }
94 }
96 }
95 }
97 m.textarea, cmd = m.textarea.Update(msg)
96 m.textarea, cmd = m.textarea.Update(msg)
internal/tui/components/chat/messages.go+351−107
@@ -1,16 +1,21 @@
1package chat
1package chat
2
2
3import (
3import (
4+ "encoding/json"
4 "fmt"
5 "fmt"
5− "regexp"
6+ "math"
6− "strconv"
7 "strings"
7 "strings"
8
8
9+ "github.com/charmbracelet/bubbles/spinner"
9 "github.com/charmbracelet/bubbles/viewport"
10 "github.com/charmbracelet/bubbles/viewport"
10 tea "github.com/charmbracelet/bubbletea"
11 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/glamour"
12 "github.com/charmbracelet/glamour"
12 "github.com/charmbracelet/lipgloss"
13 "github.com/charmbracelet/lipgloss"
14+ "github.com/charmbracelet/x/ansi"
13 "github.com/kujtimiihoxha/termai/internal/app"
15 "github.com/kujtimiihoxha/termai/internal/app"
16+ "github.com/kujtimiihoxha/termai/internal/llm/agent"
17+ "github.com/kujtimiihoxha/termai/internal/llm/models"
18+ "github.com/kujtimiihoxha/termai/internal/llm/tools"
14 "github.com/kujtimiihoxha/termai/internal/message"
19 "github.com/kujtimiihoxha/termai/internal/message"
15 "github.com/kujtimiihoxha/termai/internal/pubsub"
20 "github.com/kujtimiihoxha/termai/internal/pubsub"
16 "github.com/kujtimiihoxha/termai/internal/session"
21 "github.com/kujtimiihoxha/termai/internal/session"
@@ -18,10 +23,20 @@ import (
18 "github.com/kujtimiihoxha/termai/internal/tui/util"
23 "github.com/kujtimiihoxha/termai/internal/tui/util"
19)
24)
20
25
26+type uiMessageType int
27+
28+const (
29+ userMessageType uiMessageType = iota
30+ assistantMessageType
31+ toolMessageType
32+)
33+
21type uiMessage struct {
34type uiMessage struct {
35+ ID string
36+ messageType uiMessageType
22− position int
37+ position int
23− height int
38+ height int
24− content string
39+ content string
25}
40}
26
41
27type messagesCmp struct {
42type messagesCmp struct {
@@ -32,141 +47,116 @@ type messagesCmp struct {
32 session session.Session
47 session session.Session
33 messages []message.Message
48 messages []message.Message
34 uiMessages []uiMessage
49 uiMessages []uiMessage
35− currentIndex int
50+ currentMsgID string
36 renderer *glamour.TermRenderer
51 renderer *glamour.TermRenderer
37 focusRenderer *glamour.TermRenderer
52 focusRenderer *glamour.TermRenderer
38 cachedContent map[string]string
53 cachedContent map[string]string
54+ agentWorking bool
55+ spinner spinner.Model
56+ needsRerender bool
57+ lastViewport string
39}
58}
40
59
41func (m *messagesCmp) Init() tea.Cmd {
60func (m *messagesCmp) Init() tea.Cmd {
42− return m.viewport.Init()
61+ return tea.Batch(m.viewport.Init())
43−}
44−
45−var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
46−
47−func hexToBgSGR(hex string) (string, error) {
48− hex = strings.TrimPrefix(hex, "#")
49− if len(hex) != 6 {
50− return "", fmt.Errorf("invalid hex color: must be 6 hexadecimal digits")
51− }
52−
53− // Parse RGB components in one block
54− rgb := make([]uint64, 3)
55− for i := 0; i < 3; i++ {
56− val, err := strconv.ParseUint(hex[i*2:i*2+2], 16, 8)
57− if err != nil {
58− return "", err
59− }
60− rgb[i] = val
61− }
62−
63− return fmt.Sprintf("48;2;%d;%d;%d", rgb[0], rgb[1], rgb[2]), nil
64−}
65−
66−func forceReplaceBackgroundColors(input string, newBg string) string {
67− return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
68− // Extract content between "\x1b[" and "m"
69− content := seq[2 : len(seq)-1]
70− tokens := strings.Split(content, ";")
71− var newTokens []string
72−
73− // Skip background color tokens
74− for i := 0; i < len(tokens); i++ {
75− if tokens[i] == "" {
76− continue
77− }
78−
79− val, err := strconv.Atoi(tokens[i])
80− if err != nil {
81− newTokens = append(newTokens, tokens[i])
82− continue
83− }
84−
85− // Skip background color tokens
86− if val == 48 {
87− // Skip "48;5;N" or "48;2;R;G;B" sequences
88− if i+1 < len(tokens) {
89− if nextVal, err := strconv.Atoi(tokens[i+1]); err == nil {
90− switch nextVal {
91− case 5:
92− i += 2 // Skip "5" and color index
93− case 2:
94− i += 4 // Skip "2" and RGB components
95− }
96− }
97− }
98− } else if (val < 40 || val > 47) && (val < 100 || val > 107) && val != 49 {
99− // Keep non-background tokens
100− newTokens = append(newTokens, tokens[i])
101− }
102− }
103−
104− // Add new background if provided
105− if newBg != "" {
106− newTokens = append(newTokens, strings.Split(newBg, ";")...)
107− }
108−
109− if len(newTokens) == 0 {
110− return ""
111− }
112−
113− return "\x1b[" + strings.Join(newTokens, ";") + "m"
114− })
115}
62}
116
63
117func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
64func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
65+ var cmds []tea.Cmd
118 switch msg := msg.(type) {
66 switch msg := msg.(type) {
67+ case AgentWorkingMsg:
68+ m.agentWorking = bool(msg)
69+ if m.agentWorking {
70+ cmds = append(cmds, m.spinner.Tick)
71+ }
119 case EditorFocusMsg:
72 case EditorFocusMsg:
120 m.writingMode = bool(msg)
73 m.writingMode = bool(msg)
121 case SessionSelectedMsg:
74 case SessionSelectedMsg:
122 if msg.ID != m.session.ID {
75 if msg.ID != m.session.ID {
123 cmd := m.SetSession(msg)
76 cmd := m.SetSession(msg)
77+ m.needsRerender = true
124 return m, cmd
78 return m, cmd
125 }
79 }
126 return m, nil
80 return m, nil
81+ case SessionClearedMsg:
82+ m.session = session.Session{}
83+ m.messages = make([]message.Message, 0)
84+ m.currentMsgID = ""
85+ m.needsRerender = true
86+ return m, nil
87+
88+ case tea.KeyMsg:
89+ if m.writingMode {
90+ return m, nil
91+ }
127 case pubsub.Event[message.Message]:
92 case pubsub.Event[message.Message]:
128 if msg.Type == pubsub.CreatedEvent {
93 if msg.Type == pubsub.CreatedEvent {
129 if msg.Payload.SessionID == m.session.ID {
94 if msg.Payload.SessionID == m.session.ID {
130 // check if message exists
95 // check if message exists
96+
97+ messageExists := false
131 for _, v := range m.messages {
98 for _, v := range m.messages {
132 if v.ID == msg.Payload.ID {
99 if v.ID == msg.Payload.ID {
133− return m, nil
100+ messageExists = true
101+ break
134 }
102 }
135 }
103 }
136
104
105+ if !messageExists {
137− m.messages = append(m.messages, msg.Payload)
106+ m.messages = append(m.messages, msg.Payload)
138− m.renderView()
107+ delete(m.cachedContent, m.currentMsgID)
139− m.viewport.GotoBottom()
108+ m.currentMsgID = msg.Payload.ID
109+ m.needsRerender = true
110+ }
140 }
111 }
141 for _, v := range m.messages {
112 for _, v := range m.messages {
142 for _, c := range v.ToolCalls() {
113 for _, c := range v.ToolCalls() {
143 // the message is being added to the session of a tool called
114 // the message is being added to the session of a tool called
144 if c.ID == msg.Payload.SessionID {
115 if c.ID == msg.Payload.SessionID {
145− m.renderView()
116+ m.needsRerender = true
146− m.viewport.GotoBottom()
147 }
117 }
148 }
118 }
149 }
119 }
150 } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
120 } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
151 for i, v := range m.messages {
121 for i, v := range m.messages {
152 if v.ID == msg.Payload.ID {
122 if v.ID == msg.Payload.ID {
123+ if !m.messages[i].IsFinished() && msg.Payload.IsFinished() && msg.Payload.FinishReason() == "end_turn" || msg.Payload.FinishReason() == "canceled" {
124+ cmds = append(cmds, util.CmdHandler(AgentWorkingMsg(false)))
125+ }
153 m.messages[i] = msg.Payload
126 m.messages[i] = msg.Payload
154 delete(m.cachedContent, msg.Payload.ID)
127 delete(m.cachedContent, msg.Payload.ID)
155− m.renderView()
128+ m.needsRerender = true
156− if i == len(m.messages)-1 {
157− m.viewport.GotoBottom()
158− }
159 break
129 break
160 }
130 }
161 }
131 }
162 }
132 }
163 }
133 }
134+ if m.agentWorking {
135+ u, cmd := m.spinner.Update(msg)
136+ m.spinner = u
137+ cmds = append(cmds, cmd)
138+ }
139+ oldPos := m.viewport.YPosition
164 u, cmd := m.viewport.Update(msg)
140 u, cmd := m.viewport.Update(msg)
165 m.viewport = u
141 m.viewport = u
166− return m, cmd
142+ m.needsRerender = m.needsRerender || m.viewport.YPosition != oldPos
143+ cmds = append(cmds, cmd)
144+ if m.needsRerender {
145+ m.renderView()
146+ if len(m.messages) > 0 {
147+ if msg, ok := msg.(pubsub.Event[message.Message]); ok {
148+ if (msg.Type == pubsub.CreatedEvent) ||
149+ (msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
150+ m.viewport.GotoBottom()
151+ }
152+ }
153+ }
154+ m.needsRerender = false
155+ }
156+ return m, tea.Batch(cmds...)
167}
157}
168
158
169−func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
159+func (m *messagesCmp) renderSimpleMessage(msg message.Message, info ...string) string {
170 if v, ok := m.cachedContent[msg.ID]; ok {
160 if v, ok := m.cachedContent[msg.ID]; ok {
171 return v
161 return v
172 }
162 }
@@ -178,7 +168,7 @@ func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
178 BorderStyle(lipgloss.ThickBorder())
168 BorderStyle(lipgloss.ThickBorder())
179
169
180 renderer := m.renderer
170 renderer := m.renderer
181− if inx == m.currentIndex {
171+ if msg.ID == m.currentMsgID {
182 style = style.
172 style = style.
183 Foreground(styles.Forground).
173 Foreground(styles.Forground).
184 BorderForeground(styles.Blue).
174 BorderForeground(styles.Blue).
@@ -186,33 +176,269 @@ func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
186 renderer = m.focusRenderer
176 renderer = m.focusRenderer
187 }
177 }
188 c, _ := renderer.Render(msg.Content().String())
178 c, _ := renderer.Render(msg.Content().String())
189− col, _ := hexToBgSGR(styles.Background.Dark)
179+ parts := []string{
180+ styles.ForceReplaceBackgroundWithLipgloss(c, styles.Background),
181+ }
182+ // remove newline at the end
183+ parts[0] = strings.TrimSuffix(parts[0], "\n")
184+ if len(info) > 0 {
185+ parts = append(parts, info...)
186+ }
190− rendered := style.Render(forceReplaceBackgroundColors(c, col))
187+ rendered := style.Render(
188+ lipgloss.JoinVertical(
189+ lipgloss.Left,
190+ parts...,
191+ ),
192+ )
191 m.cachedContent[msg.ID] = rendered
193 m.cachedContent[msg.ID] = rendered
192 return rendered
194 return rendered
193}
195}
194
196
197+func formatTimeDifference(unixTime1, unixTime2 int64) string {
198+ diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
199+
200+ if diffSeconds < 60 {
201+ return fmt.Sprintf("%.1fs", diffSeconds)
202+ }
203+
204+ minutes := int(diffSeconds / 60)
205+ seconds := int(diffSeconds) % 60
206+ return fmt.Sprintf("%dm%ds", minutes, seconds)
207+}
208+
209+func (m *messagesCmp) renderToolCall(toolCall message.ToolCall, isNested bool) string {
210+ key := ""
211+ value := ""
212+ switch toolCall.Name {
213+ // TODO: add result data to the tools
214+ case agent.AgentToolName:
215+ key = "Task"
216+ var params agent.AgentParams
217+ json.Unmarshal([]byte(toolCall.Input), &params)
218+ value = params.Prompt
219+ // TODO: handle nested calls
220+ case tools.BashToolName:
221+ key = "Bash"
222+ var params tools.BashParams
223+ json.Unmarshal([]byte(toolCall.Input), &params)
224+ value = params.Command
225+ case tools.EditToolName:
226+ key = "Edit"
227+ var params tools.EditParams
228+ json.Unmarshal([]byte(toolCall.Input), &params)
229+ value = params.FilePath
230+ case tools.FetchToolName:
231+ key = "Fetch"
232+ var params tools.FetchParams
233+ json.Unmarshal([]byte(toolCall.Input), &params)
234+ value = params.URL
235+ case tools.GlobToolName:
236+ key = "Glob"
237+ var params tools.GlobParams
238+ json.Unmarshal([]byte(toolCall.Input), &params)
239+ if params.Path == "" {
240+ params.Path = "."
241+ }
242+ value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path)
243+ case tools.GrepToolName:
244+ key = "Grep"
245+ var params tools.GrepParams
246+ json.Unmarshal([]byte(toolCall.Input), &params)
247+ if params.Path == "" {
248+ params.Path = "."
249+ }
250+ value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path)
251+ case tools.LSToolName:
252+ key = "Ls"
253+ var params tools.LSParams
254+ json.Unmarshal([]byte(toolCall.Input), &params)
255+ if params.Path == "" {
256+ params.Path = "."
257+ }
258+ value = params.Path
259+ case tools.SourcegraphToolName:
260+ key = "Sourcegraph"
261+ var params tools.SourcegraphParams
262+ json.Unmarshal([]byte(toolCall.Input), &params)
263+ value = params.Query
264+ case tools.ViewToolName:
265+ key = "View"
266+ var params tools.ViewParams
267+ json.Unmarshal([]byte(toolCall.Input), &params)
268+ value = params.FilePath
269+ case tools.WriteToolName:
270+ key = "Write"
271+ var params tools.WriteParams
272+ json.Unmarshal([]byte(toolCall.Input), &params)
273+ value = params.FilePath
274+ default:
275+ key = toolCall.Name
276+ var params map[string]any
277+ json.Unmarshal([]byte(toolCall.Input), &params)
278+ jsonData, _ := json.Marshal(params)
279+ value = string(jsonData)
280+ }
281+
282+ style := styles.BaseStyle.
283+ Width(m.width).
284+ BorderLeft(true).
285+ BorderStyle(lipgloss.ThickBorder()).
286+ PaddingLeft(1).
287+ BorderForeground(styles.Yellow)
288+
289+ keyStyle := styles.BaseStyle.
290+ Foreground(styles.ForgroundDim)
291+ valyeStyle := styles.BaseStyle.
292+ Foreground(styles.Forground)
293+
294+ if isNested {
295+ valyeStyle = valyeStyle.Foreground(styles.ForgroundMid)
296+ }
297+ keyValye := keyStyle.Render(
298+ fmt.Sprintf("%s: ", key),
299+ )
300+ if !isNested {
301+ value = valyeStyle.
302+ Width(m.width - lipgloss.Width(keyValye) - 2).
303+ Render(
304+ ansi.Truncate(
305+ value,
306+ m.width-lipgloss.Width(keyValye)-2,
307+ "...",
308+ ),
309+ )
310+ } else {
311+ keyValye = keyStyle.Render(
312+ fmt.Sprintf(" └ %s: ", key),
313+ )
314+ value = valyeStyle.
315+ Width(m.width - lipgloss.Width(keyValye) - 2).
316+ Render(
317+ ansi.Truncate(
318+ value,
319+ m.width-lipgloss.Width(keyValye)-2,
320+ "...",
321+ ),
322+ )
323+ }
324+
325+ innerToolCalls := make([]string, 0)
326+ if toolCall.Name == agent.AgentToolName {
327+ messages, _ := m.app.Messages.List(toolCall.ID)
328+ toolCalls := make([]message.ToolCall, 0)
329+ for _, v := range messages {
330+ toolCalls = append(toolCalls, v.ToolCalls()...)
331+ }
332+ for _, v := range toolCalls {
333+ call := m.renderToolCall(v, true)
334+ innerToolCalls = append(innerToolCalls, call)
335+ }
336+ }
337+
338+ if isNested {
339+ return lipgloss.JoinHorizontal(
340+ lipgloss.Left,
341+ keyValye,
342+ value,
343+ )
344+ }
345+ callContent := lipgloss.JoinHorizontal(
346+ lipgloss.Left,
347+ keyValye,
348+ value,
349+ )
350+ callContent = strings.ReplaceAll(callContent, "\n", "")
351+ if len(innerToolCalls) > 0 {
352+ callContent = lipgloss.JoinVertical(
353+ lipgloss.Left,
354+ callContent,
355+ lipgloss.JoinVertical(
356+ lipgloss.Left,
357+ innerToolCalls...,
358+ ),
359+ )
360+ }
361+ return style.Render(callContent)
362+}
363+
364+func (m *messagesCmp) renderAssistantMessage(msg message.Message) []uiMessage {
365+ // find the user message that is before this assistant message
366+ var userMsg message.Message
367+ for i := len(m.messages) - 1; i >= 0; i-- {
368+ if m.messages[i].Role == message.User {
369+ userMsg = m.messages[i]
370+ break
371+ }
372+ }
373+ messages := make([]uiMessage, 0)
374+ if msg.Content().String() != "" {
375+ info := make([]string, 0)
376+ if msg.IsFinished() && msg.FinishReason() == "end_turn" {
377+ finish := msg.FinishPart()
378+ took := formatTimeDifference(userMsg.CreatedAt, finish.Time)
379+
380+ info = append(info, styles.BaseStyle.Width(m.width-1).Foreground(styles.ForgroundDim).Render(
381+ fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
382+ ))
383+ }
384+ content := m.renderSimpleMessage(msg, info...)
385+ messages = append(messages, uiMessage{
386+ messageType: assistantMessageType,
387+ position: 0, // gets updated in renderView
388+ height: lipgloss.Height(content),
389+ content: content,
390+ })
391+ }
392+ for _, v := range msg.ToolCalls() {
393+ content := m.renderToolCall(v, false)
394+ messages = append(messages,
395+ uiMessage{
396+ messageType: toolMessageType,
397+ position: 0, // gets updated in renderView
398+ height: lipgloss.Height(content),
399+ content: content,
400+ },
401+ )
402+ }
403+
404+ return messages
405+}
406+
195func (m *messagesCmp) renderView() {
407func (m *messagesCmp) renderView() {
196 m.uiMessages = make([]uiMessage, 0)
408 m.uiMessages = make([]uiMessage, 0)
197 pos := 0
409 pos := 0
198
410
199 for _, v := range m.messages {
411 for _, v := range m.messages {
200− content := ""
201 switch v.Role {
412 switch v.Role {
202 case message.User:
413 case message.User:
203− content = m.renderUserMessage(pos, v)
414+ content := m.renderSimpleMessage(v)
415+ m.uiMessages = append(m.uiMessages, uiMessage{
416+ messageType: userMessageType,
417+ position: pos,
418+ height: lipgloss.Height(content),
419+ content: content,
420+ })
421+ pos += lipgloss.Height(content) + 1 // + 1 for spacing
422+ case message.Assistant:
423+ assistantMessages := m.renderAssistantMessage(v)
424+ for _, msg := range assistantMessages {
425+ msg.position = pos
426+ m.uiMessages = append(m.uiMessages, msg)
427+ pos += msg.height + 1 // + 1 for spacing
428+ }
429+
204 }
430 }
205− m.uiMessages = append(m.uiMessages, uiMessage{
206− position: pos,
207− height: lipgloss.Height(content),
208− content: content,
209− })
210− pos += lipgloss.Height(content) + 1 // + 1 for spacing
211 }
431 }
212
432
213 messages := make([]string, 0)
433 messages := make([]string, 0)
214 for _, v := range m.uiMessages {
434 for _, v := range m.uiMessages {
215− messages = append(messages, v.content)
435+ messages = append(messages, v.content,
436+ styles.BaseStyle.
437+ Width(m.width).
438+ Render(
439+ "",
440+ ),
441+ )
216 }
442 }
217 m.viewport.SetContent(
443 m.viewport.SetContent(
218 styles.BaseStyle.
444 styles.BaseStyle.
@@ -246,7 +472,6 @@ func (m *messagesCmp) View() string {
246 )
472 )
247 }
473 }
248
474
249− m.renderView()
250 return styles.BaseStyle.
475 return styles.BaseStyle.
251 Width(m.width).
476 Width(m.width).
252 Render(
477 Render(
@@ -260,15 +485,21 @@ func (m *messagesCmp) View() string {
260
485
261func (m *messagesCmp) help() string {
486func (m *messagesCmp) help() string {
262 text := ""
487 text := ""
488+
489+ if m.agentWorking {
490+ text += styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render(
491+ fmt.Sprintf("%s %s ", m.spinner.View(), "Generating..."),
492+ )
493+ }
263 if m.writingMode {
494 if m.writingMode {
264− text = lipgloss.JoinHorizontal(
495+ text += lipgloss.JoinHorizontal(
265 lipgloss.Left,
496 lipgloss.Left,
266 styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
497 styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
267 styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
498 styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
268 styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
499 styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
269 )
500 )
270 } else {
501 } else {
271− text = lipgloss.JoinHorizontal(
502+ text += lipgloss.JoinHorizontal(
272 lipgloss.Left,
503 lipgloss.Left,
273 styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
504 styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
274 styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
505 styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
@@ -306,7 +537,15 @@ func (m *messagesCmp) SetSize(width, height int) {
306 glamour.WithWordWrap(width-1),
537 glamour.WithWordWrap(width-1),
307 )
538 )
308 m.focusRenderer = focusRenderer
539 m.focusRenderer = focusRenderer
540+ // clear the cached content
541+ for k := range m.cachedContent {
542+ delete(m.cachedContent, k)
543+ }
309 m.renderer = renderer
544 m.renderer = renderer
545+ if len(m.messages) > 0 {
546+ m.renderView()
547+ m.viewport.GotoBottom()
548+ }
310}
549}
311
550
312func (m *messagesCmp) GetSize() (int, int) {
551func (m *messagesCmp) GetSize() (int, int) {
@@ -320,7 +559,8 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
320 return util.ReportError(err)
559 return util.ReportError(err)
321 }
560 }
322 m.messages = messages
561 m.messages = messages
323− m.messages = append(m.messages, m.messages[0])
562+ m.currentMsgID = m.messages[len(m.messages)-1].ID
563+ m.needsRerender = true
324 return nil
564 return nil
325}
565}
326
566
@@ -333,6 +573,9 @@ func NewMessagesCmp(app *app.App) tea.Model {
333 glamour.WithStyles(styles.MarkdownTheme(false)),
573 glamour.WithStyles(styles.MarkdownTheme(false)),
334 glamour.WithWordWrap(80),
574 glamour.WithWordWrap(80),
335 )
575 )
576+
577+ s := spinner.New()
578+ s.Spinner = spinner.Pulse
336 return &messagesCmp{
579 return &messagesCmp{
337 app: app,
580 app: app,
338 writingMode: true,
581 writingMode: true,
@@ -340,5 +583,6 @@ func NewMessagesCmp(app *app.App) tea.Model {
340 viewport: viewport.New(0, 0),
583 viewport: viewport.New(0, 0),
341 focusRenderer: focusRenderer,
584 focusRenderer: focusRenderer,
342 renderer: renderer,
585 renderer: renderer,
586+ spinner: s,
343 }
587 }
344}
588}
internal/tui/components/chat/sidebar.go+10−1
@@ -5,6 +5,7 @@ import (
5
5
6 tea "github.com/charmbracelet/bubbletea"
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/charmbracelet/lipgloss"
7 "github.com/charmbracelet/lipgloss"
8+ "github.com/kujtimiihoxha/termai/internal/pubsub"
8 "github.com/kujtimiihoxha/termai/internal/session"
9 "github.com/kujtimiihoxha/termai/internal/session"
9 "github.com/kujtimiihoxha/termai/internal/tui/styles"
10 "github.com/kujtimiihoxha/termai/internal/tui/styles"
10)
11)
@@ -19,6 +20,14 @@ func (m *sidebarCmp) Init() tea.Cmd {
19}
20}
20
21
21func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
22func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
23+ switch msg := msg.(type) {
24+ case pubsub.Event[session.Session]:
25+ if msg.Type == pubsub.UpdatedEvent {
26+ if m.session.ID == msg.Payload.ID {
27+ m.session = msg.Payload
28+ }
29+ }
30+ }
22 return m, nil
31 return m, nil
23}
32}
24
33
@@ -45,7 +54,7 @@ func (m *sidebarCmp) sessionSection() string {
45 sessionValue := styles.BaseStyle.
54 sessionValue := styles.BaseStyle.
46 Foreground(styles.Forground).
55 Foreground(styles.Forground).
47 Width(m.width - lipgloss.Width(sessionKey)).
56 Width(m.width - lipgloss.Width(sessionKey)).
48− Render(": New Session")
57+ Render(fmt.Sprintf(": %s", m.session.Title))
49 return lipgloss.JoinHorizontal(
58 return lipgloss.JoinHorizontal(
50 lipgloss.Left,
59 lipgloss.Left,
51 sessionKey,
60 sessionKey,
internal/tui/page/chat.go+50−12
@@ -1,9 +1,10 @@
1package page
1package page
2
2
3import (
3import (
4+ "github.com/charmbracelet/bubbles/key"
4 tea "github.com/charmbracelet/bubbletea"
5 tea "github.com/charmbracelet/bubbletea"
5 "github.com/kujtimiihoxha/termai/internal/app"
6 "github.com/kujtimiihoxha/termai/internal/app"
6− "github.com/kujtimiihoxha/termai/internal/message"
7+ "github.com/kujtimiihoxha/termai/internal/llm/agent"
7 "github.com/kujtimiihoxha/termai/internal/session"
8 "github.com/kujtimiihoxha/termai/internal/session"
8 "github.com/kujtimiihoxha/termai/internal/tui/components/chat"
9 "github.com/kujtimiihoxha/termai/internal/tui/components/chat"
9 "github.com/kujtimiihoxha/termai/internal/tui/layout"
10 "github.com/kujtimiihoxha/termai/internal/tui/layout"
@@ -18,8 +19,32 @@ type chatPage struct {
18 session session.Session
19 session session.Session
19}
20}
20
21
22+type ChatKeyMap struct {
23+ NewSession key.Binding
24+}
25+
26+var keyMap = ChatKeyMap{
27+ NewSession: key.NewBinding(
28+ key.WithKeys("ctrl+n"),
29+ key.WithHelp("ctrl+n", "new session"),
30+ ),
31+}
32+
21func (p *chatPage) Init() tea.Cmd {
33func (p *chatPage) Init() tea.Cmd {
34+ // TODO: remove
35+ cmds := []tea.Cmd{
22− return p.layout.Init()
36+ p.layout.Init(),
37+ }
38+
39+ sessions, _ := p.app.Sessions.List()
40+ if len(sessions) > 0 {
41+ p.session = sessions[0]
42+ cmd := p.setSidebar()
43+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(p.session)), cmd)
44+ }
45+ return tea.Batch(
46+ cmds...,
47+ )
23}
48}
24
49
25func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
50func (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) {
31 if cmd != nil {
56 if cmd != nil {
32 return p, cmd
57 return p, cmd
33 }
58 }
59+ case tea.KeyMsg:
60+ switch {
61+ case key.Matches(msg, keyMap.NewSession):
62+ p.session = session.Session{}
63+ p.clearSidebar()
64+ return p, util.CmdHandler(chat.SessionClearedMsg{})
65+ }
34 }
66 }
35 u, cmd := p.layout.Update(msg)
67 u, cmd := p.layout.Update(msg)
36 p.layout = u.(layout.SplitPaneLayout)
68 p.layout = u.(layout.SplitPaneLayout)
@@ -51,6 +83,12 @@ func (p *chatPage) setSidebar() tea.Cmd {
51 return sidebarContainer.Init()
83 return sidebarContainer.Init()
52}
84}
53
85
86+func (p *chatPage) clearSidebar() {
87+ p.layout.SetRightPanel(nil)
88+ width, height := p.layout.GetSize()
89+ p.layout.SetSize(width, height)
90+}
91+
54func (p *chatPage) sendMessage(text string) tea.Cmd {
92func (p *chatPage) sendMessage(text string) tea.Cmd {
55 var cmds []tea.Cmd
93 var cmds []tea.Cmd
56 if p.session.ID == "" {
94 if p.session.ID == "" {
@@ -66,15 +104,15 @@ func (p *chatPage) sendMessage(text string) tea.Cmd {
66 }
104 }
67 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
105 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
68 }
106 }
69− // TODO: actually call agent
107+ // TODO: move this to a service
70− p.app.Messages.Create(p.session.ID, message.CreateMessageParams{
108+ a, err := agent.NewCoderAgent(p.app)
71− Role: message.User,
109+ if err != nil {
72− Parts: []message.ContentPart{
110+ return util.ReportError(err)
73− message.TextContent{
111+ }
74− Text: text,
112+ go func() {
75− },
113+ a.Generate(p.app.Context, p.session.ID, text)
76− },
77− })
114+ }()
115+
78 return tea.Batch(cmds...)
116 return tea.Batch(cmds...)
79}
117}
80
118
@@ -85,7 +123,7 @@ func (p *chatPage) View() string {
85func NewChatPage(app *app.App) tea.Model {
123func NewChatPage(app *app.App) tea.Model {
86 messagesContainer := layout.NewContainer(
124 messagesContainer := layout.NewContainer(
87 chat.NewMessagesCmp(app),
125 chat.NewMessagesCmp(app),
88− layout.WithPadding(1, 1, 1, 1),
126+ layout.WithPadding(1, 1, 0, 1),
89 )
127 )
90
128
91 editorContainer := layout.NewContainer(
129 editorContainer := layout.NewContainer(
internal/tui/styles/background.gonew file+81
@@ -0,0 +1,81 @@
1+package styles
2+
3+import (
4+ "fmt"
5+ "regexp"
6+ "strconv"
7+ "strings"
8+
9+ "github.com/charmbracelet/lipgloss"
10+)
11+
12+var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
13+
14+func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) {
15+ r, g, b, a := c.RGBA()
16+
17+ // Un-premultiply alpha if needed
18+ if a > 0 && a < 0xffff {
19+ r = (r * 0xffff) / a
20+ g = (g * 0xffff) / a
21+ b = (b * 0xffff) / a
22+ }
23+
24+ // Convert from 16-bit to 8-bit color
25+ return uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)
26+}
27+
28+func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string {
29+ r, g, b := getColorRGB(newBgColor)
30+
31+ newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
32+
33+ return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
34+ // Extract content between "\x1b[" and "m"
35+ content := seq[2 : len(seq)-1]
36+ tokens := strings.Split(content, ";")
37+ var newTokens []string
38+
39+ // Skip background color tokens
40+ for i := 0; i < len(tokens); i++ {
41+ if tokens[i] == "" {
42+ continue
43+ }
44+
45+ val, err := strconv.Atoi(tokens[i])
46+ if err != nil {
47+ newTokens = append(newTokens, tokens[i])
48+ continue
49+ }
50+
51+ // Skip background color tokens
52+ if val == 48 {
53+ // Skip "48;5;N" or "48;2;R;G;B" sequences
54+ if i+1 < len(tokens) {
55+ if nextVal, err := strconv.Atoi(tokens[i+1]); err == nil {
56+ switch nextVal {
57+ case 5:
58+ i += 2 // Skip "5" and color index
59+ case 2:
60+ i += 4 // Skip "2" and RGB components
61+ }
62+ }
63+ }
64+ } else if (val < 40 || val > 47) && (val < 100 || val > 107) && val != 49 {
65+ // Keep non-background tokens
66+ newTokens = append(newTokens, tokens[i])
67+ }
68+ }
69+
70+ // Add new background if provided
71+ if newBg != "" {
72+ newTokens = append(newTokens, strings.Split(newBg, ";")...)
73+ }
74+
75+ if len(newTokens) == 0 {
76+ return ""
77+ }
78+
79+ return "\x1b[" + strings.Join(newTokens, ";") + "m"
80+ })
81+}
internal/tui/styles/markdown.go+4−3
@@ -515,6 +515,7 @@ var ASCIIStyleConfig = ansi.StyleConfig{
515 Document: ansi.StyleBlock{
515 Document: ansi.StyleBlock{
516 StylePrimitive: ansi.StylePrimitive{
516 StylePrimitive: ansi.StylePrimitive{
517 BackgroundColor: stringPtr(Background.Dark),
517 BackgroundColor: stringPtr(Background.Dark),
518+ Color: stringPtr(ForgroundDim.Dark),
518 },
519 },
519 Indent: uintPtr(1),
520 Indent: uintPtr(1),
520 IndentToken: stringPtr(BaseStyle.Render(" ")),
521 IndentToken: stringPtr(BaseStyle.Render(" ")),
@@ -688,7 +689,7 @@ var DraculaStyleConfig = ansi.StyleConfig{
688 Heading: ansi.StyleBlock{
689 Heading: ansi.StyleBlock{
689 StylePrimitive: ansi.StylePrimitive{
690 StylePrimitive: ansi.StylePrimitive{
690 BlockSuffix: "\n",
691 BlockSuffix: "\n",
691− Color: stringPtr("#bd93f9"),
692+ Color: stringPtr(PrimaryColor.Dark),
692 Bold: boolPtr(true),
693 Bold: boolPtr(true),
693 BackgroundColor: stringPtr(Background.Dark),
694 BackgroundColor: stringPtr(Background.Dark),
694 },
695 },
@@ -740,7 +741,7 @@ var DraculaStyleConfig = ansi.StyleConfig{
740 },
741 },
741 Strong: ansi.StylePrimitive{
742 Strong: ansi.StylePrimitive{
742 Bold: boolPtr(true),
743 Bold: boolPtr(true),
743− Color: stringPtr("#ffb86c"),
744+ Color: stringPtr(Blue.Dark),
744 BackgroundColor: stringPtr(Background.Dark),
745 BackgroundColor: stringPtr(Background.Dark),
745 },
746 },
746 HorizontalRule: ansi.StylePrimitive{
747 HorizontalRule: ansi.StylePrimitive{
@@ -796,7 +797,7 @@ var DraculaStyleConfig = ansi.StyleConfig{
796 CodeBlock: ansi.StyleCodeBlock{
797 CodeBlock: ansi.StyleCodeBlock{
797 StyleBlock: ansi.StyleBlock{
798 StyleBlock: ansi.StyleBlock{
798 StylePrimitive: ansi.StylePrimitive{
799 StylePrimitive: ansi.StylePrimitive{
799− Color: stringPtr("#ffb86c"),
800+ Color: stringPtr(Blue.Dark),
800 BackgroundColor: stringPtr(Background.Dark),
801 BackgroundColor: stringPtr(Background.Dark),
801 },
802 },
802 Margin: uintPtr(defaultMargin),
803 Margin: uintPtr(defaultMargin),
internal/tui/styles/styles.go+10
@@ -34,6 +34,11 @@ var (
34 Light: "#d3d3d3",
34 Light: "#d3d3d3",
35 }
35 }
36
36
37+ ForgroundMid = lipgloss.AdaptiveColor{
38+ Dark: "#a0a0a0",
39+ Light: "#a0a0a0",
40+ }
41+
37 ForgroundDim = lipgloss.AdaptiveColor{
42 ForgroundDim = lipgloss.AdaptiveColor{
38 Dark: "#737373",
43 Dark: "#737373",
39 Light: "#737373",
44 Light: "#737373",
@@ -159,6 +164,11 @@ var (
159 Light: light.Peach().Hex,
164 Light: light.Peach().Hex,
160 }
165 }
161
166
167+ Yellow = lipgloss.AdaptiveColor{
168+ Dark: dark.Yellow().Hex,
169+ Light: light.Yellow().Hex,
170+ }
171+
162 Primary = Blue
172 Primary = Blue
163 Secondary = Mauve
173 Secondary = Mauve
164
174