Skip to content

Commit 7a5016d

Browse files
committed
perf: extract modal info commands
1 parent fcf5e8c commit 7a5016d

9 files changed

Lines changed: 915 additions & 442 deletions

File tree

internal/ui/cmd_context.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package ui
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
tea "github.com/charmbracelet/bubbletea"
8+
"github.com/voocel/agentcore"
9+
"github.com/voocel/codebot/internal/agent"
10+
"github.com/voocel/codebot/internal/ui/tui"
11+
)
12+
13+
// ContextCommand implements InteractiveCommand for /context.
14+
// Opens a modal overlay with tabbed sections (usage / composition / suggestions).
15+
type ContextCommand struct {
16+
app *App
17+
state *contextState
18+
}
19+
20+
type contextState struct {
21+
active int
22+
23+
snapshot *agentcore.ContextSnapshot
24+
snapshotOK bool
25+
contextUsage *agentcore.ContextUsage
26+
breakdown agent.ContextBreakdown
27+
suggestions []agent.ContextSuggestion
28+
metrics agent.RuntimeMetricsSnapshot
29+
lastCompaction agent.CompactionSnapshot
30+
hasCompaction bool
31+
}
32+
33+
var contextTabs = []string{"usage", "composition", "suggestions"}
34+
35+
func NewContextCommand(app *App) *ContextCommand {
36+
return &ContextCommand{app: app}
37+
}
38+
39+
func (c *ContextCommand) Spec() CommandSpec {
40+
return CommandSpec{
41+
Name: "context",
42+
Usage: "/context",
43+
Description: "Show current context snapshot",
44+
Category: "info",
45+
Kind: CommandKindBuiltin,
46+
}
47+
}
48+
49+
func (c *ContextCommand) Run(ctx *CommandContext, _ CommandInvocation) tea.Cmd {
50+
snapshot, ok := ctx.App.Session.ContextSnapshot()
51+
lastCompaction, hasCompaction := ctx.App.Session.LastCompaction()
52+
c.state = &contextState{
53+
active: 0,
54+
snapshot: snapshot,
55+
snapshotOK: ok,
56+
contextUsage: ctx.App.Session.ContextUsage(),
57+
breakdown: ctx.App.Session.ContextBreakdown(),
58+
suggestions: ctx.App.Session.ContextSuggestions(),
59+
metrics: ctx.App.Session.RuntimeMetrics(),
60+
lastCompaction: lastCompaction,
61+
hasCompaction: hasCompaction,
62+
}
63+
ctx.App.registry.SetOverlay(c)
64+
return nil
65+
}
66+
67+
func (c *ContextCommand) Active() bool { return c.state != nil }
68+
func (c *ContextCommand) IsModal() bool { return true }
69+
func (c *ContextCommand) Dismiss() { c.state = nil }
70+
71+
func (c *ContextCommand) HandleKey(msg tea.KeyMsg) (bool, tea.Cmd) {
72+
if c.state == nil {
73+
return false, nil
74+
}
75+
switch msg.String() {
76+
case "tab", "right", "l":
77+
c.state.active = (c.state.active + 1) % len(contextTabs)
78+
return true, nil
79+
case "shift+tab", "left", "h":
80+
c.state.active = (c.state.active - 1 + len(contextTabs)) % len(contextTabs)
81+
return true, nil
82+
case "1", "2", "3":
83+
idx := int(msg.Runes[0] - '1')
84+
if idx < len(contextTabs) {
85+
c.state.active = idx
86+
}
87+
return true, nil
88+
case "esc", "ctrl+c", "q":
89+
c.app.registry.ClearOverlay()
90+
return true, nil
91+
}
92+
return true, nil
93+
}
94+
95+
func (c *ContextCommand) View(width int) string {
96+
if c.state == nil {
97+
return ""
98+
}
99+
frame := tui.InfoOverlayFrame{
100+
Title: "Context",
101+
Tabs: []tui.InfoOverlayTab{
102+
{Name: contextTabs[0], Body: c.renderUsage},
103+
{Name: contextTabs[1], Body: c.renderComposition},
104+
{Name: contextTabs[2], Body: c.renderSuggestions},
105+
},
106+
Active: c.state.active,
107+
Hint: "Tab / ←→ switch · 1-3 jump · Esc close",
108+
Width: width,
109+
}
110+
return frame.Render()
111+
}
112+
113+
func (c *ContextCommand) renderUsage() string {
114+
s := c.state
115+
p := tui.NewInfoPanel("")
116+
117+
usage := s.contextUsage
118+
if s.snapshotOK && s.snapshot != nil && s.snapshot.Usage != nil {
119+
usage = s.snapshot.Usage
120+
}
121+
if usage != nil {
122+
p.Row("Used", fmt.Sprintf("%s (%.1f%%)", tui.FormatTokens(usage.Tokens), usage.Percent))
123+
p.Row("Window", tui.FormatTokens(usage.ContextWindow))
124+
p.Hint("Detail", fmt.Sprintf("usage=%s, trailing=%s",
125+
tui.FormatTokens(usage.UsageTokens), tui.FormatTokens(usage.TrailingTokens)))
126+
} else {
127+
p.Hint("Used", "(unavailable)")
128+
}
129+
130+
if s.breakdown.Total > 0 {
131+
window := s.breakdown.ContextWindow
132+
fmtPct := func(tokens int) string {
133+
if window <= 0 {
134+
return tui.FormatTokens(tokens)
135+
}
136+
return fmt.Sprintf("%s (%.0f%%)", tui.FormatTokens(tokens), float64(tokens)/float64(window)*100)
137+
}
138+
p.Section("Breakdown")
139+
p.Row("User text", fmtPct(s.breakdown.UserText))
140+
p.Row("Assistant text", fmtPct(s.breakdown.AssistantText))
141+
p.Row("Tool calls", fmtPct(s.breakdown.ToolCalls))
142+
p.Row("Tool results", fmtPct(s.breakdown.ToolResults))
143+
if s.breakdown.Summaries > 0 {
144+
p.Row("Summaries", fmtPct(s.breakdown.Summaries))
145+
}
146+
if s.breakdown.Images > 0 {
147+
p.Row("Images", fmtPct(s.breakdown.Images))
148+
}
149+
150+
if len(s.breakdown.TopTools) > 0 {
151+
p.Section("Top tools")
152+
for _, t := range s.breakdown.TopTools {
153+
p.Row(t.Name, fmt.Sprintf("%s calls=%s results=%s",
154+
fmtPct(t.Total), tui.FormatTokens(t.CallTokens), tui.FormatTokens(t.ResultTokens)))
155+
}
156+
}
157+
}
158+
159+
return p.Render()
160+
}
161+
162+
func (c *ContextCommand) renderComposition() string {
163+
s := c.state
164+
p := tui.NewInfoPanel("")
165+
166+
if s.snapshotOK && s.snapshot != nil {
167+
p.Row("Scope", formatContextScope(s.snapshot.Scope))
168+
if s.snapshot.TranscriptMessages != s.snapshot.ActiveMessages {
169+
p.Row("Messages", fmt.Sprintf("%d active / %d transcript",
170+
s.snapshot.ActiveMessages, s.snapshot.TranscriptMessages))
171+
} else {
172+
p.Row("Messages", fmt.Sprintf("%d", s.snapshot.ActiveMessages))
173+
}
174+
p.Row("Summaries", fmt.Sprintf("%d", s.snapshot.SummaryMessages))
175+
p.Row("Cleared results", fmt.Sprintf("%d", s.snapshot.ClearedToolResults))
176+
p.Row("Trimmed blocks", fmt.Sprintf("%d", s.snapshot.TrimmedTextBlocks))
177+
178+
p.Section("Last rewrite")
179+
strategy := prettyCompactionStrategy(s.snapshot.LastStrategy)
180+
if strategy == "" {
181+
strategy = "(none)"
182+
}
183+
p.Hint("Strategy", strategy)
184+
p.Row("Changed", formatBool(s.snapshot.LastChanged))
185+
p.Hint("Details", formatContextRewriteDetails(s.snapshot))
186+
} else {
187+
p.Hint("Snapshot", "(unavailable)")
188+
}
189+
190+
p.Section("Compaction")
191+
p.Row("Total", fmt.Sprintf("%d", s.metrics.CompactionTotal))
192+
p.Row("Changed", fmt.Sprintf("%d", s.metrics.CompactionChanged))
193+
p.Row("Saved", tui.FormatTokens(s.metrics.CompactionSaved))
194+
p.Hint("By kind", formatCompactionCounts(s.metrics.CompactionByKind))
195+
p.Hint("Last", formatLastCompaction(s.lastCompaction, s.hasCompaction))
196+
197+
return p.Render()
198+
}
199+
200+
func (c *ContextCommand) renderSuggestions() string {
201+
s := c.state
202+
if len(s.suggestions) == 0 {
203+
return tui.MutedStyle.Render(" No suggestions. Context looks healthy.")
204+
}
205+
206+
p := tui.NewInfoPanel("")
207+
for _, sug := range s.suggestions {
208+
msg := sug.Message
209+
if sug.Savings > 0 {
210+
msg += fmt.Sprintf(" (~%s saveable)", tui.FormatTokens(sug.Savings))
211+
}
212+
if sug.Severity == "warning" {
213+
p.Warn("!", msg)
214+
} else {
215+
p.Hint("i", msg)
216+
}
217+
}
218+
return strings.TrimRight(p.Render(), "\n")
219+
}

0 commit comments

Comments
 (0)