mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #74815 from NousResearch/bb/composer-clear-parity
Clearing the composer empties it whichever way you do it
This commit is contained in:
commit
fd90ef77be
3 changed files with 95 additions and 2 deletions
71
apps/desktop/src/app/chat/composer/empty-composer.test.ts
Normal file
71
apps/desktop/src/app/chat/composer/empty-composer.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { composerPlainText, normalizeComposerEditorDom, RICH_INPUT_SLOT } from './rich-editor'
|
||||
|
||||
function editor(): HTMLDivElement {
|
||||
const el = document.createElement('div')
|
||||
|
||||
el.dataset.slot = RICH_INPUT_SLOT
|
||||
el.contentEditable = 'true'
|
||||
document.body.append(el)
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
/** Whatever emptied it — Delete, cut, Chromium's own selection-delete — the
|
||||
* normalizer lands on the same DOM. */
|
||||
function emptied(): HTMLDivElement {
|
||||
const el = editor()
|
||||
|
||||
el.append(document.createTextNode('hello'))
|
||||
el.replaceChildren()
|
||||
normalizeComposerEditorDom(el)
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
describe('an emptied composer reads as empty', () => {
|
||||
it('keeps the placeholder <br> so the contenteditable holds its height', () => {
|
||||
// The scaffolding is deliberate: a childless contenteditable collapses to a
|
||||
// sliver in Chromium. It just must not read as content.
|
||||
expect(emptied().innerHTML).toBe('<br>')
|
||||
})
|
||||
|
||||
it('reads that editor as empty, not as a newline', () => {
|
||||
expect(composerPlainText(emptied())).toBe('')
|
||||
})
|
||||
|
||||
it('reads a truly childless editor as empty', () => {
|
||||
expect(composerPlainText(editor())).toBe('')
|
||||
})
|
||||
|
||||
it('still reads a real Shift+Enter line break as a newline', () => {
|
||||
const el = editor()
|
||||
|
||||
el.append(document.createTextNode('one'), document.createElement('br'), document.createTextNode('two'))
|
||||
|
||||
expect(composerPlainText(el)).toBe('one\ntwo')
|
||||
})
|
||||
|
||||
it('still reads a trailing break after text as a newline', () => {
|
||||
const el = editor()
|
||||
|
||||
el.append(document.createTextNode('one'), document.createElement('br'))
|
||||
|
||||
expect(composerPlainText(el)).toBe('one\n')
|
||||
})
|
||||
|
||||
it('only treats the EDITOR\u2019s lone <br> as scaffolding, not a nested one', () => {
|
||||
// A lone <br> inside some other element is a real line break; the exemption
|
||||
// is scoped to the editor root by its slot marker. (The block wrapper adds
|
||||
// its own trailing newline — unchanged behavior, asserted so the exemption
|
||||
// can't quietly widen to nested nodes.)
|
||||
const el = editor()
|
||||
const inner = document.createElement('div')
|
||||
|
||||
inner.append(document.createElement('br'))
|
||||
el.append(document.createTextNode('one'), inner)
|
||||
|
||||
expect(composerPlainText(el)).toBe('one\n\n')
|
||||
})
|
||||
})
|
||||
|
|
@ -2,6 +2,7 @@ import { useAui, useAuiState, useComposerRuntime } from '@assistant-ui/react'
|
|||
import { type RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
|
||||
import { SLASH_COMMAND_RE } from '@/lib/chat-runtime'
|
||||
import { sanitizeComposerInput } from '@/lib/composer-input-sanitize'
|
||||
import { type ComposerAttachment, stashSessionDraft, takeSessionDraft } from '@/store/composer'
|
||||
import { isBrowsingHistory } from '@/store/composer-input-history'
|
||||
|
||||
|
|
@ -21,7 +22,13 @@ import {
|
|||
releaseActiveComposer
|
||||
} from '../focus'
|
||||
import { type InlineRefInput, insertInlineRefsIntoEditor } from '../inline-refs'
|
||||
import { composerPlainText, placeCaretEnd, REF_RE, renderComposerContents } from '../rich-editor'
|
||||
import {
|
||||
composerPlainText,
|
||||
normalizeComposerEditorDom,
|
||||
placeCaretEnd,
|
||||
REF_RE,
|
||||
renderComposerContents
|
||||
} from '../rich-editor'
|
||||
import { useComposerScope } from '../scope'
|
||||
import type { ChatBarProps } from '../types'
|
||||
|
||||
|
|
@ -237,7 +244,13 @@ export function useComposerDraft({
|
|||
return draftRef.current
|
||||
}
|
||||
|
||||
const text = composerPlainText(editor)
|
||||
// Same normalize-then-sanitize the rAF flush does. An emptied editor still
|
||||
// holds the placeholder <br> that keeps the contenteditable from collapsing
|
||||
// to a sliver, and that serializes as "\n" — so an editor the user just
|
||||
// cleared would otherwise stash a one-newline draft and come back non-empty.
|
||||
normalizeComposerEditorDom(editor)
|
||||
|
||||
const text = sanitizeComposerInput(composerPlainText(editor))
|
||||
|
||||
if (text !== draftRef.current) {
|
||||
draftRef.current = text
|
||||
|
|
|
|||
|
|
@ -482,6 +482,15 @@ export function composerPlainText(node: Node): string {
|
|||
return el.dataset.refText
|
||||
}
|
||||
|
||||
// An editor holding nothing but the placeholder <br> is EMPTY. That <br> is
|
||||
// scaffolding normalizeComposerEditorDom adds so the contenteditable keeps
|
||||
// its height — not a line the user typed. Reading it as "\n" is how a
|
||||
// just-cleared composer stayed non-empty: the newline got stashed as the
|
||||
// session's draft and painted back on return.
|
||||
if (el.dataset.slot === RICH_INPUT_SLOT && el.childNodes.length === 1 && el.firstChild?.nodeName === 'BR') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (el.tagName === 'BR') {
|
||||
return '\n'
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue