mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
feat(ios): Mac-style page backgrounds + Dashboard + Chat redesign
iOS now uses ScarfColor.backgroundPrimary throughout instead of the default iOS systemGroupedBackground. List-based screens add .scrollContentBackground(.hidden) + the rust background underneath; list rows use ScarfColor.backgroundSecondary as their card surface. Applied to: Projects, Memory, Cron, Settings, Skills/Installed, and the Servers root. Dashboard rewritten in Mac-style cards (no more native iOS list): - ScrollView + VStack with rust background - Activity stat grid (2-col LazyVGrid) with bordered rust-tinted cards: Sessions / Messages / Tool Calls / Tokens (with in/out sub- label). - Recent sessions card — bordered, ScarfColor.backgroundSecondary, inline session rows with 1px dividers, "See all" nav to Sessions sub-tab. - Error banner styled with ScarfColor.warning tinted card per Mac. - Sessions sub-tab keeps a List view but renders against rust background with ScarfColor.backgroundSecondary row backgrounds. Chat redesigned to match the Mac chat reference: - User bubble: rust accent fill with ScarfColor.onAccent text and uneven rounded corners (top/bottom-leading + top-trailing 14px; bottom-trailing 4px) — visually pinches to the sender side, same as Mac. - Assistant bubble: rust gradient sparkles avatar tile (24x24) alongside a ScarfColor.backgroundSecondary bordered card. - ToolCallCard: kind-tinted border + uppercase tracked label (READ/EDIT/EXECUTE/FETCH/BROWSER) using ScarfColor.success/info/ warning/Tool.web/Tool.search; expanded JSON in a bordered ScarfColor.backgroundSecondary panel. - ReasoningDisclosure: warning-tinted card with REASONING uppercase label. Both Mac (scarf) and iOS (scarf mobile) schemes build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1068,32 +1068,53 @@ private struct MessageBubble: View {
|
||||
if message.isUser {
|
||||
Text(message.content)
|
||||
.font(.body)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.foregroundStyle(.white)
|
||||
.background(Color.accentColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
.foregroundStyle(ScarfColor.onAccent)
|
||||
.background(
|
||||
UnevenRoundedRectangle(cornerRadii:
|
||||
.init(topLeading: 14, bottomLeading: 14, bottomTrailing: 4, topTrailing: 14))
|
||||
.fill(ScarfColor.accent)
|
||||
)
|
||||
.textSelection(.enabled)
|
||||
.contextMenu { messageContextMenu }
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(Array(ChatContentFormatter.segments(for: message.content).enumerated()), id: \.offset) { _, segment in
|
||||
switch segment {
|
||||
case .text(let body):
|
||||
Self.markdownText(body)
|
||||
.font(.body)
|
||||
.textSelection(.enabled)
|
||||
case .code(let lang, let body):
|
||||
CodeBlockView(language: lang, body: body)
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
// Assistant avatar — rust gradient sparkles tile,
|
||||
// matches the Mac side and the ScarfChatView reference.
|
||||
RoundedRectangle(cornerRadius: 7, style: .continuous)
|
||||
.fill(ScarfGradient.brand)
|
||||
.frame(width: 24, height: 24)
|
||||
.overlay(
|
||||
Image(systemName: "sparkles")
|
||||
.foregroundStyle(.white)
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(Array(ChatContentFormatter.segments(for: message.content).enumerated()), id: \.offset) { _, segment in
|
||||
switch segment {
|
||||
case .text(let body):
|
||||
Self.markdownText(body)
|
||||
.font(.body)
|
||||
.textSelection(.enabled)
|
||||
case .code(let lang, let body):
|
||||
CodeBlockView(language: lang, body: body)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
|
||||
.fill(ScarfColor.backgroundSecondary)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
|
||||
.strokeBorder(ScarfColor.border, lineWidth: 1)
|
||||
)
|
||||
.contextMenu { messageContextMenu }
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.foregroundStyle(Color.primary)
|
||||
.background(ScarfColor.backgroundSecondary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.contextMenu { messageContextMenu }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1206,64 +1227,118 @@ private struct ReasoningDisclosure: View {
|
||||
.textSelection(.enabled)
|
||||
.padding(.top, 4)
|
||||
} label: {
|
||||
Label("Thinking…", systemImage: "brain")
|
||||
.font(.caption)
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "brain")
|
||||
.font(.caption)
|
||||
Text("REASONING")
|
||||
.font(.caption2)
|
||||
.fontWeight(.semibold)
|
||||
.tracking(0.5)
|
||||
}
|
||||
.foregroundStyle(ScarfColor.warning)
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.fill(ScarfColor.warning.opacity(0.10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.strokeBorder(ScarfColor.warning.opacity(0.30), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Expanding card for a single `HermesToolCall` — shows function name
|
||||
/// + summary collapsed; full JSON arguments expanded.
|
||||
/// Expanding card for a single `HermesToolCall` — kind-tinted with
|
||||
/// uppercase tracked label, matches the Mac ToolCallCard treatment.
|
||||
private struct ToolCallCard: View {
|
||||
let call: HermesToolCall
|
||||
@State private var isExpanded = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.15)) { isExpanded.toggle() }
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: iconName)
|
||||
.foregroundStyle(.tint)
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: call.toolKind.icon)
|
||||
.foregroundStyle(toolColor)
|
||||
.font(.caption2)
|
||||
Text(toolLabel)
|
||||
.font(.caption2)
|
||||
.fontWeight(.semibold)
|
||||
.tracking(0.4)
|
||||
.foregroundStyle(toolColor)
|
||||
}
|
||||
Text(call.functionName)
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.primary)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
Text(call.argumentsSummary.prefix(60))
|
||||
.font(.caption)
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
.truncationMode(.middle)
|
||||
Spacer(minLength: 4)
|
||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
.foregroundStyle(ScarfColor.foregroundFaint)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.fill(toolColor.opacity(0.10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.strokeBorder(toolColor.opacity(0.30), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if isExpanded {
|
||||
Text(call.arguments)
|
||||
.font(.caption2.monospaced())
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
.textSelection(.enabled)
|
||||
.padding(.top, 2)
|
||||
.padding(8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.fill(ScarfColor.backgroundSecondary)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.strokeBorder(ScarfColor.border, lineWidth: 1)
|
||||
)
|
||||
)
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color(.tertiarySystemBackground))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(Color(.separator), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
|
||||
private var iconName: String {
|
||||
call.toolKind.icon
|
||||
private var toolLabel: String {
|
||||
switch call.toolKind {
|
||||
case .read: return "READ"
|
||||
case .edit: return "EDIT"
|
||||
case .execute: return "EXECUTE"
|
||||
case .fetch: return "FETCH"
|
||||
case .browser: return "BROWSER"
|
||||
case .other: return "TOOL"
|
||||
}
|
||||
}
|
||||
|
||||
private var toolColor: Color {
|
||||
switch call.toolKind {
|
||||
case .read: return ScarfColor.success
|
||||
case .edit: return ScarfColor.info
|
||||
case .execute: return ScarfColor.warning
|
||||
case .fetch: return ScarfColor.Tool.web
|
||||
case .browser: return ScarfColor.Tool.search
|
||||
case .other: return ScarfColor.foregroundMuted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ struct CronListView: View {
|
||||
if let err = vm.lastError {
|
||||
Section {
|
||||
Label(err, systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.foregroundStyle(ScarfColor.warning)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ struct CronListView: View {
|
||||
}
|
||||
}
|
||||
.scarfGoListDensity()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
.navigationTitle("Cron jobs")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
||||
@@ -3,10 +3,10 @@ import ScarfCore
|
||||
import ScarfIOS
|
||||
import ScarfDesign
|
||||
|
||||
/// iOS Dashboard — shows session count, token usage, cost, and the
|
||||
/// last 5 sessions pulled from the remote Hermes SQLite snapshot.
|
||||
/// Every data source routes through `ServerContext → CitadelServerTransport`
|
||||
/// so the same services that drive the Mac Dashboard power this one.
|
||||
/// iOS Dashboard — adopts the Mac-style card layout (status row +
|
||||
/// stats grid + recent-sessions card) instead of a native iOS list.
|
||||
/// Sessions sub-tab keeps a List view for scrolling density but
|
||||
/// renders against the rust page background.
|
||||
struct DashboardView: View {
|
||||
let config: IOSServerConfig
|
||||
let key: SSHKeyBundle
|
||||
@@ -16,17 +16,8 @@ struct DashboardView: View {
|
||||
@State private var selectedSection: Section = .overview
|
||||
@State private var sessionProjectFilter: String? = nil
|
||||
|
||||
/// Two top-level surfaces in the Dashboard. Overview = stats +
|
||||
/// 5 most-recent sessions for glance. Sessions = the 25-session
|
||||
/// deeper list with a project filter. Split added in pass-2 per
|
||||
/// user feedback — the old single-List layout grew too busy
|
||||
/// once we started adding project badges, and users wanted a
|
||||
/// way to slice by project.
|
||||
enum Section: Hashable { case overview, sessions }
|
||||
|
||||
/// Stable ID used when building the `ServerContext` — tied to the
|
||||
/// config's host+user tuple so re-launching the app without reset
|
||||
/// yields the same ID (important for the snapshot cache dir).
|
||||
private static let contextID: ServerID = ServerID(
|
||||
uuidString: "00000000-0000-0000-0000-0000000000A1"
|
||||
)!
|
||||
@@ -48,18 +39,18 @@ struct DashboardView: View {
|
||||
Text("Sessions").tag(Section.sessions)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
.padding(.horizontal, ScarfSpace.s4)
|
||||
.padding(.top, ScarfSpace.s2)
|
||||
.padding(.bottom, ScarfSpace.s1)
|
||||
|
||||
Group {
|
||||
switch selectedSection {
|
||||
case .overview: overviewList
|
||||
case .overview: overviewContent
|
||||
case .sessions: sessionsList
|
||||
}
|
||||
}
|
||||
}
|
||||
.scarfGoListDensity()
|
||||
.background(ScarfColor.backgroundPrimary.ignoresSafeArea())
|
||||
.navigationTitle(config.displayName)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.refreshable { await vm.refresh() }
|
||||
@@ -68,62 +59,159 @@ struct DashboardView: View {
|
||||
ProgressView("Loading dashboard…")
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
|
||||
}
|
||||
}
|
||||
.task { await vm.load() }
|
||||
}
|
||||
|
||||
// MARK: - Overview
|
||||
// MARK: - Overview (Mac-style cards)
|
||||
|
||||
@ViewBuilder
|
||||
private var overviewList: some View {
|
||||
List {
|
||||
if let err = vm.lastError {
|
||||
SwiftUI.Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Connection issue", systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(ScarfColor.warning)
|
||||
.font(.headline)
|
||||
Text(err)
|
||||
.font(.callout)
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
Button("Retry") {
|
||||
Task { await vm.refresh() }
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
private var overviewContent: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: ScarfSpace.s5) {
|
||||
if let err = vm.lastError {
|
||||
errorBanner(err)
|
||||
}
|
||||
|
||||
statsSection
|
||||
|
||||
if !vm.recentSessions.isEmpty {
|
||||
recentSessionsSection
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, ScarfSpace.s4)
|
||||
.padding(.vertical, ScarfSpace.s4)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
}
|
||||
|
||||
SwiftUI.Section("Activity") {
|
||||
statRow("Total sessions", value: "\(vm.stats.totalSessions)")
|
||||
statRow("Total messages", value: "\(vm.stats.totalMessages)")
|
||||
statRow("Tool calls", value: "\(vm.stats.totalToolCalls)")
|
||||
private func errorBanner(_ err: String) -> some View {
|
||||
HStack(alignment: .top, spacing: ScarfSpace.s2) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(ScarfColor.warning)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Connection issue")
|
||||
.font(.headline)
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
Text(err)
|
||||
.font(.callout)
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Button("Retry") {
|
||||
Task { await vm.refresh() }
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(ScarfSpace.s3)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.lg, style: .continuous)
|
||||
.fill(ScarfColor.warning.opacity(0.10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.lg, style: .continuous)
|
||||
.strokeBorder(ScarfColor.warning.opacity(0.30), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SwiftUI.Section("Tokens") {
|
||||
statRow("Input", value: formatTokens(vm.stats.totalInputTokens))
|
||||
statRow("Output", value: formatTokens(vm.stats.totalOutputTokens))
|
||||
statRow("Reasoning", value: formatTokens(vm.stats.totalReasoningTokens))
|
||||
private var statsSection: some View {
|
||||
VStack(alignment: .leading, spacing: ScarfSpace.s2) {
|
||||
Text("Activity")
|
||||
.font(.headline)
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
|
||||
LazyVGrid(
|
||||
columns: [GridItem(.flexible(), spacing: ScarfSpace.s3),
|
||||
GridItem(.flexible(), spacing: ScarfSpace.s3)],
|
||||
spacing: ScarfSpace.s3
|
||||
) {
|
||||
statCard(label: "Sessions", value: "\(vm.stats.totalSessions)")
|
||||
statCard(label: "Messages", value: "\(vm.stats.totalMessages)")
|
||||
statCard(label: "Tool Calls", value: "\(vm.stats.totalToolCalls)")
|
||||
statCard(
|
||||
label: "Tokens",
|
||||
value: formatTokens(vm.stats.totalInputTokens + vm.stats.totalOutputTokens),
|
||||
sub: tokenSub
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !vm.recentSessions.isEmpty {
|
||||
SwiftUI.Section {
|
||||
ForEach(vm.recentSessions) { session in
|
||||
sessionRow(session)
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Recent sessions")
|
||||
Spacer()
|
||||
Button("See all") { selectedSection = .sessions }
|
||||
.font(.caption)
|
||||
.textCase(nil)
|
||||
private var tokenSub: String? {
|
||||
let inT = vm.stats.totalInputTokens
|
||||
let outT = vm.stats.totalOutputTokens
|
||||
guard inT + outT > 0 else { return nil }
|
||||
return "\(formatTokens(inT)) in · \(formatTokens(outT)) out"
|
||||
}
|
||||
|
||||
private func statCard(label: String, value: String, sub: String? = nil) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
Text(value)
|
||||
.font(.system(size: 22, weight: .semibold, design: .monospaced))
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
if let sub {
|
||||
Text(sub)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(ScarfColor.foregroundFaint)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(ScarfSpace.s3)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.lg, style: .continuous)
|
||||
.fill(ScarfColor.backgroundSecondary)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.lg, style: .continuous)
|
||||
.strokeBorder(ScarfColor.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
private var recentSessionsSection: some View {
|
||||
VStack(alignment: .leading, spacing: ScarfSpace.s2) {
|
||||
HStack {
|
||||
Text("Recent sessions")
|
||||
.font(.headline)
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
Spacer()
|
||||
Button("See all") { selectedSection = .sessions }
|
||||
.font(.caption)
|
||||
.foregroundStyle(ScarfColor.accent)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(vm.recentSessions.enumerated()), id: \.element.id) { idx, session in
|
||||
sessionRow(session)
|
||||
.padding(.horizontal, ScarfSpace.s3)
|
||||
.padding(.vertical, ScarfSpace.s2 + 2)
|
||||
if idx < vm.recentSessions.count - 1 {
|
||||
Rectangle()
|
||||
.fill(ScarfColor.border)
|
||||
.frame(height: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.lg, style: .continuous)
|
||||
.fill(ScarfColor.backgroundSecondary)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: ScarfRadius.lg, style: .continuous)
|
||||
.strokeBorder(ScarfColor.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +222,8 @@ struct DashboardView: View {
|
||||
VStack(spacing: 0) {
|
||||
if !vm.allProjects.isEmpty {
|
||||
filterBar
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, ScarfSpace.s3)
|
||||
.padding(.vertical, ScarfSpace.s2)
|
||||
}
|
||||
|
||||
List {
|
||||
@@ -149,20 +237,20 @@ struct DashboardView: View {
|
||||
: "No sessions for that project yet. Try another filter or start a chat in that project.")
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
} else {
|
||||
ForEach(filtered) { session in
|
||||
sessionRow(session)
|
||||
.listRowBackground(ScarfColor.backgroundSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
/// Project filter control rendered above the Sessions list. Uses
|
||||
/// a Menu instead of a segmented Picker because there can be many
|
||||
/// projects — segments don't scale past 3–4 options on a phone.
|
||||
/// Shows the active filter as the button label (tappable to
|
||||
/// change); an explicit "All projects" entry clears the filter.
|
||||
@ViewBuilder
|
||||
private var filterBar: some View {
|
||||
HStack {
|
||||
@@ -191,16 +279,16 @@ struct DashboardView: View {
|
||||
.font(.caption2)
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tint)
|
||||
.foregroundStyle(ScarfColor.accent)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(.tint.opacity(0.1), in: Capsule())
|
||||
.background(ScarfColor.accentTint, in: Capsule())
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Row helpers
|
||||
// MARK: - Row helper
|
||||
|
||||
@ViewBuilder
|
||||
private func sessionRow(_ session: HermesSession) -> some View {
|
||||
@@ -211,7 +299,7 @@ struct DashboardView: View {
|
||||
Text(session.displayTitle)
|
||||
.font(.body)
|
||||
.lineLimit(2)
|
||||
.foregroundStyle(.primary)
|
||||
.foregroundStyle(ScarfColor.foregroundPrimary)
|
||||
HStack(spacing: 12) {
|
||||
Label(session.source, systemImage: session.sourceIcon)
|
||||
.font(.caption)
|
||||
@@ -230,11 +318,11 @@ struct DashboardView: View {
|
||||
if let projectName = vm.projectName(for: session) {
|
||||
Label(projectName, systemImage: "folder.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tint)
|
||||
.foregroundStyle(ScarfColor.accent)
|
||||
.labelStyle(.titleAndIcon)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 6)
|
||||
.background(.tint.opacity(0.12), in: Capsule())
|
||||
.background(ScarfColor.accentTint, in: Capsule())
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
@@ -244,18 +332,6 @@ struct DashboardView: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func statRow(_ label: String, value: String) -> some View {
|
||||
LabeledContent(label) {
|
||||
Text(value)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror of `ScarfCore.formatTokens` — inlined here rather than
|
||||
/// exported from ScarfCore because it's currently wrapped in
|
||||
/// `#if canImport(SQLite3)` (from the M0d InsightsViewModel move).
|
||||
private func formatTokens(_ count: Int) -> String {
|
||||
if count >= 1_000_000 {
|
||||
return String(format: "%.1fM", Double(count) / 1_000_000)
|
||||
|
||||
@@ -24,16 +24,22 @@ struct MemoryListView: View {
|
||||
Section {
|
||||
memoryRow(.memory, context: ctx)
|
||||
.scarfGoCompactListRow()
|
||||
.listRowBackground(ScarfColor.backgroundSecondary)
|
||||
memoryRow(.user, context: ctx)
|
||||
.scarfGoCompactListRow()
|
||||
.listRowBackground(ScarfColor.backgroundSecondary)
|
||||
memoryRow(.soul, context: ctx)
|
||||
.scarfGoCompactListRow()
|
||||
.listRowBackground(ScarfColor.backgroundSecondary)
|
||||
} footer: {
|
||||
Text("MEMORY.md and USER.md live under `~/.hermes/memories/`. SOUL.md lives at `~/.hermes/SOUL.md`.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(ScarfColor.foregroundMuted)
|
||||
}
|
||||
}
|
||||
.scarfGoListDensity()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
.navigationTitle("Memory")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
||||
@@ -67,6 +67,7 @@ struct ProjectsListView: View {
|
||||
Section {
|
||||
ForEach(topLevel) { project in
|
||||
projectRow(project)
|
||||
.listRowBackground(ScarfColor.backgroundSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,11 +75,14 @@ struct ProjectsListView: View {
|
||||
Section(folder) {
|
||||
ForEach(visibleProjects.filter { $0.folder == folder }) { project in
|
||||
projectRow(project)
|
||||
.listRowBackground(ScarfColor.backgroundSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scarfGoListDensity()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
}
|
||||
|
||||
private func projectRow(_ project: ProjectEntry) -> some View {
|
||||
|
||||
@@ -24,7 +24,7 @@ struct ServerListView: View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Something went wrong", systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.foregroundStyle(ScarfColor.warning)
|
||||
.font(.headline)
|
||||
Text(err)
|
||||
.font(.callout)
|
||||
@@ -60,6 +60,8 @@ struct ServerListView: View {
|
||||
}
|
||||
}
|
||||
.scarfGoListDensity()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
.navigationTitle("Servers")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
||||
@@ -29,7 +29,7 @@ struct SettingsView: View {
|
||||
if let err = vm.lastError {
|
||||
Section {
|
||||
Label(err, systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
.foregroundStyle(ScarfColor.warning)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.scarfGoListDensity()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
.navigationTitle("Settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.refreshable { await vm.load() }
|
||||
|
||||
@@ -45,11 +45,14 @@ struct InstalledSkillsListView: View {
|
||||
}
|
||||
}
|
||||
.scarfGoCompactListRow()
|
||||
.listRowBackground(ScarfColor.backgroundSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scarfGoListDensity()
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(ScarfColor.backgroundPrimary)
|
||||
.searchable(text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user