agent-Specialization/cli/src/ImeTextInput.tsx
JOJO 16473c824d chore: 合并多项独立 WIP 改动
- cli: 增加 layoutRefreshTick 与 IME 输入组件,处理 slash_menu 光标偏移
- config/auto_approval: 切到 opencode 中转端点
- modules/approval_agent: 调试 transcript 增加 trace / final_result 字段
- static/useLegacySocket: 重试 toast 显示具体错误信息

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 02:01:36 +08:00

135 lines
4.3 KiB
TypeScript

import React from 'react';
import { Text, useCursor, useInput, useStdout, type DOMElement } from 'ink';
type Props = {
value: string;
disabled?: boolean;
contentRef: React.RefObject<DOMElement | null>;
cursorYOffset?: number;
refreshTick?: number;
onChange: (value: string) => void;
onSubmit: (value: string) => void | Promise<void>;
};
export default function ImeTextInput({ value, disabled, contentRef, cursorYOffset = 0, refreshTick = 0, onChange, onSubmit }: Props) {
const { stdout } = useStdout();
const { setCursorPosition } = useCursor();
const cursorOffsetRef = React.useRef<number>(Array.from(value || '').length);
const [, forceRender] = React.useReducer((x: number) => x + 1, 0);
const pendingYOffsetRef = React.useRef<number>(0);
const seenRefreshTickRef = React.useRef<number>(refreshTick);
React.useEffect(() => {
const len = Array.from(value || '').length;
if (cursorOffsetRef.current > len) cursorOffsetRef.current = len;
}, [value]);
React.useEffect(() => {
if (refreshTick !== seenRefreshTickRef.current) {
seenRefreshTickRef.current = refreshTick;
pendingYOffsetRef.current = cursorYOffset;
forceRender();
}
}, [refreshTick, cursorYOffset]);
useInput((input, key) => {
if (disabled) return;
if (key.return) {
void onSubmit(value);
return;
}
if (key.leftArrow) {
const next = Math.max(0, cursorOffsetRef.current - 1);
if (next !== cursorOffsetRef.current) {
cursorOffsetRef.current = next;
forceRender();
}
return;
}
if (key.rightArrow) {
const len = Array.from(value || '').length;
const next = Math.min(len, cursorOffsetRef.current + 1);
if (next !== cursorOffsetRef.current) {
cursorOffsetRef.current = next;
forceRender();
}
return;
}
if (key.backspace || key.delete) {
if (cursorOffsetRef.current <= 0) return;
const chars = Array.from(value || '');
chars.splice(cursorOffsetRef.current - 1, 1);
onChange(chars.join(''));
cursorOffsetRef.current = Math.max(0, cursorOffsetRef.current - 1);
return;
}
if (!input || key.ctrl || key.meta) return;
const chars = Array.from(value || '');
chars.splice(cursorOffsetRef.current, 0, input);
onChange(chars.join(''));
cursorOffsetRef.current += Array.from(input).length;
}, { isActive: !disabled });
const origin = getAbsoluteOrigin(contentRef.current);
if (origin) {
const head = Array.from(value || '').slice(0, cursorOffsetRef.current).join('');
const terminalRows = Math.max(1, stdout.rows || 24);
const bottomCompensation = origin.y >= terminalRows - 2 ? 1 : 0;
const yOffsetThisFrame = pendingYOffsetRef.current;
pendingYOffsetRef.current = 0;
setCursorPosition({
x: origin.x + 2 + displayWidth(head),
y: origin.y + yOffsetThisFrame + bottomCompensation,
});
} else {
setCursorPosition(undefined);
}
return <Text>{value}</Text>;
}
function getAbsoluteOrigin(node: DOMElement | null): { x: number; y: number } | null {
if (!node?.yogaNode) return null;
let x = 0;
let y = 0;
let current: DOMElement | undefined = node;
while (current?.parentNode) {
if (!current.yogaNode) return null;
x += current.yogaNode.getComputedLeft();
y += current.yogaNode.getComputedTop();
current = current.parentNode;
}
return { x, y };
}
function displayWidth(text: string): number {
let width = 0;
for (const char of Array.from(text)) {
const code = char.codePointAt(0) || 0;
if (code === 0) continue;
if (code < 32 || (code >= 0x7f && code < 0xa0)) continue;
width += isWideCodePoint(code) ? 2 : 1;
}
return width;
}
function isWideCodePoint(code: number): boolean {
return (
code >= 0x1100 && (
code <= 0x115f ||
code === 0x2329 ||
code === 0x232a ||
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
(code >= 0xac00 && code <= 0xd7a3) ||
(code >= 0xf900 && code <= 0xfaff) ||
(code >= 0xfe10 && code <= 0xfe19) ||
(code >= 0xfe30 && code <= 0xfe6f) ||
(code >= 0xff00 && code <= 0xff60) ||
(code >= 0xffe0 && code <= 0xffe6) ||
(code >= 0x1f300 && code <= 0x1f64f) ||
(code >= 0x1f900 && code <= 0x1f9ff) ||
(code >= 0x20000 && code <= 0x3fffd)
)
);
}