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"
|
||||
@@ -84,7 +83,6 @@ func (r *RootCmd) agentsCommand() *serpent.Command {
|
||||
return &serpent.Command{
|
||||
Use: "agents [chat-id]",
|
||||
Short: "Interactive terminal UI for AI agents.",
|
||||
Aliases: []string{"agent"},
|
||||
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
|
||||
@@ -29,7 +29,7 @@ const (
|
||||
|
||||
type (
|
||||
terminateTUIMsg struct{}
|
||||
expChatsTUIModel 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.
|
||||
Generated
-4
@@ -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"
|
||||
]
|
||||
|
||||
Generated
-4
@@ -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"
|
||||
]
|
||||
|
||||
+4
-11
@@ -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) {
|
||||
// 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+4
-8
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -4657,8 +4657,8 @@ 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` |
|
||||
|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `auto-fill-parameters`, `example`, `mcp-server-http`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-usage` |
|
||||
|
||||
## codersdk.ExternalAPIKeyScopes
|
||||
|
||||
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
# agents
|
||||
|
||||
Interactive terminal UI for AI agents.
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder agents [flags] [chat-id]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### --workspace
|
||||
|
||||
| | |
|
||||
|------|---------------------|
|
||||
| Type | <code>string</code> |
|
||||
|
||||
Associate the chat with a workspace by name, owner/name, or UUID.
|
||||
|
||||
### --model
|
||||
|
||||
| | |
|
||||
|------|---------------------|
|
||||
| Type | <code>string</code> |
|
||||
|
||||
Choose a model by ID, provider/model, or display name.
|
||||
Generated
+1
@@ -24,6 +24,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
|
||||
|
||||
| Name | Purpose |
|
||||
|--------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [<code>agents</code>](./agents.md) | Interactive terminal UI for AI agents. |
|
||||
| [<code>completion</code>](./completion.md) | Install or update shell completion scripts for the detected or chosen shell. |
|
||||
| [<code>dotfiles</code>](./dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository |
|
||||
| [<code>external-auth</code>](./external-auth.md) | Manage external authentication |
|
||||
|
||||
@@ -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
|
||||
}(),
|
||||
},
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<meta property="docs-url" content="{{ .DocsURL }}" />
|
||||
<meta property="logo-url" content="{{ .LogoURL }}" />
|
||||
<meta property="tasks-tab-visible" content="{{ .TasksTabVisible }}" />
|
||||
<meta property="agents-tab-visible" content="{{ .AgentsTabVisible }}" />
|
||||
<meta property="permissions" content="{{ .Permissions }}" />
|
||||
<meta property="organizations" content="{{ .Organizations }}" />
|
||||
<link
|
||||
|
||||
@@ -267,7 +267,6 @@ type htmlState struct {
|
||||
DocsURL string
|
||||
|
||||
TasksTabVisible string
|
||||
AgentsTabVisible string
|
||||
Permissions string
|
||||
Organizations string
|
||||
}
|
||||
@@ -525,16 +524,6 @@ func (h *Handler) populateHTMLState(
|
||||
state.TasksTabVisible = html.EscapeString(string(data))
|
||||
}
|
||||
})
|
||||
wg.Go(func() {
|
||||
agentsTabVisible := false
|
||||
if experiments != nil {
|
||||
agentsTabVisible = experiments.Enabled(codersdk.ExperimentAgents)
|
||||
}
|
||||
data, err := json.Marshal(agentsTabVisible)
|
||||
if err == nil {
|
||||
state.AgentsTabVisible = html.EscapeString(string(data))
|
||||
}
|
||||
})
|
||||
wg.Go(func() {
|
||||
sdkOrgs := slice.List(userOrgs, db2sdk.Organization)
|
||||
data, err := json.Marshal(sdkOrgs)
|
||||
|
||||
Generated
-2
@@ -3778,7 +3778,6 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning";
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type Experiment =
|
||||
| "agents"
|
||||
| "auto-fill-parameters"
|
||||
| "example"
|
||||
| "mcp-server-http"
|
||||
@@ -3788,7 +3787,6 @@ export type Experiment =
|
||||
| "workspace-usage";
|
||||
|
||||
export const Experiments: Experiment[] = [
|
||||
"agents",
|
||||
"auto-fill-parameters",
|
||||
"example",
|
||||
"mcp-server-http",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import type { Region, User } from "#/api/typesGenerated";
|
||||
import {
|
||||
MockAgentsTabVisible,
|
||||
MockAppearanceConfig,
|
||||
MockBuildInfo,
|
||||
MockEntitlements,
|
||||
@@ -46,7 +45,6 @@ const mockDataForTags = {
|
||||
userAppearance: MockUserAppearanceSettings,
|
||||
regions: MockRegions,
|
||||
"tasks-tab-visible": MockTasksTabVisible,
|
||||
"agents-tab-visible": MockAgentsTabVisible,
|
||||
permissions: MockPermissions,
|
||||
organizations: [MockOrganization],
|
||||
} as const satisfies Record<MetadataKey, MetadataValue>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<BuildInfoResponse>("build-info"),
|
||||
regions: this.registerRegionValue(),
|
||||
"tasks-tab-visible": this.registerValue<boolean>("tasks-tab-visible"),
|
||||
"agents-tab-visible": this.registerValue<boolean>("agents-tab-visible"),
|
||||
permissions: this.registerValue<Permissions>("permissions"),
|
||||
organizations: this.registerValue<Organization[]>("organizations"),
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ const meta: Meta<typeof NavbarView> = {
|
||||
parameters: {
|
||||
chromatic: chromaticWithTablet,
|
||||
layout: "fullscreen",
|
||||
experiments: ["agents"],
|
||||
queries: [
|
||||
{
|
||||
key: ["tasks", tasksFilter],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AgentPageHeaderProps> = ({
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<NavLink to="/workspaces" className="inline-flex shrink-0 md:hidden">
|
||||
<div className="inline-flex shrink-0 items-center gap-2 md:hidden">
|
||||
<NavLink to="/workspaces" className="inline-flex">
|
||||
{logoUrl ? (
|
||||
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
|
||||
) : (
|
||||
<CoderIcon className="h-6 w-6 fill-content-primary" />
|
||||
)}
|
||||
</NavLink>
|
||||
<FeatureStageBadge contentType="beta" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{isSidebarCollapsed && (
|
||||
<Button
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MemoryRouter } from "react-router";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import type { Chat } from "#/api/typesGenerated";
|
||||
import { TooltipProvider } from "#/components/Tooltip/Tooltip";
|
||||
import { ThemeOverride } from "#/contexts/ThemeProvider";
|
||||
import { DashboardContext } from "#/modules/dashboard/DashboardProvider";
|
||||
import {
|
||||
@@ -91,11 +92,13 @@ const Wrapper: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeOverride theme={themes[DEFAULT_THEME]}>
|
||||
<TooltipProvider>
|
||||
<MemoryRouter initialEntries={["/agents"]}>
|
||||
<DashboardContext.Provider value={dashboardValue}>
|
||||
{children}
|
||||
</DashboardContext.Provider>
|
||||
</MemoryRouter>
|
||||
</TooltipProvider>
|
||||
</ThemeOverride>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -89,6 +89,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 { ScrollArea } from "#/components/ScrollArea/ScrollArea";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
@@ -1087,6 +1088,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
>
|
||||
<div className="hidden border-b border-border-default px-2 pb-3 pt-1.5 md:block">
|
||||
<div className="mb-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<NavLink to="/workspaces" className="inline-flex">
|
||||
{logoUrl ? (
|
||||
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
|
||||
@@ -1094,6 +1096,8 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
<CoderIcon className="h-6 w-6 fill-content-primary" />
|
||||
)}
|
||||
</NavLink>
|
||||
<FeatureStageBadge contentType="beta" size="sm" />
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 -mr-1.5">
|
||||
<Button
|
||||
asChild
|
||||
|
||||
@@ -82,8 +82,6 @@ const WorkspacesPage: FC = () => {
|
||||
},
|
||||
});
|
||||
const { permissions, user: me } = useAuthenticated();
|
||||
const { experiments } = useDashboard();
|
||||
const agentsEnabled = experiments.includes("agents");
|
||||
const templatesQuery = useQuery(templates());
|
||||
const workspacePermissionsQuery = useQuery(
|
||||
workspacePermissionsByOrganization(
|
||||
@@ -147,7 +145,11 @@ const WorkspacesPage: FC = () => {
|
||||
);
|
||||
const chatsByWorkspaceQuery = useQuery({
|
||||
...chatsByWorkspace(workspaceIds),
|
||||
enabled: agentsEnabled && workspaceIds.length > 0,
|
||||
// Only fetch chat lookups for users who can actually create chats;
|
||||
// the endpoint still runs a DB query + RBAC post-filter and the
|
||||
// AgentsNavItem / chat link UI is already hidden for users without
|
||||
// this permission, so the query would return nothing useful for them.
|
||||
enabled: permissions.createChat && workspaceIds.length > 0,
|
||||
});
|
||||
|
||||
const [activeBatchAction, setActiveBatchAction] = useState<BatchAction>();
|
||||
|
||||
@@ -572,8 +572,6 @@ export const MockUserAppearanceSettings: TypesGen.UserAppearanceSettings = {
|
||||
|
||||
export const MockTasksTabVisible: boolean = false;
|
||||
|
||||
export const MockAgentsTabVisible: boolean = false;
|
||||
|
||||
export const MockOrganizationMember: TypesGen.OrganizationMemberWithUserData = {
|
||||
organization_id: MockOrganization.id,
|
||||
user_id: MockUserOwner.id,
|
||||
|
||||
Reference in New Issue
Block a user