Skip to content

Native file explorer sidebar, single-file Monaco editor, file search#2052

Closed
imadbz wants to merge 16 commits intomanaflow-ai:mainfrom
imadbz:experiment/native-explorer
Closed

Native file explorer sidebar, single-file Monaco editor, file search#2052
imadbz wants to merge 16 commits intomanaflow-ai:mainfrom
imadbz:experiment/native-explorer

Conversation

@imadbz
Copy link
Copy Markdown

@imadbz imadbz commented Mar 24, 2026

Summary

  • Native SwiftUI file explorer in a tabbed sidebar replacing the WebView-based explorer
  • Single-file Monaco editor — stripped from full IDE to lightweight file viewer with pre-warmed WebView pool for instant opens
  • File search across all workspace directories with Cmd+Shift+F shortcut
  • VS Code preview tab behavior — single-click reuses preview tab (italic title), double-click or edit pins it

Changes

Sidebar

  • Three-tab sidebar: Workspaces | Explorer | Search
  • Explorer shows all unique panel directories within the current workspace as collapsible roots
  • FSEvents file watching with debounced refresh (no polling)
  • Git status with .gitignore greying, folder status bubbling (conflict > modified > deleted > added > untracked)
  • Flat list rendering for instant expand/collapse (no recursive SwiftUI views)

Editor

  • Single-file Monaco panel — no built-in sidebar, tabs, or folder watching
  • Swift reads files directly and injects content into Monaco (zero JS bridge round-trips)
  • Pre-warmed WKWebView pool (2 instances at startup) for instant editor tab creation
  • Large file guard (>1MB or >50k lines shows message instead of loading)
  • Session persistence of open file path
  • Disabled conflicting Monaco shortcuts (Cmd+Shift+F, Cmd+P, Cmd+Shift+P, Cmd+N)

Bonsplit

  • Added isItalic property to Tab for preview tab italic title rendering

Test plan

  • Open file explorer tab, verify tree loads with git status decorations
  • Click file → opens in preview editor (italic tab title)
  • Double-click file → pins editor tab (normal title)
  • Edit file → auto-pins, dirty indicator shows
  • Cmd+S saves file
  • Cmd+Shift+F opens search tab with focus
  • Switch workspaces → explorer updates to show that workspace's directories
  • Terminal cd → new directory appears in explorer after ~2s
  • Large files show size message instead of loading
  • Session restore preserves open editor file
  • .gitignore'd files/folders appear greyed out
  • Folders never show strikethrough even with deleted children

🤖 Generated with Claude Code


Summary by cubic

Adds a native SwiftUI file explorer sidebar, a lightweight single-file Monaco editor with instant open and VS Code–style preview tabs, plus workspace-wide file search. Improves navigation speed, git visibility, and editing ergonomics.

  • New Features

    • Sidebar: multi-root explorer with git decorations, .gitignore greying, instant expand/collapse, and FSEvents-based updates.
    • Editor: single-file Monaco viewer with preview tab behavior (click = preview, double‑click/edit = pin), pre‑warmed WKWebView pool for instant opens, large‑file guard, and session restore.
    • Search: file search across all workspace roots; Cmd+Shift+F opens the Search tab. Conflicting Monaco shortcuts are disabled.
  • Bug Fixes

    • Hardened file I/O: prevent path traversal via symlink-aware validation and stricter root prefix checks.
    • Fixed startup JSON serialization crash and removed manual JS string escaping to avoid injection issues.
    • Prevent terminal from stealing focus when sidebar text fields are active.
    • Avoid git status deadlocks by reading process output before waiting.
    • Respect focus guards for socket-triggered actions.

Written for commit 4701a85. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Integrated code editor with syntax highlighting and file editing capabilities
    • Native file explorer sidebar for browsing and managing project files
    • File search functionality to quickly locate files across projects
    • Sidebar tab navigation to switch between workspaces, explorer, and search views
    • New keyboard shortcuts: Cmd+Shift+E to open editor, Cmd+Shift+F for search

imadbz and others added 16 commits March 15, 2026 00:45
New editor panel type that embeds Monaco Editor in a WKWebView, providing
a VS Code-like file explorer and code editor integrated into cmux.

Features:
- Monaco Editor with syntax highlighting for 30+ languages
- File tree sidebar with expand/collapse, sorted (dirs first)
- Tabbed editor with dirty state indicators
- File watching (2s polling, auto-refresh on changes)
- New file/folder creation via context menu, header buttons, or Cmd+N
- Cmd+S save, Cmd+W close tab
- Swift-to-JS bridge for sandboxed file I/O with path traversal protection
- Session persistence and restore for editor panels
- Keyboard shortcut Cmd+Shift+E to open editor for current project
- Socket command: open_editor [path]
- Claude launcher button (sparkle icon) that opens terminal with
  claude --dangerously-skip-permissions
- Editor and Claude buttons added to pane tab bar alongside
  existing terminal and browser buttons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major upgrade to the Monaco editor panel:

Design:
- VS Code Dark Modern exact dimensions (22px rows, 16x16 icons, indent guides)
- Codicon font for all icons (files, folders, chevrons, buttons)
- File type-colored icons for 20+ extensions
- VS Code interaction states (hover, active selection, inactive selection)

