/** * @license * Copyright 2025 Qwen Code * SPDX-License-Identifier: Apache-2.0 */ import { useState, useEffect } from 'react'; import { render, Box, Text, useApp } from 'ink'; import { SessionService, getGitBranch } from '@qwen-code/qwen-code-core'; import { theme } from '../semantic-colors.js'; import { useSessionPicker } from '../hooks/useSessionPicker.js'; import { SessionListItemView } from './SessionListItem.js'; // Exported for testing export interface SessionPickerProps { sessionService: SessionService; currentBranch?: string; onSelect: (sessionId: string) => void; onCancel: () => void; } // Prefix characters for standalone fullscreen picker const STANDALONE_PREFIX_CHARS = { selected: '› ', scrollUp: '↑ ', scrollDown: '↓ ', normal: ' ', }; // Exported for testing export function SessionPicker({ sessionService, currentBranch, onSelect, onCancel, }: SessionPickerProps): React.JSX.Element { const { exit } = useApp(); const [isExiting, setIsExiting] = useState(false); const [terminalSize, setTerminalSize] = useState({ width: process.stdout.columns || 80, height: process.stdout.rows || 24, }); // Update terminal size on resize useEffect(() => { const handleResize = () => { setTerminalSize({ width: process.stdout.columns || 80, height: process.stdout.rows || 24, }); }; process.stdout.on('resize', handleResize); return () => { process.stdout.off('resize', handleResize); }; }, []); // Calculate visible items // Reserved space: header (1), footer (1), separators (2), borders (2) const reservedLines = 6; // Each item takes 2 lines (prompt + metadata) + 1 line margin between items const itemHeight = 3; const maxVisibleItems = Math.max( 1, Math.floor((terminalSize.height - reservedLines) / itemHeight), ); const handleExit = () => { setIsExiting(true); exit(); }; const picker = useSessionPicker({ sessionService, currentBranch, onSelect, onCancel, maxVisibleItems, centerSelection: true, onExit: handleExit, isActive: !isExiting, }); // Calculate content width (terminal width minus border padding) const contentWidth = terminalSize.width - 4; const promptMaxWidth = contentWidth - 4; // Return empty while exiting to prevent visual glitches if (isExiting) { return ; } return ( {/* Main container with single border */} {/* Header row */} Resume Session {/* Separator line */} {'─'.repeat(terminalSize.width - 2)} {/* Session list with auto-scrolling */} {picker.filteredSessions.length === 0 ? ( {picker.filterByBranch ? `No sessions found for branch "${currentBranch}"` : 'No sessions found'} ) : ( picker.visibleSessions.map((session, visibleIndex) => { const actualIndex = picker.scrollOffset + visibleIndex; return ( ); }) )} {/* Separator line */} {'─'.repeat(terminalSize.width - 2)} {/* Footer with keyboard shortcuts */} {currentBranch && ( <> B {' to toggle branch · '} )} {'↑↓ to navigate · Esc to cancel'} ); } /** * Clears the terminal screen. */ function clearScreen(): void { // Move cursor to home position and clear screen process.stdout.write('\x1b[2J\x1b[H'); } /** * Shows an interactive session picker and returns the selected session ID. * Returns undefined if the user cancels or no sessions are available. */ export async function showResumeSessionPicker( cwd: string = process.cwd(), ): Promise { const sessionService = new SessionService(cwd); const hasSession = await sessionService.loadLastSession(); if (!hasSession) { console.log('No sessions found. Start a new session with `qwen`.'); return undefined; } const currentBranch = getGitBranch(cwd); // Clear the screen before showing the picker for a clean fullscreen experience clearScreen(); // Enable raw mode for keyboard input if not already enabled const wasRaw = process.stdin.isRaw; if (process.stdin.isTTY && !wasRaw) { process.stdin.setRawMode(true); } return new Promise((resolve) => { let selectedId: string | undefined; const { unmount, waitUntilExit } = render( { selectedId = id; }} onCancel={() => { selectedId = undefined; }} />, { exitOnCtrlC: false, }, ); waitUntilExit().then(() => { unmount(); // Clear the screen after the picker closes for a clean fullscreen experience clearScreen(); // Restore raw mode state only if we changed it and user cancelled // (if user selected a session, main app will handle raw mode) if (process.stdin.isTTY && !wasRaw && !selectedId) { process.stdin.setRawMode(false); } resolve(selectedId); }); }); }