@@ -34,6 +34,8 @@ interface InteractiveListProps<T> {
3434 onQuit : ( ) => void ;
3535 /** Called when user presses r to retry */
3636 onRetry ?: ( ) => void ;
37+ /** Called when user presses s to summarize the current item */
38+ onSummarize ?: ( item : T | null ) => void ;
3739 /** Render inline preview for selected item (narrow mode) */
3840 renderPreview ?: ( item : T ) => string | null ;
3941 /** Render side panel preview (wide mode). Receives available width for truncation. */
@@ -57,7 +59,7 @@ const PREVIEW_LINES = 3;
5759 */
5860export function InteractiveList < T > ( {
5961 items, columns, total, loading, error, hasMore,
60- onLoadMore, onSelect, onSearch, onQuit, onRetry,
62+ onLoadMore, onSelect, onSearch, onQuit, onRetry, onSummarize ,
6163 renderPreview, renderSidePreview,
6264 extraHints, title, keyboardActive = true ,
6365} : InteractiveListProps < T > ) {
@@ -66,8 +68,11 @@ export function InteractiveList<T>({
6668 const { columns : termCols , rows : termRows , isWide } = useTerminalSize ( ) ;
6769 const t = getLocale ( ) ;
6870
71+ // Only use dual-pane layout when wide AND side preview is provided
72+ const useSideLayout = isWide && ! ! renderSidePreview ;
73+
6974 // Calculate viewport height
70- const viewportHeight = Math . max ( 3 , termRows - CHROME_LINES - ( isWide ? 0 : PREVIEW_LINES ) ) ;
75+ const viewportHeight = Math . max ( 3 , termRows - CHROME_LINES - ( useSideLayout ? 0 : PREVIEW_LINES ) ) ;
7176
7277 // C2 fix: refs for values read in handleAction to avoid stale closures
7378 const cursorRef = useRef ( cursor ) ;
@@ -125,8 +130,11 @@ export function InteractiveList<T>({
125130 case 'retry' :
126131 onRetry ?.( ) ;
127132 break ;
133+ case 'summarize' :
134+ onSummarize ?.( currentItems . length > 0 ? currentItems [ cursorRef . current ] : null ) ;
135+ break ;
128136 }
129- } , [ viewportHeight , onSelect , onSearch , onQuit , onRetry ] ) ;
137+ } , [ viewportHeight , onSelect , onSearch , onQuit , onRetry , onSummarize ] ) ;
130138
131139 useKeyboard ( { active : keyboardActive , onAction : handleAction } ) ;
132140
@@ -144,7 +152,7 @@ export function InteractiveList<T>({
144152 const selectedItem = items [ cursor ] ?? null ;
145153
146154 // Available width for list content (prefix " ▸ " = 3 chars)
147- const listPanelWidth = isWide ? Math . floor ( termCols * 0.4 ) : termCols ;
155+ const listPanelWidth = useSideLayout ? Math . floor ( termCols * 0.4 ) : termCols ;
148156 const availableListWidth = listPanelWidth - 3 ;
149157
150158 // Column layout: flex column (no width) fills remaining space.
@@ -155,16 +163,26 @@ export function InteractiveList<T>({
155163 // Find the flex column (first column without explicit width)
156164 const flexIdx = columns . findIndex ( c => ! c . width ) ;
157165
158- // Start with all columns, drop rightmost fixed columns until flex has enough space
166+ // Start with all columns, drop rightmost fixed columns until content fits
159167 let cols = [ ...columns ] ;
168+ const calcTotalWidth = ( arr : typeof columns ) => {
169+ const fixedSum = arr . reduce ( ( s , c ) => s + ( c . width || 10 ) , 0 ) ;
170+ const gaps = ( arr . length - 1 ) * 2 ;
171+ return fixedSum + gaps ;
172+ } ;
160173 const calcFlexWidth = ( arr : typeof columns ) => {
161174 const fixedSum = arr . reduce ( ( s , c , i ) => i === flexIdx ? s : s + ( c . width || 10 ) , 0 ) ;
162175 const gaps = ( arr . length - 1 ) * 2 ;
163176 return availableListWidth - fixedSum - gaps ;
164177 } ;
165178
166179 // Drop from the right, but never drop the flex column
167- while ( cols . length > 1 && ( flexIdx < 0 || calcFlexWidth ( cols ) < MIN_FLEX_WIDTH ) ) {
180+ const shouldDrop = ( ) => {
181+ if ( flexIdx >= 0 ) return calcFlexWidth ( cols ) < MIN_FLEX_WIDTH ;
182+ // All fixed-width columns: drop when total exceeds available width
183+ return calcTotalWidth ( cols ) > availableListWidth ;
184+ } ;
185+ while ( cols . length > 1 && shouldDrop ( ) ) {
168186 // Find rightmost droppable column (not the flex column)
169187 let dropIdx = - 1 ;
170188 for ( let i = cols . length - 1 ; i >= 0 ; i -- ) {
@@ -213,7 +231,7 @@ export function InteractiveList<T>({
213231
214232 // Inline preview for narrow mode — fixed height, pinned at bottom
215233 function renderInlinePreview ( ) {
216- if ( isWide || ! renderPreview ) return null ;
234+ if ( useSideLayout || ! renderPreview ) return null ;
217235 const previewText = selectedItem ? renderPreview ( selectedItem ) : null ;
218236 const previewContentWidth = termCols - 2 ; // 1 char padding each side
219237 const sepLine = '┄' . repeat ( previewContentWidth ) ;
@@ -231,7 +249,7 @@ export function InteractiveList<T>({
231249 }
232250
233251 // Wide mode: side-by-side layout
234- if ( isWide && renderSidePreview ) {
252+ if ( useSideLayout ) {
235253 const listWidth = Math . floor ( termCols * 0.4 ) ;
236254 const previewWidth = termCols - listWidth - 3 ;
237255
0 commit comments