Files
coder/cli/exp_agents_e2e_test.go
T
Michael Suchacz de30488b20 feat(cli): add experimental agents TUI (#24150)
> This PR was authored by Mux on behalf of Mike.

Adds `coder exp agents`, an interactive terminal UI for managing Coder
AI agent chats. Built with bubbletea/lipgloss/glamour, the TUI provides
parity with the web dashboard for chat management, model selection, and
real-time tool execution visibility.

## What it does

- **Chat list view**: tree-based navigation with nested subagent
expansion, search filtering, windowed scrolling, and pagination.
- **Active chat view**: viewport-based transcript with markdown
rendering, WebSocket streaming, and a text input composer for sending
messages.
- **Model picker overlay**: cached model catalog with fuzzy selection.
- **Diff drawer overlay**: git changes inspection with unified diff
rendering.
- **Tool call rendering**: humanized argument summaries, consecutive
duplicate collapsing, and status indicators.

## Key implementation details

- Session lifecycle uses a monotonic `chatGeneration` counter so async
responses from stale sessions are dropped on chat switch.
- Draft mode guards prevent duplicate chat creation on double-Enter.
- Error and loading states render inline without collapsing the TUI
chrome.
- Glamour renderer access is mutex-protected (not thread-safe).
- Intentional WebSocket close is distinguished from dropped connections
to prevent spurious reconnects.

## Testing

~220 unit tests covering rendering, state transitions, keyboard
dispatch, and edge cases. 4-scenario PTY-based E2E suite covers boot,
navigation, search, and direct chat open.

14 new files, ~7,400 lines added.
2026-04-17 12:16:06 +02:00

88 lines
2.2 KiB
Go

package cli_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/testutil"
)
func TestExpAgentsE2E(t *testing.T) {
t.Parallel()
t.Run("EmptyStateBoot", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, _, _ := setupExpAgentsBackend(t)
session := startExpAgentsSession(t, ctx, client)
session.expect(ctx, "No chats yet. Press n to start a new chat.")
session.quit()
require.NoError(t, session.wait(ctx))
})
t.Run("ListAndNavigate", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, expClient, orgID := setupExpAgentsBackend(t)
_ = seedChat(t, ctx, expClient, orgID, "alpha nav seed")
_ = seedChat(t, ctx, expClient, orgID, "bravo nav seed")
_ = seedChat(t, ctx, expClient, orgID, "charlie nav seed")
session := startExpAgentsSession(t, ctx, client)
session.expect(ctx, "charlie nav seed")
session.expect(ctx, "enter: open")
session.enter()
session.expect(ctx, "esc")
session.esc()
session.expect(ctx, "enter: open")
session.quit()
require.NoError(t, session.wait(ctx))
})
t.Run("SearchFilter", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, expClient, orgID := setupExpAgentsBackend(t)
_ = seedChat(t, ctx, expClient, orgID, "alpha filter seed")
_ = seedChat(t, ctx, expClient, orgID, "zulu filter seed")
session := startExpAgentsSession(t, ctx, client)
session.expect(ctx, "alpha filter seed")
session.expect(ctx, "enter: open")
session.writeRune('/')
session.expect(ctx, "/ ")
for _, r := range "zzzznotamatch" {
session.writeRune(r)
}
session.expect(ctx, "No matches.")
session.ctrlC()
require.NoError(t, session.wait(ctx))
})
t.Run("ExistingChatHistory", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, expClient, orgID := setupExpAgentsBackend(t)
chat := seedChat(t, ctx, expClient, orgID, "direct open seed")
session := startExpAgentsSession(t, ctx, client, chat.ID.String())
session.expect(ctx, "direct open seed")
session.expect(ctx, "esc")
session.esc()
session.expect(ctx, "enter: open")
session.quit()
require.NoError(t, session.wait(ctx))
})
}