Native file explorer sidebar, single-file Monaco editor, file search#2052
Native file explorer sidebar, single-file Monaco editor, file search#2052imadbz wants to merge 16 commits intomanaflow-ai:mainfrom
Conversation
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
|
@imadbz is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughIntroduces 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
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis 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 Key concerns found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
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)
|
| gitStatusMap: gitStatusMap, | ||
| gitIgnoredPaths: gitIgnoredPaths | ||
| ) | ||
| } | ||
|
|
||
| static func loadEntries( | ||
| at directoryPath: String, | ||
| relativeTo parentRelative: String, | ||
| rootPath: String, | ||
| gitStatusMap: [String: String], |
There was a problem hiding this comment.
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 }
}
}| .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 } |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
}There was a problem hiding this comment.
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 = [] |
There was a problem hiding this comment.
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>
| ) async { | ||
| guard results.count < maxResults else { return } | ||
| let fm = FileManager.default | ||
| guard let entries = try? fm.contentsOfDirectory(atPath: path) else { return } |
There was a problem hiding this comment.
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>
| func sendRootsToJS() { | ||
| let rootsJSON = rootPaths.enumerated().map { index, path in | ||
| let name = (path as NSString).lastPathComponent | ||
| return "{\"name\":\"\(name.replacingOccurrences(of: "\"", with: "\\\""))\",\"rootIndex\":\(index)}" |
There was a problem hiding this comment.
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>
| case "pinFileExternal": | ||
| guard let relativePath = body["path"] as? String, | ||
| let root = rootPath(for: body) else { return } | ||
| let fullPath = (root as NSString).appendingPathComponent(relativePath) |
There was a problem hiding this comment.
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>
|
|
||
| surfaceIdToPanelId[newTabId] = editorPanel.id | ||
|
|
||
| if shouldFocusNewTab { |
There was a problem hiding this comment.
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>
| let process = Process() | ||
| let pipe = Pipe() | ||
| process.executableURL = URL(fileURLWithPath: "/usr/bin/git") | ||
| process.arguments = ["-C", path, "status", "--porcelain=v1", "-unormal", "--ignored"] |
There was a problem hiding this comment.
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>
|
|
||
| func toggleExpand(_ node: FileNode, in root: FileTreeRoot) { | ||
| node.isExpanded.toggle() | ||
| if node.isExpanded && node.children == nil { |
There was a problem hiding this comment.
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>
| if let existing = existingByPath[path] { | ||
| newRoots.append(existing) | ||
| } else { | ||
| let root = FileTreeRoot(path: path) |
There was a problem hiding this comment.
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>
|
|
||
| func loadChildrenForNode(_ node: FileNode) { | ||
| let dirPath = (node.rootPath as NSString).appendingPathComponent(node.relativePath) | ||
| node.children = Self.loadEntries( |
There was a problem hiding this comment.
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>
| if !sidebarState.isVisible { sidebarState.isVisible = true } | ||
| } | ||
| .onAppear { | ||
| setupExplorerPanel() |
There was a problem hiding this comment.
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>
Summary
Changes
Sidebar
Editor
Bonsplit
isItalicproperty to Tab for preview tab italic title renderingTest plan
🤖 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
WKWebViewpool for instant opens, large‑file guard, and session restore.Bug Fixes
Written for commit 4701a85. Summary will update on new commits.
Summary by CodeRabbit