Unified · Split
| @@ -263,7 +263,7 @@ export function ToolBelt({ · 262 unmodified lines | ||
| 263 | 263 | const canSend = value.trim() || attachments.length > 0; |
| 264 | 264 | |
| 265 | 265 | return ( |
| 266 | − <div className="px-2 md:px-3 pb-0 bg-(--background)"> | |
| 266 | + <div className="px-2 md:px-3 pb-0"> | |
| 267 | 267 | <div className="w-full max-w-none md:max-w-4xl md:mx-auto px-0 md:px-0"> |
| 268 | 268 | <AttachmentsPreview |
| 269 | 269 | attachments={attachments} |
| @@ -11,7 +11,7 @@ export function ChatToolbeltDock({ toolBelt, showEmptyState }: ChatToolbeltDockP · 10 unmodified lines | ||
| 11 | 11 | return ( |
| 12 | 12 | <div className="fixed left-0 right-0 bottom-0 z-20 md:static"> |
| 13 | 13 | {/* Mobile: flush to bottom, no extra padding */} |
| 14 | − <div className="md:hidden bg-[hsl(30,5%,10.5%)]"> | |
| 14 | + <div className="md:hidden"> | |
| 15 | 15 | {toolBelt} |
| 16 | 16 | </div> |
| 17 | 17 | {!showEmptyState && ( |
| @@ -117,17 +117,6 @@ export function UnifiedSidebar({ · 116 unmodified lines | ||
| 117 | 117 | {children} |
| 118 | 118 | </div> |
| 119 | 119 | |
| 120 | − {/* Toggle button when closed */} | |
| 121 | − {!isOpen && ( | |
| 122 | − <button | |
| 123 | − onClick={onToggle} | |
| 124 | − className="hidden md:flex items-center justify-center w-8 h-8 mt-4 mr-2 rounded-lg bg-white/[0.03] hover:bg-white/[0.06] border border-white/[0.06] text-[#666] hover:text-[#888] transition-colors flex-shrink-0" | |
| 125 | − title="Open sidebar" | |
| 126 | − > | |
| 127 | − <PanelRightOpen className="h-4 w-4" /> | |
| 128 | − </button> | |
| 129 | − )} | |
| 130 | − | |
| 131 | 120 | {/* Resizable sidebar */} |
| 132 | 121 | {isOpen && ( |
| 133 | 122 | <div |
| @@ -16,11 +16,17 @@ struct ChatDetailView: View { · 15 unmodified lines | ||
| 16 | 16 | ScrollView { |
| 17 | 17 | VStack(spacing: 12) { |
| 18 | 18 | if let modelId = model.sessionModel { |
| 19 | − HStack { | |
| 20 | − Text(modelId).font(AppTheme.captionFont).foregroundColor(AppTheme.muted) | |
| 19 | + HStack(spacing: 6) { | |
| 20 | + Image(systemName: "cpu") | |
| 21 | + .font(.system(size: 10)) | |
| 22 | + .foregroundColor(AppTheme.accentStrong) | |
| 23 | + Text(modelId) | |
| 24 | + .font(AppTheme.captionFont.weight(.medium)) | |
| 25 | + .foregroundColor(AppTheme.muted) | |
| 21 | 26 | Spacer() |
| 22 | 27 | } |
| 23 | 28 | .padding(.horizontal, 4) |
| 29 | + .padding(.bottom, 4) | |
| 24 | 30 | } |
| 25 | 31 | if let error = model.error { |
| 26 | 32 | HStack { |
| @@ -27,10 +27,14 @@ struct ChatMessageRow: View { · 26 unmodified lines | ||
| 27 | 27 | Text(message.content ?? "") |
| 28 | 28 | .font(AppTheme.bodyFont) |
| 29 | 29 | .foregroundColor(AppTheme.foreground) |
| 30 | − .padding(.horizontal, 14) | |
| 31 | − .padding(.vertical, 10) | |
| 32 | − .background(AppTheme.card) | |
| 33 | − .cornerRadius(16) | |
| 30 | + .padding(.horizontal, 16) | |
| 31 | + .padding(.vertical, 12) | |
| 32 | + .background(AppTheme.accent.opacity(0.25)) | |
| 33 | + .cornerRadius(18) | |
| 34 | + .overlay( | |
| 35 | + RoundedRectangle(cornerRadius: 18) | |
| 36 | + .stroke(AppTheme.accent.opacity(0.4), lineWidth: 1) | |
| 37 | + ) | |
| 34 | 38 | } |
| 35 | 39 | } |
| 36 | 40 | |
| @@ -1,3 +1,4 @@ | ||
| 1 | +// CRITICAL | |
| 1 | 2 | import SwiftUI |
| 2 | 3 | |
| 3 | 4 | struct ChatStreamingMessageView: View { |
| @@ -6,27 +7,67 @@ struct ChatStreamingMessageView: View { · 2 unmodified lines | ||
| 6 | 7 | let onShowActions: (AgentMeta) -> Void |
| 7 | 8 | |
| 8 | 9 | var body: some View { |
| 9 | − let message = StoredMessage( | |
| 10 | − id: "streaming", | |
| 11 | − role: "assistant", | |
| 12 | − content: service.streamingContent, | |
| 13 | − model: nil, | |
| 14 | − toolCalls: nil | |
| 15 | − ) | |
| 16 | − let meta = AgentMeta( | |
| 17 | − thinkingBlocks: service.streamingReasoning.isEmpty ? [] : [service.streamingReasoning], | |
| 18 | − toolCalls: service.streamingToolCalls, | |
| 19 | − toolResults: [] | |
| 20 | − ) | |
| 21 | − ChatMessageRow( | |
| 22 | − message: message, | |
| 23 | − isStreaming: true, | |
| 24 | − meta: meta, | |
| 25 | − onShowActions: { onShowActions($0) } | |
| 26 | − ) | |
| 10 | + VStack(alignment: .leading, spacing: 10) { | |
| 11 | + // Streaming content | |
| 12 | + if !service.streamingContent.isEmpty { | |
| 13 | + MarkdownText(content: service.streamingContent) | |
| 14 | + .foregroundColor(AppTheme.foreground) | |
| 15 | + } | |
| 16 | + | |
| 17 | + // Live thinking/tracing indicator | |
| 18 | + if !service.streamingReasoning.isEmpty || !service.streamingToolCalls.isEmpty { | |
| 19 | + StreamingMetaIndicator( | |
| 20 | + reasoning: service.streamingReasoning, | |
| 21 | + toolCalls: service.streamingToolCalls | |
| 22 | + ) | |
| 23 | + } | |
| 24 | + } | |
| 25 | + .frame(maxWidth: .infinity, alignment: .leading) | |
| 27 | 26 | .id("streaming") |
| 28 | 27 | .onChange(of: service.streamingContent) { _, _ in |
| 29 | 28 | scrollProxy.scrollTo("streaming", anchor: .bottom) |
| 30 | 29 | } |
| 30 | + .onChange(of: service.streamingReasoning) { _, _ in | |
| 31 | + scrollProxy.scrollTo("streaming", anchor: .bottom) | |
| 32 | + } | |
| 33 | + } | |
| 34 | +} | |
| 35 | + | |
| 36 | +private struct StreamingMetaIndicator: View { | |
| 37 | + let reasoning: String | |
| 38 | + let toolCalls: [ToolCall] | |
| 39 | + | |
| 40 | + var body: some View { | |
| 41 | + VStack(alignment: .leading, spacing: 8) { | |
| 42 | + if !reasoning.isEmpty { | |
| 43 | + HStack(spacing: 6) { | |
| 44 | + Image(systemName: "brain") | |
| 45 | + .font(.system(size: 10)) | |
| 46 | + .foregroundColor(AppTheme.accentStrong) | |
| 47 | + Text("Reasoning...") | |
| 48 | + .font(AppTheme.captionFont) | |
| 49 | + .foregroundColor(AppTheme.accentStrong) | |
| 50 | + } | |
| 51 | + .padding(.horizontal, 10) | |
| 52 | + .padding(.vertical, 5) | |
| 53 | + .background(AppTheme.accent.opacity(0.15)) | |
| 54 | + .cornerRadius(8) | |
| 55 | + } | |
| 56 | + | |
| 57 | + if !toolCalls.isEmpty { | |
| 58 | + HStack(spacing: 6) { | |
| 59 | + Image(systemName: "wrench.and.screwdriver") | |
| 60 | + .font(.system(size: 10)) | |
| 61 | + .foregroundColor(AppTheme.warning) | |
| 62 | + Text("Using \(toolCalls.count) tool\(toolCalls.count == 1 ? "" : "s")...") | |
| 63 | + .font(AppTheme.captionFont) | |
| 64 | + .foregroundColor(AppTheme.warning) | |
| 65 | + } | |
| 66 | + .padding(.horizontal, 10) | |
| 67 | + .padding(.vertical, 5) | |
| 68 | + .background(AppTheme.warning.opacity(0.15)) | |
| 69 | + .cornerRadius(8) | |
| 70 | + } | |
| 71 | + } | |
| 31 | 72 | } |
| 32 | 73 | } |
| @@ -1,3 +1,4 @@ | ||
| 1 | +// CRITICAL | |
| 1 | 2 | import SwiftUI |
| 2 | 3 | |
| 3 | 4 | struct ChatUsageBar: View { |
| @@ -5,12 +6,12 @@ struct ChatUsageBar: View { · 1 unmodified line | ||
| 5 | 6 | |
| 6 | 7 | var body: some View { |
| 7 | 8 | if let usage { |
| 8 | − HStack(spacing: 12) { | |
| 9 | − UsagePill(label: "Prompt", value: usage.promptTokens) | |
| 10 | − UsagePill(label: "Completion", value: usage.completionTokens) | |
| 11 | − UsagePill(label: "Total", value: usage.totalTokens) | |
| 9 | + HStack(spacing: 8) { | |
| 10 | + UsagePill(label: "Input", value: usage.promptTokens, color: AppTheme.accentStrong) | |
| 11 | + UsagePill(label: "Output", value: usage.completionTokens, color: AppTheme.success) | |
| 12 | + UsagePill(label: "Total", value: usage.totalTokens, color: AppTheme.foreground) | |
| 12 | 13 | } |
| 13 | − .padding(.horizontal, 16) | |
| 14 | + .padding(.horizontal, 4) | |
| 14 | 15 | } |
| 15 | 16 | } |
| 16 | 17 | } |
| @@ -18,15 +19,24 @@ struct ChatUsageBar: View { · 1 unmodified line | ||
| 18 | 19 | struct UsagePill: View { |
| 19 | 20 | let label: String |
| 20 | 21 | let value: Int |
| 22 | + let color: Color | |
| 21 | 23 | |
| 22 | 24 | var body: some View { |
| 23 | − VStack(spacing: 2) { | |
| 24 | − Text(label).font(AppTheme.captionFont).foregroundColor(AppTheme.muted) | |
| 25 | − Text("\(value)").font(AppTheme.monoFont) | |
| 25 | + HStack(spacing: 4) { | |
| 26 | + Text(label) | |
| 27 | + .font(AppTheme.captionFont) | |
| 28 | + .foregroundColor(AppTheme.muted) | |
| 29 | + Text("\(value)") | |
| 30 | + .font(AppTheme.monoFont.weight(.medium)) | |
| 31 | + .foregroundColor(color) | |
| 26 | 32 | } |
| 27 | 33 | .padding(.vertical, 6) |
| 28 | 34 | .padding(.horizontal, 10) |
| 29 | 35 | .background(AppTheme.card) |
| 30 | − .cornerRadius(10) | |
| 36 | + .cornerRadius(8) | |
| 37 | + .overlay( | |
| 38 | + RoundedRectangle(cornerRadius: 8) | |
| 39 | + .stroke(AppTheme.border, lineWidth: 1) | |
| 40 | + ) | |
| 31 | 41 | } |
| 32 | 42 | } |