mirror of
https://github.com/sst/opencode.git
synced 2025-08-25 07:24:05 +00:00
Merge pull request #22 from adamdottv/adam/retries
fix(anthropic): better 429/529 handling
This commit is contained in:
commit
fde04bbf85
10 changed files with 312 additions and 157 deletions
10
cmd/root.go
10
cmd/root.go
|
@ -107,6 +107,16 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
sub := app.Status.Subscribe(ctx)
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
for ev := range sub {
|
||||||
|
ch <- ev
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
return ch, func() {
|
return ch, func() {
|
||||||
cancel()
|
cancel()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
|
@ -11,7 +11,9 @@ import (
|
||||||
"github.com/kujtimiihoxha/termai/internal/lsp/watcher"
|
"github.com/kujtimiihoxha/termai/internal/lsp/watcher"
|
||||||
"github.com/kujtimiihoxha/termai/internal/message"
|
"github.com/kujtimiihoxha/termai/internal/message"
|
||||||
"github.com/kujtimiihoxha/termai/internal/permission"
|
"github.com/kujtimiihoxha/termai/internal/permission"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||||
"github.com/kujtimiihoxha/termai/internal/session"
|
"github.com/kujtimiihoxha/termai/internal/session"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/tui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
|
@ -25,6 +27,7 @@ type App struct {
|
||||||
|
|
||||||
Logger logging.Interface
|
Logger logging.Interface
|
||||||
|
|
||||||
|
Status *pubsub.Broker[util.InfoMsg]
|
||||||
ceanups []func()
|
ceanups []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +46,7 @@ func New(ctx context.Context, conn *sql.DB) *App {
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
Permissions: permission.NewPermissionService(),
|
Permissions: permission.NewPermissionService(),
|
||||||
Logger: log,
|
Logger: log,
|
||||||
|
Status: pubsub.NewBroker[util.InfoMsg](),
|
||||||
LSPClients: make(map[string]*lsp.Client),
|
LSPClients: make(map[string]*lsp.Client),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ import (
|
||||||
"github.com/kujtimiihoxha/termai/internal/llm/provider"
|
"github.com/kujtimiihoxha/termai/internal/llm/provider"
|
||||||
"github.com/kujtimiihoxha/termai/internal/llm/tools"
|
"github.com/kujtimiihoxha/termai/internal/llm/tools"
|
||||||
"github.com/kujtimiihoxha/termai/internal/message"
|
"github.com/kujtimiihoxha/termai/internal/message"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/tui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Agent interface {
|
type Agent interface {
|
||||||
|
@ -92,9 +94,24 @@ func (c *agent) processEvent(
|
||||||
assistantMsg.AppendContent(event.Content)
|
assistantMsg.AppendContent(event.Content)
|
||||||
return c.Messages.Update(*assistantMsg)
|
return c.Messages.Update(*assistantMsg)
|
||||||
case provider.EventError:
|
case provider.EventError:
|
||||||
|
// TODO: remove when realease
|
||||||
log.Println("error", event.Error)
|
log.Println("error", event.Error)
|
||||||
|
c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
|
||||||
|
Type: util.InfoTypeError,
|
||||||
|
Msg: event.Error.Error(),
|
||||||
|
})
|
||||||
return event.Error
|
return event.Error
|
||||||
|
case provider.EventWarning:
|
||||||
|
c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
|
||||||
|
Type: util.InfoTypeWarn,
|
||||||
|
Msg: event.Info,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
case provider.EventInfo:
|
||||||
|
c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
|
||||||
|
Type: util.InfoTypeInfo,
|
||||||
|
Msg: event.Info,
|
||||||
|
})
|
||||||
case provider.EventComplete:
|
case provider.EventComplete:
|
||||||
assistantMsg.SetToolCalls(event.Response.ToolCalls)
|
assistantMsg.SetToolCalls(event.Response.ToolCalls)
|
||||||
assistantMsg.AddFinish(event.Response.FinishReason)
|
assistantMsg.AddFinish(event.Response.FinishReason)
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/anthropics/anthropic-sdk-go"
|
"github.com/anthropics/anthropic-sdk-go"
|
||||||
"github.com/anthropics/anthropic-sdk-go/option"
|
"github.com/anthropics/anthropic-sdk-go/option"
|
||||||
|
@ -68,21 +70,24 @@ func (a *anthropicProvider) SendMessages(ctx context.Context, messages []message
|
||||||
anthropicMessages := a.convertToAnthropicMessages(messages)
|
anthropicMessages := a.convertToAnthropicMessages(messages)
|
||||||
anthropicTools := a.convertToAnthropicTools(tools)
|
anthropicTools := a.convertToAnthropicTools(tools)
|
||||||
|
|
||||||
response, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{
|
response, err := a.client.Messages.New(
|
||||||
Model: anthropic.Model(a.model.APIModel),
|
ctx,
|
||||||
MaxTokens: a.maxTokens,
|
anthropic.MessageNewParams{
|
||||||
Temperature: anthropic.Float(0),
|
Model: anthropic.Model(a.model.APIModel),
|
||||||
Messages: anthropicMessages,
|
MaxTokens: a.maxTokens,
|
||||||
Tools: anthropicTools,
|
Temperature: anthropic.Float(0),
|
||||||
System: []anthropic.TextBlockParam{
|
Messages: anthropicMessages,
|
||||||
{
|
Tools: anthropicTools,
|
||||||
Text: a.systemMessage,
|
System: []anthropic.TextBlockParam{
|
||||||
CacheControl: anthropic.CacheControlEphemeralParam{
|
{
|
||||||
Type: "ephemeral",
|
Text: a.systemMessage,
|
||||||
|
CacheControl: anthropic.CacheControlEphemeralParam{
|
||||||
|
Type: "ephemeral",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -121,84 +126,172 @@ func (a *anthropicProvider) StreamResponse(ctx context.Context, messages []messa
|
||||||
temperature = anthropic.Float(1)
|
temperature = anthropic.Float(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
stream := a.client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{
|
|
||||||
Model: anthropic.Model(a.model.APIModel),
|
|
||||||
MaxTokens: a.maxTokens,
|
|
||||||
Temperature: temperature,
|
|
||||||
Messages: anthropicMessages,
|
|
||||||
Tools: anthropicTools,
|
|
||||||
Thinking: thinkingParam,
|
|
||||||
System: []anthropic.TextBlockParam{
|
|
||||||
{
|
|
||||||
Text: a.systemMessage,
|
|
||||||
CacheControl: anthropic.CacheControlEphemeralParam{
|
|
||||||
Type: "ephemeral",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
eventChan := make(chan ProviderEvent)
|
eventChan := make(chan ProviderEvent)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(eventChan)
|
defer close(eventChan)
|
||||||
|
|
||||||
accumulatedMessage := anthropic.Message{}
|
const maxRetries = 8
|
||||||
|
attempts := 0
|
||||||
|
|
||||||
for stream.Next() {
|
for {
|
||||||
event := stream.Current()
|
// If this isn't the first attempt, we're retrying
|
||||||
err := accumulatedMessage.Accumulate(event)
|
if attempts > 0 {
|
||||||
if err != nil {
|
if attempts > maxRetries {
|
||||||
eventChan <- ProviderEvent{Type: EventError, Error: err}
|
eventChan <- ProviderEvent{
|
||||||
|
Type: EventError,
|
||||||
|
Error: errors.New("maximum retry attempts reached for rate limit (429)"),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform user we're retrying with attempt number
|
||||||
|
eventChan <- ProviderEvent{
|
||||||
|
Type: EventWarning,
|
||||||
|
Info: fmt.Sprintf("[Retrying due to rate limit... attempt %d of %d]", attempts, maxRetries),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate backoff with exponential backoff and jitter
|
||||||
|
backoffMs := 2000 * (1 << (attempts - 1)) // 2s, 4s, 8s, 16s, 32s
|
||||||
|
jitterMs := int(float64(backoffMs) * 0.2)
|
||||||
|
totalBackoffMs := backoffMs + jitterMs
|
||||||
|
|
||||||
|
// Sleep with backoff, respecting context cancellation
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
eventChan <- ProviderEvent{Type: EventError, Error: ctx.Err()}
|
||||||
|
return
|
||||||
|
case <-time.After(time.Duration(totalBackoffMs) * time.Millisecond):
|
||||||
|
// Continue with retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++
|
||||||
|
|
||||||
|
// Create new streaming request
|
||||||
|
stream := a.client.Messages.NewStreaming(
|
||||||
|
ctx,
|
||||||
|
anthropic.MessageNewParams{
|
||||||
|
Model: anthropic.Model(a.model.APIModel),
|
||||||
|
MaxTokens: a.maxTokens,
|
||||||
|
Temperature: temperature,
|
||||||
|
Messages: anthropicMessages,
|
||||||
|
Tools: anthropicTools,
|
||||||
|
Thinking: thinkingParam,
|
||||||
|
System: []anthropic.TextBlockParam{
|
||||||
|
{
|
||||||
|
Text: a.systemMessage,
|
||||||
|
CacheControl: anthropic.CacheControlEphemeralParam{
|
||||||
|
Type: "ephemeral",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Process stream events
|
||||||
|
accumulatedMessage := anthropic.Message{}
|
||||||
|
streamSuccess := false
|
||||||
|
|
||||||
|
// Process the stream until completion or error
|
||||||
|
for stream.Next() {
|
||||||
|
event := stream.Current()
|
||||||
|
err := accumulatedMessage.Accumulate(event)
|
||||||
|
if err != nil {
|
||||||
|
eventChan <- ProviderEvent{Type: EventError, Error: err}
|
||||||
|
return // Don't retry on accumulation errors
|
||||||
|
}
|
||||||
|
|
||||||
|
switch event := event.AsAny().(type) {
|
||||||
|
case anthropic.ContentBlockStartEvent:
|
||||||
|
eventChan <- ProviderEvent{Type: EventContentStart}
|
||||||
|
|
||||||
|
case anthropic.ContentBlockDeltaEvent:
|
||||||
|
if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
|
||||||
|
eventChan <- ProviderEvent{
|
||||||
|
Type: EventThinkingDelta,
|
||||||
|
Thinking: event.Delta.Thinking,
|
||||||
|
}
|
||||||
|
} else if event.Delta.Type == "text_delta" && event.Delta.Text != "" {
|
||||||
|
eventChan <- ProviderEvent{
|
||||||
|
Type: EventContentDelta,
|
||||||
|
Content: event.Delta.Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case anthropic.ContentBlockStopEvent:
|
||||||
|
eventChan <- ProviderEvent{Type: EventContentStop}
|
||||||
|
|
||||||
|
case anthropic.MessageStopEvent:
|
||||||
|
streamSuccess = true
|
||||||
|
content := ""
|
||||||
|
for _, block := range accumulatedMessage.Content {
|
||||||
|
if text, ok := block.AsAny().(anthropic.TextBlock); ok {
|
||||||
|
content += text.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toolCalls := a.extractToolCalls(accumulatedMessage.Content)
|
||||||
|
tokenUsage := a.extractTokenUsage(accumulatedMessage.Usage)
|
||||||
|
|
||||||
|
eventChan <- ProviderEvent{
|
||||||
|
Type: EventComplete,
|
||||||
|
Response: &ProviderResponse{
|
||||||
|
Content: content,
|
||||||
|
ToolCalls: toolCalls,
|
||||||
|
Usage: tokenUsage,
|
||||||
|
FinishReason: string(accumulatedMessage.StopReason),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the stream completed successfully, we're done
|
||||||
|
if streamSuccess {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch event := event.AsAny().(type) {
|
// Check for stream errors
|
||||||
case anthropic.ContentBlockStartEvent:
|
err := stream.Err()
|
||||||
eventChan <- ProviderEvent{Type: EventContentStart}
|
if err != nil {
|
||||||
|
var apierr *anthropic.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
if apierr.StatusCode == 429 || apierr.StatusCode == 529 {
|
||||||
|
// Check for Retry-After header
|
||||||
|
if retryAfterValues := apierr.Response.Header.Values("Retry-After"); len(retryAfterValues) > 0 {
|
||||||
|
// Parse the retry after value (seconds)
|
||||||
|
var retryAfterSec int
|
||||||
|
if _, err := fmt.Sscanf(retryAfterValues[0], "%d", &retryAfterSec); err == nil {
|
||||||
|
retryMs := retryAfterSec * 1000
|
||||||
|
|
||||||
case anthropic.ContentBlockDeltaEvent:
|
// Inform user of retry with specific wait time
|
||||||
if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
|
eventChan <- ProviderEvent{
|
||||||
eventChan <- ProviderEvent{
|
Type: EventWarning,
|
||||||
Type: EventThinkingDelta,
|
Info: fmt.Sprintf("[Rate limited: waiting %d seconds as specified by API]", retryAfterSec),
|
||||||
Thinking: event.Delta.Thinking,
|
}
|
||||||
}
|
|
||||||
} else if event.Delta.Type == "text_delta" && event.Delta.Text != "" {
|
// Sleep respecting context cancellation
|
||||||
eventChan <- ProviderEvent{
|
select {
|
||||||
Type: EventContentDelta,
|
case <-ctx.Done():
|
||||||
Content: event.Delta.Text,
|
eventChan <- ProviderEvent{Type: EventError, Error: ctx.Err()}
|
||||||
|
return
|
||||||
|
case <-time.After(time.Duration(retryMs) * time.Millisecond):
|
||||||
|
// Continue with retry after specified delay
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to exponential backoff if Retry-After parsing failed
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case anthropic.ContentBlockStopEvent:
|
// For non-rate limit errors, report and exit
|
||||||
eventChan <- ProviderEvent{Type: EventContentStop}
|
eventChan <- ProviderEvent{Type: EventError, Error: err}
|
||||||
|
return
|
||||||
case anthropic.MessageStopEvent:
|
|
||||||
content := ""
|
|
||||||
for _, block := range accumulatedMessage.Content {
|
|
||||||
if text, ok := block.AsAny().(anthropic.TextBlock); ok {
|
|
||||||
content += text.Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toolCalls := a.extractToolCalls(accumulatedMessage.Content)
|
|
||||||
tokenUsage := a.extractTokenUsage(accumulatedMessage.Usage)
|
|
||||||
|
|
||||||
eventChan <- ProviderEvent{
|
|
||||||
Type: EventComplete,
|
|
||||||
Response: &ProviderResponse{
|
|
||||||
Content: content,
|
|
||||||
ToolCalls: toolCalls,
|
|
||||||
Usage: tokenUsage,
|
|
||||||
FinishReason: string(accumulatedMessage.StopReason),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.Err() != nil {
|
|
||||||
eventChan <- ProviderEvent{Type: EventError, Error: stream.Err()}
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return eventChan, nil
|
return eventChan, nil
|
||||||
|
@ -311,3 +404,4 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
|
||||||
|
|
||||||
return anthropicMessages
|
return anthropicMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ const (
|
||||||
EventContentStop EventType = "content_stop"
|
EventContentStop EventType = "content_stop"
|
||||||
EventComplete EventType = "complete"
|
EventComplete EventType = "complete"
|
||||||
EventError EventType = "error"
|
EventError EventType = "error"
|
||||||
|
EventWarning EventType = "warning"
|
||||||
|
EventInfo EventType = "info"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenUsage struct {
|
type TokenUsage struct {
|
||||||
|
@ -40,6 +42,9 @@ type ProviderEvent struct {
|
||||||
ToolCall *message.ToolCall
|
ToolCall *message.ToolCall
|
||||||
Error error
|
Error error
|
||||||
Response *ProviderResponse
|
Response *ProviderResponse
|
||||||
|
|
||||||
|
// Used for giving users info on e.x retry
|
||||||
|
Info string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
|
|
|
@ -7,16 +7,16 @@ import (
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/kujtimiihoxha/termai/internal/config"
|
"github.com/kujtimiihoxha/termai/internal/config"
|
||||||
"github.com/kujtimiihoxha/termai/internal/llm/models"
|
"github.com/kujtimiihoxha/termai/internal/llm/models"
|
||||||
|
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||||
"github.com/kujtimiihoxha/termai/internal/tui/styles"
|
"github.com/kujtimiihoxha/termai/internal/tui/styles"
|
||||||
"github.com/kujtimiihoxha/termai/internal/tui/util"
|
"github.com/kujtimiihoxha/termai/internal/tui/util"
|
||||||
"github.com/kujtimiihoxha/termai/internal/version"
|
"github.com/kujtimiihoxha/termai/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type statusCmp struct {
|
type statusCmp struct {
|
||||||
err error
|
info *util.InfoMsg
|
||||||
info string
|
width int
|
||||||
width int
|
messageTTL time.Duration
|
||||||
messageTTL time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearMessageCmd is a command that clears status messages after a timeout
|
// clearMessageCmd is a command that clears status messages after a timeout
|
||||||
|
@ -34,17 +34,15 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
case util.ErrorMsg:
|
return m, m.clearMessageCmd()
|
||||||
m.err = msg
|
case pubsub.Event[util.InfoMsg]:
|
||||||
m.info = ""
|
m.info = &msg.Payload
|
||||||
return m, m.clearMessageCmd()
|
return m, m.clearMessageCmd()
|
||||||
case util.InfoMsg:
|
case util.InfoMsg:
|
||||||
m.info = string(msg)
|
m.info = &msg
|
||||||
m.err = nil
|
|
||||||
return m, m.clearMessageCmd()
|
return m, m.clearMessageCmd()
|
||||||
case util.ClearStatusMsg:
|
case util.ClearStatusMsg:
|
||||||
m.info = ""
|
m.info = nil
|
||||||
m.err = nil
|
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
@ -56,25 +54,25 @@ var (
|
||||||
|
|
||||||
func (m statusCmp) View() string {
|
func (m statusCmp) View() string {
|
||||||
status := styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render("? help")
|
status := styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render("? help")
|
||||||
|
if m.info != nil {
|
||||||
if m.err != nil {
|
infoStyle := styles.Padded.
|
||||||
status += styles.Regular.Padding(0, 1).
|
|
||||||
Background(styles.Red).
|
|
||||||
Foreground(styles.Text).
|
|
||||||
Width(m.availableFooterMsgWidth()).
|
|
||||||
Render(m.err.Error())
|
|
||||||
} else if m.info != "" {
|
|
||||||
status += styles.Padded.
|
|
||||||
Foreground(styles.Base).
|
Foreground(styles.Base).
|
||||||
Background(styles.Green).
|
Width(m.availableFooterMsgWidth())
|
||||||
Width(m.availableFooterMsgWidth()).
|
switch m.info.Type {
|
||||||
Render(m.info)
|
case util.InfoTypeInfo:
|
||||||
|
infoStyle = infoStyle.Background(styles.Blue)
|
||||||
|
case util.InfoTypeWarn:
|
||||||
|
infoStyle = infoStyle.Background(styles.Peach)
|
||||||
|
case util.InfoTypeError:
|
||||||
|
infoStyle = infoStyle.Background(styles.Red)
|
||||||
|
}
|
||||||
|
status += infoStyle.Render(m.info.Msg)
|
||||||
} else {
|
} else {
|
||||||
status += styles.Padded.
|
status += styles.Padded.
|
||||||
Foreground(styles.Base).
|
Foreground(styles.Base).
|
||||||
Background(styles.LightGrey).
|
Background(styles.LightGrey).
|
||||||
Width(m.availableFooterMsgWidth()).
|
Width(m.availableFooterMsgWidth()).
|
||||||
Render(m.info)
|
Render("")
|
||||||
}
|
}
|
||||||
status += m.model()
|
status += m.model()
|
||||||
status += versionWidget
|
status += versionWidget
|
||||||
|
@ -93,6 +91,6 @@ func (m statusCmp) model() string {
|
||||||
|
|
||||||
func NewStatusCmp() tea.Model {
|
func NewStatusCmp() tea.Model {
|
||||||
return &statusCmp{
|
return &statusCmp{
|
||||||
messageTTL: 5 * time.Second,
|
messageTTL: 15 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,13 +69,13 @@ type permissionDialogCmp struct {
|
||||||
func formatDiff(diffText string) string {
|
func formatDiff(diffText string) string {
|
||||||
lines := strings.Split(diffText, "\n")
|
lines := strings.Split(diffText, "\n")
|
||||||
var formattedLines []string
|
var formattedLines []string
|
||||||
|
|
||||||
// Define styles for different line types
|
// Define styles for different line types
|
||||||
addStyle := lipgloss.NewStyle().Foreground(styles.Green)
|
addStyle := lipgloss.NewStyle().Foreground(styles.Green)
|
||||||
removeStyle := lipgloss.NewStyle().Foreground(styles.Red)
|
removeStyle := lipgloss.NewStyle().Foreground(styles.Red)
|
||||||
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Blue)
|
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Blue)
|
||||||
contextStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
|
contextStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
|
||||||
|
|
||||||
// Process each line
|
// Process each line
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.HasPrefix(line, "+") {
|
if strings.HasPrefix(line, "+") {
|
||||||
|
@ -90,7 +90,7 @@ func formatDiff(diffText string) string {
|
||||||
formattedLines = append(formattedLines, line)
|
formattedLines = append(formattedLines, line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join all formatted lines
|
// Join all formatted lines
|
||||||
return strings.Join(formattedLines, "\n")
|
return strings.Join(formattedLines, "\n")
|
||||||
}
|
}
|
||||||
|
@ -112,13 +112,13 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
p.selectOption.Blur()
|
p.selectOption.Blur()
|
||||||
// Add a visual indicator for focus change
|
// Add a visual indicator for focus change
|
||||||
cmds = append(cmds, tea.Batch(
|
cmds = append(cmds, tea.Batch(
|
||||||
util.CmdHandler(util.InfoMsg("Viewing content - use arrow keys to scroll")),
|
util.ReportInfo("Viewing content - use arrow keys to scroll"),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
p.selectOption.Focus()
|
p.selectOption.Focus()
|
||||||
// Add a visual indicator for focus change
|
// Add a visual indicator for focus change
|
||||||
cmds = append(cmds, tea.Batch(
|
cmds = append(cmds, tea.Batch(
|
||||||
util.CmdHandler(util.InfoMsg("Select an action")),
|
util.CmdHandler(util.ReportInfo("Select an action")),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return p, tea.Batch(cmds...)
|
return p, tea.Batch(cmds...)
|
||||||
|
@ -162,44 +162,44 @@ func (p *permissionDialogCmp) render() string {
|
||||||
lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
|
lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
|
||||||
" ",
|
" ",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the header content first so it can be used in all cases
|
// Create the header content first so it can be used in all cases
|
||||||
headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
||||||
|
|
||||||
r, _ := glamour.NewTermRenderer(
|
r, _ := glamour.NewTermRenderer(
|
||||||
glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
|
glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
|
||||||
glamour.WithWordWrap(p.width-10),
|
glamour.WithWordWrap(p.width-10),
|
||||||
glamour.WithEmoji(),
|
glamour.WithEmoji(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle different tool types
|
// Handle different tool types
|
||||||
switch p.permission.ToolName {
|
switch p.permission.ToolName {
|
||||||
case tools.BashToolName:
|
case tools.BashToolName:
|
||||||
pr := p.permission.Params.(tools.BashPermissionsParams)
|
pr := p.permission.Params.(tools.BashPermissionsParams)
|
||||||
headerParts = append(headerParts, keyStyle.Render("Command:"))
|
headerParts = append(headerParts, keyStyle.Render("Command:"))
|
||||||
content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
|
content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
|
||||||
|
|
||||||
renderedContent, _ := r.Render(content)
|
renderedContent, _ := r.Render(content)
|
||||||
p.contentViewPort.Width = p.width - 2 - 2
|
p.contentViewPort.Width = p.width - 2 - 2
|
||||||
|
|
||||||
// Calculate content height dynamically based on content
|
// Calculate content height dynamically based on content
|
||||||
contentLines := len(strings.Split(renderedContent, "\n"))
|
contentLines := len(strings.Split(renderedContent, "\n"))
|
||||||
// Set a reasonable min/max for the viewport height
|
// Set a reasonable min/max for the viewport height
|
||||||
minContentHeight := 3
|
minContentHeight := 3
|
||||||
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
||||||
|
|
||||||
// Add some padding to the content lines
|
// Add some padding to the content lines
|
||||||
contentHeight := contentLines + 2
|
contentHeight := contentLines + 2
|
||||||
contentHeight = max(contentHeight, minContentHeight)
|
contentHeight = max(contentHeight, minContentHeight)
|
||||||
contentHeight = min(contentHeight, maxContentHeight)
|
contentHeight = min(contentHeight, maxContentHeight)
|
||||||
p.contentViewPort.Height = contentHeight
|
p.contentViewPort.Height = contentHeight
|
||||||
|
|
||||||
p.contentViewPort.SetContent(renderedContent)
|
p.contentViewPort.SetContent(renderedContent)
|
||||||
|
|
||||||
// Style the viewport
|
// Style the viewport
|
||||||
var contentBorder lipgloss.Border
|
var contentBorder lipgloss.Border
|
||||||
var borderColor lipgloss.TerminalColor
|
var borderColor lipgloss.TerminalColor
|
||||||
|
|
||||||
if p.isViewportFocus {
|
if p.isViewportFocus {
|
||||||
contentBorder = lipgloss.DoubleBorder()
|
contentBorder = lipgloss.DoubleBorder()
|
||||||
borderColor = styles.Blue
|
borderColor = styles.Blue
|
||||||
|
@ -207,47 +207,47 @@ func (p *permissionDialogCmp) render() string {
|
||||||
contentBorder = lipgloss.RoundedBorder()
|
contentBorder = lipgloss.RoundedBorder()
|
||||||
borderColor = styles.Flamingo
|
borderColor = styles.Flamingo
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStyle := lipgloss.NewStyle().
|
contentStyle := lipgloss.NewStyle().
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Border(contentBorder).
|
Border(contentBorder).
|
||||||
BorderForeground(borderColor)
|
BorderForeground(borderColor)
|
||||||
|
|
||||||
if p.isViewportFocus {
|
if p.isViewportFocus {
|
||||||
contentStyle = contentStyle.BorderBackground(styles.Surface0)
|
contentStyle = contentStyle.BorderBackground(styles.Surface0)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
||||||
|
|
||||||
return lipgloss.JoinVertical(
|
return lipgloss.JoinVertical(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
headerContent,
|
headerContent,
|
||||||
contentFinal,
|
contentFinal,
|
||||||
form,
|
form,
|
||||||
)
|
)
|
||||||
|
|
||||||
case tools.EditToolName:
|
case tools.EditToolName:
|
||||||
pr := p.permission.Params.(tools.EditPermissionsParams)
|
pr := p.permission.Params.(tools.EditPermissionsParams)
|
||||||
headerParts = append(headerParts, keyStyle.Render("Update"))
|
headerParts = append(headerParts, keyStyle.Render("Update"))
|
||||||
// Recreate header content with the updated headerParts
|
// Recreate header content with the updated headerParts
|
||||||
headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
||||||
|
|
||||||
// Format the diff with colors
|
// Format the diff with colors
|
||||||
formattedDiff := formatDiff(pr.Diff)
|
formattedDiff := formatDiff(pr.Diff)
|
||||||
|
|
||||||
// Set up viewport for the diff content
|
// Set up viewport for the diff content
|
||||||
p.contentViewPort.Width = p.width - 2 - 2
|
p.contentViewPort.Width = p.width - 2 - 2
|
||||||
|
|
||||||
// Calculate content height dynamically based on window size
|
// Calculate content height dynamically based on window size
|
||||||
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
||||||
p.contentViewPort.Height = maxContentHeight
|
p.contentViewPort.Height = maxContentHeight
|
||||||
p.contentViewPort.SetContent(formattedDiff)
|
p.contentViewPort.SetContent(formattedDiff)
|
||||||
|
|
||||||
// Style the viewport
|
// Style the viewport
|
||||||
var contentBorder lipgloss.Border
|
var contentBorder lipgloss.Border
|
||||||
var borderColor lipgloss.TerminalColor
|
var borderColor lipgloss.TerminalColor
|
||||||
|
|
||||||
if p.isViewportFocus {
|
if p.isViewportFocus {
|
||||||
contentBorder = lipgloss.DoubleBorder()
|
contentBorder = lipgloss.DoubleBorder()
|
||||||
borderColor = styles.Blue
|
borderColor = styles.Blue
|
||||||
|
@ -255,47 +255,47 @@ func (p *permissionDialogCmp) render() string {
|
||||||
contentBorder = lipgloss.RoundedBorder()
|
contentBorder = lipgloss.RoundedBorder()
|
||||||
borderColor = styles.Flamingo
|
borderColor = styles.Flamingo
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStyle := lipgloss.NewStyle().
|
contentStyle := lipgloss.NewStyle().
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Border(contentBorder).
|
Border(contentBorder).
|
||||||
BorderForeground(borderColor)
|
BorderForeground(borderColor)
|
||||||
|
|
||||||
if p.isViewportFocus {
|
if p.isViewportFocus {
|
||||||
contentStyle = contentStyle.BorderBackground(styles.Surface0)
|
contentStyle = contentStyle.BorderBackground(styles.Surface0)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
||||||
|
|
||||||
return lipgloss.JoinVertical(
|
return lipgloss.JoinVertical(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
headerContent,
|
headerContent,
|
||||||
contentFinal,
|
contentFinal,
|
||||||
form,
|
form,
|
||||||
)
|
)
|
||||||
|
|
||||||
case tools.WriteToolName:
|
case tools.WriteToolName:
|
||||||
pr := p.permission.Params.(tools.WritePermissionsParams)
|
pr := p.permission.Params.(tools.WritePermissionsParams)
|
||||||
headerParts = append(headerParts, keyStyle.Render("Content"))
|
headerParts = append(headerParts, keyStyle.Render("Content"))
|
||||||
// Recreate header content with the updated headerParts
|
// Recreate header content with the updated headerParts
|
||||||
headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
||||||
|
|
||||||
// Format the diff with colors
|
// Format the diff with colors
|
||||||
formattedDiff := formatDiff(pr.Content)
|
formattedDiff := formatDiff(pr.Content)
|
||||||
|
|
||||||
// Set up viewport for the content
|
// Set up viewport for the content
|
||||||
p.contentViewPort.Width = p.width - 2 - 2
|
p.contentViewPort.Width = p.width - 2 - 2
|
||||||
|
|
||||||
// Calculate content height dynamically based on window size
|
// Calculate content height dynamically based on window size
|
||||||
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
||||||
p.contentViewPort.Height = maxContentHeight
|
p.contentViewPort.Height = maxContentHeight
|
||||||
p.contentViewPort.SetContent(formattedDiff)
|
p.contentViewPort.SetContent(formattedDiff)
|
||||||
|
|
||||||
// Style the viewport
|
// Style the viewport
|
||||||
var contentBorder lipgloss.Border
|
var contentBorder lipgloss.Border
|
||||||
var borderColor lipgloss.TerminalColor
|
var borderColor lipgloss.TerminalColor
|
||||||
|
|
||||||
if p.isViewportFocus {
|
if p.isViewportFocus {
|
||||||
contentBorder = lipgloss.DoubleBorder()
|
contentBorder = lipgloss.DoubleBorder()
|
||||||
borderColor = styles.Blue
|
borderColor = styles.Blue
|
||||||
|
@ -303,75 +303,75 @@ func (p *permissionDialogCmp) render() string {
|
||||||
contentBorder = lipgloss.RoundedBorder()
|
contentBorder = lipgloss.RoundedBorder()
|
||||||
borderColor = styles.Flamingo
|
borderColor = styles.Flamingo
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStyle := lipgloss.NewStyle().
|
contentStyle := lipgloss.NewStyle().
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Border(contentBorder).
|
Border(contentBorder).
|
||||||
BorderForeground(borderColor)
|
BorderForeground(borderColor)
|
||||||
|
|
||||||
if p.isViewportFocus {
|
if p.isViewportFocus {
|
||||||
contentStyle = contentStyle.BorderBackground(styles.Surface0)
|
contentStyle = contentStyle.BorderBackground(styles.Surface0)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
||||||
|
|
||||||
return lipgloss.JoinVertical(
|
return lipgloss.JoinVertical(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
headerContent,
|
headerContent,
|
||||||
contentFinal,
|
contentFinal,
|
||||||
form,
|
form,
|
||||||
)
|
)
|
||||||
|
|
||||||
case tools.FetchToolName:
|
case tools.FetchToolName:
|
||||||
pr := p.permission.Params.(tools.FetchPermissionsParams)
|
pr := p.permission.Params.(tools.FetchPermissionsParams)
|
||||||
headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
|
headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
|
||||||
content := p.permission.Description
|
content := p.permission.Description
|
||||||
|
|
||||||
renderedContent, _ := r.Render(content)
|
renderedContent, _ := r.Render(content)
|
||||||
p.contentViewPort.Width = p.width - 2 - 2
|
p.contentViewPort.Width = p.width - 2 - 2
|
||||||
p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
||||||
p.contentViewPort.SetContent(renderedContent)
|
p.contentViewPort.SetContent(renderedContent)
|
||||||
|
|
||||||
// Style the viewport
|
// Style the viewport
|
||||||
contentStyle := lipgloss.NewStyle().
|
contentStyle := lipgloss.NewStyle().
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(styles.Flamingo)
|
BorderForeground(styles.Flamingo)
|
||||||
|
|
||||||
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
||||||
if renderedContent == "" {
|
if renderedContent == "" {
|
||||||
contentFinal = ""
|
contentFinal = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return lipgloss.JoinVertical(
|
return lipgloss.JoinVertical(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
headerContent,
|
headerContent,
|
||||||
contentFinal,
|
contentFinal,
|
||||||
form,
|
form,
|
||||||
)
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
content := p.permission.Description
|
content := p.permission.Description
|
||||||
|
|
||||||
renderedContent, _ := r.Render(content)
|
renderedContent, _ := r.Render(content)
|
||||||
p.contentViewPort.Width = p.width - 2 - 2
|
p.contentViewPort.Width = p.width - 2 - 2
|
||||||
p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
|
||||||
p.contentViewPort.SetContent(renderedContent)
|
p.contentViewPort.SetContent(renderedContent)
|
||||||
|
|
||||||
// Style the viewport
|
// Style the viewport
|
||||||
contentStyle := lipgloss.NewStyle().
|
contentStyle := lipgloss.NewStyle().
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(styles.Flamingo)
|
BorderForeground(styles.Flamingo)
|
||||||
|
|
||||||
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
contentFinal := contentStyle.Render(p.contentViewPort.View())
|
||||||
if renderedContent == "" {
|
if renderedContent == "" {
|
||||||
contentFinal = ""
|
contentFinal = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return lipgloss.JoinVertical(
|
return lipgloss.JoinVertical(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
headerContent,
|
headerContent,
|
||||||
|
|
|
@ -140,7 +140,7 @@ func (m *editorCmp) Send() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
messages, _ := m.app.Messages.List(m.sessionID)
|
messages, _ := m.app.Messages.List(m.sessionID)
|
||||||
if hasUnfinishedMessages(messages) {
|
if hasUnfinishedMessages(messages) {
|
||||||
return util.InfoMsg("Assistant is still working on the previous message")
|
return util.ReportWarn("Assistant is still working on the previous message")
|
||||||
}
|
}
|
||||||
a, _ := agent.NewCoderAgent(m.app)
|
a, _ := agent.NewCoderAgent(m.app)
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case pubsub.Event[permission.PermissionRequest]:
|
case pubsub.Event[permission.PermissionRequest]:
|
||||||
return a, dialog.NewPermissionDialogCmd(msg.Payload)
|
return a, dialog.NewPermissionDialogCmd(msg.Payload)
|
||||||
|
case pubsub.Event[util.InfoMsg]:
|
||||||
|
a.status, _ = a.status.Update(msg)
|
||||||
case dialog.PermissionResponseMsg:
|
case dialog.PermissionResponseMsg:
|
||||||
switch msg.Action {
|
switch msg.Action {
|
||||||
case dialog.PermissionAllow:
|
case dialog.PermissionAllow:
|
||||||
|
@ -121,8 +123,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return a, a.moveToPage(msg.ID)
|
return a, a.moveToPage(msg.ID)
|
||||||
case util.InfoMsg:
|
case util.InfoMsg:
|
||||||
a.status, _ = a.status.Update(msg)
|
a.status, _ = a.status.Update(msg)
|
||||||
case util.ErrorMsg:
|
|
||||||
a.status, _ = a.status.Update(msg)
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if a.editorMode == vimtea.ModeNormal {
|
if a.editorMode == vimtea.ModeNormal {
|
||||||
switch {
|
switch {
|
||||||
|
@ -141,7 +141,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if a.currentPage == page.ReplPage {
|
if a.currentPage == page.ReplPage {
|
||||||
sessions, err := a.app.Sessions.List()
|
sessions, err := a.app.Sessions.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a, util.CmdHandler(util.ErrorMsg(err))
|
return a, util.CmdHandler(util.ReportError(err))
|
||||||
}
|
}
|
||||||
lastSession := sessions[0]
|
lastSession := sessions[0]
|
||||||
if lastSession.MessageCount == 0 {
|
if lastSession.MessageCount == 0 {
|
||||||
|
@ -149,7 +149,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
s, err := a.app.Sessions.Create("New Session")
|
s, err := a.app.Sessions.Create("New Session")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return a, util.CmdHandler(util.ErrorMsg(err))
|
return a, util.CmdHandler(util.ReportError(err))
|
||||||
}
|
}
|
||||||
return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: s.ID})
|
return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: s.ID})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,39 @@ func CmdHandler(msg tea.Msg) tea.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReportError(err error) tea.Cmd {
|
func ReportError(err error) tea.Cmd {
|
||||||
return CmdHandler(ErrorMsg(err))
|
return CmdHandler(InfoMsg{
|
||||||
|
Type: InfoTypeError,
|
||||||
|
Msg: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfoType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
InfoTypeInfo InfoType = iota
|
||||||
|
InfoTypeWarn
|
||||||
|
InfoTypeError
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReportInfo(info string) tea.Cmd {
|
||||||
|
return CmdHandler(InfoMsg{
|
||||||
|
Type: InfoTypeInfo,
|
||||||
|
Msg: info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReportWarn(warn string) tea.Cmd {
|
||||||
|
return CmdHandler(InfoMsg{
|
||||||
|
Type: InfoTypeWarn,
|
||||||
|
Msg: warn,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
InfoMsg string
|
InfoMsg struct {
|
||||||
ErrorMsg error
|
Type InfoType
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
ClearStatusMsg struct{}
|
ClearStatusMsg struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue