Remove TUI support from start command

The bubbletea-based TUI for agent request visualization was not
practical and added significant binary size. This removes the TUI
entirely: the --tui flag, the agent context TUI model/messages, and
all SendTUI/GetTUIProgram call sites. Development-mode logging now
always prints ANSI-colored output to stdout.

Drops direct dependencies on charmbracelet/bubbletea and lipgloss.

Made-with: Cursor
This commit is contained in:
Max 2026-03-06 20:55:57 +08:00
parent 6408c140fc
commit f6b917afe1
7 changed files with 5 additions and 1122 deletions

View file

@ -277,26 +277,11 @@ func (o *Orchestrator) callAgentWithContext(ctx *agentContext.Context, req *Requ
)
}
// Notify TUI of A2A call start (use parent requestID so it appears in parent panel)
parentRequestID := o.ctx.RequestID()
agentContext.SendTUI(agentContext.AgentEventMsg{
RequestID: parentRequestID,
Event: agentContext.EventA2AStart,
Data: map[string]interface{}{"target": req.AgentID},
})
// Execute the agent call with the provided context
// The agent.Stream method will use the context's Writer for output
resp, err := agent.Stream(ctx, req.Messages, ctxOpts)
if err != nil {
if a2aNode != nil {
a2aNode.Fail(err)
}
agentContext.SendTUI(agentContext.AgentEventMsg{
RequestID: parentRequestID,
Event: agentContext.EventA2ADone,
Data: map[string]interface{}{"target": req.AgentID, "error": err.Error()},
})
return NewResult(req.AgentID, nil, fmt.Errorf("agent call failed: %w", err))
}
@ -306,11 +291,6 @@ func (o *Orchestrator) callAgentWithContext(ctx *agentContext.Context, req *Requ
"status": "completed",
})
}
agentContext.SendTUI(agentContext.AgentEventMsg{
RequestID: parentRequestID,
Event: agentContext.EventA2ADone,
Data: map[string]interface{}{"target": req.AgentID},
})
return NewResult(req.AgentID, resp, nil)
}

View file

