mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add Cmd/Ctrl+Enter send setting (#25062)
Adds an Agents General setting to require Cmd/Ctrl+Enter before sending
chat messages. When enabled, plain Enter inserts a newline in agent chat
inputs while the send button remains available.
The preference is now persisted server-side through
`/api/v2/users/{user}/preferences`, alongside the existing user
preference settings, and is applied to both the create-agent input and
existing chat composer. Storybook and API coverage verify the setting,
keyboard behavior, validation, and persistence.
<details>
<summary>Coder Agents notes</summary>
Generated by Coder Agents from a Slack request. Dogfooded with
agent-browser against the Storybook settings and chat input stories.
</details>
This commit is contained in:
Generated
+17
@@ -14909,6 +14909,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentChatSendShortcut": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enter",
|
||||
"modifier_enter"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"AgentChatSendShortcutEnter",
|
||||
"AgentChatSendShortcutModifierEnter"
|
||||
]
|
||||
},
|
||||
"codersdk.AgentConnectionTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23426,6 +23437,9 @@ const docTemplate = `{
|
||||
"codersdk.UpdateUserPreferenceSettingsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_chat_send_shortcut": {
|
||||
"$ref": "#/definitions/codersdk.AgentChatSendShortcut"
|
||||
},
|
||||
"code_diff_display_mode": {
|
||||
"$ref": "#/definitions/codersdk.AgentDisplayMode"
|
||||
},
|
||||
@@ -23899,6 +23913,9 @@ const docTemplate = `{
|
||||
"codersdk.UserPreferenceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_chat_send_shortcut": {
|
||||
"$ref": "#/definitions/codersdk.AgentChatSendShortcut"
|
||||
},
|
||||
"code_diff_display_mode": {
|
||||
"$ref": "#/definitions/codersdk.AgentDisplayMode"
|
||||
},
|
||||
|
||||
Generated
+14
@@ -13375,6 +13375,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentChatSendShortcut": {
|
||||
"type": "string",
|
||||
"enum": ["enter", "modifier_enter"],
|
||||
"x-enum-varnames": [
|
||||
"AgentChatSendShortcutEnter",
|
||||
"AgentChatSendShortcutModifierEnter"
|
||||
]
|
||||
},
|
||||
"codersdk.AgentConnectionTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -21566,6 +21574,9 @@
|
||||
"codersdk.UpdateUserPreferenceSettingsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_chat_send_shortcut": {
|
||||
"$ref": "#/definitions/codersdk.AgentChatSendShortcut"
|
||||
},
|
||||
"code_diff_display_mode": {
|
||||
"$ref": "#/definitions/codersdk.AgentDisplayMode"
|
||||
},
|
||||
@@ -22010,6 +22021,9 @@
|
||||
"codersdk.UserPreferenceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_chat_send_shortcut": {
|
||||
"$ref": "#/definitions/codersdk.AgentChatSendShortcut"
|
||||
},
|
||||
"code_diff_display_mode": {
|
||||
"$ref": "#/definitions/codersdk.AgentDisplayMode"
|
||||
},
|
||||
|
||||
@@ -4346,6 +4346,17 @@ func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetU
|
||||
return q.db.GetUserActivityInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserAgentChatSendShortcut(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
user, err := q.db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionReadPersonal, user); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return q.db.GetUserAgentChatSendShortcut(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetUserByEmailOrUsername)(ctx, arg)
|
||||
}
|
||||
@@ -7010,6 +7021,17 @@ func (q *querier) UpdateUsageEventsPostPublish(ctx context.Context, arg database
|
||||
return q.db.UpdateUsageEventsPostPublish(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateUserAgentChatSendShortcut(ctx context.Context, arg database.UpdateUserAgentChatSendShortcutParams) (string, error) {
|
||||
user, err := q.db.GetUserByID(ctx, arg.UserID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, user); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return q.db.UpdateUserAgentChatSendShortcut(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateUserChatCompactionThreshold(ctx context.Context, arg database.UpdateUserChatCompactionThresholdParams) (database.UserConfig, error) {
|
||||
u, err := q.db.GetUserByID(ctx, arg.UserID)
|
||||
if err != nil {
|
||||
|
||||
@@ -2842,6 +2842,19 @@ func (s *MethodTestSuite) TestUser() {
|
||||
dbm.EXPECT().UpdateUserCodeDiffDisplayMode(gomock.Any(), arg).Return("always_collapsed", nil).AnyTimes()
|
||||
check.Args(arg).Asserts(u, policy.ActionUpdatePersonal).Returns("always_collapsed")
|
||||
}))
|
||||
s.Run("GetUserAgentChatSendShortcut", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
dbm.EXPECT().GetUserByID(gomock.Any(), u.ID).Return(u, nil).AnyTimes()
|
||||
dbm.EXPECT().GetUserAgentChatSendShortcut(gomock.Any(), u.ID).Return("modifier_enter", nil).AnyTimes()
|
||||
check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns("modifier_enter")
|
||||
}))
|
||||
s.Run("UpdateUserAgentChatSendShortcut", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
arg := database.UpdateUserAgentChatSendShortcutParams{UserID: u.ID, AgentChatSendShortcut: "modifier_enter"}
|
||||
dbm.EXPECT().GetUserByID(gomock.Any(), u.ID).Return(u, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateUserAgentChatSendShortcut(gomock.Any(), arg).Return("modifier_enter", nil).AnyTimes()
|
||||
check.Args(arg).Asserts(u, policy.ActionUpdatePersonal).Returns("modifier_enter")
|
||||
}))
|
||||
s.Run("ListUserChatCompactionThresholds", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
uc := database.UserConfig{UserID: u.ID, Key: codersdk.ChatCompactionThresholdKeyPrefix + "00000000-0000-0000-0000-000000000001", Value: "75"}
|
||||
|
||||
@@ -2793,6 +2793,14 @@ func (m queryMetricsStore) GetUserActivityInsights(ctx context.Context, arg data
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserAgentChatSendShortcut(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserAgentChatSendShortcut(ctx, userID)
|
||||
m.queryLatencies.WithLabelValues("GetUserAgentChatSendShortcut").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetUserAgentChatSendShortcut").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserByEmailOrUsername(ctx, arg)
|
||||
@@ -5001,6 +5009,14 @@ func (m queryMetricsStore) UpdateUsageEventsPostPublish(ctx context.Context, arg
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserAgentChatSendShortcut(ctx context.Context, arg database.UpdateUserAgentChatSendShortcutParams) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateUserAgentChatSendShortcut(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateUserAgentChatSendShortcut").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateUserAgentChatSendShortcut").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserChatCompactionThreshold(ctx context.Context, arg database.UpdateUserChatCompactionThresholdParams) (database.UserConfig, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateUserChatCompactionThreshold(ctx, arg)
|
||||
|
||||
@@ -5223,6 +5223,21 @@ func (mr *MockStoreMockRecorder) GetUserActivityInsights(ctx, arg any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserActivityInsights", reflect.TypeOf((*MockStore)(nil).GetUserActivityInsights), ctx, arg)
|
||||
}
|
||||
|
||||
// GetUserAgentChatSendShortcut mocks base method.
|
||||
func (m *MockStore) GetUserAgentChatSendShortcut(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserAgentChatSendShortcut", ctx, userID)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserAgentChatSendShortcut indicates an expected call of GetUserAgentChatSendShortcut.
|
||||
func (mr *MockStoreMockRecorder) GetUserAgentChatSendShortcut(ctx, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAgentChatSendShortcut", reflect.TypeOf((*MockStore)(nil).GetUserAgentChatSendShortcut), ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserByEmailOrUsername mocks base method.
|
||||
func (m *MockStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -9427,6 +9442,21 @@ func (mr *MockStoreMockRecorder) UpdateUsageEventsPostPublish(ctx, arg any) *gom
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUsageEventsPostPublish", reflect.TypeOf((*MockStore)(nil).UpdateUsageEventsPostPublish), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserAgentChatSendShortcut mocks base method.
|
||||
func (m *MockStore) UpdateUserAgentChatSendShortcut(ctx context.Context, arg database.UpdateUserAgentChatSendShortcutParams) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserAgentChatSendShortcut", ctx, arg)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateUserAgentChatSendShortcut indicates an expected call of UpdateUserAgentChatSendShortcut.
|
||||
func (mr *MockStoreMockRecorder) UpdateUserAgentChatSendShortcut(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAgentChatSendShortcut", reflect.TypeOf((*MockStore)(nil).UpdateUserAgentChatSendShortcut), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateUserChatCompactionThreshold mocks base method.
|
||||
func (m *MockStore) UpdateUserChatCompactionThreshold(ctx context.Context, arg database.UpdateUserChatCompactionThresholdParams) (database.UserConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -699,6 +699,7 @@ type sqlcQuerier interface {
|
||||
// produces a bloated value if a user has used multiple templates
|
||||
// simultaneously.
|
||||
GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error)
|
||||
GetUserAgentChatSendShortcut(ctx context.Context, userID uuid.UUID) (string, error)
|
||||
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
|
||||
GetUserChatCompactionThreshold(ctx context.Context, arg GetUserChatCompactionThresholdParams) (string, error)
|
||||
@@ -1195,6 +1196,7 @@ type sqlcQuerier interface {
|
||||
UpdateTemplateVersionFlagsByJobID(ctx context.Context, arg UpdateTemplateVersionFlagsByJobIDParams) error
|
||||
UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error
|
||||
UpdateUsageEventsPostPublish(ctx context.Context, arg UpdateUsageEventsPostPublishParams) error
|
||||
UpdateUserAgentChatSendShortcut(ctx context.Context, arg UpdateUserAgentChatSendShortcutParams) (string, error)
|
||||
UpdateUserChatCompactionThreshold(ctx context.Context, arg UpdateUserChatCompactionThresholdParams) (UserConfig, error)
|
||||
UpdateUserChatCustomPrompt(ctx context.Context, arg UpdateUserChatCustomPromptParams) (UserConfig, error)
|
||||
UpdateUserChatProviderKey(ctx context.Context, arg UpdateUserChatProviderKeyParams) (UserChatProviderKey, error)
|
||||
|
||||
@@ -25607,6 +25607,23 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserAgentChatSendShortcut = `-- name: GetUserAgentChatSendShortcut :one
|
||||
SELECT
|
||||
value AS agent_chat_send_shortcut
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = $1
|
||||
AND key = 'preference_agent_chat_send_shortcut'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetUserAgentChatSendShortcut(ctx context.Context, userID uuid.UUID) (string, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserAgentChatSendShortcut, userID)
|
||||
var agent_chat_send_shortcut string
|
||||
err := row.Scan(&agent_chat_send_shortcut)
|
||||
return agent_chat_send_shortcut, err
|
||||
}
|
||||
|
||||
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
|
||||
@@ -26327,6 +26344,33 @@ func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg Updat
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateUserAgentChatSendShortcut = `-- name: UpdateUserAgentChatSendShortcut :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
($1, 'preference_agent_chat_send_shortcut', $2::text)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = $2
|
||||
WHERE user_configs.user_id = $1
|
||||
AND user_configs.key = 'preference_agent_chat_send_shortcut'
|
||||
RETURNING value AS agent_chat_send_shortcut
|
||||
`
|
||||
|
||||
type UpdateUserAgentChatSendShortcutParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
AgentChatSendShortcut string `db:"agent_chat_send_shortcut" json:"agent_chat_send_shortcut"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserAgentChatSendShortcut(ctx context.Context, arg UpdateUserAgentChatSendShortcutParams) (string, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserAgentChatSendShortcut, arg.UserID, arg.AgentChatSendShortcut)
|
||||
var agent_chat_send_shortcut string
|
||||
err := row.Scan(&agent_chat_send_shortcut)
|
||||
return agent_chat_send_shortcut, err
|
||||
}
|
||||
|
||||
const updateUserChatCompactionThreshold = `-- name: UpdateUserChatCompactionThreshold :one
|
||||
INSERT INTO user_configs (user_id, key, value)
|
||||
VALUES ($1, $2, ($3::int)::text)
|
||||
|
||||
@@ -327,6 +327,29 @@ WHERE user_configs.user_id = @user_id
|
||||
AND user_configs.key = 'preference_code_diff_display_mode'
|
||||
RETURNING value AS code_diff_display_mode;
|
||||
|
||||
-- name: GetUserAgentChatSendShortcut :one
|
||||
SELECT
|
||||
value AS agent_chat_send_shortcut
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
AND key = 'preference_agent_chat_send_shortcut';
|
||||
|
||||
-- name: UpdateUserAgentChatSendShortcut :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
(@user_id, 'preference_agent_chat_send_shortcut', @agent_chat_send_shortcut::text)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = @agent_chat_send_shortcut
|
||||
WHERE user_configs.user_id = @user_id
|
||||
AND user_configs.key = 'preference_agent_chat_send_shortcut'
|
||||
RETURNING value AS agent_chat_send_shortcut;
|
||||
|
||||
-- name: UpdateUserRoles :one
|
||||
UPDATE
|
||||
users
|
||||
|
||||
+48
-2
@@ -1257,10 +1257,20 @@ func (api *API) userPreferenceSettings(rw http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
agentChatSendShortcut, err := api.Database.GetUserAgentChatSendShortcut(ctx, user.ID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Error reading user preference settings.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserPreferenceSettings{
|
||||
TaskNotificationAlertDismissed: taskAlertDismissed,
|
||||
ThinkingDisplayMode: sanitizeThinkingDisplayMode(thinkingMode),
|
||||
CodeDiffDisplayMode: sanitizeAgentDisplayMode(codeDiffMode),
|
||||
AgentChatSendShortcut: sanitizeAgentChatSendShortcut(agentChatSendShortcut),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1305,6 +1315,16 @@ func (api *API) putUserPreferenceSettings(rw http.ResponseWriter, r *http.Reques
|
||||
})
|
||||
return
|
||||
}
|
||||
if params.AgentChatSendShortcut != "" &&
|
||||
!slices.Contains(codersdk.ValidAgentChatSendShortcuts, params.AgentChatSendShortcut) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid agent chat send shortcut.",
|
||||
Validations: []codersdk.ValidationError{
|
||||
{Field: "agent_chat_send_shortcut", Detail: agentChatSendShortcutValidationDetail},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
var settings codersdk.UserPreferenceSettings
|
||||
err := api.Database.InTx(func(tx database.Store) error {
|
||||
var err error
|
||||
@@ -1356,6 +1376,23 @@ func (api *API) putUserPreferenceSettings(rw http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
settings.CodeDiffDisplayMode = sanitizeAgentDisplayMode(stored)
|
||||
}
|
||||
|
||||
if params.AgentChatSendShortcut != "" {
|
||||
updated, err := tx.UpdateUserAgentChatSendShortcut(ctx, database.UpdateUserAgentChatSendShortcutParams{
|
||||
UserID: user.ID,
|
||||
AgentChatSendShortcut: string(params.AgentChatSendShortcut),
|
||||
})
|
||||
if err != nil {
|
||||
return newUserPreferenceSettingsAPIError("Internal error updating agent chat send shortcut.", err)
|
||||
}
|
||||
settings.AgentChatSendShortcut = sanitizeAgentChatSendShortcut(updated)
|
||||
} else {
|
||||
stored, err := tx.GetUserAgentChatSendShortcut(ctx, user.ID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return newUserPreferenceSettingsAPIError("Error reading agent chat send shortcut.", err)
|
||||
}
|
||||
settings.AgentChatSendShortcut = sanitizeAgentChatSendShortcut(stored)
|
||||
}
|
||||
return nil
|
||||
}, database.DefaultTXOptions().WithID("user_preference_settings"))
|
||||
if err != nil {
|
||||
@@ -1400,8 +1437,9 @@ func (e userPreferenceSettingsAPIError) Unwrap() error {
|
||||
}
|
||||
|
||||
const (
|
||||
thinkingDisplayModeValidationDetail = "must be one of: auto, preview, always_expanded, always_collapsed"
|
||||
agentDisplayModeValidationDetail = "must be one of: auto, always_expanded, always_collapsed"
|
||||
thinkingDisplayModeValidationDetail = "must be one of: auto, preview, always_expanded, always_collapsed"
|
||||
agentDisplayModeValidationDetail = "must be one of: auto, always_expanded, always_collapsed"
|
||||
agentChatSendShortcutValidationDetail = "must be one of: enter, modifier_enter"
|
||||
)
|
||||
|
||||
func sanitizeThinkingDisplayMode(raw string) codersdk.ThinkingDisplayMode {
|
||||
@@ -1420,6 +1458,14 @@ func sanitizeAgentDisplayMode(raw string) codersdk.AgentDisplayMode {
|
||||
return codersdk.AgentDisplayModeAuto
|
||||
}
|
||||
|
||||
func sanitizeAgentChatSendShortcut(raw string) codersdk.AgentChatSendShortcut {
|
||||
shortcut := codersdk.AgentChatSendShortcut(raw)
|
||||
if slices.Contains(codersdk.ValidAgentChatSendShortcuts, shortcut) {
|
||||
return shortcut
|
||||
}
|
||||
return codersdk.AgentChatSendShortcutEnter
|
||||
}
|
||||
|
||||
func isValidFontName(font codersdk.TerminalFontName) bool {
|
||||
return slices.Contains(codersdk.TerminalFontNames, font)
|
||||
}
|
||||
|
||||
@@ -1963,6 +1963,91 @@ func TestThinkingDisplayMode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgentChatSendShortcutPreference(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient := coderdtest.New(t, nil)
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
|
||||
requireValidationField := func(t *testing.T, err error, field string) {
|
||||
t.Helper()
|
||||
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
require.Len(t, sdkErr.Validations, 1)
|
||||
require.Equal(t, field, sdkErr.Validations[0].Field)
|
||||
}
|
||||
|
||||
t.Run("defaults to enter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.AgentChatSendShortcutEnter, settings.AgentChatSendShortcut)
|
||||
})
|
||||
|
||||
t.Run("round-trips shortcut", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
|
||||
AgentChatSendShortcut: codersdk.AgentChatSendShortcutModifierEnter,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.AgentChatSendShortcutModifierEnter, updated.AgentChatSendShortcut)
|
||||
|
||||
settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.AgentChatSendShortcutModifierEnter, settings.AgentChatSendShortcut)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid shortcut", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
|
||||
AgentChatSendShortcut: codersdk.AgentChatSendShortcut("bogus"),
|
||||
})
|
||||
requireValidationField(t, err, "agent_chat_send_shortcut")
|
||||
})
|
||||
|
||||
t.Run("updates preserve stored shortcut", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
|
||||
AgentChatSendShortcut: codersdk.AgentChatSendShortcutModifierEnter,
|
||||
ThinkingDisplayMode: codersdk.ThinkingDisplayModePreview,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
|
||||
ThinkingDisplayMode: codersdk.ThinkingDisplayModeAlwaysExpanded,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.ThinkingDisplayModeAlwaysExpanded, updated.ThinkingDisplayMode)
|
||||
require.Equal(t, codersdk.AgentChatSendShortcutModifierEnter, updated.AgentChatSendShortcut)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgentDisplayModePreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user