From 7ad78a5492faa9929c9d31dc8d7f805ac3fdcc49 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Fri, 24 Apr 2026 00:57:15 +0200 Subject: [PATCH] fix(layout): cap RichChatView/ProjectSessions idealHeight; revert broken detail wrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior commits tried to solve the "window grows whenever Chat or Sessions is selected" bug by wrapping NavigationSplitView's detail slot with an explicit frame (`205bb2c`). That broke the HSplitView layout in Projects — the project list column, dashboard header, tab bar, and Sessions-tab header all vanished. Scarf's convention (PlatformsView.swift:12 calls it out explicitly) is to apply size constraints on individual HSplitView columns, never on an outer wrapper. This commit: - Reverts the broken ContentView.swift outer frame from `205bb2c`. NavigationSplitView.detail goes back to its v2.2.1 shape. - Caps the subtrees whose natural ideal heights are what was actually pushing the window past the screen: - RichChatView: `.frame(minHeight: 0, idealHeight: 500, maxHeight: .infinity)` on the outer VStack. The message list uses a plain VStack (deliberately, to dodge the LazyVStack whitespace bug — see RichChatMessageList.swift:13-24), so its natural ideal grows with every message. Capping idealHeight at 500 gives the window a screen-safe starting size without limiting how tall the view can flex when the user drags the window bigger. - ProjectSessionsView: same treatment with `idealHeight: 400`. Replaces the earlier `.frame(maxWidth: .infinity, maxHeight: .infinity)` which set MAX but didn't influence what got reported upward as ideal. - Xcode regenerated Localizable.xcstrings during builds; riding along. `.frame(idealHeight:)` is the specific SwiftUI knob that overrides a child's reported ideal on the way up — `maxHeight: .infinity` alone doesn't. With `.windowResizability(.contentMinSize)` (still in scarfApp, left alone), the window sizes itself to the reported ideal on open and respects user drags above the content min. With a screen-safe ideal, the window opens at a usable size and never pushes past the desktop. User-verified: window behaves correctly across section switches, resize persists, chat input bar always visible. Co-Authored-By: Claude Opus 4.7 (1M context) --- scarf/scarf/ContentView.swift | 26 ----- .../Features/Chat/Views/RichChatView.swift | 13 +++ .../Projects/Views/ProjectSessionsView.swift | 18 ++-- scarf/scarf/Localizable.xcstrings | 94 +++++++++++++++++++ 4 files changed, 118 insertions(+), 33 deletions(-) diff --git a/scarf/scarf/ContentView.swift b/scarf/scarf/ContentView.swift index 20ee087..a67c211 100644 --- a/scarf/scarf/ContentView.swift +++ b/scarf/scarf/ContentView.swift @@ -17,32 +17,6 @@ struct ContentView: View { .navigationSplitViewColumnWidth(min: 180, ideal: 240, max: 360) } detail: { detailView - // The detail column's size is what NavigationSplitView - // reports up to the window. Without a bound here, the - // reported ideal is derived from the currently-rendered - // section's natural intrinsic size — and some sections - // (Chat with a fully-materialized message list, the - // v2.3 per-project Sessions tab) have intrinsic heights - // that exceed the screen. With `.windowResizability - // (.contentMinSize)` in scarfApp, the window is forced - // at least that tall, pushing its bottom edge past the - // visible desktop and hiding the input bar. - // - // This frame pins the detail's reported ideal at a - // modest 900×600 — small enough to fit any reasonable - // screen — while allowing it to expand freely to - // whatever the user drags the window to. `minHeight: 0` - // is load-bearing: it overrides the "my child's min is - // huge" chain so NavigationSplitView doesn't carry a - // massive min up to the window. - .frame( - minWidth: 500, - idealWidth: 900, - maxWidth: .infinity, - minHeight: 300, - idealHeight: 600, - maxHeight: .infinity - ) .toolbar { ToolbarItem(placement: .navigation) { ServerSwitcherToolbar() diff --git a/scarf/scarf/Features/Chat/Views/RichChatView.swift b/scarf/scarf/Features/Chat/Views/RichChatView.swift index 5fd5861..8df5e64 100644 --- a/scarf/scarf/Features/Chat/Views/RichChatView.swift +++ b/scarf/scarf/Features/Chat/Views/RichChatView.swift @@ -42,6 +42,19 @@ struct RichChatView: View { showCompressButton: richChat.supportsCompress && !richChat.hasBroaderCommandMenu ) } + // `idealHeight: 500` caps what this subtree REPORTS as its ideal + // height. Load-bearing: RichChatMessageList uses a plain VStack + // (not LazyVStack — see RichChatMessageList.swift:13-24 for the + // rationale) inside a ScrollView, so its natural ideal grows + // with message count. Under the WindowGroup's + // `.windowResizability(.contentMinSize)` policy, that uncapped + // ideal would open the window at a height that exceeds the + // screen on long conversations, pushing the input bar below + // the visible desktop. `maxHeight: .infinity` still lets the + // view fill any larger offered space, and `minHeight: 0` + // allows it to shrink freely — the ideal cap only affects the + // initial-size hint reported up to the window. + .frame(minHeight: 0, idealHeight: 500, maxHeight: .infinity) // DB polling fallback for terminal mode only — never overwrite ACP messages .onChange(of: fileWatcher.lastChangeDate) { if !isACPMode, !richChat.hasMessages, richChat.sessionId != nil { diff --git a/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift b/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift index ab4c9af..3440fef 100644 --- a/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift +++ b/scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift @@ -20,13 +20,17 @@ struct ProjectSessionsView: View { Divider() content } - // Without this clamp the inner List's intrinsic height grows - // with its row count and the enclosing VStack pushes the - // window itself past the screen. Other tabs handle this via - // their own container (widgetsTab = ScrollView, siteTab = - // explicit maxHeight) — match the siteTab pattern here so - // the List scrolls internally. - .frame(maxWidth: .infinity, maxHeight: .infinity) + // `idealHeight: 400` caps what this subtree reports as its + // ideal height. Without it, the inner List's row-materialised + // intrinsic height bubbles up through NavigationSplitView's + // detail slot and, under `.windowResizability(.contentMinSize)`, + // opens the window at a height that exceeds the screen on + // busy projects — the Sessions tab header + "New Chat" button + // end up below the visible desktop edge. `maxHeight: .infinity` + // still lets the List fill any taller offered space, and + // `minHeight: 0` allows it to shrink. Mirrors the same pattern + // applied in RichChatView. + .frame(minHeight: 0, idealHeight: 400, maxHeight: .infinity) .task(id: project.id) { // Rebuild the VM when the project changes so stale state // from a previously-selected project doesn't bleed diff --git a/scarf/scarf/Localizable.xcstrings b/scarf/scarf/Localizable.xcstrings index a0a87bb..22e82d2 100644 --- a/scarf/scarf/Localizable.xcstrings +++ b/scarf/scarf/Localizable.xcstrings @@ -1028,6 +1028,10 @@ "comment" : "A message that appears when a memory block is no longer present in MEMORY.md.", "isCommentAutoGenerated" : true }, + "A project named \"%@\" already exists." : { + "comment" : "A warning message that appears in a Rename Project sheet if the user-provided name is a duplicate of an existing project. The argument is the name of the duplicate project.", + "isCommentAutoGenerated" : true + }, "A QR code will appear below. Scan it with WhatsApp on your phone. The session is saved to ~/.hermes/platforms/whatsapp/ so you won't need to scan again after restarts." : { "localizations" : { "de" : { @@ -1391,6 +1395,10 @@ } } }, + "Add a project" : { + "comment" : "A button that adds a new project.", + "isCommentAutoGenerated" : true + }, "Add a project folder to get started. Create a .scarf/dashboard.json file in your project to define widgets." : { "localizations" : { "de" : { @@ -2521,6 +2529,10 @@ } } }, + "Archived (%lld)" : { + "comment" : "A label that opens a group of archived projects.", + "isCommentAutoGenerated" : true + }, "Args (one per line)" : { "localizations" : { "de" : { @@ -3785,6 +3797,10 @@ } } }, + "Chats you start here get attributed automatically. Older CLI-started sessions live in the global Sessions sidebar." : { + "comment" : "A description of the purpose of the Sessions tab.", + "isCommentAutoGenerated" : true + }, "Check" : { "localizations" : { "de" : { @@ -6874,6 +6890,10 @@ }, "Description" : { + }, + "Destination" : { + "comment" : "A label for the folder picker in the move-to-folder sheet.", + "isCommentAutoGenerated" : true }, "Details" : { "localizations" : { @@ -8802,6 +8822,10 @@ } } }, + "Filter projects" : { + "comment" : "A label for a search field in the sidebar.", + "isCommentAutoGenerated" : true + }, "Filter servers..." : { "localizations" : { "de" : { @@ -9006,6 +9030,10 @@ "comment" : "A placeholder for a comma-separated list of tags.", "isCommentAutoGenerated" : true }, + "Folders only affect how projects are grouped in Scarf's sidebar. Nothing on disk changes." : { + "comment" : "A description of how folders affect project grouping.", + "isCommentAutoGenerated" : true + }, "Full copy of active profile (all state)" : { "localizations" : { "de" : { @@ -9698,6 +9726,10 @@ } } }, + "Hide archived projects" : { + "comment" : "A toggle that hides archived projects.", + "isCommentAutoGenerated" : true + }, "Hide details" : { "localizations" : { "de" : { @@ -12186,6 +12218,22 @@ } } }, + "Move" : { + "comment" : "A button that moves a project to a folder.", + "isCommentAutoGenerated" : true + }, + "Move \"%@\" to folder" : { + "comment" : "A heading for a dialog that lets the user move a project to a folder.", + "isCommentAutoGenerated" : true + }, + "Move to Folder…" : { + "comment" : "A context menu action that moves a project to a folder.", + "isCommentAutoGenerated" : true + }, + "msgs" : { + "comment" : "A label for the number of messages in a session.", + "isCommentAutoGenerated" : true + }, "my_server" : { }, @@ -12309,6 +12357,17 @@ } } }, + "New Chat" : { + "comment" : "A button that starts a new chat session.", + "isCommentAutoGenerated" : true + }, + "New folder name" : { + + }, + "New folder…" : { + "comment" : "A label for a new folder name.", + "isCommentAutoGenerated" : true + }, "New name for '%@'" : { "localizations" : { "de" : { @@ -13327,6 +13386,10 @@ } } }, + "No project selected" : { + "comment" : "A label that indicates that no project is selected.", + "isCommentAutoGenerated" : true + }, "No Projects" : { "localizations" : { "de" : { @@ -15458,6 +15521,10 @@ }, "Project folder kept" : { + }, + "Project name" : { + "comment" : "A label for a text field that lets the user enter a project name.", + "isCommentAutoGenerated" : true }, "Project Name" : { "localizations" : { @@ -16870,6 +16937,10 @@ } } }, + "Rename project" : { + "comment" : "A title for a sheet that renames a project.", + "isCommentAutoGenerated" : true + }, "Rename Session" : { "localizations" : { "de" : { @@ -16949,6 +17020,9 @@ } } } + }, + "Rename…" : { + }, "required" : { @@ -19379,6 +19453,10 @@ } } }, + "Sessions in this project" : { + "comment" : "A heading for the list of sessions in a project.", + "isCommentAutoGenerated" : true + }, "Set as default — open this server when Scarf launches." : { "comment" : "A tooltip for the star button in the Manage Servers view.", "isCommentAutoGenerated" : true @@ -19623,6 +19701,10 @@ } } }, + "Show archived projects" : { + "comment" : "A toggle that shows/hides archived projects.", + "isCommentAutoGenerated" : true + }, "Show details" : { "localizations" : { "de" : { @@ -21435,6 +21517,10 @@ } } }, + "The project directory on disk isn't changed — only the label Scarf shows in the sidebar." : { + "comment" : "A description of the project name field.", + "isCommentAutoGenerated" : true + }, "The remote's SSH fingerprint no longer matches what your `~/.ssh/known_hosts` file expected. This usually means the remote was reinstalled — or, less commonly, that someone is intercepting the connection." : { "localizations" : { "de" : { @@ -22422,6 +22508,10 @@ } } }, + "Top Level" : { + "comment" : "A folder in the sidebar.", + "isCommentAutoGenerated" : true + }, "Top Tools" : { "localizations" : { "de" : { @@ -22582,6 +22672,10 @@ } } }, + "Unarchive" : { + "comment" : "A button that unarchives a project.", + "isCommentAutoGenerated" : true + }, "Uninstall" : { "localizations" : { "de" : {