@wukonglabs/wukong 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/help.js +1 -1
- package/dist/cli/session-commands.d.ts.map +1 -1
- package/dist/cli/session-commands.js +4 -3
- package/dist/cli/session-commands.js.map +1 -1
- package/dist/index.js +73 -12
- package/dist/index.js.map +1 -1
- package/dist/tui/activity-verb.d.ts +29 -0
- package/dist/tui/activity-verb.d.ts.map +1 -0
- package/dist/tui/activity-verb.js +81 -0
- package/dist/tui/activity-verb.js.map +1 -0
- package/dist/tui/app.d.ts +29 -28
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +259 -132
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/diff-count.d.ts +14 -0
- package/dist/tui/diff-count.d.ts.map +1 -0
- package/dist/tui/diff-count.js +21 -0
- package/dist/tui/diff-count.js.map +1 -0
- package/dist/tui/hint-bar.d.ts +12 -0
- package/dist/tui/hint-bar.d.ts.map +1 -0
- package/dist/tui/hint-bar.js +14 -0
- package/dist/tui/hint-bar.js.map +1 -0
- package/dist/tui/history-entry.d.ts +11 -0
- package/dist/tui/history-entry.d.ts.map +1 -0
- package/dist/tui/history-entry.js +35 -0
- package/dist/tui/history-entry.js.map +1 -0
- package/dist/tui/history-item.d.ts +40 -0
- package/dist/tui/history-item.d.ts.map +1 -0
- package/dist/tui/history-item.js +8 -0
- package/dist/tui/history-item.js.map +1 -0
- package/dist/tui/keys-help.d.ts +5 -0
- package/dist/tui/keys-help.d.ts.map +1 -0
- package/dist/tui/keys-help.js +36 -0
- package/dist/tui/keys-help.js.map +1 -0
- package/dist/tui/live-block.d.ts +54 -0
- package/dist/tui/live-block.d.ts.map +1 -0
- package/dist/tui/live-block.js +72 -0
- package/dist/tui/live-block.js.map +1 -0
- package/dist/tui/main-area.d.ts +40 -0
- package/dist/tui/main-area.d.ts.map +1 -0
- package/dist/tui/main-area.js +9 -0
- package/dist/tui/main-area.js.map +1 -0
- package/dist/tui/markdown.d.ts +14 -0
- package/dist/tui/markdown.d.ts.map +1 -0
- package/dist/tui/markdown.js +134 -0
- package/dist/tui/markdown.js.map +1 -0
- package/dist/tui/notice-panel.d.ts +20 -0
- package/dist/tui/notice-panel.d.ts.map +1 -0
- package/dist/tui/notice-panel.js +19 -0
- package/dist/tui/notice-panel.js.map +1 -0
- package/dist/tui/response-panel.d.ts +12 -16
- package/dist/tui/response-panel.d.ts.map +1 -1
- package/dist/tui/response-panel.js +8 -11
- package/dist/tui/response-panel.js.map +1 -1
- package/dist/tui/run-ink.d.ts +12 -0
- package/dist/tui/run-ink.d.ts.map +1 -1
- package/dist/tui/run-ink.js +2 -1
- package/dist/tui/run-ink.js.map +1 -1
- package/dist/tui/slash/menu.d.ts +16 -0
- package/dist/tui/slash/menu.d.ts.map +1 -0
- package/dist/tui/slash/menu.js +11 -0
- package/dist/tui/slash/menu.js.map +1 -0
- package/dist/tui/slash/parse.d.ts +21 -0
- package/dist/tui/slash/parse.d.ts.map +1 -0
- package/dist/tui/slash/parse.js +32 -0
- package/dist/tui/slash/parse.js.map +1 -0
- package/dist/tui/slash/registry.d.ts +20 -0
- package/dist/tui/slash/registry.d.ts.map +1 -0
- package/dist/tui/slash/registry.js +28 -0
- package/dist/tui/slash/registry.js.map +1 -0
- package/dist/tui/spinner.d.ts +11 -0
- package/dist/tui/spinner.d.ts.map +1 -0
- package/dist/tui/spinner.js +21 -0
- package/dist/tui/spinner.js.map +1 -0
- package/dist/tui/status-bar.d.ts +42 -0
- package/dist/tui/status-bar.d.ts.map +1 -0
- package/dist/tui/status-bar.js +32 -0
- package/dist/tui/status-bar.js.map +1 -0
- package/dist/tui/tail-visual.d.ts +11 -0
- package/dist/tui/tail-visual.d.ts.map +1 -0
- package/dist/tui/tail-visual.js +30 -0
- package/dist/tui/tail-visual.js.map +1 -0
- package/dist/tui/text-input.d.ts +20 -22
- package/dist/tui/text-input.d.ts.map +1 -1
- package/dist/tui/text-input.js +211 -81
- package/dist/tui/text-input.js.map +1 -1
- package/dist/tui/theme/context.d.ts +16 -0
- package/dist/tui/theme/context.d.ts.map +1 -0
- package/dist/tui/theme/context.js +28 -0
- package/dist/tui/theme/context.js.map +1 -0
- package/dist/tui/theme/persist.d.ts +11 -0
- package/dist/tui/theme/persist.d.ts.map +1 -0
- package/dist/tui/theme/persist.js +41 -0
- package/dist/tui/theme/persist.js.map +1 -0
- package/dist/tui/theme/theme.d.ts +26 -0
- package/dist/tui/theme/theme.d.ts.map +1 -0
- package/dist/tui/theme/theme.js +51 -0
- package/dist/tui/theme/theme.js.map +1 -0
- package/dist/tui/thinking-panel.d.ts +11 -20
- package/dist/tui/thinking-panel.d.ts.map +1 -1
- package/dist/tui/thinking-panel.js +29 -7
- package/dist/tui/thinking-panel.js.map +1 -1
- package/dist/tui/tool-cards.d.ts +5 -17
- package/dist/tui/tool-cards.d.ts.map +1 -1
- package/dist/tui/tool-cards.js +4 -5
- package/dist/tui/tool-cards.js.map +1 -1
- package/dist/tui/tool-summary.d.ts +2 -0
- package/dist/tui/tool-summary.d.ts.map +1 -0
- package/dist/tui/tool-summary.js +93 -0
- package/dist/tui/tool-summary.js.map +1 -0
- package/dist/tui/use-elapsed.d.ts +2 -0
- package/dist/tui/use-elapsed.d.ts.map +1 -0
- package/dist/tui/use-elapsed.js +22 -0
- package/dist/tui/use-elapsed.js.map +1 -0
- package/dist/tui/use-plan-state.d.ts +20 -1
- package/dist/tui/use-plan-state.d.ts.map +1 -1
- package/dist/tui/use-plan-state.js +52 -46
- package/dist/tui/use-plan-state.js.map +1 -1
- package/dist/tui/use-terminal-width.d.ts +2 -0
- package/dist/tui/use-terminal-width.d.ts.map +1 -0
- package/dist/tui/use-terminal-width.js +23 -0
- package/dist/tui/use-terminal-width.js.map +1 -0
- package/dist/tui/use-viewport.d.ts +15 -0
- package/dist/tui/use-viewport.d.ts.map +1 -0
- package/dist/tui/use-viewport.js +37 -0
- package/dist/tui/use-viewport.js.map +1 -0
- package/dist/tui/welcome.d.ts +29 -0
- package/dist/tui/welcome.d.ts.map +1 -0
- package/dist/tui/welcome.js +41 -0
- package/dist/tui/welcome.js.map +1 -0
- package/package.json +3 -2
- package/dist/tui/header.d.ts +0 -24
- package/dist/tui/header.d.ts.map +0 -1
- package/dist/tui/header.js +0 -26
- package/dist/tui/header.js.map +0 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
import { Spinner } from './spinner.js';
|
|
4
|
+
import { useTheme } from './theme/context.js';
|
|
5
|
+
import { useTerminalWidth } from './use-terminal-width.js';
|
|
6
|
+
export function StatusBar({ model, workspaceRoot, busy, lastDurationMs, currentStep = 0, maxSteps, cacheHitRate, runStartAt, aborting, noticeCount = 0, devMode = process.env.WUKONG_DEV === '1', sessionUuidShort, elapsedSec, }) {
|
|
7
|
+
const { theme } = useTheme();
|
|
8
|
+
const cols = useTerminalWidth();
|
|
9
|
+
const elapsedStr = (elapsedSec ?? 0) > 0 ? `${elapsedSec.toFixed(1)}s` : '—';
|
|
10
|
+
const lastStr = lastDurationMs !== undefined ? `${(lastDurationMs / 1000).toFixed(1)}s` : null;
|
|
11
|
+
const shortRoot = shortenPath(workspaceRoot, 36);
|
|
12
|
+
const showCwd = cols >= 60;
|
|
13
|
+
const stepStr = currentStep > 0
|
|
14
|
+
? maxSteps ? `🎯 ${currentStep}/${maxSteps}` : `🎯 ${currentStep}`
|
|
15
|
+
: '🎯 —';
|
|
16
|
+
const cacheStr = cacheHitRate !== undefined ? `💾 ${(cacheHitRate * 100).toFixed(0)}%` : null;
|
|
17
|
+
const statusText = aborting ? '⛔ 中断中…' : busy ? '思考中…' : '✓ ready';
|
|
18
|
+
const statusColor = aborting ? 'red' : busy ? theme.toolRunning : theme.planDone;
|
|
19
|
+
return (_jsxs(Box, { paddingX: 0, children: [_jsx(Text, { color: theme.status, bold: true, children: "wukong" }), _jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: theme.assistant, children: model }), showCwd && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: theme.dim, children: shortRoot })] })), busy && _jsx(Spinner, { color: theme.toolRunning }), _jsx(Text, { color: statusColor, children: statusText }), _jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsxs(Text, { color: theme.dim, children: ["\u23F1 ", busy ? elapsedStr : lastStr ?? '—'] }), noticeCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsxs(Text, { color: "yellow", children: ["\u26A0 ", noticeCount] })] })), devMode && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: theme.dim, children: stepStr }), cacheStr && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsx(Text, { color: theme.planDone, children: cacheStr })] })), sessionUuidShort && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.dim, children: " \u00B7 " }), _jsxs(Text, { color: theme.thinking, children: ["\uD83D\uDCC2 ", sessionUuidShort] })] }))] }))] }));
|
|
20
|
+
}
|
|
21
|
+
function shortenPath(p, maxLen) {
|
|
22
|
+
if (p.length <= maxLen)
|
|
23
|
+
return p;
|
|
24
|
+
const parts = p.split('/');
|
|
25
|
+
if (parts.length <= 3)
|
|
26
|
+
return p;
|
|
27
|
+
const tail = parts.slice(-2).join('/');
|
|
28
|
+
const head = parts[0] || '/';
|
|
29
|
+
const shortened = `${head}/…/${tail}`;
|
|
30
|
+
return shortened.length <= maxLen ? shortened : `…/${tail}`;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=status-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-bar.js","sourceRoot":"","sources":["../../src/tui/status-bar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按"视觉行数"取文本末尾 N 行(纯函数,便于单测)。
|
|
3
|
+
*
|
|
4
|
+
* M3:流式响应恢复逐字展示时,动态区只显示末 N 行。但一行可能超过终端宽度
|
|
5
|
+
* 被 Ink 折成多行,所以这里先按 cols 把每个物理段落拆成若干视觉行,
|
|
6
|
+
* 再取末尾 maxLines 个视觉行返回。
|
|
7
|
+
*
|
|
8
|
+
* cols <= 0 时退化为按物理行取末尾(测试 / 未知宽度时安全)。
|
|
9
|
+
*/
|
|
10
|
+
export declare function tailVisualLines(text: string, maxLines: number, cols: number): string;
|
|
11
|
+
//# sourceMappingURL=tail-visual.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tail-visual.d.ts","sourceRoot":"","sources":["../../src/tui/tail-visual.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBpF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按"视觉行数"取文本末尾 N 行(纯函数,便于单测)。
|
|
3
|
+
*
|
|
4
|
+
* M3:流式响应恢复逐字展示时,动态区只显示末 N 行。但一行可能超过终端宽度
|
|
5
|
+
* 被 Ink 折成多行,所以这里先按 cols 把每个物理段落拆成若干视觉行,
|
|
6
|
+
* 再取末尾 maxLines 个视觉行返回。
|
|
7
|
+
*
|
|
8
|
+
* cols <= 0 时退化为按物理行取末尾(测试 / 未知宽度时安全)。
|
|
9
|
+
*/
|
|
10
|
+
export function tailVisualLines(text, maxLines, cols) {
|
|
11
|
+
if (maxLines <= 0)
|
|
12
|
+
return '';
|
|
13
|
+
if (cols <= 0) {
|
|
14
|
+
const lines = text.split('\n');
|
|
15
|
+
return lines.slice(Math.max(0, lines.length - maxLines)).join('\n');
|
|
16
|
+
}
|
|
17
|
+
const paras = text.split('\n');
|
|
18
|
+
const visualLines = [];
|
|
19
|
+
for (const p of paras) {
|
|
20
|
+
if (p.length === 0) {
|
|
21
|
+
visualLines.push('');
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < p.length; i += cols) {
|
|
25
|
+
visualLines.push(p.slice(i, i + cols));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return visualLines.slice(Math.max(0, visualLines.length - maxLines)).join('\n');
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=tail-visual.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tail-visual.js","sourceRoot":"","sources":["../../src/tui/tail-visual.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB,EAAE,IAAY;IAC1E,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7B,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QACd,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC/B,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnB,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;YACxC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClF,CAAC"}
|
package/dist/tui/text-input.d.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 多行文本输入组件(自研,Ink 自带 ink-text-input 不支持多行 + Ctrl+R)。
|
|
2
|
+
* 多行文本输入组件(自研,Ink 自带 ink-text-input 不支持多行 + Ctrl+R + slash 菜单)。
|
|
3
3
|
*
|
|
4
4
|
* ## 交互
|
|
5
5
|
*
|
|
6
6
|
* - **Enter**:提交(若 `multiline` 为 false,任何 Enter 都提交)
|
|
7
7
|
* - **Shift+Enter / Alt+Enter**:换行(仅 multiline=true 时)
|
|
8
8
|
* - **Ctrl+J**:换行(终端友好,Ctrl+J = LF)
|
|
9
|
-
* - **↑ / ↓**(单行时):浏览历史
|
|
9
|
+
* - **↑ / ↓**(单行时):浏览历史(菜单未激活时)
|
|
10
10
|
* - **↑ / ↓**(多行时):移动光标到上/下一行
|
|
11
|
-
* - **Ctrl+R**:进入反查模式;再按跳下一条匹配;Enter 提交当前匹配;Esc
|
|
11
|
+
* - **Ctrl+R**:进入反查模式;再按跳下一条匹配;Enter 提交当前匹配;Esc/Ctrl+G 取消
|
|
12
12
|
* - **Ctrl+C**:清理后退出(由 App 层接管 graceful shutdown)
|
|
13
13
|
* - **Ctrl+L**:清屏(Ink 原生 clear)
|
|
14
|
+
* - **Home / End**(或 Ctrl+A / Ctrl+E):行首 / 行尾
|
|
15
|
+
* - **Alt+← / →**(或 Ctrl+← / →):按词跳转
|
|
16
|
+
* - **Ctrl+W**:删除光标前一词;**Ctrl+K / U**:删到行尾 / 行首;**Ctrl+D**:删光标后字符(空输入退出)
|
|
17
|
+
* - 以 / 开头:唤起 Slash 命令菜单(↑↓ 选择、Enter 确认、Esc 取消)
|
|
14
18
|
*
|
|
15
|
-
* ##
|
|
19
|
+
* ## 插入模型(ref-based,M11c)
|
|
16
20
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* ## 可访问性
|
|
22
|
-
*
|
|
23
|
-
* `multiline` 模式下不依赖光标可视化(终端光标控制在不同终端差异大),
|
|
24
|
-
* 用 `▋` 字符标记光标位置;`NO_COLOR` 时仍可见。
|
|
21
|
+
* 旧实现用 useCallback 闭包持有 value/cursor,多字符输入(粘贴 / IME 选词 / 快速连击)
|
|
22
|
+
* 因闭包未 flush 会丢字(实测 "你好" 只留 "好")。M11c 改用 valueRef/cursorRef,
|
|
23
|
+
* 所有可打印字符(单/多)统一走 insertText,彻底消除闭包陷阱,IME 候选可见、可退格改字。
|
|
25
24
|
*/
|
|
26
25
|
import React from 'react';
|
|
27
26
|
import { useInputHistory } from './use-input-history.js';
|
|
@@ -47,15 +46,14 @@ export interface TextInputProps {
|
|
|
47
46
|
* Ink raw mode 拦截 Ctrl+C,不走 process SIGINT,故需显式回调。
|
|
48
47
|
*/
|
|
49
48
|
onAbort?: () => void;
|
|
49
|
+
/** Ctrl+D 空输入时退出(对齐 readline) */
|
|
50
|
+
onExit?: () => void;
|
|
51
|
+
/** 选中某个 slash 命令时回调(name + 可选参数) */
|
|
52
|
+
onSlash?: (name: string, arg?: string) => void;
|
|
50
53
|
}
|
|
51
|
-
export declare function TextInput({ value, onChange, onSubmit, multiline, history, prompt, placeholder, disabled, onAbort, }: TextInputProps): React.ReactElement;
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* 视为粘贴的特征:含换行(多行粘贴)或长度 >= 5(普通打字不可能一次到 5 字符)。
|
|
57
|
-
* 短输入(2-4 字符无换行)可能是 IME 选词 / 快速连击,都按"整体插入"处理
|
|
58
|
-
* (不再逐字符,以避免 useCallback 闭包陷阱导致多字符只保留最后一个)。
|
|
59
|
-
*/
|
|
60
|
-
export declare function isPasteInput(input: string): boolean;
|
|
54
|
+
export declare function TextInput({ value, onChange, onSubmit, multiline, history, prompt, placeholder, disabled, onAbort, onExit, onSlash, }: TextInputProps): React.ReactElement;
|
|
55
|
+
/** 找 pos 之前最近的 word 边界(跳过连续 whitespace + 非 word 字符) */
|
|
56
|
+
export declare function prevWordBoundary(value: string, pos: number): number;
|
|
57
|
+
/** 找 pos 之后最近的 word 边界 */
|
|
58
|
+
export declare function nextWordBoundary(value: string, pos: number): number;
|
|
61
59
|
//# sourceMappingURL=text-input.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/tui/text-input.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/tui/text-input.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAExE,OAAO,EACL,eAAe,EAGhB,MAAM,wBAAwB,CAAC;AAQhC,MAAM,WAAW,cAAc;IAC7B,cAAc;IACd,KAAK,EAAE,MAAM,CAAC;IACd,YAAY;IACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,kBAAkB;IAClB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sBAAsB;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB;IACpB,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC7C,qBAAqB;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD;AAeD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAgB,EAChB,OAAO,EACP,MAAa,EACb,WAAgB,EAChB,QAAgB,EAChB,OAAO,EACP,MAAM,EACN,OAAO,GACR,EAAE,cAAc,GAAG,KAAK,CAAC,YAAY,CA2YrC;AAyCD,uDAAuD;AACvD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnE;AAED,0BAA0B;AAC1B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnE"}
|
package/dist/tui/text-input.js
CHANGED
|
@@ -1,41 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* 多行文本输入组件(自研,Ink 自带 ink-text-input 不支持多行 + Ctrl+R)。
|
|
3
|
+
* 多行文本输入组件(自研,Ink 自带 ink-text-input 不支持多行 + Ctrl+R + slash 菜单)。
|
|
4
4
|
*
|
|
5
5
|
* ## 交互
|
|
6
6
|
*
|
|
7
7
|
* - **Enter**:提交(若 `multiline` 为 false,任何 Enter 都提交)
|
|
8
8
|
* - **Shift+Enter / Alt+Enter**:换行(仅 multiline=true 时)
|
|
9
9
|
* - **Ctrl+J**:换行(终端友好,Ctrl+J = LF)
|
|
10
|
-
* - **↑ / ↓**(单行时):浏览历史
|
|
10
|
+
* - **↑ / ↓**(单行时):浏览历史(菜单未激活时)
|
|
11
11
|
* - **↑ / ↓**(多行时):移动光标到上/下一行
|
|
12
|
-
* - **Ctrl+R**:进入反查模式;再按跳下一条匹配;Enter 提交当前匹配;Esc
|
|
12
|
+
* - **Ctrl+R**:进入反查模式;再按跳下一条匹配;Enter 提交当前匹配;Esc/Ctrl+G 取消
|
|
13
13
|
* - **Ctrl+C**:清理后退出(由 App 层接管 graceful shutdown)
|
|
14
14
|
* - **Ctrl+L**:清屏(Ink 原生 clear)
|
|
15
|
+
* - **Home / End**(或 Ctrl+A / Ctrl+E):行首 / 行尾
|
|
16
|
+
* - **Alt+← / →**(或 Ctrl+← / →):按词跳转
|
|
17
|
+
* - **Ctrl+W**:删除光标前一词;**Ctrl+K / U**:删到行尾 / 行首;**Ctrl+D**:删光标后字符(空输入退出)
|
|
18
|
+
* - 以 / 开头:唤起 Slash 命令菜单(↑↓ 选择、Enter 确认、Esc 取消)
|
|
15
19
|
*
|
|
16
|
-
* ##
|
|
20
|
+
* ## 插入模型(ref-based,M11c)
|
|
17
21
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* ## 可访问性
|
|
23
|
-
*
|
|
24
|
-
* `multiline` 模式下不依赖光标可视化(终端光标控制在不同终端差异大),
|
|
25
|
-
* 用 `▋` 字符标记光标位置;`NO_COLOR` 时仍可见。
|
|
22
|
+
* 旧实现用 useCallback 闭包持有 value/cursor,多字符输入(粘贴 / IME 选词 / 快速连击)
|
|
23
|
+
* 因闭包未 flush 会丢字(实测 "你好" 只留 "好")。M11c 改用 valueRef/cursorRef,
|
|
24
|
+
* 所有可打印字符(单/多)统一走 insertText,彻底消除闭包陷阱,IME 候选可见、可退格改字。
|
|
26
25
|
*/
|
|
27
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
28
|
-
import { Text, useInput, useStdin } from 'ink';
|
|
26
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
27
|
+
import { Text, useInput, useStdin, useStdout } from 'ink';
|
|
29
28
|
import { reverseSearch, reverseSearchNext, } from './use-input-history.js';
|
|
29
|
+
import { parseSlashCommand } from './slash/parse.js';
|
|
30
|
+
import { filterSlashCommands } from './slash/registry.js';
|
|
31
|
+
import { SlashMenu } from './slash/menu.js';
|
|
32
|
+
/** 输入长度硬上限(M11c):粘贴大文件也够,防止异常灌入撑爆 Ink */
|
|
33
|
+
const MAX_INPUT_LENGTH = 100_000;
|
|
30
34
|
const INITIAL_RS = {
|
|
31
35
|
active: false,
|
|
32
36
|
query: '',
|
|
33
37
|
matchIndex: -1,
|
|
34
38
|
};
|
|
35
|
-
export function TextInput({ value, onChange, onSubmit, multiline = true, history, prompt = '> ', placeholder = '', disabled = false, onAbort, }) {
|
|
39
|
+
export function TextInput({ value, onChange, onSubmit, multiline = true, history, prompt = '> ', placeholder = '', disabled = false, onAbort, onExit, onSlash, }) {
|
|
36
40
|
const [cursor, setCursor] = useState(value.length);
|
|
37
41
|
const [rs, setRs] = useState(INITIAL_RS);
|
|
42
|
+
const [slashDismissed, setSlashDismissed] = useState(false);
|
|
43
|
+
const [slashHighlight, setSlashHighlight] = useState(0);
|
|
38
44
|
const { stdin, setRawMode } = useStdin();
|
|
45
|
+
const { stdout } = useStdout();
|
|
39
46
|
// stdin 原始模式(捕获 Ctrl+C / Ctrl+R 等控制字符)
|
|
40
47
|
useEffect(() => {
|
|
41
48
|
if (stdin && typeof setRawMode === 'function') {
|
|
@@ -44,15 +51,35 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
44
51
|
}
|
|
45
52
|
return;
|
|
46
53
|
}, [stdin, setRawMode]);
|
|
47
|
-
//
|
|
54
|
+
// slash 菜单取消后,只要输入不再以 / 开头就恢复激活
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!value.startsWith('/'))
|
|
57
|
+
setSlashDismissed(false);
|
|
58
|
+
}, [value]);
|
|
59
|
+
// ── ref-based 插入(M11c)──
|
|
60
|
+
const valueRef = useRef(value);
|
|
61
|
+
const cursorRef = useRef(cursor);
|
|
62
|
+
useEffect(() => { valueRef.current = value; }, [value]);
|
|
63
|
+
useEffect(() => { cursorRef.current = cursor; }, [cursor]);
|
|
48
64
|
const insertText = useCallback((text) => {
|
|
49
|
-
const
|
|
65
|
+
const v = valueRef.current;
|
|
66
|
+
const c = cursorRef.current;
|
|
67
|
+
let t = text;
|
|
68
|
+
if (v.length + t.length > MAX_INPUT_LENGTH) {
|
|
69
|
+
const room = MAX_INPUT_LENGTH - v.length;
|
|
70
|
+
if (room <= 0)
|
|
71
|
+
return;
|
|
72
|
+
t = t.slice(0, room);
|
|
73
|
+
}
|
|
74
|
+
const newValue = v.slice(0, c) + t + v.slice(c);
|
|
75
|
+
valueRef.current = newValue;
|
|
76
|
+
cursorRef.current = c + t.length;
|
|
50
77
|
onChange(newValue);
|
|
51
|
-
setCursor(
|
|
52
|
-
}, [
|
|
53
|
-
// 提交当前输入
|
|
78
|
+
setCursor(c + t.length);
|
|
79
|
+
}, [onChange]);
|
|
80
|
+
// 提交当前输入(trim,M11b)
|
|
54
81
|
const submit = useCallback(() => {
|
|
55
|
-
const trimmed = value;
|
|
82
|
+
const trimmed = value.trim();
|
|
56
83
|
if (!trimmed)
|
|
57
84
|
return;
|
|
58
85
|
history?.push(trimmed);
|
|
@@ -61,35 +88,51 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
61
88
|
setCursor(0);
|
|
62
89
|
setRs(INITIAL_RS);
|
|
63
90
|
}, [value, history, onSubmit, onChange]);
|
|
91
|
+
// slash 菜单是否激活(输入以 / 开头且未取消、且不在反查模式)
|
|
92
|
+
const slashActive = value.startsWith('/') && !slashDismissed && !rs.active;
|
|
93
|
+
const slashCmds = slashActive ? filterSlashCommands(parseSlashCommand(value).raw) : [];
|
|
64
94
|
useInput((input, key) => {
|
|
65
95
|
if (disabled)
|
|
66
96
|
return;
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
// ink 的 Key 类型未声明 home/end(Fn+方向键 / 物理 Home/End),这里扩展访问
|
|
98
|
+
const k = key;
|
|
99
|
+
// ── Slash 菜单模式(M1)──
|
|
100
|
+
if (slashActive) {
|
|
101
|
+
if (key.escape) {
|
|
102
|
+
setSlashDismissed(true);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (key.upArrow) {
|
|
106
|
+
setSlashHighlight((h) => Math.max(0, h - 1));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key.downArrow) {
|
|
110
|
+
setSlashHighlight((h) => Math.min(Math.max(slashCmds.length - 1, 0), h + 1));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.return && slashCmds.length > 0) {
|
|
114
|
+
const cmd = slashCmds[Math.min(slashHighlight, slashCmds.length - 1)];
|
|
115
|
+
const parsed = parseSlashCommand(value);
|
|
116
|
+
onSlash?.(cmd.name, parsed.arg);
|
|
117
|
+
onChange('');
|
|
118
|
+
setCursor(0);
|
|
119
|
+
setSlashDismissed(false);
|
|
120
|
+
setSlashHighlight(0);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// 其他键:作为普通编辑落到下方处理(菜单随文本实时过滤)
|
|
79
124
|
}
|
|
80
125
|
// ── Ctrl+R:反查模式 ──
|
|
81
126
|
if (key.ctrl && input === 'r') {
|
|
82
127
|
if (!history || history.entries.length === 0)
|
|
83
128
|
return;
|
|
84
129
|
if (!rs.active) {
|
|
85
|
-
// 进入反查模式,空 query → 找最近一条
|
|
86
130
|
const match = reverseSearch(history.entries, '', history.entries.length - 1);
|
|
87
131
|
setRs({ active: true, query: '', matchIndex: match.index });
|
|
88
132
|
if (match.text)
|
|
89
133
|
onChange(match.text);
|
|
90
134
|
}
|
|
91
135
|
else {
|
|
92
|
-
// 已经在反查模式 → 跳下一条匹配
|
|
93
136
|
if (rs.matchIndex >= 0) {
|
|
94
137
|
const next = reverseSearchNext(history.entries, rs.query, rs.matchIndex);
|
|
95
138
|
setRs({ ...rs, matchIndex: next.index });
|
|
@@ -101,20 +144,25 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
101
144
|
}
|
|
102
145
|
// ── 反查模式下的其他按键 ──
|
|
103
146
|
if (rs.active) {
|
|
104
|
-
if (key.escape) {
|
|
105
|
-
// Esc
|
|
147
|
+
if (key.escape || (key.ctrl && input === 'g')) {
|
|
148
|
+
// Esc / Ctrl+G:取消反查,恢复进入前的草稿
|
|
106
149
|
setRs(INITIAL_RS);
|
|
150
|
+
onChange(history?.draftBeforeNav ?? '');
|
|
107
151
|
return;
|
|
108
152
|
}
|
|
109
153
|
if (key.return) {
|
|
110
|
-
// Enter:提交当前匹配
|
|
111
154
|
setRs(INITIAL_RS);
|
|
112
155
|
submit();
|
|
113
156
|
return;
|
|
114
157
|
}
|
|
115
158
|
if (key.backspace || key.delete) {
|
|
116
|
-
// 删除 query 最后一个字符,重新匹配
|
|
117
159
|
const newQuery = rs.query.slice(0, -1);
|
|
160
|
+
if (newQuery.length === 0) {
|
|
161
|
+
// 退到空 query:自动退出反查,恢复到进入前草稿
|
|
162
|
+
setRs(INITIAL_RS);
|
|
163
|
+
onChange(history?.draftBeforeNav ?? '');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
118
166
|
const match = reverseSearch(history.entries, newQuery);
|
|
119
167
|
setRs({ ...rs, query: newQuery, matchIndex: match.index });
|
|
120
168
|
if (match.text)
|
|
@@ -123,7 +171,6 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
123
171
|
onChange(newQuery);
|
|
124
172
|
return;
|
|
125
173
|
}
|
|
126
|
-
// 可打印字符 → 追加到 query,重新匹配
|
|
127
174
|
if (input && !key.ctrl && !key.meta && input.length === 1 && input >= ' ') {
|
|
128
175
|
const newQuery = rs.query + input;
|
|
129
176
|
const match = reverseSearch(history.entries, newQuery);
|
|
@@ -134,22 +181,69 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
134
181
|
onChange(newQuery);
|
|
135
182
|
return;
|
|
136
183
|
}
|
|
137
|
-
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// ── Home / End(物理键 + Fn 方向键)──
|
|
187
|
+
if (k.home || (key.meta && key.leftArrow)) {
|
|
188
|
+
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
189
|
+
setCursor(lineStart);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (k.end || (key.meta && key.rightArrow)) {
|
|
193
|
+
const lineEnd = value.indexOf('\n', cursor);
|
|
194
|
+
setCursor(lineEnd === -1 ? value.length : lineEnd);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// ── Word jump(Alt+←/→ 或 Ctrl+←/→)──
|
|
198
|
+
if ((key.meta || key.ctrl) && key.leftArrow) {
|
|
199
|
+
setCursor(prevWordBoundary(value, cursor));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if ((key.meta || key.ctrl) && key.rightArrow) {
|
|
203
|
+
setCursor(nextWordBoundary(value, cursor));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// ── Ctrl+W:删除前一个 word ──
|
|
207
|
+
if (key.ctrl && input === 'w') {
|
|
208
|
+
const wordStart = prevWordBoundary(value, cursor);
|
|
209
|
+
onChange(value.slice(0, wordStart) + value.slice(cursor));
|
|
210
|
+
setCursor(wordStart);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// ── Ctrl+K:删到行尾 ──
|
|
214
|
+
if (key.ctrl && input === 'k') {
|
|
215
|
+
const lineEnd = value.indexOf('\n', cursor);
|
|
216
|
+
const end = lineEnd === -1 ? value.length : lineEnd;
|
|
217
|
+
onChange(value.slice(0, cursor) + value.slice(end));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// ── Ctrl+U:删到行首 ──
|
|
221
|
+
if (key.ctrl && input === 'u') {
|
|
222
|
+
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
223
|
+
onChange(value.slice(0, lineStart) + value.slice(cursor));
|
|
224
|
+
setCursor(lineStart);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// ── Ctrl+D:空输入退出;非空删除光标后字符 ──
|
|
228
|
+
if (key.ctrl && input === 'd') {
|
|
229
|
+
if (value.length === 0) {
|
|
230
|
+
onExit?.();
|
|
231
|
+
}
|
|
232
|
+
else if (cursor < value.length) {
|
|
233
|
+
onChange(value.slice(0, cursor) + value.slice(cursor + 1));
|
|
234
|
+
}
|
|
138
235
|
return;
|
|
139
236
|
}
|
|
140
237
|
// ── 普通模式 ──
|
|
141
238
|
if (key.return) {
|
|
142
|
-
// Shift+Enter / Meta+Enter → 换行(多行模式)
|
|
143
239
|
if (multiline && (key.shift || key.meta)) {
|
|
144
240
|
insertText('\n');
|
|
145
241
|
return;
|
|
146
242
|
}
|
|
147
|
-
// 普通 Enter → 提交
|
|
148
243
|
submit();
|
|
149
244
|
return;
|
|
150
245
|
}
|
|
151
246
|
if (key.ctrl && input === 'j') {
|
|
152
|
-
// Ctrl+J = LF,多行模式下换行
|
|
153
247
|
if (multiline) {
|
|
154
248
|
insertText('\n');
|
|
155
249
|
}
|
|
@@ -159,14 +253,11 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
159
253
|
return;
|
|
160
254
|
}
|
|
161
255
|
if (key.ctrl && input === 'c') {
|
|
162
|
-
// §3.3 M3:Ctrl+C 交给 App 层处理两次语义(graceful shutdown)
|
|
163
|
-
// Ink raw mode拦截 Ctrl+C 不走 process SIGINT,故需显式回调
|
|
164
256
|
onAbort?.();
|
|
165
257
|
return;
|
|
166
258
|
}
|
|
167
259
|
if (key.backspace || key.delete) {
|
|
168
260
|
if (cursor > 0) {
|
|
169
|
-
// 删除光标前一个字符(含跨行 \n)
|
|
170
261
|
const newValue = value.slice(0, cursor - 1) + value.slice(cursor);
|
|
171
262
|
onChange(newValue);
|
|
172
263
|
setCursor(cursor - 1);
|
|
@@ -182,11 +273,9 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
182
273
|
return;
|
|
183
274
|
}
|
|
184
275
|
if (key.upArrow) {
|
|
185
|
-
// 多行模式:光标移到上一行(如果有)
|
|
186
276
|
if (multiline) {
|
|
187
277
|
const { lineStart, lineIndex, colInLine } = getLineInfo(value, cursor);
|
|
188
278
|
if (lineIndex > 0) {
|
|
189
|
-
// 上一行同列位置
|
|
190
279
|
const prevLineStart = value.lastIndexOf('\n', lineStart - 1) + 1;
|
|
191
280
|
const prevLineEnd = lineStart - 1;
|
|
192
281
|
const prevLineLen = prevLineEnd - prevLineStart;
|
|
@@ -195,7 +284,6 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
195
284
|
return;
|
|
196
285
|
}
|
|
197
286
|
}
|
|
198
|
-
// 单行模式 或 多行已在第一行 → 浏览历史
|
|
199
287
|
if (history) {
|
|
200
288
|
const text = history.navigate('up', value);
|
|
201
289
|
if (text !== null) {
|
|
@@ -217,7 +305,6 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
217
305
|
return;
|
|
218
306
|
}
|
|
219
307
|
}
|
|
220
|
-
// 单行 或 多行最后一行 → 浏览历史
|
|
221
308
|
if (history) {
|
|
222
309
|
const text = history.navigate('down', value);
|
|
223
310
|
if (text !== null) {
|
|
@@ -225,7 +312,6 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
225
312
|
setCursor(text.length);
|
|
226
313
|
}
|
|
227
314
|
else {
|
|
228
|
-
// 到底恢复草稿
|
|
229
315
|
history.exitNavigation();
|
|
230
316
|
onChange(history.draftBeforeNav);
|
|
231
317
|
setCursor(history.draftBeforeNav.length);
|
|
@@ -233,58 +319,82 @@ export function TextInput({ value, onChange, onSubmit, multiline = true, history
|
|
|
233
319
|
}
|
|
234
320
|
return;
|
|
235
321
|
}
|
|
236
|
-
// Home / End
|
|
237
322
|
if (key.ctrl && input === 'a') {
|
|
238
|
-
// 行首
|
|
239
323
|
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
240
324
|
setCursor(lineStart);
|
|
241
325
|
return;
|
|
242
326
|
}
|
|
243
327
|
if (key.ctrl && input === 'e') {
|
|
244
|
-
// 行尾
|
|
245
328
|
const lineEnd = value.indexOf('\n', cursor);
|
|
246
329
|
setCursor(lineEnd === -1 ? value.length : lineEnd);
|
|
247
330
|
return;
|
|
248
331
|
}
|
|
249
|
-
// 可打印字符
|
|
250
|
-
if (input && !key.ctrl && !key.meta && input
|
|
251
|
-
//
|
|
332
|
+
// ── 可打印字符(单/多字符统一走 insertText,M11c)──
|
|
333
|
+
if (input && !key.ctrl && !key.meta && input >= ' ') {
|
|
334
|
+
// M9:空输入时按 ? 直接打开键位帮助(等效 /keys)
|
|
335
|
+
if (input === '?' && value.length === 0) {
|
|
336
|
+
onSlash?.('keys');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const cleaned = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
252
340
|
history?.exitNavigation();
|
|
253
|
-
insertText(
|
|
341
|
+
insertText(cleaned);
|
|
254
342
|
return;
|
|
255
343
|
}
|
|
256
344
|
});
|
|
345
|
+
// ── 物理光标定位(IME 候选窗跟随)──
|
|
346
|
+
// Ink raw mode 下每帧渲染完物理光标停在帧末尾(HintBar 后),
|
|
347
|
+
// OS 的 IME 候选窗固定在物理光标处弹出,导致候选窗偏离输入框.
|
|
348
|
+
// 用 setTimeout(0) 延迟到 Ink 帧刷完后重定位,避免干扰帧缓冲.
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (disabled || !value)
|
|
351
|
+
return;
|
|
352
|
+
const s = stdout;
|
|
353
|
+
if (!s?.write || !s.isTTY || !s.rows)
|
|
354
|
+
return;
|
|
355
|
+
const writeFn = s.write;
|
|
356
|
+
const termRows = s.rows;
|
|
357
|
+
const timer = setTimeout(() => {
|
|
358
|
+
try {
|
|
359
|
+
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
360
|
+
const colInLine = cursor - lineStart;
|
|
361
|
+
let cursorLineIndex = 0;
|
|
362
|
+
for (let i = 0; i < cursor; i++) {
|
|
363
|
+
if (value[i] === '\n')
|
|
364
|
+
cursorLineIndex++;
|
|
365
|
+
}
|
|
366
|
+
/*
|
|
367
|
+
* footer 钉底结构(从屏幕底向上):
|
|
368
|
+
* row termRows : HintBar
|
|
369
|
+
* row termRows - 1 : 输入框下边框 ─
|
|
370
|
+
* row termRows - 2 : 输入框内容行(光标所在)
|
|
371
|
+
* row termRows - 3 : 输入框上边框 ─
|
|
372
|
+
*/
|
|
373
|
+
const row = Math.max(1, termRows - 2 - cursorLineIndex);
|
|
374
|
+
const col = Math.max(1, 2 + prompt.length + colInLine); // │ + space + prompt + 行内列
|
|
375
|
+
writeFn(`\x1b[${row};${col}H`);
|
|
376
|
+
}
|
|
377
|
+
catch { /* 非TTY 或流已关闭时静默失败 */ }
|
|
378
|
+
}, 0);
|
|
379
|
+
return () => clearTimeout(timer);
|
|
380
|
+
}, [value, cursor, disabled, prompt, stdout]);
|
|
257
381
|
// ── 渲染 ──
|
|
258
|
-
|
|
382
|
+
if (slashActive) {
|
|
383
|
+
return _jsx(SlashMenu, { commands: slashCmds, highlight: Math.min(slashHighlight, Math.max(slashCmds.length - 1, 0)) });
|
|
384
|
+
}
|
|
259
385
|
if (rs.active) {
|
|
260
386
|
const matchText = rs.matchIndex >= 0 && history ? history.entries[rs.matchIndex] : '';
|
|
261
|
-
return (_jsxs(Text, { children: [
|
|
387
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "(reverse-i-search)`" }), _jsx(Text, { children: rs.query }), _jsx(Text, { inverse: true, children: " " }), _jsx(Text, { dimColor: true, children: "`: " }), _jsx(Text, { color: matchText ? 'cyan' : 'red', children: matchText || '(无匹配 · Esc/Ctrl+G 取消)' })] }));
|
|
262
388
|
}
|
|
263
|
-
// 空输入 + placeholder
|
|
264
389
|
if (!value && placeholder) {
|
|
265
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: prompt }), _jsx(Text, { dimColor: true, children: placeholder })] }));
|
|
390
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: prompt }), _jsx(Text, { inverse: true, children: " " }), _jsx(Text, { dimColor: true, children: placeholder })] }));
|
|
266
391
|
}
|
|
267
|
-
// 带光标渲染(▋ 标记光标位置)
|
|
268
392
|
const before = value.slice(0, cursor);
|
|
269
393
|
const at = value.slice(cursor, cursor + 1);
|
|
270
394
|
const after = value.slice(cursor + 1);
|
|
271
395
|
return (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: prompt }), before, _jsx(Text, { inverse: true, children: at || ' ' }), after] }));
|
|
272
396
|
}
|
|
273
|
-
/**
|
|
274
|
-
* 多字符输入是否为粘贴(纯函数,便于单测)。
|
|
275
|
-
*
|
|
276
|
-
* Ink useInput 把整段 stdin chunk 作为单次 input 传入。
|
|
277
|
-
* 视为粘贴的特征:含换行(多行粘贴)或长度 >= 5(普通打字不可能一次到 5 字符)。
|
|
278
|
-
* 短输入(2-4 字符无换行)可能是 IME 选词 / 快速连击,都按"整体插入"处理
|
|
279
|
-
* (不再逐字符,以避免 useCallback 闭包陷阱导致多字符只保留最后一个)。
|
|
280
|
-
*/
|
|
281
|
-
export function isPasteInput(input) {
|
|
282
|
-
if (!input || input.length < 2)
|
|
283
|
-
return false;
|
|
284
|
-
return input.includes('\n') || input.includes('\r') || input.length >= 5;
|
|
285
|
-
}
|
|
286
397
|
function getLineInfo(value, cursor) {
|
|
287
|
-
// 找当前行的起始
|
|
288
398
|
let lineStart = 0;
|
|
289
399
|
for (let i = cursor - 1; i >= 0; i--) {
|
|
290
400
|
if (value[i] === '\n') {
|
|
@@ -292,7 +402,6 @@ function getLineInfo(value, cursor) {
|
|
|
292
402
|
break;
|
|
293
403
|
}
|
|
294
404
|
}
|
|
295
|
-
// 找当前行的结束
|
|
296
405
|
let lineEnd = value.length;
|
|
297
406
|
for (let i = cursor; i < value.length; i++) {
|
|
298
407
|
if (value[i] === '\n') {
|
|
@@ -300,7 +409,6 @@ function getLineInfo(value, cursor) {
|
|
|
300
409
|
break;
|
|
301
410
|
}
|
|
302
411
|
}
|
|
303
|
-
// 行号 + 总行数
|
|
304
412
|
let lineIndex = 0;
|
|
305
413
|
for (let i = 0; i < lineStart; i++) {
|
|
306
414
|
if (value[i] === '\n')
|
|
@@ -315,4 +423,26 @@ function getLineInfo(value, cursor) {
|
|
|
315
423
|
colInLine: cursor - lineStart,
|
|
316
424
|
};
|
|
317
425
|
}
|
|
426
|
+
/** 找 pos 之前最近的 word 边界(跳过连续 whitespace + 非 word 字符) */
|
|
427
|
+
export function prevWordBoundary(value, pos) {
|
|
428
|
+
if (pos <= 0)
|
|
429
|
+
return 0;
|
|
430
|
+
let i = pos - 1;
|
|
431
|
+
while (i > 0 && /\s/.test(value[i]))
|
|
432
|
+
i--;
|
|
433
|
+
while (i > 0 && /[\w一-鿿]/.test(value[i - 1]))
|
|
434
|
+
i--;
|
|
435
|
+
return i;
|
|
436
|
+
}
|
|
437
|
+
/** 找 pos 之后最近的 word 边界 */
|
|
438
|
+
export function nextWordBoundary(value, pos) {
|
|
439
|
+
if (pos >= value.length)
|
|
440
|
+
return value.length;
|
|
441
|
+
let i = pos;
|
|
442
|
+
while (i < value.length && /\s/.test(value[i]))
|
|
443
|
+
i++;
|
|
444
|
+
while (i < value.length && /[\w一-鿿]/.test(value[i]))
|
|
445
|
+
i++;
|
|
446
|
+
return i;
|
|
447
|
+
}
|
|
318
448
|
//# sourceMappingURL=text-input.js.map
|