Tabs: '+' opens brov-chat or term.macOS (max 6, split around the camera); real zsh terminals via SwiftTerm; per-tab chat history and claude session; hover 3 s to close with confirmation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,13 +97,13 @@ struct IslandContainer: View {
|
||||
Rectangle().frame(width: islandWidth,
|
||||
height: state.mode == .expanded ? 320 : islandHeight)
|
||||
}
|
||||
.opacity(uploadActive || greetingActive ? 0 : 1)
|
||||
.opacity(uploadActive || greetingActive || (state.mode == .expanded && state.view == .terminal) ? 0 : 1)
|
||||
.animation(.easeInOut(duration: 0.25), value: uploadActive || greetingActive)
|
||||
|
||||
CountdownBar(state: state, islandW: islandWidth)
|
||||
|
||||
// Pull handle: drag the bottom edge to grow the chat (⅓, ½, full screen).
|
||||
if state.mode == .expanded && [.prompt, .overview, .empty].contains(state.view) {
|
||||
if state.mode == .expanded && [.prompt, .terminal, .overview, .empty].contains(state.view) {
|
||||
ChatResizeHandle(state: state)
|
||||
.position(x: islandWidth / 2, y: islandHeight - 7)
|
||||
.transition(.opacity)
|
||||
@@ -130,7 +130,7 @@ struct IslandContainer: View {
|
||||
let tr: CGFloat = 0
|
||||
withAnimation(anim) {
|
||||
islandWidth = w
|
||||
islandHeight = (newMode == .expanded && state.view == .prompt) ? chatPromptHeight : h
|
||||
islandHeight = (newMode == .expanded && state.view.isTall) ? chatPromptHeight : h
|
||||
cornerRadius = cr
|
||||
islandTopRadius = tr
|
||||
}
|
||||
@@ -147,7 +147,7 @@ struct IslandContainer: View {
|
||||
nw: state.notchWidth, nh: state.notchHeight)
|
||||
withAnimation(openSpring) {
|
||||
islandWidth = w
|
||||
islandHeight = newView == .prompt ? chatPromptHeight : h
|
||||
islandHeight = newView.isTall ? chatPromptHeight : h
|
||||
}
|
||||
}
|
||||
.onChange(of: state.chatHistory.count) { _, _ in
|
||||
@@ -155,15 +155,15 @@ struct IslandContainer: View {
|
||||
withAnimation(openSpring) { islandHeight = chatPromptHeight }
|
||||
}
|
||||
.onChange(of: state.chatDragHeight) { _, h in
|
||||
guard state.mode == .expanded, state.view == .prompt, let h else { return }
|
||||
guard state.mode == .expanded, state.view.isTall, let h else { return }
|
||||
islandHeight = h // follow the pointer, no spring
|
||||
}
|
||||
.onChange(of: state.chatDetent) { _, _ in
|
||||
guard state.mode == .expanded, state.view == .prompt else { return }
|
||||
guard state.mode == .expanded, state.view.isTall else { return }
|
||||
withAnimation(openSpring) { islandHeight = chatPromptHeight }
|
||||
}
|
||||
.onChange(of: state.chatWidth) { _, w in
|
||||
guard state.mode == .expanded, state.view == .prompt else { return }
|
||||
guard state.mode == .expanded, state.view.isTall else { return }
|
||||
withAnimation(openSpring) { islandWidth = w }
|
||||
}
|
||||
.onAppear {
|
||||
@@ -171,7 +171,7 @@ struct IslandContainer: View {
|
||||
progress: state.uploadProgress,
|
||||
nw: state.notchWidth, nh: state.notchHeight)
|
||||
islandWidth = w
|
||||
islandHeight = state.view == .prompt ? chatPromptHeight : h
|
||||
islandHeight = state.view.isTall ? chatPromptHeight : h
|
||||
cornerRadius = state.mode == .expanded ? IslandConst.expandedCorner : IslandConst.roundedCorner
|
||||
islandTopRadius = 0
|
||||
}
|
||||
@@ -503,7 +503,7 @@ struct IslandContentView: View {
|
||||
// Views that fill available height instead of the fixed 98pt content frame:
|
||||
// chat (prompt) is always flexible; mail is flexible only when active so
|
||||
// it doesn't push the ZStack taller when inactive.
|
||||
let isTall = v == .prompt || (v == .mail && active)
|
||||
let isTall = v == .prompt || ((v == .mail || v == .terminal) && active)
|
||||
let anim: Animation = active
|
||||
? .spring(response: 0.4, dampingFraction: 0.8).delay(0.16)
|
||||
: .easeIn(duration: 0.16)
|
||||
@@ -530,20 +530,18 @@ struct IslandContentView: View {
|
||||
|
||||
struct IslandHeader: View {
|
||||
@ObservedObject var state: AppState
|
||||
private var leftTabs: [BroTab] { Array(state.tabs.prefix(3)) }
|
||||
private var rightTabs: [BroTab] { Array(state.tabs.dropFirst(3)) }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Left: tab capsules
|
||||
// Tabs are split around the camera: three to its left, the rest to its right,
|
||||
// with "+" right after the last tab.
|
||||
HStack(spacing: 5) {
|
||||
TabButton(icon: "house.fill", view: .overview, state: state)
|
||||
TabButton(icon: "bubble.left.fill", view: .prompt, state: state, preAction: {
|
||||
#if !APPSTORE
|
||||
if state.promptContext == nil {
|
||||
state.promptContext = WindowContextCapture.captureActive(from: state.lastExternalApp)
|
||||
}
|
||||
#endif
|
||||
})
|
||||
TabButton(icon: "plus", view: .upload, state: state)
|
||||
ForEach(leftTabs) { TabChip(tab: $0, state: state) }
|
||||
if rightTabs.isEmpty { NewTabButton(state: state) }
|
||||
}
|
||||
.padding(.leading, 14)
|
||||
|
||||
@@ -551,12 +549,17 @@ struct IslandHeader: View {
|
||||
|
||||
// Right: plan pill (GitHub build, home view only) + action icons
|
||||
HStack(spacing: 8) {
|
||||
#if !APPSTORE
|
||||
if !rightTabs.isEmpty {
|
||||
HStack(spacing: 5) {
|
||||
ForEach(rightTabs) { TabChip(tab: $0, state: state) }
|
||||
NewTabButton(state: state)
|
||||
}
|
||||
}
|
||||
// Plan limits live inside the Claude / ChatGPT pills (AppGridView):
|
||||
// here they ran under the camera.
|
||||
#endif
|
||||
HStack(spacing: 14) {
|
||||
// Fold everything back into the notch (and the chat to its normal size).
|
||||
if state.view.isTall {
|
||||
Button(action: {
|
||||
state.chatDragHeight = nil
|
||||
state.chatDetent = 0
|
||||
@@ -570,6 +573,7 @@ struct IslandHeader: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Свернуть в чёлку")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
||||
|
||||
Reference in New Issue
Block a user