Skip to content

Commit ecaad55

Browse files
committed
feat: enhance UI components with internationalization support and cursor management
- Added i18n support to AgentPickerPanel and TodoPickerPanel for localized text. - Improved user feedback in FileList with enhanced loading and searching messages. - Implemented cursor visibility and positioning management in the Ink framework. - Introduced CursorContext and useCursor hook for better cursor control in terminal applications. - Refactored file loading logic in FileList for improved performance and responsiveness. - Removed unused cursor hiding logic from ChatScreen.
1 parent 00aec64 commit ecaad55

17 files changed

Lines changed: 734 additions & 407 deletions

File tree

source/app.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React, {useState, useEffect, Suspense} from 'react';
22
import {Box, Text} from 'ink';
33
import {Alert} from '@inkjs/ui';
4-
import {useCursorHide} from './hooks/ui/useCursorHide.js';
5-
64
// Lazy load all page components to improve startup time
75
// Only load components when they are actually needed
86
const WelcomeScreen = React.lazy(() => import('./ui/pages/WelcomeScreen.js'));
@@ -59,9 +57,6 @@ function ShowTaskListWrapper() {
5957
const {columns: terminalWidth} = useTerminalSize();
6058
const loadingFallback = null;
6159

62-
// Hide terminal cursor to prevent flickering during page transitions
63-
useCursorHide();
64-
6560
// Global exit handler
6661
useGlobalExit(setExitNotification);
6762

@@ -173,9 +168,6 @@ function AppContent({
173168
// Get terminal size for proper width calculation
174169
const {columns: terminalWidth} = useTerminalSize();
175170

176-
// Hide terminal cursor to prevent flickering during page transitions
177-
useCursorHide();
178-
179171
// Global exit handler (must be inside I18nProvider)
180172
useGlobalExit(setExitNotification);
181173

source/i18n/lang/en.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,7 @@ export const en: TranslationKeys = {
638638
hybridCompress:
639639
'Toggle Hybrid Compress mode (AI summary + smart truncation for /compact and auto-compress)',
640640
team: 'Toggle Agent Team mode - orchestrate multiple agents working together in independent Git worktrees',
641-
branch:
642-
'Fork current conversation into a new branch',
641+
branch: 'Fork current conversation into a new branch',
643642
worktree:
644643
'Open Git branch management panel for switching, creating and deleting branches',
645644
diff: 'Review file changes from a conversation in IDE diff view',
@@ -692,6 +691,17 @@ export const en: TranslationKeys = {
692691
},
693692
},
694693
},
694+
fileList: {
695+
loadingFiles: 'Loading files...',
696+
noFilesFound: 'No files found',
697+
searchingDeeper: 'Searching deeper (depth {depth})...',
698+
scanning: 'Scanning... ({count} indexed)',
699+
scanningDeeper: 'Searching deeper (depth {depth}, {count} indexed)...',
700+
contentSearchHeader: '≡ Content Search',
701+
filesHeader: '≡ Files [{mode} • Ctrl+T]',
702+
treeMode: 'Tree',
703+
listMode: 'List',
704+
},
695705
ideSelectPanel: {
696706
title: 'Select IDE',
697707
subtitle: 'Connect to an IDE for integrated development features.',
@@ -1756,6 +1766,32 @@ export const en: TranslationKeys = {
17561766
failedDisableExitImage: 'Failed to disable exit image',
17571767
setAsExitImage: 'Set "{name}" as exit image',
17581768
},
1769+
agentPickerPanel: {
1770+
title: 'Sub-Agent Selection',
1771+
noAgentsWarning:
1772+
'No sub-agents configured. Please configure sub-agents first.',
1773+
selectAgent: 'Select Sub-Agent',
1774+
escHint: '(Press ESC to close)',
1775+
noDescription: 'No description',
1776+
scrollHint: '· ↑↓ to scroll',
1777+
moreAbove: '{count} more above',
1778+
moreBelow: '{count} more below',
1779+
},
1780+
todoPickerPanel: {
1781+
title: 'TODO Selection',
1782+
scanning: 'Scanning project for TODO comments...',
1783+
noTodosFound: 'No TODO comments found in the project',
1784+
noMatchSearch: 'No TODOs match "{searchQuery}" (Total: {totalCount})',
1785+
typeToClearSearch: 'Type to filter · Backspace to clear search',
1786+
selectTodos: 'Select TODOs',
1787+
filteringLabel: 'Filtering: "{searchQuery}"',
1788+
typeToFilterHint:
1789+
'Type to filter · Backspace to clear · Space: toggle · Enter: confirm',
1790+
typeToSearchHint:
1791+
'Type to search · Space: toggle · Enter: confirm · Esc: cancel',
1792+
selectedCount: '{count} TODO(s) selected',
1793+
noDescription: 'No description',
1794+
},
17591795
exitScreen: {
17601796
title: 'Goodbye',
17611797
goodbye: 'Thanks for using Snow CLI',

source/i18n/lang/zh-TW.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,17 @@ export const zhTW: TranslationKeys = {
649649
},
650650
},
651651
},
652+
fileList: {
653+
loadingFiles: '正在載入檔案...',
654+
noFilesFound: '未找到檔案',
655+
searchingDeeper: '正在搜尋更深目錄(深度 {depth})...',
656+
scanning: '正在掃描...(已索引 {count})',
657+
scanningDeeper: '正在搜尋更深目錄(深度 {depth},已索引 {count})...',
658+
contentSearchHeader: '≡ 內容搜尋',
659+
filesHeader: '≡ 檔案 [{mode} • Ctrl+T]',
660+
treeMode: '樹狀',
661+
listMode: '清單',
662+
},
652663
ideSelectPanel: {
653664
title: '選擇 IDE',
654665
subtitle: '連線至 IDE 以使用整合開發功能。',
@@ -1662,6 +1673,29 @@ export const zhTW: TranslationKeys = {
16621673
failedDisableExitImage: '關閉結束畫面失敗',
16631674
setAsExitImage: '已將「{name}」設為結束畫面',
16641675
},
1676+
agentPickerPanel: {
1677+
title: '子代理選擇',
1678+
noAgentsWarning: '尚未配置子代理。請先配置子代理。',
1679+
selectAgent: '選擇子代理',
1680+
escHint: '(按 ESC 關閉)',
1681+
noDescription: '無描述',
1682+
scrollHint: '· ↑↓ 捲動',
1683+
moreAbove: '上方還有 {count} 項',
1684+
moreBelow: '下方還有 {count} 項',
1685+
},
1686+
todoPickerPanel: {
1687+
title: 'TODO 選擇',
1688+
scanning: '正在掃描專案中的 TODO 註釋...',
1689+
noTodosFound: '專案中未找到 TODO 註釋',
1690+
noMatchSearch: '沒有符合 "{searchQuery}" 的 TODO(總數:{totalCount})',
1691+
typeToClearSearch: '輸入以篩選 · 退格鍵清除搜尋',
1692+
selectTodos: '選擇 TODO',
1693+
filteringLabel: '篩選: "{searchQuery}"',
1694+
typeToFilterHint: '輸入篩選 · 退格清除 · 空白鍵: 切換 · Enter: 確認',
1695+
typeToSearchHint: '輸入搜尋 · 空白鍵: 切換 · Enter: 確認 · Esc: 取消',
1696+
selectedCount: '已選擇 {count} 個 TODO',
1697+
noDescription: '無描述',
1698+
},
16651699
exitScreen: {
16661700
title: '再見',
16671701
goodbye: '感謝使用 Snow CLI',

source/i18n/lang/zh.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,17 @@ export const zh: TranslationKeys = {
648648
},
649649
},
650650
},
651+
fileList: {
652+
loadingFiles: '正在加载文件...',
653+
noFilesFound: '未找到文件',
654+
searchingDeeper: '正在搜索更深目录(深度 {depth})...',
655+
scanning: '正在扫描...(已索引 {count})',
656+
scanningDeeper: '正在搜索更深目录(深度 {depth},已索引 {count})...',
657+
contentSearchHeader: '≡ 内容搜索',
658+
filesHeader: '≡ 文件 [{mode} • Ctrl+T]',
659+
treeMode: '树形',
660+
listMode: '列表',
661+
},
651662
ideSelectPanel: {
652663
title: '选择 IDE',
653664
subtitle: '连接到 IDE 以使用集成开发功能。',
@@ -1666,6 +1677,29 @@ export const zh: TranslationKeys = {
16661677
failedDisableExitImage: '关闭退出画面失败',
16671678
setAsExitImage: '已将「{name}」设为退出画面',
16681679
},
1680+
agentPickerPanel: {
1681+
title: '子代理选择',
1682+
noAgentsWarning: '未配置子代理。请先配置子代理。',
1683+
selectAgent: '选择子代理',
1684+
escHint: '(按 ESC 关闭)',
1685+
noDescription: '无描述',
1686+
scrollHint: '· ↑↓ 滚动',
1687+
moreAbove: '上方还有 {count} 项',
1688+
moreBelow: '下方还有 {count} 项',
1689+
},
1690+
todoPickerPanel: {
1691+
title: 'TODO 选择',
1692+
scanning: '正在扫描项目中的 TODO 注释...',
1693+
noTodosFound: '项目中未找到 TODO 注释',
1694+
noMatchSearch: '没有匹配 "{searchQuery}" 的 TODO(总数:{totalCount})',
1695+
typeToClearSearch: '输入以筛选 · 退格键清除搜索',
1696+
selectTodos: '选择 TODO',
1697+
filteringLabel: '筛选: "{searchQuery}"',
1698+
typeToFilterHint: '输入筛选 · 退格清除 · 空格: 切换 · 回车: 确认',
1699+
typeToSearchHint: '输入搜索 · 空格: 切换 · 回车: 确认 · Esc: 取消',
1700+
selectedCount: '已选择 {count} 个 TODO',
1701+
noDescription: '无描述',
1702+
},
16691703
exitScreen: {
16701704
title: '再见',
16711705
goodbye: '感谢使用 Snow CLI',

source/i18n/types.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ export type TranslationKeys = {
589589
hybridCompress: string;
590590
team: string;
591591
branch: string; // Fork conversation into a new branch
592-
worktree: string; // Git branch management panel
592+
worktree: string; // Git branch management panel
593593
diff: string;
594594
connect: string;
595595
disconnect: string;
@@ -636,6 +636,21 @@ export type TranslationKeys = {
636636
};
637637
};
638638
};
639+
// File search list (`@` panel)
640+
fileList: {
641+
loadingFiles: string;
642+
noFilesFound: string;
643+
// Used while a deeper rescan is queued or running
644+
searchingDeeper: string; // {depth}
645+
// Inline status while streaming results in
646+
scanning: string; // {count}
647+
scanningDeeper: string; // {depth} {count}
648+
// Header labels
649+
contentSearchHeader: string;
650+
filesHeader: string; // {mode}
651+
treeMode: string;
652+
listMode: string;
653+
};
639654
// IDE Select Panel
640655
ideSelectPanel: {
641656
title: string;
@@ -1641,6 +1656,29 @@ export type TranslationKeys = {
16411656
failedDisableExitImage: string;
16421657
setAsExitImage: string;
16431658
};
1659+
agentPickerPanel: {
1660+
title: string;
1661+
noAgentsWarning: string;
1662+
selectAgent: string;
1663+
escHint: string;
1664+
noDescription: string;
1665+
scrollHint: string;
1666+
moreAbove: string;
1667+
moreBelow: string;
1668+
};
1669+
todoPickerPanel: {
1670+
title: string;
1671+
scanning: string;
1672+
noTodosFound: string;
1673+
noMatchSearch: string;
1674+
typeToClearSearch: string;
1675+
selectTodos: string;
1676+
filteringLabel: string;
1677+
typeToFilterHint: string;
1678+
typeToSearchHint: string;
1679+
selectedCount: string;
1680+
noDescription: string;
1681+
};
16441682
exitScreen: {
16451683
title: string;
16461684
goodbye: string;

source/ui/components/chat/ChatInput.tsx

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import React, {
2-
useCallback,
32
useEffect,
43
useRef,
54
useMemo,
65
lazy,
76
Suspense,
87
} from 'react';
9-
import {Box, Text} from 'ink';
8+
import {Box, Text, useCursor} from 'ink';
109
import {Viewport} from '../../../utils/ui/textBuffer.js';
11-
import {cpSlice, visualPosToCodePoint} from '../../../utils/core/textUtils.js';
1210

1311
// Lazy load panel components to reduce initial bundle size
1412
const CommandPanel = lazy(() => import('../panels/CommandPanel.js'));
@@ -826,26 +824,8 @@ export default function ChatInput({
826824
isPureBashMode,
827825
]);
828826

829-
// Render cursor based on focus state
830-
const renderCursor = useCallback(
831-
(char: string) => {
832-
if (hasFocus) {
833-
// Focused: solid block cursor (use inverted colors)
834-
return (
835-
<Text
836-
backgroundColor={theme.colors.menuNormal}
837-
color={theme.colors.background}
838-
>
839-
{char}
840-
</Text>
841-
);
842-
} else {
843-
// Unfocused: no cursor, just render the character normally
844-
return <Text>{char}</Text>;
845-
}
846-
},
847-
[hasFocus, theme],
848-
);
827+
// Real terminal cursor via useCursor hook
828+
const {setCursorPosition, cursorRef} = useCursor();
849829

850830
// Render content with cursor (treat all text including placeholders as plain text)
851831
const INPUT_MAX_LINES = 6;
@@ -884,6 +864,16 @@ export default function ChatInput({
884864
endLine = startLine + maxLines;
885865
}
886866

867+
// Set real terminal cursor position
868+
const hasScrollUp = startLine > 0;
869+
const cursorYInContent =
870+
cursorRow - startLine + (hasScrollUp ? 1 : 0);
871+
if (hasFocus) {
872+
setCursorPosition({x: cursorCol, y: cursorYInContent});
873+
} else {
874+
setCursorPosition(undefined);
875+
}
876+
887877
const renderedLines: React.ReactNode[] = [];
888878

889879
// Scroll-up indicator
@@ -899,17 +889,9 @@ export default function ChatInput({
899889
const line = visualLines[i] || '';
900890

901891
if (i === cursorRow) {
902-
// This line contains the cursor
903-
const cursorIndex = visualPosToCodePoint(line, cursorCol);
904-
const beforeCursor = cpSlice(line, 0, cursorIndex);
905-
const atCursor = cpSlice(line, cursorIndex, cursorIndex + 1) || ' ';
906-
const afterCursor = cpSlice(line, cursorIndex + 1);
907-
908892
renderedLines.push(
909893
<Box key={i} flexDirection="row">
910-
<Text>{beforeCursor}</Text>
911-
{renderCursor(atCursor)}
912-
<Text>{afterCursor}</Text>
894+
<Text>{line || ' '}</Text>
913895
{commandArgsHint && i === visualLines.length - 1 ? (
914896
<Text color={theme.colors.menuSecondary} dimColor>
915897
{commandArgsHint}
@@ -918,7 +900,6 @@ export default function ChatInput({
918900
</Box>,
919901
);
920902
} else {
921-
// No cursor in this line
922903
renderedLines.push(<Text key={i}>{line || ' '}</Text>);
923904
}
924905
}
@@ -937,13 +918,17 @@ export default function ChatInput({
937918

938919
return <Box flexDirection="column">{renderedLines}</Box>;
939920
} else {
921+
// Empty input: cursor at start
922+
if (hasFocus) {
923+
setCursorPosition({x: 0, y: 0});
924+
} else {
925+
setCursorPosition(undefined);
926+
}
927+
940928
return (
941-
<>
942-
{renderCursor(' ')}
943-
<Text color={theme.colors.menuSecondary} dimColor>
944-
{disabled ? t.chatScreen.waitingForResponse : placeholder}
945-
</Text>
946-
</>
929+
<Text color={theme.colors.menuSecondary} dimColor>
930+
{disabled ? t.chatScreen.waitingForResponse : placeholder}
931+
</Text>
947932
);
948933
}
949934
};
@@ -997,7 +982,9 @@ export default function ChatInput({
997982
? '⤢'
998983
: '❯'}{' '}
999984
</Text>
1000-
<Box flexGrow={1}>{renderContent()}</Box>
985+
<Box ref={cursorRef} flexGrow={1}>
986+
{renderContent()}
987+
</Box>
1001988
</Box>
1002989
<Box flexDirection="row">
1003990
<Text

0 commit comments

Comments
 (0)