polish: tokenize remaining sheets, page headers, and widgets

Phase 1 — Page headers for the 9 non-mockup feature views: Skills,
Gateway, Platforms, Personalities, QuickCommands, CredentialPools,
Plugins, Webhooks, Profiles. Each now ships a ScarfPageHeader with
title + subtitle + tokenized trailing actions (ScarfPrimary /
Secondary / Ghost buttons), wrapped in .fixedSize so labels can't
wrap at narrow widths. Outer .background(ScarfColor.backgroundPrimary).

Phase 2 — Modal sheets: ModelPickerSheet, NousSignInSheet,
RenameProjectSheet, MoveToFolderSheet, the five Templates sheets
(TemplateInstall / TemplateConfig / TemplateExport / TemplateUninstall
/ ConfigEditorSheet), three MCPServer sheets (AddCustom / Editor /
PresetPicker), AddServerSheet, ManageServersView, MissingServerView.
.font(.headline) -> .scarfStyle(.headline);
.buttonStyle(.borderedProminent) -> ScarfPrimaryButton(); raw text
fields where touched -> ScarfTextField; cancel buttons -> ScarfGhostButton.

Phase 3 — All 12 platform setup views (Discord / Email / Feishu /
HomeAssistant / IMessage / Matrix / Mattermost / Signal / Slack /
Telegram / Webhook / WhatsApp). Connect buttons swapped to
ScarfPrimaryButton.

Phase 4 — All 7 project dashboard widgets (Chart / List / Progress /
Stat / Table / Text / Webview). .font(.caption) -> .scarfStyle(.caption);
.background(.quaternary.opacity(0.5)) -> ScarfColor.backgroundSecondary;
RoundedRectangle(cornerRadius: 8) -> ScarfRadius.lg.

Phase 5 — Project sub-views: ProjectSessionsView, ProjectsSidebar,
ProjectSlashCommandsView. Same token sweep.

Phase 6 — Common chrome:
- LoadingOverlay: .font(.callout/caption) -> .scarfStyle; secondary
  foreground -> ScarfColor.foregroundMuted; window-background ->
  ScarfColor.backgroundPrimary.
- ServerSwitcherToolbar: status dot + label tokenized.
- ConnectionStatusPill: status colors -> ScarfColor.success/warning/
  danger; error sheet header -> ScarfPrimaryButton retry.

