mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
feat(chat): hideable sessions + inspector panes for the Mac chat (#58)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<RichChatViewModel.PendingPermission?> {
|
||||
@@ -394,6 +444,15 @@ struct ChatView: View {
|
||||
set: { viewModel.richChatViewModel.pendingPermission = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
private var modelPreflightBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { viewModel.modelPreflightReason != nil },
|
||||
set: { newValue in
|
||||
if !newValue { viewModel.cancelModelPreflight() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Permission Approval View
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user