Skip to content

In-Tab File Preview#281

Merged
pedramamini merged 63 commits into0.15.0-rcfrom
in-tab-file-preview
Feb 2, 2026
Merged

In-Tab File Preview#281
pedramamini merged 63 commits into0.15.0-rcfrom
in-tab-file-preview

Conversation

@pedramamini
Copy link
Copy Markdown
Collaborator

@pedramamini pedramamini commented Feb 2, 2026

File content and AI content can now live side by side... and soon, CLI content too. Then, multi-window, then tiling, and then all the flexibility is yours. Closes #272

Phase 1 of file preview tabs feature:
- Add FilePreviewTab interface with id, path, name, extension, scrollTop,
  searchQuery, editMode, editContent, and createdAt fields
- Add UnifiedTabRef type for tab ordering across types
- Add filePreviewTabs, activeFileTabId, and unifiedTabOrder to Session
- Update all 13 session creation locations with new fields
- Update session restoration to hydrate new fields from persistence

This establishes the data model for coexisting AI tabs, file preview tabs,
and future terminal tabs in a unified tab bar.
Creates a memoized unifiedTabs array that combines aiTabs and
filePreviewTabs according to the session's unifiedTabOrder. Uses
a discriminated union type to allow components to render the
appropriate content for each tab type (AI or file preview).
Implements the handler to open file preview tabs in the unified tab system:
- Checks if a tab with the same file path already exists and selects it
- Creates new FilePreviewTab with proper extension parsing
- Adds tab to both filePreviewTabs and unifiedTabOrder arrays
- Sets activeFileTabId to activate the new file tab
Adds handleSelectFileTab handler that sets the activeFileTabId to select a file preview tab. The activeTabId is preserved (not nullified) following the established pattern from handleOpenFileTab, which tracks the last active AI tab for when the user switches back.
Modify setActiveTab function to deselect any active file preview tab
when an AI tab is selected. This ensures only one tab type (AI or file)
is active at a time. Updated early-return condition to also check if
activeFileTabId is null before returning unchanged session.
Implements unified tab reorder functionality that operates on the
unifiedTabOrder array, allowing both AI and file preview tabs to be
reordered relative to each other. This supplements the existing
handleTabReorder (AI-only) for the unified tab system.

Changes:
- Added handleUnifiedTabReorder in App.tsx with index validation
- Propagated through useMainPanelProps.ts, MainPanel.tsx, TabBar.tsx
Updated handleCloseOtherTabs, handleCloseTabsLeft, and handleCloseTabsRight
to support both AI and file preview tabs based on unifiedTabOrder position:

- Determine active tab from activeFileTabId or activeTabId
- Use unifiedTabOrder to determine which tabs are left/right/other
- Close AI tabs using closeTab helper (with wizard history handling)
- Close file tabs by removing from filePreviewTabs and unifiedTabOrder
…b support

Implements handleCloseCurrentTab that:
- Determines which tab is active (file tab via activeFileTabId first, then AI tab via activeTabId)
- Closes file tabs immediately without wizard confirmation
- For AI tabs, returns info to allow keyboard handler to show wizard confirmation
- Prevents closing the last AI tab (keeps at least one AI tab)

Updated keyboard handler (Cmd+W) to use handleCloseCurrentTab for unified tab support.
- Add UnifiedTab discriminated union type to types/index.ts
- Add new props to TabBarProps: unifiedTabs, activeFileTabId,
  onFileTabSelect, onFileTabClose
- Props are optional for backwards compatibility during transition
- Phase 3 of file preview tabs implementation
- Create FileTabProps interface with file tab specific props
- Implement FileTab memoized component with:
  - Filename without extension as label
  - Color-coded extension badge (blue for TS/JS, green for MD, etc.)
  - Unsaved edits indicator (pencil icon)
  - Middle-click to close support
  - Drag and drop handlers
  - Same active/inactive styling as AI tabs
- Add getExtensionColor() helper for extension badge coloring
Verified that the FileTab extension badge implementation is complete:
- Positioned after filename
- 10px font size (text-[10px])
- Rounded corners (rounded class)
- Color-coding by file type via getExtensionColor() helper
- Tests to be added after Task 4 (unified tab rendering) is complete
- Add displayedUnifiedTabs computed value with unread filter support
- Update render loop to conditionally render from unified tabs when provided
- Check unified tab type to render either Tab (AI) or FileTab (file)
- Update handleDrop, handleMoveToFirst, handleMoveToLast for unified tabs
- Update overflow check and empty state to consider unified tabs
- Maintain backwards compatibility with legacy AI-only tab rendering
Implements file-specific overlay menu for FileTab component:
- Add shell:showItemInFolder IPC handler for "Reveal in Finder" action
- Add showItemInFolder to shell preload API and global.d.ts types
- Extend FileTabProps with position and close action callbacks
- Implement full overlay menu with file actions:
  - Copy File Path/Name to clipboard
  - Open in Default App via file:// URL
  - Reveal in Finder via new showItemInFolder API
  - Move to First/Last Position
  - Close Tab/Other/Left/Right actions
