chore: remove agents experiment flag and mark feature as beta (#24432)

Remove the `ExperimentAgents` feature flag so the Agents feature is
always available without requiring `--experiments=agents`. The feature
is now in beta.

Existing deployments that still pass `--experiments=agents` will get a
harmless "ignoring unknown experiment" warning on startup.

### Changes

**Backend:**
- Remove `RequireExperimentWithDevBypass` middleware from chat and MCP
server routes
- Always include `AgentsAccessRole` in assignable site roles (later
refactored to org-scoped on main; rebase keeps that)
- Always set `AgentsTabVisible = true`, then drop the entire dead
`AgentsTabVisible` metadata pipeline (Go htmlState field,
populateHTMLState goroutine, HTML meta tag, useEmbeddedMetadata
registration, mock); no production consumer reads it. `AgentsNavItem`
already gates on `permissions.createChat`.
- Make `blob:` CSP `img-src` addition unconditional
- Remove `ExperimentAgents` constant, `DisplayName` case, and
`ExperimentsKnown` entry

**CLI:**
- Graduate the agents TUI from `coder exp agents` to `coder agents`
(moved from `AGPLExperimental()` to `CoreSubcommands()`)
- Drop the `agent` alias so it does not collide with the hidden
workspace-agent command
- Rename implementation files `cli/exp_agents_*.go` -> `cli/agents_*.go`
and internal identifiers (`expChatsTUIModel` -> `chatsTUIModel`,
`newExpChatsTUIModel` -> `newChatsTUIModel`, `setupExpAgentsBackend` ->
`setupAgentsBackend`, `startExpAgentsSession` -> `startAgentsSession`,
`expAgentsPtr` -> `agentsPtr`, `expAgentsSession` -> `agentsSession`,
`TestExpAgents*` -> `TestAgents*`). `expClient` (the
`*codersdk.ExperimentalClient` local) is kept; `coderd/exp_chats*.go`
and other still-experimental `cli/exp_*.go` commands are intentionally
untouched.

**Frontend:**
- Remove experiment check from `AgentsNavItem` - render when
`canCreateChat` is true
- Remove `agentsEnabled` experiment check from `WorkspacesPage`, then
gate `chatsByWorkspace` on `permissions.createChat` so users without
chat access don't trigger the per-page DB query (Copilot review
feedback)
- Add `FeatureStageBadge` (beta) next to the Coder logo in the Agents
sidebar (desktop + mobile)

**Docs:**
- Remove experiment flag setup instructions from `early-access.md` and
`getting-started.md` (and rename `early-access.md`'s "Enable Coder
Agents" heading to "Set up Coder Agents", since there is no enablement
step left)
- Update `chats-api.md` and `getting-started.md`'s Chats API note to say
"beta" instead of "experimental"
- `docs/manifest.json`: drop "experimental" from the Chats API sidebar
description
- `make gen` regenerated `docs/reference/cli/agents.md` and the CLI
index
- `scripts/check_emdash.sh`: exclude `cli/testdata/*.golden` and
`enterprise/cli/testdata/*.golden` from the new repo-wide emdash lint,
since serpent emits emdash borders in every generated `--help` golden
file

**Tests:**
- Remove `ExperimentAgents` setup from all test files (14 occurrences
across 7 files)
- Update stale "with the agents experiment" comments in
`coderd/x/chatd/integration_test.go` and `coderd/mcp_test.go`


<img width="1185" height="900" alt="image"
src="https://github.com/user-attachments/assets/b420bc8f-41d6-42c6-abd8-ad572533d651"
/>


