mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): hydrate commands when repainting inert composer text
Two repaint sites hand the editor text that is finished rather than mid-keystroke: the main composer's programmatic draft writes (restore, insert, history recall) and the inline edit composer opening a sent message. Both now render with `trailingCommitted`, so a command ending that text chips instead of reading as a half-typed token — the edit composer in particular showed plain text for a message the transcript had just rendered with a pill. Regression tests cover the paste path: a command ending the paste, one named mid-prose beside a ref, a path left alone, a paste landing against a word, and one landing after an existing chip.
This commit is contained in:
parent
ccca952b92
commit
422ecfe1da
3 changed files with 84 additions and 4 deletions
|
|
@ -121,7 +121,7 @@ export function useComposerDraft({
|
|||
const editor = editorRef.current
|
||||
|
||||
if (editor) {
|
||||
renderComposerContents(editor, next)
|
||||
renderComposerContents(editor, next, { trailingCommitted: true })
|
||||
placeCaretEnd(editor)
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ export function useComposerDraft({
|
|||
const editor = editorRef.current
|
||||
|
||||
if (editor && document.activeElement !== editor && composerPlainText(editor) !== text) {
|
||||
renderComposerContents(editor, text)
|
||||
renderComposerContents(editor, text, { trailingCommitted: true })
|
||||
}
|
||||
|
||||
if (isBrowsingHistory(sessionIdRef.current) || queueEditRef.current) {
|
||||
|
|
|
|||
|
|
@ -230,6 +230,82 @@ describe('insertComposerContentsAtCaret', () => {
|
|||
|
||||
editor.remove()
|
||||
})
|
||||
|
||||
// A directive typed by hand chips; the same directive pasted has to chip too,
|
||||
// or copy/pasting a prompt silently drops every command in it.
|
||||
it('chips a pasted slash command, including one that ends the paste', () => {
|
||||
const editor = document.createElement('div')
|
||||
editor.dataset.slot = RICH_INPUT_SLOT
|
||||
document.body.append(editor)
|
||||
caretIn(editor)
|
||||
|
||||
insertComposerContentsAtCaret(editor, '/some-skill')
|
||||
|
||||
expect(editor.querySelector('[data-slash-kind]')?.getAttribute('data-ref-text')).toBe('/some-skill')
|
||||
// Committed pills carry the trailing space the typed path appends, so a
|
||||
// later full re-render doesn't read the token as half-typed.
|
||||
expect(composerPlainText(editor)).toBe('/some-skill ')
|
||||
|
||||
editor.remove()
|
||||
})
|
||||
|
||||
it('chips a skill named mid-paste alongside a ref', () => {
|
||||
const editor = document.createElement('div')
|
||||
editor.dataset.slot = RICH_INPUT_SLOT
|
||||
document.body.append(editor)
|
||||
caretIn(editor)
|
||||
|
||||
insertComposerContentsAtCaret(editor, 'clean @file:`a.ts` with /some-skill then ship')
|
||||
|
||||
expect(editor.querySelectorAll('[data-slash-kind]').length).toBe(1)
|
||||
expect(editor.querySelectorAll('[data-ref-kind="file"]').length).toBe(1)
|
||||
expect(composerPlainText(editor)).toBe('clean @file:`a.ts` with /some-skill then ship')
|
||||
|
||||
editor.remove()
|
||||
})
|
||||
|
||||
it('leaves a pasted path alone — /usr/local is not a command', () => {
|
||||
const editor = document.createElement('div')
|
||||
editor.dataset.slot = RICH_INPUT_SLOT
|
||||
document.body.append(editor)
|
||||
caretIn(editor)
|
||||
|
||||
insertComposerContentsAtCaret(editor, 'see /usr/local/bin and /goal ship it')
|
||||
|
||||
expect(editor.querySelector('[data-slash-kind]')).toBeNull()
|
||||
expect(composerPlainText(editor)).toBe('see /usr/local/bin and /goal ship it')
|
||||
|
||||
editor.remove()
|
||||
})
|
||||
|
||||
it('does not chip a command pasted against a word — foo/clean is not a command', () => {
|
||||
const editor = document.createElement('div')
|
||||
editor.dataset.slot = RICH_INPUT_SLOT
|
||||
editor.textContent = 'foo'
|
||||
document.body.append(editor)
|
||||
caretIn(editor)
|
||||
|
||||
insertComposerContentsAtCaret(editor, '/some-skill')
|
||||
|
||||
expect(editor.querySelector('[data-slash-kind]')).toBeNull()
|
||||
expect(composerPlainText(editor)).toBe('foo/some-skill')
|
||||
|
||||
editor.remove()
|
||||
})
|
||||
|
||||
it('chips a command pasted right after an existing chip', () => {
|
||||
const editor = document.createElement('div')
|
||||
editor.dataset.slot = RICH_INPUT_SLOT
|
||||
editor.append(refChipElement('file', '`a.ts`'))
|
||||
document.body.append(editor)
|
||||
caretIn(editor)
|
||||
|
||||
insertComposerContentsAtCaret(editor, '/some-skill')
|
||||
|
||||
expect(editor.querySelector('[data-slash-kind]')).not.toBeNull()
|
||||
|
||||
editor.remove()
|
||||
})
|
||||
})
|
||||
|
||||
describe('replaceBeforeCaret', () => {
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ export const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sess
|
|||
const editor = editorRef.current
|
||||
|
||||
if (editor) {
|
||||
renderComposerContents(editor, next)
|
||||
renderComposerContents(editor, next, { trailingCommitted: true })
|
||||
placeCaretEnd(editor)
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +187,11 @@ export const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sess
|
|||
editor &&
|
||||
(editor.childNodes.length === 0 || (document.activeElement !== editor && composerPlainText(editor) !== draft))
|
||||
) {
|
||||
renderComposerContents(editor, draft)
|
||||
// Inert by construction — this repaints on mount or when the editor
|
||||
// isn't the one being typed into. A message opened for edit is finished
|
||||
// text, so a `/command` ending it is committed and chips, matching how
|
||||
// the transcript rendered that same message a moment ago.
|
||||
renderComposerContents(editor, draft, { trailingCommitted: true })
|
||||
|
||||
if (document.activeElement === editor) {
|
||||
placeCaretEnd(editor)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue