# NousResearch/hermes-agent · Desktop composer IME input and Enter key bugs

## Open now

- 4 issues, 5 PRs open on GitHub.
- #19234: Scarf: Add option to send message with ⌘+Enter instead of Enter
- #38117: Bug: typing Korean leaves last syllable in input
- #39107: \[Bug\]: 我的hermes桌面版，聊天输入框，中文输入法输入的文字不管是中文还是英文按回车都无法发送，必须是英文输入法按回车键才能正常发送，不知道什么原因
- #39112: \[Bug\]: 我的hermes桌面版，聊天输入框，中文输入法输入的文字不管是中文还是英文按回车都无法发送，必须是英文输入法按回车键才能正常发送，不知道什么原因
- PR #37603: Fix desktop IME enter handling and gateway restart guards
- PR #39246: fix(tui): guard Enter submit against IME composition via timestamp
- PR #39612: \[codex\] fix(desktop): keep composer draft synced while typing
- PR #39660: fix(desktop): Enter key does not send message on Windows (false IME composing guard)
- PR #53668: fix(desktop): read live DOM text for Ctrl+Enter steer to avoid stale-state race

Multiple bugs and fixes related to IME composition (Chinese, Japanese, Korean) breaking the desktop composer's send button, Enter key behavior, and text syncing. Several fixes address preventing Enter from submitting during IME composition and keeping draft state accurate.

## TL;DR

- 63 member(s): 4 open issue(s), 37 closed issue(s), 5 open PR(s), 3 merged PR(s), 14 PR(s) closed without merging.
- A fix was merged to main in PR #39639. Verify it addresses your case.
- **Recommended:** read PR #39639 (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: `partially_resolved` · deciding PR(s): `pr-39639,pr-86760`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.04`: 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.

## Open related issues

- #19234: Scarf: Add option to send message with ⌘+Enter instead of Enter
- #38117: Bug: typing Korean leaves last syllable in input
- #39107: \[Bug\]: 我的hermes桌面版，聊天输入框，中文输入法输入的文字不管是中文还是英文按回车都无法发送，必须是英文输入法按回车键才能正常发送，不知道什么原因
- #39112: \[Bug\]: 我的hermes桌面版，聊天输入框，中文输入法输入的文字不管是中文还是英文按回车都无法发送，必须是英文输入法按回车键才能正常发送，不知道什么原因

## Closed / duplicate issues

- #37483: \[Bug\]: Desktop app: Enter key sends message during IME composition (Japanese, Chinese, Korean, etc.)
- #38821: bug: Desktop composer fails to send CJK/Chinese text without appending a trailing digit
- #38826: Hermes Desktop App: Chinese IME input broken — pressing Enter submits empty/incomplete text, requires Shift+Enter
- #38883: \[Bug\]: Desktop chat send button hidden after IME composition (Chinese/Japanese/Korean input)
- #38923: Desktop: Enter key doesn't submit on first press — requires Shift+Enter first
- #39025: Windows desktop: Enter does not submit Chinese IME text because rich composer draft state can stay stale
- #39089: \[Bug\]: Desk app message truncation — content cut to random length before reaching CLI
- #39195: \[Bug\]: Hermes Desktop TUI 中文输入法 IME 状态下按 Enter/Return 直接提交，而非确认候选词
- #39231: Desktop composer: send button not shown during CJK IME composition
- #39436: \[Bug\]: Desktop chat composer send button hidden until space is typed, messages sometimes truncated
- #39473: Bug: Desktop app input truncates text when typing (paste works fine)
- #39534: \[Bug\]: v0.15.1 96cd37e Hermes Desktop Windows cutted off chinese prompt in chat window
- #39538: \[Bug\]: Desktop composer drops or fails to send CJK IME text on Enter
- #39560: \[Bug\]: Desktop: trailing characters lost when sending message without punctuation
- #39620: Japanese IME composition does not work correctly in Hermes Desktop composer on Windows
- #39630: Desktop\] Enter key doesn't send message — must add trailing space
- #39636: Vietnamese Telex IME drops characters & tone marks in Hermes Desktop composer (macOS)
- #39649: Desktop composer Enter key does not send message on Windows
- #39651: \[Bug\]: IME composition causes send button to show as voice button + text truncation on submit
- #39786: \[Bug\]: Chinese input gets truncated (last 1-2 characters are swallowed) in the input field
- #39872: Desktop composer can submit stale/empty/truncated hand-typed text on immediate Enter
- #39908: Bug: Cannot send messages with pure Chinese characters in Desktop — requires punctuation to trigger send
- #40142: # Bug Report: Hermes Agent 桌面端输入截断
- #40145: \[Bug\]: Bug Report: Hermes Agent 桌面端输入截断
- #40146: Desktop app: Send button doesn't switch from voice button when typing Chinese (IME composition)
- #40226: Desktop app: Chinese IME input breaks composer - text not synced, Enter doesn't send
- #40311: \[Desktop\] Pure CJK/Chinese text in composer hides submit button (IME composition state not synced)
- #40633: Desktop Korean IME input drops the final character unless a trailing space is typed
- #42186: IME composition truncates submitted text — onCompositionEnd doesn't sync draft
- #42213: \[Desktop\]\[Windows\] 中文输入时按 Enter 发送消息，只发出前半句
- #44008: \[Desktop App\] IME/中文输入法导致输入框按钮状态异常：无法提交 & 文字被截断
- #44217: Composer IME composition bug - 中文输入法回车吞字
- #47931: \[Desktop\] IME composition Enter key incorrectly sends message instead of confirming English input
- #48337: Desktop: Chinese IME composition Enter prematurely submits message on macOS
- #51403: \[Bug\] Korean IME: trailing character stuck in input and double-Enter required to send
- #53659: Desktop composer Ctrl+Enter can miss freshly typed steer text
- #54144: \[BUG\] Chinese IME: Enter key triggers message submit during IME composition

