diff --git a/cli/exp_agents.go b/cli/agents.go similarity index 92% rename from cli/exp_agents.go rename to cli/agents.go index 89c6b74999..5ae92283ee 100644 --- a/cli/exp_agents.go +++ b/cli/agents.go @@ -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 diff --git a/cli/exp_agents_chat.go b/cli/agents_chat.go similarity index 100% rename from cli/exp_agents_chat.go rename to cli/agents_chat.go diff --git a/cli/exp_agents_cmds.go b/cli/agents_cmds.go similarity index 100% rename from cli/exp_agents_cmds.go rename to cli/agents_cmds.go diff --git a/cli/exp_agents_diff.go b/cli/agents_diff.go similarity index 100% rename from cli/exp_agents_diff.go rename to cli/agents_diff.go diff --git a/cli/exp_agents_diff_test.go b/cli/agents_diff_test.go similarity index 100% rename from cli/exp_agents_diff_test.go rename to cli/agents_diff_test.go diff --git a/cli/exp_agents_e2e_helpers_test.go b/cli/agents_e2e_helpers_test.go similarity index 75% rename from cli/exp_agents_e2e_helpers_test.go rename to cli/agents_e2e_helpers_test.go index e383e70ce2..8dc41aa8b1 100644 --- a/cli/exp_agents_e2e_helpers_test.go +++ b/cli/agents_e2e_helpers_test.go @@ -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} } diff --git a/cli/exp_agents_e2e_test.go b/cli/agents_e2e_test.go similarity index 82% rename from cli/exp_agents_e2e_test.go rename to cli/agents_e2e_test.go index c43ee5c972..1bffbe985b 100644 --- a/cli/exp_agents_e2e_test.go +++ b/cli/agents_e2e_test.go @@ -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 diff --git a/cli/exp_agents_helpers.go b/cli/agents_helpers.go similarity index 100% rename from cli/exp_agents_helpers.go rename to cli/agents_helpers.go diff --git a/cli/exp_agents_list.go b/cli/agents_list.go similarity index 100% rename from cli/exp_agents_list.go rename to cli/agents_list.go diff --git a/cli/exp_agents_model.go b/cli/agents_model.go similarity index 90% rename from cli/exp_agents_model.go rename to cli/agents_model.go index 75fc4cfac0..cf04a33632 100644 --- a/cli/exp_agents_model.go +++ b/cli/agents_model.go @@ -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 "" } diff --git a/cli/exp_agents_render.go b/cli/agents_render.go similarity index 100% rename from cli/exp_agents_render.go rename to cli/agents_render.go diff --git a/cli/exp_agents_render_test.go b/cli/agents_render_test.go similarity index 99% rename from cli/exp_agents_render_test.go rename to cli/agents_render_test.go index 115ac22dc8..180dfc4599 100644 --- a/cli/exp_agents_render_test.go +++ b/cli/agents_render_test.go @@ -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() diff --git a/cli/exp_agents_stream_test.go b/cli/agents_stream_test.go similarity index 100% rename from cli/exp_agents_stream_test.go rename to cli/agents_stream_test.go diff --git a/cli/exp_agents_styles.go b/cli/agents_styles.go similarity index 100% rename from cli/exp_agents_styles.go rename to cli/agents_styles.go diff --git a/cli/exp_agents_test.go b/cli/agents_test.go similarity index 97% rename from cli/exp_agents_test.go rename to cli/agents_test.go index 6a8ed14453..cc77da20a0 100644 --- a/cli/exp_agents_test.go +++ b/cli/agents_test.go @@ -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 { diff --git a/cli/root.go b/cli/root.go index 8232db4a30..b20520b192 100644 --- a/cli/root.go +++ b/cli/root.go @@ -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(), } } diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index cb667c3a5c..47c9b3a3f7 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -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. diff --git a/cli/testdata/coder_agents_--help.golden b/cli/testdata/coder_agents_--help.golden new file mode 100644 index 0000000000..eeeaa2b73a --- /dev/null +++ b/cli/testdata/coder_agents_--help.golden @@ -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. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f45ac54ca6..460e05a902 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -16271,12 +16271,10 @@ const docTemplate = `{ "notifications", "workspace-usage", "oauth2", - "agents", "mcp-server-http", "workspace-build-updates" ], "x-enum-comments": { - "ExperimentAgents": "Enables agent-powered chat functionality.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentExample": "This isn't used for anything.", "ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.", @@ -16291,7 +16289,6 @@ const docTemplate = `{ "Sends notifications via SMTP and webhooks following certain events.", "Enables the new workspace usage tracking.", "Enables OAuth2 provider functionality.", - "Enables agent-powered chat functionality.", "Enables the MCP HTTP server functionality.", "Enables publishing workspace build updates to the all builds pubsub channel." ], @@ -16301,7 +16298,6 @@ const docTemplate = `{ "ExperimentNotifications", "ExperimentWorkspaceUsage", "ExperimentOAuth2", - "ExperimentAgents", "ExperimentMCPServerHTTP", "ExperimentWorkspaceBuildUpdates" ] diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 18b232b61d..616fb9b35a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -14731,12 +14731,10 @@ "notifications", "workspace-usage", "oauth2", - "agents", "mcp-server-http", "workspace-build-updates" ], "x-enum-comments": { - "ExperimentAgents": "Enables agent-powered chat functionality.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentExample": "This isn't used for anything.", "ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.", @@ -14751,7 +14749,6 @@ "Sends notifications via SMTP and webhooks following certain events.", "Enables the new workspace usage tracking.", "Enables OAuth2 provider functionality.", - "Enables agent-powered chat functionality.", "Enables the MCP HTTP server functionality.", "Enables publishing workspace build updates to the all builds pubsub channel." ], @@ -14761,7 +14758,6 @@ "ExperimentNotifications", "ExperimentWorkspaceUsage", "ExperimentOAuth2", - "ExperimentAgents", "ExperimentMCPServerHTTP", "ExperimentWorkspaceBuildUpdates" ] diff --git a/coderd/coderd.go b/coderd/coderd.go index dfa2d055b6..8921087c73 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -768,7 +768,7 @@ func New(options *Options) *API { } api.agentProvider = stn - { // Experimental: agents — chat daemon and git sync worker initialization. + { // Chat daemon and git sync worker initialization. maxChatsPerAcquire := options.DeploymentValues.AI.Chat.AcquireBatchSize.Value() if maxChatsPerAcquire > math.MaxInt32 { maxChatsPerAcquire = math.MaxInt32 @@ -1153,11 +1153,9 @@ func New(options *Options) *API { }) }) }) - // Experimental(agents): chat API routes gated by ExperimentAgents. r.Route("/chats", func(r chi.Router) { r.Use( apiKeyMiddleware, - httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentAgents), ) r.Get("/by-workspace", api.chatsByWorkspace) r.Get("/", api.listChats) @@ -1280,7 +1278,6 @@ func New(options *Options) *API { ) // MCP server configuration endpoints. r.Route("/servers", func(r chi.Router) { - r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentAgents)) r.Get("/", api.listMCPServerConfigs) r.Post("/", api.createMCPServerConfig) r.Route("/{mcpServer}", func(r chi.Router) { @@ -2006,14 +2003,10 @@ func New(options *Options) *API { "parsing additional CSP headers", slog.Error(cspParseErrors)) } - // Add blob: to img-src for chat file attachment previews when - // the agents experiment is enabled. - if api.Experiments.Enabled(codersdk.ExperimentAgents) { - additionalCSPHeaders[httpmw.CSPDirectiveImgSrc] = append( - additionalCSPHeaders[httpmw.CSPDirectiveImgSrc], "blob:", - ) - } - + // Add blob: to img-src for chat file attachment previews. + additionalCSPHeaders[httpmw.CSPDirectiveImgSrc] = append( + additionalCSPHeaders[httpmw.CSPDirectiveImgSrc], "blob:", + ) // Add CSP headers to all static assets and pages. CSP headers only affect // browsers, so these don't make sense on api routes. cspProxyHosts := func() []*proxyhealth.ProxyHost { @@ -2161,9 +2154,9 @@ type API struct { // dbRolluper rolls up template usage stats from raw agent and app // stats. This is used to provide insights in the WebUI. dbRolluper *dbrollup.Rolluper - // Experimental(agents): chatDaemon handles background processing of pending chats. + // chatDaemon handles background processing of pending chats. chatDaemon *chatd.Server - // Experimental(agents): gitSyncWorker refreshes stale chat diff statuses in the background. + // gitSyncWorker refreshes stale chat diff statuses in the background. gitSyncWorker *gitsync.Worker // AISeatTracker records AI seat usage. AISeatTracker aiseats.SeatTracker diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index da382bf787..b95b568a55 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -56,7 +56,6 @@ func chatDeploymentValues(t testing.TB) *codersdk.DeploymentValues { t.Helper() values := coderdtest.DeploymentValues(t) - values.Experiments = []string{string(codersdk.ExperimentAgents)} return values } diff --git a/coderd/mcp_test.go b/coderd/mcp_test.go index 8b0e847aac..1ac13b42d4 100644 --- a/coderd/mcp_test.go +++ b/coderd/mcp_test.go @@ -18,19 +18,15 @@ import ( "github.com/coder/coder/v2/testutil" ) -// mcpDeploymentValues returns deployment values with the agents -// experiment enabled, which is required by the MCP server config -// endpoints. +// mcpDeploymentValues returns deployment values for tests of the MCP +// server config endpoints. func mcpDeploymentValues(t testing.TB) *codersdk.DeploymentValues { t.Helper() - values := coderdtest.DeploymentValues(t) - values.Experiments = []string{string(codersdk.ExperimentAgents)} - return values + return coderdtest.DeploymentValues(t) } -// newMCPClient creates a test server with the agents experiment -// enabled and returns the admin client. +// newMCPClient creates a test server and returns the admin client. func newMCPClient(t testing.TB) *codersdk.Client { t.Helper() diff --git a/coderd/x/chatd/chatd_test.go b/coderd/x/chatd/chatd_test.go index c55279b628..d20f980bdf 100644 --- a/coderd/x/chatd/chatd_test.go +++ b/coderd/x/chatd/chatd_test.go @@ -201,7 +201,6 @@ func TestSubagentChatExcludesWorkspaceProvisioningTools(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, IncludeProvisionerDaemon: true, @@ -366,7 +365,6 @@ func TestPlanModeSubagentChatExcludesAskUserQuestion(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, IncludeProvisionerDaemon: true, @@ -549,7 +547,6 @@ func TestExploreSubagentIsReadOnly(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ DeploymentValues: deploymentValues, IncludeProvisionerDaemon: true, @@ -4936,7 +4933,6 @@ func TestCreateWorkspaceTool_EndToEnd(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, IncludeProvisionerDaemon: true, @@ -5117,7 +5113,6 @@ func TestStartWorkspaceTool_EndToEnd(t *testing.T) { ctx := testutil.Context(t, testutil.WaitSuperLong) deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, IncludeProvisionerDaemon: true, @@ -8504,7 +8499,6 @@ func TestAgentContextFilesAndSkillsLoadedIntoChat(t *testing.T) { ctx := testutil.Context(t, testutil.WaitSuperLong) deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, IncludeProvisionerDaemon: true, diff --git a/coderd/x/chatd/integration_test.go b/coderd/x/chatd/integration_test.go index 185d36b216..7680f10e7d 100644 --- a/coderd/x/chatd/integration_test.go +++ b/coderd/x/chatd/integration_test.go @@ -35,9 +35,8 @@ func TestAnthropicWebSearchRoundTrip(t *testing.T) { ctx := testutil.Context(t, testutil.WaitSuperLong) - // Stand up a full coderd with the agents experiment. + // Stand up a full coderd. deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, }) @@ -296,9 +295,8 @@ func TestOpenAIReasoningRoundTrip(t *testing.T) { ctx := testutil.Context(t, testutil.WaitSuperLong) - // Stand up a full coderd with the agents experiment. + // Stand up a full coderd. deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, }) @@ -451,9 +449,8 @@ func TestOpenAIReasoningRoundTripStoreFalse(t *testing.T) { ctx := testutil.Context(t, testutil.WaitSuperLong) - // Stand up a full coderd with the agents experiment. + // Stand up a full coderd. deploymentValues := coderdtest.DeploymentValues(t) - deploymentValues.Experiments = []string{string(codersdk.ExperimentAgents)} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: deploymentValues, }) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index c7b17b4235..55b8678395 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -4403,7 +4403,6 @@ const ( ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events. ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking. ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality. - ExperimentAgents Experiment = "agents" // Enables agent-powered chat functionality. ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality. ExperimentWorkspaceBuildUpdates Experiment = "workspace-build-updates" // Enables publishing workspace build updates to the all builds pubsub channel. ) @@ -4420,8 +4419,6 @@ func (e Experiment) DisplayName() string { return "Workspace Usage Tracking" case ExperimentOAuth2: return "OAuth2 Provider Functionality" - case ExperimentAgents: - return "Agents" case ExperimentMCPServerHTTP: return "MCP HTTP Server Functionality" case ExperimentWorkspaceBuildUpdates: @@ -4441,7 +4438,6 @@ var ExperimentsKnown = Experiments{ ExperimentNotifications, ExperimentWorkspaceUsage, ExperimentOAuth2, - ExperimentAgents, ExperimentMCPServerHTTP, ExperimentWorkspaceBuildUpdates, } @@ -4450,7 +4446,6 @@ var ExperimentsKnown = Experiments{ // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should // not be included here and will be essentially hidden. -// TODO: Add ExperimentAgents to ExperimentsSafe once it is safe for general use. var ExperimentsSafe = Experiments{} // Experiments is a list of experiments. diff --git a/docs/ai-coder/agents/chats-api.md b/docs/ai-coder/agents/chats-api.md index 894f4bbdce..a2b523516a 100644 --- a/docs/ai-coder/agents/chats-api.md +++ b/docs/ai-coder/agents/chats-api.md @@ -1,7 +1,7 @@ # Chats API > [!NOTE] -> The Chats API is experimental and gated behind the `agents` experiment flag. +> The Chats API is in beta. > Endpoints live under `/api/experimental/chats` and may change without notice. The Chats API lets you create and interact with Coder Agents diff --git a/docs/ai-coder/agents/early-access.md b/docs/ai-coder/agents/early-access.md index 8a0fa419bb..30df5710a3 100644 --- a/docs/ai-coder/agents/early-access.md +++ b/docs/ai-coder/agents/early-access.md @@ -40,27 +40,11 @@ Functionality available during Early Access may be a subset of planned capabilities. Some features may be incomplete, experimental, or subject to redesign. -## Enable Coder Agents +## Set up Coder Agents -Coder Agents is experimental and must not be deployed to production -environments. It is gated behind the `agents` experiment flag. To enable it, -pass the flag when starting the Coder server using an environment variable -or CLI flag: +Coder Agents is available by default. No experiment flags are required. -```sh -CODER_EXPERIMENTS="agents" coder server -# or -coder server --experiments=agents -``` - -If you are already using other experiments, add `agents` to the -comma-separated list: - -```sh -CODER_EXPERIMENTS="agents,oauth2,mcp-server-http" coder server -``` - -Once the server restarts with the experiment enabled: +To get started: 1. Navigate to the **Agents** page in the Coder dashboard. 1. Open **Admin** settings and configure at least one LLM provider and model. diff --git a/docs/ai-coder/agents/getting-started.md b/docs/ai-coder/agents/getting-started.md index dff1435e5c..7515cdff39 100644 --- a/docs/ai-coder/agents/getting-started.md +++ b/docs/ai-coder/agents/getting-started.md @@ -1,6 +1,6 @@ # Getting Started -This guide walks platform teams and administrators through enabling Coder +This guide walks platform teams and administrators through setting up Coder Agents, preparing your deployment, and running your first Coder Agent. > [!NOTE] @@ -12,8 +12,7 @@ Agents, preparing your deployment, and running your first Coder Agent. Before you begin, confirm the following: -- **Coder deployment** running the latest release with the `agents` - experiment flag available. +- **Coder deployment** running the latest release. - **LLM provider credentials** — an API key for at least one [supported provider](./models.md) (Anthropic, OpenAI, Google, Azure OpenAI, AWS Bedrock, OpenAI Compatible, OpenRouter, or Vercel AI Gateway). @@ -22,40 +21,19 @@ Before you begin, confirm the following: - **At least one template** with a [descriptive name and description](./platform-controls/template-optimization.md) for the agent to select when provisioning workspaces. -- **Admin access** to the Coder deployment for enabling the experiment and - configuring providers. +- **Admin access** to the Coder deployment for configuring providers. - **Coder Agents User role** assigned to each user who needs to interact with Coder Agents. Owners can assign this from **Admin** > **Users**. See - [Grant Coder Agents User](#step-3-grant-coder-agents-user) below. + [Grant Coder Agents User](#step-2-grant-coder-agents-user) below. -## Step 1: Enable the experiment - -Coder Agents is gated behind the `agents` experiment flag. Pass it when -starting the Coder server: - -```sh -CODER_EXPERIMENTS="agents" coder server -# or -coder server --experiments=agents -``` - -If you already use other experiments, add `agents` to the comma-separated list: - -```sh -CODER_EXPERIMENTS="agents,oauth2,mcp-server-http" coder server -``` - -See [Enable Coder Agents](./early-access.md#enable-coder-agents) for full -details. - -## Step 2: Configure an LLM provider and model +## Step 1: Configure an LLM provider and model > [!IMPORTANT] > Configuring providers, models, and system prompts requires the > **Owner** role (Coder administrator). Non-admin users cannot access the > Admin panel or modify deployment-level Agents configuration. -Once the server restarts with the experiment enabled: +To configure Coder Agents: 1. Navigate to the **Agents** page in the Coder dashboard. 1. Click **Admin** to open the configuration dialog. @@ -72,7 +50,7 @@ Detailed instructions for each provider and model option are in the > Start with a single frontier model to validate your setup before adding > additional providers. -## Step 3: Grant Coder Agents User +## Step 2: Grant Coder Agents User The **Coder Agents User** role controls which users can interact with Coder Agents. Members do not have Coder Agents User by default. @@ -105,7 +83,7 @@ coder users list -o json \ done ``` -## Step 4: Start your first Coder Agent +## Step 3: Start your first Coder Agent 1. Go to the **Agents** page in the Coder dashboard. 1. Select a model from the dropdown (your default will be pre-selected). @@ -266,7 +244,7 @@ rather than developer session tokens. Keep automation credentials narrowly scoped. > [!NOTE] -> The Chats API is experimental and may change without notice. +> The Chats API is in beta and may change without notice. > See [Chats API](./chats-api.md) for the full endpoint reference. ### Add workspace context with AGENTS.md diff --git a/docs/manifest.json b/docs/manifest.json index 2042be727d..d8fcaadc94 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1292,7 +1292,7 @@ }, { "title": "Chats API", - "description": "Programmatic access to Coder Agents via the experimental Chats API", + "description": "Programmatic access to Coder Agents via the Chats API", "path": "./ai-coder/agents/chats-api.md", "state": ["early access"] } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 8369de3824..2f6c6f4a91 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4656,9 +4656,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o #### Enumerated Values -| Value(s) | -|-----------------------------------------------------------------------------------------------------------------------------------------| -| `agents`, `auto-fill-parameters`, `example`, `mcp-server-http`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-usage` | +| Value(s) | +|-------------------------------------------------------------------------------------------------------------------------------| +| `auto-fill-parameters`, `example`, `mcp-server-http`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-usage` | ## codersdk.ExternalAPIKeyScopes diff --git a/docs/reference/cli/agents.md b/docs/reference/cli/agents.md new file mode 100644 index 0000000000..64240ca561 --- /dev/null +++ b/docs/reference/cli/agents.md @@ -0,0 +1,28 @@ + +# agents + +Interactive terminal UI for AI agents. + +## Usage + +```console +coder agents [flags] [chat-id] +``` + +## Options + +### --workspace + +| | | +|------|---------------------| +| Type | string | + +Associate the chat with a workspace by name, owner/name, or UUID. + +### --model + +| | | +|------|---------------------| +| Type | string | + +Choose a model by ID, provider/model, or display name. diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index 211cba86c8..5ebf171298 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -24,6 +24,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | Name | Purpose | |--------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| [agents](./agents.md) | Interactive terminal UI for AI agents. | | [completion](./completion.md) | Install or update shell completion scripts for the detected or chosen shell. | | [dotfiles](./dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository | | [external-auth](./external-auth.md) | Manage external authentication | diff --git a/enterprise/coderd/exp_chats_test.go b/enterprise/coderd/exp_chats_test.go index 342f617bc4..ea49812ee0 100644 --- a/enterprise/coderd/exp_chats_test.go +++ b/enterprise/coderd/exp_chats_test.go @@ -1104,7 +1104,6 @@ func TestCreateChatNonDefaultOrg(t *testing.T) { Options: &coderdtest.Options{ DeploymentValues: func() *codersdk.DeploymentValues { v := coderdtest.DeploymentValues(t) - v.Experiments = []string{string(codersdk.ExperimentAgents)} return v }(), }, @@ -1181,7 +1180,6 @@ func TestListChats_OrgAdminOnlySeesOwnChats(t *testing.T) { Options: &coderdtest.Options{ DeploymentValues: func() *codersdk.DeploymentValues { v := coderdtest.DeploymentValues(t) - v.Experiments = []string{string(codersdk.ExperimentAgents)} return v }(), }, diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 7e5bc3f2f8..562f35ab02 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -453,7 +453,6 @@ func TestListRoles(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAgents)} client, owner := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ diff --git a/scripts/check_emdash.sh b/scripts/check_emdash.sh index 4001ac2408..4ed7da6175 100755 --- a/scripts/check_emdash.sh +++ b/scripts/check_emdash.sh @@ -23,6 +23,9 @@ pattern="${emdash}|${endash}" # Git exclude_pathspecs excluded from the check. Used in both ls-files and diff comparison. exclude_pathspecs=( ":(exclude)aibridge/fixtures/**/*.txtar" + # Generated CLI golden files embed serpent's emdash-bordered footer. + ":(exclude)cli/testdata/*.golden" + ":(exclude)enterprise/cli/testdata/*.golden" ) scan_all_files() { diff --git a/site/index.html b/site/index.html index 5b3098e222..10c0b826e6 100644 --- a/site/index.html +++ b/site/index.html @@ -29,7 +29,6 @@ - ; @@ -84,10 +82,6 @@ const emptyMetadata: RuntimeHtmlMetadata = { available: false, value: undefined, }, - "agents-tab-visible": { - available: false, - value: undefined, - }, permissions: { available: false, value: undefined, @@ -131,10 +125,6 @@ const populatedMetadata: RuntimeHtmlMetadata = { available: true, value: MockTasksTabVisible, }, - "agents-tab-visible": { - available: true, - value: MockAgentsTabVisible, - }, permissions: { available: true, value: MockPermissions, diff --git a/site/src/hooks/useEmbeddedMetadata.ts b/site/src/hooks/useEmbeddedMetadata.ts index c9fa5a7f89..a75929d638 100644 --- a/site/src/hooks/useEmbeddedMetadata.ts +++ b/site/src/hooks/useEmbeddedMetadata.ts @@ -32,7 +32,6 @@ type AvailableMetadata = Readonly<{ regions: readonly Region[]; "build-info": BuildInfoResponse; "tasks-tab-visible": boolean; - "agents-tab-visible": boolean; permissions: Permissions; organizations: Organization[]; }>; @@ -97,7 +96,6 @@ export class MetadataManager implements MetadataManagerApi { "build-info": this.registerValue("build-info"), regions: this.registerRegionValue(), "tasks-tab-visible": this.registerValue("tasks-tab-visible"), - "agents-tab-visible": this.registerValue("agents-tab-visible"), permissions: this.registerValue("permissions"), organizations: this.registerValue("organizations"), }; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index 7ddc4a6191..e0e6ff095d 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -20,7 +20,6 @@ const meta: Meta = { parameters: { chromatic: chromaticWithTablet, layout: "fullscreen", - experiments: ["agents"], queries: [ { key: ["tasks", tasksFilter], diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 3d09b5d37b..18a47cdd13 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -14,7 +14,6 @@ import { } from "#/components/Tooltip/Tooltip"; import type { ProxyContextValue } from "#/contexts/ProxyContext"; import { useEmbeddedMetadata } from "#/hooks/useEmbeddedMetadata"; -import { useDashboard } from "#/modules/dashboard/useDashboard"; import { NotificationsInbox } from "#/modules/notifications/NotificationsInbox/NotificationsInbox"; import { getPrereleaseFlag } from "#/utils/buildInfo"; import { cn } from "#/utils/cn"; @@ -272,12 +271,7 @@ function idleTasksLabel(count: number) { } const AgentsNavItem: FC<{ canCreateChat: boolean }> = ({ canCreateChat }) => { - const { experiments, buildInfo } = useDashboard(); - const prerelease = getPrereleaseFlag(buildInfo); - const experimentEnabled = - experiments.includes("agents") || prerelease === "devel"; - - if (!experimentEnabled || !canCreateChat) { + if (!canCreateChat) { return null; } diff --git a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx index 52af681d52..a36b743353 100644 --- a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx +++ b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx @@ -28,6 +28,7 @@ import { DropdownMenuTrigger, } from "#/components/DropdownMenu/DropdownMenu"; import { ExternalImage } from "#/components/ExternalImage/ExternalImage"; +import { FeatureStageBadge } from "#/components/FeatureStageBadge/FeatureStageBadge"; import { CoderIcon } from "#/components/Icons/CoderIcon"; import { Spinner } from "#/components/Spinner/Spinner"; import { useWebpushNotifications } from "#/contexts/useWebpushNotifications"; @@ -132,13 +133,16 @@ export const AgentPageHeader: FC = ({ ) : ( - - {logoUrl ? ( - - ) : ( - - )} - +
+ + {logoUrl ? ( + + ) : ( + + )} + + +
)} {isSidebarCollapsed && (