feat: upgrade charmbracelet libs to v2 (bubbletea, lipgloss, bubbles)

Migrate from github.com/charmbracelet/* v1 to charm.land/* v2 vanity imports.

Key changes:
- bubbletea: View() returns tea.View, KeyMsg -> KeyPressMsg, msg.String() matching
- lipgloss: AdaptiveColor replaced with cached dark-bg detection helper
- bubbles/textarea: Styles()/SetStyles() pattern, KeyMap.InsertNewline override
- bubbles/progress: SetWidth(), WithDefaultBlend(), typed Update return
- Input: enter always submits, ctrl+j/alt+enter insert newlines
- User message newlines preserved through glamour via \n -> \n\n conversion
- glamour stays at v1 (no v2 exists)
This commit is contained in:
Ed Zynda
2026-02-25 17:07:09 +03:00
parent d24d49854f
commit ce32cea7ee
16 changed files with 248 additions and 262 deletions
+16 -17
View File
@@ -10,7 +10,6 @@ import (
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/cloudwego/eino/schema"
"github.com/mark3labs/mcphost/internal/agent"
"github.com/mark3labs/mcphost/internal/config"
@@ -251,22 +250,22 @@ func LoadConfigWithEnvSubstitution(configPath string) error {
func configToUiTheme(theme config.Theme) ui.Theme {
return ui.Theme{
Primary: lipgloss.AdaptiveColor(theme.Primary),
Secondary: lipgloss.AdaptiveColor(theme.Secondary),
Success: lipgloss.AdaptiveColor(theme.Success),
Warning: lipgloss.AdaptiveColor(theme.Warning),
Error: lipgloss.AdaptiveColor(theme.Error),
Info: lipgloss.AdaptiveColor(theme.Info),
Text: lipgloss.AdaptiveColor(theme.Text),
Muted: lipgloss.AdaptiveColor(theme.Muted),
VeryMuted: lipgloss.AdaptiveColor(theme.VeryMuted),
Background: lipgloss.AdaptiveColor(theme.Background),
Border: lipgloss.AdaptiveColor(theme.Border),
MutedBorder: lipgloss.AdaptiveColor(theme.MutedBorder),
System: lipgloss.AdaptiveColor(theme.System),
Tool: lipgloss.AdaptiveColor(theme.Tool),
Accent: lipgloss.AdaptiveColor(theme.Accent),
Highlight: lipgloss.AdaptiveColor(theme.Highlight),
Primary: ui.AdaptiveColor(theme.Primary.Light, theme.Primary.Dark),
Secondary: ui.AdaptiveColor(theme.Secondary.Light, theme.Secondary.Dark),
Success: ui.AdaptiveColor(theme.Success.Light, theme.Success.Dark),
Warning: ui.AdaptiveColor(theme.Warning.Light, theme.Warning.Dark),
Error: ui.AdaptiveColor(theme.Error.Light, theme.Error.Dark),
Info: ui.AdaptiveColor(theme.Info.Light, theme.Info.Dark),
Text: ui.AdaptiveColor(theme.Text.Light, theme.Text.Dark),
Muted: ui.AdaptiveColor(theme.Muted.Light, theme.Muted.Dark),
VeryMuted: ui.AdaptiveColor(theme.VeryMuted.Light, theme.VeryMuted.Dark),
Background: ui.AdaptiveColor(theme.Background.Light, theme.Background.Dark),
Border: ui.AdaptiveColor(theme.Border.Light, theme.Border.Dark),
MutedBorder: ui.AdaptiveColor(theme.MutedBorder.Light, theme.MutedBorder.Dark),
System: ui.AdaptiveColor(theme.System.Light, theme.System.Dark),
Tool: ui.AdaptiveColor(theme.Tool.Light, theme.Tool.Dark),
Accent: ui.AdaptiveColor(theme.Accent.Light, theme.Accent.Dark),
Highlight: ui.AdaptiveColor(theme.Highlight.Light, theme.Highlight.Dark),
}
}
+19 -17
View File
@@ -1,15 +1,17 @@
module github.com/mark3labs/mcphost
go 1.24.0
go 1.24.2
toolchain go1.24.5
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.0
github.com/JohannesKaufmann/html-to-markdown v1.6.0
github.com/PuerkitoBio/goquery v1.10.3
github.com/bytedance/sonic v1.15.0
github.com/charmbracelet/fang v0.4.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/fang v0.4.4
github.com/cloudwego/eino v0.7.13
github.com/cloudwego/eino-ext/components/model/claude v0.1.12
github.com/cloudwego/eino-ext/components/model/ollama v0.1.8
@@ -54,13 +56,18 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250902204034-1cdc10c66d5b // indirect
github.com/charmbracelet/x/exp/color v0.0.0-20250902204034-1cdc10c66d5b // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250902204034-1cdc10c66d5b // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250826113018-8c6f6358d4bb // indirect
github.com/djherbis/times v1.6.0 // indirect
@@ -142,24 +149,19 @@ require (
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.31.0 // indirect
)
+38 -35
View File
@@ -1,3 +1,9 @@
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
@@ -57,8 +63,8 @@ github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -78,36 +84,40 @@ github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9V
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/fang v0.4.0 h1:boBxmdcFghTeotqkD2itXi7SMBozdIlcslRqjboSJDg=
github.com/charmbracelet/fang v0.4.0/go.mod h1:9gCUAHmVx5BwSafeyNr3GI0GgvlB1WYjL21SkPp1jyU=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
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/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250902204034-1cdc10c66d5b h1:U9SnQTnrxy8y3gEpxhpBS3ztHAR7IvL0CjvFHOR4sbE=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250902204034-1cdc10c66d5b/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
github.com/charmbracelet/x/exp/color v0.0.0-20250902204034-1cdc10c66d5b h1:x4wRlDV7e7qM6yYS06W6wMKh6z1NeD1+DTjvOm2grzo=
github.com/charmbracelet/x/exp/color v0.0.0-20250902204034-1cdc10c66d5b/go.mod h1:hk/GyTELmEgX54pBAOHcFvH8Xed53JWo/g8kJXFo/PI=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/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/exp/slice v0.0.0-20250902204034-1cdc10c66d5b h1:DZ2Li1O0j+wWw6AgEUDrODB7PAIKpmOy65yu1UBPYc4=
github.com/charmbracelet/x/exp/slice v0.0.0-20250902204034-1cdc10c66d5b/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
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/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.7.13 h1:Ku7hY+83gGJJjf4On3UgqjC57UcA+DXe0tqAZiNDDew=
@@ -134,8 +144,6 @@ github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIj
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4=
github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -216,8 +224,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-filesystem-server v0.11.1 h1:7uKIZRMaKWfgvtDj/uLAvo0+7Mwb8gxo5DJywhqFW88=
@@ -228,11 +236,9 @@ github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0 h1:nIohpHs1ViKR0SVgW/cbBstHjmnqFZDM9RqgX9m9Xu8=
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
@@ -246,8 +252,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
@@ -426,14 +430,13 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -447,8 +450,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
tea "charm.land/bubbletea/v2"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
@@ -463,8 +463,8 @@ func (m escListenerModel) Init() tea.Cmd {
func (m escListenerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Type == tea.KeyEsc {
case tea.KeyPressMsg:
if msg.String() == "esc" {
// Signal ESC was pressed
select {
case m.escPressed <- true:
@@ -476,8 +476,8 @@ func (m escListenerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m escListenerModel) View() string {
return "" // No visual output needed
func (m escListenerModel) View() tea.View {
return tea.NewView("") // No visual output needed
}
// listenForESC listens for ESC key press using Bubble Tea and returns true if detected
+9 -10
View File
@@ -1,13 +1,15 @@
package ui
import (
"github.com/charmbracelet/lipgloss"
"image/color"
"charm.land/lipgloss/v2"
)
// blockRenderer handles rendering of content blocks with configurable options
type blockRenderer struct {
align *lipgloss.Position
borderColor *lipgloss.AdaptiveColor
borderColor *color.Color
fullWidth bool
paddingTop int
paddingBottom int
@@ -42,9 +44,9 @@ func WithAlign(align lipgloss.Position) renderingOption {
// WithBorderColor returns a renderingOption that sets the border color
// for the block. The color parameter uses lipgloss.AdaptiveColor to support
// both light and dark terminal themes automatically.
func WithBorderColor(color lipgloss.AdaptiveColor) renderingOption {
return func(c *blockRenderer) {
c.borderColor = &color
func WithBorderColor(c color.Color) renderingOption {
return func(br *blockRenderer) {
br.borderColor = &c
}
}
@@ -141,16 +143,13 @@ func renderContentBlock(content string, containerWidth int, options ...rendering
}
// Default to transparent/no border color
borderColor := lipgloss.AdaptiveColor{Light: "", Dark: ""}
var borderColor color.Color = lipgloss.NoColor{}
if renderer.borderColor != nil {
borderColor = *renderer.borderColor
}
// Very muted color for the opposite border
mutedOppositeBorder := lipgloss.AdaptiveColor{
Light: "#F3F4F6", // Very light gray, barely visible
Dark: "#1F2937", // Very dark gray, barely visible
}
mutedOppositeBorder := AdaptiveColor("#F3F4F6", "#1F2937")
switch align {
case lipgloss.Left:
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/cloudwego/eino/schema"
"golang.org/x/term"
)
+5 -1
View File
@@ -5,7 +5,7 @@ import (
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"charm.land/lipgloss/v2"
)
// CompactRenderer handles rendering messages in a space-efficient compact format,
@@ -41,6 +41,10 @@ func (r *CompactRenderer) RenderUserMessage(content string, timestamp time.Time)
symbol := lipgloss.NewStyle().Foreground(theme.Secondary).Render(">")
label := lipgloss.NewStyle().Foreground(theme.Secondary).Bold(true).Render("User")
// Convert single newlines to paragraph breaks so they survive glamour's
// markdown rendering (glamour treats single \n as a soft break).
content = strings.ReplaceAll(content, "\n", "\n\n")
// Format content for user messages (preserve formatting, no truncation)
compactContent := r.formatUserAssistantContent(content)
+66 -94
View File
@@ -2,12 +2,27 @@ package ui
import (
"fmt"
"image/color"
"os"
"github.com/charmbracelet/lipgloss"
"charm.land/lipgloss/v2"
)
// Enhanced styling utilities and theme definitions
// isDarkBg caches the terminal background detection result at package init.
var isDarkBg = lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
// AdaptiveColor picks between a light-mode and dark-mode hex color string
// based on the detected terminal background. This replaces the old
// lipgloss.AdaptiveColor{Light: ..., Dark: ...} pattern from v1.
func AdaptiveColor(light, dark string) color.Color {
if isDarkBg {
return lipgloss.Color(dark)
}
return lipgloss.Color(light)
}
// Global theme instance
var currentTheme = DefaultTheme()
@@ -27,22 +42,22 @@ func SetTheme(theme Theme) {
// both light and dark terminal modes through adaptive colors. It includes semantic
// colors for different message types and UI elements, based on the Catppuccin color palette.
type Theme struct {
Primary lipgloss.AdaptiveColor
Secondary lipgloss.AdaptiveColor
Success lipgloss.AdaptiveColor
Warning lipgloss.AdaptiveColor
Error lipgloss.AdaptiveColor
Info lipgloss.AdaptiveColor
Text lipgloss.AdaptiveColor
Muted lipgloss.AdaptiveColor
VeryMuted lipgloss.AdaptiveColor
Background lipgloss.AdaptiveColor
Border lipgloss.AdaptiveColor
MutedBorder lipgloss.AdaptiveColor
System lipgloss.AdaptiveColor
Tool lipgloss.AdaptiveColor
Accent lipgloss.AdaptiveColor
Highlight lipgloss.AdaptiveColor
Primary color.Color
Secondary color.Color
Success color.Color
Warning color.Color
Error color.Color
Info color.Color
Text color.Color
Muted color.Color
VeryMuted color.Color
Background color.Color
Border color.Color
MutedBorder color.Color
System color.Color
Tool color.Color
Accent color.Color
Highlight color.Color
}
// DefaultTheme creates and returns the default MCPHost theme based on the Catppuccin
@@ -50,70 +65,22 @@ type Theme struct {
// pleasant visual experience with carefully selected colors for different UI elements.
func DefaultTheme() Theme {
return Theme{
Primary: lipgloss.AdaptiveColor{
Light: "#8839ef", // Latte Mauve
Dark: "#cba6f7", // Mocha Mauve
},
Secondary: lipgloss.AdaptiveColor{
Light: "#04a5e5", // Latte Sky
Dark: "#89dceb", // Mocha Sky
},
Success: lipgloss.AdaptiveColor{
Light: "#40a02b", // Latte Green
Dark: "#a6e3a1", // Mocha Green
},
Warning: lipgloss.AdaptiveColor{
Light: "#df8e1d", // Latte Yellow
Dark: "#f9e2af", // Mocha Yellow
},
Error: lipgloss.AdaptiveColor{
Light: "#d20f39", // Latte Red
Dark: "#f38ba8", // Mocha Red
},
Info: lipgloss.AdaptiveColor{
Light: "#1e66f5", // Latte Blue
Dark: "#89b4fa", // Mocha Blue
},
Text: lipgloss.AdaptiveColor{
Light: "#4c4f69", // Latte Text
Dark: "#cdd6f4", // Mocha Text
},
Muted: lipgloss.AdaptiveColor{
Light: "#6c6f85", // Latte Subtext 0
Dark: "#a6adc8", // Mocha Subtext 0
},
VeryMuted: lipgloss.AdaptiveColor{
Light: "#9ca0b0", // Latte Overlay 0
Dark: "#6c7086", // Mocha Overlay 0
},
Background: lipgloss.AdaptiveColor{
Light: "#eff1f5", // Latte Base
Dark: "#1e1e2e", // Mocha Base
},
Border: lipgloss.AdaptiveColor{
Light: "#acb0be", // Latte Surface 2
Dark: "#585b70", // Mocha Surface 2
},
MutedBorder: lipgloss.AdaptiveColor{
Light: "#ccd0da", // Latte Surface 0
Dark: "#313244", // Mocha Surface 0
},
System: lipgloss.AdaptiveColor{
Light: "#179299", // Latte Teal
Dark: "#94e2d5", // Mocha Teal
},
Tool: lipgloss.AdaptiveColor{
Light: "#fe640b", // Latte Peach
Dark: "#fab387", // Mocha Peach
},
Accent: lipgloss.AdaptiveColor{
Light: "#ea76cb", // Latte Pink
Dark: "#f5c2e7", // Mocha Pink
},
Highlight: lipgloss.AdaptiveColor{
Light: "#df8e1d", // Latte Yellow (for highlights)
Dark: "#45475a", // Mocha Surface 1 (subtle highlight)
},
Primary: AdaptiveColor("#8839ef", "#cba6f7"), // Latte/Mocha Mauve
Secondary: AdaptiveColor("#04a5e5", "#89dceb"), // Latte/Mocha Sky
Success: AdaptiveColor("#40a02b", "#a6e3a1"), // Latte/Mocha Green
Warning: AdaptiveColor("#df8e1d", "#f9e2af"), // Latte/Mocha Yellow
Error: AdaptiveColor("#d20f39", "#f38ba8"), // Latte/Mocha Red
Info: AdaptiveColor("#1e66f5", "#89b4fa"), // Latte/Mocha Blue
Text: AdaptiveColor("#4c4f69", "#cdd6f4"), // Latte/Mocha Text
Muted: AdaptiveColor("#6c6f85", "#a6adc8"), // Latte/Mocha Subtext 0
VeryMuted: AdaptiveColor("#9ca0b0", "#6c7086"), // Latte/Mocha Overlay 0
Background: AdaptiveColor("#eff1f5", "#1e1e2e"), // Latte/Mocha Base
Border: AdaptiveColor("#acb0be", "#585b70"), // Latte/Mocha Surface 2
MutedBorder: AdaptiveColor("#ccd0da", "#313244"), // Latte/Mocha Surface 0
System: AdaptiveColor("#179299", "#94e2d5"), // Latte/Mocha Teal
Tool: AdaptiveColor("#fe640b", "#fab387"), // Latte/Mocha Peach
Accent: AdaptiveColor("#ea76cb", "#f5c2e7"), // Latte/Mocha Pink
Highlight: AdaptiveColor("#df8e1d", "#45475a"), // Latte Yellow / Mocha Surface 1
}
}
@@ -129,6 +96,11 @@ func StyleCard(width int, theme Theme) lipgloss.Style {
MarginBottom(1)
}
// IsDarkBackground returns the cached terminal background detection result.
func IsDarkBackground() bool {
return isDarkBg
}
// StyleHeader creates a lipgloss style for primary headers using the theme's
// primary color with bold text for emphasis and hierarchy.
func StyleHeader(theme Theme) lipgloss.Style {
@@ -187,9 +159,9 @@ func StyleInfo(theme Theme) lipgloss.Style {
// CreateSeparator generates a horizontal separator line with the specified width,
// character, and color. Useful for visually dividing sections of content in the UI.
func CreateSeparator(width int, char string, color lipgloss.AdaptiveColor) string {
func CreateSeparator(width int, char string, c color.Color) string {
return lipgloss.NewStyle().
Foreground(color).
Foreground(c).
Width(width).
Render(lipgloss.PlaceHorizontal(width, lipgloss.Center, char))
}
@@ -214,10 +186,10 @@ func CreateProgressBar(width int, percentage float64, theme Theme) string {
// CreateBadge generates a styled badge or label with inverted colors (text on
// colored background) for highlighting important tags, statuses, or categories.
func CreateBadge(text string, color lipgloss.AdaptiveColor) string {
func CreateBadge(text string, c color.Color) string {
return lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#FFFFFF", Dark: "#000000"}).
Background(color).
Foreground(AdaptiveColor("#FFFFFF", "#000000")).
Background(c).
Padding(0, 1).
Bold(true).
Render(text)
@@ -226,7 +198,7 @@ func CreateBadge(text string, color lipgloss.AdaptiveColor) string {
// CreateGradientText creates styled text with a gradient-like effect. Currently
// implements a simplified version using the start color only, as true gradients
// require more complex terminal capabilities.
func CreateGradientText(text string, startColor, endColor lipgloss.AdaptiveColor) string {
func CreateGradientText(text string, startColor, endColor color.Color) string {
// For now, just use the start color - true gradients would require more complex implementation
return lipgloss.NewStyle().
Foreground(startColor).
@@ -238,32 +210,32 @@ func CreateGradientText(text string, startColor, endColor lipgloss.AdaptiveColor
// StyleCompactSymbol creates a lipgloss style for message type indicators in
// compact mode, using bold colored text to distinguish different message categories.
func StyleCompactSymbol(symbol string, color lipgloss.AdaptiveColor) lipgloss.Style {
func StyleCompactSymbol(symbol string, c color.Color) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(color).
Foreground(c).
Bold(true)
}
// StyleCompactLabel creates a lipgloss style for message labels in compact mode
// with fixed width for alignment and bold colored text for readability.
func StyleCompactLabel(color lipgloss.AdaptiveColor) lipgloss.Style {
func StyleCompactLabel(c color.Color) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(color).
Foreground(c).
Bold(true).
Width(8)
}
// StyleCompactContent creates a simple lipgloss style for message content in
// compact mode, applying only color without additional formatting.
func StyleCompactContent(color lipgloss.AdaptiveColor) lipgloss.Style {
func StyleCompactContent(c color.Color) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(color)
Foreground(c)
}
// FormatCompactLine assembles a complete compact mode message line with consistent
// spacing and styling. Combines a symbol, fixed-width label, and content with their
// respective colors to create a uniform appearance across all message types.
func FormatCompactLine(symbol, label, content string, symbolColor, labelColor, contentColor lipgloss.AdaptiveColor) string {
func FormatCompactLine(symbol, label, content string, symbolColor, labelColor, contentColor color.Color) string {
styledSymbol := StyleCompactSymbol(symbol, symbolColor).Render(symbol)
styledLabel := StyleCompactLabel(labelColor).Render(label)
styledContent := StyleCompactContent(contentColor).Render(content)
+5 -1
View File
@@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"charm.land/lipgloss/v2"
)
// MessageType represents different categories of messages displayed in the UI,
@@ -88,6 +88,10 @@ func (r *MessageRenderer) RenderUserMessage(content string, timestamp time.Time)
timeStr := timestamp.Local().Format("15:04")
username := getSystemUsername()
// Convert single newlines to paragraph breaks so they survive glamour's
// markdown rendering (glamour treats single \n as a soft break).
content = strings.ReplaceAll(content, "\n", "\n\n")
// Render the message content
messageContent := r.renderMarkdown(content, r.width-8) // Account for padding and borders
+17 -16
View File
@@ -9,9 +9,9 @@ import (
"sync"
"time"
"github.com/charmbracelet/bubbles/progress"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"charm.land/bubbles/v2/progress"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
@@ -57,7 +57,7 @@ type ProgressModel struct {
// progress bar and initial "Initializing..." status message.
func NewProgressModel() ProgressModel {
return ProgressModel{
progress: progress.New(progress.WithDefaultGradient()),
progress: progress.New(progress.WithDefaultBlend()),
status: "Initializing...",
}
}
@@ -73,17 +73,18 @@ func (m ProgressModel) Init() tea.Cmd {
// triggers program exit on completion or cancellation.
func (m ProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
case tea.KeyPressMsg:
if msg.String() == "q" || msg.String() == "ctrl+c" {
return m, tea.Quit
}
return m, nil
case tea.WindowSizeMsg:
m.progress.Width = msg.Width - padding*2 - 4
if m.progress.Width > maxWidth {
m.progress.Width = maxWidth
newWidth := msg.Width - padding*2 - 4
if newWidth > maxWidth {
newWidth = maxWidth
}
m.progress.SetWidth(newWidth)
return m, nil
case progressErrMsg:
@@ -107,8 +108,8 @@ func (m ProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
var cmd tea.Cmd
m.progress, cmd = m.progress.Update(msg)
return m, cmd
default:
@@ -119,23 +120,23 @@ func (m ProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View implements the tea.Model interface, rendering the progress bar with
// status information and help text. Displays error messages if present or
// a completion message when the download finishes.
func (m ProgressModel) View() string {
func (m ProgressModel) View() tea.View {
if m.err != nil {
return fmt.Sprintf("Error: %s\n", m.err.Error())
return tea.NewView(fmt.Sprintf("Error: %s\n", m.err.Error()))
}
if m.complete {
return fmt.Sprintf("\n%s%s\n\n%sComplete!\n",
return tea.NewView(fmt.Sprintf("\n%s%s\n\n%sComplete!\n",
strings.Repeat(" ", padding),
m.progress.View(),
strings.Repeat(" ", padding))
strings.Repeat(" ", padding)))
}
pad := strings.Repeat(" ", padding)
return fmt.Sprintf("\n%s%s\n%s%s\n\n%s",
return tea.NewView(fmt.Sprintf("\n%s%s\n%s%s\n\n%s",
pad, m.progress.View(),
pad, m.status,
pad+helpStyle("Press 'q' or Ctrl+C to cancel"))
pad+helpStyle("Press 'q' or Ctrl+C to cancel")))
}
// ProgressReader wraps an io.Reader to intercept and parse Ollama pull operation
+23 -33
View File
@@ -3,10 +3,10 @@ package ui
import (
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/textarea"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
// SlashCommandInput provides an interactive text input field with intelligent
@@ -41,13 +41,21 @@ func NewSlashCommandInput(width int, title string) *SlashCommandInput {
ta.SetHeight(3) // Default to 3 lines like huh
ta.Focus()
// Override InsertNewline so only ctrl+j and alt+enter insert newlines.
// Enter always submits the input.
ta.KeyMap.InsertNewline = key.NewBinding(
key.WithKeys("ctrl+j", "alt+enter"),
key.WithHelp("ctrl+j", "insert newline"),
)
// Style the textarea to match huh theme
ta.FocusedStyle.Base = lipgloss.NewStyle()
ta.FocusedStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ta.FocusedStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
ta.FocusedStyle.Prompt = lipgloss.NewStyle()
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
ta.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
styles := ta.Styles()
styles.Focused.Base = lipgloss.NewStyle()
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
styles.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
styles.Focused.Prompt = lipgloss.NewStyle()
styles.Focused.CursorLine = lipgloss.NewStyle()
ta.SetStyles(styles)
return &SlashCommandInput{
textarea: ta,
@@ -78,25 +86,13 @@ func (s *SlashCommandInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
switch msg := msg.(type) {
case tea.KeyMsg: // Check for quit keys first (when popup is not shown)
case tea.KeyPressMsg: // Check for quit keys first (when popup is not shown)
if !s.showPopup {
switch msg.String() {
case "ctrl+c", "esc":
s.quitting = true
return s, tea.Quit
case "ctrl+d": // Submit on Ctrl+D like huh
s.value = s.textarea.Value()
s.quitting = true
return s, tea.Quit
}
// Check for newline keys first
if msg.String() == "ctrl+j" || msg.String() == "alt+enter" {
// Insert newline at cursor position
s.textarea, cmd = s.textarea.Update(tea.KeyMsg{Type: tea.KeyEnter, Alt: true})
return s, cmd
} else if msg.String() == "enter" && !strings.Contains(s.textarea.Value(), "\n") {
// Submit on Enter only if it's single line
case "ctrl+d", "enter": // Enter always submits
s.value = s.textarea.Value()
s.quitting = true
return s, tea.Quit
@@ -178,7 +174,7 @@ func (s *SlashCommandInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View implements the tea.Model interface, rendering the complete input field
// including the title, text area, autocomplete popup (when active), and help text.
// The view adapts based on whether single or multi-line input is detected.
func (s *SlashCommandInput) View() string {
func (s *SlashCommandInput) View() tea.View {
// Add left padding to entire component (2 spaces like other UI elements)
containerStyle := lipgloss.NewStyle().PaddingLeft(2)
@@ -228,20 +224,14 @@ func (s *SlashCommandInput) View() string {
Foreground(lipgloss.Color("240")).
MarginTop(1)
// Show different help based on whether we have multiline content
helpText := "enter submit"
if strings.Contains(s.textarea.Value(), "\n") {
helpText = "ctrl+d submit • enter new line"
} else {
helpText = "enter submit • ctrl+j / alt+enter new line"
}
helpText := "enter submit • ctrl+j / alt+enter new line"
view.WriteString("\n")
view.WriteString(helpStyle.Render(helpText))
s.renderedLines += 2 // newline + help text
// Apply container padding to entire view
return containerStyle.Render(view.String())
return tea.NewView(containerStyle.Render(view.String()))
}
// renderPopup renders the autocomplete popup
+10 -9
View File
@@ -3,11 +3,12 @@ package ui
import (
"context"
"fmt"
"image/color"
"os"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
// Spinner provides an animated loading indicator that displays while long-running
@@ -34,7 +35,7 @@ func (m spinnerModel) Init() tea.Cmd {
func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
case tea.KeyPressMsg:
m.quitting = true
return m, tea.Quit
case spinner.TickMsg:
@@ -49,9 +50,9 @@ func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
func (m spinnerModel) View() string {
func (m spinnerModel) View() tea.View {
if m.quitting {
return ""
return tea.NewView("")
}
// Enhanced spinner display with better styling
@@ -66,9 +67,9 @@ func (m spinnerModel) View() string {
Foreground(theme.Text).
Italic(true)
return fmt.Sprintf(" %s %s",
return tea.NewView(fmt.Sprintf(" %s %s",
spinnerStyle.Render(m.spinner.View()),
messageStyle.Render(m.message))
messageStyle.Render(m.message)))
}
// quitMsg is sent when we want to quit the spinner
@@ -104,7 +105,7 @@ func NewSpinner(message string) *Spinner {
// NewThemedSpinner creates a new animated spinner with custom color styling.
// This allows for different spinner colors based on the operation type or status.
// The spinner runs independently in its own tea.Program.
func NewThemedSpinner(message string, color lipgloss.AdaptiveColor) *Spinner {
func NewThemedSpinner(message string, color color.Color) *Spinner {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = s.Style.Foreground(color)
+5 -5
View File
@@ -1,9 +1,9 @@
package ui
import (
"charm.land/lipgloss/v2"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/lipgloss"
"github.com/mark3labs/mcphost/internal/config"
"github.com/spf13/viper"
)
@@ -42,7 +42,7 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
err := config.FilepathOr("markdown-theme", &mdTheme)
fromConfig := err == nil && viper.InConfig("markdown-theme")
if fromConfig && lipgloss.HasDarkBackground() {
if fromConfig && IsDarkBackground() {
textColor = mdTheme.Text.Light
mutedColor = mdTheme.Muted.Light
headingColor = mdTheme.Heading.Light
@@ -68,7 +68,7 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
stringColor = mdTheme.String.Dark
numberColor = mdTheme.Number.Dark
commentColor = mdTheme.Comment.Dark
} else if lipgloss.HasDarkBackground() {
} else if IsDarkBackground() {
textColor = "#F9FAFB" // Light text for dark backgrounds
mutedColor = "#9CA3AF" // Light muted for dark backgrounds
// Dark background colors
@@ -117,12 +117,12 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
Prefix: "┃ ",
},
Indent: uintPtr(1),
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.AdaptiveColor{Light: bgColor, Dark: bgColor}).Render(" ")),
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
},
List: ansi.StyleList{
LevelIndent: 0, // Remove list indentation
StyleBlock: ansi.StyleBlock{
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.AdaptiveColor{Light: bgColor, Dark: bgColor}).Render(" ")),
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.Color(bgColor)).Render(" ")),
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(textColor),
},
+14 -13
View File
@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"charm.land/bubbles/v2/textarea"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)
type ToolApprovalInput struct {
@@ -29,12 +29,13 @@ func NewToolApprovalInput(toolName, toolArgs string, width int) *ToolApprovalInp
ta.Focus()
// Style the textarea to match huh theme
ta.FocusedStyle.Base = lipgloss.NewStyle()
ta.FocusedStyle.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ta.FocusedStyle.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
ta.FocusedStyle.Prompt = lipgloss.NewStyle()
ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
ta.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
styles := ta.Styles()
styles.Focused.Base = lipgloss.NewStyle()
styles.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
styles.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
styles.Focused.Prompt = lipgloss.NewStyle()
styles.Focused.CursorLine = lipgloss.NewStyle()
ta.SetStyles(styles)
return &ToolApprovalInput{
textarea: ta,
@@ -51,7 +52,7 @@ func (t *ToolApprovalInput) Init() tea.Cmd {
func (t *ToolApprovalInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
case tea.KeyPressMsg:
switch msg.String() {
case "y", "Y":
t.approved = true
@@ -80,9 +81,9 @@ func (t *ToolApprovalInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return t, nil
}
func (t *ToolApprovalInput) View() string {
func (t *ToolApprovalInput) View() tea.View {
if t.done {
return "we are done"
return tea.NewView("we are done")
}
// Add left padding to entire component (2 spaces like other UI elements)
containerStyle := lipgloss.NewStyle().PaddingLeft(2)
@@ -131,5 +132,5 @@ func (t *ToolApprovalInput) View() string {
}
view.WriteString(yesText + "/" + noText + "\n")
return containerStyle.Render(inputBoxStyle.Render(view.String()))
return tea.NewView(containerStyle.Render(inputBoxStyle.Render(view.String())))
}
+4 -2
View File
@@ -4,7 +4,9 @@ import (
"fmt"
"sync"
"github.com/charmbracelet/lipgloss"
"charm.land/lipgloss/v2"
"image/color"
"github.com/mark3labs/mcphost/internal/models"
"github.com/mark3labs/mcphost/internal/tokens"
)
@@ -167,7 +169,7 @@ func (ut *UsageTracker) RenderUsageInfo() string {
// Calculate percentage based on context limit with color coding
var percentageStr string
var percentageColor lipgloss.AdaptiveColor
var percentageColor color.Color
if ut.modelInfo.Limit.Context > 0 {
percentage := float64(totalTokens) / float64(ut.modelInfo.Limit.Context) * 100
+10 -2
View File
@@ -1,12 +1,20 @@
package ui
import (
"regexp"
"strings"
"testing"
"github.com/mark3labs/mcphost/internal/models"
)
// stripAnsi removes ANSI escape codes from a string for test comparisons.
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func stripAnsi(s string) string {
return ansiRegex.ReplaceAllString(s, "")
}
func TestUsageTracker_RenderUsageInfo_OAuth(t *testing.T) {
// Create a mock model info with costs and context limit
modelInfo := &models.ModelInfo{
@@ -26,7 +34,7 @@ func TestUsageTracker_RenderUsageInfo_OAuth(t *testing.T) {
oauthTracker := NewUsageTracker(modelInfo, "anthropic", 80, true)
oauthTracker.UpdateUsage(1500, 500, 0, 0) // 2000 total tokens
rendered := oauthTracker.RenderUsageInfo()
rendered := stripAnsi(oauthTracker.RenderUsageInfo())
// Should show tokens and percentage, but cost should show "$0.00"
if !strings.Contains(rendered, "Tokens: 2.0K") {
@@ -43,7 +51,7 @@ func TestUsageTracker_RenderUsageInfo_OAuth(t *testing.T) {
regularTracker := NewUsageTracker(modelInfo, "anthropic", 80, false)
regularTracker.UpdateUsage(1500, 500, 0, 0) // Same token usage
regularRendered := regularTracker.RenderUsageInfo()
regularRendered := stripAnsi(regularTracker.RenderUsageInfo())
// Should show tokens and actual cost
if !strings.Contains(regularRendered, "Tokens: 2.0K") {