fix: TUI spacebar + improved design
Switch to bubbles textinput for proper keyboard handling (space, cursor, backspace, clipboard all work correctly). Improved design: - ❯ user prompt, ◆ assistant prefix, ✗ error prefix - Word wrapping for long responses - Separator line between chat and input - Streaming indicator (● streaming) in status bar - Better color scheme (lighter purples/blues) - Welcome message with usage hints
This commit is contained in:
9
go.mod
9
go.mod
@@ -3,21 +3,23 @@ module somegit.dev/Owlibou/gnoma
|
|||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
charm.land/bubbles/v2 v2.1.0
|
||||||
|
charm.land/bubbletea/v2 v2.0.2
|
||||||
|
charm.land/lipgloss/v2 v2.0.2
|
||||||
github.com/BurntSushi/toml v0.3.1
|
github.com/BurntSushi/toml v0.3.1
|
||||||
github.com/VikingOwl91/mistral-go-sdk v1.2.1
|
github.com/VikingOwl91/mistral-go-sdk v1.2.1
|
||||||
github.com/anthropics/anthropic-sdk-go v1.29.0
|
github.com/anthropics/anthropic-sdk-go v1.29.0
|
||||||
github.com/openai/openai-go v1.12.0
|
github.com/openai/openai-go v1.12.0
|
||||||
golang.org/x/text v0.27.0
|
golang.org/x/text v0.27.0
|
||||||
google.golang.org/genai v1.52.1
|
google.golang.org/genai v1.52.1
|
||||||
|
mvdan.cc/sh/v3 v3.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.1.0 // indirect
|
|
||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
|
||||||
charm.land/lipgloss/v2 v2.0.2 // indirect
|
|
||||||
cloud.google.com/go v0.116.0 // indirect
|
cloud.google.com/go v0.116.0 // indirect
|
||||||
cloud.google.com/go/auth v0.9.3 // indirect
|
cloud.google.com/go/auth v0.9.3 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
@@ -48,5 +50,4 @@ require (
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
google.golang.org/grpc v1.66.2 // indirect
|
google.golang.org/grpc v1.66.2 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
mvdan.cc/sh/v3 v3.13.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
24
go.sum
24
go.sum
@@ -17,6 +17,10 @@ github.com/VikingOwl91/mistral-go-sdk v1.2.1 h1:6OQMtOzJUFcvFUEtbX9VlglUPBn+dKOr
|
|||||||
github.com/VikingOwl91/mistral-go-sdk v1.2.1/go.mod h1:f4emNtHUx2zSqY3V0LBz6lNI1jE6q/zh+SEU+/hJ0i4=
|
github.com/VikingOwl91/mistral-go-sdk v1.2.1/go.mod h1:f4emNtHUx2zSqY3V0LBz6lNI1jE6q/zh+SEU+/hJ0i4=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.29.0 h1:7h1ZyRflhtxyuFkdwkVuJ1LdFAYdmizvgg0gd1uvOfI=
|
github.com/anthropics/anthropic-sdk-go v1.29.0 h1:7h1ZyRflhtxyuFkdwkVuJ1LdFAYdmizvgg0gd1uvOfI=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.29.0/go.mod h1:dSIO7kSrOI7MA4fE6RRVaw8tyWP7HNQU5/H/KS4cax8=
|
github.com/anthropics/anthropic-sdk-go v1.29.0/go.mod h1:dSIO7kSrOI7MA4fE6RRVaw8tyWP7HNQU5/H/KS4cax8=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||||
@@ -24,6 +28,8 @@ github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRb
|
|||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
@@ -45,6 +51,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
@@ -65,8 +73,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||||
@@ -76,6 +82,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT
|
|||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||||
@@ -89,6 +99,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -116,6 +128,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
@@ -131,18 +145,12 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
|
|||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
@@ -5,19 +5,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/session"
|
"somegit.dev/Owlibou/gnoma/internal/session"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
// streamEventMsg wraps a stream event for the Bubble Tea message system.
|
type streamEventMsg struct{ event stream.Event }
|
||||||
type streamEventMsg struct {
|
type turnDoneMsg struct{ err error }
|
||||||
event stream.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
// turnDoneMsg signals that a turn is complete.
|
type chatMessage struct {
|
||||||
type turnDoneMsg struct {
|
role string // "user", "assistant", "tool", "error"
|
||||||
err error
|
content string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model is the Bubble Tea application model.
|
// Model is the Bubble Tea application model.
|
||||||
@@ -26,48 +25,62 @@ type Model struct {
|
|||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
|
||||||
// Chat history
|
messages []chatMessage
|
||||||
messages []chatMessage
|
streaming bool
|
||||||
// Current streaming response
|
streamBuf strings.Builder
|
||||||
streaming bool
|
currentRole string
|
||||||
streamBuf strings.Builder
|
|
||||||
currentRole string
|
|
||||||
|
|
||||||
// Input
|
input textinput.Model
|
||||||
input string
|
|
||||||
inputCursor int
|
|
||||||
|
|
||||||
// Status
|
|
||||||
ready bool
|
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
type chatMessage struct {
|
|
||||||
role string // "user", "assistant", "tool", "error"
|
|
||||||
content string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new TUI model.
|
|
||||||
func New(sess session.Session) Model {
|
func New(sess session.Session) Model {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = "Type a message... (Enter to send, Ctrl+C to quit)"
|
||||||
|
ti.Prompt = "❯ "
|
||||||
|
ti.Focus()
|
||||||
|
ti.SetWidth(80)
|
||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
session: sess,
|
session: sess,
|
||||||
ready: true,
|
input: ti,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return nil
|
return m.input.Focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
m.input.SetWidth(m.width - 6)
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
return m.handleKey(msg)
|
switch msg.String() {
|
||||||
|
case "ctrl+c":
|
||||||
|
if m.streaming {
|
||||||
|
m.session.Cancel()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "enter":
|
||||||
|
if m.streaming {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
input := strings.TrimSpace(m.input.Value())
|
||||||
|
if input == "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.input.SetValue("")
|
||||||
|
return m.submitInput(input)
|
||||||
|
}
|
||||||
|
|
||||||
case streamEventMsg:
|
case streamEventMsg:
|
||||||
return m.handleStreamEvent(msg.event)
|
return m.handleStreamEvent(msg.event)
|
||||||
@@ -76,91 +89,62 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.streaming = false
|
m.streaming = false
|
||||||
if m.streamBuf.Len() > 0 {
|
if m.streamBuf.Len() > 0 {
|
||||||
m.messages = append(m.messages, chatMessage{
|
m.messages = append(m.messages, chatMessage{
|
||||||
role: m.currentRole,
|
role: m.currentRole, content: m.streamBuf.String(),
|
||||||
content: m.streamBuf.String(),
|
|
||||||
})
|
})
|
||||||
m.streamBuf.Reset()
|
m.streamBuf.Reset()
|
||||||
}
|
}
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
m.messages = append(m.messages, chatMessage{
|
m.messages = append(m.messages, chatMessage{
|
||||||
role: "error",
|
role: "error", content: msg.err.Error(),
|
||||||
content: msg.err.Error(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
// Forward to textinput
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.input, cmd = m.input.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m Model) submitInput(input string) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
// Slash commands
|
||||||
case "ctrl+c":
|
|
||||||
if m.streaming {
|
|
||||||
m.session.Cancel()
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m, tea.Quit
|
|
||||||
|
|
||||||
case "enter":
|
|
||||||
if m.streaming || strings.TrimSpace(m.input) == "" {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m.submitInput()
|
|
||||||
|
|
||||||
case "backspace":
|
|
||||||
if len(m.input) > 0 {
|
|
||||||
m.input = m.input[:len(m.input)-1]
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Type characters
|
|
||||||
if len(msg.String()) == 1 || msg.String() == " " {
|
|
||||||
m.input += msg.String()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) submitInput() (tea.Model, tea.Cmd) {
|
|
||||||
input := strings.TrimSpace(m.input)
|
|
||||||
m.input = ""
|
|
||||||
|
|
||||||
// Handle slash commands
|
|
||||||
if strings.HasPrefix(input, "/") {
|
if strings.HasPrefix(input, "/") {
|
||||||
return m.handleCommand(input)
|
return m.handleCommand(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user message to chat
|
|
||||||
m.messages = append(m.messages, chatMessage{role: "user", content: input})
|
m.messages = append(m.messages, chatMessage{role: "user", content: input})
|
||||||
m.streaming = true
|
m.streaming = true
|
||||||
m.currentRole = "assistant"
|
m.currentRole = "assistant"
|
||||||
m.streamBuf.Reset()
|
m.streamBuf.Reset()
|
||||||
|
|
||||||
// Send to session
|
|
||||||
if err := m.session.Send(input); err != nil {
|
if err := m.session.Send(input); err != nil {
|
||||||
m.messages = append(m.messages, chatMessage{role: "error", content: err.Error()})
|
m.messages = append(m.messages, chatMessage{role: "error", content: err.Error()})
|
||||||
m.streaming = false
|
m.streaming = false
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start listening for events
|
|
||||||
return m, m.listenForEvents()
|
return m, m.listenForEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) handleCommand(cmd string) (tea.Model, tea.Cmd) {
|
func (m Model) handleCommand(cmd string) (tea.Model, tea.Cmd) {
|
||||||
switch {
|
switch {
|
||||||
case cmd == "/quit" || cmd == "/exit":
|
case cmd == "/quit" || cmd == "/exit" || cmd == "/q":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case cmd == "/clear":
|
case cmd == "/clear":
|
||||||
m.messages = nil
|
m.messages = nil
|
||||||
return m, nil
|
return m, nil
|
||||||
case cmd == "/incognito":
|
case cmd == "/incognito":
|
||||||
m.messages = append(m.messages, chatMessage{role: "tool", content: "incognito toggle (not yet wired)"})
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: "tool", content: " incognito mode toggled (wiring pending)",
|
||||||
|
})
|
||||||
return m, nil
|
return m, nil
|
||||||
default:
|
default:
|
||||||
m.messages = append(m.messages, chatMessage{role: "error", content: fmt.Sprintf("unknown command: %s", cmd)})
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: "error", content: fmt.Sprintf("unknown command: %s", cmd),
|
||||||
|
})
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,18 +156,17 @@ func (m Model) handleStreamEvent(evt stream.Event) (tea.Model, tea.Cmd) {
|
|||||||
m.streamBuf.WriteString(evt.Text)
|
m.streamBuf.WriteString(evt.Text)
|
||||||
}
|
}
|
||||||
case stream.EventThinkingDelta:
|
case stream.EventThinkingDelta:
|
||||||
// Show thinking in dimmed text
|
|
||||||
m.streamBuf.WriteString(evt.Text)
|
m.streamBuf.WriteString(evt.Text)
|
||||||
case stream.EventToolCallStart:
|
case stream.EventToolCallStart:
|
||||||
// Flush current streaming text
|
|
||||||
if m.streamBuf.Len() > 0 {
|
if m.streamBuf.Len() > 0 {
|
||||||
m.messages = append(m.messages, chatMessage{role: m.currentRole, content: m.streamBuf.String()})
|
m.messages = append(m.messages, chatMessage{
|
||||||
|
role: m.currentRole, content: m.streamBuf.String(),
|
||||||
|
})
|
||||||
m.streamBuf.Reset()
|
m.streamBuf.Reset()
|
||||||
}
|
}
|
||||||
case stream.EventToolCallDone:
|
case stream.EventToolCallDone:
|
||||||
m.messages = append(m.messages, chatMessage{
|
m.messages = append(m.messages, chatMessage{
|
||||||
role: "tool",
|
role: "tool", content: fmt.Sprintf(" [%s] executing...", evt.ToolCallName),
|
||||||
content: fmt.Sprintf("[%s] calling...", evt.ToolCallName),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return m, m.listenForEvents()
|
return m, m.listenForEvents()
|
||||||
@@ -194,7 +177,6 @@ func (m Model) listenForEvents() tea.Cmd {
|
|||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
evt, ok := <-ch
|
evt, ok := <-ch
|
||||||
if !ok {
|
if !ok {
|
||||||
// Channel closed — turn is done
|
|
||||||
_, err := m.session.TurnResult()
|
_, err := m.session.TurnResult()
|
||||||
return turnDoneMsg{err: err}
|
return turnDoneMsg{err: err}
|
||||||
}
|
}
|
||||||
@@ -204,19 +186,25 @@ func (m Model) listenForEvents() tea.Cmd {
|
|||||||
|
|
||||||
func (m Model) View() tea.View {
|
func (m Model) View() tea.View {
|
||||||
if m.width == 0 {
|
if m.width == 0 {
|
||||||
return tea.NewView("loading...")
|
return tea.NewView("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout: chat area + input + status bar
|
statusH := 1
|
||||||
statusHeight := 1
|
inputH := 1
|
||||||
inputHeight := 3
|
separatorH := 1
|
||||||
chatHeight := m.height - statusHeight - inputHeight
|
chatH := m.height - statusH - inputH - separatorH - 1
|
||||||
|
|
||||||
chat := m.renderChat(chatHeight)
|
chat := m.renderChat(chatH)
|
||||||
|
separator := styleSeperator.Width(m.width).Render(strings.Repeat("─", m.width))
|
||||||
input := m.renderInput()
|
input := m.renderInput()
|
||||||
status := m.renderStatus()
|
status := m.renderStatus()
|
||||||
|
|
||||||
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, chat, input, status))
|
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left,
|
||||||
|
chat,
|
||||||
|
separator,
|
||||||
|
input,
|
||||||
|
status,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderChat(height int) string {
|
func (m Model) renderChat(height int) string {
|
||||||
@@ -225,68 +213,119 @@ func (m Model) renderChat(height int) string {
|
|||||||
for _, msg := range m.messages {
|
for _, msg := range m.messages {
|
||||||
switch msg.role {
|
switch msg.role {
|
||||||
case "user":
|
case "user":
|
||||||
lines = append(lines, styleUserLabel.Render("you: ")+msg.content)
|
lines = append(lines, styleUserLabel.Render(" ❯ ")+styleUserText.Render(msg.content))
|
||||||
case "assistant":
|
case "assistant":
|
||||||
lines = append(lines, styleAssistantLabel.Render("gnoma: ")+msg.content)
|
wrapped := wrapText(msg.content, m.width-6)
|
||||||
|
for i, line := range strings.Split(wrapped, "\n") {
|
||||||
|
if i == 0 {
|
||||||
|
lines = append(lines, styleAssistantLabel.Render(" ◆ ")+line)
|
||||||
|
} else {
|
||||||
|
lines = append(lines, " "+line)
|
||||||
|
}
|
||||||
|
}
|
||||||
case "tool":
|
case "tool":
|
||||||
lines = append(lines, styleToolOutput.Render(" "+msg.content))
|
lines = append(lines, styleToolOutput.Render(msg.content))
|
||||||
case "error":
|
case "error":
|
||||||
lines = append(lines, styleError.Render("error: "+msg.content))
|
lines = append(lines, styleError.Render(" ✗ "+msg.content))
|
||||||
}
|
}
|
||||||
|
lines = append(lines, "") // blank line between messages
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show streaming buffer
|
// Streaming buffer
|
||||||
if m.streaming && m.streamBuf.Len() > 0 {
|
if m.streaming && m.streamBuf.Len() > 0 {
|
||||||
lines = append(lines, styleAssistantLabel.Render("gnoma: ")+m.streamBuf.String()+"▊")
|
wrapped := wrapText(m.streamBuf.String(), m.width-6)
|
||||||
|
first := true
|
||||||
|
for _, line := range strings.Split(wrapped, "\n") {
|
||||||
|
if first {
|
||||||
|
lines = append(lines, styleAssistantLabel.Render(" ◆ ")+line)
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
lines = append(lines, " "+line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines = append(lines, styleCursor.Render(" ▊"))
|
||||||
} else if m.streaming {
|
} else if m.streaming {
|
||||||
lines = append(lines, styleAssistantLabel.Render("gnoma: ")+"▊")
|
lines = append(lines, styleAssistantLabel.Render(" ◆ ")+styleCursor.Render("▊"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty state
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
lines = append(lines, styleHint.Render(" Type a message and press Enter. /quit to exit."))
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, styleHint.Render(" gnoma — provider-agnostic coding assistant"))
|
||||||
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, styleHint.Render(" Type a message and press Enter."))
|
||||||
|
lines = append(lines, styleHint.Render(" /quit to exit, /clear to reset, Ctrl+C to cancel."))
|
||||||
}
|
}
|
||||||
|
|
||||||
content := strings.Join(lines, "\n")
|
// Scroll to bottom
|
||||||
|
allLines := strings.Split(strings.Join(lines, "\n"), "\n")
|
||||||
// Scroll to bottom — show last N lines
|
if len(allLines) > height {
|
||||||
contentLines := strings.Split(content, "\n")
|
allLines = allLines[len(allLines)-height:]
|
||||||
if len(contentLines) > height {
|
|
||||||
contentLines = contentLines[len(contentLines)-height:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Width(m.width).
|
Width(m.width).
|
||||||
Height(height).
|
Height(height).
|
||||||
Render(strings.Join(contentLines, "\n"))
|
Render(strings.Join(allLines, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderInput() string {
|
func (m Model) renderInput() string {
|
||||||
prompt := "❯ "
|
return " " + m.input.View()
|
||||||
cursor := ""
|
|
||||||
if !m.streaming {
|
|
||||||
cursor = "▏"
|
|
||||||
}
|
|
||||||
content := prompt + m.input + cursor
|
|
||||||
|
|
||||||
return styleInputBorder.
|
|
||||||
Width(m.width - 4).
|
|
||||||
Render(content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderStatus() string {
|
func (m Model) renderStatus() string {
|
||||||
status := m.session.Status()
|
status := m.session.Status()
|
||||||
|
|
||||||
parts := []string{
|
left := styleStatusProvider.Render(
|
||||||
styleStatusProvider.Render(fmt.Sprintf(" %s/%s", status.Provider, status.Model)),
|
fmt.Sprintf(" %s/%s", status.Provider, status.Model),
|
||||||
fmt.Sprintf("tokens: %d", status.TokensUsed),
|
)
|
||||||
fmt.Sprintf("turns: %d", status.TurnCount),
|
|
||||||
|
right := fmt.Sprintf("tokens: %d │ turns: %d ", status.TokensUsed, status.TurnCount)
|
||||||
|
|
||||||
|
if m.streaming {
|
||||||
|
right = styleStatusStreaming.Render("● streaming ") + "│ " + right
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.State == session.StateStreaming {
|
// Pad middle
|
||||||
parts = append(parts, "streaming...")
|
gap := m.width - lipgloss.Width(left) - lipgloss.Width(right)
|
||||||
|
if gap < 0 {
|
||||||
|
gap = 0
|
||||||
}
|
}
|
||||||
|
middle := strings.Repeat(" ", gap)
|
||||||
|
|
||||||
return styleStatusBar.
|
return styleStatusBar.Width(m.width).Render(left + middle + right)
|
||||||
Width(m.width).
|
}
|
||||||
Render(strings.Join(parts, " │ "))
|
|
||||||
|
func wrapText(text string, width int) string {
|
||||||
|
if width <= 0 {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
var result strings.Builder
|
||||||
|
for _, line := range strings.Split(text, "\n") {
|
||||||
|
if len(line) <= width {
|
||||||
|
if result.Len() > 0 {
|
||||||
|
result.WriteByte('\n')
|
||||||
|
}
|
||||||
|
result.WriteString(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Simple word wrap
|
||||||
|
words := strings.Fields(line)
|
||||||
|
lineLen := 0
|
||||||
|
for _, word := range words {
|
||||||
|
if lineLen+len(word)+1 > width && lineLen > 0 {
|
||||||
|
result.WriteByte('\n')
|
||||||
|
lineLen = 0
|
||||||
|
} else if lineLen > 0 {
|
||||||
|
result.WriteByte(' ')
|
||||||
|
lineLen++
|
||||||
|
}
|
||||||
|
result.WriteString(word)
|
||||||
|
lineLen += len(word)
|
||||||
|
}
|
||||||
|
if result.Len() > 0 && !strings.HasSuffix(result.String(), "\n") {
|
||||||
|
// don't add extra newline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,50 +4,55 @@ import "charm.land/lipgloss/v2"
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// Colors
|
// Colors
|
||||||
colorPrimary = lipgloss.Color("#7C3AED") // purple — gnoma brand
|
colorPrimary = lipgloss.Color("#A78BFA") // light purple
|
||||||
colorSecondary = lipgloss.Color("#10B981") // green
|
colorUser = lipgloss.Color("#60A5FA") // light blue
|
||||||
colorMuted = lipgloss.Color("#6B7280") // gray
|
colorAssistant = lipgloss.Color("#A78BFA") // light purple
|
||||||
colorError = lipgloss.Color("#EF4444") // red
|
colorTool = lipgloss.Color("#34D399") // green
|
||||||
colorWarning = lipgloss.Color("#F59E0B") // amber
|
colorError = lipgloss.Color("#F87171") // red
|
||||||
colorUser = lipgloss.Color("#3B82F6") // blue
|
colorMuted = lipgloss.Color("#6B7280") // gray
|
||||||
colorAssistant = lipgloss.Color("#7C3AED") // purple
|
colorStreaming = lipgloss.Color("#FBBF24") // amber
|
||||||
colorTool = lipgloss.Color("#10B981") // green
|
colorStatusBg = lipgloss.Color("#1E1E2E") // dark bg
|
||||||
colorIncognito = lipgloss.Color("#F59E0B") // amber
|
|
||||||
|
|
||||||
// Styles
|
// Chat styles
|
||||||
styleUserLabel = lipgloss.NewStyle().
|
styleUserLabel = lipgloss.NewStyle().
|
||||||
Foreground(colorUser).
|
Foreground(colorUser).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
|
styleUserText = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#E5E7EB"))
|
||||||
|
|
||||||
styleAssistantLabel = lipgloss.NewStyle().
|
styleAssistantLabel = lipgloss.NewStyle().
|
||||||
Foreground(colorAssistant).
|
Foreground(colorAssistant).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
styleToolOutput = lipgloss.NewStyle().
|
styleToolOutput = lipgloss.NewStyle().
|
||||||
Foreground(colorTool)
|
Foreground(colorTool).
|
||||||
|
Italic(true)
|
||||||
styleStatusBar = lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#1F2937")).
|
|
||||||
Foreground(lipgloss.Color("#D1D5DB")).
|
|
||||||
Padding(0, 1)
|
|
||||||
|
|
||||||
styleStatusProvider = lipgloss.NewStyle().
|
|
||||||
Foreground(colorPrimary).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
styleStatusIncognito = lipgloss.NewStyle().
|
|
||||||
Foreground(colorIncognito).
|
|
||||||
Bold(true)
|
|
||||||
|
|
||||||
styleError = lipgloss.NewStyle().
|
styleError = lipgloss.NewStyle().
|
||||||
Foreground(colorError)
|
Foreground(colorError)
|
||||||
|
|
||||||
styleHint = lipgloss.NewStyle().
|
styleHint = lipgloss.NewStyle().
|
||||||
Foreground(colorMuted).
|
Foreground(colorMuted)
|
||||||
Italic(true)
|
|
||||||
|
|
||||||
styleInputBorder = lipgloss.NewStyle().
|
styleCursor = lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Foreground(colorStreaming)
|
||||||
BorderForeground(colorPrimary).
|
|
||||||
Padding(0, 1)
|
styleSeperator = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#374151"))
|
||||||
|
|
||||||
|
// Status bar
|
||||||
|
styleStatusBar = lipgloss.NewStyle().
|
||||||
|
Background(colorStatusBg).
|
||||||
|
Foreground(lipgloss.Color("#9CA3AF"))
|
||||||
|
|
||||||
|
styleStatusProvider = lipgloss.NewStyle().
|
||||||
|
Background(colorStatusBg).
|
||||||
|
Foreground(colorPrimary).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
styleStatusStreaming = lipgloss.NewStyle().
|
||||||
|
Background(colorStatusBg).
|
||||||
|
Foreground(colorStreaming).
|
||||||
|
Bold(true)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user