@deepseek-harness-tui/dsh-tui 0.7.2 → 0.7.3
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/README.md +1 -1
- package/bin/dsh-tui.js +5 -0
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +6 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +5 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +5 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +52 -74
- package/lib/types/components/SearchBox.d.ts +4 -0
- package/lib/types/components/SearchBox.d.ts.map +1 -1
- package/lib/types/components/SearchBox.js +101 -2
- package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts +4 -1
- package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts.map +1 -1
- package/lib/types/components/Spinner/SpinnerAnimationRow.js +10 -5
- package/lib/types/components/WorkingSpinner.d.ts +3 -1
- package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
- package/lib/types/components/WorkingSpinner.js +2 -2
- package/lib/types/components/design-system/ThemedText.d.ts +7 -1
- package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemedText.js +2 -2
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +52 -8
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +12 -3
- package/lib/types/dsh-adapter/channel.d.ts +10 -0
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +45 -9
- package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/packaged-presets.js +5 -2
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +7 -3
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +3 -1
- package/lib/types/i18n.d.ts +32 -8
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +9 -3
- package/lib/types/ink/components/Text.d.ts +9 -1
- package/lib/types/ink/components/Text.d.ts.map +1 -1
- package/lib/types/ink/components/Text.js +7 -6
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +4 -2
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +76 -9
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +4 -1
- package/lib/types/ink/terminal.d.ts +40 -4
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +57 -5
- package/lib/types/ink/termio/tokenize.d.ts +6 -0
- package/lib/types/ink/termio/tokenize.d.ts.map +1 -1
- package/lib/types/ink/termio/tokenize.js +18 -4
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +76 -16
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +30 -15
- package/lib/types/sessions/view.d.ts.map +1 -1
- package/lib/types/sessions/view.js +13 -1
- package/lib/types/update.d.ts +21 -7
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +66 -16
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +4 -1
- package/package.json +33 -9
package/README.md
CHANGED
|
@@ -207,7 +207,7 @@ compaction 和持久化继续由 DSH 服务拥有。更详细的模块边界与
|
|
|
207
207
|
- **TPS 仪表**:参考 pi-tps-meter——流式 1/8 格 gauge、历史 min-max sparkline、
|
|
208
208
|
速度语义色(≥50 绿 / ≥20 黄 / <20 红)。
|
|
209
209
|
- **working-activity 生态**:工作状态行复用
|
|
210
|
-
[dsh-working-activity](https://github.com/ccch1mneyyy/
|
|
210
|
+
[dsh-working-activity](https://github.com/ccch1mneyyy/working-activity)
|
|
211
211
|
的纯状态机,在进程内从基础会话事件派生,不向共享日志写入 UI 状态。
|
|
212
212
|
- **终端粘贴**:raw 模式下 Ctrl+V 由应用接管,按平台读取系统剪贴板——Windows
|
|
213
213
|
走 PowerShell `Get-Clipboard`,macOS 走 `osascript`/`pbpaste`,Linux 自动探测
|
package/bin/dsh-tui.js
CHANGED
|
@@ -96,6 +96,10 @@ const MSG = {
|
|
|
96
96
|
en: err => `[dsh-tui] Failed to launch: ${err.message}`,
|
|
97
97
|
zh: err => `[dsh-tui] 启动失败:${err.message}`,
|
|
98
98
|
},
|
|
99
|
+
profileExited: {
|
|
100
|
+
en: code => `[dsh-tui] dsh profile exited with code ${code}. Run it directly for diagnostics:\n dsh --profile ${PROFILE}`,
|
|
101
|
+
zh: code => `[dsh-tui] dsh profile 已退出(退出码 ${code})。可直接运行以下命令查看诊断:\n dsh --profile ${PROFILE}`,
|
|
102
|
+
},
|
|
99
103
|
legacyEnv: {
|
|
100
104
|
en: (oldName, newName) => `[dsh-tui] note: env ${oldName} was renamed to ${newName}; the old name no longer takes effect.`,
|
|
101
105
|
zh: (oldName, newName) => `[dsh-tui] 提示:环境变量 ${oldName} 已更名为 ${newName},旧名不再生效。`,
|
|
@@ -233,6 +237,7 @@ child.on('exit', (code, signal) => {
|
|
|
233
237
|
if (signal) {
|
|
234
238
|
process.kill(process.pid, signal)
|
|
235
239
|
} else {
|
|
240
|
+
if (code !== null && code !== 0) console.error(MSG.profileExited[lang](code))
|
|
236
241
|
process.exit(code ?? 0)
|
|
237
242
|
}
|
|
238
243
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GoalTodoPanel.d.ts","sourceRoot":"","sources":["../../../src/components/GoalTodoPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,OAAO,EAA8B,MAAM,2BAA2B,CAAA;AAwDpF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"GoalTodoPanel.d.ts","sourceRoot":"","sources":["../../../src/components/GoalTodoPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,OAAO,EAA8B,MAAM,2BAA2B,CAAA;AAwDpF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAC,SAAS,CAgEhF"}
|
|
@@ -45,7 +45,12 @@ function BranchPrefix({ last }) {
|
|
|
45
45
|
*/
|
|
46
46
|
export function GoalTodoPanel({ channel }) {
|
|
47
47
|
const goal = channel.goal;
|
|
48
|
-
const
|
|
48
|
+
const allTodos = channel.todos ?? [];
|
|
49
|
+
// Completed rows are useful progress while a turn is running, but become
|
|
50
|
+
// stale footer noise once the agent is idle. Keep unfinished work visible.
|
|
51
|
+
const todos = channel.working
|
|
52
|
+
? allTodos
|
|
53
|
+
: allTodos.filter(todo => todo.status !== 'completed');
|
|
49
54
|
if (goal === undefined && todos.length === 0)
|
|
50
55
|
return null;
|
|
51
56
|
const visible = todos.slice(0, MAX_TODOS);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HistorySearchDialog.d.ts","sourceRoot":"","sources":["../../../src/components/HistorySearchDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,YAAY,EACZ,OAAO,EACP,UAAU,GACX,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"HistorySearchDialog.d.ts","sourceRoot":"","sources":["../../../src/components/HistorySearchDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,YAAY,EACZ,OAAO,EACP,UAAU,GACX,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,KAAK,CAAC,SAAS,CAuDlB"}
|
|
@@ -26,7 +26,11 @@ export function HistorySearchDialog({ query, cursorOffset, matches, focusIndex,
|
|
|
26
26
|
const { start, end } = listWindow(matches.map(() => 2), focusIndex, Math.max(terminalRows - 18, 2), 1);
|
|
27
27
|
return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: "permission", children: t('history-search-title') }), _jsx(SearchBox, { query: query, cursorOffset: cursorOffset, isFocused: true, isTerminalFocused: isTerminalFocused, placeholder: t('history-search-placeholder') }), matches.length === 0 ? (_jsx(Text, { dimColor: true, children: t('history-search-empty') })) : (matches.slice(start, end).map((entry, index) => {
|
|
28
28
|
const absoluteIndex = start + index;
|
|
29
|
-
return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex,
|
|
29
|
+
return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex,
|
|
30
|
+
// The SearchBox owns the native-cursor declaration while this
|
|
31
|
+
// dialog is open — result rows must not park the cursor on
|
|
32
|
+
// themselves, or IME preedit lands on a list row.
|
|
33
|
+
declareCursor: false, description: formatRelativeAge(entry.ts), showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < matches.length, children: entry.text }, historyEntryId(entry)));
|
|
30
34
|
})), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-history-search') }) })] }) }));
|
|
31
35
|
}
|
|
32
36
|
/** Relative age like CC's formatRelativeTimeAgo ("now" / "5m ago" / …), localized. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/MessageList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAC3E,OAAO,KAAK,EAAE,OAAO,EAAyC,MAAM,2BAA2B,CAAA;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAuC/C,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,WAAW,EACX,eAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACjC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB;2EACuE;IACvE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IAC/D,oDAAoD;IACpD,YAAY,CAAC,EAAE,eAAe,GAAG,IAAI,CAAA;IACrC,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;8EAC0E;IAC1E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,
|
|
1
|
+
{"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/MessageList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAC3E,OAAO,KAAK,EAAE,OAAO,EAAyC,MAAM,2BAA2B,CAAA;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAuC/C,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,WAAW,EACX,eAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACjC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB;2EACuE;IACvE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IAC/D,oDAAoD;IACpD,YAAY,CAAC,EAAE,eAAe,GAAG,IAAI,CAAA;IACrC,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;8EAC0E;IAC1E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,qBAwPA;AAuQD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,MAAM,EACN,GAAG,GACJ,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,KAAK,CAAC,SAAS,CAMlB"}
|
|
@@ -158,8 +158,12 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
const lastUnseenReportRef = React.useRef(-1);
|
|
161
162
|
React.useEffect(() => {
|
|
162
|
-
|
|
163
|
+
if (unseenCount !== lastUnseenReportRef.current) {
|
|
164
|
+
lastUnseenReportRef.current = unseenCount;
|
|
165
|
+
onUnseenCount?.(unseenCount);
|
|
166
|
+
}
|
|
163
167
|
});
|
|
164
168
|
// Post-commit: measure mounted rows, derive the content-space base from
|
|
165
169
|
// the first mounted row's Yoga top, and clamp render-time scrollTop to the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAkDxD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAkDxD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBAg5BlB"}
|
|
@@ -103,6 +103,8 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
103
103
|
controllerRef.current = {
|
|
104
104
|
hasText: () => value.length > 0,
|
|
105
105
|
clear: () => {
|
|
106
|
+
valueRef.current = '';
|
|
107
|
+
cursorRef.current = 0;
|
|
106
108
|
setValue('');
|
|
107
109
|
setCursor(0);
|
|
108
110
|
},
|
|
@@ -120,6 +122,8 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
120
122
|
React.useEffect(() => {
|
|
121
123
|
if (fillText && fillText !== lastFill.current) {
|
|
122
124
|
lastFill.current = fillText;
|
|
125
|
+
valueRef.current = fillText;
|
|
126
|
+
cursorRef.current = fillText.length;
|
|
123
127
|
setValue(fillText);
|
|
124
128
|
setCursor(fillText.length);
|
|
125
129
|
onFillConsumed?.();
|
|
@@ -132,16 +136,6 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
132
136
|
const clipboardBusyRef = React.useRef(false);
|
|
133
137
|
/** True while the external editor owns the terminal (Ctrl+X round-trip). */
|
|
134
138
|
const editorBusyRef = React.useRef(false);
|
|
135
|
-
/**
|
|
136
|
-
* Latest committed input state. The Ctrl+V clipboard read resolves
|
|
137
|
-
* asynchronously; inserting against the render that STARTED the read
|
|
138
|
-
* would drop whatever the user typed while waiting. The continuation
|
|
139
|
-
* inserts against this mirror instead.
|
|
140
|
-
*/
|
|
141
|
-
const liveInputRef = React.useRef({ value: '', cursor: 0 });
|
|
142
|
-
React.useEffect(() => {
|
|
143
|
-
liveInputRef.current = { value, cursor };
|
|
144
|
-
});
|
|
145
139
|
/** Enter dedupe window: cmd pipelines can deliver one Enter as `\r`+`\n`. */
|
|
146
140
|
const lastEnterAtRef = React.useRef(0);
|
|
147
141
|
React.useEffect(() => {
|
|
@@ -193,6 +187,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
193
187
|
!helpOpen &&
|
|
194
188
|
!selectionActive &&
|
|
195
189
|
fileEscRef.current !== mention?.start;
|
|
190
|
+
const setInput = (next, cursorOffset = next.length) => {
|
|
191
|
+
valueRef.current = next;
|
|
192
|
+
cursorRef.current = Math.max(0, Math.min(cursorOffset, next.length));
|
|
193
|
+
setValue(next);
|
|
194
|
+
setCursor(cursorRef.current);
|
|
195
|
+
};
|
|
196
196
|
/**
|
|
197
197
|
* Accept the selected file suggestion: replace ONLY the mention token at
|
|
198
198
|
* the caret (prefix/suffix text survives), quoting whitespace paths. A
|
|
@@ -216,8 +216,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
216
216
|
if (history.current.length > HISTORY_LIMIT)
|
|
217
217
|
history.current.shift();
|
|
218
218
|
historyIndex.current = -1;
|
|
219
|
-
|
|
220
|
-
setCursor(0);
|
|
219
|
+
setInput('', 0);
|
|
221
220
|
setSelectedCommand(0);
|
|
222
221
|
appendHistory(trimmed);
|
|
223
222
|
channel.submit(trimmed);
|
|
@@ -243,8 +242,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
243
242
|
if (history.current.length > HISTORY_LIMIT)
|
|
244
243
|
history.current.shift();
|
|
245
244
|
historyIndex.current = -1;
|
|
246
|
-
|
|
247
|
-
setCursor(0);
|
|
245
|
+
setInput('', 0);
|
|
248
246
|
setSelectedCommand(0);
|
|
249
247
|
appendHistory(trimmed);
|
|
250
248
|
channel.steer(trimmed);
|
|
@@ -262,8 +260,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
262
260
|
if (history.current.length > HISTORY_LIMIT)
|
|
263
261
|
history.current.shift();
|
|
264
262
|
historyIndex.current = -1;
|
|
265
|
-
|
|
266
|
-
setCursor(0);
|
|
263
|
+
setInput('', 0);
|
|
267
264
|
setSelectedCommand(0);
|
|
268
265
|
appendHistory(trimmed);
|
|
269
266
|
channel.submit(trimmed);
|
|
@@ -282,8 +279,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
282
279
|
channel.notify(t('input-cannot-retract'), { color: 'warning', timeoutMs: 2500 });
|
|
283
280
|
return;
|
|
284
281
|
}
|
|
285
|
-
|
|
286
|
-
setCursor(item.text.length);
|
|
282
|
+
setInput(item.text);
|
|
287
283
|
setSelectedCommand(0);
|
|
288
284
|
setFileSelected(0);
|
|
289
285
|
channel.notify(t('input-retracted'), { timeoutMs: 2000 });
|
|
@@ -308,8 +304,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
308
304
|
if (history.current.length > HISTORY_LIMIT)
|
|
309
305
|
history.current.shift();
|
|
310
306
|
historyIndex.current = -1;
|
|
311
|
-
|
|
312
|
-
setCursor(0);
|
|
307
|
+
setInput('', 0);
|
|
313
308
|
setSelectedCommand(0);
|
|
314
309
|
setFileSelected(0);
|
|
315
310
|
appendHistory(trimmed);
|
|
@@ -336,19 +331,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
336
331
|
if (history.current.length > HISTORY_LIMIT)
|
|
337
332
|
history.current.shift();
|
|
338
333
|
historyIndex.current = -1;
|
|
339
|
-
|
|
340
|
-
setCursor(0);
|
|
334
|
+
setInput('', 0);
|
|
341
335
|
setSelectedCommand(0);
|
|
342
336
|
appendHistory(text.trim());
|
|
343
337
|
}
|
|
344
338
|
return handled;
|
|
345
339
|
};
|
|
346
|
-
const setInput = (next, cursorOffset = next.length) => {
|
|
347
|
-
valueRef.current = next;
|
|
348
|
-
cursorRef.current = Math.max(0, Math.min(cursorOffset, next.length));
|
|
349
|
-
setValue(next);
|
|
350
|
-
setCursor(cursorRef.current);
|
|
351
|
-
};
|
|
352
340
|
/** Clipboard reads are asynchronous; insert against the latest render so
|
|
353
341
|
* typing while PowerShell owns the clipboard never gets overwritten. */
|
|
354
342
|
const insertClipboardAtCaret = (text) => {
|
|
@@ -359,13 +347,13 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
359
347
|
setFileSelected(0);
|
|
360
348
|
};
|
|
361
349
|
/** Line index of the cursor; -1 when the cursor is at the very end. */
|
|
362
|
-
const cursorLine = () => {
|
|
363
|
-
const before =
|
|
350
|
+
const cursorLine = (text, cursorOffset) => {
|
|
351
|
+
const before = text.slice(0, cursorOffset);
|
|
364
352
|
return before.split('\n').length - 1;
|
|
365
353
|
};
|
|
366
354
|
/** Column of the cursor within its line. */
|
|
367
|
-
const cursorColumn = () => {
|
|
368
|
-
const before =
|
|
355
|
+
const cursorColumn = (text, cursorOffset) => {
|
|
356
|
+
const before = text.slice(0, cursorOffset);
|
|
369
357
|
const line = before.split('\n').pop() ?? '';
|
|
370
358
|
return line.length;
|
|
371
359
|
};
|
|
@@ -377,13 +365,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
377
365
|
// setValue can never overwrite fresh typing (and vice versa).
|
|
378
366
|
if (editorBusyRef.current)
|
|
379
367
|
return;
|
|
368
|
+
// App deliberately dispatches every parsed key from one stdin read in a
|
|
369
|
+
// single React update. Read the synchronous mirrors so each event sees
|
|
370
|
+
// the text/caret produced by the preceding event in that batch.
|
|
371
|
+
const value = valueRef.current;
|
|
372
|
+
const cursor = cursorRef.current;
|
|
380
373
|
/** Insert text at the caret (typing, paste) and dismiss overlays. */
|
|
381
374
|
const insertAtCaret = (text) => {
|
|
382
375
|
if (helpOpen)
|
|
383
376
|
onToggleHelp();
|
|
384
377
|
const next = value.slice(0, cursor) + text + value.slice(cursor);
|
|
385
|
-
|
|
386
|
-
setCursor(cursor + text.length);
|
|
378
|
+
setInput(next, cursor + text.length);
|
|
387
379
|
setSelectedCommand(0);
|
|
388
380
|
setFileSelected(0);
|
|
389
381
|
};
|
|
@@ -441,10 +433,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
441
433
|
// Insert against the LIVE input state: the read above resolved
|
|
442
434
|
// asynchronously and the user may have typed while waiting.
|
|
443
435
|
const text = formatClipboardInsert(content);
|
|
444
|
-
|
|
445
|
-
const caret = Math.min(live.cursor, live.value.length);
|
|
446
|
-
setValue(live.value.slice(0, caret) + text + live.value.slice(caret));
|
|
447
|
-
setCursor(caret + text.length);
|
|
436
|
+
insertClipboardAtCaret(text);
|
|
448
437
|
})
|
|
449
438
|
.catch(() => {
|
|
450
439
|
channel.notify(t('input-clipboard-read-failed'), { color: 'warning' });
|
|
@@ -469,8 +458,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
469
458
|
try {
|
|
470
459
|
const outcome = await editInExternalEditor(value);
|
|
471
460
|
if (outcome.kind === 'edited') {
|
|
472
|
-
|
|
473
|
-
setCursor(outcome.text.length);
|
|
461
|
+
setInput(outcome.text);
|
|
474
462
|
setSelectedCommand(0);
|
|
475
463
|
setFileSelected(0);
|
|
476
464
|
}
|
|
@@ -575,8 +563,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
575
563
|
// ink/terminal.ts); Option+Enter (ESC CR) is the fallback on terminals
|
|
576
564
|
// that can't report shift — e.g. macOS Terminal.app (issue #110).
|
|
577
565
|
const next = value.slice(0, cursor) + '\n' + value.slice(cursor);
|
|
578
|
-
|
|
579
|
-
setCursor(cursor + 1);
|
|
566
|
+
setInput(next, cursor + 1);
|
|
580
567
|
setSelectedCommand(0);
|
|
581
568
|
return;
|
|
582
569
|
}
|
|
@@ -620,13 +607,13 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
620
607
|
setFileSelected(index => index <= 0 ? fileMatches.length - 1 : index - 1);
|
|
621
608
|
return;
|
|
622
609
|
}
|
|
623
|
-
const line = cursorLine();
|
|
610
|
+
const line = cursorLine(value, cursor);
|
|
624
611
|
if (line > 0) {
|
|
625
612
|
// Move to the previous line, clamping to its length.
|
|
626
613
|
const upToLineStart = value.lastIndexOf('\n', cursor - 1);
|
|
627
614
|
const prevLineStart = upToLineStart === -1 ? 0 : value.lastIndexOf('\n', upToLineStart - 1) + 1;
|
|
628
615
|
const prevLine = value.slice(prevLineStart, upToLineStart);
|
|
629
|
-
|
|
616
|
+
setInput(value, prevLineStart + Math.min(cursorColumn(value, cursor), prevLine.length));
|
|
630
617
|
return;
|
|
631
618
|
}
|
|
632
619
|
if (overlayOpen) {
|
|
@@ -639,8 +626,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
639
626
|
? history.current.length - 1
|
|
640
627
|
: Math.max(0, historyIndex.current - 1);
|
|
641
628
|
const entry = history.current[historyIndex.current] ?? '';
|
|
642
|
-
|
|
643
|
-
setCursor(entry.length);
|
|
629
|
+
setInput(entry);
|
|
644
630
|
return;
|
|
645
631
|
}
|
|
646
632
|
if (key.downArrow) {
|
|
@@ -648,13 +634,13 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
648
634
|
setFileSelected(index => index >= fileMatches.length - 1 ? 0 : index + 1);
|
|
649
635
|
return;
|
|
650
636
|
}
|
|
651
|
-
const line = cursorLine();
|
|
637
|
+
const line = cursorLine(value, cursor);
|
|
652
638
|
const lines = value.split('\n');
|
|
653
639
|
if (line < lines.length - 1) {
|
|
654
640
|
const nextLineStart = value.indexOf('\n', cursor) + 1;
|
|
655
641
|
const nextLineEnd = value.indexOf('\n', nextLineStart);
|
|
656
642
|
const nextLine = value.slice(nextLineStart, nextLineEnd === -1 ? value.length : nextLineEnd);
|
|
657
|
-
|
|
643
|
+
setInput(value, nextLineStart + Math.min(cursorColumn(value, cursor), nextLine.length));
|
|
658
644
|
return;
|
|
659
645
|
}
|
|
660
646
|
if (overlayOpen) {
|
|
@@ -667,76 +653,73 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
667
653
|
const entry = historyIndex.current >= history.current.length
|
|
668
654
|
? ''
|
|
669
655
|
: (history.current[historyIndex.current] ?? '');
|
|
670
|
-
|
|
671
|
-
setCursor(entry.length);
|
|
656
|
+
setInput(entry);
|
|
672
657
|
return;
|
|
673
658
|
}
|
|
674
659
|
if (isMod(key) && key.leftArrow) {
|
|
675
660
|
// Jump to the previous word boundary (readline alt+b). Must precede the
|
|
676
661
|
// bare-arrow arms: Ctrl+Left arrives as leftArrow + ctrl.
|
|
677
|
-
|
|
662
|
+
setInput(value, wordBoundaryLeft(value, cursor));
|
|
678
663
|
return;
|
|
679
664
|
}
|
|
680
665
|
if (isMod(key) && key.rightArrow) {
|
|
681
666
|
// Jump to the next word boundary (readline alt+f).
|
|
682
|
-
|
|
667
|
+
setInput(value, wordBoundaryRight(value, cursor));
|
|
683
668
|
return;
|
|
684
669
|
}
|
|
685
670
|
if (key.leftArrow) {
|
|
686
|
-
|
|
671
|
+
setInput(value, Math.max(0, cursor - 1));
|
|
687
672
|
return;
|
|
688
673
|
}
|
|
689
674
|
if (key.rightArrow) {
|
|
690
|
-
|
|
675
|
+
setInput(value, Math.min(value.length, cursor + 1));
|
|
691
676
|
return;
|
|
692
677
|
}
|
|
693
678
|
if (key.backspace) {
|
|
694
679
|
if (cursor === 0)
|
|
695
680
|
return;
|
|
696
|
-
|
|
697
|
-
setCursor(cursor - 1);
|
|
681
|
+
setInput(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
|
|
698
682
|
return;
|
|
699
683
|
}
|
|
700
684
|
if (key.delete) {
|
|
701
685
|
if (cursor >= value.length)
|
|
702
686
|
return;
|
|
703
|
-
|
|
687
|
+
setInput(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
|
|
704
688
|
return;
|
|
705
689
|
}
|
|
706
690
|
if (key.home) {
|
|
707
691
|
// Start of the current line.
|
|
708
692
|
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
709
|
-
|
|
693
|
+
setInput(value, lineStart);
|
|
710
694
|
return;
|
|
711
695
|
}
|
|
712
696
|
if (key.end) {
|
|
713
697
|
// End of the current line.
|
|
714
698
|
const nextLine = value.indexOf('\n', cursor);
|
|
715
|
-
|
|
699
|
+
setInput(value, nextLine === -1 ? value.length : nextLine);
|
|
716
700
|
return;
|
|
717
701
|
}
|
|
718
702
|
if (isMod(key) && input === 'a') {
|
|
719
703
|
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
720
|
-
|
|
704
|
+
setInput(value, lineStart);
|
|
721
705
|
return;
|
|
722
706
|
}
|
|
723
707
|
if (isMod(key) && input === 'e') {
|
|
724
708
|
const nextLine = value.indexOf('\n', cursor);
|
|
725
|
-
|
|
709
|
+
setInput(value, nextLine === -1 ? value.length : nextLine);
|
|
726
710
|
return;
|
|
727
711
|
}
|
|
728
712
|
if (isMod(key) && input === 'u') {
|
|
729
713
|
// Delete to start of line.
|
|
730
714
|
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
731
|
-
|
|
732
|
-
setCursor(lineStart);
|
|
715
|
+
setInput(value.slice(0, lineStart) + value.slice(cursor), lineStart);
|
|
733
716
|
return;
|
|
734
717
|
}
|
|
735
718
|
if (isMod(key) && input === 'k') {
|
|
736
719
|
// Delete to end of line.
|
|
737
720
|
const nextLine = value.indexOf('\n', cursor);
|
|
738
721
|
const end = nextLine === -1 ? value.length : nextLine;
|
|
739
|
-
|
|
722
|
+
setInput(value.slice(0, cursor) + value.slice(end), cursor);
|
|
740
723
|
return;
|
|
741
724
|
}
|
|
742
725
|
if (isMod(key) && input === 'w') {
|
|
@@ -749,8 +732,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
749
732
|
let start = end;
|
|
750
733
|
while (start > 0 && !/\s/.test(before[start - 1]))
|
|
751
734
|
start--;
|
|
752
|
-
|
|
753
|
-
setCursor(start);
|
|
735
|
+
setInput(value.slice(0, start) + value.slice(cursor), start);
|
|
754
736
|
return;
|
|
755
737
|
}
|
|
756
738
|
if (key.escape) {
|
|
@@ -761,8 +743,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
761
743
|
// A single Esc closes the open command menu first (CC/pi behavior);
|
|
762
744
|
// the double-tap-clear semantics only apply to ordinary input.
|
|
763
745
|
if (overlayOpen) {
|
|
764
|
-
|
|
765
|
-
setCursor(0);
|
|
746
|
+
setInput('', 0);
|
|
766
747
|
setSelectedCommand(0);
|
|
767
748
|
setFileSelected(0);
|
|
768
749
|
return;
|
|
@@ -786,8 +767,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
786
767
|
// A single Esc clears the current input (if any); the double-tap
|
|
787
768
|
// path below handles rewind/clear on an already-empty input.
|
|
788
769
|
if (value.length > 0) {
|
|
789
|
-
|
|
790
|
-
setCursor(0);
|
|
770
|
+
setInput('', 0);
|
|
791
771
|
setSelectedCommand(0);
|
|
792
772
|
setFileSelected(0);
|
|
793
773
|
return;
|
|
@@ -803,8 +783,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
803
783
|
onRewindRequest?.();
|
|
804
784
|
}
|
|
805
785
|
else {
|
|
806
|
-
|
|
807
|
-
setCursor(0);
|
|
786
|
+
setInput('', 0);
|
|
808
787
|
}
|
|
809
788
|
return;
|
|
810
789
|
}
|
|
@@ -826,8 +805,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
826
805
|
if (helpOpen)
|
|
827
806
|
onToggleHelp();
|
|
828
807
|
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
829
|
-
|
|
830
|
-
setCursor(cursor + input.length);
|
|
808
|
+
setInput(next, cursor + input.length);
|
|
831
809
|
setSelectedCommand(0);
|
|
832
810
|
setFileSelected(0);
|
|
833
811
|
}
|
|
@@ -6,6 +6,10 @@ import React from 'react';
|
|
|
6
6
|
* placeholder is right-aligned (dimmed) — kept off the caret's cell so the
|
|
7
7
|
* terminal-painted IME preedit (pinyin) can never be overlaid on it during
|
|
8
8
|
* CJK composition.
|
|
9
|
+
*
|
|
10
|
+
* The query row is strictly single-line: an overlong query is windowed
|
|
11
|
+
* around the caret (horizontal scroll) instead of wrapping, so the native
|
|
12
|
+
* cursor declaration below stays exact for any query length.
|
|
9
13
|
*/
|
|
10
14
|
export declare function SearchBox({ query, placeholder, isFocused, isTerminalFocused, prefix, width, cursorOffset, borderless, }: {
|
|
11
15
|
query: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchBox.d.ts","sourceRoot":"","sources":["../../../src/components/SearchBox.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"SearchBox.d.ts","sourceRoot":"","sources":["../../../src/components/SearchBox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyD,MAAM,OAAO,CAAA;AAyD7E;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,WAAuB,EACvB,SAAS,EACT,iBAAiB,EACjB,MAAY,EACZ,KAAK,EACL,YAAY,EACZ,UAAkB,GACnB,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;IAClB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,GAAG,KAAK,CAAC,SAAS,CA0GlB"}
|
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
2
3
|
import Box from '../ink/components/Box.js';
|
|
3
4
|
import Text from '../ink/components/Text.js';
|
|
5
|
+
import measureElement from '../ink/measure-element.js';
|
|
6
|
+
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js';
|
|
7
|
+
import { useTerminalSize } from '../ink/hooks/use-terminal-size.js';
|
|
8
|
+
import { stringWidth } from '../ink/stringWidth.js';
|
|
9
|
+
/**
|
|
10
|
+
* Window a single-line query around the caret so the visible slice fits
|
|
11
|
+
* `avail` display cells (CJK display width, code-point safe — `[...s]`
|
|
12
|
+
* iterates code points, not UTF-16 units). The caret character itself
|
|
13
|
+
* always stays inside the window: leading characters are dropped until
|
|
14
|
+
* before-caret text + caret fit, then after-caret text fills what remains.
|
|
15
|
+
* `offset` arrives in UTF-16 units (callers move the caret by key count);
|
|
16
|
+
* a mid-surrogate offset would split an emoji, so it snaps back to the
|
|
17
|
+
* pair's start first.
|
|
18
|
+
*/
|
|
19
|
+
function windowQuery(query, offset, avail) {
|
|
20
|
+
const budget = Math.max(avail, 1);
|
|
21
|
+
let caret = Math.max(0, Math.min(offset, query.length));
|
|
22
|
+
if (caret > 0 &&
|
|
23
|
+
caret < query.length &&
|
|
24
|
+
query.charCodeAt(caret - 1) >= 0xd800 &&
|
|
25
|
+
query.charCodeAt(caret - 1) <= 0xdbff &&
|
|
26
|
+
query.charCodeAt(caret) >= 0xdc00 &&
|
|
27
|
+
query.charCodeAt(caret) <= 0xdfff) {
|
|
28
|
+
caret--; // mid-surrogate: snap to the emoji's start
|
|
29
|
+
}
|
|
30
|
+
const beforeChars = [...query.slice(0, caret)];
|
|
31
|
+
const at = caret < query.length ? [...query.slice(caret)][0] : ' ';
|
|
32
|
+
const atWidth = Math.max(1, stringWidth(at));
|
|
33
|
+
let caretColumn = 0;
|
|
34
|
+
for (const ch of beforeChars)
|
|
35
|
+
caretColumn += stringWidth(ch);
|
|
36
|
+
let start = 0;
|
|
37
|
+
while (start < beforeChars.length && caretColumn + atWidth > budget) {
|
|
38
|
+
caretColumn -= stringWidth(beforeChars[start]);
|
|
39
|
+
start++;
|
|
40
|
+
}
|
|
41
|
+
let rest = budget - caretColumn - atWidth;
|
|
42
|
+
let after = '';
|
|
43
|
+
for (const ch of [...query.slice(caret + at.length)]) {
|
|
44
|
+
const w = stringWidth(ch);
|
|
45
|
+
if (w > rest)
|
|
46
|
+
break;
|
|
47
|
+
after += ch;
|
|
48
|
+
rest -= w;
|
|
49
|
+
}
|
|
50
|
+
return { before: beforeChars.slice(start).join(''), at, after, caretColumn };
|
|
51
|
+
}
|
|
4
52
|
/**
|
|
5
53
|
* A single-line search input in the round-bordered box of Claude Code's
|
|
6
54
|
* SearchBox: `⌕ ` prefix, block cursor at `cursorOffset` (inverse cell).
|
|
@@ -8,6 +56,10 @@ import Text from '../ink/components/Text.js';
|
|
|
8
56
|
* placeholder is right-aligned (dimmed) — kept off the caret's cell so the
|
|
9
57
|
* terminal-painted IME preedit (pinyin) can never be overlaid on it during
|
|
10
58
|
* CJK composition.
|
|
59
|
+
*
|
|
60
|
+
* The query row is strictly single-line: an overlong query is windowed
|
|
61
|
+
* around the caret (horizontal scroll) instead of wrapping, so the native
|
|
62
|
+
* cursor declaration below stays exact for any query length.
|
|
11
63
|
*/
|
|
12
64
|
export function SearchBox({ query, placeholder = 'Search…', isFocused, isTerminalFocused, prefix = '⌕', width, cursorOffset, borderless = false, }) {
|
|
13
65
|
const offset = cursorOffset ?? query.length;
|
|
@@ -17,10 +69,57 @@ export function SearchBox({ query, placeholder = 'Search…', isFocused, isTermi
|
|
|
17
69
|
// Focused + empty + terminal focused: inline caret row (block caret at the
|
|
18
70
|
// start, placeholder right-aligned) instead of the inline placeholder.
|
|
19
71
|
const inlineCaret = isFocused && query === '' && isTerminalFocused;
|
|
72
|
+
// Content width of the box in display cells. Measured from yoga after
|
|
73
|
+
// layout (resize re-layouts without any prop/state change, so measure on
|
|
74
|
+
// every commit — the setState is a no-op when the width is unchanged);
|
|
75
|
+
// the terminal-columns estimate only covers the very first frame.
|
|
76
|
+
const { columns } = useTerminalSize();
|
|
77
|
+
const chrome = borderless ? 0 : 4; // 2 border cells + paddingX 2
|
|
78
|
+
const [measuredWidth, setMeasuredWidth] = useState(null);
|
|
79
|
+
const contentWidth = measuredWidth ?? Math.max(8, columns - chrome - 2);
|
|
80
|
+
const prefixWidth = stringWidth(`${prefix} `);
|
|
81
|
+
const win = windowQuery(query, offset, contentWidth - prefixWidth);
|
|
82
|
+
// Park the native terminal cursor at the caret so IME preedit (pinyin)
|
|
83
|
+
// renders inline at the input instead of the screen's bottom row (same
|
|
84
|
+
// mechanism as PromptInput). The declaration hangs on the outer Box —
|
|
85
|
+
// content stays a single Text (splitting it into flex siblings
|
|
86
|
+
// reorders/drops glyphs on narrow widths), so the position is computed:
|
|
87
|
+
// border (1) + paddingX (1) per edge when bordered, then the `prefix `
|
|
88
|
+
// run and the windowed before-caret text, all in display cells.
|
|
89
|
+
const showCaret = isFocused && isTerminalFocused;
|
|
90
|
+
// Clamp into the box's content area: on absurdly narrow layouts the
|
|
91
|
+
// prefix alone can meet or exceed the content width, and the park must
|
|
92
|
+
// never land outside the box's rect.
|
|
93
|
+
const edge = borderless ? 0 : 2;
|
|
94
|
+
const maxColumn = edge + Math.max(0, contentWidth - 1);
|
|
95
|
+
const caretColumn = Math.min(edge + prefixWidth + win.caretColumn, maxColumn);
|
|
96
|
+
const declarationRef = useDeclaredCursor({
|
|
97
|
+
line: borderless ? 0 : 1,
|
|
98
|
+
column: caretColumn,
|
|
99
|
+
active: showCaret,
|
|
100
|
+
});
|
|
101
|
+
const boxNodeRef = useRef(null);
|
|
102
|
+
const boxRef = useCallback((node) => {
|
|
103
|
+
boxNodeRef.current = node;
|
|
104
|
+
declarationRef(node);
|
|
105
|
+
}, [declarationRef]);
|
|
106
|
+
useLayoutEffect(() => {
|
|
107
|
+
const node = boxNodeRef.current;
|
|
108
|
+
if (!node)
|
|
109
|
+
return;
|
|
110
|
+
// Layout runs before layout effects (reconciler resetAfterCommit), so a
|
|
111
|
+
// zero raw width means a genuinely zero-width box — but a zero CONTENT
|
|
112
|
+
// width (chrome eats the whole box) is a real, must-clamp case.
|
|
113
|
+
const raw = measureElement(node).width;
|
|
114
|
+
if (raw > 0) {
|
|
115
|
+
const w = Math.max(0, raw - chrome);
|
|
116
|
+
setMeasuredWidth(prev => (prev === w ? prev : w));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
20
119
|
let content;
|
|
21
120
|
if (isFocused) {
|
|
22
121
|
if (query) {
|
|
23
|
-
content = isTerminalFocused ? (_jsxs(_Fragment, { children: [_jsx(Text, { children:
|
|
122
|
+
content = isTerminalFocused ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: win.before }), _jsx(Text, { inverse: true, children: win.at }), win.after !== '' && _jsx(Text, { children: win.after })] })) : (_jsx(Text, { children: query }));
|
|
24
123
|
}
|
|
25
124
|
else if (!isTerminalFocused) {
|
|
26
125
|
content = _jsx(Text, { dimColor: true, children: placeholder });
|
|
@@ -29,5 +128,5 @@ export function SearchBox({ query, placeholder = 'Search…', isFocused, isTermi
|
|
|
29
128
|
else {
|
|
30
129
|
content = query ? _jsx(Text, { children: query }) : _jsx(Text, { children: placeholder });
|
|
31
130
|
}
|
|
32
|
-
return (_jsx(Box, { flexShrink: 0, borderStyle: borderStyle, borderColor: borderColor, borderDimColor: borderDimColor, paddingX: borderless ? 0 : 1, width: width, children: inlineCaret ? (_jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsxs(Text, { children: [prefix, " "] }), _jsx(Text, { inverse: true, children: " " }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, wrap: "truncate", children: placeholder })] })) : (_jsxs(Text, { dimColor: !isFocused, children: [prefix, " ", content] })) }));
|
|
131
|
+
return (_jsx(Box, { ref: boxRef, flexShrink: 0, borderStyle: borderStyle, borderColor: borderColor, borderDimColor: borderDimColor, paddingX: borderless ? 0 : 1, width: width, children: inlineCaret ? (_jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsxs(Text, { children: [prefix, " "] }), _jsx(Text, { inverse: true, children: " " }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, wrap: "truncate", children: placeholder })] })) : (_jsxs(Text, { dimColor: !isFocused, wrap: "truncate-end", children: [prefix, " ", content] })) }));
|
|
33
132
|
}
|
|
@@ -7,6 +7,9 @@ export type SpinnerAnimationRowProps = {
|
|
|
7
7
|
hasActiveTools: boolean;
|
|
8
8
|
/** Raw response length (chars) — feeds the animated token counter. */
|
|
9
9
|
responseLengthRef: React.RefObject<number>;
|
|
10
|
+
/** Most recent request's real upload tokens (input + cache read/write);
|
|
11
|
+
* 0 until the first usage event lands. */
|
|
12
|
+
uploadTokensRef: React.RefObject<number>;
|
|
10
13
|
/** Stable within a turn. */
|
|
11
14
|
message: string;
|
|
12
15
|
messageColor: keyof Theme;
|
|
@@ -27,5 +30,5 @@ export type SpinnerAnimationRowProps = {
|
|
|
27
30
|
* animation clock (frame, glimmer, token counter animation, elapsed time,
|
|
28
31
|
* stalled intensity, thinking shimmer).
|
|
29
32
|
*/
|
|
30
|
-
export declare function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, responseLengthRef, message, messageColor, shimmerColor, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, spinnerSuffix, verbose, columns, thinkingStatus, }: SpinnerAnimationRowProps): React.ReactNode;
|
|
33
|
+
export declare function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, responseLengthRef, uploadTokensRef, message, messageColor, shimmerColor, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, spinnerSuffix, verbose, columns, thinkingStatus, }: SpinnerAnimationRowProps): React.ReactNode;
|
|
31
34
|
//# sourceMappingURL=SpinnerAnimationRow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpinnerAnimationRow.d.ts","sourceRoot":"","sources":["../../../../src/components/Spinner/SpinnerAnimationRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAA0B,MAAM,OAAO,CAAA;AAM9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAI3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAcnD,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,sEAAsE;IACtE,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC1C,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,mBAAmB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5C,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACzC,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACjD,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,cAAc,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,OAAO,EACP,cAAc,GACf,EAAE,wBAAwB,GAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"SpinnerAnimationRow.d.ts","sourceRoot":"","sources":["../../../../src/components/Spinner/SpinnerAnimationRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAA0B,MAAM,OAAO,CAAA;AAM9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAI3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAcnD,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,sEAAsE;IACtE,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC1C;+CAC2C;IAC3C,eAAe,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACxC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,mBAAmB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5C,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACzC,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACjD,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,cAAc,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,OAAO,EACP,cAAc,GACf,EAAE,wBAAwB,GAAG,KAAK,CAAC,SAAS,CA6M5C"}
|