From 959e177cdfe6fb222e36edb1c0c064c8ec6bbd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Wed, 3 Jun 2026 09:29:57 +0800 Subject: [PATCH] fix: keep agent slash selection editable before send (#653) Selecting a concrete /agent entry from the composer suggestions should prepare the command and leave focus in the editor so the user can type the required prompt. The Enter send shortcut still applies after the suggestion menu is closed, and exact non-agent slash commands keep their existing direct execution path when they remain highlighted. Constraint: The desktop composer supports configurable Enter vs Ctrl/Cmd+Enter sending. Rejected: Special-case only /agent entries | the highlighted suggestion should consistently win while the menu is open. Confidence: high Scope-risk: narrow Directive: Keep ChatInput and EmptySession slash-key handling aligned with shared send shortcut behavior. Tested: cd desktop && bun run test -- --run src/components/chat/composerUtils.test.ts src/components/chat/ChatInput.test.tsx src/pages/EmptySession.test.tsx Tested: bun run check:desktop Not-tested: Live backend agent-list integration beyond mocked desktop composer coverage. --- .../src/components/chat/ChatInput.test.tsx | 50 +++++++++++++++++++ desktop/src/components/chat/ChatInput.tsx | 3 +- desktop/src/pages/EmptySession.test.tsx | 37 ++++++++++++++ desktop/src/pages/EmptySession.tsx | 3 +- 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/desktop/src/components/chat/ChatInput.test.tsx b/desktop/src/components/chat/ChatInput.test.tsx index a482110b..d95a66cb 100644 --- a/desktop/src/components/chat/ChatInput.test.tsx +++ b/desktop/src/components/chat/ChatInput.test.tsx @@ -1101,4 +1101,54 @@ describe('ChatInput file mentions', () => { expect(input).toHaveValue('/agent debugger ') }) + + it('selects a highlighted agent entry from /agent without sending until the configured send shortcut is used', async () => { + useSettingsStore.setState({ + chatSendBehavior: 'modifierEnter', + }) + mocks.listAgents.mockResolvedValue({ + activeAgents: [ + { + agentType: 'debugger', + description: 'Debug failures', + modelDisplay: 'OPUS', + source: 'userSettings', + isActive: true, + }, + ], + allAgents: [], + }) + + render() + + await waitFor(() => { + expect(mocks.listAgents).toHaveBeenCalledWith('/repo') + }) + + const input = screen.getByRole('textbox') as HTMLTextAreaElement + fireEvent.change(input, { + target: { value: '/agent', selectionStart: 6 }, + }) + + await screen.findByText('/agent debugger') + fireEvent.keyDown(input, { key: 'ArrowDown' }) + fireEvent.keyDown(input, { key: 'Enter' }) + + expect(input).toHaveValue('/agent debugger ') + expect(mocks.wsSend).not.toHaveBeenCalled() + + const prompt = '/agent debugger investigate this failure' + fireEvent.change(input, { + target: { value: prompt, selectionStart: prompt.length }, + }) + fireEvent.keyDown(input, { key: 'Enter' }) + expect(mocks.wsSend).not.toHaveBeenCalled() + + fireEvent.keyDown(input, { key: 'Enter', ctrlKey: true }) + expect(mocks.wsSend).toHaveBeenCalledWith(sessionId, { + type: 'user_message', + content: prompt, + attachments: [], + }) + }) }) diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index 54fa73bb..a17c7d99 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -737,8 +737,10 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro return } if (event.key === 'Enter') { + const selected = filteredCommands[slashSelectedIndex] if ( exactSlashCommand && + selected?.name.toLowerCase() === exactSlashCommand.name.toLowerCase() && slashFilter.trim().toLowerCase() === exactSlashCommand.name.toLowerCase() && shouldSubmitOnEnter(event, chatSendBehavior) ) { @@ -747,7 +749,6 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro return } event.preventDefault() - const selected = filteredCommands[slashSelectedIndex] if (selected) selectSlashCommand(selected.name) return } diff --git a/desktop/src/pages/EmptySession.test.tsx b/desktop/src/pages/EmptySession.test.tsx index 7f19ee76..0926f8bd 100644 --- a/desktop/src/pages/EmptySession.test.tsx +++ b/desktop/src/pages/EmptySession.test.tsx @@ -353,6 +353,43 @@ describe('EmptySession', () => { expect(input).toHaveValue('/agent debugger ') }) + it('selects a highlighted agent entry from /agent without creating a session', async () => { + useSettingsStore.setState({ + chatSendBehavior: 'enter', + }) + mocks.listAgents.mockResolvedValue({ + activeAgents: [ + { + agentType: 'debugger', + description: 'Debug failures', + modelDisplay: 'OPUS', + source: 'userSettings', + isActive: true, + }, + ], + allAgents: [], + }) + + render() + + await waitFor(() => { + expect(mocks.listAgents).toHaveBeenCalledWith(undefined) + }) + + const input = screen.getByRole('textbox') as HTMLTextAreaElement + fireEvent.change(input, { + target: { value: '/agent', selectionStart: 6 }, + }) + + await screen.findByText('/agent debugger') + fireEvent.keyDown(input, { key: 'ArrowDown' }) + fireEvent.keyDown(input, { key: 'Enter' }) + + expect(input).toHaveValue('/agent debugger ') + expect(mocks.createSession).not.toHaveBeenCalled() + expect(mocks.wsSend).not.toHaveBeenCalled() + }) + it('integrates repository launch controls into the desktop composer panel', async () => { render() diff --git a/desktop/src/pages/EmptySession.tsx b/desktop/src/pages/EmptySession.tsx index e733a348..92482e1d 100644 --- a/desktop/src/pages/EmptySession.tsx +++ b/desktop/src/pages/EmptySession.tsx @@ -415,9 +415,11 @@ export function EmptySession() { return } if (event.key === 'Enter' || event.key === 'Tab') { + const selected = filteredCommands[slashSelectedIndex] if ( event.key === 'Enter' && exactSlashCommand && + selected?.name.toLowerCase() === exactSlashCommand.name.toLowerCase() && slashFilter.trim().toLowerCase() === exactSlashCommand.name.toLowerCase() && shouldSubmitOnEnter(event, chatSendBehavior) ) { @@ -426,7 +428,6 @@ export function EmptySession() { return } event.preventDefault() - const selected = filteredCommands[slashSelectedIndex] if (selected) selectSlashCommand(selected.name) return }