Build green on both Mac (scarf) and iOS (scarf mobile) schemes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 14:39:13 +02:00
parent 41769e289c
commit 23dd8becb9
49 changed files with 373 additions and 265 deletions
@@ -1,4 +1,5 @@
import SwiftUI
import ScarfDesign
/// Translucent loading overlay used by feature views while their VM's
/// `load()` runs in the background. Shows a centered ProgressView with
@@ -27,15 +28,15 @@ struct LoadingOverlay: ViewModifier {
if isEmpty {
// Full cover: empty state. User has no data to look at,
// so own the whole pane with the spinner.
VStack(spacing: 12) {
VStack(spacing: ScarfSpace.s3) {
ProgressView()
.controlSize(.large)
Text(label)
.font(.callout)
.foregroundStyle(.secondary)
.scarfStyle(.callout)
.foregroundStyle(ScarfColor.foregroundMuted)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(NSColor.windowBackgroundColor))
.background(ScarfColor.backgroundPrimary)
} else {
// Stale-content refresh: top-trailing pill so the
// user sees data is being refreshed without losing
@@ -47,13 +48,13 @@ struct LoadingOverlay: ViewModifier {
ProgressView()
.controlSize(.small)
Text(label)
.font(.caption)
.foregroundStyle(.secondary)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
}
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(.thinMaterial, in: Capsule())
.padding(8)
.padding(ScarfSpace.s2)
}
Spacer()
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct CredentialPoolsView: View {
@State private var viewModel: CredentialPoolsViewModel
@@ -12,9 +13,10 @@ struct CredentialPoolsView: View {
var body: some View {
VStack(spacing: 0) {
header
ScrollView {
VStack(alignment: .leading, spacing: 16) {
header
safetyNotice
if viewModel.isLoading {
ProgressView().padding()
@@ -29,6 +31,8 @@ struct CredentialPoolsView: View {
.padding()
.frame(maxWidth: .infinity, alignment: .topLeading)
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Credential Pools")
.loadingOverlay(
viewModel.isLoading,
@@ -58,21 +62,26 @@ struct CredentialPoolsView: View {
}
private var header: some View {
HStack {
ScarfPageHeader(
"Credential Pools",
subtitle: "Shared OAuth + token pools rotated across runs."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message {
Label(msg, systemImage: "info.circle.fill")
.font(.caption)
.foregroundStyle(.secondary)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
}
Spacer()
Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button {
showAddSheet = true
} label: {
Label("Add Credential", systemImage: "plus")
}
.controlSize(.small)
Button("Reload") { viewModel.load() }
.controlSize(.small)
.buttonStyle(ScarfPrimaryButton())
}
.fixedSize(horizontal: true, vertical: false)
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct GatewayView: View {
@State private var viewModel: GatewayViewModel
@@ -11,6 +12,11 @@ struct GatewayView: View {
var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Messaging Gateway",
subtitle: "Outbound channel bridge — Discord, Telegram, Slack, etc."
)
ScrollView {
VStack(alignment: .leading, spacing: 24) {
serviceSection
@@ -20,6 +26,8 @@ struct GatewayView: View {
.padding()
.frame(maxWidth: .infinity, alignment: .topLeading)
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Messaging Gateway")
.onAppear { viewModel.load() }
.onChange(of: fileWatcher.lastChangeDate) { viewModel.load() }
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct MCPServerAddCustomView: View {
let viewModel: MCPServersViewModel
@@ -16,13 +17,13 @@ struct MCPServerAddCustomView: View {
VStack(spacing: 0) {
HStack {
Text("Add Custom MCP Server")
.font(.headline)
.scarfStyle(.headline)
Spacer()
Button("Cancel") { dismiss() }
Button("Add") {
submit()
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.disabled(!canSubmit)
}
.padding()
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct MCPServerEditorView: View {
@State var viewModel: MCPServerEditorViewModel
@@ -11,7 +12,7 @@ struct MCPServerEditorView: View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Edit \(viewModel.server.name)")
.font(.headline)
.scarfStyle(.headline)
Text(viewModel.server.transport.displayName)
.font(.caption)
.foregroundStyle(.secondary)
@@ -30,7 +31,7 @@ struct MCPServerEditorView: View {
Text("Save")
}
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction)
.disabled(viewModel.isSaving)
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct MCPServerPresetPickerView: View {
let viewModel: MCPServersViewModel
@@ -35,7 +36,7 @@ struct MCPServerPresetPickerView: View {
}
VStack(alignment: .leading, spacing: 2) {
(selectedPreset.map { Text(verbatim: $0.displayName) } ?? Text("Add from Preset"))
.font(.headline)
.scarfStyle(.headline)
(selectedPreset.map { Text(verbatim: $0.description) } ?? Text("Pick an MCP server to add."))
.font(.caption)
.foregroundStyle(.secondary)
@@ -132,7 +133,7 @@ struct MCPServerPresetPickerView: View {
Button("Add Server") {
submit(preset: preset)
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.disabled(!canSubmit(preset: preset))
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct PersonalitiesView: View {
@State private var viewModel: PersonalitiesViewModel
@@ -12,9 +13,26 @@ struct PersonalitiesView: View {
var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Personalities",
subtitle: "Per-personality model + prompt overrides defined in config.yaml."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message {
Label(msg, systemImage: "checkmark.circle.fill")
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.success)
}
Button("Edit config.yaml") { viewModel.openConfigInEditor() }
.buttonStyle(ScarfGhostButton())
Button("Reload") { viewModel.load(); soulDraft = viewModel.soulMarkdown }
.buttonStyle(ScarfSecondaryButton())
}
.fixedSize(horizontal: true, vertical: false)
}
ScrollView {
VStack(alignment: .leading, spacing: 20) {
header
activeSection
listSection
soulSection
@@ -22,6 +40,8 @@ struct PersonalitiesView: View {
.padding()
.frame(maxWidth: .infinity, alignment: .topLeading)
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Personalities")
.onAppear {
viewModel.load()
@@ -29,21 +49,6 @@ struct PersonalitiesView: View {
}
}
private var header: some View {
HStack {
if let msg = viewModel.message {
Label(msg, systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundStyle(.green)
}
Spacer()
Button("Edit config.yaml") { viewModel.openConfigInEditor() }
.controlSize(.small)
Button("Reload") { viewModel.load(); soulDraft = viewModel.soulMarkdown }
.controlSize(.small)
}
}
private var activeSection: some View {
SettingsSection(title: "Active Personality", icon: "theatermasks.fill") {
if viewModel.personalities.isEmpty {
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct DiscordSetupView: View {
@State private var viewModel: DiscordSetupViewModel
@@ -57,7 +58,7 @@ struct DiscordSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct EmailSetupView: View {
@State private var viewModel: EmailSetupViewModel
@@ -75,7 +76,7 @@ struct EmailSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct FeishuSetupView: View {
@State private var viewModel: FeishuSetupViewModel
@@ -52,7 +53,7 @@ struct FeishuSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct HomeAssistantSetupView: View {
@State private var viewModel: HomeAssistantSetupViewModel
@@ -72,7 +73,7 @@ struct HomeAssistantSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct IMessageSetupView: View {
@State private var viewModel: IMessageSetupViewModel
@@ -61,7 +62,7 @@ struct IMessageSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct MatrixSetupView: View {
@State private var viewModel: MatrixSetupViewModel
@@ -69,7 +70,7 @@ struct MatrixSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct MattermostSetupView: View {
@State private var viewModel: MattermostSetupViewModel
@@ -52,7 +53,7 @@ struct MattermostSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct SignalSetupView: View {
@State private var viewModel: SignalSetupViewModel
@@ -73,7 +74,7 @@ struct SignalSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
@@ -87,7 +88,7 @@ struct SignalSetupView: View {
case .none:
Button("Link Device") { viewModel.startLink() }.controlSize(.small)
.disabled(!viewModel.signalCLIInstalled)
Button("Start Daemon") { viewModel.startDaemon() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Start Daemon") { viewModel.startDaemon() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
.disabled(!viewModel.signalCLIInstalled || viewModel.account.isEmpty)
case .link:
Text("Linking…").font(.caption).foregroundStyle(.secondary)
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct SlackSetupView: View {
@State private var viewModel: SlackSetupViewModel
@@ -56,7 +57,7 @@ struct SlackSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct TelegramSetupView: View {
@State private var viewModel: TelegramSetupViewModel
@@ -57,7 +58,7 @@ struct TelegramSetupView: View {
Button("Reload") { viewModel.load() }
.controlSize(.small)
Button("Save") { viewModel.save() }
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.controlSize(.small)
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct WebhookSetupView: View {
@State private var viewModel: WebhookSetupViewModel
@@ -53,7 +54,7 @@ struct WebhookSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct WhatsAppSetupView: View {
@State private var viewModel: WhatsAppSetupViewModel
@@ -56,7 +57,7 @@ struct WhatsAppSetupView: View {
}
Spacer()
Button("Reload") { viewModel.load() }.controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(.borderedProminent).controlSize(.small)
Button("Save") { viewModel.save() }.buttonStyle(ScarfPrimaryButton()).controlSize(.small)
}
}
@@ -71,7 +72,7 @@ struct WhatsAppSetupView: View {
.controlSize(.small)
} else {
Button("Start Pairing") { viewModel.startPairing() }
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.controlSize(.small)
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct PlatformsView: View {
@State private var viewModel: PlatformsViewModel
@@ -13,12 +14,19 @@ struct PlatformsView: View {
// HSplitView (not nested NavigationSplitView) because ContentView already
// hosts the outer NavigationSplitView nesting them breaks layout on macOS.
var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Platforms",
subtitle: "Inbound channels the agent listens on. Set up tokens per platform."
)
HSplitView {
platformList
.frame(minWidth: 220, idealWidth: 240, maxWidth: 300)
detail
.frame(minWidth: 480)
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Platforms")
.onAppear { viewModel.load() }
// Re-read config.yaml / .env / gateway_state.json when any of them
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct PluginsView: View {
@State private var viewModel: PluginsViewModel
@@ -15,7 +16,6 @@ struct PluginsView: View {
var body: some View {
VStack(spacing: 0) {
header
Divider()
if viewModel.isLoading && viewModel.plugins.isEmpty {
ProgressView().padding()
} else if viewModel.plugins.isEmpty {
@@ -24,6 +24,7 @@ struct PluginsView: View {
list
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Plugins")
.loadingOverlay(
viewModel.isLoading,
@@ -45,25 +46,28 @@ struct PluginsView: View {
}
private var header: some View {
HStack {
ScarfPageHeader(
"Plugins",
subtitle: "Hermes plugins discovered from `~/.hermes/plugins/`."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message {
Label(msg, systemImage: "info.circle.fill")
.font(.caption)
.foregroundStyle(.green)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.success)
}
Spacer()
Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button {
installIdentifier = ""
showInstall = true
} label: {
Label("Install", systemImage: "plus")
}
.controlSize(.small)
Button("Reload") { viewModel.load() }
.controlSize(.small)
.buttonStyle(ScarfPrimaryButton())
}
.fixedSize(horizontal: true, vertical: false)
}
.padding(.horizontal)
.padding(.vertical, 8)
}
private var emptyState: some View {
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
import AppKit
import UniformTypeIdentifiers
@@ -21,12 +22,19 @@ struct ProfilesView: View {
@State private var pendingDelete: HermesProfile?
var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Profiles",
subtitle: "Named config bundles you can swap between."
)
HSplitView {
listSection
.frame(minWidth: 260, idealWidth: 300)
detailSection
.frame(minWidth: 400)
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Profiles")
.onAppear { viewModel.load() }
.sheet(isPresented: $showCreate) { createSheet }
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Sheet for assigning a project to a folder in the sidebar. Folders
/// are implicit they exist because at least one project references
@@ -55,11 +56,13 @@ struct MoveToFolderSheet: View {
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Move \"\(project.name)\" to folder").font(.headline)
VStack(alignment: .leading, spacing: ScarfSpace.s3) {
Text("Move \"\(project.name)\" to folder")
.scarfStyle(.headline)
.foregroundStyle(ScarfColor.foregroundPrimary)
Text("Folders only affect how projects are grouped in Scarf's sidebar. Nothing on disk changes.")
.font(.caption)
.foregroundStyle(.secondary)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
.fixedSize(horizontal: false, vertical: true)
Picker("Destination", selection: $mode) {
@@ -77,8 +80,7 @@ struct MoveToFolderSheet: View {
.pickerStyle(.inline)
if case .new = mode {
TextField("New folder name", text: $newFolderName)
.textFieldStyle(.roundedBorder)
ScarfTextField("New folder name", text: $newFolderName)
.onSubmit {
if canMove { commit() }
}
@@ -86,15 +88,16 @@ struct MoveToFolderSheet: View {
HStack {
Button("Cancel") { dismiss() }
.buttonStyle(ScarfGhostButton())
.keyboardShortcut(.cancelAction)
Spacer()
Button("Move") { commit() }
.buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.disabled(!canMove)
}
}
.padding()
.padding(ScarfSpace.s5)
.frame(minWidth: 420, minHeight: 320)
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Per-project Sessions tab (v2.3). Lives beside the Dashboard and
/// Site tabs in the project view; populated from the session
@@ -59,9 +60,9 @@ struct ProjectSessionsView: View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text("Sessions in this project")
.font(.headline)
.scarfStyle(.headline)
Text("Chats you start here get attributed automatically. Older CLI-started sessions live in the global Sessions sidebar.")
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -75,7 +76,7 @@ struct ProjectSessionsView: View {
} label: {
Label("New Chat", systemImage: "message.badge.filled.fill")
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
}
.padding()
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// The "Slash Commands" tab on the per-project surface. Lists the
/// project-scoped commands stored at `<project>/.scarf/slash-commands/`
@@ -52,9 +53,9 @@ struct ProjectSlashCommandsView: View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Slash Commands")
.font(.headline)
.scarfStyle(.headline)
Text("`/<name>` shortcuts that expand into prompt templates. Stored at `<project>/.scarf/slash-commands/` so they ship with `.scarftemplate` bundles.")
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
@@ -83,7 +84,7 @@ struct ProjectSlashCommandsView: View {
Text("Add reusable prompt templates here. Each command shows up in the chat slash menu when you're chatting in this project.")
} actions: {
Button("Add Command") { viewModel.beginNew() }
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
@@ -117,7 +118,7 @@ struct ProjectSlashCommandsView: View {
Text("Couldn't update slash commands")
.font(.subheadline.weight(.semibold))
Text(message)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
}
Spacer()
@@ -165,7 +166,7 @@ private struct CommandRow: View {
}
}
Text(command.description)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
if let tags = command.tags, !tags.isEmpty {
@@ -208,7 +209,7 @@ struct SlashCommandEditorSheet: View {
Button("Save") {
Task { await viewModel.saveDraft() }
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction)
.disabled(saveDisabled)
}
@@ -244,7 +245,7 @@ struct SlashCommandEditorSheet: View {
.help("Lowercase letters, digits, and hyphens. Must start with a letter.")
if let nameError = nameValidationMessage {
Text(nameError)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.orange)
}
TextField("Description", text: Binding(
@@ -277,7 +278,7 @@ struct SlashCommandEditorSheet: View {
Section("Prompt template") {
Text("Use `{{argument}}` to substitute the user's input. `{{argument | default: \"\"}}` provides a fallback when the user invokes the command without arguments.")
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
TextEditor(text: Binding(
get: { viewModel.draft?.body ?? "" },
@@ -305,12 +306,12 @@ struct SlashCommandEditorSheet: View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Preview")
.font(.headline)
.scarfStyle(.headline)
Spacer()
}
HStack {
Text("Sample argument")
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
TextField("(empty)", text: $sampleArgument)
.textFieldStyle(.roundedBorder)
@@ -324,7 +325,7 @@ struct SlashCommandEditorSheet: View {
.textSelection(.enabled)
}
Text("This is the prompt Hermes will receive. The user sees the literal `/\(viewModel.draft?.name ?? "name")` they typed in their own bubble; the expanded body goes to the agent with a `<!-- scarf-slash:<name> -->` marker.")
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Sidebar view for the Projects feature. Renders the registry as:
/// - A search field at the top (F focus).
@@ -68,18 +69,18 @@ struct ProjectsSidebar: View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
.font(.caption)
.scarfStyle(.caption)
TextField("Filter projects", text: $filterText)
.textFieldStyle(.plain)
.focused($searchFocused)
.font(.caption)
.scarfStyle(.caption)
if !filterText.isEmpty {
Button {
filterText = ""
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.tertiary)
.font(.caption)
.scarfStyle(.caption)
}
.buttonStyle(.borderless)
}
@@ -122,7 +123,7 @@ struct ProjectsSidebar: View {
}
} label: {
Label(folder, systemImage: "folder")
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
}
}
@@ -137,7 +138,7 @@ struct ProjectsSidebar: View {
}
} label: {
Label("Archived (\(archivedVisible.count))", systemImage: "archivebox")
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
}
}
@@ -208,7 +209,7 @@ struct ProjectsSidebar: View {
Toggle(isOn: $showArchived) {
Image(systemName: showArchived ? "archivebox.fill" : "archivebox")
.font(.caption)
.scarfStyle(.caption)
}
.toggleStyle(.button)
.buttonStyle(.borderless)
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Sheet for renaming a project in the registry. Preserves the
/// project's `path`, `folder`, and `archived` fields the rename
@@ -45,15 +46,16 @@ struct RenameProjectSheet: View {
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Rename project").font(.headline)
VStack(alignment: .leading, spacing: ScarfSpace.s3) {
Text("Rename project")
.scarfStyle(.headline)
.foregroundStyle(ScarfColor.foregroundPrimary)
Text("The project directory on disk isn't changed — only the label Scarf shows in the sidebar.")
.font(.caption)
.foregroundStyle(.secondary)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
.fixedSize(horizontal: false, vertical: true)
TextField("Project name", text: $newName)
.textFieldStyle(.roundedBorder)
ScarfTextField("Project name", text: $newName)
.onSubmit {
if validation.isValid {
save()
@@ -62,21 +64,22 @@ struct RenameProjectSheet: View {
if let message = validation.message {
Label(message, systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.red)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.danger)
}
HStack {
Button("Cancel") { dismiss() }
.buttonStyle(ScarfGhostButton())
.keyboardShortcut(.cancelAction)
Spacer()
Button("Save") { save() }
.buttonStyle(ScarfPrimaryButton())
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.disabled(!validation.isValid)
}
}
.padding()
.padding(ScarfSpace.s5)
.frame(minWidth: 420)
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
import Charts
// Flattened data point for Charts to avoid complex nested generic inference
@@ -27,15 +28,15 @@ struct ChartWidgetView: View {
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(widget.title)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
chartContent
.frame(height: 150)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
}
@ViewBuilder
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct ListWidgetView: View {
let widget: DashboardWidget
@@ -10,10 +11,10 @@ struct ListWidgetView: View {
if let icon = widget.icon {
Image(systemName: icon)
.foregroundStyle(.secondary)
.font(.caption)
.scarfStyle(.caption)
}
Text(widget.title)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
}
if let items = widget.items {
@@ -32,8 +33,8 @@ struct ListWidgetView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
}
private func statusIcon(_ status: String?) -> String {
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct ProgressWidgetView: View {
let widget: DashboardWidget
@@ -14,7 +15,7 @@ struct ProgressWidgetView: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(widget.title)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
ProgressView(value: progressValue) {
if let label = widget.label {
@@ -27,7 +28,7 @@ struct ProgressWidgetView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct StatWidgetView: View {
let widget: DashboardWidget
@@ -14,10 +15,10 @@ struct StatWidgetView: View {
if let icon = widget.icon {
Image(systemName: icon)
.foregroundStyle(widgetColor)
.font(.caption)
.scarfStyle(.caption)
}
Text(widget.title)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
}
if let value = widget.value {
@@ -32,7 +33,7 @@ struct StatWidgetView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct TableWidgetView: View {
let widget: DashboardWidget
@@ -7,7 +8,7 @@ struct TableWidgetView: View {
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(widget.title)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
if let columns = widget.columns, let rows = widget.rows {
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 4) {
@@ -32,7 +33,7 @@ struct TableWidgetView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct TextWidgetView: View {
let widget: DashboardWidget
@@ -7,7 +8,7 @@ struct TextWidgetView: View {
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(widget.title)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
if let content = widget.content {
if widget.format == "markdown" {
@@ -20,7 +21,7 @@ struct TextWidgetView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
import WebKit
struct WebviewWidgetView: View {
@@ -29,7 +30,7 @@ struct WebviewWidgetView: View {
VStack(spacing: 0) {
if let url = webURL {
WebViewRepresentable(url: url)
.clipShape(RoundedRectangle(cornerRadius: 8))
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
} else {
ContentUnavailableView {
Label("Invalid URL", systemImage: "globe")
@@ -49,10 +50,10 @@ struct WebviewWidgetView: View {
if let icon = widget.icon {
Image(systemName: icon)
.foregroundStyle(.secondary)
.font(.caption)
.scarfStyle(.caption)
}
Text(widget.title)
.font(.caption)
.scarfStyle(.caption)
.foregroundStyle(.secondary)
Spacer()
if let urlString = widget.url {
@@ -76,8 +77,8 @@ struct WebviewWidgetView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.background(ScarfColor.backgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: ScarfRadius.lg))
}
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct QuickCommandsView: View {
@State private var viewModel: QuickCommandsViewModel
@@ -12,9 +13,10 @@ struct QuickCommandsView: View {
var body: some View {
VStack(spacing: 0) {
header
ScrollView {
VStack(alignment: .leading, spacing: 16) {
header
intro
if viewModel.commands.isEmpty {
emptyState
@@ -25,6 +27,8 @@ struct QuickCommandsView: View {
.padding()
.frame(maxWidth: .infinity, alignment: .topLeading)
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Quick Commands")
.onAppear { viewModel.load() }
.sheet(isPresented: $showAddSheet) {
@@ -46,28 +50,33 @@ struct QuickCommandsView: View {
}
private var header: some View {
HStack {
ScarfPageHeader(
"Quick Commands",
subtitle: "Shell shortcuts hermes exposes in chat as `/command_name`."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message {
Label(msg, systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundStyle(.green)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.success)
}
Spacer()
Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button {
showAddSheet = true
} label: {
Label("Add Command", systemImage: "plus")
}
.controlSize(.small)
Button("Reload") { viewModel.load() }
.controlSize(.small)
.buttonStyle(ScarfPrimaryButton())
}
.fixedSize(horizontal: true, vertical: false)
}
}
private var intro: some View {
Text("Quick commands are shell shortcuts hermes exposes in chat as `/command_name`. They live under `quick_commands:` in config.yaml.")
.font(.caption)
.foregroundStyle(.secondary)
Text("Stored under `quick_commands:` in config.yaml.")
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
}
private var emptyState: some View {
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Sheet for adding a new remote server. Collects SSH connection details,
/// runs a "Test Connection" probe, and on save hands the persisted
@@ -36,7 +37,7 @@ struct AddServerSheet: View {
Image(systemName: "server.rack")
.font(.title2)
Text("Add Remote Server")
.font(.headline)
.scarfStyle(.headline)
Spacer()
}
.padding(.horizontal, 20)
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Small colored pill shown in the toolbar reflecting the server's reach-
/// ability. Green = connected, yellow = probing, red = unreachable.
@@ -33,8 +34,8 @@ struct ConnectionStatusPill: View {
.foregroundStyle(color)
.symbolRenderingMode(.hierarchical)
labelText
.font(.caption)
.foregroundStyle(.secondary)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
.lineLimit(1)
}
.padding(.horizontal, 4)
@@ -51,10 +52,10 @@ struct ConnectionStatusPill: View {
private var color: Color {
switch status.status {
case .connected: return .green
case .degraded: return .orange
case .idle: return .yellow
case .error: return .red
case .connected: return ScarfColor.success
case .degraded: return ScarfColor.warning
case .idle: return ScarfColor.warning.opacity(0.7)
case .error: return ScarfColor.danger
}
}
@@ -101,13 +102,14 @@ struct ConnectionStatusPill: View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Label(message, systemImage: "xmark.octagon.fill")
.foregroundStyle(.red)
.font(.headline)
.foregroundStyle(ScarfColor.danger)
.scarfStyle(.headline)
Spacer()
Button("Retry") {
status.retry()
showDetails = false
}
.buttonStyle(ScarfPrimaryButton())
}
Divider()
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// List of registered remote servers with add/remove actions. Rendered as a
/// popover from the toolbar switcher.
@@ -59,7 +60,7 @@ struct ManageServersView: View {
private var header: some View {
HStack {
Text("Servers").font(.headline)
Text("Servers").scarfStyle(.headline)
Spacer()
Button {
showAddSheet = true
@@ -76,7 +77,7 @@ struct ManageServersView: View {
Image(systemName: "server.rack")
.font(.system(size: 28))
.foregroundStyle(.secondary)
Text("No remote servers").font(.headline)
Text("No remote servers").scarfStyle(.headline)
Text("Click Add to connect to a remote Hermes installation over SSH.")
.font(.caption)
.foregroundStyle(.secondary)
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Shown when a window is restored after the user removed the server it
/// was bound to. Lets them open Local or any remaining registered server
@@ -33,7 +34,7 @@ struct MissingServerView: View {
openWindow(value: ServerContext.local.id)
dismissWindow()
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
if !registry.entries.isEmpty {
Menu {
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Toolbar control that shows the current window's server and exposes a
/// menu for opening *other* servers in additional windows. Multi-window is
@@ -36,13 +37,15 @@ struct ServerSwitcherToolbar: View {
} label: {
HStack(spacing: 6) {
Circle()
.fill(current.isRemote ? Color.blue : Color.green)
.fill(current.isRemote ? ScarfColor.info : ScarfColor.success)
.frame(width: 8, height: 8)
Text(verbatim: current.displayName)
.font(.callout)
.scarfStyle(.callout)
.foregroundStyle(ScarfColor.foregroundPrimary)
.lineLimit(1)
Image(systemName: "chevron.down")
.font(.caption2)
.font(.system(size: 10))
.foregroundStyle(ScarfColor.foregroundMuted)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
/// Two-column model browser sheet. Left column lists providers, right column
/// lists models for the selected provider. Supports filtering and a "Custom"
@@ -109,7 +110,7 @@ struct ModelPickerSheet: View {
HStack(spacing: 8) {
Image(systemName: "cpu")
Text("Select Model")
.font(.headline)
.scarfStyle(.headline)
Spacer()
if !customMode {
TextField("Search…", text: $searchText)
@@ -294,7 +295,7 @@ struct ModelPickerSheet: View {
} label: {
Label("Sign in to Nous Portal", systemImage: "person.badge.key.fill")
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.controlSize(.regular)
}
}
@@ -361,7 +362,7 @@ struct ModelPickerSheet: View {
Spacer()
Button("Cancel") { onCancel() }
Button("Select") { submitSelection() }
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.disabled(!canSubmit)
}
.padding()
@@ -1,5 +1,6 @@
import SwiftUI
import AppKit
import ScarfDesign
/// In-app sign-in sheet for Nous Portal hosts a ``NousAuthFlow`` and
/// renders one of four sub-views keyed on `flow.state`. Reached from the
@@ -69,7 +70,7 @@ struct NousSignInSheet: View {
Image(systemName: "person.badge.key.fill")
.foregroundStyle(.tint)
Text("Sign in to Nous Portal")
.font(.headline)
.scarfStyle(.headline)
Spacer()
if case .waitingForApproval = flowState {
Button("Cancel") { dismiss() }
@@ -123,7 +124,7 @@ struct NousSignInSheet: View {
VStack(alignment: .leading, spacing: 14) {
VStack(alignment: .leading, spacing: 4) {
Text("Approve in your browser")
.font(.headline)
.scarfStyle(.headline)
Text("We opened the Nous Portal approval page. Confirm this code matches what it shows, then approve.")
.font(.callout)
.foregroundStyle(.secondary)
@@ -176,7 +177,7 @@ struct NousSignInSheet: View {
.foregroundStyle(.green)
.font(.system(size: 48))
Text("Signed in to Nous Portal")
.font(.headline)
.scarfStyle(.headline)
Text("Your tools will now route through your subscription.")
.font(.callout)
.foregroundStyle(.secondary)
@@ -193,7 +194,7 @@ struct NousSignInSheet: View {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
Text(billingURL == nil ? "Sign-in didn't complete" : "Subscription required")
.font(.headline)
.scarfStyle(.headline)
}
Text(reason)
@@ -209,7 +210,7 @@ struct NousSignInSheet: View {
Label("Subscribe", systemImage: "creditcard")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.controlSize(.large)
}
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
struct SkillsView: View {
@State private var viewModel: SkillsViewModel
@@ -38,6 +39,10 @@ struct SkillsView: View {
var body: some View {
VStack(spacing: 0) {
ScarfPageHeader(
"Skills",
subtitle: "Pre-packaged prompt collections the agent can call into. \(viewModel.totalSkillCount) installed."
)
modePicker
// v2.5 "What's New" pill only renders when the diff has
// changes against a non-empty prior snapshot (first launch
@@ -55,7 +60,8 @@ struct SkillsView: View {
case .updates: updatesContent
}
}
.navigationTitle("Skills (\(viewModel.totalSkillCount))")
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Skills")
// SkillsViewModel.load() is async after the v2.5 ScarfCore
// promotion. Wrap in a Task here so the existing onAppear
// contract (fire-and-forget) keeps working without making
@@ -1,4 +1,5 @@
import ScarfCore
import ScarfDesign
import SwiftUI
/// Post-install configuration editor. Thin wrapper around the same
@@ -73,7 +74,7 @@ struct ConfigEditorSheet: View {
Text("Configuration saved").font(.title2.bold())
Button("Done") { dismiss() }
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(minWidth: 560, minHeight: 280)
@@ -1,4 +1,5 @@
import ScarfCore
import ScarfDesign
import SwiftUI
/// The configure form rendered for template install + post-install
@@ -106,7 +107,7 @@ struct TemplateConfigSheet: View {
// handles dismissal via the success view's Done button.
}
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
}
.padding(16)
}
@@ -117,10 +118,10 @@ struct TemplateConfigSheet: View {
private func fieldRow(_ field: TemplateConfigField) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(field.label).font(.headline)
Text(field.label).scarfStyle(.headline)
if field.required {
Text("*")
.font(.headline)
.scarfStyle(.headline)
.foregroundStyle(.red)
}
Spacer()
@@ -2,6 +2,7 @@ import SwiftUI
import AppKit
import UniformTypeIdentifiers
import ScarfCore
import ScarfDesign
/// Author-facing sheet for exporting an existing project as a
/// `.scarftemplate`. Mirrors the profile-export flow: fill in a few fields,
@@ -89,7 +90,7 @@ struct TemplateExportSheet: View {
Spacer()
Button("Export…") { runExport() }
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.disabled(!canExport)
}
.padding(.top, 8)
@@ -97,7 +98,7 @@ struct TemplateExportSheet: View {
private var metadataGroup: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Metadata").font(.headline)
Text("Metadata").scarfStyle(.headline)
LabeledContent("Template ID") {
TextField("owner/name", text: $viewModel.templateId)
.textFieldStyle(.roundedBorder)
@@ -137,7 +138,7 @@ struct TemplateExportSheet: View {
private var requiredFilesGroup: some View {
let plan = viewModel.previewPlan()
return VStack(alignment: .leading, spacing: 6) {
Text("Required Files").font(.headline)
Text("Required Files").scarfStyle(.headline)
check(label: "dashboard.json (\(plan.projectDir)/.scarf/dashboard.json)", ok: plan.dashboardPresent)
check(label: "README.md (\(plan.projectDir)/README.md)", ok: plan.readmePresent)
check(label: "AGENTS.md (\(plan.projectDir)/AGENTS.md)", ok: plan.agentsMdPresent)
@@ -147,7 +148,7 @@ struct TemplateExportSheet: View {
private var instructionsGroup: some View {
let plan = viewModel.previewPlan()
return VStack(alignment: .leading, spacing: 4) {
Text("Agent-specific instructions (optional)").font(.headline)
Text("Agent-specific instructions (optional)").scarfStyle(.headline)
if plan.instructionFiles.isEmpty {
Text("No per-agent instruction files found in the project root.")
.font(.caption)
@@ -163,7 +164,7 @@ struct TemplateExportSheet: View {
private var skillsGroup: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Include Skills").font(.headline)
Text("Include Skills").scarfStyle(.headline)
if viewModel.availableSkills.isEmpty {
Text("No skills found.")
.font(.caption)
@@ -186,7 +187,7 @@ struct TemplateExportSheet: View {
private var cronGroup: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Include Cron Jobs").font(.headline)
Text("Include Cron Jobs").scarfStyle(.headline)
if viewModel.availableCronJobs.isEmpty {
Text("No cron jobs found.")
.font(.caption)
@@ -214,7 +215,7 @@ struct TemplateExportSheet: View {
private var memoryGroup: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Memory Appendix (optional)").font(.headline)
Text("Memory Appendix (optional)").scarfStyle(.headline)
Text("Markdown that will be appended to the installer's MEMORY.md, wrapped in template-specific markers so it can be removed cleanly later.")
.font(.caption)
.foregroundStyle(.secondary)
@@ -1,5 +1,6 @@
import AppKit
import ScarfCore
import ScarfDesign
import SwiftUI
/// Preview-and-confirm sheet for installing a `.scarftemplate`. Honest
@@ -47,7 +48,7 @@ struct TemplateInstallSheet: View {
private var idleView: some View {
VStack(spacing: 16) {
Text("No template loaded.")
.font(.headline)
.scarfStyle(.headline)
Button("Close") { dismiss() }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -70,7 +71,7 @@ struct TemplateInstallSheet: View {
Divider()
}
Text("Where should this project live?")
.font(.headline)
.scarfStyle(.headline)
Text("Scarf will create a new folder inside the directory you pick, named after the template id.")
.font(.subheadline)
.foregroundStyle(.secondary)
@@ -169,7 +170,7 @@ struct TemplateInstallSheet: View {
.foregroundStyle(.secondary)
Button("Install") { viewModel.confirmInstall() }
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
}
.padding(.top, 8)
}
@@ -353,7 +354,7 @@ struct TemplateInstallSheet: View {
@ViewBuilder
private func section<Content: View>(title: String, subtitle: String?, @ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline)
Text(title).scarfStyle(.headline)
if let subtitle {
Text(subtitle)
.font(.caption.monospaced())
@@ -391,7 +392,7 @@ struct TemplateInstallSheet: View {
dismiss()
}
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@@ -1,4 +1,5 @@
import ScarfCore
import ScarfDesign
import SwiftUI
/// Preview-and-confirm sheet for uninstalling a template-installed
@@ -41,7 +42,7 @@ struct TemplateUninstallSheet: View {
private var idleView: some View {
VStack(spacing: 16) {
Text("No template loaded.")
.font(.headline)
.scarfStyle(.headline)
Button("Close") { dismiss() }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -87,7 +88,7 @@ struct TemplateUninstallSheet: View {
.foregroundStyle(.secondary)
Button("Remove") { viewModel.confirmUninstall() }
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
.tint(.red)
}
.padding(.top, 8)
@@ -244,7 +245,7 @@ struct TemplateUninstallSheet: View {
@ViewBuilder content: () -> Content
) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline)
Text(title).scarfStyle(.headline)
if let subtitle {
Text(subtitle)
.font(.caption.monospaced())
@@ -296,7 +297,7 @@ struct TemplateUninstallSheet: View {
dismiss()
}
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.buttonStyle(ScarfPrimaryButton())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
@@ -316,7 +317,7 @@ struct TemplateUninstallSheet: View {
Image(systemName: "folder.badge.questionmark")
.foregroundStyle(.orange)
Text("Project folder kept")
.font(.headline)
.scarfStyle(.headline)
}
Text("These files weren't installed by the template (the agent or you created them after install), so Scarf left them in place along with the folder itself.")
.font(.caption)
@@ -1,5 +1,6 @@
import SwiftUI
import ScarfCore
import ScarfDesign
import AppKit
struct WebhooksView: View {
@@ -25,7 +26,6 @@ struct WebhooksView: View {
var body: some View {
VStack(spacing: 0) {
header
Divider()
if viewModel.isLoading && viewModel.webhooks.isEmpty {
ProgressView().padding()
} else if viewModel.webhookPlatformNotEnabled {
@@ -36,6 +36,7 @@ struct WebhooksView: View {
list
}
}
.background(ScarfColor.backgroundPrimary)
.navigationTitle("Webhooks")
.onAppear { viewModel.load() }
.sheet(isPresented: $showAddSheet) { addSheet }
@@ -52,25 +53,28 @@ struct WebhooksView: View {
}
private var header: some View {
HStack {
ScarfPageHeader(
"Webhooks",
subtitle: "HTTP receivers that trigger sessions on incoming events."
) {
HStack(spacing: ScarfSpace.s2) {
if let msg = viewModel.message {
Label(msg, systemImage: "info.circle.fill")
.font(.caption)
.foregroundStyle(.green)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.success)
}
Spacer()
Button("Reload") { viewModel.load() }
.buttonStyle(ScarfGhostButton())
Button {
resetAddForm()
showAddSheet = true
} label: {
Label("Subscribe", systemImage: "plus")
}
.controlSize(.small)
Button("Reload") { viewModel.load() }
.controlSize(.small)
.buttonStyle(ScarfPrimaryButton())
}
.fixedSize(horizontal: true, vertical: false)
}
.padding(.horizontal)
.padding(.vertical, 8)
}
/// Shown when hermes reports the webhook platform isn't enabled. Direct users