mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
feat(ui): wire interactive path to unified Bubble Tea TUI
Replace the old SetupCLI/runInteractiveLoop flow with a single tea.NewProgram(AppModel) + appInstance.SetProgram() call. NewAppModel now constructs InputComponent and StreamComponent inline so the parent model is fully wired on construction.
This commit is contained in:
+45
-3
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcphost/internal/agent"
|
||||
"github.com/mark3labs/mcphost/internal/app"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/mark3labs/mcphost/internal/ui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -774,8 +776,7 @@ func runNormalMode(ctx context.Context) error {
|
||||
return fmt.Errorf("--quiet flag can only be used with --prompt/-p")
|
||||
}
|
||||
|
||||
_ = appInstance // TODO(TAS-19): wire appInstance into the interactive tea.Program
|
||||
return runInteractiveMode(ctx, mcpAgent, cli, serverNames, toolNames, modelName, messages, sessionManager, hookExecutor, approveToolRun)
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, cli, modelName)
|
||||
}
|
||||
|
||||
// AgenticLoopConfig configures the behavior of the unified agentic loop.
|
||||
@@ -1373,7 +1374,7 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C
|
||||
return runAgenticLoop(ctx, mcpAgent, cli, messages, config, hookExecutor)
|
||||
}
|
||||
|
||||
// runInteractiveMode handles the interactive mode execution
|
||||
// runInteractiveMode handles the interactive mode execution (legacy path, kept for reference)
|
||||
func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, serverNames, toolNames []string, modelName string, messages []fantasy.Message, sessionManager *session.Manager, hookExecutor *hooks.Executor, approveToolRun bool) error {
|
||||
// Configure and run unified agentic loop
|
||||
config := AgenticLoopConfig{
|
||||
@@ -1391,3 +1392,44 @@ func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI,
|
||||
|
||||
return runAgenticLoop(ctx, mcpAgent, cli, messages, config, hookExecutor)
|
||||
}
|
||||
|
||||
// runInteractiveModeBubbleTea starts the new unified Bubble Tea interactive TUI.
|
||||
//
|
||||
// It:
|
||||
// 1. Gets the terminal dimensions (falls back to 80x24 if unavailable).
|
||||
// 2. Creates a ui.AppModel (parent model) with the appInstance as the controller,
|
||||
// wiring up all child components (InputComponent, StreamComponent).
|
||||
// 3. Creates a single tea.NewProgram and registers it with appInstance via SetProgram
|
||||
// so that agent events are routed to the TUI.
|
||||
// 4. Calls program.Run() which blocks until the user quits (Ctrl+C or /quit).
|
||||
//
|
||||
// The old SetupCLI / runInteractiveLoop flow is bypassed entirely for interactive mode.
|
||||
// cli may be nil if SetupCLI was not called (quiet path); its debug output is displayed
|
||||
// before handing control to the TUI.
|
||||
func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, _ *ui.CLI, modelName string) error {
|
||||
// Determine terminal size; fall back gracefully.
|
||||
termWidth, termHeight, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil || termWidth == 0 {
|
||||
termWidth = 80
|
||||
termHeight = 24
|
||||
}
|
||||
|
||||
// Display startup info via the legacy CLI before handing off to TUI.
|
||||
// (The CLI was already initialised with model info, tool counts, etc. in runNormalMode.)
|
||||
// Nothing additional needed here — factory.SetupCLI already displayed it above.
|
||||
|
||||
appModel := ui.NewAppModel(appInstance, ui.AppModelOptions{
|
||||
CompactMode: viper.GetBool("compact"),
|
||||
ModelName: modelName,
|
||||
Width: termWidth,
|
||||
Height: termHeight,
|
||||
})
|
||||
|
||||
program := tea.NewProgram(appModel)
|
||||
|
||||
// Register the program with the app layer so agent events are sent to the TUI.
|
||||
appInstance.SetProgram(program)
|
||||
|
||||
_, runErr := program.Run()
|
||||
return runErr
|
||||
}
|
||||
|
||||
@@ -155,6 +155,10 @@ type approvalComponentIface interface {
|
||||
//
|
||||
// To use with the concrete *app.App type, pass it directly — *app.App
|
||||
// satisfies AppController once the app layer is implemented (TAS-4).
|
||||
//
|
||||
// NewAppModel constructs all child components (InputComponent, StreamComponent)
|
||||
// using the provided options. ApprovalComponent is created dynamically per-step
|
||||
// in Update when a ToolApprovalNeededEvent arrives.
|
||||
func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel {
|
||||
width := opts.Width
|
||||
if width == 0 {
|
||||
@@ -176,8 +180,9 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel {
|
||||
height: height,
|
||||
}
|
||||
|
||||
// Child components are nil until they are attached via setters or until the
|
||||
// concrete implementations are in place (TAS-15, TAS-16, TAS-17).
|
||||
// Wire up child components now that we have the concrete implementations.
|
||||
m.input = NewInputComponent(width, "Enter your prompt (Type /help for commands, Ctrl+C to quit)", appCtrl)
|
||||
m.stream = NewStreamComponent(opts.CompactMode, width, opts.ModelName)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user