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:
Thomas Kosiewski
2026-05-12 10:09:34 +02:00
committed by GitHub
parent 376fc80451
commit 5c3b59151e
32 changed files with 720 additions and 63 deletions
+48 -2
View File
@@ -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)
}