Skip to content

feat: Multiple terminal tabs with focus and resize fixes#18

Merged
kshivang merged 5 commits intomasterfrom
dev
Nov 19, 2025
Merged

feat: Multiple terminal tabs with focus and resize fixes#18
kshivang merged 5 commits intomasterfrom
dev

Conversation

@kshivang
Copy link
Copy Markdown
Owner

Summary

Implements GitHub issue #7 - Multiple terminal tabs support with comprehensive tab management and bug fixes.

Features

Tab Management

  • ✅ Tab bar UI with Material 3 design
  • ✅ Create new tabs with "+" button
  • ✅ Close tabs with "X" button
  • ✅ Switch between tabs by clicking
  • ✅ Independent terminal sessions per tab
  • ✅ Auto-close tabs when shell exits
  • ✅ Close application when last tab closes
  • ✅ Working directory inheritance for new tabs

Architecture

  • TerminalTab.kt (223 lines): Complete per-tab state container

    • Terminal components (JediTerminal, TextBuffer, Display, Emulator)
    • Process management (PTY handle, connection state, coroutine scope)
    • UI state (focus, scroll, selection, search, IME, context menu)
  • TabController.kt (336 lines): Tab lifecycle manager

    • Creates tabs with full terminal initialization
    • Spawns PTY with 3 background coroutines per tab
    • Handles tab switching with proper cleanup
    • Tracks working directories for inheritance
  • TabBar.kt (135 lines): Material 3 tab bar component

    • Horizontal tab strip with close buttons
    • Active tab highlighting with blue border
    • New tab button
  • Main.kt (114 lines): Window-level integration

    • Shared font loading for performance
    • TabController initialization
    • Renders active tab only
  • ProperTerminal.kt: Refactored to accept TerminalTab parameter

    • Removed per-tab state initialization (now in TabController)
    • PTY spawning moved to TabController

Bug Fixes

1. Keyboard Input Bug

Problem: New tabs couldn't receive keyboard input

Fix (ProperTerminal.kt:436):

// Changed LaunchedEffect key from isActiveTab to tab.id
LaunchedEffect(tab.id) {
    kotlinx.coroutines.delay(100)
    focusRequester.requestFocus()
}

2. TUI App Initial Rendering Bug

Problem: nvim, htop, and other TUI apps rendered incorrectly on launch (required window resize to fix)

Root Cause: Terminal initialized with hardcoded 80x24 dimensions, PTY process never received actual window size

Fix (ProperTerminal.kt:147, 474, 482):

var hasPerformedInitialResize by remember { mutableStateOf(false) }

if ((!hasPerformedInitialResize || (currentCols != newCols || currentRows != newRows)) && ...) {
    terminal.resize(newTermSize, RequestOrigin.User)
    processHandle?.resize(newCols, newRows)  // Sends SIGWINCH
    hasPerformedInitialResize = true
}

Technical Details

  • Each tab has independent CoroutineScope with SupervisorJob
  • Per-tab coroutines:
    1. Emulator processing (Dispatchers.Default)
    2. PTY output reading (Dispatchers.IO)
    3. Process exit monitoring (Dispatchers.IO - triggers auto-close)
  • Only active tab receives keyboard focus
  • Background tabs continue processing output (ready for Phase 8 optimization)
  • Shared font loading (MesloLGS NF) for performance

Testing

Manual Testing Performed

  • ✅ Build successful, no compilation errors
  • ✅ Terminal launches with one tab ("Shell 1")
  • ✅ Can create multiple tabs with "+" button
  • ✅ Can switch between tabs by clicking
  • ✅ Can close tabs with "X" button
  • ✅ Keyboard input works in all tabs
  • ✅ Each tab maintains independent state (scroll, selection, etc.)
  • ✅ TUI apps (nvim, htop) render correctly on first launch
  • ✅ App exits when last tab closes
  • ✅ Tab titles update correctly (Shell 1, Shell 2, etc.)

Test Scenarios

# Test 1: TUI app rendering
nvim  # Should render correctly without resize

# Test 2: Multiple independent sessions
# Tab 1: cd ~/Documents && ls
# Tab 2: cd ~/Downloads && ls
# Switch between tabs - each shows different directory

# Test 3: Tab lifecycle
# Create 3 tabs, close middle tab, verify switching works
# Close all tabs - app should exit

Future Enhancements (Next Phase)

  • Keyboard shortcuts (Cmd/Ctrl+T, Cmd/Ctrl+W, Ctrl+Tab, Cmd/Ctrl+1-9)
  • OSC 7 working directory tracking (shell integration)
  • Background tab performance optimization (pause UI updates)
  • Tab reordering (drag and drop)
  • Custom tab titles (double-click to rename)

Files Changed

File Changes Lines
TerminalTab.kt New file +223
TabController.kt New file +336
TabBar.kt New file +135
Main.kt Complete rewrite ~114
ProperTerminal.kt Refactored signature + bug fixes ~50 changes
CLAUDE.md Documentation update +300

Closes

Closes #7

🤖 Generated with Claude Code

…andling

This commit implements GitHub issue #7 - Multiple terminal tabs support.

## Features Implemented:
- Tab bar UI with Material 3 design (Shell 1, Shell 2, etc.)
- Independent terminal sessions per tab (PTY, emulator, state)
- Tab lifecycle management (create, close, switch)
- Working directory inheritance for new tabs
- Auto-close tabs when shell exits
- Close application when last tab closes
- Keyboard shortcuts ready (Ctrl+T, Ctrl+W will be added in next phase)

