From 8b9712aa5a47db9e3ca5858247e390f2700f9701 Mon Sep 17 00:00:00 2001 From: Timur Kheyfets Date: Tue, 10 Mar 2026 16:26:31 -0400 Subject: [PATCH 01/11] M1: Add ChatLoadingSkeleton component (#14893) * Add ChatLoadingSkeleton component with skeleton message bubbles Co-Authored-By: Claude * Remove unused avatarReserve property and add #if DEBUG guard to preview * Use adaptive maxWidth for skeleton bones to handle narrow containers * Use VRadius.pill instead of hardcoded radius for avatar circle --------- Co-authored-by: Claude --- .../Features/Chat/ChatLoadingSkeleton.swift | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift new file mode 100644 index 00000000000..eaa82f51d6f --- /dev/null +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -0,0 +1,72 @@ +import SwiftUI +import VellumAssistantShared + +/// Skeleton placeholder for the chat area while a thread is loading. +/// Shows alternating assistant/user message bones that mimic the real +/// `ChatBubble` layout, replacing a generic spinner with a content-aware preview. +struct ChatLoadingSkeleton: View { + /// Defines a skeleton row: either an assistant or user message placeholder. + private struct SkeletonRow: Identifiable { + let id: Int + let isUser: Bool + /// Fraction of `chatBubbleMaxWidth` the main bone should occupy. + let widthFraction: CGFloat + /// Height of the main text bone. + let boneHeight: CGFloat + } + + private let rows: [SkeletonRow] = [ + SkeletonRow(id: 0, isUser: false, widthFraction: 0.75, boneHeight: 48), + SkeletonRow(id: 1, isUser: true, widthFraction: 0.40, boneHeight: 20), + SkeletonRow(id: 2, isUser: false, widthFraction: 0.60, boneHeight: 32), + SkeletonRow(id: 3, isUser: true, widthFraction: 0.35, boneHeight: 20), + ] + + var body: some View { + VStack(alignment: .leading, spacing: VSpacing.sm) { + ForEach(rows) { row in + if row.isUser { + userRow(row) + } else { + assistantRow(row) + } + } + } + .accessibilityHidden(true) + } + + // MARK: - Assistant Row + + @ViewBuilder + private func assistantRow(_ row: SkeletonRow) -> some View { + HStack(alignment: .top, spacing: VSpacing.sm) { + // Avatar placeholder + VSkeletonBone(width: 28, height: 28, radius: VRadius.pill) + + // Text bone + VSkeletonBone(height: row.boneHeight, radius: VRadius.lg) + .frame(maxWidth: VSpacing.chatBubbleMaxWidth * row.widthFraction) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + // MARK: - User Row + + @ViewBuilder + private func userRow(_ row: SkeletonRow) -> some View { + VSkeletonBone(height: row.boneHeight, radius: VRadius.lg) + .frame(maxWidth: VSpacing.chatBubbleMaxWidth * row.widthFraction) + .frame(maxWidth: .infinity, alignment: .trailing) + } +} + +#if DEBUG +#Preview("ChatLoadingSkeleton") { + ZStack { + VColor.background.ignoresSafeArea() + ChatLoadingSkeleton() + .padding(VSpacing.lg) + } + .frame(width: 700, height: 400) +} +#endif From 63397380445a0680bded46f39d487dba1831d636 Mon Sep 17 00:00:00 2001 From: Timur Kheyfets Date: Tue, 10 Mar 2026 16:42:35 -0400 Subject: [PATCH 02/11] M2: Replace spinners with ChatLoadingSkeleton (#14902) * Replace loading spinners with ChatLoadingSkeleton in chat area Co-Authored-By: Claude * Update stale docstring on DaemonLoadingChatSkeleton * Add accessibility labels for VoiceOver on skeleton loading states --------- Co-authored-by: Claude --- .../Features/Chat/ChatView.swift | 37 ++++++++----------- .../MainWindow/DaemonLoadingOverlay.swift | 7 ++-- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift index 3ee4a320623..1b40bc4b26a 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift @@ -108,14 +108,11 @@ struct ChatView: View { APIKeyBanner(onOpenSettings: onOpenSettings) } if messages.isEmpty && !isHistoryLoaded { - Spacer() - HStack { - Spacer() - ProgressView() - .controlSize(.small) - Spacer() - } - Spacer() + ChatLoadingSkeleton() + .padding(VSpacing.lg) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityElement(children: .ignore) + .accessibilityLabel("Loading chat history") } else if isEmptyState && isBootstrapping { // During first-launch bootstrap, suppress the empty state // and show a simple loading panel until the first assistant @@ -404,21 +401,17 @@ private struct ChatBootstrapLoadingView: View { @State private var visible = false var body: some View { - VStack(spacing: VSpacing.lg) { - Spacer() - VLoadingIndicator(size: 24, color: VColor.accent) - Text("Getting ready...") - .font(VFont.body) - .foregroundColor(VColor.textSecondary) - Spacer() - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .opacity(visible ? 1 : 0) - .onAppear { - withAnimation(VAnimation.standard) { - visible = true + ChatLoadingSkeleton() + .padding(VSpacing.lg) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityElement(children: .ignore) + .accessibilityLabel("Getting ready") + .opacity(visible ? 1 : 0) + .onAppear { + withAnimation(VAnimation.standard) { + visible = true + } } - } } } diff --git a/clients/macos/vellum-assistant/Features/MainWindow/DaemonLoadingOverlay.swift b/clients/macos/vellum-assistant/Features/MainWindow/DaemonLoadingOverlay.swift index f9970720880..b741d7ccce0 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/DaemonLoadingOverlay.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/DaemonLoadingOverlay.swift @@ -1,14 +1,15 @@ import SwiftUI import VellumAssistantShared -/// Blank page with a centered loading spinner, shown over the chat area -/// while waiting for the daemon to connect. +/// Skeleton placeholder shown over the chat area while waiting for the +/// daemon to connect. struct DaemonLoadingChatSkeleton: View { var body: some View { ZStack { VColor.backgroundSubtle .clipShape(RoundedRectangle(cornerRadius: VRadius.xl)) - VLoadingIndicator(size: 24, color: VColor.textMuted) + ChatLoadingSkeleton() + .padding(VSpacing.lg) } .accessibilityHidden(true) } From 466fee595f5a9fbb2c1cb179a09db0edfd52f7e6 Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 17:53:25 -0400 Subject: [PATCH 03/11] fix: remove accessibilityHidden from ChatLoadingSkeleton The skeleton itself should not unconditionally hide from VoiceOver. Each usage site already controls accessibility appropriately: - DaemonLoadingOverlay hides the ZStack (decorative) - ChatView loading states use accessibilityElement + accessibilityLabel Co-Authored-By: Claude Opus 4.6 --- .../vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index eaa82f51d6f..6a9149c6de4 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -32,7 +32,6 @@ struct ChatLoadingSkeleton: View { } } } - .accessibilityHidden(true) } // MARK: - Assistant Row From 7710fa4906e0212ad23e7219d135cc93b82f5cc1 Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:06:56 -0400 Subject: [PATCH 04/11] fix: rewrite skeleton to match real chat bubble layout - User message: right-aligned bubble with two short text lines, matching real ChatBubble user styling (fill, padding, corner radius) - Assistant message: left-aligned with 28pt avatar placeholder and six varying-width text lines, using the same overlay/offset pattern as real ChatBubble (avatar at -(28+sm), content padded .leading 36pt) - Spacing uses VSpacing.md between messages (matching LazyVStack) - Constrained to chatColumnMaxWidth for proper centering Co-Authored-By: Claude Opus 4.6 --- .../Features/Chat/ChatLoadingSkeleton.swift | 94 ++++++++++--------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index 6a9149c6de4..7cfbdc07ae5 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -2,60 +2,68 @@ import SwiftUI import VellumAssistantShared /// Skeleton placeholder for the chat area while a thread is loading. -/// Shows alternating assistant/user message bones that mimic the real -/// `ChatBubble` layout, replacing a generic spinner with a content-aware preview. +/// Mimics the real `ChatBubble` layout — a short user message followed by a +/// multi-line assistant response — so the transition to real content feels seamless. struct ChatLoadingSkeleton: View { - /// Defines a skeleton row: either an assistant or user message placeholder. - private struct SkeletonRow: Identifiable { - let id: Int - let isUser: Bool - /// Fraction of `chatBubbleMaxWidth` the main bone should occupy. - let widthFraction: CGFloat - /// Height of the main text bone. - let boneHeight: CGFloat - } - - private let rows: [SkeletonRow] = [ - SkeletonRow(id: 0, isUser: false, widthFraction: 0.75, boneHeight: 48), - SkeletonRow(id: 1, isUser: true, widthFraction: 0.40, boneHeight: 20), - SkeletonRow(id: 2, isUser: false, widthFraction: 0.60, boneHeight: 32), - SkeletonRow(id: 3, isUser: true, widthFraction: 0.35, boneHeight: 20), - ] + /// Line widths for the multi-line assistant text block. + /// Varying lengths look more natural than uniform bones. + private let assistantLineWidths: [CGFloat] = [0.92, 0.85, 0.78, 0.95, 0.70, 0.45] var body: some View { - VStack(alignment: .leading, spacing: VSpacing.sm) { - ForEach(rows) { row in - if row.isUser { - userRow(row) - } else { - assistantRow(row) - } - } + VStack(alignment: .leading, spacing: VSpacing.md) { + userMessage + assistantMessage } + .frame(maxWidth: VSpacing.chatColumnMaxWidth, alignment: .leading) } - // MARK: - Assistant Row - - @ViewBuilder - private func assistantRow(_ row: SkeletonRow) -> some View { - HStack(alignment: .top, spacing: VSpacing.sm) { - // Avatar placeholder - VSkeletonBone(width: 28, height: 28, radius: VRadius.pill) + // MARK: - User Message - // Text bone - VSkeletonBone(height: row.boneHeight, radius: VRadius.lg) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth * row.widthFraction) + /// Right-aligned user bubble with two short text lines inside, + /// matching real ChatBubble user styling (fill + padding + corner radius). + private var userMessage: some View { + VStack(alignment: .trailing, spacing: VSpacing.xs) { + VSkeletonBone(width: 180, height: 14, radius: VRadius.sm) + VSkeletonBone(width: 120, height: 14, radius: VRadius.sm) } - .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, VSpacing.lg) + .padding(.vertical, VSpacing.md) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .fill(VColor.surfaceBorder.opacity(0.25)) + ) + .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .trailing) + .frame(maxWidth: .infinity, alignment: .trailing) } - // MARK: - User Row + // MARK: - Assistant Message - @ViewBuilder - private func userRow(_ row: SkeletonRow) -> some View { - VSkeletonBone(height: row.boneHeight, radius: VRadius.lg) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth * row.widthFraction) - .frame(maxWidth: .infinity, alignment: .trailing) + /// Left-aligned assistant block with avatar placeholder and six text lines, + /// matching real ChatBubble assistant layout (28pt avatar + 8pt gap + content). + private var assistantMessage: some View { + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading, spacing: VSpacing.xs) { + ForEach(assistantLineWidths.indices, id: \.self) { idx in + VSkeletonBone( + height: 14, + radius: VRadius.sm + ) + .frame( + maxWidth: VSpacing.chatBubbleMaxWidth * assistantLineWidths[idx], + alignment: .leading + ) + } + } + .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) + .overlay(alignment: .topLeading) { + // Avatar bone positioned identically to real ChatBubble + VSkeletonBone(width: 28, height: 28, radius: VRadius.pill) + .offset(x: -(28 + VSpacing.sm), y: 0) + } + .padding(.leading, 28 + VSpacing.sm) + + Spacer(minLength: 0) + } } } From 6c1da198ee86395f106c77500aab4452c98ac486 Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:11:46 -0400 Subject: [PATCH 05/11] fix: align skeleton to top, use real avatar, add assistant bubble - Start from top of chat area instead of vertically centered - Use real assistant avatar (AvatarAppearanceManager) instead of skeleton bone for the avatar circle - Add subtle bubble background to assistant message block - Add Spacer at bottom of skeleton to push content to top Co-Authored-By: Claude Opus 4.6 --- .../Features/Chat/ChatLoadingSkeleton.swift | 24 +++++++++++++++---- .../Features/Chat/ChatView.swift | 4 ++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index 7cfbdc07ae5..bd3539380e2 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -5,6 +5,8 @@ import VellumAssistantShared /// Mimics the real `ChatBubble` layout — a short user message followed by a /// multi-line assistant response — so the transition to real content feels seamless. struct ChatLoadingSkeleton: View { + @State private var appearance = AvatarAppearanceManager.shared + /// Line widths for the multi-line assistant text block. /// Varying lengths look more natural than uniform bones. private let assistantLineWidths: [CGFloat] = [0.92, 0.85, 0.78, 0.95, 0.70, 0.45] @@ -13,6 +15,7 @@ struct ChatLoadingSkeleton: View { VStack(alignment: .leading, spacing: VSpacing.md) { userMessage assistantMessage + Spacer(minLength: 0) } .frame(maxWidth: VSpacing.chatColumnMaxWidth, alignment: .leading) } @@ -38,8 +41,8 @@ struct ChatLoadingSkeleton: View { // MARK: - Assistant Message - /// Left-aligned assistant block with avatar placeholder and six text lines, - /// matching real ChatBubble assistant layout (28pt avatar + 8pt gap + content). + /// Left-aligned assistant block with real avatar and six text lines inside + /// a subtle bubble, matching real ChatBubble assistant layout. private var assistantMessage: some View { HStack(alignment: .top, spacing: 0) { VStack(alignment: .leading, spacing: VSpacing.xs) { @@ -54,10 +57,23 @@ struct ChatLoadingSkeleton: View { ) } } + .padding(.horizontal, VSpacing.lg) + .padding(.vertical, VSpacing.md) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .fill(VColor.surfaceBorder.opacity(0.15)) + ) .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) .overlay(alignment: .topLeading) { - // Avatar bone positioned identically to real ChatBubble - VSkeletonBone(width: 28, height: 28, radius: VRadius.pill) + Image(nsImage: appearance.chatAvatarImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 28, height: 28) + .clipShape(Circle()) + .overlay( + Circle() + .strokeBorder(VColor.surfaceBorder, lineWidth: 1) + ) .offset(x: -(28 + VSpacing.sm), y: 0) } .padding(.leading, 28 + VSpacing.sm) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift index 1b40bc4b26a..ccedd24d634 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift @@ -110,7 +110,7 @@ struct ChatView: View { if messages.isEmpty && !isHistoryLoaded { ChatLoadingSkeleton() .padding(VSpacing.lg) - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .accessibilityElement(children: .ignore) .accessibilityLabel("Loading chat history") } else if isEmptyState && isBootstrapping { @@ -403,7 +403,7 @@ private struct ChatBootstrapLoadingView: View { var body: some View { ChatLoadingSkeleton() .padding(VSpacing.lg) - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .accessibilityElement(children: .ignore) .accessibilityLabel("Getting ready") .opacity(visible ? 1 : 0) From ec5308f7c27f64c57d402ca8c62fdaa47e2d9930 Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:13:50 -0400 Subject: [PATCH 06/11] fix: darker skeleton bones with subtler shimmer, wider user bubble - Use darker bone fill (surfaceBorder 0.7) instead of default 0.5 - Use surfaceBorder as shimmer highlight instead of bright surface color - Widen user message bones (280/200pt) for more realistic proportions Co-Authored-By: Claude Opus 4.6 --- .../Features/Chat/ChatLoadingSkeleton.swift | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index bd3539380e2..544cad16b19 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -11,6 +11,14 @@ struct ChatLoadingSkeleton: View { /// Varying lengths look more natural than uniform bones. private let assistantLineWidths: [CGFloat] = [0.92, 0.85, 0.78, 0.95, 0.70, 0.45] + /// Darker bone that uses a subtler shimmer to avoid the bright white sweep. + private func chatBone(width: CGFloat? = nil, height: CGFloat = 14) -> some View { + RoundedRectangle(cornerRadius: VRadius.sm) + .fill(VColor.surfaceBorder.opacity(0.7)) + .frame(width: width, height: height) + .vShimmer(highlightColor: VColor.surfaceBorder) + } + var body: some View { VStack(alignment: .leading, spacing: VSpacing.md) { userMessage @@ -22,12 +30,12 @@ struct ChatLoadingSkeleton: View { // MARK: - User Message - /// Right-aligned user bubble with two short text lines inside, + /// Right-aligned user bubble with two text lines inside, /// matching real ChatBubble user styling (fill + padding + corner radius). private var userMessage: some View { VStack(alignment: .trailing, spacing: VSpacing.xs) { - VSkeletonBone(width: 180, height: 14, radius: VRadius.sm) - VSkeletonBone(width: 120, height: 14, radius: VRadius.sm) + chatBone(width: 280, height: 14) + chatBone(width: 200, height: 14) } .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.md) @@ -47,14 +55,11 @@ struct ChatLoadingSkeleton: View { HStack(alignment: .top, spacing: 0) { VStack(alignment: .leading, spacing: VSpacing.xs) { ForEach(assistantLineWidths.indices, id: \.self) { idx in - VSkeletonBone( - height: 14, - radius: VRadius.sm - ) - .frame( - maxWidth: VSpacing.chatBubbleMaxWidth * assistantLineWidths[idx], - alignment: .leading - ) + chatBone(height: 14) + .frame( + maxWidth: VSpacing.chatBubbleMaxWidth * assistantLineWidths[idx], + alignment: .leading + ) } } .padding(.horizontal, VSpacing.lg) From 7f2909e54c3d338180e477d4dfc49c2b86623745 Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:16:02 -0400 Subject: [PATCH 07/11] fix: make user skeleton bubble full chatBubbleMaxWidth User skeleton bubble now spans the same width as the assistant block (680pt) instead of shrink-wrapping around short bones. Co-Authored-By: Claude Opus 4.6 --- .../Features/Chat/ChatLoadingSkeleton.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index 544cad16b19..a05e3644c62 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -34,8 +34,9 @@ struct ChatLoadingSkeleton: View { /// matching real ChatBubble user styling (fill + padding + corner radius). private var userMessage: some View { VStack(alignment: .trailing, spacing: VSpacing.xs) { - chatBone(width: 280, height: 14) - chatBone(width: 200, height: 14) + chatBone(height: 14) + chatBone(height: 14) + .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.6, alignment: .trailing) } .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.md) @@ -43,7 +44,7 @@ struct ChatLoadingSkeleton: View { RoundedRectangle(cornerRadius: VRadius.lg) .fill(VColor.surfaceBorder.opacity(0.25)) ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .trailing) + .frame(maxWidth: VSpacing.chatBubbleMaxWidth) .frame(maxWidth: .infinity, alignment: .trailing) } From b4bf651bd61764f92f12ee1841758dee3d40b281 Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:18:41 -0400 Subject: [PATCH 08/11] fix: use textMuted for skeleton bones so they're visible in light mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit surfaceBorder is Moss._100 in light mode — same as background, making bones invisible. Switch to textMuted (Stone._600 light / Moss._500 dark) which contrasts well against the background in both color schemes. Co-Authored-By: Claude Opus 4.6 --- .../Features/Chat/ChatLoadingSkeleton.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index a05e3644c62..53a37fdabe2 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -14,9 +14,9 @@ struct ChatLoadingSkeleton: View { /// Darker bone that uses a subtler shimmer to avoid the bright white sweep. private func chatBone(width: CGFloat? = nil, height: CGFloat = 14) -> some View { RoundedRectangle(cornerRadius: VRadius.sm) - .fill(VColor.surfaceBorder.opacity(0.7)) + .fill(VColor.textMuted.opacity(0.15)) .frame(width: width, height: height) - .vShimmer(highlightColor: VColor.surfaceBorder) + .vShimmer(highlightColor: VColor.textMuted.opacity(0.1)) } var body: some View { @@ -42,7 +42,7 @@ struct ChatLoadingSkeleton: View { .padding(.vertical, VSpacing.md) .background( RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.surfaceBorder.opacity(0.25)) + .fill(VColor.textMuted.opacity(0.06)) ) .frame(maxWidth: VSpacing.chatBubbleMaxWidth) .frame(maxWidth: .infinity, alignment: .trailing) @@ -67,7 +67,7 @@ struct ChatLoadingSkeleton: View { .padding(.vertical, VSpacing.md) .background( RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.surfaceBorder.opacity(0.15)) + .fill(VColor.textMuted.opacity(0.06)) ) .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) .overlay(alignment: .topLeading) { @@ -78,7 +78,7 @@ struct ChatLoadingSkeleton: View { .clipShape(Circle()) .overlay( Circle() - .strokeBorder(VColor.surfaceBorder, lineWidth: 1) + .strokeBorder(VColor.textMuted.opacity(0.2), lineWidth: 1) ) .offset(x: -(28 + VSpacing.sm), y: 0) } From ac72caf08715850418abf9ff06e6747e9758e776 Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:23:14 -0400 Subject: [PATCH 09/11] fix: narrow user skeleton bubble to 65% of chatBubbleMaxWidth User messages are typically shorter than assistant responses. The skeleton now reflects this with a 65% width bubble. Co-Authored-By: Claude Opus 4.6 --- .../vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index 53a37fdabe2..f646600cf75 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -36,7 +36,7 @@ struct ChatLoadingSkeleton: View { VStack(alignment: .trailing, spacing: VSpacing.xs) { chatBone(height: 14) chatBone(height: 14) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.6, alignment: .trailing) + .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.45, alignment: .trailing) } .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.md) @@ -44,7 +44,7 @@ struct ChatLoadingSkeleton: View { RoundedRectangle(cornerRadius: VRadius.lg) .fill(VColor.textMuted.opacity(0.06)) ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth) + .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.65) .frame(maxWidth: .infinity, alignment: .trailing) } From 28c0cecfcfb5ac546599dbc848162a63d8e62b3e Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:25:08 -0400 Subject: [PATCH 10/11] fix: remove bubble backgrounds from skeleton to match real chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real ChatBubble uses Color.clear for assistant messages and the skeleton doesn't need bubble chrome — just bare bones matching the layout. Co-Authored-By: Claude Opus 4.6 --- .../Features/Chat/ChatLoadingSkeleton.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index f646600cf75..1be4952f637 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -38,12 +38,6 @@ struct ChatLoadingSkeleton: View { chatBone(height: 14) .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.45, alignment: .trailing) } - .padding(.horizontal, VSpacing.lg) - .padding(.vertical, VSpacing.md) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.textMuted.opacity(0.06)) - ) .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.65) .frame(maxWidth: .infinity, alignment: .trailing) } @@ -63,12 +57,6 @@ struct ChatLoadingSkeleton: View { ) } } - .padding(.horizontal, VSpacing.lg) - .padding(.vertical, VSpacing.md) - .background( - RoundedRectangle(cornerRadius: VRadius.lg) - .fill(VColor.textMuted.opacity(0.06)) - ) .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) .overlay(alignment: .topLeading) { Image(nsImage: appearance.chatAvatarImage) From aba2e197d2f75a4cdf6f9d666bb1fa60cceb244e Mon Sep 17 00:00:00 2001 From: tkheyfets Date: Tue, 10 Mar 2026 18:27:40 -0400 Subject: [PATCH 11/11] fix: add VColor.userBubble to skeleton user message User messages have a visible bubble in both dark and light modes (Moss._950 / Moss._200). Skeleton now uses the real userBubble color with proper padding and corner radius matching ChatBubble. Co-Authored-By: Claude Opus 4.6 --- .../Features/Chat/ChatLoadingSkeleton.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift index 1be4952f637..b8f14dcaab6 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatLoadingSkeleton.swift @@ -38,6 +38,12 @@ struct ChatLoadingSkeleton: View { chatBone(height: 14) .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.45, alignment: .trailing) } + .padding(.horizontal, VSpacing.lg) + .padding(.vertical, VSpacing.md) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .fill(VColor.userBubble) + ) .frame(maxWidth: VSpacing.chatBubbleMaxWidth * 0.65) .frame(maxWidth: .infinity, alignment: .trailing) }