Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawn } from 'child_process'
import { spawn, type StdioOptions } from 'child_process'
type ExecFileOptions = {
input?: string
timeout?: number
Expand Down Expand Up @@ -32,9 +32,7 @@ export function execFileNoThrow(
// doesn't inherit those pipe FDs — prevents handle leaks that can
// keep the parent process alive. No output data is collected in
// this mode; both stdout and stderr will be empty strings.
const stdioConfig = options.resolveOnExit
? ['pipe', 'ignore', 'ignore'] as const
: 'pipe' as const
const stdioConfig: StdioOptions = options.resolveOnExit ? ['pipe', 'ignore', 'ignore'] : 'pipe'

const child = spawn(file, args, {
cwd: options.useCwd ? process.cwd() : undefined,
Expand Down
25 changes: 25 additions & 0 deletions ui-tui/src/__tests__/textInputReturnBurst.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'

import { valueForReturnSubmit } from '../components/textInput.js'

describe('valueForReturnSubmit', () => {
it('includes printable input that arrives in the same keypress as return', () => {
expect(valueForReturnSubmit('为什么打字上屏,', 8, '会丢失内容')).toEqual({
cursor: 13,
value: '为什么打字上屏,会丢失内容'
})
})

it('keeps IME commit text when it arrives in the same burst as return', () => {
expect(valueForReturnSubmit('为什么打字上屏,', 8, '会丢失内容\r')).toEqual({
cursor: 13,
value: '为什么打字上屏,会丢失内容'
})
})

it('leaves the draft unchanged when return carries no printable input', () => {
expect(valueForReturnSubmit('hello', 5, '')).toEqual({ cursor: 5, value: 'hello' })
expect(valueForReturnSubmit('hello', 5, '\r')).toEqual({ cursor: 5, value: 'hello' })
expect(valueForReturnSubmit('hello', 5, '\n')).toEqual({ cursor: 5, value: 'hello' })
})
})
33 changes: 31 additions & 2 deletions ui-tui/src/components/textInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@ export function applyPrintableInsert(

export const shouldRouteMultiCharInputAsPaste = (text: string): boolean => text.includes('\n')

export function valueForReturnSubmit(
value: string,
cursor: number,
input: string,
range?: { end: number; start: number } | null
): TextInsertResult {
const pending = input.replace(BRACKET_PASTE, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')

if (!pending) {
return { cursor, value }
}

// Browser/xterm IME commits can arrive as one burst immediately followed by
// Return (for example "会丢失内容\r"). The Return keypath is already about to
// submit, but the committed text has not passed through the ordinary
// printable-input branch yet. Preserve the printable prefix before the first
// newline so the visible, just-committed IME text is part of the submitted
// prompt instead of being silently dropped.
const [beforeReturn] = pending.split('\n', 1)

if (!beforeReturn) {
return { cursor, value }
}

return applyPrintableInsert(value, cursor, beforeReturn, range) ?? { cursor, value }
}

export function shouldPreserveCtrlJNewline(env: MinimalEnv = process.env): boolean {
if (env.WT_SESSION) {
return true
Expand Down Expand Up @@ -968,13 +995,15 @@ export function TextInput({
if (k.return) {
flushKeyBurst()

const range = selRange()
const pending = valueForReturnSubmit(vRef.current, curRef.current, inp, range)
const sequence = (event.keypress as { sequence?: string }).sequence
const preserveBareLineFeed = shouldPreserveCtrlJNewline() && sequence === '\n'

if (k.shift || k.ctrl || preserveBareLineFeed || (isMac ? isActionMod(k) : k.meta)) {
commit(ins(vRef.current, curRef.current, '\n'), curRef.current + 1)
commit(ins(pending.value, pending.cursor, '\n'), pending.cursor + 1)
} else {
cbSubmit.current?.(vRef.current)
cbSubmit.current?.(pending.value)
}

return
Expand Down