## Architecture:
- **TerminalTab.kt**: Data class encapsulating all per-tab state
  - Terminal components: JediTerminal, TextBuffer, Display, Emulator
  - Process management: PTY handle, connection state, coroutine scope
  - UI state: focus, scroll, selection, search, IME, context menu

- **TabController.kt**: Manages tab lifecycle
  - Creates tabs with full terminal initialization
  - Spawns PTY process with 3 background coroutines per tab
  - Handles tab switching with proper cleanup
  - Tracks working directories for inheritance

- **TabBar.kt**: Material 3 tab bar component
  - Horizontal tab strip with close buttons
  - Active tab highlighting
  - New tab button (+)

- **Main.kt**: Window-level integration
  - Shared font loading (performance optimization)
  - TabController initialization
  - Renders active tab only

- **ProperTerminal.kt**: Refactored to accept TerminalTab parameter
  - Removed per-tab state initialization (now in TabController)
  - PTY spawning moved to TabController

## Bug Fixes:
- **Focus issue**: Fixed keyboard input not working in new tabs
  - Changed LaunchedEffect key from isActiveTab to tab.id
  - Ensures focus is requested every time a different tab is rendered

- **Initial resize issue**: Fixed TUI apps (nvim, htop) rendering incorrectly on launch
  - Added hasPerformedInitialResize flag
  - Forces initial resize to send actual window dimensions to PTY
  - Ensures SIGWINCH is sent on first render

## Technical Details:
- Each tab has independent coroutine scope with SupervisorJob
- Per-tab coroutines: emulator processing, PTY output reading, exit monitoring
- Only active tab receives keyboard focus
- Background tabs continue processing output (UI updates paused for future optimization)

## Testing:
- ✅ Build successful, no compilation errors
- ✅ Terminal launches with one tab
- ✅ Can create multiple tabs with "+" button
- ✅ Can switch between tabs
- ✅ Can close tabs with "X" button
- ✅ Keyboard input works in all tabs
- ✅ TUI apps (nvim) render correctly on launch
- ✅ App exits when last tab closes

## Future Enhancements (Phase 5):
- Keyboard shortcuts: Cmd/Ctrl+T, Cmd/Ctrl+W, Ctrl+Tab, Cmd/Ctrl+1-9
- OSC 7 working directory tracking
- Background tab performance optimization

Closes #7

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

Code Review: Multiple Terminal Tabs Implementation

Overview

This is an excellent implementation of multi-tab terminal support! The architecture is well-designed with proper separation of concerns, comprehensive lifecycle management, and good attention to the project's specific requirements. The code quality is high and the PR documentation is thorough.

✅ Strengths

Architecture & Design

  • Clean separation: TerminalTab (state), TabController (lifecycle), TabBar (UI), Main (composition)
  • Proper encapsulation: All per-tab state moved from ProperTerminal into TerminalTab
  • Stateful emulator preservation: Correctly maintains single long-lived emulator per tab to prevent CSI truncation
  • Resource sharing: Smart font loading - loads MesloLGSNF once and shares across tabs

Lifecycle Management

  • Per-tab coroutine scopes with SupervisorJob for proper isolation
  • Three coroutines per tab: Emulator processing, PTY reading, exit monitoring
  • Proper dispose() implementation
  • Good UX: Tabs auto-close when shell exits

Bug Fixes

  • Focus fix (ProperTerminal.kt:436): LaunchedEffect(tab.id) fixes keyboard input in new tabs
  • Initial resize fix (ProperTerminal.kt:147): hasPerformedInitialResize flag fixes TUI apps

Code Quality

  • Comprehensive KDoc comments
  • Good error handling
  • Proper Kotlin idioms

⚠️ Issues & Suggestions

1. GlobalScope Usage (TerminalTab.kt:207) 🔴 Critical

Using GlobalScope is an anti-pattern. The launched job is not tied to any lifecycle and can leak.

Recommendation: Use runBlocking with timeout or pass external scope.

2. Memory Leak Risk (TabController.kt:258) ⚠️ Medium

Tab object might be GC'd before kill() completes, potentially leaving zombie processes.

3. Hardcoded Shell (TabController.kt:71) ⚠️ Medium

Hardcodes /bin/zsh which may not exist on all systems.

Recommendation: Use System.getenv("SHELL") ?: "/bin/bash"

4. UI State Not Isolated (ProperTerminal.kt:153) ℹ️ Low

Some UI state still in ProperTerminal remember blocks instead of TerminalTab.

5. Race Condition (TabController.kt:183-184) ℹ️ Low

tab.isVisible is not thread-safe.

Recommendation: Use MutableState instead of mutable var.

6. Tab Bar Performance (TabBar.kt:52-56) ℹ️ Low

With 20+ tabs, all are rendered even if off-screen. Consider LazyRow for future.

🧪 Testing

  • No unit tests for TabController lifecycle
  • Manual testing checklist incomplete

📝 Documentation

Excellent KDoc and CLAUDE.md updates. Minor: remove empty preview function.

🔒 Security

No major issues. Proper input validation and environment filtering.

Recommendations Summary