- Add 10 tests for file tab overlay menu
- Add 3 tests for shell:showItemInFolder handler
Add 12 comprehensive tests for unified tabs drag-and-drop reordering:
- AI tab to file tab drag-and-drop calls onUnifiedTabReorder
- File tab to AI tab drag-and-drop calls onUnifiedTabReorder
- File tab to file tab drag-and-drop works correctly
- Dropping on same tab does not trigger reorder
- Drag over visual feedback (ring-2 class) on target tab
- Falls back to legacy onTabReorder when unifiedTabs not provided
- Move to First/Last shown for file tabs not at edges
- Move to First hidden for first tab
- Move to Last hidden for last tab
- Move to First click calls onUnifiedTabReorder
- Move to Last click calls onUnifiedTabReorder
- Middle-click closes file tabs
Verify that both AI tabs and file tabs use identical active indicator
styling (bright background connecting to content area). The visual
difference is correctly the extension badge, not the active state.

Added 3 tests:
- applies same active styling to both AI tabs and file tabs
- applies same inactive styling to both AI tabs and file tabs
- file tab displays extension badge with file extension text
- Verified FileTab's handleMouseDown correctly handles button === 1 (middle-click)
- Added test: left-click does NOT close file tab
- Added test: right-click does NOT close file tab
- Added test: middle-click on AI tab still works in unified mode
- All 115 TabBar tests pass
Connect FilePreviewTab rendering to the unified tab system:
- Add unified tab props to MainPanel (unifiedTabs, activeFileTabId, etc.)
- Remove !previewFile condition so TabBar always renders in AI mode
- Update content area to prioritize activeFileTabId over legacy previewFile
- Add showCloseButton prop to FilePreview (false when rendered as tab)
- Add activeFileTab computation in App.tsx
- Connect handlers through useMainPanelProps hook
Store file content directly on FilePreviewTab (Option A) for simplicity.
File previews are typically small, and we already store larger AI logs.
Updated handleOpenFileTab to populate content when opening file tabs.
- Fix MainPanel to use activeFileTab.content as source (was using empty editContent)
- Add sshRemoteId and isLoading fields to FilePreviewTab interface
- Update handleOpenFileTab to accept optional sshRemoteId parameter
- Add handleOpenFileTabAsync for SSH files with async loading:
  - Creates tab immediately with loading state
  - Fetches content asynchronously
  - Updates tab when content loaded (or removes on error)
- Add loading state UI in MainPanel for file tabs
- Add tests for file tab content storage and SSH loading support

This enables proper file content management for the unified tab system,
with support for SSH remote files that need async loading.
- Add `lastModified: number` field to FilePreviewTab interface to track
  when content was loaded from disk
- Add `fileTabAutoRefreshEnabled` setting (default: disabled) to control
  whether file tabs auto-refresh when switched to
- Update handleOpenFileTab to accept and store lastModified
- Update handleOpenFileTabAsync to fetch file stat and set lastModified
  from the file's actual modification time
- Modify handleSelectFileTab to check if file changed on disk when
  auto-refresh is enabled:
  - Compares current file mtime with stored lastModified
  - Refreshes content if file was modified since last load
  - Skips refresh if tab has unsaved edits to prevent data loss
- Update all FilePreviewTab test fixtures with lastModified field
- Renamed handleCloseFileTab to forceCloseFileTab (performs unconditional close)
- Added new handleCloseFileTab wrapper that checks for unsaved changes
- When tab.editContent !== undefined, shows confirmation modal before closing
- Uses existing showConfirmation pattern with setConfirmModal* state
- Modal displays: "<filename>" has unsaved changes. Are you sure you want to close it?
- On confirm, calls forceCloseFileTab to perform actual close
- Added initialScrollTop and onScrollPositionChange props to FilePreview
- Implemented throttled scroll position reporting (200ms) in content scroll handler
- Added scroll position restoration with requestAnimationFrame for DOM readiness
- Created handleFileTabScrollPositionChange handler in App.tsx
- Wired handler through useMainPanelProps to MainPanel
- Added 4 tests for scroll position persistence behavior
- Add initialSearchQuery and onSearchQueryChange props to FilePreview
- Auto-open search panel when initialSearchQuery is provided and non-empty
- Add handleFileTabSearchQueryChange handler in App.tsx
- Wire search state through useMainPanelProps and MainPanel
- Add 4 tests for search state persistence in FilePreview.test.tsx
Update keyboard shortcuts to navigate through unified tab order
(both AI and file tabs) instead of just AI tabs:

- Add navigateToUnifiedTabByIndex() for Cmd+1-9 tab jumping
- Add navigateToLastUnifiedTab() for Cmd+0 (last tab)
- Functions handle AI tabs (clear activeFileTabId) and file tabs
  (preserve activeTabId for easy switching back)
