Context Window Warning (#152)

* context window warning & compact command

* auto compact

* fix permissions

* update readme

* fix 3.5 context window

* small update

* remove unused interface

* remove unused msg
This commit is contained in:
Kujtim Hoxha
2025-05-09 19:30:57 +02:00
committed by GitHub
parent 9345830c8a
commit 90084ce43d
12 changed files with 537 additions and 98 deletions

View File

@@ -21,7 +21,6 @@ import (
type StatusCmp interface {
tea.Model
SetHelpWidgetMsg(string)
}
type statusCmp struct {
@@ -74,11 +73,9 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var helpWidget = ""
// getHelpWidget returns the help widget with current theme colors
func getHelpWidget(helpText string) string {
func getHelpWidget() string {
t := theme.CurrentTheme()
if helpText == "" {
helpText = "ctrl+? help"
}
helpText := "ctrl+? help"
return styles.Padded().
Background(t.TextMuted()).
@@ -87,7 +84,7 @@ func getHelpWidget(helpText string) string {
Render(helpText)
}
func formatTokensAndCost(tokens int64, cost float64) string {
func formatTokensAndCost(tokens, contextWindow int64, cost float64) string {
// Format tokens in human-readable format (e.g., 110K, 1.2M)
var formattedTokens string
switch {
@@ -110,32 +107,48 @@ func formatTokensAndCost(tokens int64, cost float64) string {
// Format cost with $ symbol and 2 decimal places
formattedCost := fmt.Sprintf("$%.2f", cost)
return fmt.Sprintf("Tokens: %s, Cost: %s", formattedTokens, formattedCost)
percentage := (float64(tokens) / float64(contextWindow)) * 100
if percentage > 80 {
// add the warning icon and percentage
formattedTokens = fmt.Sprintf("%s(%d%%)", styles.WarningIcon, int(percentage))
}
return fmt.Sprintf("Context: %s, Cost: %s", formattedTokens, formattedCost)
}
func (m statusCmp) View() string {
t := theme.CurrentTheme()
modelID := config.Get().Agents[config.AgentCoder].Model
model := models.SupportedModels[modelID]
// Initialize the help widget
status := getHelpWidget("")
status := getHelpWidget()
tokenInfoWidth := 0
if m.session.ID != "" {
tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
totalTokens := m.session.PromptTokens + m.session.CompletionTokens
tokens := formatTokensAndCost(totalTokens, model.ContextWindow, m.session.Cost)
tokensStyle := styles.Padded().
Background(t.Text()).
Foreground(t.BackgroundSecondary()).
Render(tokens)
status += tokensStyle
Foreground(t.BackgroundSecondary())
percentage := (float64(totalTokens) / float64(model.ContextWindow)) * 100
if percentage > 80 {
tokensStyle = tokensStyle.Background(t.Warning())
}
tokenInfoWidth = lipgloss.Width(tokens) + 2
status += tokensStyle.Render(tokens)
}
diagnostics := styles.Padded().
Background(t.BackgroundDarker()).
Render(m.projectDiagnostics())
availableWidht := max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-tokenInfoWidth)
if m.info.Msg != "" {
infoStyle := styles.Padded().
Foreground(t.Background()).
Width(m.availableFooterMsgWidth(diagnostics))
Width(availableWidht)
switch m.info.Type {
case util.InfoTypeInfo:
@@ -146,18 +159,18 @@ func (m statusCmp) View() string {
infoStyle = infoStyle.Background(t.Error())
}
infoWidth := availableWidht - 10
// Truncate message if it's longer than available width
msg := m.info.Msg
availWidth := m.availableFooterMsgWidth(diagnostics) - 10
if len(msg) > availWidth && availWidth > 0 {
msg = msg[:availWidth] + "..."
if len(msg) > infoWidth && infoWidth > 0 {
msg = msg[:infoWidth] + "..."
}
status += infoStyle.Render(msg)
} else {
status += styles.Padded().
Foreground(t.Text()).
Background(t.BackgroundSecondary()).
Width(m.availableFooterMsgWidth(diagnostics)).
Width(availableWidht).
Render("")
}
@@ -245,12 +258,10 @@ func (m *statusCmp) projectDiagnostics() string {
return strings.Join(diagnostics, " ")
}
func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
tokens := ""
func (m statusCmp) availableFooterMsgWidth(diagnostics, tokenInfo string) int {
tokensWidth := 0
if m.session.ID != "" {
tokens = formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
tokensWidth = lipgloss.Width(tokens) + 2
tokensWidth = lipgloss.Width(tokenInfo) + 2
}
return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-tokensWidth)
}
@@ -272,14 +283,8 @@ func (m statusCmp) model() string {
Render(model.Name)
}
func (m statusCmp) SetHelpWidgetMsg(s string) {
// Update the help widget text using the getHelpWidget function
helpWidget = getHelpWidget(s)
}
func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
// Initialize the help widget with default text
helpWidget = getHelpWidget("")
helpWidget = getHelpWidget()
return &statusCmp{
messageTTL: 10 * time.Second,

View File

@@ -302,11 +302,8 @@ func (f *filepickerCmp) View() string {
}
if file.IsDir() {
filename = filename + "/"
} else if isExtSupported(file.Name()) {
filename = filename
} else {
filename = filename
}
// No need to reassign filename if it's not changing
files = append(files, itemStyle.Padding(0, 1).Render(filename))
}

View File

@@ -2,6 +2,8 @@ package dialog
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
@@ -13,7 +15,6 @@ import (
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
"strings"
)
type PermissionAction string
@@ -150,7 +151,7 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
func (p *permissionDialogCmp) renderButtons() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
allowStyle := baseStyle
allowSessionStyle := baseStyle
denyStyle := baseStyle
@@ -196,7 +197,7 @@ func (p *permissionDialogCmp) renderButtons() string {
func (p *permissionDialogCmp) renderHeader() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
toolValue := baseStyle.
Foreground(t.Text()).
@@ -229,9 +230,36 @@ func (p *permissionDialogCmp) renderHeader() string {
case tools.BashToolName:
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
case tools.EditToolName:
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
params := p.permission.Params.(tools.EditPermissionsParams)
fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File")
filePath := baseStyle.
Foreground(t.Text()).
Width(p.width - lipgloss.Width(fileKey)).
Render(fmt.Sprintf(": %s", params.FilePath))
headerParts = append(headerParts,
lipgloss.JoinHorizontal(
lipgloss.Left,
fileKey,
filePath,
),
baseStyle.Render(strings.Repeat(" ", p.width)),
)
case tools.WriteToolName:
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
params := p.permission.Params.(tools.WritePermissionsParams)
fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File")
filePath := baseStyle.
Foreground(t.Text()).
Width(p.width - lipgloss.Width(fileKey)).
Render(fmt.Sprintf(": %s", params.FilePath))
headerParts = append(headerParts,
lipgloss.JoinHorizontal(
lipgloss.Left,
fileKey,
filePath,
),
baseStyle.Render(strings.Repeat(" ", p.width)),
)
case tools.FetchToolName:
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
}
@@ -242,13 +270,13 @@ func (p *permissionDialogCmp) renderHeader() string {
func (p *permissionDialogCmp) renderBashContent() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r := styles.GetMarkdownRenderer(p.width-10)
r := styles.GetMarkdownRenderer(p.width - 10)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
})
@@ -302,13 +330,13 @@ func (p *permissionDialogCmp) renderWriteContent() string {
func (p *permissionDialogCmp) renderFetchContent() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r := styles.GetMarkdownRenderer(p.width-10)
r := styles.GetMarkdownRenderer(p.width - 10)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
})
@@ -325,12 +353,12 @@ func (p *permissionDialogCmp) renderFetchContent() string {
func (p *permissionDialogCmp) renderDefaultContent() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
content := p.permission.Description
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r := styles.GetMarkdownRenderer(p.width-10)
r := styles.GetMarkdownRenderer(p.width - 10)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
})
@@ -358,7 +386,7 @@ func (p *permissionDialogCmp) styleViewport() string {
func (p *permissionDialogCmp) render() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
title := baseStyle.
Bold(true).
Width(p.width - 4).