From ce32cea7eec4a3f1fa4f2de9b2bda978e141d18d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Wed, 25 Feb 2026 17:07:09 +0300 Subject: [PATCH] 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) --- cmd/root.go | 33 +++-- go.mod | 36 ++--- go.sum | 73 ++++++----- internal/agent/agent.go | 10 +- internal/ui/block_renderer.go | 19 ++- internal/ui/cli.go | 4 +- internal/ui/compact_renderer.go | 6 +- internal/ui/enhanced_styles.go | 160 ++++++++++------------- internal/ui/messages.go | 6 +- internal/ui/progress/ollama.go | 33 ++--- internal/ui/slash_command_input.go | 56 ++++---- internal/ui/spinner.go | 19 +-- internal/ui/styles.go | 10 +- internal/ui/tool_approval_input.go | 27 ++-- internal/ui/usage_tracker.go | 6 +- internal/ui/usage_tracker_render_test.go | 12 +- 16 files changed, 248 insertions(+), 262 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 7d91f7ea..75cc1f63 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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), } } diff --git a/go.mod b/go.mod index dfb19995..63d19451 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 62aa6760..caf57807 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 4e810a3d..14141906 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -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 diff --git a/internal/ui/block_renderer.go b/internal/ui/block_renderer.go index 013ad476..28dbfc51 100644 --- a/internal/ui/block_renderer.go +++ b/internal/ui/block_renderer.go @@ -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: diff --git a/internal/ui/cli.go b/internal/ui/cli.go index 89bef174..99b29090 100644 --- a/internal/ui/cli.go +++ b/internal/ui/cli.go @@ -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" ) diff --git a/internal/ui/compact_renderer.go b/internal/ui/compact_renderer.go index 1b614a1b..60fc26ec 100644 --- a/internal/ui/compact_renderer.go +++ b/internal/ui/compact_renderer.go @@ -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) diff --git a/internal/ui/enhanced_styles.go b/internal/ui/enhanced_styles.go index d54711f3..18cc6ab6 100644 --- a/internal/ui/enhanced_styles.go +++ b/internal/ui/enhanced_styles.go @@ -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) diff --git a/internal/ui/messages.go b/internal/ui/messages.go index eb2dfe13..55af586d 100644 --- a/internal/ui/messages.go +++ b/internal/ui/messages.go @@ -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 diff --git a/internal/ui/progress/ollama.go b/internal/ui/progress/ollama.go index 258dc5b9..0bc8a88a 100644 --- a/internal/ui/progress/ollama.go +++ b/internal/ui/progress/ollama.go @@ -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 diff --git a/internal/ui/slash_command_input.go b/internal/ui/slash_command_input.go index 01d81edf..5ad8d267 100644 --- a/internal/ui/slash_command_input.go +++ b/internal/ui/slash_command_input.go @@ -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 diff --git a/internal/ui/spinner.go b/internal/ui/spinner.go index 994e3a49..f8ec62c4 100644 --- a/internal/ui/spinner.go +++ b/internal/ui/spinner.go @@ -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) diff --git a/internal/ui/styles.go b/internal/ui/styles.go index f4d092e9..f5f4220e 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -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), }, diff --git a/internal/ui/tool_approval_input.go b/internal/ui/tool_approval_input.go index 01970ccf..9ff16928 100644 --- a/internal/ui/tool_approval_input.go +++ b/internal/ui/tool_approval_input.go @@ -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()))) } diff --git a/internal/ui/usage_tracker.go b/internal/ui/usage_tracker.go index f9656914..b255c396 100644 --- a/internal/ui/usage_tracker.go +++ b/internal/ui/usage_tracker.go @@ -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 diff --git a/internal/ui/usage_tracker_render_test.go b/internal/ui/usage_tracker_render_test.go index 4fc6be7d..e683b587 100644 --- a/internal/ui/usage_tracker_render_test.go +++ b/internal/ui/usage_tracker_render_test.go @@ -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") {