Files
scarf/scarf/scarf/Features/Chat/Views/RichChatView.swift
T
Alan Wizemann 7ad78a5492 fix(layout): cap RichChatView/ProjectSessions idealHeight; revert broken detail wrap
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) <noreply@anthropic.com>
2026-04-24 00:57:15 +02:00

66 lines
2.8 KiB
Swift

import SwiftUI
struct RichChatView: View {
@Bindable var richChat: RichChatViewModel
var onSend: (String) -> Void
var isEnabled: Bool
@Environment(HermesFileWatcher.self) private var fileWatcher
@Environment(ChatViewModel.self) private var chatViewModel
/// In ACP mode, events drive updates directly no DB polling needed.
private var isACPMode: Bool { chatViewModel.isACPConnected }
var body: some View {
VStack(spacing: 0) {
SessionInfoBar(
session: richChat.currentSession,
isWorking: richChat.isAgentWorking,
acpInputTokens: richChat.acpInputTokens,
acpOutputTokens: richChat.acpOutputTokens,
acpThoughtTokens: richChat.acpThoughtTokens
)
Divider()
// Always mount RichChatMessageList; empty state lives inside it.
// Swapping between a ContentUnavailableView and the ScrollView
// hierarchy on first message caused a full view tree rebuild,
// which manifests as a white flash.
RichChatMessageList(
groups: richChat.messageGroups,
isWorking: richChat.isAgentWorking,
isLoadingSession: chatViewModel.isPreparingSession,
scrollTrigger: richChat.scrollTrigger
)
Divider()
RichChatInputBar(
onSend: { text in
onSend(text)
},
isEnabled: isEnabled,
commands: richChat.availableCommands,
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 {
richChat.scheduleRefresh()
}
}
}
}