Merge branch 'dev' into project

This commit is contained in:
Dax Raad 2025-08-30 15:22:48 -04:00
commit 11afe56f63
178 changed files with 4236 additions and 6777 deletions

View file

@ -820,11 +820,13 @@ func (a *App) SendCommand(ctx context.Context, command string, args string) (*Ap
opencode.SessionCommandParams{
Command: opencode.F(command),
Arguments: opencode.F(args),
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
Model: opencode.F(a.State.Provider + "/" + a.State.Model),
},
)
if err != nil {
slog.Error("Failed to execute command", "error", err)
return toast.NewErrorToast("Failed to execute command")
return toast.NewErrorToast(fmt.Sprintf("Failed to execute command: %v", err))()
}
return nil
})
@ -856,7 +858,7 @@ func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
)
if err != nil {
slog.Error("Failed to submit shell command", "error", err)
return toast.NewErrorToast("Failed to submit shell command")()
return toast.NewErrorToast(fmt.Sprintf("Failed to submit shell command: %v", err))()
}
return nil
})

View file

@ -92,6 +92,12 @@ func (r CommandRegistry) Sorted() []Command {
if b.Name == AppExitCommand {
return -1
}
if a.Custom && !b.Custom {
return 1
}
if !a.Custom && b.Custom {
return -1
}
return strings.Compare(string(a.Name), string(b.Name))
})

View file

@ -92,7 +92,41 @@ func (c *CommandCompletionProvider) GetChildEntries(
}
matches := fuzzy.RankFindFold(query, commandNames)
sort.Sort(matches)
// Custom sort to prioritize exact matches
sort.Slice(matches, func(i, j int) bool {
// Check for exact match (case-insensitive)
iExact := strings.EqualFold(matches[i].Target, query)
jExact := strings.EqualFold(matches[j].Target, query)
// Exact matches come first
if iExact && !jExact {
return true
}
if !iExact && jExact {
return false
}
// Check for prefix match (case-insensitive)
iPrefix := strings.HasPrefix(strings.ToLower(matches[i].Target), strings.ToLower(query))
jPrefix := strings.HasPrefix(strings.ToLower(matches[j].Target), strings.ToLower(query))
// Prefix matches come before fuzzy matches
if iPrefix && !jPrefix {
return true
}
if !iPrefix && jPrefix {
return false
}
// Otherwise, sort by fuzzy match score (lower distance is better)
if matches[i].Distance != matches[j].Distance {
return matches[i].Distance < matches[j].Distance
}
// If distances are equal, sort by original index (stable sort)
return matches[i].OriginalIndex < matches[j].OriginalIndex
})
// Convert matches to completion items, deduplicating by command name
items := []CompletionSuggestion{}

View file

@ -489,11 +489,22 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
if strings.HasPrefix(value, "/") {
value = value[1:]
commandName := strings.Split(value, " ")[0]
// Expand attachments in the value to get actual content
expandedValue := value
attachments := m.textarea.GetAttachments()
for _, att := range attachments {
if att.Type == "text" && att.Source != nil {
if textSource, ok := att.Source.(*attachment.TextSource); ok {
expandedValue = strings.Replace(expandedValue, att.Display, textSource.Value, 1)
}
}
}
expandedValue = expandedValue[1:] // Remove the "/"
commandName := strings.Split(expandedValue, " ")[0]
command := m.app.Commands[commands.CommandName(commandName)]
if command.Custom {
args := strings.TrimPrefix(value, command.PrimaryTrigger()+" ")
args := strings.TrimPrefix(expandedValue, command.PrimaryTrigger()+" ")
cmds = append(
cmds,
util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}),

View file

@ -1173,10 +1173,10 @@ func (m *messagesComponent) UndoLastMessage() (tea.Model, tea.Cmd) {
)
if err != nil {
slog.Error("Failed to undo message", "error", err)
return toast.NewErrorToast("Failed to undo message")
return toast.NewErrorToast("Failed to undo message")()
}
if response == nil {
return toast.NewErrorToast("Failed to undo message")
return toast.NewErrorToast("Failed to undo message")()
}
return app.MessageRevertedMsg{Session: *response, Message: revertedMessage}
}
@ -1241,10 +1241,10 @@ func (m *messagesComponent) RedoLastMessage() (tea.Model, tea.Cmd) {
)
if err != nil {
slog.Error("Failed to unrevert session", "error", err)
return toast.NewErrorToast("Failed to redo message")
return toast.NewErrorToast("Failed to redo message")()
}
if response == nil {
return toast.NewErrorToast("Failed to redo message")
return toast.NewErrorToast("Failed to redo message")()
}
return app.SessionUnrevertedMsg{Session: *response}
}
@ -1261,10 +1261,10 @@ func (m *messagesComponent) RedoLastMessage() (tea.Model, tea.Cmd) {
)
if err != nil {
slog.Error("Failed to redo message", "error", err)
return toast.NewErrorToast("Failed to redo message")
return toast.NewErrorToast("Failed to redo message")()
}
if response == nil {
return toast.NewErrorToast("Failed to redo message")
return toast.NewErrorToast("Failed to redo message")()
}
return app.MessageRevertedMsg{Session: *response, Message: revertedMessage}
}

