summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components
diff options
context:
space:
mode:
authorYtzhak <[email protected]>2025-08-18 06:55:01 -0400
committerGitHub <[email protected]>2025-08-18 05:55:01 -0500
commit667ff90dd6be20bf19d5424a80d57559d38352f5 (patch)
tree1be3df414954011ff3caa859702679acbe619a7e /packages/tui/internal/components
parentcd3d91209ab843a5a84eec8aae371510ab9e3178 (diff)
downloadopencode-667ff90dd6be20bf19d5424a80d57559d38352f5.tar.gz
opencode-667ff90dd6be20bf19d5424a80d57559d38352f5.zip
feat: add shimmer text rendering (#2027)
Diffstat (limited to 'packages/tui/internal/components')
-rw-r--r--packages/tui/internal/components/chat/editor.go15
-rw-r--r--packages/tui/internal/components/chat/message.go12
-rw-r--r--packages/tui/internal/components/chat/messages.go18
3 files changed, 39 insertions, 6 deletions
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index 38a579057..72daf2886 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -339,6 +339,7 @@ func (m *editorComponent) Content() string {
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
+
promptStyle := styles.NewStyle().Foreground(t.Primary()).
Padding(0, 0, 0, 1).
Bold(true)
@@ -381,9 +382,11 @@ func (m *editorComponent) Content() string {
status = "waiting for permission"
}
if m.interruptKeyInDebounce && m.app.CurrentPermission.ID == "" {
- hint = muted(
- status,
- ) + m.spinner.View() + muted(
+ bright := t.Accent()
+ if status == "waiting for permission" {
+ bright = t.Warning()
+ }
+ hint = util.Shimmer(status, t.Background(), t.TextMuted(), bright) + m.spinner.View() + muted(
" ",
) + base(
keyText+" again",
@@ -391,7 +394,11 @@ func (m *editorComponent) Content() string {
" interrupt",
)
} else {
- hint = muted(status) + m.spinner.View()
+ bright := t.Accent()
+ if status == "waiting for permission" {
+ bright = t.Warning()
+ }
+ hint = util.Shimmer(status, t.Background(), t.TextMuted(), bright) + m.spinner.View()
if m.app.CurrentPermission.ID == "" {
hint += muted(" ") + base(keyText) + muted(" interrupt")
}
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index eecfe2611..cb166246e 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -234,7 +234,13 @@ func renderText(
}
content = util.ToMarkdown(text, width, backgroundColor)
if isThinking {
- content = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking") + "\n\n" + content
+ label := util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
+ label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
+ content = label + "\n\n" + content
+ } else if strings.TrimSpace(text) == "Generating..." {
+ label := util.Shimmer(text, backgroundColor, t.TextMuted(), t.Text())
+ label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
+ content = label
}
case opencode.UserMessage:
ts = time.UnixMilli(int64(casted.Time.Created))
@@ -779,7 +785,9 @@ func renderToolTitle(
) string {
if toolCall.State.Status == opencode.ToolPartStateStatusPending {
title := renderToolAction(toolCall.Tool)
- return styles.NewStyle().Width(width - 6).Render(title)
+ t := theme.CurrentTheme()
+ shiny := util.Shimmer(title, t.BackgroundPanel(), t.TextMuted(), t.Accent())
+ return styles.NewStyle().Width(width - 6).Render(shiny)
}
toolArgs := ""
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 375254668..f63de16a1 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -8,6 +8,7 @@ import (
"sort"
"strconv"
"strings"
+ "time"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
@@ -59,6 +60,7 @@ type messagesComponent struct {
lineCount int
selection *selection
messagePositions map[string]int // map message ID to line position
+ animating bool
}
type selection struct {
@@ -99,6 +101,7 @@ func (s selection) coords(offset int) *selection {
type ToggleToolDetailsMsg struct{}
type ToggleThinkingBlocksMsg struct{}
+type shimmerTickMsg struct{}
func (m *messagesComponent) Init() tea.Cmd {
return tea.Batch(m.viewport.Init())
@@ -107,6 +110,15 @@ func (m *messagesComponent) Init() tea.Cmd {
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
+ case shimmerTickMsg:
+ if !m.app.HasAnimatingWork() {
+ m.animating = false
+ return m, nil
+ }
+ return m, tea.Sequence(
+ m.renderView(),
+ tea.Tick(90*time.Millisecond, func(t time.Time) tea.Msg { return shimmerTickMsg{} }),
+ )
case tea.MouseClickMsg:
slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset)
y := msg.Y + m.viewport.YOffset
@@ -270,6 +282,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.dirty {
cmds = append(cmds, m.renderView())
}
+
+ // Start shimmer ticks if any assistant/tool is in-flight
+ if !m.animating && m.app.HasAnimatingWork() {
+ m.animating = true
+ cmds = append(cmds, tea.Tick(90*time.Millisecond, func(t time.Time) tea.Msg { return shimmerTickMsg{} }))
+ }
}
m.tail = m.viewport.AtBottom()