on main help sign in
perf(ui): skip theme rebuild when provider keeps the same theme
173b2beKieran Klukas
internal/ui/model/ui.go+30−2
@@ -196,6 +196,11 @@ type UI struct {
196
196
197 isTransparent bool
197 isTransparent bool
198
198
199+ // themeKey identifies the currently applied theme so applyTheme can
200+ // skip the expensive style rebuild when switching to a provider that
201+ // resolves to the same theme.
202+ themeKey string
203+
199 focus uiFocusState
204 focus uiFocusState
200 state uiState
205 state uiState
201
206
@@ -380,6 +385,12 @@ func New(com *common.Common, initialSessionID string, continueLast bool) *UI {
380
385
381 status := NewStatus(com, ui)
386 status := NewStatus(com, ui)
382
387
388+ // Seed the active theme key from the large model provider so the
389+ // first model selection can correctly skip a redundant theme swap.
390+ if cfg := com.Config(); cfg != nil {
391+ ui.themeKey = styles.ThemeKeyForProvider(cfg.Models[config.SelectedModelTypeLarge].Provider)
392+ }
393+
383 ui.setEditorPrompt(com.Workspace.PermissionSkipRequests())
394 ui.setEditorPrompt(com.Workspace.PermissionSkipRequests())
384 ui.randomizePlaceholders()
395 ui.randomizePlaceholders()
385 ui.textarea.Placeholder = ui.readyPlaceholder
396 ui.textarea.Placeholder = ui.readyPlaceholder
@@ -1842,8 +1853,10 @@ func (m *UI) handleSelectModel(msg dialog.ActionSelectModel) tea.Cmd {
1842 } else {
1853 } else {
1843 if msg.ModelType == config.SelectedModelTypeLarge {
1854 if msg.ModelType == config.SelectedModelTypeLarge {
1844 // Swap the theme live based on the newly selected large
1855 // Swap the theme live based on the newly selected large
1845− // model's provider.
1856+ // model's provider. Skipped when the provider resolves to
1846− m.applyTheme(styles.ThemeForProvider(providerID))
1857+ // the already-active theme, which avoids a full markdown
1858+ // re-render of the transcript on every selection.
1859+ m.applyThemeForProvider(providerID)
1847 }
1860 }
1848 if _, ok := cfg.Models[config.SelectedModelTypeSmall]; !ok {
1861 if _, ok := cfg.Models[config.SelectedModelTypeSmall]; !ok {
1849 // Ensure small model is set is unset.
1862 // Ensure small model is set is unset.
@@ -3371,6 +3384,21 @@ func (m *UI) cacheSidebarLogo(width int) {
3371 m.sidebarLogo = renderLogo(m.com.Styles, true, m.com.IsHyper(), width)
3384 m.sidebarLogo = renderLogo(m.com.Styles, true, m.com.IsHyper(), width)
3372}
3385}
3373
3386
3387+// applyThemeForProvider swaps the active theme to the one associated with
3388+// the given provider, but only when that theme differs from the one
3389+// already applied. Most providers share a single theme, so re-selecting a
3390+// model from the same theme family would otherwise pay the full cost of
3391+// invalidating the markdown renderer cache and re-rendering the entire
3392+// transcript for no visible change.
3393+func (m *UI) applyThemeForProvider(providerID string) {
3394+ key := styles.ThemeKeyForProvider(providerID)
3395+ if key == m.themeKey {
3396+ return
3397+ }
3398+ m.themeKey = key
3399+ m.applyTheme(styles.ThemeForProvider(providerID))
3400+}
3401+
3374// applyTheme replaces the active styles with the given theme, drops the
3402// applyTheme replaces the active styles with the given theme, drops the
3375// shared markdown renderer cache, and refreshes every component that
3403// shared markdown renderer cache, and refreshes every component that
3376// caches style data.
3404// caches style data.
internal/ui/styles/themes.go+16−1
@@ -4,11 +4,26 @@ import (
4 "github.com/charmbracelet/x/exp/charmtone"
4 "github.com/charmbracelet/x/exp/charmtone"
5)
5)
6
6
7+// ThemeKeyForProvider returns a stable identifier for the theme
8+// associated with the given provider ID. Providers that share a theme
9+// yield the same key, so callers can cheaply detect when switching
10+// providers would not actually change the active theme and skip the
11+// expensive style rebuild. This is the single source of truth for the
12+// provider-to-theme mapping; [ThemeForProvider] builds on it.
13+func ThemeKeyForProvider(providerID string) string {
14+ switch providerID {
15+ case "hyper":
16+ return "hyper"
17+ default:
18+ return "default"
19+ }
20+}
21+
7// ThemeForProvider returns the Styles associated with the given provider
22// ThemeForProvider returns the Styles associated with the given provider
8// ID. Unknown or empty provider IDs yield the default Charmtone Pantera
23// ID. Unknown or empty provider IDs yield the default Charmtone Pantera
9// theme.
24// theme.
10func ThemeForProvider(providerID string) Styles {
25func ThemeForProvider(providerID string) Styles {
11− switch providerID {
26+ switch ThemeKeyForProvider(providerID) {
12 case "hyper":
27 case "hyper":
13 return HypercrushObsidiana()
28 return HypercrushObsidiana()
14 default:
29 default: