Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f62e8e5
feat(tui): add terminal UI dashboard with Bubble Tea
tjsingleton Feb 9, 2026
202a801
test(tui): add comprehensive unit tests for model and views
tjsingleton Feb 9, 2026
25e842b
test(tui): add critical tests for server actions and edge cases
tjsingleton Feb 9, 2026
b587552
fix(tui): use Client interface instead of concrete type for testability
tjsingleton Feb 9, 2026
b986bcf
fix(tui): match ListActivities signature with cliclient.ActivityFilte…
tjsingleton Feb 9, 2026
74b06d3
fix(tui): use switch statement to satisfy staticcheck QF1003
tjsingleton Feb 9, 2026
15f3a1e
feat(tui): shared styling layer with AdaptiveColor and OAuth refresh-all
tjsingleton Feb 9, 2026
6a68953
fix(tui): surface action errors, accept context, Unicode-safe truncation
tjsingleton Feb 9, 2026
7d35bdb
fix(tui): validate refresh interval, clamp cursor, strengthen tests
tjsingleton Feb 9, 2026
57f5db0
feat(tui): add sort indicators and filter badges to rendering
tjsingleton Feb 10, 2026
3ba054a
fix(tui): replace deprecated strings.Title with manual capitalization
tjsingleton Feb 10, 2026
288ba45
test(tui): fix unused variable warnings in tests
tjsingleton Feb 10, 2026
fb98964
feat(tui): implement keyboard handlers and mode switching for stable …
tjsingleton Feb 10, 2026
5e96ea8
feat(tui): implement sort and filter state management
tjsingleton Feb 10, 2026
064b671
refactor(tui): consolidate triggerOAuthRefresh to model.go
tjsingleton Feb 10, 2026
b7b0234
fix(tui): clean up keyboard handler imports and remove duplicate trig…
tjsingleton Feb 10, 2026
7d7ca6e
fix(tui): iterate through servers for OAuth refresh instead of empty …
tjsingleton Feb 10, 2026
1a56f26
test(tui): add comprehensive tests for handlers and views to reach 80…
tjsingleton Feb 10, 2026
e269880
fix(tui): apply filters and sorting to rendered views
tjsingleton Feb 10, 2026
fe88242
fix(tui): correct sort mode help text to show single-letter commands
tjsingleton Feb 10, 2026
9a136f5
test(tui): add comprehensive E2E test suite covering full user workflows
tjsingleton Feb 10, 2026
bb6a7f3
docs(tui): add comprehensive E2E testing guide
tjsingleton Feb 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/mcpproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ func main() {
// Add activity command
activityCmd := GetActivityCommand()

// Add TUI command
tuiCmd := GetTUICommand()

// Add commands to root
rootCmd.AddCommand(serverCmd)
rootCmd.AddCommand(searchCmd)
Expand All @@ -170,6 +173,7 @@ func main() {
rootCmd.AddCommand(upstreamCmd)
rootCmd.AddCommand(doctorCmd)
rootCmd.AddCommand(activityCmd)
rootCmd.AddCommand(tuiCmd)

// Setup --help-json for machine-readable help discovery
// This must be called AFTER all commands are added
Expand Down
75 changes: 75 additions & 0 deletions cmd/mcpproxy/tui_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"context"
"fmt"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"

"github.com/smart-mcp-proxy/mcpproxy-go/internal/cliclient"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/logs"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/socket"
"github.com/smart-mcp-proxy/mcpproxy-go/internal/tui"
)

// GetTUICommand creates the TUI subcommand.
func GetTUICommand() *cobra.Command {
var refreshSeconds int

cmd := &cobra.Command{
Use: "tui",
Short: "Launch the terminal UI dashboard",
Long: "Launch an interactive terminal UI for monitoring servers, OAuth tokens, and activity.",
RunE: func(cmd *cobra.Command, _ []string) error {
cmdLogLevel, _ := cmd.Flags().GetString("log-level")
cmdLogToFile, _ := cmd.Flags().GetBool("log-to-file")
cmdLogDir, _ := cmd.Flags().GetString("log-dir")

logger, err := logs.SetupCommandLogger(false, cmdLogLevel, cmdLogToFile, cmdLogDir)
if err != nil {
return fmt.Errorf("failed to setup logger: %w", err)
}
defer func() { _ = logger.Sync() }()

// Load config to find daemon connection
cfg, err := config.Load()
if err != nil {
cfg = config.DefaultConfig()
}

// Detect socket or fall back to TCP
socketPath := socket.DetectSocketPath(cfg.DataDir)
var endpoint string
if socket.IsSocketAvailable(socketPath) {
endpoint = socketPath
} else {
endpoint = fmt.Sprintf("http://%s", cfg.Listen)
}

client := cliclient.NewClientWithAPIKey(endpoint, cfg.APIKey, logger.Sugar())

ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()

refreshInterval := time.Duration(refreshSeconds) * time.Second
if refreshInterval < 1*time.Second {
return fmt.Errorf("--refresh must be at least 1 (got %d)", refreshSeconds)
}
m := tui.NewModel(ctx, client, refreshInterval)

p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
return fmt.Errorf("TUI error: %w", err)
}

return nil
},
}

cmd.Flags().IntVar(&refreshSeconds, "refresh", 5, "Refresh interval in seconds")

return cmd
}
147 changes: 147 additions & 0 deletions docs/E2E_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# TUI End-to-End Testing

## Overview

The TUI module includes a comprehensive E2E test suite covering complete user interaction workflows. These tests verify that individual components (handlers, renderers, state management) work correctly together in realistic usage scenarios.

## Test Coverage

**File**: `internal/tui/e2e_test.go`
**Tests**: 18 end-to-end workflow tests
**Coverage**: 87.5% of statements
**All tests pass with `-race` flag** for concurrency safety

## Test Categories

### Navigation & Cursor Movement
- **TestE2ECursorNavigation** - Tests j/k key navigation with boundary checks
- **TestE2ESequentialKeyPresses** - Tests handling multiple key presses in sequence

### Filtering
- **TestE2EFilterWorkflow** - Complete filter mode workflow (enter, navigate, exit)
- **TestE2EClearFiltersWorkflow** - Clearing all active filters
- **TestE2EFilterSummaryDisplay** - Filter badges display in view
- **TestE2EMultipleFiltersApply** - Applying multiple filters simultaneously

### Sorting
- **TestE2ESortWorkflow** - Complete sort mode workflow with indicators
- **TestE2ETabbedSortingByTab** - Sort columns and rendering by tab

### Tab Management
- **TestE2ETabSwitching** - Switching between servers/activity tabs and state preservation

### OAuth
- **TestE2EOAuthRefreshWorkflow** - OAuth refresh trigger via 'o' key

### Display & Rendering
- **TestE2EHealthStatusDisplay** - Health indicator rendering (●, ◐, ○)
- **TestE2EHelpDisplay** - Tab-aware help text display
- **TestE2ELongServerNames** - Name truncation for long names
- **TestE2EResponseToWindowResize** - Terminal size change handling
- **TestE2EEmptyState** - Empty list behavior

### Commands
- **TestE2EQuitCommand** - Quit ('q') command
- **TestE2ERefreshCommand** - Refresh ('r') command

## Running Tests

### All E2E tests
```bash
go test ./internal/tui/... -v -run E2E -race
```

### All TUI tests (unit + E2E)
```bash
go test ./internal/tui/... -race
```

### Coverage report
```bash
go test ./internal/tui/... -cover
```

### Verbose output
```bash
go test ./internal/tui/... -v -race
```

## Test Structure

Each E2E test follows this pattern:

1. **Setup** - Create model with test data
2. **Execute** - Simulate user interactions (key presses)
3. **Verify** - Assert expected state and rendering output

### Example: Filter Workflow
```go
// Create model with servers
m := NewModel(context.Background(), client, 5*time.Second)
m.servers = []serverInfo{ ... }

// Enter filter mode (press 'f')
result, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'f'}})
m = result.(model)

// Verify state
assert.Equal(t, ModeFilterEdit, m.uiMode)
```

## Key Testing Insights

### Model Updates
- The `Update()` method returns `(tea.Model, tea.Cmd)`
- Must type-assert back to `model`: `m = result.(model)`
- Commands are typically nil for these tests (real commands execute in Bubble Tea)

### State Transitions
- Pressing Escape in filter mode resets cursor to 0
- Filters are cleared when exiting filter mode
- Tab switching preserves cursor position per tab

### Rendering
- `renderServers()` and `renderActivity()` require height parameter for visible rows
- Health indicators and filter badges are included in view output
- Names are truncated to fit column width

