Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 1 addition & 130 deletions cli/exp_chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@ package cli
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strings"

"github.com/google/uuid"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/agent/agentcontextconfig"
"github.com/coder/coder/v2/agent/agentsocket"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/serpent"
)

Expand Down Expand Up @@ -54,7 +51,6 @@ func (r *RootCmd) chatContextCommand() *serpent.Command {
r.chatContextAddCommand(&socketPath),
r.chatContextRemoveCommand(&socketPath),
r.chatContextRefreshCommand(&socketPath),
r.chatContextClearCommand(),
},
Options: serpent.OptionSet{{
Flag: "socket-path",
Expand Down Expand Up @@ -203,8 +199,6 @@ func (*RootCmd) chatContextShowCommand(socketPath *string) *serpent.Command {
}

func (*RootCmd) chatContextAddCommand(socketPath *string) *serpent.Command {
var chatID string
agentAuth := &AgentAuth{}
cmd := &serpent.Command{
Use: "add <path>",
Short: "Register a workspace context source",
Expand All @@ -214,21 +208,13 @@ func (*RootCmd) chatContextAddCommand(socketPath *string) *serpent.Command {
".agents/skills/<name>/SKILL.md, and .mcp.json are picked up now and as they " +
"appear. Any change to a recognized file dirties this workspace's chats until " +
"you refresh.\n\nA path may be a file or a directory. Must be run from inside " +
"the workspace.\n\nPass --chat <chat> to keep the legacy one-shot behavior: read " +
"context from the path once and inject it into a single chat without " +
"registering a source.",
"the workspace.",
Middleware: serpent.RequireNArgs(1),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
ctx, stop := inv.SignalNotifyContext(ctx, StopSignals...)
defer stop()

// Legacy one-shot inject into a single chat.
if chatID != "" {
return addChatContextOneShot(ctx, inv, agentAuth, inv.Args[0], chatID)
}

// Source registration (default).
path, err := resolveContextSourcePath(inv.Args[0])
if err != nil {
return err
Expand All @@ -246,62 +232,10 @@ func (*RootCmd) chatContextAddCommand(socketPath *string) *serpent.Command {
_, _ = fmt.Fprintf(inv.Stdout, "Registered context source %s\n", src.Path)
return nil
},
Options: serpent.OptionSet{{
Name: "Chat ID",
Flag: "chat",
Env: "CODER_CHAT_ID",
Description: "Inject context from <path> into a single chat (legacy one-shot) instead of registering a source. Auto-detected from CODER_CHAT_ID, the only active chat, or the only top-level active chat.",
Value: serpent.StringOf(&chatID),
}},
}
agentAuth.AttachOptions(cmd, false)
return cmd
}

// addChatContextOneShot preserves the legacy `add --chat` behavior: read
// context files and skills from a directory and inject them into a single
// chat via coderd, without registering a persistent source.
func addChatContextOneShot(ctx context.Context, inv *serpent.Invocation, agentAuth *AgentAuth, dir, chatID string) error {
client, err := agentAuth.CreateClient()
if err != nil {
return xerrors.Errorf("create agent client: %w", err)
}

resolvedDir, err := filepath.Abs(dir)
if err != nil {
return xerrors.Errorf("resolve directory: %w", err)
}
info, err := os.Stat(resolvedDir)
if err != nil {
return xerrors.Errorf("cannot read directory %q: %w", resolvedDir, err)
}
if !info.IsDir() {
return xerrors.Errorf("--chat one-shot inject requires a directory, but %q is a file", resolvedDir)
}

parts := agentcontextconfig.ContextPartsFromDir(resolvedDir)
if len(parts) == 0 {
_, _ = fmt.Fprintln(inv.Stderr, "No context files or skills found in "+resolvedDir)
return nil
}

resolvedChatID, err := parseChatID(chatID)
if err != nil {
return err
}

resp, err := client.AddChatContext(ctx, agentsdk.AddChatContextRequest{
ChatID: resolvedChatID,
Parts: parts,
})
if err != nil {
return xerrors.Errorf("add chat context: %w", err)
}

_, _ = fmt.Fprintf(inv.Stdout, "Added %d context part(s) to chat %s\n", resp.Count, resp.ChatID)
return nil
}

func (*RootCmd) chatContextRemoveCommand(socketPath *string) *serpent.Command {
cmd := &serpent.Command{
Use: "remove <path>",
Expand Down Expand Up @@ -405,66 +339,3 @@ func (r *RootCmd) chatContextRefreshCommand(socketPath *string) *serpent.Command
agentAuth.AttachOptions(cmd, false)
return cmd
}

func (*RootCmd) chatContextClearCommand() *serpent.Command {
var chatID string
agentAuth := &AgentAuth{}
cmd := &serpent.Command{
Use: "clear",
Short: "Clear context from an active chat",
Long: "Soft-delete all context-file and skill messages from an active chat. " +
"The next turn will re-fetch default context from the agent.",
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
ctx, stop := inv.SignalNotifyContext(ctx, StopSignals...)
defer stop()

client, err := agentAuth.CreateClient()
if err != nil {
return xerrors.Errorf("create agent client: %w", err)
}

resolvedChatID, err := parseChatID(chatID)
if err != nil {
return err
}

resp, err := client.ClearChatContext(ctx, agentsdk.ClearChatContextRequest{
ChatID: resolvedChatID,
})
if err != nil {
return xerrors.Errorf("clear chat context: %w", err)
}

if resp.ChatID == uuid.Nil {
_, _ = fmt.Fprintln(inv.Stdout, "No active chats to clear.")
} else {
_, _ = fmt.Fprintf(inv.Stdout, "Cleared context from chat %s\n", resp.ChatID)
}
return nil
},
Options: serpent.OptionSet{{
Name: "Chat ID",
Flag: "chat",
Env: "CODER_CHAT_ID",
Description: "Chat ID to clear context from. Auto-detected from CODER_CHAT_ID, the only active chat, or the only top-level active chat.",
Value: serpent.StringOf(&chatID),
}},
}
agentAuth.AttachOptions(cmd, false)
return cmd
}

// parseChatID returns the chat UUID from the flag value (which
// serpent already populates from --chat or CODER_CHAT_ID). Returns
// uuid.Nil if empty (the server will auto-detect).
func parseChatID(flagValue string) (uuid.UUID, error) {
if flagValue == "" {
return uuid.Nil, nil
}
parsed, err := uuid.Parse(flagValue)
if err != nil {
return uuid.Nil, xerrors.Errorf("invalid chat ID %q: %w", flagValue, err)
}
return parsed, nil
}
27 changes: 0 additions & 27 deletions cli/exp_chat_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,9 @@ import (
"path/filepath"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/require"
)

func TestParseChatID(t *testing.T) {
t.Parallel()

t.Run("EmptyIsNil", func(t *testing.T) {
t.Parallel()
got, err := parseChatID("")
require.NoError(t, err)
require.Equal(t, uuid.Nil, got)
})

t.Run("ValidUUID", func(t *testing.T) {
t.Parallel()
want := uuid.MustParse("11111111-1111-4111-8111-111111111111")
got, err := parseChatID(want.String())
require.NoError(t, err)
require.Equal(t, want, got)
})

t.Run("InvalidErrors", func(t *testing.T) {
t.Parallel()
_, err := parseChatID("not-a-uuid")
require.Error(t, err)
require.Contains(t, err.Error(), "invalid chat ID")
})
}

func TestResolveContextSourcePath(t *testing.T) {
t.Parallel()

Expand Down
7 changes: 0 additions & 7 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1794,8 +1794,6 @@ func New(options *Options) *API {
r.Post("/log-source", api.workspaceAgentPostLogSource)
r.Get("/reinit", api.workspaceAgentReinit)
r.Route("/experimental", func(r chi.Router) {
r.Post("/chat-context", api.workspaceAgentAddChatContext)
r.Delete("/chat-context", api.workspaceAgentClearChatContext)
r.Post("/chat-context/refresh", api.workspaceAgentRefreshChatContext)
})
r.Route("/tasks/{task}", func(r chi.Router) {
Expand Down
11 changes: 0 additions & 11 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1752,17 +1752,6 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
})
}
}
if c.LastInjectedContext.Valid {
var parts []codersdk.ChatMessagePart
// Internal fields are stripped at write time in
// chatd.updateLastInjectedContext, so no
// StripInternal call is needed here. Unmarshal
// errors are suppressed — the column is written by
// us with a known schema.
if err := json.Unmarshal(c.LastInjectedContext.RawMessage, &parts); err == nil {
chat.LastInjectedContext = parts
}
}
// Report pinned-context state when the chat is context-tracked
// (has a pinned hash), dirty, or carries a snapshot error.
if len(c.ContextAggregateHash) > 0 || c.ContextDirtySince.Valid || c.ContextError != "" {
Expand Down
7 changes: 0 additions & 7 deletions coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,13 +720,6 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
PlanMode: database.NullChatPlanMode{ChatPlanMode: database.ChatPlanModePlan, Valid: true},
MCPServerIDs: []uuid.UUID{uuid.New()},
Labels: database.StringMap{"env": "prod"},
LastInjectedContext: pqtype.NullRawMessage{
// Use a context-file part to verify internal
// fields are not present (they are stripped at
// write time by chatd, not at read time).
RawMessage: json.RawMessage(`[{"type":"context-file","context_file_path":"/AGENTS.md"}]`),
Valid: true,
},
DynamicTools: pqtype.NullRawMessage{
RawMessage: json.RawMessage(`[{"name":"tool1","description":"test tool","inputSchema":{"type":"object"}}]`),
Valid: true,
Expand Down
11 changes: 0 additions & 11 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -7123,17 +7123,6 @@ func (q *querier) UpdateChatLabelsByID(ctx context.Context, arg database.UpdateC
return q.db.UpdateChatLabelsByID(ctx, arg)
}

func (q *querier) UpdateChatLastInjectedContext(ctx context.Context, arg database.UpdateChatLastInjectedContextParams) (database.Chat, error) {
chat, err := q.db.GetChatByID(ctx, arg.ID)
if err != nil {
return database.Chat{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
return database.Chat{}, err
}
return q.db.UpdateChatLastInjectedContext(ctx, arg)
}

func (q *querier) UpdateChatLastModelConfigByID(ctx context.Context, arg database.UpdateChatLastModelConfigByIDParams) (database.Chat, error) {
chat, err := q.db.GetChatByID(ctx, arg.ID)
if err != nil {
Expand Down
13 changes: 0 additions & 13 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1899,19 +1899,6 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().UpdateChatMCPServerIDs(gomock.Any(), arg).Return(chat, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
}))
s.Run("UpdateChatLastInjectedContext", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatLastInjectedContextParams{
ID: chat.ID,
LastInjectedContext: pqtype.NullRawMessage{
RawMessage: json.RawMessage(`[{"type":"text","text":"test"}]`),
Valid: true,
},
}
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
dbm.EXPECT().UpdateChatLastInjectedContext(gomock.Any(), arg).Return(chat, nil).AnyTimes()
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
}))
s.Run("UpdateChatLastTurnSummary", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
chat := testutil.Fake(s.T(), faker, database.Chat{})
arg := database.UpdateChatLastTurnSummaryParams{
Expand Down
8 changes: 0 additions & 8 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 0 additions & 15 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading