From 61e61f556aa2b0600f61188589750356bb4463c8 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Wed, 29 Apr 2026 13:04:55 +0200 Subject: [PATCH] feat(chat): hideable sessions + inspector panes for the Mac chat (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 3-pane layout (264px sessions list + transcript + 320px inspector) ate ~584px of horizontal space on every chat window — squeezing the actual transcript on smaller windows AND keeping the "No tool selected" empty-state visible even when irrelevant. User reported that as "reasoning, in/out, hard to read because of the tool selected box taking so much space". Add toolbar toggles + Settings parity to hide either side pane: - Two new @AppStorage keys in ChatDensitySettings: scarf.chat.showSessionsList (default true) scarf.chat.showInspector (default true) - ChatView toolbar gains two buttons next to the View picker: sidebar.left toggles the sessions list, sidebar.right toggles the inspector. Both highlight in accent color when visible. Hidden when in terminal mode (the 3-pane layout doesn't apply there). - RichChatView body conditionally renders each side pane and its divider, with .transition(.move + .opacity) and a 180ms easeInOut animation so the transcript reflows smoothly rather than snapping. - Auto-show inspector when a tool card is focused so a click never silently dies — onChange of focusedToolCallId flips showInspector back on if it was off. The slide-in animation covers the visual transition. - DisplayTab → Chat density gains parity Toggle rows for "Sessions list" and "Tool inspector" — same group as the existing density pickers from #47/#48 so the settings home is consistent. Defaults match today's behavior so existing users see no change until they opt out. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Features/Chat/ChatDensitySettings.swift | 7 +++ .../scarf/Features/Chat/Views/ChatView.swift | 59 +++++++++++++++++++ .../Features/Chat/Views/RichChatView.swift | 41 +++++++++++-- .../Settings/Views/Tabs/DisplayTab.swift | 8 +++ 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/scarf/scarf/Features/Chat/ChatDensitySettings.swift b/scarf/scarf/Features/Chat/ChatDensitySettings.swift index 8e54dd2..6f0dd6d 100644 --- a/scarf/scarf/Features/Chat/ChatDensitySettings.swift +++ b/scarf/scarf/Features/Chat/ChatDensitySettings.swift @@ -15,6 +15,13 @@ enum ChatDensityKeys { static let toolCardStyle = "scarf.chat.toolCardStyle" static let reasoningStyle = "scarf.chat.reasoningStyle" static let fontScale = "scarf.chat.fontScale" + /// Whether the left sessions list pane is visible in the Mac + /// 3-pane chat layout. Defaults true (today's behavior). Issue #58. + static let showSessionsList = "scarf.chat.showSessionsList" + /// Whether the right tool inspector pane is visible. Defaults true. + /// When hidden, clicking a tool card auto-flips it back on so the + /// click does what the user expects (`ToolCallCard.onFocus`). Issue #58. + static let showInspector = "scarf.chat.showInspector" } /// How `RichMessageBubble` renders the per-call tool widgets. diff --git a/scarf/scarf/Features/Chat/Views/ChatView.swift b/scarf/scarf/Features/Chat/Views/ChatView.swift index ae74165..2f6b0f0 100644 --- a/scarf/scarf/Features/Chat/Views/ChatView.swift +++ b/scarf/scarf/Features/Chat/Views/ChatView.swift @@ -7,6 +7,15 @@ struct ChatView: View { @Environment(AppCoordinator.self) private var coordinator @State private var showErrorDetails = false + /// Side-pane visibility toggles (issue #58). Drive the new + /// sidebar.left / sidebar.right toolbar buttons; `RichChatView.body` + /// reads the same `@AppStorage` keys and conditionally renders the + /// panes with a slide animation. + @AppStorage(ChatDensityKeys.showSessionsList) + private var showSessionsList: Bool = true + @AppStorage(ChatDensityKeys.showInspector) + private var showInspector: Bool = true + var body: some View { @Bindable var vm = viewModel @Bindable var coord = coordinator @@ -225,6 +234,30 @@ struct ChatView: View { voiceControls } + // Side-pane toggles (issue #58). Only meaningful in rich-chat + // mode where the 3-pane layout exists; terminal mode is a + // single SwiftTerm view and these would do nothing. Hide + // them on the terminal side so the toolbar stays uncluttered. + if viewModel.displayMode == .richChat { + Button { + showSessionsList.toggle() + } label: { + Image(systemName: "sidebar.left") + .foregroundStyle(showSessionsList ? Color.accentColor : .secondary) + } + .buttonStyle(.borderless) + .help(showSessionsList ? "Hide sessions list" : "Show sessions list") + + Button { + showInspector.toggle() + } label: { + Image(systemName: "sidebar.right") + .foregroundStyle(showInspector ? Color.accentColor : .secondary) + } + .buttonStyle(.borderless) + .help(showInspector ? "Hide tool inspector" : "Show tool inspector") + } + Picker("View", selection: Bindable(viewModel).displayMode) { Image(systemName: "terminal") .help("Terminal") @@ -386,6 +419,23 @@ struct ChatView: View { } ) } + // Model preflight — open before any ACP plumbing when the active + // server has no `model.default` / `model.provider` set. Keeps the + // user from typing a prompt only to find out the upstream + // provider rejected it. + .sheet(isPresented: modelPreflightBinding) { + ChatModelPreflightSheet( + reason: viewModel.modelPreflightReason ?? "", + serverDisplayName: viewModel.context.displayName, + onSelect: { model, provider in + viewModel.confirmModelPreflight(model: model, provider: provider) + }, + onCancel: { + viewModel.cancelModelPreflight() + } + ) + .environment(\.serverContext, viewModel.context) + } } private var permissionBinding: Binding { @@ -394,6 +444,15 @@ struct ChatView: View { set: { viewModel.richChatViewModel.pendingPermission = $0 } ) } + + private var modelPreflightBinding: Binding { + Binding( + get: { viewModel.modelPreflightReason != nil }, + set: { newValue in + if !newValue { viewModel.cancelModelPreflight() } + } + ) + } } // MARK: - Permission Approval View diff --git a/scarf/scarf/Features/Chat/Views/RichChatView.swift b/scarf/scarf/Features/Chat/Views/RichChatView.swift index ae684cd..c7e7d86 100644 --- a/scarf/scarf/Features/Chat/Views/RichChatView.swift +++ b/scarf/scarf/Features/Chat/Views/RichChatView.swift @@ -29,14 +29,25 @@ struct RichChatView: View { @AppStorage(ChatDensityKeys.fontScale) private var fontScale: Double = ChatFontScale.default + /// Sessions-list / inspector pane visibility (issue #58). Defaults + /// `true` so existing users see no change until they opt out via + /// the toolbar buttons or Settings → Display → Chat density. + @AppStorage(ChatDensityKeys.showSessionsList) + private var showSessionsList: Bool = true + @AppStorage(ChatDensityKeys.showInspector) + private var showInspector: Bool = true + /// In ACP mode, events drive updates directly — no DB polling needed. private var isACPMode: Bool { chatViewModel.isACPConnected } var body: some View { HStack(spacing: 0) { - ChatSessionListPane(chatViewModel: chatViewModel, richChat: richChat) - .frame(width: 264) - Divider().background(ScarfColor.border) + if showSessionsList { + ChatSessionListPane(chatViewModel: chatViewModel, richChat: richChat) + .frame(width: 264) + .transition(.move(edge: .leading).combined(with: .opacity)) + Divider().background(ScarfColor.border) + } ChatTranscriptPane( richChat: richChat, chatViewModel: chatViewModel, @@ -44,12 +55,30 @@ struct RichChatView: View { isEnabled: isEnabled ) .frame(maxWidth: .infinity) - Divider().background(ScarfColor.border) - ChatInspectorPane(chatViewModel: chatViewModel) - .frame(width: 320) + if showInspector { + Divider().background(ScarfColor.border) + ChatInspectorPane(chatViewModel: chatViewModel) + .frame(width: 320) + .transition(.move(edge: .trailing).combined(with: .opacity)) + } } .frame(minHeight: 0, idealHeight: 500, maxHeight: .infinity) .environment(\.dynamicTypeSize, ChatFontScale.dynamicTypeSize(for: fontScale)) + // Animate side-pane shows/hides so the transcript reflows + // smoothly rather than snapping. ~180ms feels responsive + // without being jarring. + .animation(.easeInOut(duration: 0.18), value: showSessionsList) + .animation(.easeInOut(duration: 0.18), value: showInspector) + // Auto-show inspector when a tool call is focused so a click + // on a tool card is never silently lost (issue #58 follow-up). + // Tool clicks set `chatViewModel.focusedToolCallId`; if that + // becomes non-nil while the inspector is hidden, flip it back + // on. The animation modifiers above cover the slide-in. + .onChange(of: chatViewModel.focusedToolCallId) { _, new in + if new != nil, !showInspector { + showInspector = true + } + } // 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/Settings/Views/Tabs/DisplayTab.swift b/scarf/scarf/Features/Settings/Views/Tabs/DisplayTab.swift index 4d5d6e9..226feae 100644 --- a/scarf/scarf/Features/Settings/Views/Tabs/DisplayTab.swift +++ b/scarf/scarf/Features/Settings/Views/Tabs/DisplayTab.swift @@ -16,6 +16,12 @@ struct DisplayTab: View { private var reasoningStyle: String = ReasoningStyle.disclosure.rawValue @AppStorage(ChatDensityKeys.fontScale) private var fontScale: Double = ChatFontScale.default + /// Side-pane visibility (issue #58). Mirrors the toolbar buttons in + /// ChatView; this is the canonical preferences home. + @AppStorage(ChatDensityKeys.showSessionsList) + private var showSessionsList: Bool = true + @AppStorage(ChatDensityKeys.showInspector) + private var showInspector: Bool = true var body: some View { SettingsSection(title: "Chat density", icon: "rectangle.compress.vertical") { @@ -30,6 +36,8 @@ struct DisplayTab: View { options: ReasoningStyle.allCases.map { ($0.rawValue, $0.displayName) } ) FontScaleRow(scale: $fontScale) + ToggleRow(label: "Sessions list", isOn: showSessionsList) { showSessionsList = $0 } + ToggleRow(label: "Tool inspector", isOn: showInspector) { showInspector = $0 } DensityFootnote() }