1- import React , { useState } from "react" ;
2- import { Box , Text , useInput } from "ink" ;
3- import type { SessionEntry } from "../session" ;
1+ import React , { useState , useMemo } from "react" ;
2+ import { Box , Text , useInput , useWindowSize } from "ink" ;
3+ import type { SessionEntry } from "../session" ;
44
55type Props = {
66 sessions : SessionEntry [ ] ;
77 onSelect : ( sessionId : string ) => void ;
88 onCancel : ( ) => void ;
99} ;
1010
11- export function SessionList ( { sessions, onSelect, onCancel } : Props ) : React . ReactElement {
11+ export function SessionList ( { sessions, onSelect, onCancel} : Props ) : React . ReactElement {
1212 const [ index , setIndex ] = useState ( 0 ) ;
13+ const { columns, rows} = useWindowSize ( ) ;
14+
15+ // 根据终端高度动态计算可见的会话数量
16+ const maxVisibleSessions = useMemo ( ( ) => {
17+ // 减去边框、标题、页脚、滚动指示器等占用的空间
18+ // 外层容器 height=rows-1,外边框2 + header1 + 内边框2 + footer1 + 滚动指示器1 = 8
19+ const reservedLines = 8 ;
20+ const linesPerSession = 3 ; // height=2 + marginBottom=1
21+ const availableLines = Math . max ( 0 , rows - reservedLines ) ;
22+ return Math . max ( 1 , Math . floor ( availableLines / linesPerSession ) ) ;
23+ } , [ rows ] ) ;
24+
25+ // 确保index在有效范围内
26+ const safeIndex = useMemo ( ( ) => {
27+ if ( sessions . length === 0 ) return 0 ;
28+ return Math . max ( 0 , Math . min ( index , sessions . length - 1 ) ) ;
29+ } , [ index , sessions . length ] ) ;
30+
31+ // 计算滚动偏移量,确保选中的项目始终可见
32+ const scrollOffset = useMemo ( ( ) => {
33+ if ( safeIndex < maxVisibleSessions ) return 0 ;
34+ return safeIndex - maxVisibleSessions + 1 ;
35+ } , [ safeIndex , maxVisibleSessions ] ) ;
36+
37+ // 获取当前可见的会话列表
38+ const visibleSessions = useMemo ( ( ) => {
39+ return sessions . slice ( scrollOffset , scrollOffset + maxVisibleSessions ) ;
40+ } , [ sessions , scrollOffset , maxVisibleSessions ] ) ;
1341
1442 useInput ( ( input , key ) => {
1543 if ( key . escape || ( key . ctrl && ( input === "c" || input === "C" ) ) ) {
1644 onCancel ( ) ;
1745 return ;
1846 }
47+ if ( sessions . length === 0 ) {
48+ return ;
49+ }
1950 if ( key . upArrow ) {
2051 setIndex ( ( i ) => Math . max ( 0 , i - 1 ) ) ;
2152 return ;
@@ -24,8 +55,24 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
2455 setIndex ( ( i ) => Math . min ( sessions . length - 1 , i + 1 ) ) ;
2556 return ;
2657 }
58+ if ( key . pageUp ) {
59+ setIndex ( ( i ) => Math . max ( 0 , i - maxVisibleSessions ) ) ;
60+ return ;
61+ }
62+ if ( key . pageDown ) {
63+ setIndex ( ( i ) => Math . min ( sessions . length - 1 , i + maxVisibleSessions ) ) ;
64+ return ;
65+ }
66+ if ( key . home ) {
67+ setIndex ( 0 ) ;
68+ return ;
69+ }
70+ if ( key . end ) {
71+ setIndex ( sessions . length - 1 ) ;
72+ return ;
73+ }
2774 if ( key . return ) {
28- const session = sessions [ index ] ;
75+ const session = sessions [ safeIndex ] ;
2976 if ( session ) {
3077 onSelect ( session . id ) ;
3178 }
@@ -42,19 +89,54 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
4289 }
4390
4491 return (
45- < Box flexDirection = "column" >
46- < Text bold color = "cyanBright" > Resume a session</ Text >
47- { sessions . slice ( 0 , 30 ) . map ( ( session , i ) => (
48- < Text key = { session . id } color = { i === index ? "cyanBright" : undefined } >
49- { i === index ? "› " : " " }
50- < Text dimColor > { formatTimestamp ( session . updateTime ) } </ Text >
51- < Text > { formatSessionTitle ( session . summary || "Untitled" ) } </ Text >
52- < Text dimColor > ({ session . status } )</ Text >
53- </ Text >
54- ) ) }
55- { sessions . length > 30 ? < Text dimColor > … { sessions . length - 30 } older sessions hidden.</ Text > : null }
56- < Box marginTop = { 1 } >
57- < Text dimColor > ↑/↓ to navigate · Enter to select · Esc to cancel</ Text >
92+ < Box
93+ flexDirection = "column"
94+ width = { columns - 6 }
95+ height = { rows - 1 }
96+ overflow = "hidden"
97+ paddingX = { 1 }
98+ marginTop = { 1 }
99+ >
100+ < Box flexDirection = "column" borderStyle = 'round' borderDimColor flexGrow = { 1 } overflow = "hidden" >
101+ { /* Header row */ }
102+ < Box paddingX = { 1 } > < Text bold color = "#229ac3" > Resume a session ({ sessions . length } total)</ Text > </ Box >
103+ { /* Session list */ }
104+ < Box borderTop = { true } borderBottom = { true } borderLeft = { false } borderRight = { false } borderStyle = 'round'
105+ borderDimColor flexDirection = 'column' flexGrow = { 1 } paddingX = { 1 } overflow = "hidden" >
106+ { visibleSessions . map ( ( session , i ) => {
107+ const actualIndex = scrollOffset + i ;
108+ return (
109+ < Box key = { session . id } height = { 2 } marginBottom = { 1 } >
110+ < Box >
111+ < Text color = '#229ac3' >
112+ { actualIndex === safeIndex ? "› " : " " }
113+ </ Text >
114+ </ Box >
115+ < Box flexDirection = 'column' flexGrow = { 1 } >
116+ < Box width = { '100%' } >
117+ < Text { ...( actualIndex === safeIndex ? { bold : true } : { } ) } color = { actualIndex === safeIndex ? "#229ac3" : undefined } >
118+ { formatSessionTitle ( session . summary || "Untitled" ) }
119+ </ Text >
120+ < Text dimColor > ({ session . status } )</ Text >
121+ </ Box >
122+ < Box width = '100%' >
123+ < Text dimColor > { formatTimestamp ( session . updateTime ) } </ Text >
124+ </ Box >
125+ </ Box >
126+ </ Box >
127+ ) ;
128+ } ) }
129+ { ( scrollOffset > 0 || scrollOffset + maxVisibleSessions < sessions . length ) ? (
130+ < Box marginTop = { 1 } >
131+ { scrollOffset > 0 ? < Text dimColor > … { scrollOffset } newer sessions above. </ Text > : null }
132+ { scrollOffset + maxVisibleSessions < sessions . length ? < Text dimColor > … { sessions . length - scrollOffset - maxVisibleSessions } older sessions below.</ Text > : null }
133+ </ Box >
134+ ) : null }
135+ </ Box >
136+ { /* Footer */ }
137+ < Box >
138+ < Text dimColor > ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel</ Text >
139+ </ Box >
58140 </ Box >
59141 </ Box >
60142 ) ;
0 commit comments