Priority Issue Fix
🔴 Critical GlobalScope Use runBlocking/scoped coroutine
⚠️ Medium Memory leak Ensure cleanup completes
⚠️ Medium Hardcoded shell Use System.getenv("SHELL")
ℹ️ Low State isolation Move to TerminalTab
ℹ️ Low Race condition Make isVisible thread-safe
ℹ️ Low Tests Add TabController tests

🎉 Conclusion

High-quality work with proper architecture. Critical issues are fixable with small changes.

Recommendation: ✅ Approve with changes

  • Fix GlobalScope usage (critical)
  • Fix hardcoded shell (medium)
  • Consider moving remaining state to TerminalTab (can be follow-up)

Great job! 🚀


Review by Claude Code following CLAUDE.md guidelines

Comprehensive code cleanup based on TODO/unused code analysis and implementation of
standard terminal hyperlink behavior requiring Ctrl/Cmd modifier for link opening.

## Code Cleanup (Tier 1)

### Removed Dead Code
- **ProperTerminal.kt:146**: Removed unused `resizeJob` variable (declared for debouncing but never used)
- **ProperTerminal.kt:399**: Removed unused `baselineOffset` variable (calculated but never referenced)
- **ProperTerminal.kt:66**: Removed duplicate `import kotlinx.coroutines.withContext`
- **ProperTerminal.kt:815-819**: Removed commented testing code (old variation selector skip logic)

### Updated Misleading Comments
- **TerminalTab.kt:185, 193**: Updated Phase 8 TODO comments
  - Removed outdated TODOs about redraw pause/resume
  - Clarified that feature is already implemented via Phase 2 adaptive debouncing
  - TabController checks `isVisible` flag to skip redraws for hidden tabs

## Hyperlink Enhancement (Tier 2)

### Problem
Hyperlinks opened on ANY mouse click, risking accidental navigation during text selection.

### Solution
Implemented standard terminal behavior: **Ctrl+Click** (Windows/Linux) or **Cmd+Click** (macOS).

### Implementation Details

**ProperTerminal.kt changes:**

1. Added state variable for modifier tracking (line 146):
   ```kotlin
   var isModifierPressed by remember { mutableStateOf(false) }
   ```

2. Track Ctrl/Cmd keys in `onPreviewKeyEvent` (lines 665-668):
   ```kotlin
   when (keyEvent.key) {
       Key.CtrlLeft, Key.CtrlRight, Key.MetaLeft, Key.MetaRight -> {
           isModifierPressed = keyEvent.type == KeyEventType.KeyDown
       }
   }
   ```

3. Updated hyperlink click handler (lines 547-550):
   ```kotlin
   if (hoveredHyperlink != null && isModifierPressed) {
       HyperlinkDetector.openUrl(hoveredHyperlink!!.url)
   }
   ```

### Behavior Now
- ✅ Regular click on hyperlink → No action (safe)
- ✅ Ctrl+Click (Windows/Linux) → Opens browser
- ✅ Cmd+Click (macOS) → Opens browser
- ✅ Matches iTerm2, Alacritty, Hyper behavior

## Benefits

1. **Standard Behavior**: Matches industry-standard terminal emulators
2. **Prevents Accidents**: No more accidental link opening during text selection
3. **Platform-Aware**: Automatically uses Ctrl (Windows/Linux) or Cmd (macOS)
4. **Clean Code**: Removed 4 dead code items (2 variables, 1 import, 1 comment block)
5. **Clear Documentation**: Updated comments to reflect actual implementation state

## Files Modified

| File | Changes | Lines |
|------|---------|-------|
| ProperTerminal.kt | Cleanup + hyperlink modifier | -10 +15 |
| TerminalTab.kt | Updated TODO comments | ~6 |
| CLAUDE.md | Documentation | +100 |

## Testing

- ✅ BUILD SUCCESSFUL - All changes compile without errors
- ✅ Terminal launches and runs normally
- ✅ Existing keyboard shortcuts still work
- ✅ Terminal input not affected
- ✅ Tabs functionality working correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

Pull Request Review: Multiple Terminal Tabs Implementation

Hi @kshivang! This is an impressive feature implementation. Here is my detailed review:

Strengths

Excellent Architecture

  • Clean separation of concerns: TerminalTab state, TabController lifecycle, TabBar UI
  • Proper state management moving from ProperTerminal remember blocks to TerminalTab
  • Resource cleanup with dispose pattern and coroutine cancellation
  • Shared font loading optimization avoiding repeated 2.5MB loads

Critical Bug Fixes

  1. Keyboard focus fix ProperTerminal.kt:435 - Using tab.id as LaunchedEffect key is crucial
  2. Initial resize fix ProperTerminal.kt:472-481 - hasPerformedInitialResize flag fixes nvim/htop rendering

Code Quality

  • Comprehensive documentation
  • Consistent naming
  • Well-structured coroutine management

Critical Issues

1. GlobalScope Leak TerminalTab.kt:209
GlobalScope usage leaks coroutines beyond tab lifetime. Use the tabs own coroutineScope instead.

2. Race Condition TabController.kt:227
Using tabs.indexOf can return wrong index if user closes tab while shell exits. Use tabs.indexOfFirst with id comparison instead.

3. Memory Leak Risk TabController.kt:180
Emulator loop needs cancellation check. Add isActive check to while condition.

4. Hard-coded Shell TabController.kt:71
Using /bin/zsh hardcoded. Should use System.getenv SHELL with /bin/bash fallback.

5. Missing Null Safety ProperTerminal.kt:478
processHandle could be null. Use let block before launching resize coroutine.

Testing Recommendations

  • Open 5 tabs and exit all simultaneously to test race conditions
  • Create and close 50 tabs rapidly to check for memory leaks
  • Verify no orphaned PTY processes after tab closure
  • Test focus consistency when switching between tabs
  • Test with long-running processes

Priority Fixes

HIGH: GlobalScope leak TerminalTab.kt:209
HIGH: Race condition TabController.kt:227
MEDIUM: Hard-coded shell TabController.kt:71
MEDIUM: Missing cancellation check TabController.kt:180
LOW: Code duplication TabController.kt:339

Summary

High-quality work with solid foundation. Architecture is sound and bug fixes demonstrate deep understanding.

Recommendation: Approve with changes. Fix GlobalScope and race condition before merge. Other issues can be follow-up PRs.

Estimated fixes: 10 minutes total for critical issues.

Great work!


Reviewed by Claude Code

Addresses two code review findings:

1. Shell Compatibility (CRITICAL):
   - Changed hardcoded /bin/zsh to use $SHELL environment variable
   - Fallback to /bin/bash for systems without $SHELL set
   - Fixes compatibility with Ubuntu/Debian/Alpine Linux systems
   - Location: TabController.kt:71

2. Process Cleanup Safety (IMPROVEMENT):
   - Moved process kill logic from TerminalTab to TabController
   - Prevents theoretical GC race condition during tab disposal
   - Holds explicit reference to processHandle before cleanup
   - Cleaner separation of concerns (TabController owns lifecycle)
   - Locations: TabController.kt:252-295, TerminalTab.kt:206-212

Technical Details:
- GlobalScope usage for process termination is intentional and correct
  (process must outlive tab disposal to complete kill operation)
- All changes tested and verified to compile successfully
- Terminal launches correctly and uses correct shell

Documentation:
- Added comprehensive "Code Review Responses" section to CLAUDE.md
- Documented both fixes with code examples and rationale
- Updated "Recent Changes" and "Last Updated" sections

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

Code Review - PR #18: Multiple Terminal Tabs

I have reviewed this PR implementing multiple terminal tabs support. Overall, this is a high-quality implementation with solid architecture and good attention to detail.

Strengths

1. Excellent Architecture

  • Clean separation of concerns with TerminalTab, TabController, and TabBar
  • Proper state isolation per tab - each tab has independent terminal stack
  • Well-documented code with comprehensive KDoc comments
  • Follows Material 3 design patterns for UI consistency

2. Resource Management

  • Proper lifecycle management with coroutine scopes per tab
  • Correct cleanup sequence: coroutines → tabs list → process kill
  • Good fix for GC safety issue (explicit process reference holding in TabController.kt:257-276)
  • Fixed hardcoded shell path to use SHELL env var

3. Critical Bug Fixes

  • Focus bug fix (ProperTerminal.kt:436): Changed LaunchedEffect key from isActiveTab to tab.id - this is correct
  • TUI rendering fix (ProperTerminal.kt:147): Added hasPerformedInitialResize flag to ensure PTY gets initial window size

4. Performance Considerations

  • Shared font loading across all tabs (good optimization)
  • Redraw optimization for background tabs (tab.isVisible check)
  • Proper use of SupervisorJob for coroutine isolation

@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

Issues and Recommendations

1. High Priority - Potential Race Condition

Location: TabController.kt:183-184

Problem: The isVisible check happens AFTER processing the character but the redraw is conditional. If a tab switches from visible to hidden between these two lines, you might skip a redraw that should have happened.

Recommendation: Capture the visibility state before processing, or make isVisible volatile.

2. Medium Priority - Coroutine Job Management

Location: TabController.kt:178-198, 201-218, 221-230

Problem: Three background coroutines are launched but their Job references are not stored. This makes debugging and graceful shutdown difficult.

Recommendation: Store job references in TerminalTab for better lifecycle tracking.

3. Medium Priority - Error Handling

Location: TabController.kt:232-239

Problem: When terminal initialization fails, the tab is created but in an error state. The tab is never removed from the list, leaving a zombie tab.

Recommendation: Auto-close failed tabs after showing error message briefly.

4. Low Priority - Memory Leak (Minor)

Location: TabController.kt:269-276

Problem: Using GlobalScope for process kill. While documented, there is a cleaner approach using NonCancellable context.

Recommendation: Use tab.coroutineScope.launch(Dispatchers.IO + NonCancellable) instead of GlobalScope.

@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

5. Low Priority - Type Safety

Location: Main.kt:96

Problem: Accessing tabs[activeTabIndex] without bounds checking. While unlikely, rapid tab closing could cause IndexOutOfBoundsException.

Recommendation: Use tabs.getOrNull(activeTabIndex) with null check.

6. Test Coverage - Missing

Observation: No unit tests for the new tab management code.

Recommendation: Add tests for TabController operations (create, close, switch) and edge cases.

Code Quality Metrics

Metric Score Notes
Architecture 5/5 Excellent separation of concerns
Documentation 5/5 Comprehensive KDoc and CLAUDE.md
Error Handling 3/5 Needs cleanup for failed tabs
Test Coverage 1/5 No unit tests for new code
Thread Safety 4/5 Minor isVisible race condition
Resource Management 5/5 Proper cleanup with minor improvement possible

@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

Recommendations Priority

Before Merge (High Priority):

  1. Fix race condition with isVisible check
  2. Add bounds checking in Main.kt
  3. Auto-close failed tabs

Post-Merge (Medium Priority):
4. Store coroutine job references
5. Replace GlobalScope with NonCancellable
6. Add unit tests

Summary

This is a solid implementation that addresses GitHub issue #7 comprehensively. The architecture is clean, the code is well-documented, and the bug fixes are appropriate. With the high-priority fixes above, this will be production-ready.

The tab management system provides a solid foundation for future enhancements (keyboard shortcuts, OSC 7 tracking, performance optimization).

Recommendation: Approve with minor revisions

Great work on this feature! The multiple tabs implementation significantly enhances the terminal usability.

Addresses code review finding #5 (race condition on tab visibility flag).

Thread Safety Issue (FIXED):
- isVisible was a plain var accessed from multiple threads:
  * Written from Main thread (Compose UI) in onVisible/onHidden
  * Read from Dispatchers.Default thread (emulator processing)
- Plain var has no memory visibility guarantees (JVM memory model)
- Could result in stale reads (worst case: missed/extra redraw)

Solution:
Changed isVisible from var Boolean to MutableState<Boolean>:
- TerminalTab.kt:179 - Changed field declaration
- TerminalTab.kt:187,196 - Updated onVisible/onHidden to use .value
- TabController.kt:183 - Updated read access to use .value

Benefits:
- Thread-safe reads/writes via Compose snapshot system
- Proper memory visibility guarantees across coroutines
- Aligns with Compose best practices for shared state
- Observable for potential future optimizations

Code Review Analysis:
- Review #4 (UI state isolation) - FALSE ALARM (correct architecture)
- Review #5 (isVisible race) - LEGITIMATE (fixed)
- Review #6 (tab bar performance) - VALID but defer to Phase 9+

Technical Details:
- MutableState uses Compose snapshot system for thread safety
- State writes are atomic and visible to all threads immediately
- No performance impact (MutableState is optimized for this use case)
- Maintains same public API (internal implementation change only)

Documentation:
- Added "Code Review Responses - Round 2" section to CLAUDE.md
- Detailed threading analysis with code examples
- Architectural justification for UI state separation
- Performance impact analysis for deferred optimizations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

Code Review: Multiple Terminal Tabs Implementation

This is an excellent, production-ready implementation of multi-tab terminal support! The architecture is solid, the code is clean, and the bug fixes are well-reasoned. Here's my detailed review:


Strengths

1. Architecture & Design ⭐⭐⭐⭐⭐

  • Clean separation of concerns: TerminalTab (data model) → TabController (lifecycle manager) → TabBar (UI) → ProperTerminal (view)
  • Proper state isolation: Each tab has completely independent terminal state, avoiding interference between sessions
  • Shared font optimization: Loading MesloLGSNF once and sharing across tabs is smart (~2.5MB saved per tab)
  • Lifecycle management: SupervisorJob + proper coroutine cancellation prevents leaks
  • Thread safety: Excellent fix to use MutableState<Boolean> for isVisible instead of plain var (addresses cross-coroutine access)

2. Bug Fixes ⭐⭐⭐⭐⭐

Both bugs are critical and well-fixed:

a) Keyboard Input Bug (ProperTerminal.kt:436)

LaunchedEffect(tab.id) {  // Changed from isActiveTab
    delay(100)
    focusRequester.requestFocus()
}

Why this works: LaunchedEffect(isActiveTab) only ran when the boolean changed, but switching between tabs didn't change the value (both tabs had isActiveTab=false). Using tab.id ensures focus is requested every time a different tab is rendered. Smart!

b) TUI Initial Rendering Bug (ProperTerminal.kt:147, 474, 482)

var hasPerformedInitialResize by remember { mutableStateOf(false) }

if ((!hasPerformedInitialResize || (currentCols != newCols || currentRows != newRows)) && ...) {
    terminal.resize(newTermSize, RequestOrigin.User)
    processHandle?.resize(newCols, newRows)  // CRITICAL: Sends SIGWINCH
    hasPerformedInitialResize = true
}

Why this is critical: PTY processes (nvim, htop) rely on SIGWINCH to detect terminal dimensions. Without this, they start with 80x24 and never learn the actual window size. Forcing an initial resize ensures TUI apps render correctly from the start.

3. Code Quality ⭐⭐⭐⭐⭐

  • Excellent documentation: Every class, function, and critical section has clear KDoc comments
  • Consistent naming: createTab, closeTab, switchToTab - intuitive API
  • Error handling: Proper try-catch blocks with informative logging
  • Resource cleanup: Tab disposal cancels coroutines, kills processes, removes references
  • Platform compatibility: Shell detection (System.getenv("SHELL") ?: "/bin/bash") works on all Linux distros + macOS

4. Performance Considerations ⭐⭐⭐⭐

  • Per-tab coroutines: Emulator processing, PTY reading, and exit monitoring run independently
  • Conditional redraws: if (tab.isVisible.value) tab.display.requestRedraw() skips UI updates for hidden tabs (ready for Phase 8 optimization)
  • Efficient tab switching: onVisible()/onHidden() lifecycle callbacks enable future optimizations
  • Tab bar: Lightweight composition (eager rendering is fine for <20 tabs, as documented)

🟡 Minor Issues / Suggestions

1. Potential Race Condition in Auto-Close (Low Priority)

Location: TabController.kt:221-230

handle.waitFor()  // Blocks until process exits
println("INFO: Shell process exited for tab: ${tab.title.value}")

withContext(Dispatchers.Main) {
    val tabIndex = tabs.indexOf(tab)  // <-- Race condition here
    if (tabIndex != -1) {
        closeTab(tabIndex)
    }
}

Issue: Between waitFor() returning and withContext(Main) executing, the user could manually close the tab. tabs.indexOf(tab) might return -1 or stale index.

Impact: Very low - The if (tabIndex != -1) check handles it gracefully. Worst case: logged message + no-op.

Suggestion (optional improvement):

withContext(Dispatchers.Main) {
    // Use tab.id for safer lookup
    val tabIndex = tabs.indexOfFirst { it.id == tab.id }
    if (tabIndex != -1) {
        closeTab(tabIndex)
    }
}

2. GlobalScope Usage is Correct (Not an Issue)

Location: TabController.kt:269

Reviewers might flag GlobalScope.launch as an anti-pattern, but it's architecturally correct here:

  • The tab's coroutineScope is already cancelled
  • Process cleanup must continue even after tab disposal
  • "Fire-and-forget" is appropriate for cleanup tasks
  • When last tab closes, exitApplication terminates JVM, so no leak

No changes needed


3. Tab Bar Scrolling for 20+ Tabs (Documented, Deferred)

Location: TabBar.kt:57-65

Current Row + forEachIndexed renders all tabs eagerly. With 20+ tabs, this could cause layout issues (tabs squished to minimum 80dp width, no horizontal scrolling).

Mitigation: Already documented in CLAUDE.md as Phase 9+ enhancement.

Recommendation: Add horizontalScrollState when implementing:

Row(
    modifier = Modifier
        .weight(1f)
        .horizontalScroll(rememberScrollState()),  // Enable scrolling
    horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ... }

4. Missing Null Check in Main.kt (Edge Case)

Location: Main.kt:96

if (tabController.tabs.isNotEmpty()) {
    val activeTab = tabController.tabs[tabController.activeTabIndex]  // <-- Could throw IOOBE
    ProperTerminal(...)
}

Issue: If activeTabIndex is out of bounds (rare, but possible during rapid tab closing), this crashes.

Fix (defensive programming):

val activeTab = tabController.tabs.getOrNull(tabController.activeTabIndex)
if (activeTab != null) {
    ProperTerminal(...)
}

🟢 Best Practices Followed

  1. Compose best practices: Hoisted state, unidirectional data flow, side effects in LaunchedEffect
  2. Coroutine hygiene: SupervisorJob, Dispatchers.IO for blocking I/O, Dispatchers.Main for UI updates
  3. Resource management: Proper cleanup in dispose(), process termination with explicit reference holding
  4. Accessibility: Tab titles, close buttons, keyboard shortcuts ready (Phase 5)
  5. Testing: Comprehensive test plan documented, manual testing performed
  6. Documentation: CLAUDE.md updated with 711 lines of architecture docs, design decisions, and future roadmap

🔒 Security Considerations

  1. Environment filtering: filterEnvironmentVariables() removes ITERM_*, KITTY_* to prevent privilege escalation
  2. Shell safety: Uses $SHELL env var (respects user's choice) with safe fallback to /bin/bash
  3. Process isolation: Each tab has its own PTY process (no shared state)
  4. Input validation: Tab indices validated before access (if (index < 0 || index >= tabs.size))

📊 Test Coverage Assessment

Manual Testing: ✅ Comprehensive (see PR description)
Unit Tests: ⚠️ None added (but existing tests still pass)

Recommendation: Add integration tests for tab lifecycle:

@Test
fun `test tab creation and switching`() {
    val controller = TabController(settings, {})
    controller.createTab()
    controller.createTab()
    assertEquals(2, controller.tabs.size)
    controller.switchToTab(0)
    assertEquals(0, controller.activeTabIndex)
}

🎯 Final Verdict

APPROVE

This PR represents months of work compressed into a single, well-executed implementation. The code is:

  • ✅ Production-ready
  • ✅ Well-documented
  • ✅ Performant
  • ✅ Secure
  • ✅ Maintainable

The bug fixes alone justify merging (TUI apps not rendering is a showstopper), and the tab architecture is excellent.

Recommended Next Steps (After Merge):

  1. Phase 5: Keyboard shortcuts (Cmd/Ctrl+T, Cmd/Ctrl+W, Ctrl+Tab)
  2. Phase 4: OSC 7 working directory tracking
  3. Phase 8: Background tab performance optimization (pause redraws)
  4. Add integration tests for tab lifecycle
  5. Consider horizontal scrolling for tab bar (low priority)

Great work on this feature! 🎉 The terminal emulator is now on par with iTerm2/Alacritty in terms of tab support. Merge with confidence! 🚀

…and performance

Implements three major enhancement phases for the terminal emulator:

Phase 5: Keyboard Shortcuts for Tab Management
- Added 15 keyboard shortcuts for tab management (Cmd/Ctrl+T, Cmd/Ctrl+W, etc.)
- Extended ActionRegistry system with addTabManagementActions()
- Platform-aware shortcuts (macOS uses Cmd, Windows/Linux use Ctrl)

Keyboard Shortcuts Implemented:
- Cmd/Ctrl+T: New tab (inherits working directory from active tab)
- Cmd/Ctrl+W: Close current tab
- Ctrl+Tab: Next tab
- Ctrl+Shift+Tab: Previous tab
- Cmd/Ctrl+1-9: Switch to specific tab (1-9)

Phase 4: OSC 7 Working Directory Tracking
- Implemented WorkingDirectoryOSCListener using TerminalCustomCommandListener
- Parses OSC 7 sequences: ESC]7;file://hostname/path\a
- Updates tab's workingDirectory state for CWD inheritance
- Thread-safe via MutableState (works from emulator thread)
- Enables new tabs to start in the same directory as active tab

Setup for OSC 7 (add to ~/.zshrc or ~/.bashrc):
```bash
# Bash:
PROMPT_COMMAND='echo -ne "\033]7;file://${HOSTNAME}${PWD}\007"'

# Zsh:
precmd() { echo -ne "\033]7;file://${HOST}${PWD}\007" }
```

Phase 8: Background Tab Performance Optimization
- VERIFIED: Already implemented via isVisible check (line 183 in TabController)
- Background tabs (isVisible.value = false) skip requestRedraw() calls
- Saves CPU by not rendering UI updates for hidden tabs
- Only active tab triggers redraws (Phase 2 adaptive debouncing still applies)

Technical Details:

BuiltinActions.kt (+112 lines):
- Added addTabManagementActions() function
- Registers 15 tab management shortcuts with ActionRegistry
- Supports platform-specific modifier keys

WorkingDirectoryOSCListener.kt (NEW, 73 lines):
- Implements TerminalCustomCommandListener interface
- Processes OSC args: ["7", "file://hostname/path"]
- Parses file:// URIs with error handling
- Updates Compose MutableState for reactive UI

ProperTerminal.kt (+11 lines):
- Added 5 tab management callback parameters (onNewTab, onCloseTab, etc.)
- Integrated addTabManagementActions() into action registry setup
- Callbacks delegate to TabController methods

Main.kt (+26 lines):
- Wired up all 5 tab management callbacks
- Ensures proper working directory inheritance for new tabs
- Validates tab index before switching

TabController.kt (+8 lines):
- Registers WorkingDirectoryOSCListener on terminal creation
- Creates working directory state before terminal initialization
- OSC listener updates state automatically on directory changes