View file

@ -134,7 +134,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
)
if err != nil {
slog.Error("Failed to respond to permission request", "error", err)
return toast.NewErrorToast("Failed to respond to permission request")
return toast.NewErrorToast("Failed to respond to permission request")()
}
slog.Debug("Responded to permission request", "response", resp)
return nil
@ -915,9 +915,10 @@ func (a Model) Cleanup() {
func (a Model) home() (string, int, int) {
t := theme.CurrentTheme()
effectiveWidth := a.width - 4
baseStyle := styles.NewStyle().Background(t.Background())
baseStyle := styles.NewStyle().Foreground(t.Text()).Background(t.Background())
base := baseStyle.Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
highlight := styles.NewStyle().Foreground(t.Accent()).Background(t.Background()).Render
open := `
@ -952,9 +953,9 @@ func (a Model) home() (string, int, int) {
)
// Use limit of 4 for vscode, 6 for others
limit := 6
limit := 4
if util.IsVSCode() {
limit = 4
limit = 2
}
showVscode := util.IsVSCode()
@ -971,15 +972,23 @@ func (a Model) home() (string, int, int) {
styles.WhitespaceStyle(t.Background()),
)
grok := highlight("Grok Code is free for a limited time")
grok = lipgloss.PlaceHorizontal(
effectiveWidth,
lipgloss.Center,
grok,
styles.WhitespaceStyle(t.Background()),
)
lines := []string{}
lines = append(lines, "")
lines = append(lines, "")
lines = append(lines, logoAndVersion)
lines = append(lines, "")
lines = append(lines, "")
lines = append(lines, cmds)
lines = append(lines, "")
lines = append(lines, "")
lines = append(lines, grok)
lines = append(lines, "")
mainHeight := lipgloss.Height(strings.Join(lines, "\n"))

View file

@ -11,7 +11,10 @@ import (
"github.com/sst/opencode/internal/styles"
)
var shimmerStart = time.Now()
var (
shimmerStart = time.Now()
trueColorSupport = hasTrueColor()
)
// Shimmer renders text with a moving foreground highlight.
// bg is the background color, dim is the base text color, bright is the highlight color.
@ -32,7 +35,7 @@ func Shimmer(s string, bg compat.AdaptiveColor, _ compat.AdaptiveColor, _ compat
elapsed := time.Since(shimmerStart).Seconds()
pos := (math.Mod(elapsed, sweep) / sweep) * period
half := 4.0
half := 2.0
type seg struct {
useHex bool
@ -41,60 +44,52 @@ func Shimmer(s string, bg compat.AdaptiveColor, _ compat.AdaptiveColor, _ compat
faint bool
text string
}
var segs []seg
segs := make([]seg, 0, n/4)
useHex := hasTrueColor()
useHex := trueColorSupport
for i, r := range runes {
ip := float64(i + pad)
dist := math.Abs(ip - pos)
t := 0.0
if dist <= half {
x := math.Pi * (dist / half)
t = 0.5 * (1.0 + math.Cos(x))
}
// Cosine brightness: base + amp*t (quantized for grouping)
base := 0.55
amp := 0.45
brightness := base
if t > 0 {
brightness = base + amp*t
}
lvl := int(math.Round(brightness * 255.0))
if !useHex {
step := 24 // ~11 steps across range for non-truecolor
lvl = int(math.Round(float64(lvl)/float64(step))) * step
}
bold := lvl >= 208
faint := lvl <= 128
// truecolor if possible; else fallback to modifiers only
bold := false
faint := true
hex := ""
if useHex {
if lvl < 0 {
lvl = 0
if dist <= half {
// Simple 3-level brightness based on distance
if dist <= half/3 {
// Center: brightest
bold = true
faint = false
if useHex {
hex = "#ffffff"
}
} else {
// Edge: medium bright
bold = false
faint = false
if useHex {
hex = "#cccccc"
}
}
if lvl > 255 {
lvl = 255
}
hex = rgbHex(lvl, lvl, lvl)
}
if len(segs) == 0 {
if len(segs) == 0 ||
segs[len(segs)-1].useHex != useHex ||
segs[len(segs)-1].hex != hex ||
segs[len(segs)-1].bold != bold ||
segs[len(segs)-1].faint != faint {
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
} else {
last := &segs[len(segs)-1]
if last.useHex == useHex && last.hex == hex && last.bold == bold && last.faint == faint {
last.text += string(r)
} else {
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
}
segs[len(segs)-1].text += string(r)
}
}
baseStyle := styles.NewStyle().Background(bg)
var b strings.Builder
b.Grow(len(s) * 2)
for _, g := range segs {
st := styles.NewStyle().Background(bg)
st := baseStyle
if g.useHex && g.hex != "" {
c := compat.AdaptiveColor{Dark: lipgloss.Color(g.hex), Light: lipgloss.Color(g.hex)}
st = st.Foreground(c)