mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -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
@@ -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.
|
||||
Reference in New Issue
Block a user