- Update useMainKeyboardHandler to use new unified functions
- Add comprehensive test coverage (18 new tests)
Add unified closed tab history system that tracks both AI and file
preview tabs, enabling Cmd+Shift+T to restore any recently closed tab:

- Add ClosedTabEntry discriminated union type for AI/file tabs
- Add unifiedClosedTabHistory to Session (runtime-only, not persisted)
- Add closeFileTab() function with unified history support
- Add addAiTabToUnifiedHistory() for AI tab close tracking
- Add reopenUnifiedClosedTab() with duplicate detection for both types
- Update closeTab() to also update unifiedTabOrder
- Update forceCloseFileTab() to use helper and add to history
- Update performTabClose() to track AI tabs in unified history
- Add comprehensive test coverage (12 new tests)
- Create navigateToNextUnifiedTab() and navigateToPrevUnifiedTab() functions
  that cycle through unifiedTabOrder (both AI and file tabs)
- Update keyboard handler to use new unified navigation functions
- Support showUnreadOnly filter (AI tabs filtered, file tabs always navigable)
- Add comprehensive test coverage (20 new tests)
Updated TabSwitcherModal to display both AI and file preview tabs:
- Added fileTabs, activeFileTabId, and onFileTabSelect props
- File tabs show in "Open Tabs" mode with AI tabs, sorted alphabetically
- Each file tab displays: filename, extension badge with type-specific
  coloring, file path, "File" indicator, active/unsaved indicators
- Search works across file names, extensions, and paths
- Updated AppModals and App.tsx to pass file tabs and handlers
- Added 9 new tests (total 85 tests pass)
Added 17 new tests for keyboard shortcuts working with unified tab order:

- Cmd+W: Tests file tab closing when active, AI tab closing when no
  file tab active, and last AI tab protection
- Cmd+Shift+[/]: Tests navigation through unified tab order including
  file tabs, with showUnreadOnly filter support
- Cmd+1-9: Tests jumping to specific positions in unified tab order,
  including file tabs, and showUnreadOnly filter blocking
- Cmd+0: Tests jumping to last tab in unified order
- Cmd+Shift+T: Tests reopening from unified closed tab history
- Tests tab shortcuts are disabled in group chat and terminal mode

All 36 tests in useMainKeyboardHandler.test.ts pass (was 19).
Updated handleFileClick in useAppHandlers.ts to use the new tab-based
file preview system. When onOpenFileTab callback is provided, file clicks
now open tabs instead of using the legacy setPreviewFile overlay.

Changes:
- Added FileTabInfo interface and onOpenFileTab optional callback to
  UseAppHandlersDeps
- Modified handleFileClick to fetch file stat for lastModified timestamp
- handleFileClick now calls onOpenFileTab when provided, falling back to
  legacy setPreviewFile behavior otherwise
- Moved handleOpenFileTab definition in App.tsx to before useAppHandlers
  call and wired it as the onOpenFileTab callback
- Created comprehensive SessionContext.test.tsx with 15 unit tests
- Verifies each session maintains independent file preview tabs
- Tests session switching updates visible file tabs correctly
- Verifies switching back restores scroll position, search query, edit mode
- Tests active file tab ID is tracked per-session
- Verifies rapid session switching preserves state
Verify that the file tab system correctly allows the same file to be
open in multiple sessions simultaneously. Each session maintains its
own independent file tab state (scroll position, search query, edit
mode) even when viewing the same file path.

Tests added:
- allows the same file path to be open in different sessions simultaneously
- each session maintains independent state for the same file
- sessions can have different number of tabs for the same files
…sion badges

- Fix max-width truncation inconsistency: file tabs now use max-w-[120px] to match AI tabs
- Add theme-aware hover background: dark mode (rgba(255,255,255,0.08)), light mode (rgba(0,0,0,0.06))
- Enhance getExtensionColor() with theme-aware colors for both light and dark modes:
  - TypeScript/JavaScript, Markdown, JSON, CSS, HTML with contrast-appropriate colors
  - Add support for Python (teal), Rust (red), Go (cyan), Shell (gray)
  - Unknown extensions fall back to theme border/textDim colors
- Add 10 unit tests verifying extension badge colors across themes
Use Wong's colorblind-safe palette for file extension badges when
colorBlindMode is enabled. Colors are distinguishable across
protanopia, deuteranopia, and tritanopia color vision deficiencies.

- Add COLORBLIND_EXTENSION_PALETTE to colorblindPalettes.ts
- Update getExtensionColor() in TabBar and TabSwitcherModal
- Pass colorBlindMode through App → MainPanel → TabBar
- Add 11 unit tests for colorblind extension badge colors
- Add comprehensive "File Preview Tab System" section to ARCHITECTURE.md
  documenting features, interfaces, unified tab system, session fields,
  behavior, extension color mapping, and key files
