Skip to content

Confirm a composed character with Enter in the web app's text fields, instead of saving or moving on - #576

Open
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:ime-enter-web
Open

kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:ime-enter-web

Conversation

@kevin9327

@kevin9327 kevin9327 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What this changes

Japanese, Chinese and Korean are typed through an input method (IME), and Enter is how the character being built is confirmed. That press is still a keydown with key === "Enter":

  • Chromium marks it isComposing: true;
  • WebKit (Safari) sends it after compositionend, so isComposing is already false, but with keyCode === 229.

Three fields in the web app act on Enter without checking either one, so they act on text the person has not finished typing:

Field What the confirming Enter did
Coworker dialog, Name or Title edited in place (EditableTextItem in agent-dialog.tsx) Saved the value with its last character unconfirmed, and closed the editor
New coworker wizard, Enter in an identity input (create-agent-dialog.tsx) Moved on to the next step with the name still unconfirmed
Boundaries, the rule input (admin/boundaries.tsx) Saved the half-typed rule into the policy in force, e.g. contains(element.name, "送信")

The libraries under the app already skip this Enter:

  • prompt-area, which draws the chat composer, checks isComposing.
  • The questionnaire primitive checks it too. Its root onKeyDown starts with if (n.defaultPrevented || n.nativeEvent.isComposing || n.keyCode === 229 || …) return;. The wizard's own handler runs ahead of the primitive and calls preventDefault(), so the wizard also skipped the primitive's check.

Both checks now live in one helper, app/src/lib/composing.tsisComposing(event), and each of the three handlers asks it. An ordinary Enter behaves exactly as before. The Role textarea has no Enter handler and is unchanged.

This is the same fix as #507, which was closed only because it was in desktop/. These three are in the web app.

Where it runs

  • New state that outlives a request? None.
  • What happens on the second replica? Not applicable. This is keyboard handling in the browser.
  • Anything serialised? No.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. Unchanged. The policy save and the coworker update are the same requests, just no longer sent early.
  • New refusals and new failures each write a row. None are added.
  • Nothing new is trusted from the client that the server can resolve itself.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

New app/tests/composing-enter.test.tsx. It draws each real component: AgentDialog, CreateAgentDialog, and the Boundaries route's component inside a one-route router. It types with userEvent, then fires both confirming shapes at the field: { key: "Enter", isComposing: true }, then { key: "Enter", keyCode: 229 }. fetch records every write. After that it presses an ordinary Enter to pin that the field still acts, once.

On main (only the test file added):

(fail) a coworker's name is not saved by the Enter that confirms a composed character
- []
+ [ { "request": "PATCH /api/agents/expenses", "body": { "name": "経費", … } },
+   { "request": "PATCH /api/agents/expenses", "body": { "name": "経費", … } } ]

(fail) the new-coworker wizard does not move on from the Enter that confirms a composed character
TestingLibraryElementError: Unable to find an element with the text: Step 1 of 3.

(fail) a boundary rule is not saved by the Enter that confirms a composed character
- []
+ [ { "request": "PUT /api/computers/policy", "body": { "deny": [ "contains(element.name, \"送信\")" ], … } },
+   { "request": "PUT /api/computers/policy", "body": { "deny": [ "contains(element.name, \"送信\")" ], … } } ]

 0 pass
 3 fail

Every write came from a confirming Enter. Each shape triggers it on its own, which is why there are two.

With the fix:

  • bun test app/tests/composing-enter.test.tsx: 3 pass, 0 fail. Each case covers both shapes (both halves of isComposing) and an ordinary Enter.
  • bun test app/: 853 pass, 29 fail. On main it is 850 pass with the same 29 failures, all from this Windows machine: serve.test.ts port and websocket cases, Windows path separators, and a relative-time assertion reading a Korean locale.
  • bun run typecheck (app, server, worker): exit 0.
  • bunx biome check on the changed files: clean.

This PR adds its line at the top of ## Unreleased, the same place other open PRs add theirs, so the changelog may conflict. I'm happy to rebase.

🤖 Generated with Claude Code

… instead of saving or moving on

Japanese, Chinese and Korean are typed through an input method, and
Enter is how the character being built is confirmed. That press is
still a keydown with `key === "Enter"`: Chromium marks it
`isComposing`, and WebKit sends it after `compositionend` with key code
229. Three fields acted on it with the text still unconfirmed:

- a coworker's name or title, edited in place, was saved;
- the new-coworker wizard moved on to its next step. Its handler runs
  ahead of the questionnaire primitive and prevents default, which also
  skipped the primitive's own `isComposing || keyCode === 229` check;
- a boundary rule was saved into the policy in force.

The chat composer already skips this Enter through `prompt-area`. The
two checks now live in one helper, `isComposing`, and each of the three
handlers asks it. An ordinary Enter acts as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant