mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
fix(desktop): restore configured composer line breaks
This commit is contained in:
@@ -8,6 +8,8 @@ const viewportMocks = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth')
|
||||
const originalRangeGetClientRects = Object.getOwnPropertyDescriptor(Range.prototype, 'getClientRects')
|
||||
const originalRangeGetBoundingClientRect = Object.getOwnPropertyDescriptor(Range.prototype, 'getBoundingClientRect')
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
@@ -239,8 +241,29 @@ describe('ChatInput file mentions', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
// jsdom does not implement it, and the branch list scrolls its active row
|
||||
// into view whenever the selection moves.
|
||||
// jsdom does not implement these layout APIs. ProseMirror reads Range
|
||||
// geometry when a newline transaction scrolls the new selection into view.
|
||||
Object.defineProperties(Range.prototype, {
|
||||
getClientRects: {
|
||||
configurable: true,
|
||||
value: () => [],
|
||||
},
|
||||
getBoundingClientRect: {
|
||||
configurable: true,
|
||||
value: () => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
// The branch list also scrolls its active row whenever the selection moves.
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
mocks.getGitInfo.mockResolvedValue({ branch: 'main', repoName: 'repo', workDir: '/repo', changedFiles: 0 })
|
||||
mocks.getRepositoryContext.mockResolvedValue(okRepositoryContext())
|
||||
@@ -260,6 +283,16 @@ describe('ChatInput file mentions', () => {
|
||||
} else {
|
||||
Reflect.deleteProperty(HTMLElement.prototype, 'offsetWidth')
|
||||
}
|
||||
if (originalRangeGetClientRects) {
|
||||
Object.defineProperty(Range.prototype, 'getClientRects', originalRangeGetClientRects)
|
||||
} else {
|
||||
Reflect.deleteProperty(Range.prototype, 'getClientRects')
|
||||
}
|
||||
if (originalRangeGetBoundingClientRect) {
|
||||
Object.defineProperty(Range.prototype, 'getBoundingClientRect', originalRangeGetBoundingClientRect)
|
||||
} else {
|
||||
Reflect.deleteProperty(Range.prototype, 'getBoundingClientRect')
|
||||
}
|
||||
})
|
||||
|
||||
// jsdom lays nothing out, so the composer column's width has to be stated.
|
||||
@@ -1935,7 +1968,37 @@ describe('ChatInput file mentions', () => {
|
||||
expect(screen.getByTestId('chat-input-toolbar')).toHaveClass('-mx-3')
|
||||
})
|
||||
|
||||
it('uses Ctrl or Command Enter to send when that composer preference is selected', async () => {
|
||||
it('uses Shift+Enter for a newline when Enter is the configured send shortcut', async () => {
|
||||
useSettingsStore.setState({
|
||||
chatSendBehavior: 'enter',
|
||||
})
|
||||
|
||||
render(<ChatInput />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.getGitInfo).toHaveBeenCalledWith(sessionId)
|
||||
})
|
||||
|
||||
const input = getComposerElement()
|
||||
setComposerText('firstsecond', 5)
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter', shiftKey: true })
|
||||
|
||||
expect(mocks.wsSend).not.toHaveBeenCalled()
|
||||
expect(getComposerText()).toBe('first\nsecond')
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
expect(mocks.wsSend).toHaveBeenCalledWith(sessionId, {
|
||||
type: 'user_message',
|
||||
content: 'first\nsecond',
|
||||
attachments: [],
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['Ctrl+Enter', { ctrlKey: true }],
|
||||
['Command+Enter', { metaKey: true }],
|
||||
])('uses Enter or Shift+Enter for newlines and %s to send when configured', async (_shortcut, modifier) => {
|
||||
useSettingsStore.setState({
|
||||
chatSendBehavior: 'modifierEnter',
|
||||
})
|
||||
@@ -1947,15 +2010,20 @@ describe('ChatInput file mentions', () => {
|
||||
})
|
||||
|
||||
const input = getComposerElement()
|
||||
setComposerText('avoid accidental sends', 'avoid accidental sends'.length)
|
||||
setComposerText('firstsecond', 5)
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
expect(mocks.wsSend).not.toHaveBeenCalled()
|
||||
expect(getComposerText()).toBe('first\nsecond')
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter', ctrlKey: true })
|
||||
fireEvent.keyDown(input, { key: 'Enter', shiftKey: true })
|
||||
expect(mocks.wsSend).not.toHaveBeenCalled()
|
||||
expect(getComposerText()).toBe('first\n\nsecond')
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter', ...modifier })
|
||||
expect(mocks.wsSend).toHaveBeenCalledWith(sessionId, {
|
||||
type: 'user_message',
|
||||
content: 'avoid accidental sends',
|
||||
content: 'first\n\nsecond',
|
||||
attachments: [],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { history, redo, undo } from 'prosemirror-history'
|
||||
import { baseKeymap } from 'prosemirror-commands'
|
||||
import { baseKeymap, splitBlock } from 'prosemirror-commands'
|
||||
import {
|
||||
buildComposerDoc,
|
||||
composerSchema,
|
||||
@@ -139,6 +139,10 @@ export const MentionComposer = forwardRef<MentionComposerHandle, MentionComposer
|
||||
keymap({
|
||||
'Backspace': deleteAdjacentMentionAtom('backward'),
|
||||
'Delete': deleteAdjacentMentionAtom('forward'),
|
||||
// Unlike a textarea, ProseMirror does not include Shift+Enter in
|
||||
// its base keymap. Keep the composer's configured newline path
|
||||
// explicit instead of relying on browser contenteditable behavior.
|
||||
'Shift-Enter': splitBlock,
|
||||
}),
|
||||
keymap({ 'Mod-z': undo, 'Mod-y': redo, 'Mod-Shift-z': redo }),
|
||||
keymap(baseKeymap),
|
||||
|
||||
Reference in New Issue
Block a user