- Update CLAUDE-SESSION.md with FilePreviewTab interface and session fields
- Add file preview tab modification entry to CLAUDE.md Key Files table
Added 7 comprehensive performance-focused unit tests for the file tab system:
- Renders 15 file tabs without performance issues
- Renders 30 file tabs with mixed AI tabs (interleaved)
- Selects file tab correctly among many tabs
- Closes file tab correctly among many tabs
- Supports drag and drop reorder with many file tabs
- Renders file tabs with different extensions correctly
- Maintains active tab styling among many tabs

Verified existing performance optimizations:
- React.memo() wrapping on Tab and FileTab components
- useMemo() for computed values (display names, styles, extension colors)
- useCallback() for all event handlers
- Large file content truncation (100KB limit for syntax highlighting)
- Add isGeneratingName property to AITab interface
- Show Loader2 spinner in tab while name generation is in progress
- Set isGeneratingName true before API call, false on completion/error
- Spinner only shows when automatic tab naming is enabled
Implements file-specific overlay menu for FileTab component:
- Add shell:showItemInFolder IPC handler for "Reveal in Finder" action
- Add showItemInFolder to shell preload API and global.d.ts types
- Extend FileTabProps with position and close action callbacks
- Implement full overlay menu with file actions:
  - Copy File Path/Name to clipboard
  - Open in Default App via file:// URL
  - Reveal in Finder via new showItemInFolder API
  - Move to First/Last Position
  - Close Tab/Other/Left/Right actions
- Add 10 tests for file tab overlay menu
- Add 3 tests for shell:showItemInFolder handler
- Fix dual border issue where both AI tab and file tab showed active
  styling when file tab was selected. AI tabs now only show border when
  activeFileTabId is null.
- Update extension badge styling: remove leading dot, uppercase letters,
  smaller font (9px), reduced padding for minimal height impact.
- Update tests to expect new uppercase extension badge format (e.g.,
  'TS' instead of '.ts').
- Fix new tab creation (Cmd+T) not appearing in unified tab bar by
  adding new AI tabs to unifiedTabOrder in createTab()
- Fix new AI tab not being displayed when file tab was active by
  clearing activeFileTabId in createTab()
- Fix AI tab isActive check to handle undefined activeFileTabId
  (use !activeFileTabId instead of === null)
- Remove redundant filename/path header from file tab context menu
  (info already shown in tab and file preview subheading)
- Use JavaScript toUpperCase() for extension badges instead of CSS
  text-transform to ensure DOM content matches displayed text
- Update tests to use 'Copy File Path' selector instead of removed
  file-text-icon for identifying file tab overlays
Relocate the log level setting to be more appropriately placed in General
settings, positioned above the GitHub CLI Path setting.
The issue was caused by legacy overlay behavior in FilePreview component:
- useClickOutside hook was calling onClose when clicking outside the preview
- Layer registration was blocking lower layers and capturing focus

Added isTabMode prop to FilePreview to distinguish tab-based rendering:
- When isTabMode=true: disable click-outside-to-close behavior
- When isTabMode=true: don't block lower layers or aggressively capture focus
- When isTabMode=true: set focusTrap to 'none' and allowClickOutside to true

File preview tabs now persist until explicitly closed by:
- Clicking the X button on the tab
- Using Cmd+W keyboard shortcut
- Middle-clicking the tab
- Using context menu "Close Tab"

Updated tests to verify click-outside is disabled in tab mode.
Use theme colors to create vibrant, readable diagrams instead of washed-out
defaults. Nodes now have tinted backgrounds with prominent accent-colored
borders, and different node types use distinct colors (accent, success,
warning) for visual variety.

- Add blendColors() and transparentize() helpers for color mixing
- Use accent color for primary nodes, success for secondary, warning for tertiary
- Connection lines use accent color instead of dim text
- Enhanced styling for flowcharts, sequence diagrams, Gantt charts, pie charts,
  ER diagrams, git graphs, quadrant charts, and Sankey diagrams
- Better edge label backgrounds for readability
- Extended pie chart palette to 12 distinct colors
Tab management shortcuts (Cmd+T new tab, Cmd+W close tab) now work
when viewing a file preview tab. Previously these shortcuts were
blocked because file preview registers as an overlay in the layer
stack. Added these to the allowlist of shortcuts that work when
overlays (but not modals) are open.
…tration

Tab behavior changes:
- Escape key no longer closes file tabs (only closes internal UI like
  search and TOC overlay). Use Cmd+W or close button to close tabs.
- File preview in tab mode skips layer stack registration entirely
  since tabs are main content, not overlays. This prevents tabs from
  intercepting keyboard shortcuts or participating in overlay logic.