Git integration:
- git status --porcelain parsing with M/A/D/U/R/! badges
- Exact VS Code git decoration colors (#e2c08d modified, #81b88b added, etc.)
- Ignored files faded at 0.4 opacity
- Deleted files with strikethrough
- Parent folder dot indicators bubbled from child status
- .gitignore respected via git ls-files --ignored

Features:
- Multi-select with Cmd+Click (toggle) and Shift+Click (range)
- Mouse-based drag-and-drop for moving files/folders (WKWebView compatible)
- Auto-expand folders on 500ms drag hover
- Inline rename with F2 selection cycling (stem -> full -> ext)
- Context menu: New File, New Folder, Rename, Delete, Copy Path
- File delete and rename with open tab updates
- File watching pauses during inline input and drag operations

Theme integration:
- Reads Ghostty terminal background color on startup
- Derives all UI colors (sidebar, editor, tabs, borders) from terminal theme
- Listens for ghosttyDefaultBackgroundDidChange notifications
- Monaco editor theme updates dynamically
- Auto-detects light/dark mode from background brightness

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Path traversal: use resolvingSymlinksInPath() instead of standardizingPath
  to prevent symlink-based directory escape
- JS injection: use JSONSerialization for all Swift-to-JS string transport
  instead of manual escaping (which missed newlines and control chars)
- Pipe deadlock: read git process output before waitUntilExit to prevent
  blocking when git status produces large output
- Path prefix check: use exact match or slash-delimited prefix to prevent
  rootPath="/foo" matching "/foobar"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JSONSerialization.data(withJSONObject:) does not accept bare strings as
top-level objects. Wrap in array and strip brackets for jsStringLiteral.
Also simplify sendResponse to pass JSON data directly without re-encoding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Dirty tab close: confirm before discarding unsaved changes
- Dirty tracking: closure references fileEntry directly, survives rename
- Git conflict ordering: U/U, A/A, D/D checks before generic A/D
- createFile: check return value, error on failure
- Socket focus: respect socketCommandAllowsInAppFocusMutations()
- Poll guard: prevent overlapping async polls with pollRunning flag
- Drag dedup: filter nested selections when parent is also selected

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Block delete/rename of project root (empty path guard + canonical check)
- Show dotfiles except .git (was hiding all dotfiles)
- Folder rename propagates to all descendant open tabs
- Submodule commits on detached HEAD acknowledged as known limitation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	Sources/Workspace.swift
#	vendor/bonsplit
- Add file explorer WebView in the left sidebar with git status, tree
  navigation, context menus, inline rename, and new file/folder creation
- Clicking a file in the sidebar opens it as a new editor tab (one file
  per panel)
- Strip editor panel down to single-file Monaco: no built-in explorer,
  no tabs, no folder watching — just readFile/writeFile/save
- Editor auto-opens its file when Monaco signals ready (editorReady msg)
- Explorer uses its own JS bridge (cmuxExplorer) with self-contained
  file system operations separate from the editor's cmuxEditor bridge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Editor panel is now a minimal single-file Monaco viewer (no sidebar,
  no tabs, no folder watching)
- VS Code preview tab behavior: single-click reuses preview tab (italic
  title), double-click or editing pins it, pinned files open new tabs
- Pre-warm WKWebView pool (2 WebViews with Monaco loaded at startup) so
  editor tabs open instantly with zero flash
- Add isItalic property to Bonsplit Tab for italic tab title rendering
- Monaco loaded from CDN with language worker support for full syntax
  highlighting
- Explorer sidebar: double-click pins file, context menu rename moved
  to right-click only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sible

- Explorer shows all unique panel directories within the current
  workspace as collapsible root folders (switches on workspace change)
- Replace JS polling with native FSEvents file watching (50ms coalesce)
  for near-instant tree updates on file changes
- Diff-based refresh: only patches git badges in-place unless directory
  structure actually changed (no DOM teardown flash)
- Show all dotfiles (.gitignore, .env, etc.) — only .git is hidden
- Gitignored files/folders greyed out (40% opacity) including children
  of ignored directories
- Folders never get strikethrough even with deleted children
- Folder git status priority matches VS Code: conflict > modified >
  deleted > added > untracked > renamed
- Large file guard: files over 1MB or 50k lines show italic filename
  with size/line count message instead of loading Monaco
- Added statFile bridge for size check before content read
- Single git status command (--porcelain -unormal --ignored) replaces
  separate status + ls-files calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace WebView explorer with native SwiftUI file tree (lazy loading,
  git status, expand/collapse, gitignore greying, FSEvents watching)
- Add file search view with async search across all workspace roots
- Tabbed sidebar: workspaces | explorer | search with icon selector
- Cmd+Shift+F opens sidebar search tab
- Incremental refresh: git badges update in-place, structure diffed
  before reload, expanded state preserved across refreshes
- Disable Monaco Cmd+Shift+F/P/N shortcuts to avoid conflicts
- Fix traffic light padding for tabbed sidebar layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Flatten file tree into single ForEach for instant expand/collapse
  (no recursive views, LazyVStack diffs by stable node ID)
- Synchronous file read in openFileByPath — zero dispatch hops, content
  injected directly into Monaco via JSON-encoded evaluateJavaScript
