# NousResearch/hermes-agent · Desktop send button missing with committed IME text input

## Open now

- Nothing open on GitHub.

The members concern the desktop app's send button not appearing when Chinese, Japanese, or Korean text is entered via an IME. Two items share the same title, fix(desktop): show send button for committed IME text, and one is a bug report about Chinese characters making the send button disappear.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #40210. Verify it addresses your case.
- **Recommended:** read PR #40210 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-40210`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #39614: \[Bug\]:  Chinese Character Send Button Disappear Bug

## Prior & competing PRs

- **#40210: fix(desktop): show send button for committed IME text (Chinese/Japanese/Korean)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx`, `apps/desktop/src/app/chat/composer/index.tsx`
  - fix hunks:
  - `apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx`

    ```diff
    @@ -0,0 +1,108 @@
    +import { act, cleanup, fireEvent, render } from '@testing-library/react'
    +import { useRef, useState } from 'react'
    +import { afterEach, describe, expect, it } from 'vitest'
    +
    +// No global setupFiles registers auto-cleanup, so unmount between tests —
    +// otherwise a second render() leaks the first editor and getByTestId('editor')
    +// matches multiple nodes.
    +afterEach(cleanup)
    +
    +// Faithful mirror of index.tsx's composer text wiring for IME input, driven
    +// through REAL DOM composition + input events on a contentEditable.
    +//
    +// Regression repro for #39614: typing committed multi-character IME text (e.g.
    +// Chinese "你好") used to leave the send button hidden. The input events fired
    +// during composition carry uncommitted preedit text and are intentionally
    +// skipped; Chromium then does NOT reliably emit a trailing input event after
    +// compositionend on Windows IMEs, so the finalized text never reached composer
    +// state and `hasPayload` stayed false until an unrelated edit forced a sync.
    +// The fix flushes the live DOM text in onCompositionEnd.
    +function Harness({ onPayload }: { onPayload: (hasPayload: boolean) => void }) {
    +  const editorRef = useRef<HTMLDivElement>(null)
    +  const composingRef = useRef(false)
    +  const draftRef = useRef('')
    +  const [draft, setDraft] = useState('')
    +
    +  const flushEditorToDraft = (editor: HTMLDivElement) => {
    +    const next = editor.textContent ?? ''
    +
    +    if (next !== draftRef.current) {
    +      draftRef.current = next
    +      setDraft(next)
    +    }
    +  }
    +
    +  onPayload(draft.trim().length > 0)
    +
    +  return (
    +    <div
    +      contentEditable
    +      data-testid="editor"
    +      onCompositionEnd={event => {
    +        composingRef.current = false
    +        flushEditorToDraft(event.currentTarget)
    +      }}
    +      onCompositionStart={() => {
    +        composingRef.current = true
    +      }}
    +      onInput={event => {
    +        if (composingRef.current) {
    +          return
    +        }
    +
    +        flushEditorToDraft(event.currentTarget)
    +      }}
    +      ref={editorRef}
    +      suppressContentEditableWarning
    +    />
    +  )
    +}
    +
    +describe('composer IME composition — send button visibility (#39614)', () => {
    +  it('shows the send button after committing CJK text without a trailing edit', async () => {
    +    let hasPayload = false
    +    const { getByTestId } = render(<Harness onPayload={p => (hasPayload = p)} />)
    +    const editor = getByTestId('editor')
    +
    +    // Compose "你好" the way a Windows Chinese IME does: compositionstart, then
    +    // input events carrying uncommitted preedit text, then compositionend with
    +    // the committed text already in the DOM — and crucially NO input event
    +    // afterwards.
    +    await act(async () => {
    +      fireEvent.compositionStart(editor)
    +      editor.textContent = '你'
    +      fireEvent.input(editor)
    +      editor.textContent = '你好'
    +      fireEvent.input(editor)
    +      fireEvent.compositionEnd(editor)
    +    })
    +
    +    // Before the fix this was false (button hidden) until a further edit.
    +    expect(hasPayload).toBe(true)
    +    expect(editor.textContent).toBe('你好')
    +  })
    +
    +  it('also covers Japanese/Korean and any IME-composed script', async () => {
    +    let hasPayload = false
    +    const { getByTestId } = render(<Harness onPayload={p => (hasPayload = p)} />)
    +    const editor = getByTestId('editor')
    +
    +    for (const committed of ['こんにちは', '안녕하세요']) {
    +      await act(async () => {
    +        fireEvent.compositionStart(editor)
    +        editor.textContent = committed
    +        fireEvent.input(editor)
    +        fireEvent.compositionEnd(editor)
    +      })
    +
    +      expect(hasPayload).toBe(true)
    +
    +      // Clear for the next script.
    +      await act(async () => {
    +        editor.textContent = ''
    +        fireEvent.input(editor)
    +      })
    +      expect(hasPayload).toBe(false)
    +    }
    +  })
    +})
    ```
  - `apps/desktop/src/app/chat/composer/index.tsx`

    ```diff
    @@ -549,16 +549,10 @@ export function ChatBar({
         }
       }, [trigger])
     
    -  const handleEditorInput = (event: FormEvent<HTMLDivElement>) => {
    -    // During IME composition the DOM contains uncommitted preedit text
    -    // mixed with real content.  Skip state writes — compositionend will
    -    // deliver the finalized text via a clean input event.
    -    if (composingRef.current) {
    -      return
    -    }
    -
    -    const editor = event.currentTarget
    -
    +  // Pull the live contentEditable text into draftRef + the AUI composer state
    +  // (which drives `hasComposerPayload` → the send button). Shared by the input
    +  // and compositionend paths so committed IME text reaches state through either.
    +  const flushEditorToDraft = (editor: HTMLDivElement) => {
         if (editor.childNodes.length === 1 && editor.firstChild?.nodeName === 'BR') {
           editor.replaceChildren()
         }
    ```

    ```diff
    @@ -573,6 +567,17 @@ export function ChatBar({
         window.setTimeout(refreshTrigger, 0)
       }
     
    +  const handleEditorInput = (event: FormEvent<HTMLDivElement>) => {
    +    // During IME composition the DOM contains uncommitted preedit text
    +    // mixed with real content.  Skip state writes — compositionend flushes
    +    // the finalized text (see onCompositionEnd).
    +    if (composingRef.current) {
    +      return
    +    }
    +
    +    flushEditorToDraft(event.currentTarget)
    +  }
    +
       const triggerAdapter: Unstable_TriggerAdapter | null =
         trigger?.kind === '@' ? at.adapter : trigger?.kind === '/' ? slash.adapter : null
     
    ```

    ```diff
    @@ -1208,8 +1213,17 @@ export function ChatBar({
             data-placeholder={placeholder}
             data-slot={RICH_INPUT_SLOT}
             onBlur={() => window.setTimeout(closeTrigger, 80)}
    -        onCompositionEnd={() => {
    +        onCompositionEnd={event => {
               composingRef.current = false
    +
    +          // The input events fired *during* composition were skipped (they
    +          // carried uncommitted preedit text), and Chromium does NOT reliably
    +          // emit a trailing input event after compositionend on Windows IMEs.
    +          // Without flushing here, committed multi-character IME input (e.g.
    +          // Chinese "你好", Japanese, Korean) never reaches composer state, so
    +          // `hasComposerPayload` stays false and the send button stays hidden
    +          // until an unrelated edit forces a sync (#39614).
    +          flushEditorToDraft(event.currentTarget)
             }}
             onCompositionStart={() => {
               composingRef.current = true
    ```
- **#39650: fix(desktop): show send button for committed IME text (Chinese/Japanese/Korean)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#40210`

## Fix status

- Merged to main in PR #40210 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-06`.
