mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
de30488b20
> 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.
88 lines
2.2 KiB
Go
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))
|
|
})
|
|
}
|