Benefits:
- ✅ Full keyboard-driven tab management (standard terminal UX)
- ✅ Smart directory inheritance (new tabs start in active tab's CWD)
- ✅ CPU-efficient background tabs (no wasted redraws)
- ✅ Thread-safe OSC processing via Compose snapshot system
- ✅ Platform-aware shortcuts (macOS/Windows/Linux)

Testing:
- ✅ Compilation successful (all phases)
- ⏳ Manual testing required for keyboard shortcuts
- ⏳ OSC 7 testing requires shell configuration

Known Limitations:
- OSC 7 requires shell configuration (not automatic)
- Working directory tracking depends on shell support
- Older shells may not support OSC 7 sequences

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Nov 19, 2025

Code Review - PR #18: Multiple Terminal Tabs Feature

Overall Assessment

EXCELLENT WORK! 🎉 This is a comprehensive, well-architected implementation of multiple terminal tabs. The code quality is high, the documentation is exceptional, and the progressive implementation through phases shows careful planning.

Strengths

1. Architecture & Design ⭐⭐⭐⭐⭐

  • Clean separation of concerns: TerminalTab (data model), TabController (lifecycle), TabBar (view)
  • Proper state management: Per-tab state isolation prevents cross-tab contamination
  • Thread-safe design: Using MutableState for isVisible (commit a27cf0c) shows attention to concurrency
  • Lifecycle management: Proper resource cleanup with dispose() and explicit process termination

2. Bug Fixes

  • Focus issue fix (commit 06c955c): Changing LaunchedEffect key from isActiveTab to tab.id is the correct solution
  • Initial resize fix: The hasPerformedInitialResize flag ensures TUI apps get proper dimensions immediately
  • Shell compatibility fix (commit a27cf0c): Using System.getenv("SHELL") ?: "/bin/bash" instead of hardcoded /bin/zsh makes this cross-platform compatible

3. Feature Implementation

  • OSC 7 working directory tracking: Clean implementation of WorkingDirectoryOSCListener with proper URI parsing and error handling
  • Keyboard shortcuts: Comprehensive shortcuts (Cmd/Ctrl+T, Cmd/Ctrl+W, Ctrl+Tab, Cmd/Ctrl+1-9) following platform conventions
  • Tab bar UI: Material 3 design consistency with existing components

4. Code Quality

  • Extensive documentation: Every commit has detailed explanations, CLAUDE.md is exemplary
  • Error handling: Try-catch blocks with meaningful error messages
  • Platform awareness: macOS vs Windows/Linux keyboard shortcuts handled correctly

Issues & Recommendations

Critical Issues: None ✅

Medium Priority Issues

1. Resource Cleanup Potential Race Condition (TabController.kt:279-286)

if (processToKill != null) {
    GlobalScope.launch(Dispatchers.IO) {
        try {
            processToKill.kill()
        } catch (e: Exception) {
            println("WARN: Error killing process: ${e.message}")
        }
    }
}

Issue: Using GlobalScope here is fine (as documented in CLAUDE.md), but there's no way to ensure the kill operation completes before application shutdown. If the last tab is closed, onLastTabClosed() may call exitApplication() before the process is killed, potentially leaving zombie processes.

Recommendation: Consider adding a timeout-based approach:

// In TabController
private val cleanupJobs = mutableListOf<Job>()

fun closeTab(index: Int) {
    // ...
    if (processToKill != null) {
        val cleanupJob = GlobalScope.launch(Dispatchers.IO) {
            try {
                withTimeout(5000) {  // 5 second timeout
                    processToKill.kill()
                }
            } catch (e: TimeoutCancellationException) {
                println("WARN: Process kill timed out, forcing termination")
                processToKill.forceKill()  // If available
            }
        }
        cleanupJobs.add(cleanupJob)
    }
    // ...
}

// Call in onLastTabClosed before exitApplication
suspend fun waitForCleanup() {
    cleanupJobs.joinAll()
}

2. Tab Bar Scalability (TabBar.kt:52-65)

The current implementation using Row + forEachIndexed will compose all tabs immediately. With 20+ tabs, this could impact performance. While CLAUDE.md correctly identifies this as deferred (Phase 9+), consider adding a TODO comment in the code itself:

// TODO(Phase 9): Consider LazyRow for 15+ tabs to improve performance
Row(modifier = Modifier.weight(1f)) {
    tabs.forEachIndexed { index, tab ->

Minor Issues

3. Error Message Consistency (TabController.kt:200, 218)

println("WARNING: Error processing terminal output: ${e.message}")
// vs
println("WARN: Error killing process: ${e.message}")

Recommendation: Use consistent logging prefixes (either WARN or WARNING, not both). Consider extracting to a logger utility:

private object TabLogger {
    fun warn(message: String) = println("WARN: $message")
    fun info(message: String) = println("INFO: $message")
    fun error(message: String) = System.err.println("ERROR: $message")
}

4. Magic Numbers (TabController.kt:212)

val maxChunkSize = 64 * 1024

Recommendation: Extract to a constant with documentation:

companion object {
    /**
     * Maximum size for a single PTY output chunk.
     * Larger chunks are truncated to prevent memory exhaustion from misbehaving processes.
     */
    private const val MAX_CHUNK_SIZE = 64 * 1024  // 64KB
}

5. OSC 7 URI Parsing Edge Cases (WorkingDirectoryOSCListener.kt:55-69)

The current implementation handles basic URI parsing well, but consider these edge cases:

  • Encoded paths: Spaces and special characters in paths (e.g., /Users/My Documents)
  • Windows paths: file:///C:/Users/... (triple slash, drive letters)

Recommendation:

val uri = URI(uriString)
if (uri.scheme == "file") {
    val path = try {
        // URLDecoder handles %20, %2F, etc.
        java.net.URLDecoder.decode(uri.path ?: "", "UTF-8")
    } catch (e: Exception) {
        uri.path
    }
    
    // Handle Windows drive letters (file:///C:/Users -> C:/Users)
    val normalizedPath = if (path.matches(Regex("^/[A-Za-z]:/.+"))) {
        path.substring(1)  // Remove leading slash
    } else {
        path
    }
    
    workingDirectoryState.value = normalizedPath
    println("INFO: Updated working directory via OSC 7: $normalizedPath")
}

Testing Recommendations

Since manual testing was performed, consider adding:

  1. Integration tests for TabController:

    • Test tab creation/closing with mock PTY processes
    • Test working directory inheritance
    • Test keyboard shortcut registration
  2. Edge case tests:

    • Rapid tab creation/closing (stress test)
    • Closing active tab vs inactive tab
    • Tab switching with 0, 1, 2, and 10 tabs
  3. Performance benchmarks:

    • Memory usage per tab (document baseline)
    • CPU usage with multiple tabs running output-heavy processes

Security Considerations

No critical security issues identified

  • Environment variable filtering (line 353-358) correctly removes parent terminal variables
  • Process spawning uses proper configuration objects
  • No command injection vulnerabilities (commands are not constructed from strings)

Minor note: Consider adding validation for workingDir parameter to prevent path traversal:

private fun validateWorkingDirectory(path: String?): String? {
    if (path == null) return null
    val canonical = File(path).canonicalPath
    // Ensure path is under user's home or explicitly allowed directories
    return if (canonical.startsWith(System.getProperty("user.home"))) {
        canonical
    } else {
        null  // Reject suspicious paths
    }
}

Documentation

Outstanding documentation! 📚 The CLAUDE.md file is comprehensive and includes:

  • Architecture diagrams
  • Implementation details for all phases
  • Code review responses with rationale
  • Known limitations and future enhancements

Small suggestion: Add a troubleshooting section to CLAUDE.md:

  • "Tabs not inheriting working directory" → Check OSC 7 shell configuration
  • "Zombie processes after tab close" → Known limitation, workaround
  • "High CPU with many tabs" → Phase 8 optimization status

Conclusion

This PR represents production-quality code with minor issues that don't block merging. The phased approach (Phases 4, 5, 8) shows excellent project management. The fixes for focus and resize issues demonstrate thorough testing and debugging.

Recommendation: APPROVE with minor suggested improvements

The suggested improvements are non-blocking and can be addressed in follow-up PRs if desired. The current implementation is solid, well-documented, and ready for merge.

Great work! 🚀


Reviewed by: Claude Code Assistant
Review Date: 2025-11-19

@kshivang kshivang merged commit 293b9cc into master Nov 19, 2025
1 of 7 checks passed
kshivang added a commit that referenced this pull request Dec 12, 2025
feat: Multiple terminal tabs with focus and resize fixes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📂 Implement multiple terminal sessions/tabs

1 participant