mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
132 lines
4.3 KiB
Go
132 lines
4.3 KiB
Go
package cli //nolint:testpackage // Tests unexported chat stream helpers.
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
type chatWatchWriters struct{ stdout, stderr io.Writer }
|
|
|
|
func (w chatWatchWriters) Write(p []byte) (int, error) { return w.stdout.Write(p) }
|
|
|
|
func (w chatWatchWriters) Stderr() io.Writer {
|
|
if w.stderr != nil {
|
|
return w.stderr
|
|
}
|
|
return w.stdout
|
|
}
|
|
|
|
func consumeChatStream(eventCh <-chan codersdk.ChatStreamEvent, out io.Writer) error {
|
|
errOut := out
|
|
if writer, ok := out.(interface{ Stderr() io.Writer }); ok {
|
|
errOut = writer.Stderr()
|
|
}
|
|
|
|
printedInline := false
|
|
flush := func() error {
|
|
if !printedInline {
|
|
return nil
|
|
}
|
|
printedInline = false
|
|
_, err := fmt.Fprintln(out)
|
|
return err
|
|
}
|
|
|
|
printLine := func(dst io.Writer, format string, args ...any) error {
|
|
if err := flush(); err != nil {
|
|
return err
|
|
}
|
|
_, err := fmt.Fprintf(dst, format, args...)
|
|
return err
|
|
}
|
|
|
|
for event := range eventCh {
|
|
var err error
|
|
switch event.Type {
|
|
case codersdk.ChatStreamEventTypeMessagePart:
|
|
if part := event.MessagePart; part != nil &&
|
|
part.Part.Type == codersdk.ChatMessagePartTypeText && part.Part.Text != "" {
|
|
printedInline = true
|
|
_, err = fmt.Fprint(out, part.Part.Text)
|
|
}
|
|
case codersdk.ChatStreamEventTypeMessage:
|
|
if message := event.Message; message != nil && !printedInline {
|
|
for _, part := range message.Content {
|
|
if part.Type != codersdk.ChatMessagePartTypeText || part.Text == "" {
|
|
continue
|
|
}
|
|
printedInline = true
|
|
if _, err = fmt.Fprint(out, part.Text); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if err == nil {
|
|
err = flush()
|
|
}
|
|
case codersdk.ChatStreamEventTypeStatus:
|
|
if event.Status == nil {
|
|
err = flush()
|
|
break
|
|
}
|
|
err = printLine(out, "[Status: %s]\n", event.Status.Status)
|
|
case codersdk.ChatStreamEventTypeError:
|
|
if event.Error == nil {
|
|
err = flush()
|
|
break
|
|
}
|
|
err = printLine(errOut, "[Error: %s]\n", event.Error.Message)
|
|
case codersdk.ChatStreamEventTypeRetry:
|
|
if event.Retry == nil {
|
|
err = flush()
|
|
break
|
|
}
|
|
err = printLine(out, "[Retry attempt %d after error: %s]\n", event.Retry.Attempt, event.Retry.Error)
|
|
case codersdk.ChatStreamEventTypeQueueUpdate:
|
|
default:
|
|
err = printLine(out, "[Event: %s]\n", event.Type)
|
|
}
|
|
if err != nil {
|
|
return xerrors.Errorf("render chat stream event: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := flush(); err != nil {
|
|
return xerrors.Errorf("flush chat stream output: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestConsumeChatStreamText(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
events := make(chan codersdk.ChatStreamEvent, 7)
|
|
for _, event := range []codersdk.ChatStreamEvent{
|
|
{Type: codersdk.ChatStreamEventTypeMessagePart, MessagePart: &codersdk.ChatStreamMessagePart{Role: codersdk.ChatMessageRoleAssistant, Part: codersdk.ChatMessagePart{Type: codersdk.ChatMessagePartTypeText, Text: "Hello"}}},
|
|
{Type: codersdk.ChatStreamEventTypeMessagePart, MessagePart: &codersdk.ChatStreamMessagePart{Role: codersdk.ChatMessageRoleAssistant, Part: codersdk.ChatMessagePart{Type: codersdk.ChatMessagePartTypeToolCall, Text: "ignored"}}},
|
|
{Type: codersdk.ChatStreamEventTypeMessagePart, MessagePart: &codersdk.ChatStreamMessagePart{Role: codersdk.ChatMessageRoleAssistant, Part: codersdk.ChatMessagePart{Type: codersdk.ChatMessagePartTypeText, Text: " world"}}},
|
|
{Type: codersdk.ChatStreamEventTypeMessage, Message: &codersdk.ChatMessage{ID: 1, ChatID: uuid.New(), Role: codersdk.ChatMessageRoleAssistant, Content: []codersdk.ChatMessagePart{{Type: codersdk.ChatMessagePartTypeText, Text: "Hello world"}}}},
|
|
{Type: codersdk.ChatStreamEventTypeStatus, Status: &codersdk.ChatStreamStatus{Status: codersdk.ChatStatusRunning}},
|
|
{Type: codersdk.ChatStreamEventTypeRetry, Retry: &codersdk.ChatStreamRetry{Attempt: 2, Error: "rate limited"}},
|
|
{Type: codersdk.ChatStreamEventTypeError, Error: &codersdk.ChatStreamError{Message: "boom"}},
|
|
} {
|
|
events <- event
|
|
}
|
|
close(events)
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
err := consumeChatStream(events, chatWatchWriters{stdout: &stdout, stderr: &stderr})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "Hello world\n[Status: running]\n[Retry attempt 2 after error: rate limited]\n", stdout.String())
|
|
require.Equal(t, "[Error: boom]\n", stderr.String())
|
|
}
|