Updated tests to verify tab-mode Escape behavior.
Added isTabSwitcherShortcut to the overlay allowlist so users can
open the tab switcher modal while viewing a file preview tab.
Add onWheel stopPropagation to TOC overlay container so mouse wheel
events over the ToC scroll only the ToC, not the underlying file
content. Also update tests to reflect the previous change where ToC
stays open when clicking items (dismiss via click outside or Escape).
Extended isTabManagementShortcut to include Cmd+Shift+T for reopening
closed tabs when overlays are open. The closeFileTab helper already
adds file tabs to unifiedClosedTabHistory, and reopenUnifiedClosedTab
handles restoring both AI and file tabs. This ensures Cmd+Shift+T works
from file preview and other overlay contexts.
Added global image cache to MarkdownImage component to prevent
re-fetching images on re-renders, which was causing scrollbar
flickering. Key changes:

- Global imageCache Map stores loaded images with TTL (10 min)
- Images are cached by resolved path, including dimensions
- Loading placeholder has min-height/min-width to reduce layout shift
- Loaded images use aspectRatio from cached dimensions
- onLoad handler captures and caches natural dimensions

This eliminates the flickering/scrollbar jumping when viewing
markdown files with embedded images.
Added overscrollBehavior: 'contain' to both the main content
container and the TOC overlay's scrollable entries section. Also
added onWheel stopPropagation to the TOC entries div to fully
prevent scroll events from leaking between the ToC overlay and
the main file preview content. This prevents scroll chaining that
could cause scrollbar flickering.
Regular click on file links replaces current tab content, while
Cmd/Ctrl+Click opens a new tab adjacent to the current tab.
Replace backdrop-based click-outside detection with useClickOutside hook.
The previous fixed backdrop div intercepted all pointer events including
wheel events, preventing file content from scrolling while ToC was open.
Now wheel events over file content scroll the content, while wheel events
over the ToC scroll the ToC list.
- Added FilePreviewHistoryEntry type to track navigation history per tab
- Extended FilePreviewTab with navigationHistory and navigationIndex fields
- Updated handleOpenFileTab to build navigation history when replacing content
- Added handleFileTabNavigateBack/Forward/ToIndex handlers for per-tab nav
- Wired navigation props through MainPanel to FilePreview component
- Each file tab maintains its own independent navigation history
After saving a file in the file preview tab, the UI was reverting to the
original content despite showing "Saved". This occurred because:
- editContent was cleared to undefined after save
- The base content field was never updated to the saved value
- UI fell back to stale original content

Added savedContent parameter to handleFileTabEditContentChange to update
the tab's base content alongside clearing editContent after save.
@claude
Copy link
Copy Markdown

claude bot commented Feb 2, 2026

