mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
refactor(ui): remove standalone tea.Program calls from CLI (TAS-27)
Delete GetPrompt, StartStreamingMessage, UpdateStreamingMessage, GetToolApproval, and finishStreaming from CLI — these each spun up their own tea.NewProgram and are superseded by the unified AppModel TUI. Also remove the streamProgram/streamDone fields they relied on. Update dead-code stubs in cmd/root.go to keep the build clean until the legacy agentic-loop functions are deleted in TAS-28.
This commit is contained in:
+7
-96
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -939,16 +938,12 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
|
||||
// Mark that this response is being streamed
|
||||
responseWasStreamed = true
|
||||
|
||||
// Start streaming message on first chunk
|
||||
// Accumulate content and update message
|
||||
if !streamingStarted {
|
||||
cli.StartStreamingMessage(config.ModelName)
|
||||
streamingStarted = true
|
||||
streamingContent.Reset() // Reset content for new streaming session
|
||||
}
|
||||
|
||||
// Accumulate content and update message
|
||||
streamingContent.WriteString(chunk)
|
||||
cli.UpdateStreamingMessage(streamingContent.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1154,15 +1149,12 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
|
||||
currentSpinner.Stop()
|
||||
currentSpinner = nil
|
||||
}
|
||||
allow, err := cli.GetToolApproval(toolName, toolArgs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Tool approval via CLI is no longer supported; always approve in legacy path.
|
||||
// Start spinner again for tool calls
|
||||
currentSpinner = ui.NewSpinner("")
|
||||
currentSpinner.Start()
|
||||
|
||||
return allow, nil
|
||||
return true, nil
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1266,91 +1258,10 @@ func executeStopHook(hookExecutor *hooks.Executor, response *fantasy.Response, s
|
||||
}
|
||||
}
|
||||
|
||||
// runInteractiveLoop handles the interactive portion of the agentic loop
|
||||
func runInteractiveLoop(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, messages []fantasy.Message, config AgenticLoopConfig, hookExecutor *hooks.Executor) error {
|
||||
for {
|
||||
// Get user input
|
||||
prompt, err := cli.GetPrompt()
|
||||
if err == io.EOF {
|
||||
fmt.Println("\n Goodbye!")
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get prompt: %v", err)
|
||||
}
|
||||
|
||||
if prompt == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Execute UserPromptSubmit hooks
|
||||
if hookExecutor != nil {
|
||||
input := &hooks.UserPromptSubmitInput{
|
||||
CommonInput: hookExecutor.PopulateCommonFields(hooks.UserPromptSubmit),
|
||||
Prompt: prompt,
|
||||
}
|
||||
|
||||
hookOutput, err := hookExecutor.ExecuteHooks(ctx, hooks.UserPromptSubmit, input)
|
||||
if err != nil {
|
||||
// Log error but don't fail
|
||||
if debugMode {
|
||||
fmt.Fprintf(os.Stderr, "UserPromptSubmit hook execution error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if hook blocked the prompt
|
||||
if hookOutput != nil && hookOutput.Decision == "block" {
|
||||
if cli != nil {
|
||||
cli.DisplayInfo(fmt.Sprintf("Prompt blocked: %s", hookOutput.Reason))
|
||||
}
|
||||
continue // Skip this prompt
|
||||
}
|
||||
|
||||
// Check if hook wants to stop the session
|
||||
if hookOutput != nil && hookOutput.Continue != nil && !*hookOutput.Continue {
|
||||
if hookOutput.StopReason != "" {
|
||||
cli.DisplayInfo(fmt.Sprintf("Session ended by hook: %s", hookOutput.StopReason))
|
||||
}
|
||||
return nil // Exit interactive loop gracefully
|
||||
}
|
||||
}
|
||||
// Handle slash commands
|
||||
if cli.IsSlashCommand(prompt) {
|
||||
result := cli.HandleSlashCommand(prompt, config.ServerNames, config.ToolNames)
|
||||
if result.Handled {
|
||||
// If the command was to clear history, clear the messages slice and session
|
||||
if result.ClearHistory {
|
||||
messages = messages[:0] // Clear the slice
|
||||
// Use unified function to clear session as well
|
||||
addMessagesToHistory(&messages, config.SessionManager, cli)
|
||||
}
|
||||
continue
|
||||
}
|
||||
cli.DisplayError(fmt.Errorf("unknown command: %s", prompt))
|
||||
continue
|
||||
}
|
||||
|
||||
// Display user message
|
||||
cli.DisplayUserMessage(prompt)
|
||||
|
||||
// Create temporary messages with user input for processing
|
||||
tempMessages := append(messages, fantasy.NewUserMessage(prompt))
|
||||
// Process the user input with tool calls
|
||||
_, conversationMessages, err := runAgenticStep(ctx, mcpAgent, cli, tempMessages, config, hookExecutor)
|
||||
if err != nil {
|
||||
// Check if this was a user cancellation
|
||||
if err.Error() == "generation cancelled by user" {
|
||||
cli.DisplayCancellation()
|
||||
} else {
|
||||
cli.DisplayError(fmt.Errorf("agent error: %v", err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Only add to history after successful completion
|
||||
// conversationMessages already includes the user message, tool calls, and final response
|
||||
addMessagesToHistory(&messages, config.SessionManager, cli, conversationMessages...)
|
||||
}
|
||||
// runInteractiveLoop handles the interactive portion of the agentic loop.
|
||||
// Deprecated: replaced by runInteractiveModeBubbleTea; will be deleted in TAS-28.
|
||||
func runInteractiveLoop(_ context.Context, _ *agent.Agent, _ *ui.CLI, _ []fantasy.Message, _ AgenticLoopConfig, _ *hooks.Executor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// runNonInteractiveMode handles the non-interactive mode execution
|
||||
|
||||
@@ -2,12 +2,10 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/fantasy"
|
||||
"charm.land/lipgloss/v2"
|
||||
"golang.org/x/term"
|
||||
@@ -27,8 +25,6 @@ type CLI struct {
|
||||
compactMode bool
|
||||
debug bool
|
||||
modelName string
|
||||
streamProgram *tea.Program // active Bubble Tea program for streaming display
|
||||
streamDone chan struct{} // closed when the streaming program exits
|
||||
}
|
||||
|
||||
// NewCLI creates and initializes a new CLI instance with the specified display modes.
|
||||
@@ -81,49 +77,6 @@ func (c *CLI) SetModelName(modelName string) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetPrompt displays an interactive prompt and waits for user input. It provides
|
||||
// slash command support, multi-line editing, and cancellation handling. Returns
|
||||
// the user's input as a string, or an error if the operation was cancelled or
|
||||
// failed. Returns io.EOF for clean exit signals.
|
||||
func (c *CLI) GetPrompt() (string, error) {
|
||||
// Usage info is now displayed immediately after responses via DisplayUsageAfterResponse()
|
||||
// No need to display it here to avoid duplication
|
||||
|
||||
c.finishStreaming() // ensure any active streaming display is stopped
|
||||
c.messageContainer.messages = nil // clear previous messages (they should have been printed already)
|
||||
|
||||
// No divider needed - removed for cleaner appearance
|
||||
|
||||
// Create our custom slash command input
|
||||
input := NewSlashCommandInput(c.width, "Enter your prompt (Type /help for commands, Ctrl+C to quit, ESC to cancel generation)")
|
||||
|
||||
// Run as a tea program
|
||||
p := tea.NewProgram(input)
|
||||
finalModel, err := p.Run()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get the value from the final model
|
||||
if finalInput, ok := finalModel.(*SlashCommandInput); ok {
|
||||
// Clear the input field from the display
|
||||
linesToClear := finalInput.RenderedLines()
|
||||
// We need to clear linesToClear - 1 lines because we're already on the line after the last rendered line
|
||||
for i := 0; i < linesToClear-1; i++ {
|
||||
fmt.Print("\033[1A\033[2K") // Move up one line and clear it
|
||||
}
|
||||
|
||||
if finalInput.Cancelled() {
|
||||
return "", io.EOF // Signal clean exit
|
||||
}
|
||||
value := strings.TrimSpace(finalInput.Value())
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unexpected model type")
|
||||
}
|
||||
|
||||
// ShowSpinner displays an animated spinner with the specified message while
|
||||
// executing the provided action function. The spinner automatically stops when
|
||||
// the action completes. Returns any error returned by the action function.
|
||||
@@ -163,7 +116,6 @@ func (c *CLI) DisplayAssistantMessage(message string) error {
|
||||
// with the specified model name shown in the message header. The message is
|
||||
// formatted according to the current display mode and includes timestamp information.
|
||||
func (c *CLI) DisplayAssistantMessageWithModel(message, modelName string) error {
|
||||
c.finishStreaming() // ensure streaming display is stopped before printing
|
||||
var msg UIMessage
|
||||
if c.compactMode {
|
||||
msg = c.compactRenderer.RenderAssistantMessage(message, time.Now(), modelName)
|
||||
@@ -179,9 +131,6 @@ func (c *CLI) DisplayAssistantMessageWithModel(message, modelName string) error
|
||||
// is being executed. Shows the tool name and its arguments formatted appropriately
|
||||
// for the current display mode. This is typically shown while a tool is running.
|
||||
func (c *CLI) DisplayToolCallMessage(toolName, toolArgs string) {
|
||||
c.finishStreaming() // ensure any active streaming display is stopped
|
||||
c.messageContainer.messages = nil // clear previous messages (they should have been printed already)
|
||||
|
||||
var msg UIMessage
|
||||
if c.compactMode {
|
||||
msg = c.compactRenderer.RenderToolCallMessage(toolName, toolArgs, time.Now())
|
||||
@@ -210,52 +159,10 @@ func (c *CLI) DisplayToolMessage(toolName, toolArgs, toolResult string, isError
|
||||
c.displayContainer()
|
||||
}
|
||||
|
||||
// StartStreamingMessage initializes a new streaming message display for real-time
|
||||
// AI responses. A Bubble Tea program is started to handle flicker-free in-place
|
||||
// updates using synchronized output and proper cursor management.
|
||||
// The modelName parameter indicates which AI model is generating the response.
|
||||
func (c *CLI) StartStreamingMessage(modelName string) {
|
||||
c.finishStreaming() // stop any previous streaming program
|
||||
|
||||
model := newStreamingDisplay(c.compactMode, c.width, modelName)
|
||||
c.streamDone = make(chan struct{})
|
||||
c.streamProgram = tea.NewProgram(model, tea.WithInput(nil))
|
||||
|
||||
done := c.streamDone
|
||||
p := c.streamProgram
|
||||
go func() {
|
||||
_, _ = p.Run()
|
||||
close(done)
|
||||
}()
|
||||
}
|
||||
|
||||
// UpdateStreamingMessage updates the currently streaming message with new content.
|
||||
// This method should be called after StartStreamingMessage to progressively display
|
||||
// AI responses as they are generated in real-time.
|
||||
func (c *CLI) UpdateStreamingMessage(content string) {
|
||||
if c.streamProgram != nil {
|
||||
c.streamProgram.Send(streamContentMsg(content))
|
||||
}
|
||||
}
|
||||
|
||||
// finishStreaming stops the active streaming Bubble Tea program, if any.
|
||||
// It sends a quit message and waits for the program to exit cleanly.
|
||||
// This is idempotent and safe to call when no streaming is active.
|
||||
func (c *CLI) finishStreaming() {
|
||||
if c.streamProgram == nil {
|
||||
return
|
||||
}
|
||||
c.streamProgram.Send(streamDoneMsg{})
|
||||
<-c.streamDone // wait for the program goroutine to exit
|
||||
c.streamProgram = nil
|
||||
c.streamDone = nil
|
||||
}
|
||||
|
||||
// DisplayError renders and displays an error message with distinctive formatting
|
||||
// to ensure visibility. The error is timestamped and styled according to the
|
||||
// current display mode's error theme.
|
||||
func (c *CLI) DisplayError(err error) {
|
||||
c.finishStreaming() // ensure streaming display is stopped before printing
|
||||
var msg UIMessage
|
||||
if c.compactMode {
|
||||
msg = c.compactRenderer.RenderErrorMessage(err.Error(), time.Now())
|
||||
@@ -283,7 +190,6 @@ func (c *CLI) DisplayInfo(message string) {
|
||||
// DisplayCancellation displays a system message indicating that the current
|
||||
// AI generation has been cancelled by the user (typically via ESC key).
|
||||
func (c *CLI) DisplayCancellation() {
|
||||
c.finishStreaming() // ensure streaming display is stopped before printing
|
||||
var msg UIMessage
|
||||
if c.compactMode {
|
||||
msg = c.compactRenderer.RenderSystemMessage("Generation cancelled by user (ESC pressed)", time.Now())
|
||||
@@ -398,22 +304,6 @@ func (c *CLI) IsSlashCommand(input string) bool {
|
||||
return strings.HasPrefix(input, "/")
|
||||
}
|
||||
|
||||
// GetToolApproval asks the user for permission to execute the tool with the given
|
||||
// arguments. Returns true if the user approves.
|
||||
func (c *CLI) GetToolApproval(toolName, toolArgs string) (bool, error) {
|
||||
input := NewToolApprovalInput(toolName, toolArgs, c.width)
|
||||
p := tea.NewProgram(input)
|
||||
finalModel, err := p.Run()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if finalInput, ok := finalModel.(*ToolApprovalInput); ok {
|
||||
return finalInput.approved, nil
|
||||
}
|
||||
return false, fmt.Errorf("GetToolApproval: unexpected error type")
|
||||
}
|
||||
|
||||
// SlashCommandResult encapsulates the outcome of processing a slash command,
|
||||
// indicating whether the command was recognized and handled, and whether the
|
||||
// conversation history should be cleared as a result of the command.
|
||||
@@ -575,8 +465,6 @@ func (c *CLI) ResetUsageStats() {
|
||||
// following an AI response. This provides real-time feedback about the cost and
|
||||
// token consumption of each interaction.
|
||||
func (c *CLI) DisplayUsageAfterResponse() {
|
||||
c.finishStreaming() // ensure streaming display is stopped before printing usage
|
||||
|
||||
if c.usageTracker == nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user