diff --git a/apps/desktop/src/app/chat/composer/hooks/use-composer-draft.ts b/apps/desktop/src/app/chat/composer/hooks/use-composer-draft.ts index d981d6435d8..82464ffd5d4 100644 --- a/apps/desktop/src/app/chat/composer/hooks/use-composer-draft.ts +++ b/apps/desktop/src/app/chat/composer/hooks/use-composer-draft.ts @@ -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) { diff --git a/apps/desktop/src/app/chat/composer/rich-editor.test.ts b/apps/desktop/src/app/chat/composer/rich-editor.test.ts index e55f24e8cf1..6c3d2f87332 100644 --- a/apps/desktop/src/app/chat/composer/rich-editor.test.ts +++ b/apps/desktop/src/app/chat/composer/rich-editor.test.ts @@ -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', () => { diff --git a/apps/desktop/src/components/assistant-ui/thread/user-edit-composer.tsx b/apps/desktop/src/components/assistant-ui/thread/user-edit-composer.tsx index f27c5358af6..fe7b917afbe 100644 --- a/apps/desktop/src/components/assistant-ui/thread/user-edit-composer.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/user-edit-composer.tsx @@ -168,7 +168,7 @@ export const UserEditComposer: FC = ({ 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 = ({ 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)