({
onSelectChat,
onCancel,
onNewChat,
})
| 37 | } |
| 38 | |
| 39 | export const ChatHistoryScreen: React.FC<ChatHistoryScreenProps> = ({ |
| 40 | onSelectChat, |
| 41 | onCancel, |
| 42 | onNewChat, |
| 43 | }) => { |
| 44 | const theme = useTheme() |
| 45 | const { terminalWidth, terminalHeight } = useTerminalLayout() |
| 46 | |
| 47 | // Layout calculations - use full width |
| 48 | const contentWidth = terminalWidth - LAYOUT.CONTENT_PADDING |
| 49 | |
| 50 | // Two-phase loading: load initial chats immediately, then more in background |
| 51 | const [chats, setChats] = useState(() => getAllChats(LAYOUT.INITIAL_CHATS)) |
| 52 | const [statusMessage, setStatusMessage] = useState<string | null>(null) |
| 53 | |
| 54 | // Load more chats in the background after initial render |
| 55 | useEffect(() => { |
| 56 | // Use setTimeout to defer the expensive loading to after first paint |
| 57 | const timer = setTimeout(() => { |
| 58 | setChats(getAllChats(LAYOUT.INITIAL_CHATS + LAYOUT.BACKGROUND_CHATS)) |
| 59 | }, 0) |
| 60 | return () => clearTimeout(timer) |
| 61 | }, []) |
| 62 | |
| 63 | const handleDeleteChat = useCallback((chatId: string) => { |
| 64 | const deleted = deleteChatSession(chatId) |
| 65 | if (deleted) { |
| 66 | setChats((prev) => prev.filter((chat) => chat.chatId !== chatId)) |
| 67 | setStatusMessage('Chat deleted') |
| 68 | return |
| 69 | } |
| 70 | |
| 71 | setStatusMessage('Could not delete chat') |
| 72 | }, []) |
| 73 | |
| 74 | // Calculate available width for the prompt text (last column, variable width) |
| 75 | // Format: "[time] [msgs] [prompt...] [×]" |
| 76 | // reservedWidth accounts for: time col, msgs col, delete button area, |
| 77 | // 2 gaps between columns, list border (2), scrollbar (1), and button padding (2) |
| 78 | const reservedWidth = |
| 79 | LAYOUT.TIME_COL_WIDTH + |
| 80 | LAYOUT.MSGS_COL_WIDTH + |
| 81 | LAYOUT.DELETE_COL_WIDTH + |
| 82 | LAYOUT.GAP_WIDTH * 2 + |
| 83 | 5 // border + scrollbar + button padding |
| 84 | const maxPromptWidth = Math.max(20, contentWidth - reservedWidth) |
| 85 | |
| 86 | // Truncate text to fit single line |
| 87 | const truncateText = (text: string, maxLen: number): string => { |
| 88 | const singleLine = text.replace(/\n/g, ' ').trim() |
| 89 | if (singleLine.length <= maxLen) return singleLine |
| 90 | return singleLine.slice(0, maxLen - 1) + '…' |
| 91 | } |
| 92 | |
| 93 | // Pad text to fixed width (right-pad with spaces) |
| 94 | const padRight = (text: string, width: number): string => { |
| 95 | // Use Array.from to count code points so emoji/wide chars don't break padding |
| 96 | const len = Array.from(text).length |
nothing calls this directly
no test coverage detected