fix: polish macos web chat composer

This commit is contained in:
Peter Steinberger
2026-01-01 12:49:05 +01:00
parent c7e2b1230c
commit 5b33a7dcbe
4 changed files with 62 additions and 20 deletions

2
.gitignore vendored
View File

@@ -9,6 +9,8 @@ coverage
.DS_Store .DS_Store
**/.DS_Store **/.DS_Store
ui/src/ui/__screenshots__/ ui/src/ui/__screenshots__/
ui/playwright-report/
ui/test-results/
# Bun build artifacts # Bun build artifacts
*.bun-build *.bun-build

View File

@@ -41,7 +41,7 @@
- Android Chat UI: use `onPrimary` for user bubble text to preserve contrast (thanks @Syhids). - Android Chat UI: use `onPrimary` for user bubble text to preserve contrast (thanks @Syhids).
- Control UI: sync sidebar navigation with the URL for deep-linking, and auto-scroll chat to the latest message. - Control UI: sync sidebar navigation with the URL for deep-linking, and auto-scroll chat to the latest message.
- Control UI: disable Web Chat + Talk when no iOS/Android node is connected; refreshed Web Chat styling and keyboard send. - Control UI: disable Web Chat + Talk when no iOS/Android node is connected; refreshed Web Chat styling and keyboard send.
- macOS Web Chat: improve empty/error states, focus message field on open, and keep pill/send inside the input field. - macOS Web Chat: improve empty/error states, focus message field on open, keep pill/send inside the input field, and make the composer pill edge-to-edge with square top corners.
- macOS: bundle Control UI assets into the app relay so the packaged app can serve them (thanks @mbelinky). - macOS: bundle Control UI assets into the app relay so the packaged app can serve them (thanks @mbelinky).
- Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android). - Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android).
- iOS Talk Mode: fix chat completion wait to time out even if no events arrive (prevents “Thinking…” hangs). - iOS Talk Mode: fix chat completion wait to time out even if no events arrive (prevents “Thinking…” hangs).

View File

@@ -37,20 +37,44 @@ struct ClawdisChatComposer: View {
self.editor self.editor
} }
.padding(self.composerPadding) .padding(self.composerPadding)
.background( .background {
RoundedRectangle(cornerRadius: 18, style: .continuous) let cornerRadius: CGFloat = 18
#if os(macOS)
if self.style == .standard {
let shape = UnevenRoundedRectangle(
cornerRadii: RectangleCornerRadii(
topLeading: 0,
bottomLeading: cornerRadius,
bottomTrailing: cornerRadius,
topTrailing: 0),
style: .continuous)
shape
.fill(ClawdisChatTheme.composerBackground)
.overlay(shape.strokeBorder(ClawdisChatTheme.composerBorder, lineWidth: 1))
.shadow(color: .black.opacity(0.12), radius: 12, y: 6)
} else {
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
shape
.fill(ClawdisChatTheme.composerBackground)
.overlay(shape.strokeBorder(ClawdisChatTheme.composerBorder, lineWidth: 1))
.shadow(color: .black.opacity(0.12), radius: 12, y: 6)
}
#else
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
shape
.fill(ClawdisChatTheme.composerBackground) .fill(ClawdisChatTheme.composerBackground)
.overlay( .overlay(shape.strokeBorder(ClawdisChatTheme.composerBorder, lineWidth: 1))
RoundedRectangle(cornerRadius: 18, style: .continuous) .shadow(color: .black.opacity(0.12), radius: 12, y: 6)
.strokeBorder(ClawdisChatTheme.composerBorder, lineWidth: 1)) #endif
.shadow(color: .black.opacity(0.12), radius: 12, y: 6)) }
#if os(macOS) #if os(macOS)
.onDrop(of: [.fileURL], isTargeted: nil) { providers in .onDrop(of: [.fileURL], isTargeted: nil) { providers in
self.handleDrop(providers) self.handleDrop(providers)
} }
.onAppear { .onAppear {
self.shouldFocusTextView = true self.shouldFocusTextView = true
} }
#endif #endif
} }

View File

@@ -21,7 +21,7 @@ public struct ClawdisChatView: View {
static let outerPaddingHorizontal: CGFloat = 6 static let outerPaddingHorizontal: CGFloat = 6
static let outerPaddingVertical: CGFloat = 0 static let outerPaddingVertical: CGFloat = 0
static let composerPaddingHorizontal: CGFloat = 0 static let composerPaddingHorizontal: CGFloat = 0
static let stackSpacing: CGFloat = 6 static let stackSpacing: CGFloat = 0
static let messageSpacing: CGFloat = 6 static let messageSpacing: CGFloat = 6
static let messageListPaddingTop: CGFloat = 0 static let messageListPaddingTop: CGFloat = 0
static let messageListPaddingBottom: CGFloat = 4 static let messageListPaddingBottom: CGFloat = 4
@@ -79,13 +79,33 @@ public struct ClawdisChatView: View {
private var messageList: some View { private var messageList: some View {
ZStack { ZStack {
ScrollView { ScrollView {
LazyVStack(spacing: Layout.messageSpacing) { #if os(macOS)
self.messageListRows VStack(spacing: 0) {
LazyVStack(spacing: Layout.messageSpacing) {
self.messageListRows
}
Color.clear
.frame(height: 0)
.id(self.scrollerBottomID)
} }
// Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches. // Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches.
.scrollTargetLayout() .scrollTargetLayout()
.padding(.top, Layout.messageListPaddingTop) .padding(.top, Layout.messageListPaddingTop)
.padding(.horizontal, Layout.messageListPaddingHorizontal) .padding(.horizontal, Layout.messageListPaddingHorizontal)
#else
LazyVStack(spacing: Layout.messageSpacing) {
self.messageListRows
Color.clear
.frame(height: Layout.messageListPaddingBottom + 1)
.id(self.scrollerBottomID)
}
// Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches.
.scrollTargetLayout()
.padding(.top, Layout.messageListPaddingTop)
.padding(.horizontal, Layout.messageListPaddingHorizontal)
#endif
} }
// Keep the scroll pinned to the bottom for new messages. // Keep the scroll pinned to the bottom for new messages.
.scrollPosition(id: self.$scrollPosition, anchor: .bottom) .scrollPosition(id: self.$scrollPosition, anchor: .bottom)
@@ -147,10 +167,6 @@ public struct ClawdisChatView: View {
ChatStreamingAssistantBubble(text: text) ChatStreamingAssistantBubble(text: text)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
Color.clear
.frame(height: Layout.messageListPaddingBottom + 1)
.id(self.scrollerBottomID)
} }
private var visibleMessages: [ClawdisChatMessage] { private var visibleMessages: [ClawdisChatMessage] {