### Mode System
The TUI has 5 modes:
- **ModeNormal** - Navigation mode
- **ModeFilterEdit** - Filter editing mode
- **ModeSortSelect** - Sort selection mode
- **ModeSearch** - Search mode (optional)
- **ModeHelp** - Help mode (optional)

## Integration with CI

These tests run as part of the standard test suite:
```bash
go test -race ./internal/tui/...
```

No additional dependencies are required—tests use only Go standard library and existing test frameworks (testify).

## Future Enhancements

Potential areas for additional E2E tests:
1. Search mode workflow (when implemented)
2. Help mode details (when implemented)
3. Performance testing with large server lists
4. Unicode/emoji handling edge cases
5. Accessibility features testing

## Debugging Failed E2E Tests

1. **Check test output** - Detailed assert messages show expected vs actual
2. **Add debug prints** - Use `t.Logf()` to print model state
3. **Verify handlers** - Check `internal/tui/handlers.go` for key handling logic
4. **Check renders** - Verify `internal/tui/views.go` for display logic
5. **Run individual test** - Use `-run TestName` to isolate and debug

## References

- **Bubble Tea Framework**: https://github.com/charmbracelet/bubbletea
- **TUI Architecture**: See `internal/tui/model.go` for state management
- **Handler Logic**: See `internal/tui/handlers.go` for key bindings
- **Rendering**: See `internal/tui/views.go` for display logic
77 changes: 77 additions & 0 deletions docs/tui-research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Terminal UI Research Report

## CLI Structure & Cobra Command Organization

**Location:** `cmd/mcpproxy/main.go`

**Pattern:**
- Root command with subcommands added via `rootCmd.AddCommand()`
- Entry point: `GetXCommand()` functions that return `*cobra.Command`
- Existing commands: `serve`, `upstream`, `activity`, `tools`, `auth`, `code`, `secrets`, `doctor`, `search-servers`, `trust-cert`

**TUI Integration Point:**
- Create `cmd/mcpproxy/tui_cmd.go` with a `GetTUICommand()` function
- Register via `rootCmd.AddCommand(GetTUICommand())`

## REST API Client

**Location:** `internal/cliclient/client.go`

**Key Methods:**
- `GetServers(ctx)` - `GET /api/v1/servers` -> `[]map[string]interface{}`
- `ServerAction(ctx, name, action)` - `POST /api/v1/servers/{name}/{action}`
- `GetServerLogs(ctx, name, tail)` - `GET /api/v1/servers/{name}/logs`
- `ListActivities(ctx, filter)` - `GET /api/v1/activity`
- `GetActivitySummary(ctx, period, groupBy)` - `GET /api/v1/activity/summary`
- `TriggerOAuthLogin(ctx, name)` - `POST /api/v1/servers/{name}/login`
- `TriggerOAuthLogout(ctx, name)` - `POST /api/v1/servers/{name}/logout`
- `Ping(ctx)` - `GET /api/v1/status`
- `GetDiagnostics(ctx)` - `GET /api/v1/diagnostics`
- `GetInfo(ctx)` - `GET /api/v1/info`

**Connection:** Supports Unix sockets, named pipes, and TCP.

## Key Data Models (`internal/contracts/types.go`)

### Server
```go
type Server struct {
Name, Status, LastError, OAuthStatus string
Enabled, Quarantined, Connected bool
ToolCount int
TokenExpiresAt *time.Time
Health *HealthStatus
}
```

### HealthStatus
```go
type HealthStatus struct {
Level string // "healthy" | "degraded" | "unhealthy"
AdminState string // "enabled" | "disabled" | "quarantined"
Summary string // e.g. "Connected (5 tools)"
Detail string
Action string // "login" | "restart" | "enable" | "approve" | ""
}
```

### SSE Events (`internal/runtime/events.go`)
- `EventTypeServersChanged` - Server state change
- `EventTypeConfigReloaded` - Config file reloaded
- `EventTypeOAuthTokenRefreshed` - Token refresh success
- `EventTypeOAuthRefreshFailed` - Token refresh failure
- `EventTypeActivityToolCallStarted/Completed` - Activity

## Socket Detection

```go
socketPath := socket.DetectSocketPath(dataDir)
isAvailable := socket.IsSocketAvailable(socketPath)
```

## Config Loading

```go
cfg, _ := config.LoadFromFile("")
cfg, _ := config.Load()
```
Loading
Loading