@ -192,11 +192,8 @@ func (l *RequestLogger) processEntry(entry LogEntry) {
}
}
// printDev sends to TUI if available, otherwise prints colored output to stdout
// printDev prints colored output to stdout in development mode
func (l *RequestLogger) printDev(entry LogEntry) {
if GetTUIProgram() != nil {
return
}
switch entry.Level {
case LogLevelTrace:
fmt.Printf("%s → %s%s\n", colorGray, entry.Message, colorReset)
@ -315,17 +312,6 @@ func (l *RequestLogger) Start() {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
ParentID: l.parentID,
AssistantID: l.currentAssistantID(),
Event: EventRequestStart,
})
if GetTUIProgram() != nil {
return
}
fmt.Println()
fmt.Printf("%s%s%s\n", colorBoldCyan, strings.Repeat("═", 60), colorReset)
fmt.Printf("%s AGENT REQUEST %s%s\n", colorBoldCyan, l.shortID, colorReset)
@ -357,21 +343,6 @@ func (l *RequestLogger) End(success bool, err error) {
return
}
data := map[string]interface{}{"duration": duration.Round(time.Millisecond)}
if err != nil {
data["error"] = err.Error()
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
AssistantID: l.currentAssistantID(),
Event: EventRequestEnd,
Data: data,
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s%s%s\n", colorCyan, strings.Repeat("─", 60), colorReset)
if success {
fmt.Printf("%s REQUEST %s COMPLETED%s\n", colorBoldGreen, l.shortID, colorReset)
@ -400,15 +371,6 @@ func (l *RequestLogger) Phase(name string) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventPhase,
Data: map[string]interface{}{"name": name, "elapsed": elapsed},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s > %s%s %s[+%v]%s\n", colorBoldBlue, name, colorReset, colorGray, elapsed, colorReset)
}
@ -425,15 +387,6 @@ func (l *RequestLogger) PhaseComplete(name string) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventPhaseDone,
Data: map[string]interface{}{"name": name, "elapsed": elapsed},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s + %s%s %s[+%v]%s\n", colorGreen, name, colorReset, colorGray, elapsed, colorReset)
}
@ -447,15 +400,6 @@ func (l *RequestLogger) PhaseSkip(name, reason string) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventPhaseSkip,
Data: map[string]interface{}{"name": name, "reason": reason},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s - %s (%s)%s\n", colorGray, name, reason, colorReset)
}
@ -472,19 +416,6 @@ func (l *RequestLogger) LLMStart(connector, model string, messageCount int) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventLLMCall,
Data: map[string]interface{}{
"connector": connector,
"model": model,
"messages": messageCount,
},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s LLM Call%s %s[+%v]%s\n", colorBoldMagenta, colorReset, colorGray, elapsed, colorReset)
fmt.Printf("%s Connector: %s%s%s\n", colorGray, colorWhite, connector, colorReset)
if model != "" {
@ -511,19 +442,6 @@ func (l *RequestLogger) LLMComplete(tokens int, hasToolCalls bool) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventLLMDone,
Data: map[string]interface{}{
"detail": fmt.Sprintf("%s [tokens:%d, %v]", status, tokens, elapsed),
"tokens": tokens,
"status": status,
},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s + LLM Response (%s)%s", colorGreen, status, colorReset)
if tokens > 0 {
fmt.Printf(" %s[tokens: %d]%s", colorGray, tokens, colorReset)
@ -543,15 +461,6 @@ func (l *RequestLogger) ToolStart(toolName string) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventToolCall,
Data: map[string]interface{}{"name": toolName},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s Tool: %s%s\n", colorYellow, toolName, colorReset)
}
@ -571,15 +480,6 @@ func (l *RequestLogger) ToolComplete(toolName string, success bool) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventToolDone,
Data: map[string]interface{}{"name": toolName, "success": success},
})
if GetTUIProgram() != nil {
return
}
if success {
fmt.Printf("%s + %s completed%s\n", colorGreen, toolName, colorReset)
} else {
@ -600,15 +500,6 @@ func (l *RequestLogger) HookStart(hookName string) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventHook,
Data: map[string]interface{}{"name": hookName},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s Hook: %s%s %s[+%v]%s\n", colorMagenta, hookName, colorReset, colorGray, elapsed, colorReset)
}
@ -624,15 +515,6 @@ func (l *RequestLogger) HookComplete(hookName string) {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventHookDone,
Data: map[string]interface{}{"name": hookName},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s + %s done%s\n", colorGreen, hookName, colorReset)
}
@ -644,7 +526,7 @@ func (l *RequestLogger) Cleanup(resource string) {
kunlog.Trace("[AGENT] %s Cleanup: %s", l.shortID, resource)
if !config.IsDevelopment() || GetTUIProgram() != nil {
if !config.IsDevelopment() {
return
}
fmt.Printf("%s + %s%s\n", colorGray, resource, colorReset)
@ -658,7 +540,7 @@ func (l *RequestLogger) HistoryLoad(count, maxSize int) {
kunlog.Trace("[AGENT] %s History loaded: %d/%d messages", l.shortID, count, maxSize)
if !config.IsDevelopment() || GetTUIProgram() != nil {
if !config.IsDevelopment() {
return
}
fmt.Printf("%s Loaded %d/%d history messages%s\n", colorGray, count, maxSize, colorReset)
@ -673,7 +555,7 @@ func (l *RequestLogger) HistoryOverlap(overlapCount int) {
if overlapCount > 0 {
kunlog.Trace("[AGENT] %s History overlap removed: %d messages", l.shortID, overlapCount)
if !config.IsDevelopment() || GetTUIProgram() != nil {
if !config.IsDevelopment() {
return
}
fmt.Printf("%s Removed %d overlapping messages%s\n", colorYellow, overlapCount, colorReset)
@ -692,15 +574,6 @@ func (l *RequestLogger) Release() {
return
}
SendTUI(AgentEventMsg{
RequestID: l.requestID,
Event: EventContextRelease,
Data: map[string]interface{}{"assistant": l.currentAssistantID()},
})
if GetTUIProgram() != nil {
return
}
fmt.Printf("%s RELEASE %s%s %s(%s)%s\n", colorBoldYellow, l.shortID, colorReset, colorGray, l.currentAssistantID(), colorReset)
}

View file

@ -1,799 +0,0 @@
package context
import (
"fmt"
"strings"
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
tuiProgram *tea.Program
tuiProgramMu sync.RWMutex
)
// SetTUIProgram sets the global TUI program (called from start.go after HTTP READY)
func SetTUIProgram(p *tea.Program) {
tuiProgramMu.Lock()
tuiProgram = p
tuiProgramMu.Unlock()
}
// GetTUIProgram returns the global TUI program (nil if not in TUI mode)
func GetTUIProgram() *tea.Program {
tuiProgramMu.RLock()
defer tuiProgramMu.RUnlock()
return tuiProgram
}
// SendTUI sends a message to the TUI program if available
func SendTUI(msg tea.Msg) {
if p := GetTUIProgram(); p != nil {
p.Send(msg)
}
}
// TUILogWriter implements io.Writer to bridge gou DevWriter -> TUI AppLogMsg
type TUILogWriter struct {
Program *tea.Program
}
func (w *TUILogWriter) Write(p []byte) (n int, err error) {
content := strings.TrimRight(string(p), "\n")
if content == "" {
return len(p), nil
}
w.Program.Send(AppLogMsg{Content: content})
return len(p), nil
}
// ─── Styles ───────────────────────────────────────────────────────────────────
var (
boxRunning = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("33")).
PaddingLeft(1).PaddingRight(1)
boxDone = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("240")).
PaddingLeft(1).PaddingRight(1)
boxFailed = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("31")).
PaddingLeft(1).PaddingRight(1)
boxAppLog = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("240")).
PaddingLeft(1).PaddingRight(1)
sRunning = lipgloss.NewStyle().Foreground(lipgloss.Color("33"))
sDone = lipgloss.NewStyle().Foreground(lipgloss.Color("34"))
sFailed = lipgloss.NewStyle().Foreground(lipgloss.Color("31"))
sDim = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
sBold = lipgloss.NewStyle().Bold(true)
sYellow = lipgloss.NewStyle().Foreground(lipgloss.Color("33"))
sRed = lipgloss.NewStyle().Foreground(lipgloss.Color("31"))
sBlue = lipgloss.NewStyle().Foreground(lipgloss.Color("34"))
sMagenta = lipgloss.NewStyle().Foreground(lipgloss.Color("35"))
sTree = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
)
// ─── Data ─────────────────────────────────────────────────────────────────────
// RequestPanel represents a single top-level agent request
type RequestPanel struct {
RequestID string
ShortID string
AssistantID string
StartTime time.Time
EndTime time.Time // set when done/failed, freezes elapsed display
Status PanelStatus
Nodes []TreeNode
ParentID string
Collapsed bool
viewRow int // Y offset of the header line (for mouse click)
}
// TreeNode represents a step within a request panel
type TreeNode struct {
Kind NodeKind
Label string
Status NodeStatus
Detail string
Children []*TreeNode
StartTime time.Time
EndTime time.Time
Collapsed bool
}
// AgentTUIModel is the bubbletea Model for agent request visualization
type AgentTUIModel struct {
panels []*RequestPanel
panelIndex map[string]int // requestID -> index in panels (first registration wins)
appLogs []AppLogEntry
appLogExpand bool
appLogRow int // Y offset of app log header
cursor int
width int
height int
scrollOffset int
autoFollow bool // auto-scroll to bottom when new content arrives
mouseOn bool
quitting bool
}
// NewAgentTUIModel creates a new TUI model
func NewAgentTUIModel() AgentTUIModel {
return AgentTUIModel{
panels: []*RequestPanel{},
panelIndex: map[string]int{},
appLogs: []AppLogEntry{},
width: 80,
height: 24,
autoFollow: true,
}
}
func (m AgentTUIModel) Init() tea.Cmd {
return tickCmd()
}
func tickCmd() tea.Cmd {
return tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg {
return TickMsg(t)
})
}
// ─── Update ───────────────────────────────────────────────────────────────────
func (m AgentTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
case tea.KeyMsg:
return m.handleKey(msg)
case tea.MouseMsg:
return m.handleMouse(msg)
case AgentEventMsg:
return m.handleAgentEvent(msg), nil
case AppLogMsg:
m.appLogs = append(m.appLogs, AppLogEntry{
Content: msg.Content,
Time: time.Now(),
})
return m, nil
case TickMsg:
return m, tickCmd()
}
return m, nil
}
func (m AgentTUIModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
topPanels := m.topLevelPanels()
total := len(topPanels) + 1 // +1 for app log
viewH := m.viewHeight()
switch msg.String() {
case "q", "ctrl+c":
m.quitting = true
return m, tea.Quit
// Scrolling
case "j", "down":
m.scrollOffset++
m.autoFollow = false
case "k", "up":
if m.scrollOffset > 0 {
m.scrollOffset--
}
m.autoFollow = false
case "pgdown", "ctrl+d":
m.scrollOffset += viewH / 2
m.autoFollow = false
case "pgup", "ctrl+u":
m.scrollOffset -= viewH / 2
if m.scrollOffset < 0 {
m.scrollOffset = 0
}
m.autoFollow = false
case "G", "end":
m.autoFollow = true
case "g", "home":
m.scrollOffset = 0
m.autoFollow = false
// Cursor navigation for panel selection (wraps around)
case "tab":
m.cursor = (m.cursor + 1) % total
m.scrollToCursor(topPanels)
case "shift+tab":
m.cursor = (m.cursor - 1 + total) % total
m.scrollToCursor(topPanels)
case "enter", " ":
if m.cursor < len(topPanels) {
topPanels[m.cursor].Collapsed = !topPanels[m.cursor].Collapsed
} else {
m.appLogExpand = !m.appLogExpand
}
case "c":
m.appLogExpand = !m.appLogExpand
case "a":
for _, p := range m.panels {
p.Collapsed = false
}
m.appLogExpand = true
case "A":
for _, p := range m.panels {
p.Collapsed = true
}
m.appLogExpand = false
case "m":
m.mouseOn = !m.mouseOn
if m.mouseOn {
return m, tea.EnableMouseCellMotion
}
return m, tea.DisableMouse
}
return m, nil
}
func (m AgentTUIModel) viewHeight() int {
h := m.height - 2 // reserve for status bar
if h < 4 {
h = 4
}
return h
}
func (m *AgentTUIModel) scrollToCursor(topPanels []*RequestPanel) {
targetRow := 0
if m.cursor < len(topPanels) {
targetRow = topPanels[m.cursor].viewRow
} else {
targetRow = m.appLogRow
}
viewH := m.viewHeight()
if targetRow < m.scrollOffset {
m.scrollOffset = targetRow
} else if targetRow >= m.scrollOffset+viewH {
m.scrollOffset = targetRow - viewH + 3
}
}
func (m AgentTUIModel) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
switch {
case msg.Button == tea.MouseButtonWheelUp:
m.scrollOffset -= 3
if m.scrollOffset < 0 {
m.scrollOffset = 0
}
m.autoFollow = false
return m, nil
case msg.Button == tea.MouseButtonWheelDown:
m.scrollOffset += 3
m.autoFollow = false
return m, nil
}
if msg.Button != tea.MouseButtonLeft || msg.Action != tea.MouseActionRelease {
return m, nil
}
y := msg.Y + m.scrollOffset
// Check app log header
if y == m.appLogRow {
m.appLogExpand = !m.appLogExpand
return m, nil
}
// Check panel headers
for _, p := range m.panels {
if p.ParentID != "" {
continue
}
if y == p.viewRow {
p.Collapsed = !p.Collapsed
return m, nil
}
}
return m, nil
}
// ─── Agent Events ─────────────────────────────────────────────────────────────
func (m *AgentTUIModel) handleAgentEvent(msg AgentEventMsg) tea.Model {
switch msg.Event {
case EventRequestStart:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
// Delegate sub-call: same requestID, different assistantID.
// Add as a tree node inside the existing panel instead of creating a new one.
p := m.panels[idx]
p.Nodes = append(p.Nodes, TreeNode{
Kind: NodeA2A,
Label: msg.AssistantID,
Status: NodeRunning,
StartTime: time.Now(),
})
return m
}
panel := &RequestPanel{
RequestID: msg.RequestID,
ShortID: shortID(msg.RequestID),
AssistantID: msg.AssistantID,
StartTime: time.Now(),
Status: PanelRunning,
ParentID: msg.ParentID,
}
m.panelIndex[msg.RequestID] = len(m.panels)
m.panels = append(m.panels, panel)
case EventRequestEnd:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
p := m.panels[idx]
// Only mark panel done if the ending assistantID matches the panel's original assistantID
// (delegate sub-calls End with a different assistantID, they update their tree node instead)
if msg.AssistantID == p.AssistantID || msg.AssistantID == "" {
if errVal, has := msg.Data["error"]; has && errVal != nil {
p.Status = PanelFailed
} else {
p.Status = PanelSuccess
}
p.EndTime = time.Now()
p.Collapsed = true
// Finalize any still-running child nodes (e.g. hook interrupted mid-execution)
finalStatus := NodeDone
if p.Status == PanelFailed {
finalStatus = NodeFailed
}
for i := range p.Nodes {
if p.Nodes[i].Status == NodeRunning {
p.Nodes[i].Status = finalStatus
p.Nodes[i].EndTime = p.EndTime
}
}
} else {
// Delegate sub-call finished: mark its tree node as done
for i := len(p.Nodes) - 1; i >= 0; i-- {
if p.Nodes[i].Kind == NodeA2A && p.Nodes[i].Label == msg.AssistantID && p.Nodes[i].Status == NodeRunning {
p.Nodes[i].Status = NodeDone
p.Nodes[i].EndTime = time.Now()
break
}
}
}
}
case EventLLMCall:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
m.panels[idx].Nodes = append(m.panels[idx].Nodes, TreeNode{
Kind: NodeLLM, Label: "LLM", Status: NodeRunning, StartTime: time.Now(),
})
}
case EventLLMDone:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
p := m.panels[idx]
for i := len(p.Nodes) - 1; i >= 0; i-- {
if p.Nodes[i].Kind == NodeLLM && p.Nodes[i].Status == NodeRunning {
p.Nodes[i].Status = NodeDone
p.Nodes[i].EndTime = time.Now()
if d, has := msg.Data["detail"]; has {
p.Nodes[i].Detail = fmt.Sprintf("%v", d)
}
break
}
}
}
case EventToolCall:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
name := dataStr(msg.Data, "name")
m.panels[idx].Nodes = append(m.panels[idx].Nodes, TreeNode{
Kind: NodeTool, Label: name, Status: NodeRunning, StartTime: time.Now(),
})
}
case EventToolDone:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
name := dataStr(msg.Data, "name")
p := m.panels[idx]
for i := len(p.Nodes) - 1; i >= 0; i-- {
if p.Nodes[i].Kind == NodeTool && p.Nodes[i].Label == name && p.Nodes[i].Status == NodeRunning {
p.Nodes[i].Status = NodeDone
p.Nodes[i].EndTime = time.Now()
break
}
}
}
case EventHook:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
name := dataStr(msg.Data, "name")
m.panels[idx].Nodes = append(m.panels[idx].Nodes, TreeNode{
Kind: NodeHook, Label: name, Status: NodeRunning, StartTime: time.Now(),
})
}
case EventHookDone:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
p := m.panels[idx]
for i := len(p.Nodes) - 1; i >= 0; i-- {
if p.Nodes[i].Kind == NodeHook && p.Nodes[i].Status == NodeRunning {
p.Nodes[i].Status = NodeDone
p.Nodes[i].EndTime = time.Now()
break
}
}
}
case EventA2AStart:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
target := dataStr(msg.Data, "target")
m.panels[idx].Nodes = append(m.panels[idx].Nodes, TreeNode{
Kind: NodeA2A, Label: target, Status: NodeRunning, StartTime: time.Now(),
})
}
case EventA2ADone:
if idx, ok := m.panelIndex[msg.RequestID]; ok {
target := dataStr(msg.Data, "target")
p := m.panels[idx]
for i := len(p.Nodes) - 1; i >= 0; i-- {
if p.Nodes[i].Kind == NodeA2A && p.Nodes[i].Status == NodeRunning && (target == "" || p.Nodes[i].Label == target) {
p.Nodes[i].Status = NodeDone
p.Nodes[i].EndTime = time.Now()
break
}
}
}
}
return m
}
// ─── View ─────────────────────────────────────────────────────────────────────
func (m AgentTUIModel) View() string {
if m.quitting {
return ""
}
boxW := m.width - 2
if boxW < 40 {
boxW = 40
}
// Render full content
var sb strings.Builder
row := 0
topIdx := 0
for _, panel := range m.panels {
if panel.ParentID != "" {
continue
}
selected := (topIdx == m.cursor)
rendered := m.renderPanelBox(panel, boxW, selected, &row)
sb.WriteString(rendered)
sb.WriteString("\n")
row++
topIdx++
}
// App Log
m.appLogRow = row
sb.WriteString(m.renderAppLogBox(boxW, topIdx == m.cursor, &row))
fullContent := sb.String()
lines := strings.Split(fullContent, "\n")
totalLines := len(lines)
viewH := m.viewHeight()
// Auto-follow: snap to bottom
if m.autoFollow {
m.scrollOffset = totalLines - viewH
}
// Clamp scroll offset
maxScroll := totalLines - viewH
if maxScroll < 0 {
maxScroll = 0
}
if m.scrollOffset > maxScroll {
m.scrollOffset = maxScroll
}
if m.scrollOffset < 0 {
m.scrollOffset = 0
}
// Slice visible lines
end := m.scrollOffset + viewH
if end > totalLines {
end = totalLines
}
visible := lines[m.scrollOffset:end]
// Build output
var out strings.Builder
out.WriteString(strings.Join(visible, "\n"))
// Status bar with scroll indicator
mouseLabel := "off"
if m.mouseOn {
mouseLabel = "on"
}
scrollInfo := ""
if totalLines > viewH {
pct := 100
if maxScroll > 0 {
pct = m.scrollOffset * 100 / maxScroll
}
scrollInfo = fmt.Sprintf(" [%d%%]", pct)
}
followLabel := ""
if m.autoFollow {
followLabel = " AUTO"
}
hint := sDim.Render(fmt.Sprintf(" j/k:scroll tab:select space:toggle a/A:all G:bottom g:top m:mouse(%s)%s%s q:quit",
mouseLabel, scrollInfo, followLabel))
out.WriteString("\n" + hint)
return out.String()
}
func (m AgentTUIModel) renderPanelBox(panel *RequestPanel, boxW int, selected bool, row *int) string {
// Record header row for mouse
panel.viewRow = *row
elapsed := m.panelElapsed(panel)
icon, statusText, style := panelStatusDisplay(panel.Status, elapsed)
// Title line
collapser := "▾"
if panel.Collapsed {
collapser = "▸"
}
cursor := " "
if selected {
cursor = ""
}
title := fmt.Sprintf("%s %s %s %s %s",
sDim.Render(cursor),
sDim.Render(collapser),
sBold.Render(panel.ShortID),
panel.AssistantID,
style.Render(icon+" "+statusText),
)
if panel.Collapsed {
box := boxForStatus(panel.Status).Width(boxW)
result := box.Render(title)
*row += strings.Count(result, "\n") + 1
return result
}
// Build body
var body strings.Builder
body.WriteString(title + "\n")
for _, node := range panel.Nodes {
body.WriteString(m.renderTreeNode(node, " ", false, panel))
}
// Render fork children (different requestID, parentID matches)
children := m.childPanels(panel.RequestID)
for i, child := range children {
isLast := (i == len(children)-1)
body.WriteString(m.renderChildSummary(child, " ", isLast))
}
box := boxForStatus(panel.Status).Width(boxW)
result := box.Render(body.String())
*row += strings.Count(result, "\n") + 1
return result
}
func (m AgentTUIModel) renderTreeNode(node TreeNode, prefix string, isChild bool, panel *RequestPanel) string {
panelEnded := panel != nil && panel.Status != PanelRunning
displayNode := node
if panelEnded && displayNode.Status == NodeRunning {
displayNode.Status = NodeFailed
}
icon, statusText := nodeStatusDisplay(displayNode)
elapsed := m.nodeElapsed(node, panelEnded, panel.EndTime)
label := ""
switch node.Kind {
case NodeHook:
label = sMagenta.Render("Hook: "+node.Label) + " " + statusText
case NodeLLM:
detail := ""
if node.Detail != "" {
detail = " " + sDim.Render("["+node.Detail+"]")
}
label = sBlue.Render("LLM") + " " + statusText + detail
case NodeTool:
label = sTree.Render("├ ") + sYellow.Render(node.Label) + " " + statusText
case NodeA2A:
label = sTree.Render("⤷ ") + sBold.Render(node.Label) + " " + statusText
case NodePhase:
label = node.Label + " " + statusText
default:
label = node.Label + " " + statusText
}
_ = icon
line := prefix + label
if elapsed != "" {
line += " " + sDim.Render(elapsed)
}
return line + "\n"
}
func (m AgentTUIModel) renderChildSummary(panel *RequestPanel, prefix string, isLast bool) string {
elapsed := m.panelElapsed(panel)
icon, statusText, style := panelStatusDisplay(panel.Status, elapsed)
branch := sTree.Render("├─ ")
if isLast {
branch = sTree.Render("└─ ")
}
return fmt.Sprintf("%s%s%s %s %s\n",
prefix, branch,
sBold.Render(panel.ShortID+" "+panel.AssistantID),
style.Render(icon+" "+statusText),
sDim.Render(elapsed),
)
}
func (m AgentTUIModel) renderAppLogBox(boxW int, selected bool, row *int) string {
cursor := " "
if selected {
cursor = ""
}
collapser := "▸"
if m.appLogExpand {
collapser = "▾"
}
count := len(m.appLogs)
title := fmt.Sprintf("%s %s %s (%d)",
sDim.Render(cursor),
sDim.Render(collapser),
sBold.Render("App Output"),
count,
)
if !m.appLogExpand || count == 0 {
result := boxAppLog.Width(boxW).Render(title)
*row += strings.Count(result, "\n") + 1
return result
}
var body strings.Builder
body.WriteString(title + "\n")
start := 0
if count > 50 {
start = count - 50
}
for _, entry := range m.appLogs[start:] {
body.WriteString(" " + entry.Content + "\n")
}
result := boxAppLog.Width(boxW).Render(body.String())
*row += strings.Count(result, "\n") + 1
return result
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
func (m AgentTUIModel) topLevelPanels() []*RequestPanel {
var result []*RequestPanel
for _, p := range m.panels {
if p.ParentID == "" {
result = append(result, p)
}
}
return result
}
func (m AgentTUIModel) childPanels(parentRequestID string) []*RequestPanel {
var result []*RequestPanel
for _, p := range m.panels {
if p.ParentID == parentRequestID {
result = append(result, p)
}
}
return result
}
func (m AgentTUIModel) panelElapsed(p *RequestPanel) string {
if p.Status != PanelRunning && !p.EndTime.IsZero() {
return fmtDuration(p.EndTime.Sub(p.StartTime))
}
return fmtDuration(time.Since(p.StartTime))
}
func (m AgentTUIModel) nodeElapsed(n TreeNode, panelEnded bool, panelEndTime time.Time) string {
if n.Status == NodeDone || n.Status == NodeFailed {
if !n.EndTime.IsZero() {
return fmtDuration(n.EndTime.Sub(n.StartTime))
}
}
if n.Status == NodeRunning {
if panelEnded && !panelEndTime.IsZero() {
return fmtDuration(panelEndTime.Sub(n.StartTime))
}
return fmtDuration(time.Since(n.StartTime))
}
return ""
}
func panelStatusDisplay(status PanelStatus, elapsed string) (icon string, text string, style lipgloss.Style) {
switch status {
case PanelRunning:
return "⟳", "running " + elapsed, sRunning
case PanelSuccess:
return "✓", "done " + elapsed, sDone
case PanelFailed:
return "✗", "failed " + elapsed, sFailed
}
return "", "", sDim
}
func nodeStatusDisplay(n TreeNode) (icon string, text string) {
switch n.Status {
case NodePending:
return "…", sDim.Render("…")
case NodeRunning:
return "⟳", sRunning.Render("⟳")
case NodeDone:
return "✓", sDone.Render("✓")
case NodeFailed:
return "✗", sFailed.Render("✗")
}
return "", ""
}
func boxForStatus(status PanelStatus) lipgloss.Style {
switch status {
case PanelRunning:
return boxRunning
case PanelFailed:
return boxFailed
default:
return boxDone
}
}
func dataStr(data map[string]interface{}, key string) string {
if v, ok := data[key]; ok {
return fmt.Sprintf("%v", v)
}
return ""
}
func fmtDuration(d time.Duration) string {
if d < time.Second {
return fmt.Sprintf("%dms", d.Milliseconds())
}
return fmt.Sprintf("%.1fs", d.Seconds())
}

View file

@ -1,90 +0,0 @@
package context
import "time"
// EventType represents the type of agent lifecycle event
type EventType int
const (
EventRequestStart EventType = iota
EventPhase
EventPhaseDone
EventPhaseSkip
EventLLMCall
EventLLMDone
EventToolCall
EventToolDone
EventHook
EventHookDone
EventA2AStart
EventA2ADone
EventRequestEnd
EventContextFork
EventContextRelease
)
// AgentEventMsg is sent from RequestLogger to the TUI Program
type AgentEventMsg struct {
RequestID string
ParentID string
AssistantID string
Event EventType
Data map[string]interface{}
}
// AppLogLevel represents the severity of application-side output
type AppLogLevel int
const (
AppLogLevelLog AppLogLevel = iota
AppLogLevelInfo
AppLogLevelWarn
AppLogLevelError
AppLogLevelException
)
// AppLogMsg is sent from the DevWriter (gou layer) to the TUI Program
type AppLogMsg struct {
Level AppLogLevel
Content string
}
// AppLogEntry stores a single application output entry
type AppLogEntry struct {
Level AppLogLevel
Content string
Time time.Time
}
// PanelStatus represents the lifecycle state of a request panel
type PanelStatus int
const (
PanelRunning PanelStatus = iota
PanelSuccess
PanelFailed
)
// NodeKind represents the type of a tree node within a request panel
type NodeKind int
const (
NodePhase NodeKind = iota
NodeLLM
NodeTool
NodeHook
NodeA2A
)
// NodeStatus represents the state of a tree node
type NodeStatus int
const (
NodePending NodeStatus = iota
NodeRunning
NodeDone
NodeFailed
)
// TickMsg triggers periodic UI refresh for elapsed time display
type TickMsg time.Time

View file

@ -8,14 +8,11 @@ import (
"strings"
"syscall"
tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"github.com/yaoapp/gou/api"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/fs"
"github.com/yaoapp/gou/helper"
"github.com/yaoapp/gou/mcp"
"github.com/yaoapp/gou/plugin"
"github.com/yaoapp/gou/schedule"
@ -23,9 +20,7 @@ import (
"github.com/yaoapp/gou/store"
"github.com/yaoapp/gou/task"
"github.com/yaoapp/gou/websocket"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/kun/log"
agentcontext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/engine"
yaogrpc "github.com/yaoapp/yao/grpc"
@ -41,7 +36,6 @@ import (
var startDebug = false
var startDisableWatching = false
var startTUI = false
var startCmd = &cobra.Command{
Use: "start",
@ -293,7 +287,6 @@ var startCmd = &cobra.Command{
case http.READY:
fmt.Println(color.GreenString(L("Server is up and running...")))
fmt.Println(color.GreenString("Ctrl+C to stop"))
initAgentTUI()
break
case http.CLOSED:
@ -654,38 +647,7 @@ func colorMehtod(method string) string {
}
}
// initAgentTUI initializes the TUI for agent request visualization in dev mode.
// Must be called after HTTP READY to avoid interfering with startup messages.
func initAgentTUI() {
if !config.IsDevelopment() {
return
}
if !startTUI && os.Getenv("YAO_TUI") != "on" {
return
}
if !isatty.IsTerminal(os.Stdout.Fd()) {
return
}
model := agentcontext.NewAgentTUIModel()
p := tea.NewProgram(model, tea.WithoutSignalHandler())
agentcontext.SetTUIProgram(p)
tuiWriter := &agentcontext.TUILogWriter{Program: p}
helper.SetDevWriter(tuiWriter)
exception.SetWriter(tuiWriter)
go func() {
if _, err := p.Run(); err != nil {
log.Error("TUI error: %s", err.Error())
}
}()
}
func init() {
startCmd.PersistentFlags().BoolVarP(&startDebug, "debug", "", false, L("Development mode"))
startCmd.PersistentFlags().BoolVarP(&startDisableWatching, "disable-watching", "", false, L("Disable watching"))
startCmd.PersistentFlags().BoolVarP(&startTUI, "tui", "", false, L("Enable TUI for agent request visualization"))
}

16
go.mod
View file

@ -10,8 +10,6 @@ require (
github.com/blang/semver v3.5.1+incompatible
github.com/bwmarrin/discordgo v0.29.0
github.com/caarlos0/env/v6 v6.10.1
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/dchest/captcha v1.1.0
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.5.0
@ -34,7 +32,6 @@ require (
github.com/kaptinlin/jsonschema v0.6.1
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/mattn/go-isatty v0.0.20
github.com/mozillazg/go-pinyin v0.20.0
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
github.com/pierrec/lz4/v4 v4.1.25
@ -79,17 +76,12 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
@ -101,7 +93,6 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
@ -153,11 +144,10 @@ require (
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mark3labs/mcp-go v0.32.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/miekg/dns v1.1.66 // indirect
@ -167,9 +157,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/neo4j/neo4j-go-driver/v5 v5.28.1 // indirect
@ -211,7 +198,6 @@ require (
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.1 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect

29
go.sum
View file

@ -43,8 +43,6 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6U
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k=
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
@ -70,18 +68,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
@ -123,8 +109,6 @@ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTe
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/evanw/esbuild v0.25.4 h1:k1bTSim+usBG27w7BfOCorhgx3tO+6bAfMj5pR+6SKg=
github.com/evanw/esbuild v0.25.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
@ -304,8 +288,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
@ -320,8 +302,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
@ -351,12 +331,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ=
github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
@ -488,8 +462,6 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
@ -612,7 +584,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=