> 🤖 Generated by Coder Agents
This commit is contained in:
Dean Sheather
2026-05-01 01:49:00 +10:00
committed by GitHub
parent b975262a97
commit e57525002c
48 changed files with 214 additions and 265 deletions
+5 -7
View File
@@ -2,7 +2,6 @@ package cli
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
@@ -82,9 +81,8 @@ func (r *RootCmd) agentsCommand() *serpent.Command {
)
return &serpent.Command{
Use: "agents [chat-id]",
Short: "Interactive terminal UI for AI agents.",
Aliases: []string{"agent"},
Use: "agents [chat-id]",
Short: "Interactive terminal UI for AI agents.",
Options: serpent.OptionSet{
{
Name: "workspace",
@@ -152,7 +150,7 @@ func (r *RootCmd) agentsCommand() *serpent.Command {
)
renderer.SetHasDarkBackground(true)
model := newExpChatsTUIModel(inv.Context(), expClient, initialChatID, workspaceID, modelID, defaultOrgID)
model := newChatsTUIModel(inv.Context(), expClient, initialChatID, workspaceID, modelID, defaultOrgID)
model.setRenderer(renderer)
program := tea.NewProgram(
model,
@@ -171,8 +169,8 @@ func (r *RootCmd) agentsCommand() *serpent.Command {
return err
}
if _, ok := runModel.(expChatsTUIModel); !ok {
return xerrors.New(fmt.Sprintf("unknown model found %T (%+v)", runModel, runModel))
if _, ok := runModel.(chatsTUIModel); !ok {
return xerrors.Errorf("unknown model found %T (%+v)", runModel, runModel)
}
return nil
@@ -16,15 +16,14 @@ import (
"github.com/coder/coder/v2/testutil"
)
func expAgentsPtr[T any](v T) *T {
func agentsPtr[T any](v T) *T {
return &v
}
func setupExpAgentsBackend(t *testing.T) (*codersdk.Client, *codersdk.ExperimentalClient, uuid.UUID) {
func setupAgentsBackend(t *testing.T) (*codersdk.Client, *codersdk.ExperimentalClient, uuid.UUID) {
t.Helper()
values := coderdtest.DeploymentValues(t)
values.Experiments = []string{string(codersdk.ExperimentAgents)}
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: values,
@@ -43,8 +42,8 @@ func setupExpAgentsBackend(t *testing.T) (*codersdk.Client, *codersdk.Experiment
_, err = expClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4o-mini",
ContextLimit: expAgentsPtr(int64(4096)),
IsDefault: expAgentsPtr(true),
ContextLimit: agentsPtr(int64(4096)),
IsDefault: agentsPtr(true),
})
require.NoError(t, err)
@@ -68,59 +67,59 @@ func seedChat(t *testing.T, ctx context.Context, expClient *codersdk.Experimenta
return chat
}
type expAgentsSession struct {
type agentsSession struct {
t *testing.T
pty *ptytest.PTY
errCh <-chan error
}
func (s *expAgentsSession) expect(ctx context.Context, text string) {
func (s *agentsSession) expect(ctx context.Context, text string) {
s.t.Helper()
s.pty.ExpectMatchContext(ctx, text)
}
func (s *expAgentsSession) wait(ctx context.Context) error {
func (s *agentsSession) wait(ctx context.Context) error {
s.t.Helper()
return testutil.RequireReceive(ctx, s.t, s.errCh)
}
//nolint:unused // Kept as a small PTY helper for future multi-character input.
func (s *expAgentsSession) write(text string) {
func (s *agentsSession) write(text string) {
s.t.Helper()
s.pty.WriteLine(text)
}
func (s *expAgentsSession) writeRune(r rune) {
func (s *agentsSession) writeRune(r rune) {
s.t.Helper()
_, err := s.pty.Input().Write([]byte(string(r)))
require.NoError(s.t, err)
}
func (s *expAgentsSession) enter() {
func (s *agentsSession) enter() {
s.t.Helper()
_, err := s.pty.Input().Write([]byte("\r"))
require.NoError(s.t, err)
}
func (s *expAgentsSession) esc() {
func (s *agentsSession) esc() {
s.t.Helper()
_, err := s.pty.Input().Write([]byte("\x1b"))
require.NoError(s.t, err)
}
func (s *expAgentsSession) ctrlC() {
func (s *agentsSession) ctrlC() {
s.t.Helper()
_, err := s.pty.Input().Write([]byte{3})
require.NoError(s.t, err)
}
func (s *expAgentsSession) quit() {
func (s *agentsSession) quit() {
s.t.Helper()
s.writeRune('q')
}
//nolint:revive // Test helper signature keeps t first for consistency with other helpers.
func startExpAgentsSession(t *testing.T, ctx context.Context, client *codersdk.Client, args ...string) *expAgentsSession {
func startAgentsSession(t *testing.T, ctx context.Context, client *codersdk.Client, args ...string) *agentsSession {
t.Helper()
// Reading to / writing from the PTY is flaky on non-linux systems.
@@ -128,7 +127,7 @@ func startExpAgentsSession(t *testing.T, ctx context.Context, client *codersdk.C
t.Skip("skipping on non-linux")
}
fullArgs := append([]string{"exp", "agents"}, args...)
fullArgs := append([]string{"agents"}, args...)
inv, root := clitest.New(t, fullArgs...)
clitest.SetupConfig(t, client, root)
@@ -148,5 +147,5 @@ func startExpAgentsSession(t *testing.T, ctx context.Context, client *codersdk.C
errCh <- inv.WithContext(ctx).Run()
})
return &expAgentsSession{t: t, pty: pty, errCh: errCh}
return &agentsSession{t: t, pty: pty, errCh: errCh}
}
@@ -8,15 +8,15 @@ import (
"github.com/coder/coder/v2/testutil"
)
func TestExpAgentsE2E(t *testing.T) {
func TestAgentsE2E(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)
client, _, _ := setupAgentsBackend(t)
session := startAgentsSession(t, ctx, client)
session.expect(ctx, "No chats yet. Press n to start a new chat.")
session.quit()
@@ -27,13 +27,13 @@ func TestExpAgentsE2E(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, expClient, orgID := setupExpAgentsBackend(t)
client, expClient, orgID := setupAgentsBackend(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 := startAgentsSession(t, ctx, client)
session.expect(ctx, "charlie nav seed")
session.expect(ctx, "enter: open")
@@ -49,12 +49,12 @@ func TestExpAgentsE2E(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, expClient, orgID := setupExpAgentsBackend(t)
client, expClient, orgID := setupAgentsBackend(t)
_ = seedChat(t, ctx, expClient, orgID, "alpha filter seed")
_ = seedChat(t, ctx, expClient, orgID, "zulu filter seed")
session := startExpAgentsSession(t, ctx, client)
session := startAgentsSession(t, ctx, client)
session.expect(ctx, "alpha filter seed")
session.expect(ctx, "enter: open")
@@ -72,10 +72,10 @@ func TestExpAgentsE2E(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client, expClient, orgID := setupExpAgentsBackend(t)
client, expClient, orgID := setupAgentsBackend(t)
chat := seedChat(t, ctx, expClient, orgID, "direct open seed")
session := startExpAgentsSession(t, ctx, client, chat.ID.String())
session := startAgentsSession(t, ctx, client, chat.ID.String())
// The initial render contains both the chat title/content
// and the status bar in a single frame. Their relative
+24 -24
View File
@@ -28,8 +28,8 @@ const (
)
type (
terminateTUIMsg struct{}
expChatsTUIModel struct {
terminateTUIMsg struct{}
chatsTUIModel struct {
ctx context.Context
client *codersdk.ExperimentalClient
styles tuiStyles
@@ -49,14 +49,14 @@ type (
}
)
func newExpChatsTUIModel(
func newChatsTUIModel(
ctx context.Context,
client *codersdk.ExperimentalClient,
initialChatID *uuid.UUID,
workspaceID *uuid.UUID,
modelOverride *string,
organizationID uuid.UUID,
) expChatsTUIModel {
) chatsTUIModel {
styles := newTUIStyles()
currentView := viewList
if initialChatID != nil {
@@ -72,7 +72,7 @@ func newExpChatsTUIModel(
chat.historyResolved = false
chatGeneration = 1
}
return expChatsTUIModel{
return chatsTUIModel{
ctx: ctx,
client: client,
styles: styles,
@@ -92,7 +92,7 @@ func newExpChatsTUIModel(
// window dimensions from the previous session, and advances
// the monotonic generation counter so in-flight async messages
// from the old session are ignored.
func (m *expChatsTUIModel) resetChatSession() {
func (m *chatsTUIModel) resetChatSession() {
old := m.chat
m.chat = newChatViewModel(m.ctx, m.client, m.workspaceID, m.modelOverride, m.organizationID, m.styles)
m.chat.width = old.width
@@ -104,7 +104,7 @@ func (m *expChatsTUIModel) resetChatSession() {
m.chat.chatGeneration = m.chatGeneration
}
func (m *expChatsTUIModel) setRenderer(renderer *lipgloss.Renderer) {
func (m *chatsTUIModel) setRenderer(renderer *lipgloss.Renderer) {
styles := newTUIStyles(renderer)
m.styles = styles
m.list.styles = styles
@@ -113,7 +113,7 @@ func (m *expChatsTUIModel) setRenderer(renderer *lipgloss.Renderer) {
m.chat.spinner.Style = styles.dimmedText
}
func (m expChatsTUIModel) Init() tea.Cmd {
func (m chatsTUIModel) Init() tea.Cmd {
if m.initialChatID != nil {
m.chat.activeChatID = *m.initialChatID
return tea.Batch(append([]tea.Cmd{m.chat.Init()}, m.loadChatCmd(*m.initialChatID, m.chat.chatGeneration)...)...)
@@ -121,17 +121,17 @@ func (m expChatsTUIModel) Init() tea.Cmd {
return tea.Batch(m.loadChatsCmd(), m.list.Init())
}
func (m expChatsTUIModel) loadChatsCmd() tea.Cmd {
func (m chatsTUIModel) loadChatsCmd() tea.Cmd {
return apiCmd(func() ([]codersdk.Chat, error) { return m.client.ListChats(m.ctx, nil) }, func(chats []codersdk.Chat, err error) tea.Msg { return chatsListedMsg{chats: chats, err: err} })
}
func (m expChatsTUIModel) loadChatCmd(chatID uuid.UUID, generation uint64) []tea.Cmd {
func (m chatsTUIModel) loadChatCmd(chatID uuid.UUID, generation uint64) []tea.Cmd {
return []tea.Cmd{apiCmd(func() (codersdk.Chat, error) { return m.client.GetChat(m.ctx, chatID) }, func(chat codersdk.Chat, err error) tea.Msg {
return chatOpenedMsg{generation: generation, chatID: chatID, chat: chat, err: err}
}), loadChatHistoryCmd(m.ctx, m.client, chatID, generation)}
}
func (m expChatsTUIModel) childWindowSizeMsg() tea.WindowSizeMsg {
func (m chatsTUIModel) childWindowSizeMsg() tea.WindowSizeMsg {
h := m.height
if m.currentView == viewList {
h = max(0, h-1)
@@ -139,7 +139,7 @@ func (m expChatsTUIModel) childWindowSizeMsg() tea.WindowSizeMsg {
return tea.WindowSizeMsg{Width: m.width, Height: h}
}
func (m *expChatsTUIModel) toggleOverlay(overlay tuiOverlay) bool {
func (m *chatsTUIModel) toggleOverlay(overlay tuiOverlay) bool {
if m.overlay == overlay {
m.overlay = overlayNone
return false
@@ -148,7 +148,7 @@ func (m *expChatsTUIModel) toggleOverlay(overlay tuiOverlay) bool {
return true
}
func (m *expChatsTUIModel) handleEsc(msg tea.KeyMsg) tea.Cmd {
func (m *chatsTUIModel) handleEsc(msg tea.KeyMsg) tea.Cmd {
if m.currentView == viewList && m.list.searching {
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
@@ -175,7 +175,7 @@ func isOverlayCloseKey(msg tea.KeyMsg) bool {
return key == "esc" || key == "ctrl+["
}
func (m *expChatsTUIModel) handleModelPickerKey(msg tea.KeyMsg) tea.Cmd {
func (m *chatsTUIModel) handleModelPickerKey(msg tea.KeyMsg) tea.Cmd {
switch msg.String() {
case "up", "k":
if m.chat.modelPickerCursor > 0 {
@@ -198,7 +198,7 @@ func (m *expChatsTUIModel) handleModelPickerKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *expChatsTUIModel) handleAskUserQuestionKey(msg tea.KeyMsg) tea.Cmd {
func (m *chatsTUIModel) handleAskUserQuestionKey(msg tea.KeyMsg) tea.Cmd {
state := m.chat.pendingAskUserQuestion
if state == nil || state.Submitting || len(state.Questions) == 0 {
return nil
@@ -269,7 +269,7 @@ func (m *expChatsTUIModel) handleAskUserQuestionKey(msg tea.KeyMsg) tea.Cmd {
return nil
}
func (m *expChatsTUIModel) recordAskAnswer(answer, optionLabel string, freeform bool) tea.Cmd {
func (m *chatsTUIModel) recordAskAnswer(answer, optionLabel string, freeform bool) tea.Cmd {
state := m.chat.pendingAskUserQuestion
if state == nil || len(state.Questions) == 0 {
return nil
@@ -305,7 +305,7 @@ func (m *expChatsTUIModel) recordAskAnswer(answer, optionLabel string, freeform
return submitAskUserQuestionCmd(m.client.Client, m.chat.activeChatID, m.chat.chatGeneration, state)
}
func (m *expChatsTUIModel) openChatCmd(chatID *uuid.UUID) tea.Cmd {
func (m *chatsTUIModel) openChatCmd(chatID *uuid.UUID) tea.Cmd {
m.currentView = viewChat
m.chat.stopStream()
m.resetChatSession()
@@ -322,7 +322,7 @@ func (m *expChatsTUIModel) openChatCmd(chatID *uuid.UUID) tea.Cmd {
return tea.Batch(append([]tea.Cmd{m.chat.Init()}, m.loadChatCmd(*chatID, m.chat.chatGeneration)...)...)
}
func (m *expChatsTUIModel) toggleModelPickerCmd() tea.Cmd {
func (m *chatsTUIModel) toggleModelPickerCmd() tea.Cmd {
if !m.toggleOverlay(overlayModelPicker) {
return nil
}
@@ -337,7 +337,7 @@ func (m *expChatsTUIModel) toggleModelPickerCmd() tea.Cmd {
return nil
}
func (m *expChatsTUIModel) toggleDiffDrawerCmd() tea.Cmd {
func (m *chatsTUIModel) toggleDiffDrawerCmd() tea.Cmd {
if m.chat.chat == nil {
return nil
}
@@ -355,7 +355,7 @@ func (m *expChatsTUIModel) toggleDiffDrawerCmd() tea.Cmd {
return nil
}
func (m expChatsTUIModel) updateChild(msg tea.Msg, view tuiView) (expChatsTUIModel, tea.Cmd) {
func (m chatsTUIModel) updateChild(msg tea.Msg, view tuiView) (chatsTUIModel, tea.Cmd) {
var cmd tea.Cmd
if view == viewChat {
m.chat, cmd = m.chat.Update(msg)
@@ -365,11 +365,11 @@ func (m expChatsTUIModel) updateChild(msg tea.Msg, view tuiView) (expChatsTUIMod
return m, cmd
}
func (m expChatsTUIModel) renderOverlay(title, body string) string {
func (m chatsTUIModel) renderOverlay(title, body string) string {
return renderOverlayFrame(m.styles, m.width, m.styles.title.Render(title), body, m.styles.helpText.Render("Esc to close"))
}
func (m expChatsTUIModel) diffOverlayView() string {
func (m chatsTUIModel) diffOverlayView() string {
switch {
case m.chat.diffErr != nil:
return m.renderOverlay("Diff", m.styles.errorText.Render(wrapPreservingNewlines(m.chat.diffErr.Error(), contentWidth(m.width, 6))))
@@ -394,7 +394,7 @@ func padViewHeight(text string, height int) string {
return text + strings.Repeat("\n", height-lineCount)
}
func (m expChatsTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m chatsTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
@@ -480,7 +480,7 @@ func (m expChatsTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateChild(msg, m.currentView)
}
func (m expChatsTUIModel) View() string {
func (m chatsTUIModel) View() string {
if m.quitting {
return ""
}
@@ -16,7 +16,7 @@ import (
var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func TestExpAgentsRender(t *testing.T) {
func TestAgentsRender(t *testing.T) {
t.Parallel()
styles := newTUIStyles()
+39 -39
View File
@@ -21,7 +21,7 @@ import (
"github.com/coder/websocket"
)
func TestExpAgents(t *testing.T) {
func TestAgents(t *testing.T) {
t.Parallel()
t.Run("ResolveModel", func(t *testing.T) {
t.Parallel()
@@ -73,7 +73,7 @@ func TestExpAgents(t *testing.T) {
for _, tt := range tests {
t.Run("EscFromOverlayClosesIt/"+tt.name, func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.overlay = tt.overlay
@@ -99,7 +99,7 @@ func TestExpAgents(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.overlay = tt.overlay
@@ -113,7 +113,7 @@ func TestExpAgents(t *testing.T) {
t.Run("EscFromChatViewReturnsToListAndRefreshes", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.overlay = overlayNone
@@ -126,7 +126,7 @@ func TestExpAgents(t *testing.T) {
t.Run("EscFromChatViewAdvancesGeneration", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.overlay = overlayNone
model.chatGeneration = 4
@@ -142,7 +142,7 @@ func TestExpAgents(t *testing.T) {
t.Run("EscFromChatViewRejectsLateChatLoadMessages", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.overlay = overlayNone
model.chatGeneration = 4
@@ -175,7 +175,7 @@ func TestExpAgents(t *testing.T) {
{ID: uuid.New(), Title: "beta", Status: codersdk.ChatStatusCompleted, CreatedAt: time.Now(), UpdatedAt: time.Now()},
{ID: uuid.New(), Title: "gamma", Status: codersdk.ChatStatusCompleted, CreatedAt: time.Now(), UpdatedAt: time.Now()},
}
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
updatedModel, cmd := model.Update(tea.WindowSizeMsg{Width: 80, Height: 10})
model = mustTUIModel(t, updatedModel, cmd)
model.currentView = viewList
@@ -211,7 +211,7 @@ func TestExpAgents(t *testing.T) {
} {
t.Run("CtrlCQuitsFromAnyState/"+name, func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = view
updatedModel, cmd := model.Update(tea.KeyMsg{Type: tea.KeyCtrlC})
@@ -237,7 +237,7 @@ func TestExpAgents(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.width, model.height = 100, 40
updatedModel, cmd := model.Update(tt.msg)
updated, cmd := mustTUIModelWithCmd(t, updatedModel, cmd)
@@ -259,7 +259,7 @@ func TestExpAgents(t *testing.T) {
})
t.Run("EscFromChatViewRestoresListHeaderAndPadsTerminal", func(t *testing.T) {
t.Parallel()
assertReturnToList := func(t testing.TB, model expChatsTUIModel) {
assertReturnToList := func(t testing.TB, model chatsTUIModel) {
t.Helper()
updatedModel, cmd := model.Update(tea.KeyMsg{Type: tea.KeyEsc})
updated, _ := mustTUIModelWithCmd(t, updatedModel, cmd)
@@ -271,7 +271,7 @@ func TestExpAgents(t *testing.T) {
t.Run("SelectedChat", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
updatedModel, cmd := model.Update(tea.WindowSizeMsg{Width: 80, Height: 12})
model = mustTUIModel(t, updatedModel, cmd)
model.list.loading = false
@@ -292,7 +292,7 @@ func TestExpAgents(t *testing.T) {
t.Run("DraftChat", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
updatedModel, cmd := model.Update(tea.WindowSizeMsg{Width: 80, Height: 12})
model = mustTUIModel(t, updatedModel, cmd)
model.list.loading = false
@@ -308,7 +308,7 @@ func TestExpAgents(t *testing.T) {
t.Run("ChatViewOmitsListHeaderAndLoadingSpinner", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
updatedModel, cmd := model.Update(tea.WindowSizeMsg{Width: 80, Height: 12})
model = mustTUIModel(t, updatedModel, cmd)
model.currentView = viewChat
@@ -336,7 +336,7 @@ func TestExpAgents(t *testing.T) {
t.Run("ReopensModelPickerAfterClosing", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
catalog := codersdk.ChatModelsResponse{
Providers: []codersdk.ChatModelProvider{{
@@ -378,7 +378,7 @@ func TestExpAgents(t *testing.T) {
t.Run("CancelClosesOverlay", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
model.height = 24
@@ -396,7 +396,7 @@ func TestExpAgents(t *testing.T) {
t.Run("EscClosesPickerWithoutLeavingChat", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
model.height = 24
@@ -430,7 +430,7 @@ func TestExpAgents(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
model.height = 24
@@ -456,7 +456,7 @@ func TestExpAgents(t *testing.T) {
t.Run("EnterSelectsModelWithoutSendingDraft", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
model.height = 24
@@ -488,7 +488,7 @@ func TestExpAgents(t *testing.T) {
t.Run("LoadErrorClosesOverlay", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
model.height = 24
@@ -507,7 +507,7 @@ func TestExpAgents(t *testing.T) {
t.Run("ScrollAndSelectModel", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
model.height = 24
@@ -535,7 +535,7 @@ func TestExpAgents(t *testing.T) {
t.Run("DiffDrawerLoadingState", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
chat := testChat(codersdk.ChatStatusCompleted)
model.chat.chat = &chat
@@ -549,7 +549,7 @@ func TestExpAgents(t *testing.T) {
t.Run("DiffDrawerErrorState", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
chat := testChat(codersdk.ChatStatusCompleted)
@@ -565,7 +565,7 @@ func TestExpAgents(t *testing.T) {
t.Run("DiffDrawerMemoizesSummary", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.width = 80
chat := testChat(codersdk.ChatStatusCompleted)
@@ -602,7 +602,7 @@ func TestExpAgents(t *testing.T) {
t.Run("OverlayDismissedOnViewSwitch", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.overlay = overlayModelPicker
@@ -634,7 +634,7 @@ func TestExpAgents(t *testing.T) {
}},
}
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.overlay = overlayModelPicker
model.catalog = &catalog
@@ -665,7 +665,7 @@ func TestExpAgents(t *testing.T) {
}},
}
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
model.catalog = &catalog
model.chat.modelPickerFlat = catalog.Providers[0].Models
@@ -685,7 +685,7 @@ func TestExpAgents(t *testing.T) {
t.Parallel()
firstChatID := uuid.New()
secondChatID := uuid.New()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.width = 100
model.height = 40
@@ -1877,7 +1877,7 @@ func TestExpAgents(t *testing.T) {
t.Run("ChatView/ViewportScrolling", func(t *testing.T) {
t.Parallel()
applyWindowSize := func(t *testing.T, model expChatsTUIModel, width int, height int) expChatsTUIModel {
applyWindowSize := func(t *testing.T, model chatsTUIModel, width int, height int) chatsTUIModel {
t.Helper()
updatedModel, cmd := model.Update(tea.WindowSizeMsg{Width: width, Height: height})
return mustTUIModel(t, updatedModel, cmd)
@@ -2175,7 +2175,7 @@ func TestExpAgents(t *testing.T) {
}},
}
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
updatedModel, cmd := model.Update(modelsListedMsg{catalog: catalog})
@@ -2210,7 +2210,7 @@ func TestExpAgents(t *testing.T) {
t.Parallel()
t.Run("StreamingChatSwitchBackToList", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.currentView = viewChat
chat := testChat(codersdk.ChatStatusRunning)
model.chat.chat = &chat
@@ -2230,7 +2230,7 @@ func TestExpAgents(t *testing.T) {
t.Run("ReOpenSameChatAfterEsc", func(t *testing.T) {
t.Parallel()
chatID := uuid.New()
model := newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
model.width = 100
model.height = 40
@@ -2895,7 +2895,7 @@ func TestExpAgents(t *testing.T) {
t.Run("RecordAskAnswer", func(t *testing.T) {
t.Parallel()
model := newExpChatsTUIModel(context.Background(), failingExperimentalClient(), nil, nil, nil, uuid.Nil)
model := newChatsTUIModel(context.Background(), failingExperimentalClient(), nil, nil, nil, uuid.Nil)
model.chat.activeChatID = uuid.New()
model.chat.chatGeneration = 4
state := newAskUserQuestionState("tool-1", []parsedAskQuestion{firstQuestion})
@@ -3136,7 +3136,7 @@ func TestExpAgents(t *testing.T) {
})
}
func TestExpAgents_View_LongInputFitsTerminal(t *testing.T) {
func TestAgents_View_LongInputFitsTerminal(t *testing.T) {
t.Parallel()
model := newTestChatViewModel(nil)
model.width, model.height = 80, 24
@@ -3162,17 +3162,17 @@ func TestExpAgents_View_LongInputFitsTerminal(t *testing.T) {
require.NotEmpty(t, strings.TrimSpace(lines[len(lines)-1]))
}
func mustTUIModel(t testing.TB, model tea.Model, cmd tea.Cmd) expChatsTUIModel {
func mustTUIModel(t testing.TB, model tea.Model, cmd tea.Cmd) chatsTUIModel {
t.Helper()
updated, ok := model.(expChatsTUIModel)
updated, ok := model.(chatsTUIModel)
require.True(t, ok)
require.Nil(t, cmd)
return updated
}
func mustTUIModelWithCmd(t testing.TB, model tea.Model, cmd tea.Cmd) (expChatsTUIModel, tea.Cmd) {
func mustTUIModelWithCmd(t testing.TB, model tea.Model, cmd tea.Cmd) (chatsTUIModel, tea.Cmd) {
t.Helper()
updated, ok := model.(expChatsTUIModel)
updated, ok := model.(chatsTUIModel)
require.True(t, ok)
return updated, cmd
}
@@ -3264,8 +3264,8 @@ func newTestChatViewModel(client *codersdk.ExperimentalClient) chatViewModel {
return newChatViewModel(context.Background(), client, nil, nil, uuid.Nil, newTUIStyles())
}
func newTestTUIModel() expChatsTUIModel {
return newExpChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
func newTestTUIModel() chatsTUIModel {
return newChatsTUIModel(context.Background(), nil, nil, nil, nil, uuid.Nil)
}
func newReadyChatListModel() chatListModel {
+1 -1
View File
@@ -100,6 +100,7 @@ const (
func (r *RootCmd) CoreSubcommands() []*serpent.Command {
// Please re-sort this list alphabetically if you change it!
return []*serpent.Command{
r.agentsCommand(),
r.completion(),
r.dotfiles(),
externalAuth(),
@@ -163,7 +164,6 @@ func (r *RootCmd) AGPLExperimental() []*serpent.Command {
r.promptExample(),
r.rptyCommand(),
r.syncCommand(),
r.agentsCommand(),
}
}
+1
View File
@@ -14,6 +14,7 @@ USAGE:
$ coder templates init
SUBCOMMANDS:
agents Interactive terminal UI for AI agents.
autoupdate Toggle auto-update policy for a workspace
completion Install or update shell completion scripts for the
detected or chosen shell.
+16
View File
@@ -0,0 +1,16 @@
coder v0.0.0-devel
USAGE:
coder agents [flags] [chat-id]
Interactive terminal UI for AI agents.
OPTIONS:
--model string
Choose a model by ID, provider/model, or display name.
--workspace string
Associate the chat with a workspace by name, owner/name, or UUID.
———
Run `coder --help` for a list of global options.