- Fix crash: JSONSerialization wrap-in-array for safe string encoding
- Fix terminal stealing sidebar search focus: applyFirstResponderIfNeeded
  now skips when an NSTextField outside the terminal is being edited
- NSTextField-based search field (SidebarSearchField) for proper AppKit
  first responder handling
- Remove duplicate SidebarTopScrim from VerticalTabsSidebar — parent
  sidebarView owns the scrim for all three tabs
- Always show root headers in explorer (no 1-vs-many branch switch)
- loadChildrenForNode synchronous (directory listing is <1ms)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WebView pool makes MonacoCache redundant (Monaco pre-loaded at startup).
Swift reads files directly — JS bridge readFile/statFile no longer used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lorer

# Conflicts:
#	Sources/KeyboardShortcutSettings.swift
#	vendor/bonsplit
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 24, 2026

@imadbz is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Introduces a Monaco-based code editor and native file explorer sidebar to the application. The editor runs in a pre-warmed WebView pool, while the explorer provides file tree navigation with Git integration. Both integrate into a new sidebar tab system alongside workspace selection, with keyboard shortcuts (Cmd+Shift+E editor, Cmd+Shift+F search) and session persistence support.

Changes

Cohort / File(s) Summary
Editor WebView Infrastructure
Sources/Panels/EditorPanel.swift, Sources/Panels/EditorPanelView.swift, Sources/MonacoWebViewPool.swift
Implements Monaco editor pooling, message handler for file I/O and theme injection, Swift↔JS bridge, and SwiftUI rendering with focus-flash animation and pointer event monitoring.
Editor Web Assets
Resources/editor/editor.js, Resources/editor/index.html
Provides Monaco editor initialization, file open/save via Ctrl/Cmd+S, dirty state tracking, large-file detection, and theming via CSS variables.
Explorer Sidebar Infrastructure
Sources/Panels/ExplorerSidebarPanel.swift, Sources/Panels/ExplorerSidebarView.swift
Wraps file explorer in WKWebView with message handler for directory ops (read/create/delete/rename), Git status caching, FSEvents-based refresh, and theme injection.
Explorer Web Assets
Resources/explorer/explorer.js, Resources/explorer/explorer.css, Resources/explorer/index.html
Provides multi-root file tree rendering, expand/collapse, inline rename/create, context menu, Git badges, single/range/toggle selection, and theme support.
File Search Infrastructure
Sources/FileSearchView.swift
Implements async directory traversal with heavy-directory skipping (.git, node_modules, etc.), file search results model, text field binding, and result row rendering.
Sidebar Tab System & Integration
Sources/SidebarTabSelector.swift, Sources/ContentView.swift
Adds three-tab sidebar switch (Workspaces/Explorer/Search) with notification-driven switching, root path sync, and file open/pin event routing to preview or new editors.
Panel & Workspace Support
Sources/Panels/Panel.swift, Sources/Panels/PanelContentView.swift, Sources/Workspace.swift, Sources/SessionPersistence.swift
Adds .editor panel type, editor panel rendering in content view, editor surface creation/lifecycle, session snapshot capture/restore, and focus management.
Keyboard & Command Integration
Sources/AppDelegate.swift, Sources/KeyboardShortcutSettings.swift, Sources/TabManager.swift, Sources/TerminalController.swift
Binds Cmd+Shift+E (open editor) and Cmd+Shift+F (search) shortcuts, adds openEditor() API to TabManager, implements socket open_editor command, and adds .openEditor shortcut configuration.
Terminal View & Build Config
Sources/GhosttyTerminalView.swift, GhosttyTabs.xcodeproj/project.pbxproj
Prevents terminal from stealing focus during text field edits; registers new source files and resource folders in Xcode build phases.
Submodule Updates
ghostty, homebrew-cmux, vendor/bonsplit
Updates submodule commit pointers to upstream revisions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as AppDelegate<br/>(Main)
    participant TabMgr as TabManager
    participant WS as Workspace
    participant Pool as MonacoWebViewPool
    participant WV as EditorPanel<br/>(WKWebView)
    participant JS as editor.js<br/>(Monaco)
    participant FS as File System

    User->>App: Press Cmd+Shift+E
    App->>TabMgr: openEditor(rootPath:)
    TabMgr->>WS: newEditorSurface(inPane:rootPath:)
    WS->>Pool: take()
    alt WebView Pool Ready
        Pool-->>WS: (webView, handler)
        WS->>WV: openFileByPath(path)
    else No Pooled WebView
        Pool->>Pool: warmUp() async
        WS->>WV: create new WKWebView
        WV->>JS: load editor.html
        JS-->>WV: ready
    end
    WV->>FS: read file content (UTF-8)
    FS-->>WV: file bytes
    WV->>JS: window.cmux.openFileWithContent(path, name, content)
    JS->>JS: init Monaco model + language mode
    JS-->>WV: dirtyState notification
    WV->>WS: onDirtyStateChanged callback
    WS-->>App: editor panel ready
    App-->>User: editor displayed