## Prior & competing PRs

- **#38001: fix(desktop): ignore IME composition enter**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#38785: fix(desktop): ignore IME composition Enter shortcuts**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#39435: fix(desktop): commit IME-finalised text on compositionend so Enter submits**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#39614`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#39460: fix(desktop): use draftRef for hasComposerPayload to fix stale send-button and truncation**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `00694b935`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#39639: fix(desktop): send on Enter from live editor text, not stale composer state**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx`, `apps/desktop/src/app/chat/composer/index.tsx`
  - fix hunks:
  - `apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx`

    ```diff
    @@ -0,0 +1,189 @@
    +import { act, cleanup, fireEvent, render } from '@testing-library/react'
    +import { useRef, useState } from 'react'
    +import { afterEach, describe, expect, it, vi } 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 Enter wiring (handleEditorKeyDown's Enter
    +// branch + submitDraft), driven through REAL DOM keydown events on a
    +// contentEditable.
    +//
    +// Regression repro for #39630: pressing Enter right after typing (fast typing /
    +// IME) did nothing. The composer state (`draft` from useAuiState) and its
    +// derived `hasComposerPayload` lag the DOM by a render, so the keydown handler
    +// read empty state and either dropped the message, drained a queued prompt
    +// instead of sending, or (while busy) refused to queue. The fix reads the live
    +// editor text — `hasLivePayload` in the handler and a DOM re-sync at the top of
    +// submitDraft — so the just-typed text always wins.
    +//
    +// We model the race deterministically the way the IME repro does: mutate the
    +// editor's textContent WITHOUT firing an input event, so the React `draft`
    +// state stays stale while the DOM already holds the text.
    +function Harness({
    +  busy = false,
    +  queued = [],
    +  onSubmit,
    +  onQueue,
    +  onCancel,
    +  onDrain
    +}: {
    +  busy?: boolean
    +  queued?: readonly string[]
    +  onSubmit: (text: string) => void
    +  onQueue: (text: string) => void
    +  onCancel: () => void
    +  onDrain: () => void
    +}) {
    +  const editorRef = useRef<HTMLDivElement>(null)
    +  const draftRef = useRef('')
    +  // Mirrors `useAuiState(s => s.composer.text)` — updated only via setText, so
    +  // it lags the DOM until React re-renders (the source of the bug).
    +  const [draft, setDraft] = useState('')
    +  const attachments: unknown[] = []
    +
    +  const composerPlainText = (el: HTMLElement) => el.textContent ?? ''
    +
    +  const setText = (next: string) => {
    +    draftRef.current = next
    +    setDraft(next)
    +  }
    +
    +  const submitDraft = () => {
    +    const editor = editorRef.current
    +    if (editor) {
    +      const domText = composerPlainText(editor)
    +      if (domText !== draftRef.current) {
    +        draftRef.current = domText
    +        setDraft(domText)
    +      }
    +    }
    +
    +    const text = draftRef.current
    +    const payloadPresent = text.trim().length > 0 || attachments.length > 0
    +
    +    if (busy) {
    +      if (payloadPresent) {
    +        onQueue(text)
    +      } else {
    +        onCancel()
    +      }
    +    } else if (!payloadPresent && queued.length > 0) {
    +      onDrain()
    +    } else if (payloadPresent) {
    +      onSubmit(text)
    +    }
    +  }
    +
    +  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
    +    if (event.key === 'Enter' && !event.shiftKey) {
    +      event.preventDefault()
    +
    +      const editorText = editorRef.current ? composerPlainText(editorRef.current) : draftRef.current
    +      const hasLivePayload = editorText.trim().length > 0 || attachments.length > 0
    +
    +      if (!busy && !hasLivePayload && queued.length > 0) {
    +        onDrain()
    +
    +        return
    +      }
    +
    +      if (busy && !hasLivePayload) {
    +        return
    +      }
    +
    +      submitDraft()
    +    }
    +  }
    +
    +  // `draft` is read so the lint/compiler treats the stale-state mirror as live;
    +  // the assertions prove the handler never relies on it.
    +  void draft
    +
    +  return (
    +    <div
    +      contentEditable
    +      data-testid="editor"
    +      onInput={event => setText(composerPlainText(event.currentTarget))}
    +      onKeyDown={handleKeyDown}
    +      ref={editorRef}
    +      suppressContentEditableWarning
    +    />
    +  )
    +}
    +
    +describe('composer Enter submit — live DOM vs stale composer state (#39630)', () => {
    +  it('sends the just-typed text on Enter even when composer state has not synced', async () => {
    +    const onSubmit = vi.fn()
    +    const { getByTestId } = render(
    +      <Harness onCancel={vi.fn()} onDrain={vi.fn()} onQueue={vi.fn()} onSubmit={onSubmit} />
    +    )
    +    const editor = getByTestId('editor')
    +
    +    // Fast typing: the DOM has the text but NO input event fired, so `draft`
    +    // state is still empty (the exact stale-state race).
    +    await act(async () => {
    +      editor.textContent = 'hello world'
    +      fireEvent.keyDown(editor, { key: 'Enter' })
    +    })
    +
    +    expect(onSubmit).toHaveBeenCalledWith('hello world')
    +  })
    +
    +  it('queues a fast-typed message while busy instead of draining the queue or cancelling', async () => {
    +    const onQueue = vi.fn()
    +    const onDrain = vi.fn()
    +    const onCancel = vi.fn()
    +    const { getByTestId } = render(
    +      <Harness busy onCancel={onCancel} onDrain={onDrain} onQueue={onQueue} onSubmit={vi.fn()} queued={['queued-1']} />
    +    )
    +    const editor = getByTestId('editor')
    +
    +    await act(async () => {
    +      editor.textContent = 'urgent follow-up'
    +      fireEvent.keyDown(editor, { key: 'Enter' })
    +    })
    +
    +    expect(onQueue).toHaveBeenCalledWith('urgent follow-up')
    +    expect(onDrain).not.toHaveBeenCalled()
    +    expect(onCancel).not.toHaveBeenCalled()
    +  })
    +
    +  it('treats an empty Enter while busy as a no-op (never an accidental Stop)', async () => {
    +    const onCancel = vi.fn()
    +    const onSubmit = vi.fn()
    +    const onQueue = vi.fn()
    +    const { getByTestId } = render(
    +      <Harness busy onCancel={onCancel} onDrain={vi.fn()} onQueue={onQueue} onSubmit={onSubmit} />
    +    )
    +    const editor = getByTestId('editor')
    +
    +    await act(async () => {
    +      editor.textContent = ''
    +      fireEvent.keyDown(editor, { key: 'Enter' })
    +    })
    +
    +    expect(onCancel).not.toHaveBeenCalled()
    +    expect(onSubmit).not.toHaveBeenCalled()
    +    expect(onQueue).not.toHaveBeenCalled()
    +  })
    +
    +  it('drains the next queued prompt on Enter when idle with a truly empty editor', async () => {
    +    const onDrain = vi.fn()
    +    const onSubmit = vi.fn()
    +    const { getByTestId } = render(
    +      <Harness onCancel={vi.fn()} onDrain={onDrain} onQueue={vi.fn()} onSubmit={onSubmit} queued={['queued-1']} />
    +    )
    +    const editor = getByTestId('editor')
    +
    +    await act(async () => {
    +      editor.textContent = ''
    +      fireEvent.keyDown(editor, { key: 'Enter' })
    +    })
    +
    +    expect(onDrain).toHaveBeenCalledTimes(1)
    +    expect(onSubmit).not.toHaveBeenCalled()
    +  })
    +})
    ```
  - `apps/desktop/src/app/chat/composer/index.tsx`

    ```diff
    @@ -814,7 +814,16 @@ export function ChatBar({
         if (event.key === 'Enter' && !event.shiftKey) {
           event.preventDefault()
     
    -      if (!busy && !hasComposerPayload && queuedPrompts.length > 0) {
    +      // Decide from the DOM, not React state. `hasComposerPayload` is derived
    +      // from the AUI composer state, which lags the latest keystroke by a
    +      // render, so on fast typing / IME the just-typed text isn't in state yet.
    +      // Without the live read, a real message typed while prompts are queued
    +      // would drain the queue instead of sending. submitDraft() re-syncs and
    +      // sends the live editor text.
    +      const editorText = editorRef.current ? composerPlainText(editorRef.current) : draftRef.current
    +      const hasLivePayload = editorText.trim().length > 0 || attachments.length > 0
    +
    +      if (!busy && !hasLivePayload && queuedPrompts.length > 0) {
             void drainNextQueued()
     
             return
    ```

    ```diff
    @@ -822,7 +831,10 @@ export function ChatBar({
     
           // Empty Enter while busy is a no-op — interrupting is explicit (Stop/Esc),
           // never a stray Enter after sending. With a payload, submitDraft queues it.
    -      if (busy && !hasComposerPayload) {
    +      // Gate on the live DOM payload (not the render-lagged composer state) so a
    +      // message typed fast / via IME while busy still reaches submitDraft() and
    +      // gets queued instead of being mistaken for an empty Enter.
    +      if (busy && !hasLivePayload) {
             return
           }
     
    ```

    ```diff
    @@ -1212,6 +1224,26 @@ export function ChatBar({
       }, [activeQueueSessionKey, editingQueuedPrompt, queueEdit]) // eslint-disable-line react-hooks/exhaustive-deps
     
       const submitDraft = () => {
    +    // Source the text from the DOM editor, not React state. The AUI composer
    +    // state (`draft`) and the derived `hasComposerPayload` lag the DOM by a
    +    // render, so on fast typing or IME composition the final keystroke(s) may
    +    // not have synced yet — reading state here drops the message (Enter looks
    +    // like it does nothing; typing a trailing space only "fixes" it because the
    +    // extra input event forces a state sync). draftRef is updated on every
    +    // input event; refresh it from the editor once more to also cover an
    +    // in-flight keystroke that hasn't fired its input event yet.
    +    const editor = editorRef.current
    +    if (editor) {
    +      const domText = composerPlainText(editor)
    +      if (domText !== draftRef.current) {
    +        draftRef.current = domText
    +        aui.composer().setText(domText)
    +      }
    +    }
    +
    +    const text = draftRef.current
    +    const payloadPresent = text.trim().length > 0 || attachments.length > 0
    +
         if (queueEdit) {
           exitQueuedEdit('save')
         } else if (busy) {
    ```

    ```diff
    @@ -1222,12 +1254,12 @@ export function ChatBar({
           // busy guard for commands that genuinely need an idle session (skill
           // /send directives).  Queuing them would make every slash command wait
           // for the current turn to finish, which is how the TUI never behaves.
    -      if (!attachments.length && SLASH_COMMAND_RE.test(draft.trim())) {
    -        const submitted = draft
    +      if (!attachments.length && SLASH_COMMAND_RE.test(text.trim())) {
    +        const submitted = text
             triggerHaptic('submit')
             clearDraft()
             void onSubmit(submitted)
    -      } else if (hasComposerPayload) {
    +      } else if (payloadPresent) {
             queueCurrentDraft()
           } else {
             // Stop button (the only way to reach here while busy with an empty
    ```

    ```diff
    @@ -1235,10 +1267,10 @@ export function ChatBar({
             triggerHaptic('cancel')
             void Promise.resolve(onCancel())
           }
    -    } else if (!hasComposerPayload && queuedPrompts.length > 0) {
    +    } else if (!payloadPresent && queuedPrompts.length > 0) {
           void drainNextQueued()
    -    } else if (draft.trim() || attachments.length > 0) {
    -      const submitted = draft
    +    } else if (payloadPresent) {
    +      const submitted = text
           triggerHaptic('submit')
           resetBrowseState(sessionId)
           clearDraft()
    ```
