diff --git a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx
new file mode 100644
index 00000000000..c03751ba3e3
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx
@@ -0,0 +1,65 @@
+// @vitest-environment happy-dom
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { endComposition } from '@/lib/ime-composition-event.test-support'
+import { SshPortForwardDialog } from './ssh-port-forward-dialog'
+
+const addPortForward = vi.fn(() => Promise.resolve())
+
+beforeEach(() => {
+ vi.useFakeTimers()
+ addPortForward.mockClear()
+ Object.defineProperty(window, 'api', {
+ configurable: true,
+ value: { ssh: { addPortForward } }
+ })
+})
+
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
+
+// happy-dom has no implicit submission, so a keydown left unprevented submits the form here.
+function pressEnterWithImplicitSubmit(field: HTMLElement): boolean {
+ const notPrevented = fireEvent.keyDown(field, { key: 'Enter', keyCode: 13 })
+ if (notPrevented) {
+ fireEvent.submit(field.closest('form')!)
+ }
+ return notPrevented
+}
+
+describe('SshPortForwardDialog IME confirmation', () => {
+ it.each([
+ { field: 'Remote Port', value: '8080', sent: { remotePort: 8080 } },
+ { field: 'Local Port', value: '18080', sent: { localPort: 18080 } },
+ { field: 'Remote Host', value: '開発機', sent: { remoteHost: '開発機' } },
+ { field: 'Label (optional)', value: '開発', sent: { label: '開発' } }
+ ])('keeps the confirm redispatch in $field from submitting', async ({ field, value, sent }) => {
+ render(
+
+ )
+ const input = screen.getByLabelText(field)
+
+ fireEvent.compositionStart(input)
+ fireEvent.keyDown(input, { key: 'Process', keyCode: 229, isComposing: true })
+ fireEvent.change(input, { target: { value } })
+ endComposition(input, value)
+ fireEvent.keyUp(input, { key: 'Process', keyCode: 229 })
+
+ await act(async () => {
+ expect(pressEnterWithImplicitSubmit(input)).toBe(false)
+ })
+ expect(addPortForward).not.toHaveBeenCalled()
+
+ await act(async () => {
+ expect(pressEnterWithImplicitSubmit(input)).toBe(true)
+ })
+ expect(addPortForward).toHaveBeenCalledOnce()
+ expect(addPortForward).toHaveBeenCalledWith(expect.objectContaining(sent))
+ })
+})
diff --git a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx
index bbd7108cc2f..24a44768cf4 100644
--- a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx
+++ b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx
@@ -9,6 +9,7 @@ import {
import { Button } from '@/components/ui/button'
import type { PortForwardEntry } from '../../../../shared/ssh-types'
import { translate } from '@/i18n/i18n'
+import { ImeInput } from '@/lib/ime-text-field'
// Remap privileged ports to avoid EACCES when binding locally without root.
function safeLocalPort(remotePort: number): number {
@@ -218,7 +219,7 @@ function PortForwardForm({
{translate('auto.components.right.sidebar.PortsPanel.9e5a4118b0', 'Remote Port')}
-
{translate('auto.components.right.sidebar.PortsPanel.b950b1948b', 'Local Port')}
-
{translate('auto.components.right.sidebar.PortsPanel.a3721a50b0', 'Remote Host')}
- setRemoteHost(e.target.value)}
@@ -276,7 +277,7 @@ function PortForwardForm({
{translate('auto.components.right.sidebar.PortsPanel.8dfed0a15c', 'Label (optional)')}
- setLabel(e.target.value)}