Loading
sequenceDiagram
    participant User
    participant Explorer as ExplorerSidebarPanel<br/>(WKWebView)
    participant JS as explorer.js
    participant FS as File System
    participant Git as git command
    participant FSEvents as FSEvents

    User->>Explorer: Click refresh button
    Explorer->>JS: window.cmuxExplorer.refresh()
    JS->>Explorer: POST readDir(root)
    Explorer->>FS: FileManager.contentsOfDirectory
    FS-->>Explorer: [FileNode]
    Explorer->>Git: git status --porcelain=v1
    Git-->>Explorer: status lines
    Explorer->>Explorer: parse + cache git status
    Explorer->>JS: handleResponse(results, gitStatus)
    JS->>JS: render tree + git badges
    JS-->>User: updated tree displayed
    
    FS->>FSEvents: file changed event
    FSEvents->>Explorer: FSEventStream callback
    Explorer->>Explorer: debounce + refresh

    User->>JS: double-click file
    JS->>Explorer: POST openFile(path)
    Explorer-->>Explorer: onOpenFileExternal callback
    Explorer-->>User: file opened in editor (external)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #1951 — Modifies keyboard shortcut system in AppDelegate and KeyboardShortcutSettings; overlaps with editor/search shortcut additions.
  • #667 — Updates GhosttyBackgroundTheme which is used for theme color injection into Monaco and Explorer webviews.
  • #1898 — Introduces panel flash reason API (triggerFlash(reason:)); integrates with editor panel focus-flash animation.

Poem

🐰 A Monaco dream in a pool, warming and ready,
File trees that dance with Git badges steady,
Sidebar tabs flick—Explorer, Search, Workspaces three,
Cmd+Shift+E opens the editor spree! ✨📝🌳

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the three main features added in this changeset: native file explorer sidebar, single-file Monaco editor, and file search functionality.
Description check ✅ Passed The PR description comprehensively covers all required template sections: Summary with clear bullet points on what changed and why, detailed Changes section, Test plan with checkboxes, and additional context via cubic's summary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@imadbz imadbz closed this Mar 24, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR introduces a native SwiftUI file explorer sidebar, a lightweight Monaco-based single-file editor, and a file search panel — replacing the previous WebView-only explorer with a three-tab sidebar (Workspaces | Explorer | Search). The architecture is well thought out: pre-warmed WKWebView pool for instant editor opens, FSEventStreamRef-based file watching with debounce, path-traversal guards on all file operations, and VS Code-style preview/pin tab semantics.

Key concerns found:

  • File search blocks the main thread (P1): FileSearchViewModel.searchDirectory is async but has no internal await suspension points, and the enclosing Task {} inherits the @MainActor context. The entire recursive directory traversal runs synchronously on the main thread — on a large repo this will freeze the UI for the duration of the search. The task needs to be Task.detached or the search function made nonisolated.

  • Directory expand blocks the main thread (P1): FileTreeRoot.loadChildrenForNode calls Self.loadEntries(...) synchronously on the @MainActor, unlike loadChildren() which correctly dispatches to a background queue. Expanding any directory node blocks the main thread proportional to the number of children.

  • ExplorerSidebarPanel initialised but never rendered (P2): setupExplorerPanel() in ContentView.onAppear creates a WKWebView, loads the explorer HTML, registers a WKScriptMessageHandler, and starts FSEvents watching — but the .explorer sidebar tab renders NativeFileExplorerView, never ExplorerSidebarView. This leaks a WebView and an FSEvent stream at startup.

  • Incomplete JSON escaping in sendRootsToJS (P2): Folder names are interpolated with only double-quote escaping; backslash, newline, tab, and other JSON control characters in path components will produce malformed JSON or silent mis-decodes. Use JSONSerialization instead of manual string building.

  • Force cast on pooled WebView (P2): pooled.webView as! CmuxWebView will crash at runtime if the pool's stored type ever changes; the pool should vend CmuxWebView directly or use a conditional cast.

Confidence Score: 3/5

  • Safe to explore but two main-thread blocking issues should be fixed before shipping to avoid UI freezes on large repos
  • The feature set is substantial and well-architected overall (path-traversal guards, pool pre-warming, FSEvents debounce, proper model lifecycle in Monaco). However the two P1 issues — file search and directory expand both blocking the main thread — are reliability problems that will manifest in normal usage on medium-to-large codebases, and the ExplorerSidebarPanel resource leak fires unconditionally on every app launch. These need fixes before this lands in a production build.
  • Sources/FileSearchView.swift (main-thread blocking search), Sources/NativeFileExplorer.swift (main-thread loadChildrenForNode), Sources/ContentView.swift (unused ExplorerSidebarPanel setup)

Important Files Changed

