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:
parent
6408c140fc
commit
f6b917afe1
7 changed files with 5 additions and 1122 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
38
cmd/start.go
38
cmd/start.go
|
|
@ -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
16
go.mod
|
|
@ -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
29
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue