# Phase 8 Task 8.5: Nested ListItem Visual Component Implementation
**Status**: ✅ COMPLETED
**Discovered**: 2025-07-15
**Completed**: 2025-07-19
**Priority**: HIGH - Blocking folder setup wizards and complex interfaces
## Overview
Create a `ContainerListItem` that can display other `IListItem` instances inside it when expanded, with proper responsive behavior that mimics `GenericListPanel`'s scrolling and input delegation.
**This is a pure visual component architecture task** - NOT business logic, model recommendations, or wizard features.
## Technical Research Summary
### Current Architecture Analysis
#### GenericListPanel's Responsibility (Container)
- **Space Allocation**: Calculates `itemMaxLines = remainingLines` for each item
- **Viewport Scrolling**: Tracks line positions and scrolls items into view
- **Input Delegation**: Routes input to `selectedItem.handleInput()` when `isControllingInput = true`
- **Truncation**: Items that don't fit are scrolled out of view
#### IListItem's Responsibility (Items)
- **Reports space needed**: `getRequiredLines(maxWidth)` tells container how much space needed
- **Handles internal scrolling**: Uses `maxLines` parameter to implement internal scrolling
- **Self-constrains width**: Truncates content to fit `maxWidth`
- **Fixed height strategy**: FilePickerListItem shows "max 4 items" then scrolls internally
### Key Discovery: Fixed Height Strategy
**FilePickerListItem Pattern**:
```typescript
getRequiredLines(maxWidth: number): number {
if (!this._isControllingInput) return 1; // Collapsed
// Fixed height: header + path + max 4 items + confirm
return 2 + Math.min(regularItems.length, 4) + confirmLines;
}
```
**Then internally in FilePickerBody**:
```typescript
if (regularItems.length > maxLines) {
// Internal scrolling - show subset of items
const visibleItems = regularItems.slice(startIndex, startIndex + maxLines);
showScrollUp = startIndex > 0;
showScrollDown = startIndex + maxLines < regularItems.length;
}
```
### Input Flow Architecture
**Current**: `GenericListPanel` → `ActiveItem.handleInput()`
**Needed**: `GenericListPanel` → `ContainerListItem` → `ActiveChildItem.handleInput()`
## Implementation Design
### ContainerListItem Interface
```typescript
class ContainerListItem implements IListItem {
readonly selfConstrained = true as const;
private _isControllingInput: boolean = false;
private _childItems: IListItem[] = [];
private _childSelectedIndex: number = 0;
private _childScrollOffset: number = 0;
private _childLinePositions: Array<{start: number, end: number}> = [];
constructor(
public icon: string,
private label: string,
private childItems: IListItem[],
private onComplete?: (results: any) => void
) {
this._childItems = [...childItems];
}
get isControllingInput(): boolean {
return this._isControllingInput;
}
// Core IListItem methods
render(maxWidth: number, maxLines?: number): ReactElement | ReactElement[];
getRequiredLines(maxWidth: number): number;
handleInput(input: string, key: Key): boolean;
onEnter(): void;
onExit(): void;
}
```
### Fixed Height Strategy
```typescript
getRequiredLines(maxWidth: number): number {
if (!this._isControllingInput) {
return 1; // Collapsed view
}
// FIXED HEIGHT - prevents infinite recursion
// Similar to FilePickerListItem's approach
return 8; // Always request 8 lines, scroll internally if needed
}
```
### Internal Scrolling Logic (Copy from GenericListPanel)
```typescript
private calculateChildViewport(availableLines: number): {
visibleChildren: IListItem[],
scrollOffset: number,
showScrollUp: boolean,
showScrollDown: boolean
} {
// Step 1: Calculate line positions for all children
this._childLinePositions = [];
let currentLine = 0;
for (let i = 0; i < this._childItems.length; i++) {
const child = this._childItems[i];
const childLines = child.getRequiredLines ? child.getRequiredLines(maxWidth - 2) : 1;
this._childLinePositions.push({
start: currentLine,
end: currentLine + childLines
});
currentLine += childLines;
}
// Step 2: Calculate scroll offset to keep selected child visible
const selectedPosition = this._childLinePositions[this._childSelectedIndex];
if (selectedPosition) {
// Keep selected child visible (copy GenericListPanel logic lines 149-156)
if (selectedPosition.end > this._childScrollOffset + availableLines) {
// Item is cut off at bottom - scroll down
this._childScrollOffset = selectedPosition.end - availableLines;
} else if (selectedPosition.start < this._childScrollOffset) {
// Item is cut off at top - scroll up
this._childScrollOffset = selectedPosition.start;
}
}
// Step 3: Find visible children based on scroll offset
const visibleChildren: IListItem[] = [];
for (let i = 0; i < this._childItems.length; i++) {
const pos = this._childLinePositions[i];
if (pos && pos.end > this._childScrollOffset && pos.start < this._childScrollOffset + availableLines) {
visibleChildren.push(this._childItems[i]);
}
}
return {
visibleChildren,
scrollOffset: this._childScrollOffset,
showScrollUp: this._childScrollOffset > 0,
showScrollDown: this._childScrollOffset + availableLines < currentLine
};
}
```
### Render Implementation
```typescript
render(maxWidth: number, maxLines?: number): ReactElement | ReactElement[] {
if (!this._isControllingInput) {
// Collapsed view
return (
<Text>
<Text color={this.isActive ? theme.colors.accent : theme.colors.textMuted}>
{this.icon}
</Text>
<Text color={this.isActive ? theme.colors.accent : undefined}>
{' '}{this.label}
</Text>
</Text>
);
}
// Expanded view
const elements: ReactElement[] = [];
const availableLines = (maxLines || 8) - 2; // Reserve header + footer
// Calculate viewport
const viewport = this.calculateChildViewport(availableLines);
// Header
elements.push(
<Text key="header" color={theme.colors.accent}>
■ {this.label}
</Text>
);
// Render visible children
viewport.visibleChildren.forEach((child, index) => {
// Set active state
const isChildSelected = this._childItems.indexOf(child) === this._childSelectedIndex;
child.isActive = isChildSelected;
// Calculate available lines for this child
const childPosition = this._childLinePositions[this._childItems.indexOf(child)];
const childMaxLines = childPosition ? childPosition.end - childPosition.start : 1;
// Render child with indentation
const childElements = child.render(maxWidth - 2, childMaxLines);
if (Array.isArray(childElements)) {
childElements.forEach((element, elemIndex) => {
elements.push(
<Box key={`child-${index}-${elemIndex}`} marginLeft={1}>
{element}
</Box>
);
});
} else {
elements.push(
<Box key={`child-${index}`} marginLeft={1}>
{childElements}
</Box>
);
}
});
// Scroll indicators
if (viewport.showScrollUp) {
elements.push(
<Text key="scroll-up" color={theme.colors.textMuted}>
↑ More items above
</Text>
);
}
if (viewport.showScrollDown) {
elements.push(
<Text key="scroll-down" color={theme.colors.textMuted}>
↓ More items below
</Text>
);
}
return elements;
}
```
### Input Delegation System
```typescript
handleInput(input: string, key: Key): boolean {
if (!this._isControllingInput) return false;
const activeChild = this._childItems[this._childSelectedIndex];
// Priority 1: If child is controlling input, delegate to it
if (activeChild?.isControllingInput && activeChild.handleInput) {
return activeChild.handleInput(input, key);
}
// Priority 2: Handle navigation between children
if (key.upArrow) {
this._childSelectedIndex = Math.max(0, this._childSelectedIndex - 1);
return true;
}
if (key.downArrow) {
this._childSelectedIndex = Math.min(this._childItems.length - 1, this._childSelectedIndex + 1);
return true;
}
if (key.return && activeChild?.onEnter) {
activeChild.onEnter(); // Child takes control
return true;
}
if (key.escape) {
this.onExit(); // Exit container
return true;
}
return false;
}
```
### Focus Management
```typescript
onEnter(): void {
this._isControllingInput = true;
// Initialize child selection
if (this._childSelectedIndex >= this._childItems.length) {
this._childSelectedIndex = 0;
}
// Set initial active states
this._childItems.forEach((child, index) => {
child.isActive = (index === this._childSelectedIndex);
});
}
onExit(): void {
this._isControllingInput = false;
// Clean up child states
this._childItems.forEach(child => {
child.isActive = false;
if (child.isControllingInput && child.onExit) {
child.onExit();
}
});
}
```
## Implementation Phases
### Phase 1: Basic Structure (Proof of Concept)
**Goal**: Create ContainerListItem that can show/hide child items
**Files to Create**:
- `src/interfaces/tui-ink/components/core/ContainerListItem.tsx`
- `src/interfaces/tui-ink/components/core/TextListItem.tsx` (simple test child)
**Test Setup**:
```typescript
const testChildren = [
new TextListItem("·", "What language is your content?", "English"),
new TextListItem("·", "What type of content?", "Documents"),
new TextListItem("·", "Select folder", "/Users/me/Documents")
];
const containerItem = new ContainerListItem("📁", "Add Folder", testChildren);
```
**Success Criteria**:
- Shows "📁 Add Folder" when collapsed
- Shows 3 child items when expanded
- Handles ↑↓ navigation between children
- Properly handles Enter/Escape
### Phase 2: Internal Scrolling
**Goal**: Implement viewport scrolling when children exceed available space
**Implementation**:
- Add `calculateChildViewport()` method
- Implement line position tracking
- Add scroll indicators
- Test with 10+ children in limited space
**Success Criteria**:
- Shows scroll indicators when needed
- Keeps selected child visible
- Scrolls smoothly with arrow keys
- Respects available space limits
### Phase 3: Input Delegation
**Goal**: Properly delegate input to child items
**Implementation**:
- Add child `isControllingInput` detection
- Implement input delegation chain
- Handle child onEnter/onExit events
- Test with interactive children (SelectionListItem)
**Success Criteria**:
- Child items can take control of input
- Navigation works at both levels
- Proper focus management
- Clean state transitions
### Phase 4: Integration Testing
**Goal**: Integrate with existing GenericListPanel
**Implementation**:
- Add ContainerListItem to main app
- Test alongside other ListItems
- Verify focus management
- Test terminal resizing
**Success Criteria**:
- Works in real application
- No conflicts with other items
- Proper responsive behavior
- Smooth user experience
## Technical Challenges & Solutions
### Challenge 1: Infinite Recursion
**Problem**: Child items calling `getRequiredLines()` could cause infinite recursion
**Solution**: Use fixed height strategy like FilePickerListItem
### Challenge 2: Input Focus Chain
**Problem**: Input needs to flow through multiple levels
**Solution**: Priority-based input delegation with state tracking
### Challenge 3: Viewport Calculation
**Problem**: Need to mimic GenericListPanel's complex scrolling logic
**Solution**: Copy exact algorithms from GenericListPanel with adaptations
### Challenge 4: State Management
**Problem**: Multiple levels of active/selected/controlling states
**Solution**: Clear state hierarchy with proper cleanup
## Test Cases
### Basic Functionality
- [x] Collapsed state shows single line
- [x] Expanded state shows child items
- [x] Navigation between children works
- [x] Enter/Escape handling works
### Scrolling Behavior
- [x] Shows scroll indicators when needed
- [x] Keeps selected child visible
- [x] Handles terminal resize
- [x] Works with varying child heights
### Input Delegation
- [x] Child items can take control
- [x] Navigation works at both levels
- [x] Proper state cleanup
- [x] No input conflicts
### Integration
- [x] Works in GenericListPanel
- [x] No conflicts with other items
- [x] Proper focus management
- [x] Responsive behavior
## Success Metrics
1. **Functionality**: All basic operations work correctly
2. **Performance**: No noticeable lag with 20+ children
3. **Usability**: Intuitive navigation and interaction
4. **Stability**: No crashes or state corruption
5. **Integration**: Seamless with existing codebase
## Future Extensions
Once the basic ContainerListItem is working, it can be extended for:
- **Folder Setup Wizards**: Multi-step configuration flows
- **Complex Forms**: Nested input validation
- **Tree Navigation**: Hierarchical data structures
- **Dynamic Content**: Runtime child addition/removal
This provides the visual architecture foundation for all advanced TUI interfaces in the application.
---
# ContainerListItem Comprehensive Redesign Plan
**Status**: 🟡 IN PROGRESS
**Priority**: HIGH - Current implementation has fundamental architectural issues
**Discovered**: 2025-07-18 (After extensive debugging revealed systemic problems)
## Problem Analysis
The current ContainerListItem implementation is fragmented with scroll indicator and viewport management issues. Extensive debugging sessions revealed that the approach is fundamentally flawed - trying to patch scroll indicators and viewport calculations during rendering creates a cascade of complex interdependencies that are impossible to maintain.
## Core Architecture Issues
### 1. **Fragmented Viewport Logic**
- Width/height calculations scattered across multiple methods
- Inconsistent line counting between different calculation paths
- Viewport boundaries computed reactively instead of proactively
### 2. **Complex Scroll Indicator Logic**
- Scroll indicators computed during rendering loop
- No centralized overflow detection
- Conflicting logic between different rendering paths (array vs single elements)
### 3. **Reactive Patching Approach**
- Each fix introduces new complexity
- No separation of concerns between scroll state and rendering
- Debugging becomes exponentially more difficult
### 4. **Missing Central State Management**
- No unified viewport state
- Scroll offset and visibility calculated independently
- Element positioning logic duplicated across methods
## Comprehensive Redesign Strategy
### Phase 1: Viewport Foundation System
**Goal**: Create unified viewport management with clear boundaries
**Implementation**:
- [ ] Create `ViewportCalculator` class
- Calculates available width/height based on parent constraints
- Reserves space for header (1 line) and confirmation (1 line)
- Determines actual content viewport dimensions
- Tracks total content height vs available viewport height
**Success Criteria**:
- Single source of truth for viewport dimensions
- Clear separation between allocated space and content space
- Proper boundary detection for overflow conditions
### Phase 2: Centralized Scroll State Management
**Goal**: Implement scroll state manager completely separate from rendering
**Implementation**:
- [ ] Create `ScrollStateManager` class
- Tracks current scroll offset
- Determines overflow conditions (top/bottom)
- Manages scroll indicators (`│▲` and `│▼`) based on pure overflow state
- Provides scroll commands (scrollUp, scrollDown, scrollToElement)
**Success Criteria**:
- Scroll indicators work correctly based on overflow state only
- Clean separation between scroll logic and rendering logic
- Scroll state can be tested independently
### Phase 3: Element Visibility Engine
**Goal**: Create smart element visibility calculator with proper text wrapping
**Implementation**:
- [ ] Create `ElementVisibilityCalculator` class
- Determines which elements are visible in current viewport
- Handles text wrapping using actual `getRequiredLines()` calls
- Calculates element positions in viewport coordinates
- Manages element clipping when partially visible
**Success Criteria**:
- Accurate element visibility detection
- Proper text wrapping calculations
- Element clipping works correctly for partial visibility
### Phase 4: Bring-Into-View Navigation Logic
**Goal**: Implement intelligent navigation separate from rendering
**Implementation**:
- [ ] Implement bring-into-view algorithms
- Skip text-only elements during navigation
- Bring elements to topmost line when scrolling down
- Bring elements to bottom line when scrolling up
- Handle edge cases (first/last elements)
**Success Criteria**:
- Smooth navigation that always keeps selected element visible
- Intelligent element skipping
- Proper positioning for different scroll directions
### Phase 5: Subitem Expansion Management
**Goal**: Create expansion-aware viewport that handles complex expansion scenarios
**Implementation**:
- [ ] Add subitem expansion handling
- Calculate expanded item dimensions
- Ensure bottom line stays visible after expansion
- Scroll up if needed to fit expanded content
- Constrain oversized content to available space
**Success Criteria**:
- Subitems expand without breaking viewport
- Proper scroll adjustment for large expanded content
- Content never overflows viewport boundaries
## Technical Architecture
### Core Classes
#### ViewportCalculator
```typescript
class ViewportCalculator {
calculateViewport(maxWidth: number, maxLines: number): ViewportState {
// Calculate actual available space
// Account for header, confirmation, borders
// Return viewport dimensions
}
getContentDimensions(): { width: number, height: number } {
// Return available space for content
}
isOverflowing(contentHeight: number): { top: boolean, bottom: boolean } {
// Determine overflow conditions
}
}
```
#### ScrollStateManager
```typescript
class ScrollStateManager {
private scrollOffset: number = 0;
private viewportHeight: number = 0;
private contentHeight: number = 0;
getScrollIndicators(): { showUp: boolean, showDown: boolean } {
// Pure overflow-based calculation
}
scrollToElement(elementIndex: number): void {
// Bring element into view
}
handleNavigation(direction: 'up' | 'down'): boolean {
// Handle scroll navigation
}
}
```
#### ElementVisibilityCalculator
```typescript
class ElementVisibilityCalculator {
calculateVisibleElements(
elements: IListItem[],
viewport: ViewportState,
scrollOffset: number
): VisibleElement[] {
// Return elements that are visible in current viewport
}
getElementPosition(elementIndex: number): { start: number, end: number } {
// Get element position in viewport coordinates
}
isElementVisible(elementIndex: number): boolean {
// Check if element is visible
}
}
```
## Implementation Benefits
### 1. **Maintainability**
- Clear separation of concerns
- Each system can be tested independently
- Easy to debug specific issues
### 2. **Reliability**
- No complex interdependencies
- Predictable behavior
- Less prone to edge case bugs
### 3. **Performance**
- Calculations done once per render cycle
- No duplicate computations
- Efficient viewport updates
### 4. **Extensibility**
- New features can be added without breaking existing logic
- Each system can be enhanced independently
- Easy to add new scroll behaviors
## Testing Strategy
### Unit Testing
- [ ] Test ViewportCalculator with various dimensions
- [ ] Test ScrollStateManager with different content sizes
- [ ] Test ElementVisibilityCalculator with various scroll positions
- [ ] Test navigation logic with different element configurations
### Integration Testing
- [ ] Test full ContainerListItem with new architecture
- [ ] Test with existing GenericListPanel
- [ ] Test terminal resizing scenarios
- [ ] Test complex nested scenarios
### Regression Testing
- [ ] Ensure all existing functionality still works
- [ ] Verify scroll indicators appear correctly
- [ ] Confirm navigation brings elements into view
- [ ] Test subitem expansion scenarios
## Success Metrics
1. **Scroll Indicators**: Always appear correctly based on overflow state
2. **Navigation**: Selected elements always brought into view smoothly
3. **Expansion**: Subitems expand without breaking viewport
4. **Maintainability**: New features can be added without breaking existing logic
5. **Performance**: No noticeable performance regression
This redesign treats ContainerListItem as a proper viewport component with dedicated systems for each concern, replacing the current patchwork approach with a robust, maintainable architecture.