Filename Overview
Sources/FileSearchView.swift New file search view — recursive search task runs entirely on the main actor, will freeze UI on large repos
Sources/NativeFileExplorer.swift New native SwiftUI file explorer with FSEvents watching; loadChildrenForNode does synchronous FS I/O on main thread unlike the background-dispatched loadChildren()
Sources/Panels/EditorPanel.swift New Monaco editor panel with pre-warmed WebView pool, path traversal guards, and VS Code preview/pin behavior; force cast on pooled WebView is minor
Sources/Panels/ExplorerSidebarPanel.swift WebView-based explorer sidebar — good path-traversal guards; incomplete JSON escaping in sendRootsToJS; instantiated but never rendered alongside the new native explorer
Sources/ContentView.swift Adds three-tab sidebar (Workspaces/Explorer/Search) and wires up open/pin callbacks; setupExplorerPanel() initializes ExplorerSidebarPanel which is never rendered, wasting resources
Sources/MonacoWebViewPool.swift New pre-warmed WebView pool for instant Monaco editor opens; clean implementation with proper pool refill logic
Sources/Panels/EditorPanelView.swift SwiftUI view wrapping Monaco WebView with focus-flash animation and pointer observer for panel focus; clean implementation
Sources/Workspace.swift Adds EditorPanel surface creation, session snapshot encode/restore, and bonsplitDelegate routing for the new editor kind
Resources/editor/editor.js New Monaco single-file editor JS bridge — proper pending-open queue, model lifecycle with dispose, and disabled conflicting shortcuts
Sources/GhosttyTerminalView.swift Adds guard to prevent terminal from stealing focus from sidebar NSTextField inputs (file search field)

Sequence Diagram

sequenceDiagram
    participant User
    participant ContentView
    participant NativeFileExplorerView
    participant FileSearchView
    participant EditorPanel
    participant MonacoWebViewPool
    participant WKWebView

    Note over MonacoWebViewPool: App startup — warmUp()
    MonacoWebViewPool->>WKWebView: load editor/index.html (×2)

    User->>ContentView: clicks Explorer tab
    ContentView->>NativeFileExplorerView: render(viewModel)
    NativeFileExplorerView->>NativeFileExplorerView: flattenedRows() → LazyVStack

    User->>NativeFileExplorerView: single-click file
    NativeFileExplorerView->>ContentView: onOpenFile(absolutePath)
    ContentView->>ContentView: find existing preview EditorPanel?
    alt preview exists
        ContentView->>EditorPanel: openFileByPath(path)
        EditorPanel->>WKWebView: evaluateJS openFileWithContent(...)
    else no preview
        ContentView->>MonacoWebViewPool: take()
        MonacoWebViewPool-->>ContentView: (webView, handler)
        ContentView->>EditorPanel: init(rootPath, filePath)
        EditorPanel->>WKWebView: evaluateJS openFileWithContent(...)
    end

    User->>NativeFileExplorerView: double-click file
    NativeFileExplorerView->>ContentView: onOpenFile + onPinFile(path)
    ContentView->>EditorPanel: isPreview = false

    User->>ContentView: Cmd+Shift+F
    ContentView->>FileSearchView: switch to search tab
    FileSearchView->>FileSearchView: search() → Task (⚠️ main actor)

    User->>EditorPanel: Cmd+S
    EditorPanel->>WKWebView: saveActive() → writeFile bridge
    WKWebView->>EditorPanel: handleResponse(requestId, success)
Loading

Comments Outside Diff (1)

  1. Sources/FileSearchView.swift, line 287-306 (link)

    P1 File search blocks the main thread

    FileSearchViewModel is @MainActor and searchTask = Task { ... } (without .detached) inherits the actor context, so the entire recursive traversal runs on the main thread. searchDirectory is declared async but has no actual await suspension points inside it — each recursive await searchDirectory(...) call does not yield to the runtime and runs to completion synchronously. On a large repo (e.g., one with tens of thousands of files), this will freeze the UI for the duration of the search.

    The fix is to run the search on a non-isolated task:

    searchTask = Task.detached(priority: .userInitiated) {
        var found: [FileSearchResult] = []
        // ... search logic ...
        if !Task.isCancelled {
            await MainActor.run {
                self.results = found
                self.isSearching = false
            }
        }
    }

    Or mark searchDirectory as nonisolated and dispatch it to a background thread.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +84 to +93
gitStatusMap: gitStatusMap,
gitIgnoredPaths: gitIgnoredPaths
)
}

