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
+85
View File
@@ -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()