- **#39938: fix(desktop): sync contentEditable state before submit (#39872)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `8b84d82227a3b637a26ba5f4f41bb08281e2ab84`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#40148: fix: send button not switching from voice button during IME composition**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `8e629b9f386d12b726bccb32e9d7b48402ea73ea`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#40657: fix(desktop): flush DOM to draft before submit to avoid IME character drop**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `8b84d82227a3b637a26ba5f4f41bb08281e2ab84`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#53660: fix(desktop): steer from live composer text**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#53668`, `#53659`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#86760: fix(desktop): consolidated IME composition handling — Enter guards, keyCode 229, stale-flag self-heal, keybind combos**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `apps/desktop/src/app/chat/composer/enter-stale-ime-flag.test.tsx`, `apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx`, `apps/desktop/src/app/chat/composer/index.tsx`, `apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx`, `apps/desktop/src/components/assistant-ui/thread/user-edit-composer.tsx`, `apps/desktop/src/components/assistant-ui/thread/user-message-edit.test.tsx`, `apps/desktop/src/components/onboarding/flow.tsx`, `apps/desktop/src/components/onboarding/index.tsx`, `apps/desktop/src/lib/keybinds/combo.test.ts`, `apps/desktop/src/lib/keybinds/combo.ts`, `contributors/emails/[redacted:email]`, `contributors/emails/[redacted:email]`, `contributors/emails/[redacted:email]`
- **#86761: fix(tui,dashboard): consolidated IME input fixes — Korean/Telex recomposition, return-burst commits, PTY composition forwarding**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `contributors/emails/[redacted:email]`, `contributors/emails/[redacted:email]`, `contributors/emails/[redacted:email]`, `contributors/emails/[redacted:email]`, `contributors/emails/[redacted:email]`, `ui-tui/packages/hermes-ink/src/ink/parse-keypress-drop-probe.test.ts`, `ui-tui/packages/hermes-ink/src/ink/parse-keypress-noregress.test.ts`, `ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts`, `ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts`, `ui-tui/src/__tests__/imeVietnameseTelex.test.tsx`, `ui-tui/src/__tests__/textInputReturnBurst.test.ts`, `ui-tui/src/__tests__/textInputSubmitClear.test.tsx`, `ui-tui/src/components/textInput.tsx`, `web/src/lib/pty-composition.test.ts`, `web/src/lib/pty-composition.ts`, `web/src/pages/ChatPage.tsx`
- **#37487: fix(desktop): prevent Enter from submitting during IME composition**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#86760`
- **#37603: Fix desktop IME enter handling and gateway restart guards**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `e551da6ddb39a712c93e6c48a16aa748e2c7bc95`, `245b95b09470bb3887943122a7d0de5bf20da055`
  - files: `apps/desktop/src/app/chat/composer/index.tsx`, `apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx`, `apps/desktop/src/components/assistant-ui/thread.tsx`, `apps/desktop/src/components/desktop-onboarding-overlay.tsx`, `apps/desktop/src/lib/ime.test.ts`, `apps/desktop/src/lib/ime.ts`, `hermes_cli/gateway.py`, `tests/hermes_cli/test_gateway_service.py`, `tests/tools/test_approval.py`, `tools/approval.py`