static func loadEntries(
at directoryPath: String,
relativeTo parentRelative: String,
rootPath: String,
gitStatusMap: [String: String],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Directory expansion blocks the main thread

loadChildrenForNode calls Self.loadEntries(...) synchronously on the main actor (FileTreeRoot is @MainActor). This does a FileManager.contentsOfDirectory + per-entry fileExists loop synchronously on the main thread every time a user expands a directory node.

Compare to loadChildren() above, which correctly dispatches to DispatchQueue.global(qos: .userInitiated). loadChildrenForNode is missing the same treatment. For a directory with hundreds of entries (or on a slow/network-mounted volume), this will cause a visible UI stutter on every expand gesture.

func loadChildrenForNode(_ node: FileNode) {
    let dirPath = (node.rootPath as NSString).appendingPathComponent(node.relativePath)
    let statusMap = gitStatusMap
    let ignored = gitIgnoredPaths
    DispatchQueue.global(qos: .userInitiated).async {
        let entries = Self.loadEntries(
            at: dirPath, relativeTo: node.relativePath,
            rootPath: node.rootPath,
            gitStatusMap: statusMap, gitIgnoredPaths: ignored
        )
        DispatchQueue.main.async { node.children = entries }
    }
}

Comment on lines +2401 to +2412
.first(where: { $0.isPreview })

if let preview = previewEditor {
preview.openFileByPath(filePath)
workspace.focusPanel(preview.id)
} else {
let rootPath = workspace.currentDirectory
_ = tabManager.openEditor(rootPath: rootPath, filePath: filePath, focus: true)
}
}
panel.onPinFile = { [weak tabManager] filePath in
guard let tabManager else { return }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ExplorerSidebarPanel is initialized but never rendered

setupExplorerPanel() and setupNativeExplorer() are both called in .onAppear. The Explorer tab (case .explorer:) renders NativeFileExplorerView(viewModel: nativeExplorerViewModel) — the native SwiftUI tree — not ExplorerSidebarView(panel: explorerPanel).

However ExplorerSidebarPanel still creates a WKWebView, loads Resources/explorer/index.html, registers a WKScriptMessageHandler, and starts an FSEventStreamRef watching the workspace paths — all for a WebView that is never shown. This wastes memory, CPU (FSEvents), and the JavaScript bridge slots.

If ExplorerSidebarPanel is intentionally being kept as a fallback or future path, consider lazy-initializing it only when actually displayed. Otherwise it should be removed from setupExplorerPanel and its call sites.

Comment on lines +159 to +165
sendError(requestId: requestId, message: "Invalid root", webView: webView); return
}
DispatchQueue.global(qos: .userInitiated).async {
let canonicalRoot = URL(fileURLWithPath: rootPath).resolvingSymlinksInPath().path
guard let oldFull = self.resolvedPath(oldRelPath, rootPath: rootPath), oldFull != canonicalRoot,
let newFull = self.resolvedPath(newRelPath, rootPath: rootPath) else {
self.sendError(requestId: requestId, message: "Invalid path", webView: webView); return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Incomplete JSON escaping in sendRootsToJS

The manual JSON construction only escapes double quotes. Folder names containing a backslash (\), newline (\n), carriage return (\r), or tab (\t) will produce malformed JSON (or silently mis-parse as escape sequences). For example, a path whose lastPathComponent is foo\bar generates {"name":"foo\bar","rootIndex":0}\b is the JSON backspace escape, so the name would be decoded as "fooar".

Use JSONSerialization for safe encoding instead:

func sendRootsToJS() {
    let rootObjects: [[String: Any]] = rootPaths.enumerated().map { index, path in
        ["name": (path as NSString).lastPathComponent, "rootIndex": index]
    }
    guard let jsonData = try? JSONSerialization.data(withJSONObject: rootObjects),
          let jsonString = String(data: jsonData, encoding: .utf8) else { return }
    let js = "if (window.cmuxExplorer && window.cmuxExplorer.setRoots) { window.cmuxExplorer.setRoots(\(jsonString)); }"
    webView.evaluateJavaScript(js, completionHandler: nil)
}

pendingFilePath = nil
openFileByPath(filePath)
}
} else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Force cast could crash if pool implementation changes

MonacoWebViewPool.available is typed [(webView: WKWebView, handler: EditorMessageHandler)], so the pool's stored type is WKWebView. The pool does create CmuxWebView instances, but the force cast pooled.webView as! CmuxWebView will crash at runtime if the pool is ever changed to vend plain WKWebViews. Consider changing the pool's stored type to CmuxWebView directly, or using a conditional cast with a fallback:

// Option A: update pool storage type
private var available: [(webView: CmuxWebView, handler: EditorMessageHandler)] = []

// Option B: conditional cast with fallback path
if let cmuxWebView = pooled.webView as? CmuxWebView {
    self.webView = cmuxWebView
    // ...
} else {
    // fall through to fresh creation
}

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 issues found across 27 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/FileSearchView.swift">

<violation number="1" location="Sources/FileSearchView.swift:60">
P2: Synchronous filesystem traversal runs on the @MainActor, so searchDirectory performs FileManager recursion on the UI thread. Large workspaces will block UI during search. Move traversal to a background task and only publish results on MainActor.</violation>

<violation number="2" location="Sources/FileSearchView.swift:178">
P2: Clearing the search doesn't cancel the in-flight task, so stale results can reappear when the previous search finishes.</violation>
</file>

<file name="Sources/Panels/ExplorerSidebarPanel.swift">

<violation number="1" location="Sources/Panels/ExplorerSidebarPanel.swift:29">
P2: `pinFileExternal`/`openFileExternal` bypass root-bound `resolvedPath` validation, allowing paths outside configured roots to reach callbacks.</violation>

<violation number="2" location="Sources/Panels/ExplorerSidebarPanel.swift:427">
P2: Root names are injected into JS via manual JSON string construction that only escapes double quotes. This misses other required escapes (e.g., backslashes/newlines), so valid folder names can generate malformed JS and break explorer initialization.</violation>
</file>

<file name="Sources/Workspace.swift">

<violation number="1" location="Sources/Workspace.swift:7520">
P2: newEditorSurface doesn’t preserve focus when focus is false. Other surface creation paths call preserveFocusAfterNonFocusSplit to undo Bonsplit’s internal focus/selection changes on background tab creation, but the editor path omits it, so session restore/background editor tabs can still steal focus.</violation>
</file>

<file name="Sources/Panels/EditorPanel.swift">

<violation number="1" location="Sources/Panels/EditorPanel.swift:331">
P2: Saving an external file opened outside rootPath writes to rootPath/<basename> because openFileByPath strips the absolute path to a basename before sending to Monaco.</violation>
</file>

