Unified · Split
| @@ -263,7 +263,7 @@ export function ToolBelt({ · 262 unmodified lines | |||
| 263 | const canSend = value.trim() || attachments.length > 0; | 263 | const canSend = value.trim() || attachments.length > 0; |
| 264 | 264 | ||
| 265 | return ( | 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 | <div className="w-full max-w-none md:max-w-4xl md:mx-auto px-0 md:px-0"> | 267 | <div className="w-full max-w-none md:max-w-4xl md:mx-auto px-0 md:px-0"> |
| 268 | <AttachmentsPreview | 268 | <AttachmentsPreview |
| 269 | attachments={attachments} | 269 | attachments={attachments} |
| @@ -11,7 +11,7 @@ export function ChatToolbeltDock({ toolBelt, showEmptyState }: ChatToolbeltDockP · 10 unmodified lines | |||
| 11 | return ( | 11 | return ( |
| 12 | <div className="fixed left-0 right-0 bottom-0 z-20 md:static"> | 12 | <div className="fixed left-0 right-0 bottom-0 z-20 md:static"> |
| 13 | {/* Mobile: flush to bottom, no extra padding */} | 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 | {toolBelt} | 15 | {toolBelt} |
| 16 | </div> | 16 | </div> |
| 17 | {!showEmptyState && ( | 17 | {!showEmptyState && ( |
| @@ -117,17 +117,6 @@ export function UnifiedSidebar({ · 116 unmodified lines | |||
| 117 | {children} | 117 | {children} |
| 118 | </div> | 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 | {/* Resizable sidebar */} | 120 | {/* Resizable sidebar */} |
| 132 | {isOpen && ( | 121 | {isOpen && ( |
| 133 | <div | 122 | <div |
| @@ -16,11 +16,17 @@ struct ChatDetailView: View { · 15 unmodified lines | |||
| 16 | ScrollView { | 16 | ScrollView { |
| 17 | VStack(spacing: 12) { | 17 | VStack(spacing: 12) { |
| 18 | if let modelId = model.sessionModel { | 18 | if let modelId = model.sessionModel { |
| 19 | HStack { | 19 | HStack(spacing: 6) { |
| 20 | Text(modelId).font(AppTheme.captionFont).foregroundColor(AppTheme.muted) | 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 | Spacer() | 26 | Spacer() |
| 22 | } | 27 | } |
| 23 | .padding(.horizontal, 4) | 28 | .padding(.horizontal, 4) |
| 29 | .padding(.bottom, 4) | ||
| 24 | } | 30 | } |
| 25 | if let error = model.error { | 31 | if let error = model.error { |
| 26 | HStack { | 32 | HStack { |
| @@ -27,10 +27,14 @@ struct ChatMessageRow: View { · 26 unmodified lines | |||
| 27 | Text(message.content ?? "") | 27 | Text(message.content ?? "") |
| 28 | .font(AppTheme.bodyFont) | 28 | .font(AppTheme.bodyFont) |
| 29 | .foregroundColor(AppTheme.foreground) | 29 | .foregroundColor(AppTheme.foreground) |
| 30 | .padding(.horizontal, 14) | 30 | .padding(.horizontal, 16) |
| 31 | .padding(.vertical, 10) | 31 | .padding(.vertical, 12) |
| 32 | .background(AppTheme.card) | 32 | .background(AppTheme.accent.opacity(0.25)) |
| 33 | .cornerRadius(16) | 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 | import SwiftUI | 2 | import SwiftUI |
| 2 | 3 | ||
| 3 | struct ChatStreamingMessageView: View { | 4 | struct ChatStreamingMessageView: View { |
| @@ -6,27 +7,67 @@ struct ChatStreamingMessageView: View { · 2 unmodified lines | |||
| 6 | let onShowActions: (AgentMeta) -> Void | 7 | let onShowActions: (AgentMeta) -> Void |
| 7 | 8 | ||
| 8 | var body: some View { | 9 | var body: some View { |
| 9 | let message = StoredMessage( | 10 | VStack(alignment: .leading, spacing: 10) { |
| 10 | id: "streaming", | 11 | // Streaming content |
| 11 | role: "assistant", | 12 | if !service.streamingContent.isEmpty { |
| 12 | content: service.streamingContent, | 13 | MarkdownText(content: service.streamingContent) |
| 13 | model: nil, | 14 | .foregroundColor(AppTheme.foreground) |
| 14 | toolCalls: nil | 15 | } |
| 15 | ) | 16 | |
| 16 | let meta = AgentMeta( | 17 | // Live thinking/tracing indicator |
| 17 | thinkingBlocks: service.streamingReasoning.isEmpty ? [] : [service.streamingReasoning], | 18 | if !service.streamingReasoning.isEmpty || !service.streamingToolCalls.isEmpty { |
| 18 | toolCalls: service.streamingToolCalls, | 19 | StreamingMetaIndicator( |
| 19 | toolResults: [] | 20 | reasoning: service.streamingReasoning, |
| 20 | ) | 21 | toolCalls: service.streamingToolCalls |
| 21 | ChatMessageRow( | 22 | ) |
| 22 | message: message, | 23 | } |
| 23 | isStreaming: true, | 24 | } |
| 24 | meta: meta, | 25 | .frame(maxWidth: .infinity, alignment: .leading) |
| 25 | onShowActions: { onShowActions($0) } | ||
| 26 | ) | ||
| 27 | .id("streaming") | 26 | .id("streaming") |
| 28 | .onChange(of: service.streamingContent) { _, _ in | 27 | .onChange(of: service.streamingContent) { _, _ in |
| 29 | scrollProxy.scrollTo("streaming", anchor: .bottom) | 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 | import SwiftUI | 2 | import SwiftUI |
| 2 | 3 | ||
| 3 | struct ChatUsageBar: View { | 4 | struct ChatUsageBar: View { |
| @@ -5,12 +6,12 @@ struct ChatUsageBar: View { · 1 unmodified line | |||
| 5 | 6 | ||
| 6 | var body: some View { | 7 | var body: some View { |
| 7 | if let usage { | 8 | if let usage { |
| 8 | HStack(spacing: 12) { | 9 | HStack(spacing: 8) { |
| 9 | UsagePill(label: "Prompt", value: usage.promptTokens) | 10 | UsagePill(label: "Input", value: usage.promptTokens, color: AppTheme.accentStrong) |
| 10 | UsagePill(label: "Completion", value: usage.completionTokens) | 11 | UsagePill(label: "Output", value: usage.completionTokens, color: AppTheme.success) |
| 11 | UsagePill(label: "Total", value: usage.totalTokens) | 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 | struct UsagePill: View { | 19 | struct UsagePill: View { |
| 19 | let label: String | 20 | let label: String |
| 20 | let value: Int | 21 | let value: Int |
| 22 | let color: Color | ||
| 21 | 23 | ||
| 22 | var body: some View { | 24 | var body: some View { |
| 23 | VStack(spacing: 2) { | 25 | HStack(spacing: 4) { |
| 24 | Text(label).font(AppTheme.captionFont).foregroundColor(AppTheme.muted) | 26 | Text(label) |
| 25 | Text("\(value)").font(AppTheme.monoFont) | 27 | .font(AppTheme.captionFont) |
| 28 | .foregroundColor(AppTheme.muted) | ||
| 29 | Text("\(value)") | ||
| 30 | .font(AppTheme.monoFont.weight(.medium)) | ||
| 31 | .foregroundColor(color) | ||
| 26 | } | 32 | } |
| 27 | .padding(.vertical, 6) | 33 | .padding(.vertical, 6) |
| 28 | .padding(.horizontal, 10) | 34 | .padding(.horizontal, 10) |
| 29 | .background(AppTheme.card) | 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 | } |