Pull Request Review: In-Tab File Preview (#281)

Overview

This is a substantial feature addition that implements a unified tab system allowing AI tabs and file preview tabs to coexist in the same tab bar. The implementation is comprehensive, well-tested, and follows the project's architectural patterns.

Summary:Approved with minor suggestions


Code Quality: ⭐⭐⭐⭐⭐ (Excellent)

Strengths

  1. Excellent Test Coverage

    • 2,769 new tests in TabBar.test.tsx alone
    • Comprehensive coverage of edge cases (drag-and-drop, keyboard shortcuts, session switching)
    • Tests for accessibility features (colorblind mode)
    • Performance tests for many tabs (10+)
  2. Type Safety

    • Proper discriminated unions (UnifiedTab type)
    • Clear interfaces for FilePreviewTab and UnifiedTabRef
    • Type guards used correctly throughout
  3. Documentation

    • Detailed commit messages explaining each phase
    • ARCHITECTURE.md updated with file preview tab system section
    • CLAUDE-SESSION.md updated with new session fields
    • Inline comments explaining complex logic
  4. Performance Optimizations

    • React.memo() for Tab and FileTab components
    • useMemo() for computed values
    • useCallback() for event handlers
    • Image caching with TTL to prevent flickering (global imageCache)
    • Throttled scroll position reporting (200ms)
  5. Incremental Implementation

    • 62 commits organized into logical phases
    • Each commit builds on the previous foundation
    • Easy to understand the evolution of the feature

Potential Issues & Suggestions

🟡 Minor: Memory Management

Issue: File content is stored directly on FilePreviewTab

export interface FilePreviewTab {
  // ...
  content: string;  // Could be large for big files
  editContent: string | undefined;  // Duplicates content when editing
}

Location: src/renderer/types/index.ts:442-460

Suggestion: Consider implementing:

  • A content size limit (you have 100KB for syntax highlighting, but content is still stored)
  • Periodic cleanup of file tabs not accessed recently
  • Warning users when total tab memory exceeds threshold

Severity: Low - Acceptable for typical usage, but could be an issue with many large files open


🟡 Minor: Navigation History Memory Leak Risk

Issue: Each file tab maintains its own navigation history, but there's no limit on history size

export interface FilePreviewTab {
  // ...
  navigationHistory: FilePreviewHistoryEntry[];  // No max size
  navigationIndex: number;
}

Location: src/renderer/types/index.ts:442-460

Suggestion: Implement a max history size (similar to MAX_CLOSED_TAB_HISTORY = 25)

Severity: Low - Would take extensive navigation to cause issues


🟢 Good: Unified Closed Tab History

Positive: The unified closed tab history (unifiedClosedTabHistory) correctly limits to 25 entries, preventing unbounded growth:

const MAX_CLOSED_TAB_HISTORY = 25;

Location: src/renderer/utils/tabHelpers.ts:30


🟡 Minor: Duplicate Content Storage During Edit

Issue: When editing a file, content is stored twice:

  • FilePreviewTab.content (original)
  • FilePreviewTab.editContent (edited version)

Location: src/renderer/types/index.ts:457

Suggestion: Consider using a diff/patch approach for large files, or at least document this trade-off

Severity: Low - Acceptable trade-off for simplicity


🟢 Good: SSH Remote Loading State

Positive: Excellent handling of async file loading for SSH remote files with proper loading states:

export interface FilePreviewTab {
  // ...
  isLoading: boolean;
  sshRemoteId?: string;
}

This prevents UI jank and provides good UX feedback.

Location: src/renderer/App.tsx:5377-5440


🟡 Minor: Global Image Cache Cleanup

Issue: Image cache cleanup runs on a setInterval at module level, which means it persists even if all file tabs are closed

Location: src/renderer/components/FilePreview.tsx:59-66

setInterval(() => {
  const now = Date.now();
  for (const [key, value] of imageCache.entries()) {
    if (now - value.loadedAt > IMAGE_CACHE_TTL) {
      imageCache.delete(key);
    }
  }
}, IMAGE_CACHE_TTL);

Suggestion: Consider using a more sophisticated cleanup strategy (e.g., clear on component unmount if no tabs remain, or use WeakMap if possible)

Severity: Very Low - 10 minute interval is reasonable, minimal resource impact


🔵 Code Style: Debug Comments

Issue: Several DEBUG comments left in production code

// DEBUG: Log context calculation details
// DEBUG: Log to trace progress updates
// DEBUG: Log MainPanel context display calculation

Location: Multiple files in src/renderer/

Suggestion: Either remove or convert to proper logging with log levels

Severity: Very Low - Doesn't affect functionality


Security Considerations: ✅ No Issues

  • File path handling appears safe (no eval, exec, or command injection vectors)
  • SSH remote execution properly uses existing wrapSpawnWithSsh pattern
  • No new external dependencies introduced
  • File content is properly validated before display

Performance: ⭐⭐⭐⭐⭐ (Excellent)

Strengths

  1. Memoization Strategy

    • unifiedTabs computed with proper dependencies
    • Extension colors memoized
    • Tab display names computed once
  2. Event Handler Optimization

    • All handlers wrapped in useCallback with proper dependencies
    • Stable references prevent unnecessary re-renders
  3. Rendering Optimization

    • React.memo on Tab and FileTab components
    • Proper key usage in lists (tab.id)
    • Conditional rendering to avoid unnecessary work
  4. Scroll Performance

    • Throttled scroll position updates (200ms)
    • requestAnimationFrame for scroll restoration

Test Evidence

  • Tests verify rendering 30 tabs without performance issues
  • Drag-and-drop works correctly with many tabs

Best Practices Adherence: ⭐⭐⭐⭐⭐ (Excellent)

Follows Project Conventions ✅

  1. Session Management Pattern

    • All 13 session creation locations updated consistently
    • Session restoration properly hydrates new fields
  2. Layer Stack Integration

    • FilePreview correctly skips layer registration in tab mode
    • Proper allowlist for keyboard shortcuts in overlay mode
  3. SSH Remote Support

    • Properly checks for sshRemoteConfig
    • Uses handleOpenFileTabAsync for remote files
  4. Settings Integration

    • New setting: fileTabAutoRefreshEnabled (default: disabled)
    • Follows existing settings patterns
  5. Keyboard Shortcuts

    • Cmd+W, Cmd+Shift+T, Cmd+1-9, Cmd+0, Cmd+Shift+[/] all work with unified tabs
    • Shortcuts properly allowlisted when overlays are open

Architecture: ⭐⭐⭐⭐⭐ (Excellent)

Design Decisions

  1. Discriminated Union for Unified Tabs

    type UnifiedTab =
      | { type: 'ai'; id: string; data: AITab }
      | { type: 'file'; id: string; data: FilePreviewTab };

    This is the correct approach - type-safe and extensible for future tab types (terminal tabs mentioned in PR description)

  2. Separate Storage Arrays

    • session.aiTabs and session.filePreviewTabs stored separately
    • session.unifiedTabOrder for visual ordering
    • Maintains type safety while allowing flexible ordering
  3. Per-Tab State

    • Each file tab tracks its own: scroll position, search query, edit state, navigation history
    • Proper isolation between tabs
  4. Active Tab Tracking

    • activeTabId for AI tabs (preserved when file tab active)
    • activeFileTabId for file tabs (null when AI tab active)
    • Clean mutual exclusivity

Testing: ⭐⭐⭐⭐⭐ (Excellent)

Coverage Breakdown

  • TabBar: 2,769 new tests (drag-drop, overlay menus, extension badges, unified rendering)
  • MainPanel: 151 additions (file tab rendering, loading states)
  • FilePreview: 307 additions (scroll persistence, search persistence, tab mode behavior)
  • useMainKeyboardHandler: 639 additions (all keyboard shortcuts with unified tabs)
  • tabHelpers: 905 additions (unified navigation, closed tab history)
  • SessionContext: 733 new tests (session switching with file tabs)
  • useDebouncedPersistence: 333 additions (file tab persistence)
  • TabSwitcherModal: 258 additions (file tabs in Cmd+Shift+O)

Test Quality

  • Edge cases covered (last tab protection, duplicate detection)
  • Session switching tested thoroughly
  • Colorblind mode tested
  • SSH loading states tested
  • Performance scenarios tested (30+ tabs)

Migration & Compatibility: ✅ Excellent

Backward Compatibility

  • Legacy previewFile overlay removed cleanly
  • All session creation points updated (13 locations)
  • Graceful handling of sessions without new fields:
    filePreviewTabs: correctedSession.filePreviewTabs || [],
    activeFileTabId: correctedSession.activeFileTabId ?? null,
    unifiedTabOrder: correctedSession.unifiedTabOrder || 
      resetAiTabs.map((tab) => ({ type: 'ai' as const, id: tab.id }))

Recommendations

Must Address Before Merge: None ✅

The PR is production-ready as-is.

Should Address Soon:

  1. Consider adding memory usage monitoring/warnings for file tabs
  2. Add max size for navigationHistory array
  3. Clean up DEBUG comments

Nice to Have:

  1. Add metrics tracking for file tab usage (how many users open, average number of tabs)
  2. Consider virtualization if users frequently open 50+ tabs
  3. Document memory trade-offs in ARCHITECTURE.md

Final Verdict: ✅ APPROVED

This is exemplary work:

  • Well-architected with clear separation of concerns
  • Thoroughly tested with excellent coverage
  • Properly documented
  • Performance-optimized
  • Follows project conventions
  • No security concerns
  • Backward compatible

The minor issues noted are truly minor and don't block merging. This is one of the best-structured PRs I've reviewed.

Recommendation: Merge with confidence. Address the minor suggestions in follow-up work if desired.


Additional Notes

The phased approach (62 commits) makes this large change reviewable:

  1. Phase 1: Data model
  2. Phase 2-3: Tab operations
  3. Phase 4-5: UI integration
  4. Phase 6-20: Feature completion
  5. Final phases: Bug fixes and polish

This is a textbook example of how to implement a large feature incrementally.

Great work! 🎉

@pedramamini pedramamini merged commit 11721c4 into 0.15.0-rc Feb 2, 2026
2 checks passed
@pedramamini pedramamini deleted the in-tab-file-preview branch February 2, 2026 23:31
@claude
Copy link
Copy Markdown

claude bot commented Feb 2, 2026

Comprehensive PR Review: File Preview Tab System

Executive Summary

This PR introduces a well-architected file preview tab system that successfully integrates with the existing AI tab infrastructure. The implementation is production-ready with some recommended improvements. Code quality is high, test coverage is good, and security concerns are minimal.

Overall Assessment: ⭐⭐⭐⭐ (4/5 stars)

Recommendation: Approve with required changes. Address the 4 high-priority issues below before merging.


🔴 High Priority Issues (Fix Before Merge)

1. CRITICAL: Global setInterval Memory Leak

File: src/renderer/components/FilePreview.tsx:60-67

// Clean up old cache entries periodically
setInterval(() => {
	const now = Date.now();
	for (const [key, value] of imageCache.entries()) {
		if (now - value.loadedAt > IMAGE_CACHE_TTL) {
			imageCache.delete(key);
		}
	}
}, IMAGE_CACHE_TTL);

Problem: This setInterval runs at module scope and can never be cleared. During hot-reload, multiple intervals will stack up. In production, wastes CPU cycles when no FilePreview instances are mounted.

Fix: Use lazy cleanup on cache access instead:

function cleanupImageCache() {
  const now = Date.now();
  for (const [key, value] of imageCache.entries()) {
    if (now - value.loadedAt > IMAGE_CACHE_TTL) {
      imageCache.delete(key);
    }
  }
}
// Call in ImageDisplay useEffect before accessing cache

2. BUG: Stale Closure in Scroll Save Timer

File: src/renderer/components/FilePreview.tsx:1046-1049

Problem: The scroll save timeout references onScrollPositionChange from the effect closure. If onScrollPositionChange changes, the effect re-runs but pending setTimeout callbacks still use the old function.

Fix:

const onScrollPositionChangeRef = useRef(onScrollPositionChange);
useEffect(() => {
  onScrollPositionChangeRef.current = onScrollPositionChange;
}, [onScrollPositionChange]);

// Then use onScrollPositionChangeRef.current in setTimeout

3. SECURITY: Potential Path Traversal

File: src/renderer/hooks/ui/useAppHandlers.ts:188-190

const treeRoot = activeSession.projectRoot || activeSession.fullPath;
const fullPath = `${treeRoot}/${path}`;

Problem: Concatenating paths without validation could allow traversal (e.g., ../../etc/passwd).

Fix:

import * as path from 'path';

const treeRoot = activeSession.projectRoot || activeSession.fullPath;
const fullPath = path.normalize(path.join(treeRoot, path));
if (!fullPath.startsWith(path.normalize(treeRoot))) {
  console.error('Path traversal attempt detected:', path);
  return;
}

4. BUG: Empty File Extension Handling

File: src/renderer/App.tsx:4025-4030

const extension = file.name.includes('.')
  ? '.' + file.name.split('.').pop()
  : '';
const nameWithoutExtension = extension
  ? file.name.slice(0, -extension.length)
  : file.name;

Problem: Files like .gitignore will be parsed as:

  • extension: .gitignore
  • nameWithoutExtension: `` (empty string!)

Fix:

const lastDotIndex = file.name.lastIndexOf('.');
const extension = lastDotIndex > 0 ? file.name.substring(lastDotIndex) : '';
const nameWithoutExtension = lastDotIndex > 0 
  ? file.name.substring(0, lastDotIndex)
  : file.name;

🟡 Medium Priority Issues (Fix Soon)

5. PERFORMANCE: Unnecessary Re-renders in FilePreview

File: src/renderer/components/FilePreview.tsx

The component is 2,421 lines and accepts 30+ props but isn't memoized. Every parent re-render triggers a full FilePreview re-render.

Recommendation:

export const FilePreview = React.memo(
  forwardRef<FilePreviewHandle, FilePreviewProps>((props, ref) => { ... }),
  (prev, next) => prev.file?.path === next.file?.path // Custom comparison
);

6. BUG: Missing Error Handling for File Save

File: src/renderer/components/FilePreview.tsx:1012-1030

Save failures only show a 2-second toast. Users might not notice critical errors (permissions, disk full, SSH timeout) and lose work.

Recommendation:

  • Show modal for critical errors
  • Include error details in notification
  • Add retry functionality for transient errors

7. Race Condition: Duplicate Tab Prevention

File: src/renderer/App.tsx:3923-3943

If the same file is clicked rapidly before setState completes, two tabs could be created. The duplicate check happens in setState callback, which is not atomic.

Fix:

const inFlightFileOpens = useRef(new Set<string>());

// Before opening:
if (inFlightFileOpens.current.has(file.path)) return;
inFlightFileOpens.current.add(file.path);

// After setState:
inFlightFileOpens.current.delete(file.path);

8. TESTING: Missing Navigation History Tests

File: src/renderer/App.tsx:3960-4014

Complex navigation history logic (breadcrumb back/forward) has no test coverage. High risk of off-by-one errors.

Recommendation: Add tests for:

  • Maintaining history when replacing tab content
  • Truncating forward history when navigating from middle
  • Restoring scroll position when navigating back

9. TESTING: Missing SSH Remote File Tests

No tests for:

  • Loading states during fetch
  • Connection error handling
  • What happens if connection drops mid-read/write

✅ Strengths

Code Quality

  • Excellent adherence to documented patterns (CLAUDE.md, ARCHITECTURE.md)
  • Clean separation of concerns (tabHelpers.ts contains pure logic)
  • Strong TypeScript usage with proper discriminated unions
  • Proper layer stack integration with correct priority handling

Performance

  • Large file handling with 1MB token skip threshold and 100KB syntax highlighting limit
  • Debounced persistence at 200ms for scroll saves (consistent with TerminalOutput)
  • Passive scroll listeners for better scrolling performance

Security

  • No XSS vulnerabilities (no dangerouslySetInnerHTML)
  • No command injection (uses safe IPC to main process)
  • SSH operations properly delegated to main process

Test Coverage

  • 175+ test cases in tabHelpers.test.ts covering core logic
  • Tests for tab creation, closing, reopening, navigation, edge cases
  • Good coverage of display name logic and filter buttons

📝 Low Priority Improvements (Future)

  1. Add virtualization for very large files (>500KB) to prevent DOM freezing
  2. Move nested components (like ImageDisplay) to module scope
  3. Migrate scroll debouncing to use useDebouncedPersistence hook
  4. Add retry functionality for failed saves
  5. Document SSH remote save behavior more clearly

Summary

This is a well-implemented feature that successfully extends Maestro's architecture. The unified tab system is elegant, state management is solid, and SSH integration follows documented patterns. The critical issues are straightforward to fix:

  1. Memory leak in image cache cleanup (easy fix)
  2. Stale closure in scroll handler (standard fix)
  3. Path traversal protection (security best practice)
  4. Edge case in file name parsing (simple logic fix)

With these fixes, this will be a robust production feature that achieves its goal: "File content and AI content can now live side by side." 🎉

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.

1 participant