<file name="Sources/MonacoWebViewPool.swift">

<violation number="1" location="Sources/MonacoWebViewPool.swift:73">
P2: `warming` is decremented in `didFinish` before the WebView is actually ready and appended to `available`, so there’s a window where the in‑flight warmup is counted as neither `warming` nor `available`. This lets `refill()` over-create WebViews and can exceed the intended pool size once `onEditorReady` fires.</violation>
</file>

<file name="Sources/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:2246">
P2: Monaco WebView warm-up now runs unconditionally at launch, so it executes under XCTest even though the method otherwise skips heavyweight startup work for tests. This calls WKWebView creation/loading and can slow or destabilize UI tests.</violation>
</file>

<file name="Sources/NativeFileExplorer.swift">

<violation number="1" location="Sources/NativeFileExplorer.swift:80">
P2: Directory expansion does synchronous filesystem enumeration on the main actor, which can block UI responsiveness.</violation>

<violation number="2" location="Sources/NativeFileExplorer.swift:172">
P2: Git porcelain parsing is not robust for quoted/escaped filenames, so status/ignored badges can be missing for files with spaces/quotes/backslashes.</violation>

<violation number="3" location="Sources/NativeFileExplorer.swift:320">
P2: Async `FileTreeRoot.children` updates are not propagated to the view model, so flattened tree rows may not refresh after initial load.</violation>

<violation number="4" location="Sources/NativeFileExplorer.swift:329">
P2: Collapsed directories can retain stale child lists because refresh skips non-expanded nodes and re-expand does not reload already-populated children.</violation>
</file>