- **#38245: fix(tui): clear input after Korean IME submit**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#86761`
- **#38968: fix(desktop): read composer text from DOM on Enter submit to avoid stale state**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#39210: fix(desktop): prevent message truncation in Windows composer**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `8b84d82227a3b637a26ba5f4f41bb08281e2ab84`, `#39639`, `#39089`
- **#39230: fix(tui): guard Enter submit against IME composition via timestamp**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#39246: fix(tui): guard Enter submit against IME composition via timestamp**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#39695`
  - files: `ui-tui/src/components/textInput.tsx`
- **#39612: \[codex\] fix(desktop): keep composer draft synced while typing**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `apps/desktop/src/app/chat/composer/rich-editor.test.ts`, `apps/desktop/src/app/chat/composer/rich-editor.ts`
- **#39647: fix(desktop): reconcile composer draft on compositionend for IME reliability**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#39660: fix(desktop): Enter key does not send message on Windows (false IME composing guard)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `apps/desktop/src/app/chat/composer/index.tsx`, `apps/desktop/src/app/chat/composer/windows-stale-composing-guard.test.tsx`
- **#53668: fix(desktop): read live DOM text for Ctrl+Enter steer to avoid stale-state race**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `773a3703bfc1f8ff2f3aef40d7a565e7f4fe1404`
  - files: `apps/desktop/src/app/chat/composer/ctrl-enter-steer-dom-race.test.tsx`, `apps/desktop/src/app/chat/composer/index.tsx`

## Fix status

- Merged to main in PR #39639 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-08-15`.
- Subsystem: `apps/desktop`