<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:2370">
P2: `setupExplorerPanel()` eagerly initializes the legacy WebView explorer even though the Explorer tab renders `NativeFileExplorerView`; this creates an unused WKWebView and file watchers on every appearance.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if !viewModel.query.isEmpty {
Button {
viewModel.query = ""
viewModel.results = []
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Clearing the search doesn't cancel the in-flight task, so stale results can reappear when the previous search finishes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/FileSearchView.swift, line 178:

<comment>Clearing the search doesn't cancel the in-flight task, so stale results can reappear when the previous search finishes.</comment>

<file context>
@@ -0,0 +1,258 @@
+                if !viewModel.query.isEmpty {
+                    Button {
+                        viewModel.query = ""
+                        viewModel.results = []
+                    } label: {
+                        Image(systemName: "xmark.circle.fill")
</file context>
Fix with Cubic

) async {
guard results.count < maxResults else { return }
let fm = FileManager.default
guard let entries = try? fm.contentsOfDirectory(atPath: path) else { return }
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Synchronous filesystem traversal runs on the @mainactor, so searchDirectory performs FileManager recursion on the UI thread. Large workspaces will block UI during search. Move traversal to a background task and only publish results on MainActor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/FileSearchView.swift, line 60:

<comment>Synchronous filesystem traversal runs on the @MainActor, so searchDirectory performs FileManager recursion on the UI thread. Large workspaces will block UI during search. Move traversal to a background task and only publish results on MainActor.</comment>

<file context>
@@ -0,0 +1,258 @@
+    ) async {
+        guard results.count < maxResults else { return }
+        let fm = FileManager.default
+        guard let entries = try? fm.contentsOfDirectory(atPath: path) else { return }
+
+        for entry in entries.sorted() {
</file context>
Fix with Cubic

func sendRootsToJS() {
let rootsJSON = rootPaths.enumerated().map { index, path in
let name = (path as NSString).lastPathComponent
return "{\"name\":\"\(name.replacingOccurrences(of: "\"", with: "\\\""))\",\"rootIndex\":\(index)}"
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Root names are injected into JS via manual JSON string construction that only escapes double quotes. This misses other required escapes (e.g., backslashes/newlines), so valid folder names can generate malformed JS and break explorer initialization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/ExplorerSidebarPanel.swift, line 427:

<comment>Root names are injected into JS via manual JSON string construction that only escapes double quotes. This misses other required escapes (e.g., backslashes/newlines), so valid folder names can generate malformed JS and break explorer initialization.</comment>

<file context>
@@ -0,0 +1,444 @@
+    func sendRootsToJS() {
+        let rootsJSON = rootPaths.enumerated().map { index, path in
+            let name = (path as NSString).lastPathComponent
+            return "{\"name\":\"\(name.replacingOccurrences(of: "\"", with: "\\\""))\",\"rootIndex\":\(index)}"
+        }.joined(separator: ",")
+        let js = "if (window.cmuxExplorer && window.cmuxExplorer.setRoots) { window.cmuxExplorer.setRoots([\(rootsJSON)]); }"
</file context>
Fix with Cubic

case "pinFileExternal":
guard let relativePath = body["path"] as? String,
let root = rootPath(for: body) else { return }
let fullPath = (root as NSString).appendingPathComponent(relativePath)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: pinFileExternal/openFileExternal bypass root-bound resolvedPath validation, allowing paths outside configured roots to reach callbacks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/ExplorerSidebarPanel.swift, line 29:

<comment>`pinFileExternal`/`openFileExternal` bypass root-bound `resolvedPath` validation, allowing paths outside configured roots to reach callbacks.</comment>

<file context>
@@ -0,0 +1,444 @@
+        case "pinFileExternal":
+            guard let relativePath = body["path"] as? String,
+                  let root = rootPath(for: body) else { return }
+            let fullPath = (root as NSString).appendingPathComponent(relativePath)
+            DispatchQueue.main.async { [weak self] in
+                self?.onPinFile?(fullPath)
</file context>
Fix with Cubic


surfaceIdToPanelId[newTabId] = editorPanel.id

if shouldFocusNewTab {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: newEditorSurface doesn’t preserve focus when focus is false. Other surface creation paths call preserveFocusAfterNonFocusSplit to undo Bonsplit’s internal focus/selection changes on background tab creation, but the editor path omits it, so session restore/background editor tabs can still steal focus.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Workspace.swift, line 7520:

<comment>newEditorSurface doesn’t preserve focus when focus is false. Other surface creation paths call preserveFocusAfterNonFocusSplit to undo Bonsplit’s internal focus/selection changes on background tab creation, but the editor path omits it, so session restore/background editor tabs can still steal focus.</comment>

<file context>
@@ -7453,6 +7485,81 @@ final class Workspace: Identifiable, ObservableObject {
+
+        surfaceIdToPanelId[newTabId] = editorPanel.id
+
+        if shouldFocusNewTab {
+            bonsplitController.focusPane(paneId)
+            bonsplitController.selectTab(newTabId)
</file context>
Fix with Cubic

let process = Process()
let pipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
process.arguments = ["-C", path, "status", "--porcelain=v1", "-unormal", "--ignored"]
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Git porcelain parsing is not robust for quoted/escaped filenames, so status/ignored badges can be missing for files with spaces/quotes/backslashes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/NativeFileExplorer.swift, line 172:

<comment>Git porcelain parsing is not robust for quoted/escaped filenames, so status/ignored badges can be missing for files with spaces/quotes/backslashes.</comment>

<file context>
@@ -0,0 +1,584 @@
+            let process = Process()
+            let pipe = Pipe()
+            process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
+            process.arguments = ["-C", path, "status", "--porcelain=v1", "-unormal", "--ignored"]
+            process.standardOutput = pipe
+            process.standardError = FileHandle.nullDevice
</file context>
Fix with Cubic


func toggleExpand(_ node: FileNode, in root: FileTreeRoot) {
node.isExpanded.toggle()
if node.isExpanded && node.children == nil {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Collapsed directories can retain stale child lists because refresh skips non-expanded nodes and re-expand does not reload already-populated children.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/NativeFileExplorer.swift, line 329:

<comment>Collapsed directories can retain stale child lists because refresh skips non-expanded nodes and re-expand does not reload already-populated children.</comment>

<file context>
@@ -0,0 +1,584 @@
+
+    func toggleExpand(_ node: FileNode, in root: FileTreeRoot) {
+        node.isExpanded.toggle()
+        if node.isExpanded && node.children == nil {
+            root.loadChildrenForNode(node)
+        }
</file context>
Fix with Cubic

if let existing = existingByPath[path] {
newRoots.append(existing)
} else {
let root = FileTreeRoot(path: path)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Async FileTreeRoot.children updates are not propagated to the view model, so flattened tree rows may not refresh after initial load.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/NativeFileExplorer.swift, line 320:

<comment>Async `FileTreeRoot.children` updates are not propagated to the view model, so flattened tree rows may not refresh after initial load.</comment>

<file context>
@@ -0,0 +1,584 @@
+            if let existing = existingByPath[path] {
+                newRoots.append(existing)
+            } else {
+                let root = FileTreeRoot(path: path)
+                newRoots.append(root)
+            }
</file context>
Fix with Cubic


func loadChildrenForNode(_ node: FileNode) {
let dirPath = (node.rootPath as NSString).appendingPathComponent(node.relativePath)
node.children = Self.loadEntries(
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Directory expansion does synchronous filesystem enumeration on the main actor, which can block UI responsiveness.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/NativeFileExplorer.swift, line 80:

<comment>Directory expansion does synchronous filesystem enumeration on the main actor, which can block UI responsiveness.</comment>

<file context>
@@ -0,0 +1,584 @@
+
+    func loadChildrenForNode(_ node: FileNode) {
+        let dirPath = (node.rootPath as NSString).appendingPathComponent(node.relativePath)
+        node.children = Self.loadEntries(
+            at: dirPath,
+            relativeTo: node.relativePath,
</file context>
Fix with Cubic

if !sidebarState.isVisible { sidebarState.isVisible = true }
}
.onAppear {
setupExplorerPanel()
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: setupExplorerPanel() eagerly initializes the legacy WebView explorer even though the Explorer tab renders NativeFileExplorerView; this creates an unused WKWebView and file watchers on every appearance.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 2370:

<comment>`setupExplorerPanel()` eagerly initializes the legacy WebView explorer even though the Explorer tab renders `NativeFileExplorerView`; this creates an unused WKWebView and file watchers on every appearance.</comment>

<file context>
@@ -2317,16 +2329,183 @@ struct ContentView: View {
+            if !sidebarState.isVisible { sidebarState.isVisible = true }
+        }
+        .onAppear {
+            setupExplorerPanel()
+            setupNativeExplorer()
+        }
</file context>
Fix with Cubic

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