feat(design): adopt ScarfDesign system across Mac UI

Add a typed design-system package (Packages/ScarfDesign) with rust-tone
color tokens, type scale, spacing/radius tokens, ScarfPageHeader and
component primitives (ScarfCard, ScarfBadge, ScarfTextField,
ScarfSectionHeader, ScarfDivider, four button styles). Both Mac and iOS
targets `import ScarfDesign`.

Sidebar redesigned per design/static-site/ui-kit/Sidebar.jsx — glassy
translucent background, 224 px width, app-icon header with server pill,
custom tokenized rows with rust accent-tint when active, footer with
live Hermes-running indicator (wired to ServerLiveStatusRegistry).

14 mockup-backed feature screens redesigned: Settings, Dashboard,
Sessions, Memory, Chat (visual sweep), Activity, Cron, Insights,
MCPServers, Health, Logs, Tools (full); Projects light-touch.
Non-mockup features inherit rust through AccentColor.colorset repoint.

Mac AppIcon.appiconset replaced with the rust set. AccentColor.colorset
repointed to BrandRust hex (light + dark variants).

Visual sweep: every multi-button page-header / action-bar cluster now
wraps in .fixedSize(horizontal: true, vertical: false) so labels can't
wrap letter-by-letter at narrow widths (regression seen on the MCP
detail pane with 4 buttons).

Follow-ups landed:
- Sidebar Hermes-running probe wired to per-window
  ServerLiveStatusRegistry (no more placeholder green).
- Sessions: today filter predicate (isDateInToday(startedAt)); pill
  count reflects real count. Starred stays a no-op pending an upstream
  pinned/starred field on HermesSession.
- Dashboard: Recent activity column rendered alongside Recent sessions
  in a ViewThatFits 2-col grid. Populated from
  HermesDataService.fetchRecentToolCalls(limit:) flattened to
  ActivityEntry. ActivityEntry gains a public memberwise init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 13:27:54 +02:00
parent f04d95c960
commit 8a2d89654b
135 changed files with 11752 additions and 1434 deletions
+12
View File
@@ -22,6 +22,18 @@ scarf/scarf/ Xcode project root (PBXFileSystemSynchronizedRootGroup
- **Sandbox disabled**: App reads `~/.hermes/` directly.
- **Swift 6 concurrency**: `@MainActor` default. Services use `nonisolated` + async/await.
## Design System (ScarfDesign)
All app UI uses the typed token bundle in [scarf/Packages/ScarfDesign/](scarf/Packages/ScarfDesign/) — both the `scarf` and `scarf mobile` targets `import ScarfDesign`. Reach for these tokens before inventing new colors, fonts, or spacings.
- **Colors**: `ScarfColor.accent`, `.foregroundPrimary/Muted/Faint`, `.backgroundPrimary/Secondary/Tertiary`, `.border/.borderStrong`, `.success/.danger/.warning/.info`, `.Tool.bash/edit/search/web/think`. All resolve from `ScarfBrand.xcassets` and adapt light/dark automatically.
- **Typography**: `.scarfStyle(.title2)`, `.scarfStyle(.body)`, `.scarfStyle(.captionUppercase)`, etc. Use these instead of `.font(.system(...))`. Eleven preset styles cover the type scale.
- **Spacing / radius / shadow**: `ScarfSpace.s1...s10` (4/8/12/16/20/24/32/40), `ScarfRadius.sm/md/lg/xl/xxl/pill`, `.scarfShadow(.sm/.md/.lg/.xl)`. Hardcoded `.padding(12)` or `cornerRadius: 8` is a code smell — convert.
- **Components**: `ScarfPageHeader("Title", subtitle: "...") { trailing }`, `ScarfCard { ... }`, `ScarfBadge("text", kind: .success)`, `ScarfTextField`, `ScarfSectionHeader`, `ScarfDivider`, `ScarfPrimaryButton/SecondaryButton/GhostButton/DestructiveButton` (apply with `.buttonStyle(...)`).
- **App icon + accent**: `Assets.xcassets/AppIcon.appiconset/` is the rust set; `Assets.xcassets/AccentColor.colorset` resolves `Color.accentColor` to rust so any unmigrated SwiftUI control still tints correctly.
- **Reference**: full screen mockups live at `design/static-site/ui-kit/*.jsx` (open `design/static-site/index.html` in a browser). The `ScarfChatView.ChatRootView` reference component in the package is a 3-pane chat redesign target — usable for previews but not yet swapped into the live chat (the existing `RichChatView` machinery still owns the real ACP pipeline).
- **Don't**: introduce purple/violet tones (we shifted to rust); use yellow `#F0AD4E` for success (that's `.warning``.success` is green); bypass the type scale with `.font(.system(size: 13.5))`; ship terminal/syntax-highlight palettes through ScarfColor (those are content semantics, keep them inline).
## Key Paths
- Hermes home: `~/.hermes/`
@@ -0,0 +1,11 @@
{
"images" : [
{
"filename" : "Scarf-AppIcon-iOS-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+3
View File
@@ -0,0 +1,3 @@
{
"info" : { "author" : "xcode", "version" : 1 }
}
+52
View File
@@ -0,0 +1,52 @@
# Scarf Design System — static site
A self-contained, offline-friendly site that browses every artifact in the
Scarf design system. Open `index.html` directly in any browser — no server,
no build step.
## What's here
```
static-site/
├── index.html ← landing page, links into everything
├── colors_and_type.css ← shared design tokens (referenced everywhere)
├── ui-kit/ ← interactive macOS UI kit
│ ├── index.html ← click-thru of every screen in the app
│ └── *.jsx ← React components (Sidebar, Chat, Dashboard…)
├── tokens/ ← design-system cards
│ ├── _preview.css ← shared card styling
│ ├── colors-*.html ← brand / neutrals / semantic / tool-kinds
│ ├── type-*.html ← display / body / mono
│ ├── spacing-*.html ← scale / radii / shadows
│ ├── components-*.html ← buttons / forms / sidebar / cards / chat / composer / tool-call
│ ├── iconography.html
│ └── brand-mark.html
└── assets/ ← icons, brand artwork
```
## How to use it
- **Browse offline**: double-click `index.html`. Everything renders locally;
the only network dependency is Google Fonts (Inter + JetBrains Mono).
- **Host as a site**: drop the whole folder onto any static host (Netlify,
GitHub Pages, S3, your own nginx). Nothing needs building.
- **Embed in a doc**: link individual cards directly, e.g.
`static-site/tokens/colors-brand.html`.
- **Show the macOS app**: `static-site/ui-kit/index.html` runs the full
React-based interactive kit (single self-contained file — works from
`file://`, no server needed). The traffic-light corner makes it look like
the real app. Source components live alongside as `*.jsx` for editing —
re-bundle into `index.html` when you change them.
## Notes
- The kit's `index.html` is a self-contained bundle — React, Babel, Lucide
and every component are inlined, so it works from `file://` with no
network. The original split-file source is preserved as
`ui-kit/index.source.html` next to the `.jsx` files for editing.
- The font import in `colors_and_type.css` (`fonts.googleapis.com`) is the
only other network call. Replace with locally-served WOFF2 if you need
airgapped use.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

+193
View File
@@ -0,0 +1,193 @@
/* Scarf Design System — colors + type tokens. v2 (amber→rust)
*
* Light/dark via [data-theme="dark"] override on a parent. Default light.
*
* v2 changes: brand shifted from purple to a tri-stop amber→rust gradient.
* Neutrals warmed (yellow undertone). Semantic green/blue/red/orange preserved
* — those still mean success/info/danger and remain the tool-kind colors in chat.
*/
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root {
/* ───── Brand — amber → rust ───── */
--brand-50: #FBF1E8;
--brand-100: #F6E0CB;
--brand-200: #EFC59E; /* highlight stop in tri-gradient */
--brand-300: #E89360; /* gradient start */
--brand-400: #D87844;
--brand-500: #C25A2A; /* primary accent — Scarf Rust */
--brand-600: #A6481E;
--brand-700: #7A2E14; /* gradient end */
--brand-800: #5C220F;
--brand-900: #3B1608;
/* ───── Neutrals (warm, slight amber tint) ───── */
--gray-0: #FFFFFF;
--gray-50: #FBF9F6;
--gray-100: #F4F1EC;
--gray-200: #EAE5DD;
--gray-300: #D8D1C5;
--gray-400: #B5ABA0;
--gray-500: #8C857B;
--gray-600: #6A645B;
--gray-700: #4A463F;
--gray-800: #2D2A25;
--gray-900: #1A1814;
--gray-950: #100E0B;
/* ───── Semantic palette ───── */
--green-500: #2AA876;
--green-600: #1F7F5A;
--green-100: #D8F0E5;
--red-500: #D9534F;
--red-600: #B83C38;
--red-100: #F8DAD8;
--orange-500: #F0AD4E; /* reasoning / warning — distinct from brand rust */
--orange-100: #FCEAD0;
--blue-500: #3498DB;
--blue-100: #D8ECF8;
--indigo-500: #5B6CD9;
--purple-tool-500: #8E5BC9;
/* ───── Surfaces (light) ───── */
--fg: var(--gray-900);
--fg-muted: var(--gray-600);
--fg-faint: var(--gray-500);
--bg: var(--gray-50);
--bg-card: var(--gray-0);
--bg-quaternary: rgba(45, 42, 37, 0.04);
--bg-tertiary: rgba(45, 42, 37, 0.07);
--border: rgba(45, 42, 37, 0.08);
--border-strong: rgba(45, 42, 37, 0.14);
/* ───── Brand tokens (semantic) ───── */
--accent: var(--brand-500);
--accent-hover: var(--brand-600);
--accent-active: var(--brand-700);
--accent-tint: rgba(194, 90, 42, 0.10);
--accent-tint-strong: rgba(194, 90, 42, 0.18);
--on-accent: #FFFFFF;
/* ───── Type stacks ───── */
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
--font-display: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", "Segoe UI", sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
/* ───── Type scale ───── */
--text-caption2: 10px;
--text-caption: 12px;
--text-footnote: 13px;
--text-body: 14px;
--text-callout: 15px;
--text-subhead: 16px;
--text-headline: 17px;
--text-title3: 20px;
--text-title2: 22px;
--text-title1: 28px;
--text-largeTitle: 34px;
--leading-tight: 1.2;
--leading-snug: 1.35;
--leading-normal: 1.5;
--leading-relaxed: 1.6;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* ───── Radii / spacing / shadow ───── */
--r-sm: 4px;
--r-md: 6px;
--r-lg: 8px;
--r-xl: 12px;
--r-2xl: 14px;
--r-pill: 999px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--shadow-sm: 0 1px 2px rgba(45, 42, 37, 0.05);
--shadow-md: 0 1px 2px rgba(45, 42, 37, 0.04), 0 4px 12px rgba(45, 42, 37, 0.04);
--shadow-lg: 0 2px 4px rgba(45, 42, 37, 0.06), 0 8px 24px rgba(45, 42, 37, 0.07);
--shadow-xl: 0 4px 8px rgba(45, 42, 37, 0.08), 0 16px 40px rgba(45, 42, 37, 0.10);
--shadow-focus: 0 0 0 3px rgba(194, 90, 42, 0.28);
--gradient-brand: linear-gradient(135deg, #E89360 0%, #C25A2A 50%, #7A2E14 100%);
--gradient-brand-soft: linear-gradient(135deg, #F6E0CB 0%, #EFC59E 100%);
--ease-smooth: cubic-bezier(0.32, 0.72, 0, 1);
--dur-fast: 120ms;
--dur-base: 200ms;
--dur-slow: 300ms;
}
[data-theme="dark"] {
--fg: #EDE8E0;
--fg-muted: #A39C92;
--fg-faint: #756F66;
--bg: #15130F;
--bg-card: #1F1C18;
--bg-quaternary: rgba(255, 248, 235, 0.05);
--bg-tertiary: rgba(255, 248, 235, 0.08);
--border: rgba(255, 248, 235, 0.08);
--border-strong: rgba(255, 248, 235, 0.14);
--accent: #E89360;
--accent-hover: #F0A879;
--accent-active: #D87844;
--accent-tint: rgba(232, 147, 96, 0.14);
--accent-tint-strong: rgba(232, 147, 96, 0.24);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
--shadow-md: 0 1px 2px rgba(0, 0, 0, 0.35), 0 4px 12px rgba(0, 0, 0, 0.35);
--shadow-lg: 0 2px 4px rgba(0, 0, 0, 0.45), 0 8px 24px rgba(0, 0, 0, 0.45);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--fg: #EDE8E0;
--fg-muted: #A39C92;
--fg-faint: #756F66;
--bg: #15130F;
--bg-card: #1F1C18;
--bg-quaternary: rgba(255, 248, 235, 0.05);
--bg-tertiary: rgba(255, 248, 235, 0.08);
--border: rgba(255, 248, 235, 0.08);
--border-strong: rgba(255, 248, 235, 0.14);
--accent: #E89360;
--accent-hover: #F0A879;
--accent-active: #D87844;
--accent-tint: rgba(232, 147, 96, 0.14);
--accent-tint-strong: rgba(232, 147, 96, 0.24);
}
}
/* ───── Semantic type rules ───── */
body, .scarf-body {
font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--leading-normal);
color: var(--fg);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
.scarf-h1 { font-family: var(--font-display); font-size: var(--text-largeTitle); font-weight: 600; line-height: 1.2; letter-spacing: -0.02em; }
.scarf-h2 { font-family: var(--font-display); font-size: var(--text-title1); font-weight: 600; line-height: 1.2; letter-spacing: -0.015em; }
.scarf-h3 { font-family: var(--font-display); font-size: var(--text-title2); font-weight: 600; line-height: 1.35; letter-spacing: -0.01em; }
.scarf-headline { font-family: var(--font-sans); font-size: var(--text-headline); font-weight: 600; line-height: 1.35; }
.scarf-subhead { font-family: var(--font-sans); font-size: var(--text-subhead); font-weight: 500; line-height: 1.35; }
.scarf-body-text { font-family: var(--font-sans); font-size: var(--text-body); line-height: 1.5; }
.scarf-caption { font-family: var(--font-sans); font-size: var(--text-caption); line-height: 1.5; color: var(--fg-muted); }
.scarf-caption-strong { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-muted); }
.scarf-mono { font-family: var(--font-mono); font-size: 0.92em; }
.scarf-code { font-family: var(--font-mono); font-size: 0.9em; background: var(--bg-quaternary); padding: 1px 5px; border-radius: var(--r-sm); color: var(--fg); }
+382
View File
@@ -0,0 +1,382 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scarf Design System</title>
<link rel="stylesheet" href="colors_and_type.css">
<link rel="icon" type="image/png" href="assets/scarf-app-icon-256.png">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
min-height: 100vh;
background:
radial-gradient(ellipse 100% 60% at 50% -10%, rgba(232, 147, 96, 0.18), transparent 60%),
var(--bg);
color: var(--fg);
font-family: var(--font-sans);
}
.wrap { max-width: 1080px; margin: 0 auto; padding: 80px 32px 120px; }
header { display: flex; align-items: center; gap: 20px; margin-bottom: 56px; }
.icon-tile {
width: 88px; height: 88px;
border-radius: 22px;
background-image: url('assets/scarf-app-icon-256.png');
background-size: cover;
box-shadow: var(--shadow-lg);
}
h1 {
font-family: var(--font-display);
font-size: 44px;
font-weight: 600;
letter-spacing: -0.02em;
margin: 0 0 6px;
line-height: 1.1;
}
.tagline {
font-size: 17px;
color: var(--fg-muted);
line-height: 1.5;
max-width: 56ch;
margin: 0;
}
.section-label {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--fg-faint);
margin: 64px 0 20px;
}
/* Big feature card */
.hero-card {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 0;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 18px;
overflow: hidden;
box-shadow: var(--shadow-md);
margin-bottom: 40px;
}
.hero-card .text {
padding: 36px 36px 32px;
display: flex; flex-direction: column;
justify-content: center;
}
.hero-card .preview {
background: var(--gradient-brand);
position: relative;
min-height: 320px;
display: flex; align-items: center; justify-content: center;
}
.hero-card .preview img {
width: 60%; max-width: 240px;
filter: drop-shadow(0 14px 40px rgba(60, 18, 6, 0.35));
}
.hero-card h2 {
font-family: var(--font-display);
font-size: 28px;
font-weight: 600;
letter-spacing: -0.015em;
margin: 0 0 10px;
}
.hero-card p {
font-size: 15px;
color: var(--fg-muted);
line-height: 1.55;
margin: 0 0 24px;
}
.hero-card .cta {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
background: var(--accent);
color: var(--on-accent);
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
align-self: flex-start;
transition: background 120ms ease;
}
.hero-card .cta:hover { background: var(--accent-hover); }
.hero-card .cta svg { width: 16px; height: 16px; }
/* Token grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.tile {
display: block;
text-decoration: none;
color: inherit;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px 20px;
transition: transform 160ms var(--ease-smooth), border-color 160ms ease, box-shadow 160ms ease;
}
.tile:hover {
transform: translateY(-2px);
border-color: var(--border-strong);
box-shadow: var(--shadow-md);
}
.tile .kicker {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--fg-faint);
margin-bottom: 6px;
}
.tile h3 {
margin: 0 0 4px;
font-size: 16px;
font-weight: 600;
color: var(--fg);
}
.tile p {
margin: 0;
font-size: 13px;
color: var(--fg-muted);
line-height: 1.45;
}
.swatches {
display: flex; gap: 4px; margin-top: 14px;
}
.sw {
flex: 1; height: 22px; border-radius: 4px;
border: 1px solid rgba(0,0,0,0.05);
}
/* Group titles */
.group-title {
font-family: var(--font-display);
font-size: 22px;
font-weight: 600;
letter-spacing: -0.01em;
margin: 0 0 16px;
}
.group-blurb {
font-size: 14px;
color: var(--fg-muted);
margin: 0 0 24px;
line-height: 1.5;
max-width: 60ch;
}
footer {
margin-top: 80px;
padding-top: 28px;
border-top: 1px solid var(--border);
font-size: 13px;
color: var(--fg-faint);
display: flex; justify-content: space-between; align-items: center;
}
footer a { color: var(--fg-muted); text-decoration: none; }
footer a:hover { color: var(--accent); }
@media (max-width: 760px) {
.hero-card { grid-template-columns: 1fr; }
.hero-card .preview { min-height: 200px; order: -1; }
h1 { font-size: 36px; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="icon-tile" role="img" aria-label="Scarf app icon"></div>
<div>
<h1>Scarf Design System</h1>
<p class="tagline">A native macOS &amp; iOS companion for the Hermes AI agent — calm, confident, and rust-warm. This site documents the palette, type, components, and screens.</p>
</div>
</header>
<!-- UI Kit hero -->
<div class="section-label">UI Kit</div>
<a href="ui-kit/index.html" class="hero-card" style="text-decoration: none; color: inherit;">
<div class="text">
<h2>Interactive macOS app</h2>
<p>Click through every screen — Dashboard, Sessions, Insights, Projects, Chat, Settings, Tools, MCP servers, Cron, Logs, Memory, Activity, Health and more. Faithful to the real Scarf macOS app, with a working sidebar and the rust palette throughout.</p>
<span class="cta">
Open the kit
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</span>
</div>
<div class="preview">
<img src="assets/scarf-app-icon-1024.png" alt="">
</div>
</a>
<!-- Tokens & components -->
<div class="section-label">Tokens &amp; components</div>
<h2 class="group-title">Foundations</h2>
<p class="group-blurb">Each tile opens a single design-system card. They're sized for ~700px wide and render one concept at a time.</p>
<div class="grid">
<a class="tile" href="tokens/colors-brand.html">
<div class="kicker">Color</div>
<h3>Brand — amber → rust</h3>
<p>The 9-step rust ramp. Primary accent is <code>#C25A2A</code>.</p>
<div class="swatches">
<div class="sw" style="background:#FBF1E8"></div>
<div class="sw" style="background:#EFC59E"></div>
<div class="sw" style="background:#E89360"></div>
<div class="sw" style="background:#C25A2A"></div>
<div class="sw" style="background:#7A2E14"></div>
<div class="sw" style="background:#3B1608"></div>
</div>
</a>
<a class="tile" href="tokens/colors-neutrals.html">
<div class="kicker">Color</div>
<h3>Warm neutrals</h3>
<p>Slight amber undertone — never cool grey. 11 steps for surfaces and text.</p>
<div class="swatches">
<div class="sw" style="background:#FBF9F6"></div>
<div class="sw" style="background:#EAE5DD"></div>
<div class="sw" style="background:#B5ABA0"></div>
<div class="sw" style="background:#6A645B"></div>
<div class="sw" style="background:#2D2A25"></div>
<div class="sw" style="background:#100E0B"></div>
</div>
</a>
<a class="tile" href="tokens/colors-semantic.html">
<div class="kicker">Color</div>
<h3>Semantic palette</h3>
<p>Success, danger, warning, info — preserved from system conventions.</p>
<div class="swatches">
<div class="sw" style="background:#2AA876"></div>
<div class="sw" style="background:#D9534F"></div>
<div class="sw" style="background:#F0AD4E"></div>
<div class="sw" style="background:#3498DB"></div>
</div>
</a>
<a class="tile" href="tokens/colors-tool-kinds.html">
<div class="kicker">Color</div>
<h3>Tool-kind palette</h3>
<p>Bash, edit, search, web, think — the per-tool decorations in chat.</p>
<div class="swatches">
<div class="sw" style="background:#2AA876"></div>
<div class="sw" style="background:#3498DB"></div>
<div class="sw" style="background:#5B6CD9"></div>
<div class="sw" style="background:#8E5BC9"></div>
<div class="sw" style="background:#F0AD4E"></div>
</div>
</a>
<a class="tile" href="tokens/type-display.html">
<div class="kicker">Type</div>
<h3>Display scale</h3>
<p>Large titles &amp; headlines — SF Pro Display, tight tracking.</p>
</a>
<a class="tile" href="tokens/type-body.html">
<div class="kicker">Type</div>
<h3>Body scale</h3>
<p>14px base, the working text of the app.</p>
</a>
<a class="tile" href="tokens/type-mono.html">
<div class="kicker">Type</div>
<h3>Mono</h3>
<p>SF Mono — for transcripts, paths, command output.</p>
</a>
<a class="tile" href="tokens/spacing-scale.html">
<div class="kicker">Layout</div>
<h3>Spacing scale</h3>
<p>4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 — that's the whole grid.</p>
</a>
<a class="tile" href="tokens/spacing-radii.html">
<div class="kicker">Layout</div>
<h3>Radii</h3>
<p>4 / 6 / 8 / 12 / 14 / pill — tuned for native macOS controls.</p>
</a>
<a class="tile" href="tokens/spacing-shadows.html">
<div class="kicker">Layout</div>
<h3>Shadows</h3>
<p>Four elevation tiers, all on a warm-black tint.</p>
</a>
<a class="tile" href="tokens/iconography.html">
<div class="kicker">Brand</div>
<h3>Iconography</h3>
<p>Lucide icons at 16/18/20/24, 1.6px stroke, currentColor.</p>
</a>
<a class="tile" href="tokens/brand-mark.html">
<div class="kicker">Brand</div>
<h3>App mark</h3>
<p>The flowing-silk icon — preferred backgrounds &amp; minimum sizes.</p>
</a>
</div>
<h2 class="group-title" style="margin-top: 56px;">Components</h2>
<p class="group-blurb">Composable pieces lifted directly from the macOS app's surfaces.</p>
<div class="grid">
<a class="tile" href="tokens/components-buttons.html">
<div class="kicker">Component</div>
<h3>Buttons</h3>
<p>Primary / secondary / ghost / destructive — three sizes each.</p>
</a>
<a class="tile" href="tokens/components-forms.html">
<div class="kicker">Component</div>
<h3>Forms</h3>
<p>Text fields, toggles, selects — with focus &amp; error states.</p>
</a>
<a class="tile" href="tokens/components-sidebar.html">
<div class="kicker">Component</div>
<h3>Sidebar</h3>
<p>Section headers, items, active state, count pills.</p>
</a>
<a class="tile" href="tokens/components-stat-cards.html">
<div class="kicker">Component</div>
<h3>Stat cards</h3>
<p>Number-forward dashboard tiles.</p>
</a>
<a class="tile" href="tokens/components-status-cards.html">
<div class="kicker">Component</div>
<h3>Status cards</h3>
<p>Connection / health / run cards with semantic dots.</p>
</a>
<a class="tile" href="tokens/components-chat-bubbles.html">
<div class="kicker">Component</div>
<h3>Chat bubbles</h3>
<p>User &amp; agent rich messages, avatars, timestamps.</p>
</a>
<a class="tile" href="tokens/components-composer.html">
<div class="kicker">Component</div>
<h3>Composer</h3>
<p>Multiline input with attachments &amp; tool toggles.</p>
</a>
<a class="tile" href="tokens/components-tool-call.html">
<div class="kicker">Component</div>
<h3>Tool-call card</h3>
<p>Inline transcript card showing what the agent did.</p>
</a>
</div>
<footer>
<span>Scarf Design System · v2 (rust)</span>
<span><a href="ui-kit/index.html">UI kit</a> · <a href="tokens/colors-brand.html">First token</a></span>
</footer>
</div>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
/* Shared styling for design-system preview cards.
Each card is sized for ~700px wide and renders one focused concept. */
@import url('../colors_and_type.css');
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
}
.card-root {
padding: 20px 24px;
min-height: 110px;
display: flex;
flex-direction: column;
gap: 12px;
}
.row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.col { display: flex; flex-direction: column; gap: 8px; }
.label { font-size: 11px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
.mono { font-family: var(--font-mono); font-size: 11px; color: var(--fg-muted); }
/* swatches */
.swatch {
width: 92px;
height: 64px;
border-radius: 8px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 6px 8px;
position: relative;
overflow: hidden;
}
.swatch .name { font-size: 10px; font-weight: 600; }
.swatch .hex { font-family: var(--font-mono); font-size: 10px; opacity: 0.85; }
.swatch.dark-text { color: var(--gray-900); }
.swatch.light-text { color: #fff; }
+13
View File
@@ -0,0 +1,13 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Brand mark</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="flex-direction:row;align-items:center;gap:24px;min-height:160px">
<img src="../assets/scarf-app-icon-128.png" alt="Scarf icon" width="96" height="96"
style="border-radius:22px;box-shadow:var(--shadow-md);background:var(--gradient-brand)">
<div class="col" style="flex:1;gap:6px">
<div style="font-family:var(--font-display);font-size:28px;font-weight:600;letter-spacing:-0.015em">Scarf</div>
<div style="color:var(--fg-muted);font-size:14px;max-width:380px">A native macOS GUI for the Hermes AI agent. Full visibility into what an autonomous agent is doing, when, and what it creates.</div>
<div class="mono" style="margin-top:4px">brand: white silk on lavender → magenta gradient</div>
</div>
</div>
</body></html>
@@ -0,0 +1,17 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Primary palette</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div class="label">Brand · Scarf Purple</div>
<div class="row">
<div class="swatch light-text" style="background:#F5F0FA;color:#36204A"><div class="name">50</div><div class="hex">#F5F0FA</div></div>
<div class="swatch light-text" style="background:#EADDF3;color:#36204A"><div class="name">100</div><div class="hex">#EADDF3</div></div>
<div class="swatch light-text" style="background:#D4B8E8;color:#36204A"><div class="name">200</div><div class="hex">#D4B8E8</div></div>
<div class="swatch light-text" style="background:#B288D9"><div class="name">300</div><div class="hex">#B288D9</div></div>
<div class="swatch light-text" style="background:#8B5BB8"><div class="name">500 ★</div><div class="hex">#8B5BB8</div></div>
<div class="swatch light-text" style="background:#7848A0"><div class="name">600</div><div class="hex">#7848A0</div></div>
<div class="swatch light-text" style="background:#4D2C68"><div class="name">800</div><div class="hex">#4D2C68</div></div>
</div>
<div class="mono">★ var(--accent) · used for primary buttons, focused borders, active sidebar items</div>
</div>
</body></html>
@@ -0,0 +1,21 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Neutral palette</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div class="label">Neutrals · warm-cool gray scale</div>
<div class="row">
<div class="swatch dark-text" style="background:#FFFFFF"><div class="name">0</div><div class="hex">#FFFFFF</div></div>
<div class="swatch dark-text" style="background:#FAFAFB"><div class="name">50</div><div class="hex">#FAFAFB</div></div>
<div class="swatch dark-text" style="background:#F3F2F5"><div class="name">100</div><div class="hex">#F3F2F5</div></div>
<div class="swatch dark-text" style="background:#E8E6EC"><div class="name">200</div><div class="hex">#E8E6EC</div></div>
<div class="swatch dark-text" style="background:#D6D3DC"><div class="name">300</div><div class="hex">#D6D3DC</div></div>
<div class="swatch dark-text" style="background:#B5B1BD"><div class="name">400</div><div class="hex">#B5B1BD</div></div>
<div class="swatch light-text" style="background:#8C8893"><div class="name">500</div><div class="hex">#8C8893</div></div>
<div class="swatch light-text" style="background:#6A666F"><div class="name">600</div><div class="hex">#6A666F</div></div>
<div class="swatch light-text" style="background:#4A464E"><div class="name">700</div><div class="hex">#4A464E</div></div>
<div class="swatch light-text" style="background:#2E2C32"><div class="name">800</div><div class="hex">#2E2C32</div></div>
<div class="swatch light-text" style="background:#1A181E"><div class="name">900</div><div class="hex">#1A181E</div></div>
</div>
<div class="mono">slight violet tint — bg=50, bg-card=0, fg=900, fg-muted=600</div>
</div>
</body></html>
@@ -0,0 +1,19 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Semantic colors</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div class="label">Semantic · status &amp; feedback</div>
<div class="row">
<div class="swatch light-text" style="background:#2AA876"><div class="name">success</div><div class="hex">#2AA876</div></div>
<div class="swatch light-text" style="background:#D9534F"><div class="name">danger</div><div class="hex">#D9534F</div></div>
<div class="swatch dark-text" style="background:#F0AD4E"><div class="name">warning</div><div class="hex">#F0AD4E</div></div>
<div class="swatch light-text" style="background:#3498DB"><div class="name">info</div><div class="hex">#3498DB</div></div>
</div>
<div class="row" style="gap:8px;margin-top:4px">
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#D8F0E5;color:#1F7F5A;font-weight:600">● Running</span>
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#F8DAD8;color:#B83C38;font-weight:600">● Error</span>
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#FCEAD0;color:#A8741F;font-weight:600">● Reasoning</span>
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#D8ECF8;color:#1F70A8;font-weight:600">● Model</span>
</div>
</div>
</body></html>
@@ -0,0 +1,16 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Tool-kind colors</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div class="label">Tool-kind colors · agent activity</div>
<div class="row">
<div class="swatch light-text" style="background:#2AA876"><div class="name">read</div><div class="hex">green</div></div>
<div class="swatch light-text" style="background:#3498DB"><div class="name">edit</div><div class="hex">blue</div></div>
<div class="swatch dark-text" style="background:#F0AD4E"><div class="name">execute</div><div class="hex">orange</div></div>
<div class="swatch light-text" style="background:#8E5BC9"><div class="name">fetch</div><div class="hex">purple</div></div>
<div class="swatch light-text" style="background:#5B6CD9"><div class="name">browser</div><div class="hex">indigo</div></div>
<div class="swatch light-text" style="background:#8C8893"><div class="name">other</div><div class="hex">gray</div></div>
</div>
<div class="mono">preserved verbatim from ToolCallCard.swift — semantic to the product</div>
</div>
</body></html>
@@ -0,0 +1,31 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Buttons</title>
<link rel="stylesheet" href="_preview.css">
<style>
.btn { font-family:var(--font-sans); font-size:14px; font-weight:500; padding:7px 14px; border-radius:8px; border:1px solid transparent; cursor:pointer; transition:all 120ms var(--ease-smooth); }
.btn-primary { background:var(--accent); color:#fff; }
.btn-primary:hover { background:var(--accent-hover); }
.btn-secondary { background:var(--bg-card); color:var(--fg); border-color:var(--border-strong); }
.btn-secondary:hover { border-color:var(--accent); color:var(--accent-hover); }
.btn-ghost { background:transparent; color:var(--fg); }
.btn-ghost:hover { background:var(--bg-quaternary); }
.btn-danger { background:#fff; color:var(--red-600); border-color:var(--red-500); }
.btn-link { background:transparent; color:var(--accent); padding:6px 0; border:none; }
.btn-sm { font-size:12px; padding:4px 10px; }
</style></head>
<body>
<div class="card-root">
<div class="label">Buttons</div>
<div class="row">
<button class="btn btn-primary">Install Template</button>
<button class="btn btn-secondary">Run Diagnostics…</button>
<button class="btn btn-ghost">Cancel</button>
<button class="btn btn-danger">Delete</button>
<button class="btn btn-link">View All →</button>
</div>
<div class="row" style="margin-top:4px">
<button class="btn btn-primary btn-sm">Add</button>
<button class="btn btn-secondary btn-sm">Export</button>
<button class="btn btn-secondary btn-sm" disabled style="opacity:.4">Configure</button>
</div>
</div>
</body></html>
@@ -0,0 +1,15 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Chat bubbles</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="gap:8px">
<div style="display:flex;justify-content:flex-end">
<div style="background:var(--accent-tint);padding:8px 12px;border-radius:12px;font-size:14px;max-width:70%">What's the status of the cron job?</div>
</div>
<div style="text-align:right;font-size:10px;color:var(--fg-faint);margin-bottom:6px">9:42 AM</div>
<div style="background:var(--bg-quaternary);padding:8px 12px;border-radius:12px;font-size:14px;max-width:80%">
<div style="font-size:11px;color:var(--orange-500);font-weight:600;margin-bottom:4px">▾ Reasoning <span style="color:var(--fg-faint);font-weight:400">(127 tokens)</span></div>
The <span class="scarf-code" style="font-family:var(--font-mono);font-size:12px;background:rgba(0,0,0,.05);padding:1px 5px;border-radius:4px">daily-summary</span> job ran 14 minutes ago and completed successfully.
</div>
<div style="font-size:10px;color:var(--fg-faint);margin-left:4px">284 tokens · stop · 9:42 AM</div>
</div>
</body></html>
@@ -0,0 +1,12 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Composer</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div style="border-top:1px solid var(--border);padding:10px 12px;display:flex;gap:8px;align-items:flex-end;background:var(--bg-card);border-radius:8px;box-shadow:var(--shadow-sm)">
<div style="opacity:.6;font-size:18px;cursor:pointer"></div>
<div style="flex:1;background:var(--bg-quaternary);border-radius:12px;padding:8px 12px;font-size:14px;color:var(--fg-faint)">Message Hermes…</div>
<div style="font-size:22px;color:var(--accent)"></div>
</div>
<div class="mono">Rich Chat composer · /-menu opens above on slash, Shift+Enter for newline</div>
</div>
</body></html>
@@ -0,0 +1,26 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Form inputs</title>
<link rel="stylesheet" href="_preview.css">
<style>
.field { display:flex; flex-direction:column; gap:4px; flex:1; }
.field label { font-size:11px; color:var(--fg-muted); font-weight:600; text-transform:uppercase; letter-spacing:.05em; }
.field input, .field select { font-family:var(--font-sans); font-size:14px; padding:6px 10px; border:1px solid var(--border-strong); border-radius:6px; background:var(--bg-card); color:var(--fg); outline:none; transition:all 120ms; }
.field input:focus { border-color:var(--accent); box-shadow:var(--shadow-focus); }
.toggle { width:36px; height:20px; background:var(--accent); border-radius:999px; position:relative; cursor:pointer; }
.toggle::after { content:''; position:absolute; right:2px; top:2px; width:16px; height:16px; background:#fff; border-radius:50%; box-shadow:0 1px 2px rgba(0,0,0,.2); }
.toggle.off { background:var(--gray-300); }
.toggle.off::after { right:auto; left:2px; }
</style></head>
<body>
<div class="card-root">
<div class="row" style="gap:14px;align-items:flex-end">
<div class="field"><label>Project Name</label><input value="hermes-blog"/></div>
<div class="field"><label>Strategy</label><select><option>round_robin</option></select></div>
</div>
<div class="row" style="gap:18px">
<div class="row" style="gap:8px"><div class="toggle"></div><span style="font-size:13px">Auto-update</span></div>
<div class="row" style="gap:8px"><div class="toggle off"></div><span style="font-size:13px">Pause cron</span></div>
<div class="row" style="gap:8px;font-size:13px"><input type="checkbox" checked style="accent-color:var(--accent)"/>Verified</div>
<div class="row" style="gap:8px;font-size:13px"><input type="radio" checked style="accent-color:var(--accent)"/>Local</div>
</div>
</div>
</body></html>
@@ -0,0 +1,25 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Sidebar</title>
<link rel="stylesheet" href="_preview.css">
<style>
.sb { width:220px; background:var(--bg-quaternary); border-radius:10px; padding:10px 8px; font-size:13px; }
.sb-title { font-size:10px; color:var(--fg-muted); font-weight:600; text-transform:uppercase; letter-spacing:.06em; padding:6px 8px 4px }
.sb-item { display:flex; align-items:center; gap:8px; padding:5px 8px; border-radius:6px; color:var(--fg); cursor:pointer }
.sb-item:hover { background:var(--bg-tertiary) }
.sb-item.active { background:var(--accent-tint); color:var(--accent-active) }
.sb-icon { width:14px; opacity:.7 }
.sb-item.active .sb-icon { opacity:1 }
</style></head>
<body>
<div class="card-root" style="padding:14px">
<div class="sb">
<div class="sb-title">Monitor</div>
<div class="sb-item"><span class="sb-icon"></span>Dashboard</div>
<div class="sb-item active"><span class="sb-icon">📊</span>Insights</div>
<div class="sb-item"><span class="sb-icon">💬</span>Sessions</div>
<div class="sb-title">Interact</div>
<div class="sb-item"><span class="sb-icon"></span>Chat</div>
<div class="sb-item"><span class="sb-icon"></span>Memory</div>
<div class="sb-item"><span class="sb-icon"></span>Skills</div>
</div>
</div>
</body></html>
@@ -0,0 +1,18 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Stat cards</title>
<link rel="stylesheet" href="_preview.css">
<style>
.stat { background:var(--bg-quaternary); border-radius:8px; padding:14px 12px; flex:1; min-width:110px; text-align:center; }
.stat .v { font-family:var(--font-mono); font-size:22px; font-weight:600; }
.stat .l { font-size:11px; color:var(--fg-muted); margin-top:2px; }
</style></head>
<body>
<div class="card-root">
<div class="row" style="gap:12px">
<div class="stat"><div class="v">847</div><div class="l">Sessions</div></div>
<div class="stat"><div class="v">12,394</div><div class="l">Messages</div></div>
<div class="stat"><div class="v">3,221</div><div class="l">Tool Calls</div></div>
<div class="stat"><div class="v">2.4M</div><div class="l">Tokens</div></div>
<div class="stat"><div class="v">$42.18</div><div class="l">Cost</div></div>
</div>
</div>
</body></html>
@@ -0,0 +1,19 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Status cards</title>
<link rel="stylesheet" href="_preview.css">
<style>
.scard { background:var(--bg-quaternary); border-radius:8px; padding:12px; flex:1; min-width:130px; }
.scard .head { display:flex; align-items:center; gap:6px; font-size:11px; color:var(--fg-muted); margin-bottom:4px; }
.scard .dot { width:8px; height:8px; border-radius:50%; }
.scard .val { font-family:var(--font-mono); font-size:14px; font-weight:500; }
</style></head>
<body>
<div class="card-root">
<div class="row" style="gap:12px">
<div class="scard"><div class="head"><span class="dot" style="background:var(--green-500)"></span>Hermes</div><div class="val">Running</div></div>
<div class="scard"><div class="head" style="color:var(--blue-500)">⌬ Model</div><div class="val">claude-sonnet-4.5</div></div>
<div class="scard"><div class="head" style="color:var(--accent)">☁ Provider</div><div class="val">Anthropic</div></div>
<div class="scard"><div class="head"><span class="dot" style="background:var(--green-500)"></span>Gateway</div><div class="val">Connected · 3</div></div>
</div>
<div class="mono">Status cards · 4 across at standard width</div>
</div>
</body></html>
@@ -0,0 +1,31 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Tool call card</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div style="background:var(--bg-quaternary);border-radius:6px;padding:6px 8px;display:flex;align-items:center;gap:6px;font-size:12px">
<div style="width:3px;height:16px;background:var(--green-500);border-radius:1px"></div>
<span style="color:var(--green-500)">📖</span>
<span style="font-family:var(--font-mono);font-weight:600">read_file</span>
<span style="font-family:var(--font-mono);color:var(--fg-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">~/.hermes/config.yaml</span>
<span style="color:var(--green-500)"></span>
<span style="color:var(--fg-faint)"></span>
</div>
<div style="background:var(--bg-quaternary);border-radius:6px;padding:6px 8px;display:flex;align-items:center;gap:6px;font-size:12px">
<div style="width:3px;height:16px;background:var(--orange-500);border-radius:1px"></div>
<span style="color:var(--orange-500)"></span>
<span style="font-family:var(--font-mono);font-weight:600">execute</span>
<span style="font-family:var(--font-mono);color:var(--fg-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">{ "cmd": "hermes status" }</span>
<span style="color:var(--green-500)"></span>
<span style="color:var(--fg-faint)"></span>
</div>
<div style="background:var(--bg-quaternary);border-radius:6px;padding:6px 8px;display:flex;align-items:center;gap:6px;font-size:12px">
<div style="width:3px;height:16px;background:var(--blue-500);border-radius:1px"></div>
<span style="color:var(--blue-500)"></span>
<span style="font-family:var(--font-mono);font-weight:600">write_file</span>
<span style="font-family:var(--font-mono);color:var(--fg-faint);flex:1">cron/jobs.json</span>
<div style="width:10px;height:10px;border:1.5px solid var(--fg-faint);border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite"></div>
<span style="color:var(--fg-faint)"></span>
</div>
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
</div>
</body></html>
@@ -0,0 +1,24 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Iconography</title>
<link rel="stylesheet" href="_preview.css">
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
.ico { display:flex; flex-direction:column; align-items:center; gap:6px; font-size:10px; color:var(--fg-muted); width:64px }
.ico svg { width:22px; height:22px; stroke-width:1.5; color:var(--fg) }
</style></head>
<body>
<div class="card-root">
<div class="label">Iconography · Lucide (web sub for SF Symbols)</div>
<div class="row" style="gap:14px">
<div class="ico"><i data-lucide="layout-grid"></i>Dashboard</div>
<div class="ico"><i data-lucide="bar-chart-3"></i>Insights</div>
<div class="ico"><i data-lucide="messages-square"></i>Sessions</div>
<div class="ico"><i data-lucide="cpu"></i>Model</div>
<div class="ico"><i data-lucide="cloud"></i>Provider</div>
<div class="ico"><i data-lucide="package"></i>Templates</div>
<div class="ico"><i data-lucide="folder"></i>Projects</div>
<div class="ico"><i data-lucide="wrench"></i>Tools</div>
<div class="ico"><i data-lucide="stethoscope"></i>Diagnostics</div>
</div>
<script>lucide.createIcons();</script>
</div>
</body></html>
@@ -0,0 +1,14 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Radii</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div class="label">Radii · 4 / 6 / 8 / 12 / 14</div>
<div class="row" style="gap:14px;align-items:flex-end">
<div class="col" style="align-items:center;gap:6px"><div style="width:64px;height:64px;background:var(--accent-tint);border:1px solid var(--accent);border-radius:4px"></div><div class="mono">4 · chips, code</div></div>
<div class="col" style="align-items:center;gap:6px"><div style="width:64px;height:64px;background:var(--accent-tint);border:1px solid var(--accent);border-radius:6px"></div><div class="mono">6 · tool cards</div></div>
<div class="col" style="align-items:center;gap:6px"><div style="width:64px;height:64px;background:var(--accent-tint);border:1px solid var(--accent);border-radius:8px"></div><div class="mono">8 · cards, btns</div></div>
<div class="col" style="align-items:center;gap:6px"><div style="width:64px;height:64px;background:var(--accent-tint);border:1px solid var(--accent);border-radius:12px"></div><div class="mono">12 · bubbles</div></div>
<div class="col" style="align-items:center;gap:6px"><div style="width:64px;height:64px;background:var(--accent-tint);border:1px solid var(--accent);border-radius:14px"></div><div class="mono">14 · windows</div></div>
</div>
</div>
</body></html>
@@ -0,0 +1,16 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Spacing scale</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div class="label">Spacing · 4-base scale</div>
<div class="col" style="gap:6px">
<div class="row" style="gap:10px"><div style="width:4px;height:14px;background:var(--accent)"></div><div class="mono">4 · 1 · inline gaps</div></div>
<div class="row" style="gap:10px"><div style="width:8px;height:14px;background:var(--accent)"></div><div class="mono">8 · 2 · button padding y</div></div>
<div class="row" style="gap:10px"><div style="width:12px;height:14px;background:var(--accent)"></div><div class="mono">12 · 3 · card padding</div></div>
<div class="row" style="gap:10px"><div style="width:16px;height:14px;background:var(--accent)"></div><div class="mono">16 · 4 · view padding</div></div>
<div class="row" style="gap:10px"><div style="width:20px;height:14px;background:var(--accent)"></div><div class="mono">20 · 5 · section gap</div></div>
<div class="row" style="gap:10px"><div style="width:24px;height:14px;background:var(--accent)"></div><div class="mono">24 · 6 · header gap</div></div>
<div class="row" style="gap:10px"><div style="width:32px;height:14px;background:var(--accent)"></div><div class="mono">32 · 8 · page-level</div></div>
</div>
</div>
</body></html>
@@ -0,0 +1,13 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Shadows</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="background:var(--bg)">
<div class="label">Shadows · two-layer Apple style</div>
<div class="row" style="gap:24px;padding:12px 4px">
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 1px 2px rgba(28,26,32,.05)"></div><div class="mono">sm · subtle lift</div></div>
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 1px 2px rgba(28,26,32,.04),0 4px 12px rgba(28,26,32,.04)"></div><div class="mono">md · cards</div></div>
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 2px 4px rgba(28,26,32,.06),0 8px 24px rgba(28,26,32,.07)"></div><div class="mono">lg · hover</div></div>
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 4px 8px rgba(28,26,32,.08),0 16px 40px rgba(28,26,32,.10)"></div><div class="mono">xl · sheet</div></div>
</div>
</div>
</body></html>
+11
View File
@@ -0,0 +1,11 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Type · body</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="gap:10px">
<div class="label">Body · sentence case, calm and direct</div>
<div style="font-size:17px;font-weight:600">Hermes actually knows what project it's in</div>
<div style="font-size:15px;color:var(--fg-muted)">Every project-scoped chat gets a Scarf-managed block auto-injected into the project's <span class="scarf-code" style="font-family:var(--font-mono);font-size:13px">AGENTS.md</span> before the session starts.</div>
<div style="font-size:14px">Ask the agent <em>"what project am I in?"</em> and it answers with the project name, directory, template id, and registered cron jobs.</div>
<div style="font-size:12px;color:var(--fg-muted)">headline 17 · subhead 15 · body 14 · caption 12 — same rhythm as SwiftUI's text styles</div>
</div>
</body></html>
@@ -0,0 +1,11 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Type · display</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="gap:14px">
<div class="label">Display · SF Pro Display / Inter</div>
<div style="font-family:var(--font-display);font-size:34px;font-weight:600;letter-spacing:-0.02em;line-height:1.15">Make the complex simple</div>
<div style="font-family:var(--font-display);font-size:28px;font-weight:600;letter-spacing:-0.015em;line-height:1.2">Recent sessions</div>
<div style="font-family:var(--font-display);font-size:22px;font-weight:600;letter-spacing:-0.01em">Activity patterns</div>
<div class="mono">largeTitle 34 / title1 28 / title2 22 — used for view titles only</div>
</div>
</body></html>
+15
View File
@@ -0,0 +1,15 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Type · mono</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="gap:10px">
<div class="label">Mono · SF Mono / JetBrains Mono</div>
<div style="font-family:var(--font-mono);font-size:14px;font-weight:500">claude-haiku-4-5</div>
<div style="font-family:var(--font-mono);font-size:13px;color:var(--fg-muted)">~/.hermes/state.db · 14.2 MB</div>
<div style="font-family:var(--font-mono);font-size:12px">{ "tokens": 2384, "model": "claude-haiku-4-5" }</div>
<div class="row" style="gap:6px">
<span style="font-family:var(--font-mono);font-size:11px;background:var(--bg-quaternary);padding:2px 8px;border-radius:4px">v2.3.0</span>
<span style="font-family:var(--font-mono);font-size:11px;background:var(--bg-quaternary);padding:2px 8px;border-radius:4px">2,847 tokens</span>
<span style="font-family:var(--font-mono);font-size:11px;background:var(--bg-quaternary);padding:2px 8px;border-radius:4px">$0.0421</span>
</div>
</div>
</body></html>
+98
View File
@@ -0,0 +1,98 @@
// Activity — chronological feed of everything that happened recently across
// all projects, sessions, cron, and tools. Day-grouped, filterable.
const ACTIVITY_GROUPS = [
{ day: 'Today', items: [
{ time: '09:42', icon: 'message-square', tone: 'accent', title: 'Sera — chat session resumed', sub: 'Forge · 14 turns · refactored CronRunner', proj: 'sera' },
{ time: '09:30', icon: 'clock', tone: 'green', title: 'incident-triage ran', sub: 'cron · ok in 4.2s · 0 issues created', proj: '—' },
{ time: '09:00', icon: 'clock', tone: 'green', title: 'daily-summary ran', sub: 'cron · ok in 36s · posted to #standup', proj: '—' },
{ time: '08:42', icon: 'git-pull-request', tone: 'blue', title: 'PR #284 opened', sub: 'sera · "Switch to AbortController for cron timeouts"', proj: 'sera' },
{ time: '08:14', icon: 'shield', tone: 'amber', title: 'Approval: execute git push origin main', sub: 'sera · approved by Aurora · 3.2s wait', proj: 'sera' },
]},
{ day: 'Yesterday', items: [
{ time: '17:22', icon: 'check-circle', tone: 'green', title: 'release-notes generated', sub: 'cron · ok in 1m 03s · draft saved', proj: '—' },
{ time: '15:08', icon: 'plug', tone: 'accent', title: 'MCP server connected — Figma', sub: '6 tools, 2 prompts available', proj: '—' },
{ time: '14:31', icon: 'message-square', tone: 'accent', title: 'Hermes — onboarding draft', sub: '8 turns · drafted welcome email', proj: 'hermes' },
{ time: '11:02', icon: 'alert-triangle', tone: 'red', title: 'Tool denied — rm -rf node_modules', sub: 'sera · matched deny rule "rm -rf"', proj: 'sera' },
{ time: '09:00', icon: 'clock', tone: 'green', title: 'daily-summary ran', sub: 'cron · ok in 41s', proj: '—' },
]},
{ day: 'Mon, Apr 21', items: [
{ time: '16:48', icon: 'user-plus', tone: 'accent', title: 'New personality — Atlas', sub: 'Created by Aurora · long-form writing model', proj: '—' },
{ time: '14:00', icon: 'database', tone: 'blue', title: 'Postgres (prod, ro) reconfigured', sub: 'switched to read replica', proj: '—' },
{ time: '09:00', icon: 'clock', tone: 'red', title: 'daily-summary failed', sub: 'cron · github 502 bad gateway · retried ok at 09:14', proj: '—' },
]},
];
const ACT_TONES = {
accent: { bg: 'var(--accent-tint)', fg: 'var(--accent)' },
green: { bg: 'var(--green-100)', fg: 'var(--green-600)' },
blue: { bg: 'var(--blue-100)', fg: 'var(--blue-500)' },
amber: { bg: 'var(--orange-100)', fg: 'var(--orange-500)' },
red: { bg: 'var(--red-100)', fg: 'var(--red-500)' },
};
function Activity() {
const [filter, setFilter] = React.useState('all');
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Activity"
subtitle="Everything Scarf has done recently — sessions, cron, tools, MCP, approvals"
actions={<Btn icon="filter">Filter</Btn>}
right={
<Segmented value={filter} onChange={setFilter} size="sm" options={[
{ value: 'all', label: 'All' },
{ value: 'sessions', label: 'Sessions' },
{ value: 'cron', label: 'Cron' },
{ value: 'tools', label: 'Tools' },
]} />
} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
{ACTIVITY_GROUPS.map(g => (
<div key={g.day} style={{ marginBottom: 28 }}>
<div style={{
fontSize: 11, fontWeight: 600, color: 'var(--fg-muted)',
textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8,
padding: '0 4px',
}}>{g.day}</div>
<div style={{
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
borderRadius: 10, overflow: 'hidden',
}}>
{g.items.map((it, i) => <ActivityRow key={i} it={it} last={i === g.items.length - 1} />)}
</div>
</div>
))}
</div>
</div>
);
}
function ActivityRow({ it, last }) {
const tone = ACT_TONES[it.tone];
const [hover, setHover] = React.useState(false);
return (
<div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
background: hover ? 'var(--bg-quaternary)' : 'transparent', cursor: 'pointer',
}}>
<span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--fg-faint)', width: 44 }}>{it.time}</span>
<div style={{
width: 26, height: 26, borderRadius: 6, background: tone.bg, color: tone.fg,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<i data-lucide={it.icon} style={{ width: 14, height: 14 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{it.title}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg-muted)', marginTop: 1 }}>{it.sub}</div>
</div>
{it.proj !== '—' && <Pill size="sm">{it.proj}</Pill>}
<i data-lucide="chevron-right" style={{ width: 14, height: 14, color: 'var(--fg-faint)' }}></i>
</div>
);
}
window.Activity = Activity;
+787
View File
@@ -0,0 +1,787 @@
// Chat — three-pane: session list / transcript / inspector.
// Inspector defaults to ToolCall details for the focused tool call; falls
// back to session-level metadata. Transcript supports reasoning, multi-step
// tool calls, file diffs, and a slash-command palette in the composer.
const TOOL_TONES = {
read: { color: 'var(--green-500)', tint: 'var(--green-100)', icon: 'book-open', label: 'Read' },
edit: { color: 'var(--blue-500)', tint: 'var(--blue-100)', icon: 'file-edit', label: 'Edit' },
execute: { color: 'var(--orange-500)', tint: 'var(--orange-100)', icon: 'terminal', label: 'Execute' },
fetch: { color: 'var(--purple-tool-500)', tint: '#EFE0F8', icon: 'globe', label: 'Fetch' },
browser: { color: 'var(--indigo-500)', tint: '#E0E5F8', icon: 'compass', label: 'Browser' },
search: { color: 'var(--accent)', tint: 'var(--accent-tint)',icon: 'search', label: 'Search' },
};
// ─────────────── Top-level Chat ───────────────
function Chat() {
const [active, setActive] = React.useState('s1');
const [focused, setFocused] = React.useState({ kind: 'tool', id: 'tc-2' }); // inspector subject
const [composerOpen, setComposerOpen] = React.useState(false); // slash menu
React.useEffect(() => {
requestAnimationFrame(() => window.lucide && window.lucide.createIcons());
});
const sessions = [
{ id: 's1', title: 'Cron diagnostics', project: 'scarf', preview: 'The daily-summary job ran 14 minutes ago…', time: '14m', model: 'sonnet-4.5', unread: 0, pinned: true, status: 'live' },
{ id: 's2', title: 'Release notes draft', project: 'hermes-blog', preview: 'Pulled the merged PRs from this week…', time: '42m', model: 'haiku-4.5', unread: 2, status: 'idle' },
{ id: 's3', title: 'PR review summary', project: 'hermes-blog', preview: 'Three PRs are ready for review.', time: '2h', model: 'sonnet-4.5', status: 'idle' },
{ id: 's4', title: 'Function calling models', project: '—', preview: 'Sonnet handles structured tool use…', time: '3h', model: 'haiku-4.5', status: 'idle' },
{ id: 's5', title: 'Memory layout question', project: 'scarf', preview: 'The shared memory keys live at…', time: 'yesterday', model: 'sonnet-4.5', status: 'idle' },
{ id: 's6', title: 'Catalog publish flow', project: 'hermes-blog', preview: 'Walked through the .scarftemplate bundle…', time: 'yesterday', model: 'sonnet-4.5', status: 'idle' },
{ id: 's7', title: 'SSH tunnel debug', project: 'scarf-remote', preview: 'Connection drops after ~90s of idle…', time: 'Mon', model: 'sonnet-4.5', status: 'error' },
];
return (
<div style={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
<ChatList sessions={sessions} active={active} setActive={setActive} />
<Transcript focused={focused} setFocused={setFocused} composerOpen={composerOpen} setComposerOpen={setComposerOpen} />
<Inspector focused={focused} setFocused={setFocused} />
</div>
);
}
// ─────────────── Pane 1 — session list ───────────────
function ChatList({ sessions, active, setActive }) {
const [filter, setFilter] = React.useState('all');
return (
<div style={{
width: 264, borderRight: '0.5px solid var(--border)',
background: 'var(--gray-50)', display: 'flex', flexDirection: 'column'
}}>
<div style={{ padding: '14px 14px 8px', display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ flex: 1, fontFamily: 'var(--font-display)', fontSize: 17, fontWeight: 600 }}>Chats</div>
<IconBtn icon="search" tooltip="Search ⌘F" />
<Btn size="sm" kind="primary" icon="plus">New</Btn>
</div>
<div style={{ padding: '0 12px 8px' }}>
<Segmented value={filter} onChange={setFilter} size="sm" options={[
{ value: 'all', label: 'All', count: sessions.length },
{ value: 'live', label: 'Live', count: 1 },
{ value: 'pinned', label: 'Pinned', count: 1 },
]} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '0 6px 8px' }}>
<SessionGroupHeader>Today</SessionGroupHeader>
{sessions.slice(0, 4).map(s => <SessionRow key={s.id} s={s} active={active === s.id} onClick={() => setActive(s.id)} />)}
<SessionGroupHeader>Earlier</SessionGroupHeader>
{sessions.slice(4).map(s => <SessionRow key={s.id} s={s} active={active === s.id} onClick={() => setActive(s.id)} />)}
</div>
<div style={{ padding: '8px 14px', borderTop: '0.5px solid var(--border)',
display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--fg-muted)' }}>
<i data-lucide="message-square" style={{ width: 12, height: 12 }}></i>
<span>{sessions.length} chats</span>
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 10 }}>1.2 MB · state.db</span>
</div>
</div>
);
}
function SessionGroupHeader({ children }) {
return (
<div style={{
padding: '10px 10px 4px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em',
}}>{children}</div>
);
}
function SessionRow({ s, active, onClick }) {
const [hover, setHover] = React.useState(false);
const statusColor = s.status === 'live' ? 'var(--green-500)' : s.status === 'error' ? 'var(--red-500)' : 'var(--gray-400)';
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
padding: '8px 10px', borderRadius: 7, cursor: 'pointer', marginBottom: 1,
background: active ? 'var(--accent-tint)' : (hover ? 'var(--bg-quaternary)' : 'transparent'),
position: 'relative',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
{s.status === 'live'
? <span style={{ width: 7, height: 7, borderRadius: '50%', background: statusColor,
boxShadow: '0 0 0 2px rgba(42,168,118,0.20)' }}></span>
: <span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor }}></span>}
{s.pinned && <i data-lucide="pin" style={{ width: 11, height: 11, color: 'var(--accent)' }}></i>}
<div style={{ flex: 1, fontSize: 13, fontWeight: 500,
color: active ? 'var(--accent-active)' : 'var(--fg)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.title}</div>
<div style={{ fontSize: 10, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>{s.time}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4, paddingLeft: 14 }}>
{s.project !== '—' && <span style={{
fontSize: 10, fontWeight: 500, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)',
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
padding: '0 5px', borderRadius: 4,
}}>{s.project}</span>}
<div style={{ flex: 1, fontSize: 11, color: 'var(--fg-muted)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.preview}</div>
{s.unread > 0 && <span style={{
fontSize: 9, fontWeight: 700, fontFamily: 'var(--font-mono)',
padding: '1px 5px', borderRadius: 999, background: 'var(--accent)', color: '#fff', minWidth: 14, textAlign: 'center',
}}>{s.unread}</span>}
</div>
</div>
);
}
// ─────────────── Pane 2 — transcript ───────────────
function Transcript({ focused, setFocused, composerOpen, setComposerOpen }) {
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0,
background: 'var(--bg)' }}>
<TranscriptHeader />
<div style={{ flex: 1, overflowY: 'auto', padding: '20px 28px 8px',
display: 'flex', flexDirection: 'column', gap: 16, scrollBehavior: 'smooth' }}>
<DateMarker>Today · 9:42 AM</DateMarker>
<UserMsg time="9:42 AM">What's the status of the daily-summary cron job? I need to know if it's healthy before I push the new schedule changes.</UserMsg>
<AssistantMsg time="9:42 AM" tokens={284} model="sonnet-4.5" durationMs={2140}>
<Reasoning tokens={127} preview="Check the registry first, then the most recent execution." />
<ToolCall id="tc-1" kind="read" name="read_file" arg="~/.scarf/cron/jobs.json" duration="86 ms" focus={focused} setFocus={setFocused} />
<ToolCall id="tc-2" kind="execute" name="execute" arg='hermes cron status daily-summary' duration="1.4 s" focus={focused} setFocus={setFocused} expanded />
<p style={msgPara}>
The <code style={inlineCode}>daily-summary</code> job ran <strong>14 minutes ago</strong> and completed
successfully in 14.2 s, using 1,847 tokens. Next run is scheduled for tomorrow at 09:00 safe to ship the schedule changes.
</p>
<MsgFooter />
</AssistantMsg>
<UserMsg time="9:43 AM">Show me what it produced.</UserMsg>
<AssistantMsg time="9:43 AM" tokens={612} model="sonnet-4.5" inProgress durationMs={4280}>
<ToolCall id="tc-3" kind="read" name="read_file" arg="~/.scarf/cron/output/daily-summary.md" duration="42 ms" focus={focused} setFocus={setFocused} />
<p style={msgPara}>The latest summary covers <strong>April 24, 2026</strong>. Highlights:</p>
<ul style={{ ...msgPara, paddingLeft: 18, margin: '4px 0' }}>
<li>3 PRs merged across <code style={inlineCode}>hermes</code> and <code style={inlineCode}>scarf</code></li>
<li>2 cron failures auto-recovered (gateway timeouts)</li>
<li>Token spend down 8% week-over-week</li>
</ul>
<ToolCall id="tc-4" kind="edit" name="apply_patch" arg="~/.scarf/cron/jobs.json" duration="120 ms" diff focus={focused} setFocus={setFocused} />
</AssistantMsg>
<SuggestedReplies items={['Schedule a dry run', 'Show last 5 runs', 'Disable daily-summary']} />
</div>
<Composer open={composerOpen} setOpen={setComposerOpen} />
</div>
);
}
function TranscriptHeader() {
return (
<div style={{
padding: '14px 24px', borderBottom: '0.5px solid var(--border)',
display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-card)',
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<i data-lucide="pin" style={{ width: 13, height: 13, color: 'var(--accent)' }}></i>
<div style={{ fontSize: 14, fontWeight: 600 }}>Cron diagnostics</div>
<Pill tone="green" dot size="sm">live</Pill>
</div>
<div style={{ fontSize: 11, color: 'var(--fg-muted)', display: 'flex', gap: 10, marginTop: 3, alignItems: 'center', flexWrap: 'wrap' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<i data-lucide="folder" style={{ width: 11, height: 11, color: 'var(--accent)' }}></i>
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>scarf</span>
</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>claude-sonnet-4.5</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span>14 messages</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>12,847 tok</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>$0.0421</span>
</div>
</div>
<Btn size="sm" kind="ghost" icon="git-branch">Branch</Btn>
<Btn size="sm" kind="secondary" icon="share">Share</Btn>
<IconBtn icon="more-horizontal" tooltip="More" />
</div>
);
}
function DateMarker({ children }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, color: 'var(--fg-faint)' }}>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
<span style={{ fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em' }}>{children}</span>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
</div>
);
}
const msgPara = { fontSize: 14, lineHeight: 1.55, color: 'var(--fg)', margin: '6px 0' };
const inlineCode = { fontFamily: 'var(--font-mono)', fontSize: 12.5,
background: 'var(--bg-quaternary)', padding: '1px 5px', borderRadius: 4 };
function UserMsg({ time, children }) {
return (
<div style={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{
maxWidth: '76%', padding: '10px 14px', borderRadius: 14, borderBottomRightRadius: 4,
background: 'var(--accent)', color: 'var(--on-accent)', fontSize: 14, lineHeight: 1.5,
boxShadow: '0 1px 0 rgba(0,0,0,0.06)',
}}>{children}</div>
<div style={{ fontSize: 10, color: 'var(--fg-faint)', marginTop: 4, marginRight: 4,
display: 'flex', gap: 6, alignItems: 'center' }}>
<i data-lucide="check-check" style={{ width: 11, height: 11, color: 'var(--green-500)' }}></i>
<span>{time}</span>
</div>
</div>
);
}
function AssistantMsg({ time, tokens, model, inProgress, durationMs, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', maxWidth: '88%', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, width: '100%' }}>
<div style={{
width: 26, height: 26, borderRadius: 7, marginTop: 2, flexShrink: 0,
background: 'var(--gradient-brand)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff',
boxShadow: '0 1px 2px rgba(122, 46, 20, 0.25)',
}}>
<i data-lucide="sparkles" style={{ width: 14, height: 14 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
background: 'var(--bg-card)', borderRadius: 12,
border: '0.5px solid var(--border)',
padding: '12px 14px', boxShadow: 'var(--shadow-sm)',
}}>{children}</div>
<div style={{ fontSize: 10, color: 'var(--fg-faint)', marginTop: 4, marginLeft: 4,
display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
{inProgress && <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<span style={{
width: 7, height: 7, borderRadius: '50%', background: 'var(--accent)',
animation: 'pulseScarf 1.4s ease-in-out infinite',
}}></span>
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>thinking</span>
</span>}
<span style={{ fontFamily: 'var(--font-mono)' }}>{model}</span>
<span>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>{tokens} tok</span>
<span>·</span>
<span>{(durationMs / 1000).toFixed(1)}s</span>
<span>·</span>
<span>{time}</span>
</div>
</div>
</div>
</div>
);
}
function MsgFooter() {
const Btnn = ({ icon, label }) => {
const [hover, setHover] = React.useState(false);
return (
<button onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
padding: '3px 7px', fontSize: 11, color: hover ? 'var(--fg)' : 'var(--fg-muted)',
background: hover ? 'var(--bg-quaternary)' : 'transparent',
border: 'none', borderRadius: 5, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 4, fontFamily: 'var(--font-sans)',
}}>
<i data-lucide={icon} style={{ width: 11, height: 11 }}></i>{label}
</button>
);
};
return (
<div style={{ display: 'flex', gap: 2, marginTop: 6, paddingTop: 6, borderTop: '0.5px solid var(--border)' }}>
<Btnn icon="copy" label="Copy" />
<Btnn icon="thumbs-up" label="" />
<Btnn icon="thumbs-down" label="" />
<Btnn icon="rotate-cw" label="Retry" />
<div style={{ flex: 1 }}></div>
<Btnn icon="pin" label="Pin" />
</div>
);
}
// ─────────────── Reasoning disclosure ───────────────
function Reasoning({ tokens, preview, children }) {
const [open, setOpen] = React.useState(false);
return (
<div style={{ marginBottom: 8, background: 'var(--orange-100)', borderRadius: 7,
padding: '6px 10px', border: '0.5px solid rgba(240, 173, 78, 0.3)' }}>
<div onClick={() => setOpen(!open)} style={{
cursor: 'pointer', fontSize: 11, fontWeight: 600,
display: 'flex', alignItems: 'center', gap: 5, color: '#A8741F',
}}>
<i data-lucide="brain" style={{ width: 12, height: 12 }}></i>
<span style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Reasoning</span>
<span style={{ color: 'var(--fg-faint)', fontWeight: 500, fontFamily: 'var(--font-mono)' }}>· {tokens} tok</span>
<span style={{ flex: 1 }}></span>
<i data-lucide={open ? 'chevron-down' : 'chevron-right'} style={{ width: 12, height: 12 }}></i>
</div>
{!open && preview && (
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 3,
fontStyle: 'italic', lineHeight: 1.5,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{preview}</div>
)}
{open && (
<div style={{ fontSize: 12.5, color: 'var(--fg-muted)', lineHeight: 1.55,
padding: '6px 0 2px', fontStyle: 'italic' }}>
The user wants the status of a specific cron job named "daily-summary".
I should check the cron registry first, then look at the most recent execution
via <code style={inlineCode}>hermes cron status</code>. If exit_code is 0,
the job is healthy and the schedule push is safe.
</div>
)}
</div>
);
}
// ─────────────── ToolCall card ───────────────
function ToolCall({ id, kind, name, arg, duration, expanded: initial, diff, focus, setFocus }) {
const [open, setOpen] = React.useState(initial || false);
const t = TOOL_TONES[kind] || TOOL_TONES.read;
const isFocused = focus.kind === 'tool' && focus.id === id;
return (
<div style={{ marginBottom: 5 }}>
<div onClick={() => { setOpen(!open); setFocus({ kind: 'tool', id }); }} style={{
background: isFocused ? t.tint : 'var(--bg-quaternary)',
border: `0.5px solid ${isFocused ? t.color : 'var(--border)'}`,
outline: isFocused ? `1px solid ${t.color}` : 'none', outlineOffset: '-1px',
borderRadius: 7, padding: '6px 10px',
display: 'flex', alignItems: 'center', gap: 9,
fontSize: 12, cursor: 'pointer', transition: 'all 120ms',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<i data-lucide={t.icon} style={{ width: 12, height: 12, color: t.color }}></i>
<span style={{ fontSize: 10, fontWeight: 700, color: t.color,
textTransform: 'uppercase', letterSpacing: '0.04em' }}>{t.label}</span>
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600, color: 'var(--fg)' }}>{name}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--fg-muted)', flex: 1, minWidth: 0,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{arg}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-faint)' }}>{duration}</span>
<i data-lucide="check-circle-2" style={{ width: 13, height: 13, color: 'var(--green-500)' }}></i>
<i data-lucide={open ? 'chevron-down' : 'chevron-right'} style={{ width: 12, height: 12, color: 'var(--fg-faint)' }}></i>
</div>
{open && (
diff
? <DiffPreview />
: <ToolOutput kind={kind} />
)}
</div>
);
}
function ToolOutput({ kind }) {
if (kind === 'execute') {
return (
<div style={{
background: 'var(--gray-900)', color: '#E8E1D2', borderRadius: 7,
padding: '10px 12px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
marginTop: 6, lineHeight: 1.55, overflow: 'auto',
border: '1px solid var(--gray-800)',
}}>
<div><span style={{ color: '#7A7367' }}>$</span> <span style={{ color: '#EFC59E' }}>hermes</span> cron status daily-summary</div>
<div style={{ marginTop: 4 }}>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>last_run</span>: <span>2026-04-25T09:28:14Z</span><br/>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>duration</span>: <span>14.2s</span><br/>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>exit_code</span>: <span>0</span><br/>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>tokens_used</span>: <span>1,847</span><br/>
<span style={{ color: '#A39C92' }}>next_run</span>: <span>2026-04-26T09:00:00Z</span>
</div>
</div>
);
}
// read
return (
<div style={{
background: 'var(--bg-card)', borderRadius: 7,
padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
marginTop: 6, lineHeight: 1.6, color: 'var(--fg-muted)',
border: '0.5px solid var(--border)', maxHeight: 120, overflow: 'auto',
}}>
<div><span style={{ color: 'var(--fg-faint)' }}>1</span> &#123;</div>
<div><span style={{ color: 'var(--fg-faint)' }}>2</span> "name": "daily-summary",</div>
<div><span style={{ color: 'var(--fg-faint)' }}>3</span> "schedule": "0 9 * * *",</div>
<div><span style={{ color: 'var(--fg-faint)' }}>4</span> "enabled": true</div>
<div><span style={{ color: 'var(--fg-faint)' }}>5</span> &#125;</div>
</div>
);
}
function DiffPreview() {
return (
<div style={{
background: 'var(--bg-card)', borderRadius: 7,
padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
marginTop: 6, lineHeight: 1.6, color: 'var(--fg)',
border: '0.5px solid var(--border)',
}}>
<div><span style={{ color: 'var(--fg-faint)', display: 'inline-block', width: 22 }}>3</span><span> "schedule": "0 9 * * *",</span></div>
<div style={{ background: 'rgba(217, 83, 79, 0.10)' }}>
<span style={{ color: 'var(--red-600)', display: 'inline-block', width: 22 }}>-</span>
<span> "timezone": "UTC",</span>
</div>
<div style={{ background: 'rgba(42, 168, 118, 0.10)' }}>
<span style={{ color: 'var(--green-600)', display: 'inline-block', width: 22 }}>+</span>
<span> "timezone": "America/New_York",</span>
</div>
<div><span style={{ color: 'var(--fg-faint)', display: 'inline-block', width: 22 }}>5</span><span> "enabled": true</span></div>
</div>
);
}
// ─────────────── Suggested replies ───────────────
function SuggestedReplies({ items }) {
return (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 4, paddingLeft: 36 }}>
{items.map(s => (
<button key={s} style={{
fontSize: 12, padding: '5px 10px', borderRadius: 999,
background: 'var(--bg-card)', border: '0.5px solid var(--border-strong)',
color: 'var(--fg)', fontFamily: 'var(--font-sans)', cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 4,
}}>
<i data-lucide="sparkles" style={{ width: 11, height: 11, color: 'var(--accent)' }}></i>
{s}
</button>
))}
</div>
);
}
// ─────────────── Composer ───────────────
const SLASH_COMMANDS = [
{ cmd: 'compress', desc: 'Compress conversation context', icon: 'minimize-2' },
{ cmd: 'clear', desc: 'Clear and start fresh', icon: 'trash-2' },
{ cmd: 'model', desc: 'Switch model', icon: 'cpu' },
{ cmd: 'project', desc: 'Change project', icon: 'folder' },
{ cmd: 'memory', desc: 'Edit AGENTS.md', icon: 'database' },
{ cmd: 'cost', desc: 'Show token / cost report', icon: 'circle-dollar-sign' },
];
function Composer({ open, setOpen }) {
const [text, setText] = React.useState('');
const onChange = e => {
const v = e.currentTarget.innerText;
setText(v);
setOpen(v.trim().startsWith('/'));
};
return (
<div style={{
borderTop: '0.5px solid var(--border)', padding: '12px 24px 14px',
background: 'var(--bg-card)', position: 'relative',
}}>
{open && (
<div style={{
position: 'absolute', bottom: 'calc(100% - 4px)', left: 24, right: 24,
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
borderRadius: 9, boxShadow: 'var(--shadow-lg)', padding: 4, maxWidth: 360,
}}>
<div style={{ padding: '4px 8px 6px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
Slash commands
</div>
{SLASH_COMMANDS.map((c, i) => (
<div key={c.cmd} style={{
display: 'flex', alignItems: 'center', gap: 9, padding: '6px 8px',
borderRadius: 6, fontSize: 13, cursor: 'pointer',
background: i === 0 ? 'var(--accent-tint)' : 'transparent',
color: i === 0 ? 'var(--accent-active)' : 'var(--fg)',
}}>
<i data-lucide={c.icon} style={{ width: 14, height: 14 }}></i>
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>/{c.cmd}</span>
<span style={{ flex: 1, color: 'var(--fg-muted)', fontSize: 12 }}>{c.desc}</span>
{i === 0 && <KbdKey></KbdKey>}
</div>
))}
</div>
)}
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
border: `1px solid ${open ? 'var(--accent)' : 'var(--border-strong)'}`,
borderRadius: 12, padding: '10px 12px',
background: 'var(--bg-card)',
boxShadow: open ? 'var(--shadow-focus)' : 'none',
transition: 'box-shadow 120ms, border-color 120ms',
}}>
{/* Attached context chips */}
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<ContextChip icon="folder" label="scarf" tone="accent" />
<ContextChip icon="file-text" label="cron/jobs.json" />
<ContextChip icon="plus" label="Add context" muted />
</div>
{/* Input */}
<div contentEditable suppressContentEditableWarning onInput={onChange}
style={{
fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none',
color: 'var(--fg)', padding: '2px 0', minHeight: 22, maxHeight: 160, overflowY: 'auto',
lineHeight: 1.5,
}}
data-placeholder="Message Hermes… / for commands · @ for files"></div>
{/* Footer row */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<ComposerChip icon="paperclip" label="" />
<ComposerChip icon="at-sign" label="@" />
<ComposerChip icon="image" label="" />
<Divider vertical />
<ComposerChip icon="cpu" label="sonnet-4.5" />
<ComposerChip icon="folder" label="scarf" />
<div style={{ flex: 1 }}></div>
<span style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>
send · newline
</span>
<button style={{
width: 30, height: 30, borderRadius: 8, background: 'var(--accent)',
color: '#fff', border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 1px 2px rgba(122, 46, 20, 0.3)',
}}>
<i data-lucide="arrow-up" style={{ width: 15, height: 15 }}></i>
</button>
</div>
</div>
</div>
);
}
function ContextChip({ icon, label, tone, muted }) {
return (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
padding: '2px 8px', borderRadius: 999, fontSize: 11, fontWeight: 500,
background: tone === 'accent' ? 'var(--accent-tint)' : 'var(--bg-quaternary)',
color: tone === 'accent' ? 'var(--accent-active)' : (muted ? 'var(--fg-muted)' : 'var(--fg)'),
fontFamily: tone === 'accent' ? 'var(--font-sans)' : 'var(--font-mono)',
border: muted ? '0.5px dashed var(--border-strong)' : 'none',
cursor: muted ? 'pointer' : 'default',
}}>
<i data-lucide={icon} style={{ width: 11, height: 11 }}></i>{label}
</div>
);
}
function ComposerChip({ icon, label }) {
const [hover, setHover] = React.useState(false);
return (
<button onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
display: 'inline-flex', alignItems: 'center', gap: 4,
padding: label ? '3px 7px' : '4px', borderRadius: 6, fontSize: 12,
background: hover ? 'var(--bg-quaternary)' : 'transparent',
color: 'var(--fg-muted)', border: 'none', cursor: 'pointer',
fontFamily: 'var(--font-mono)',
}}>
<i data-lucide={icon} style={{ width: 13, height: 13 }}></i>{label}
</button>
);
}
// ─────────────── Pane 3 — Inspector ───────────────
function Inspector({ focused }) {
const [tab, setTab] = React.useState('details');
// Find the focused tool call. For demo, hard-code tc-2 details.
const FOCUS_DATA = {
'tc-1': { kind: 'read', name: 'read_file', arg: '~/.scarf/cron/jobs.json',
duration: '86 ms', startedAt: '09:42:18.214', tokens: 412 },
'tc-2': { kind: 'execute', name: 'execute', arg: 'hermes cron status daily-summary',
duration: '1.4 s', startedAt: '09:42:18.302', tokens: 86,
cwd: '~/.scarf', exitCode: 0 },
'tc-3': { kind: 'read', name: 'read_file', arg: '~/.scarf/cron/output/daily-summary.md',
duration: '42 ms', startedAt: '09:43:01.190', tokens: 1284 },
'tc-4': { kind: 'edit', name: 'apply_patch', arg: '~/.scarf/cron/jobs.json',
duration: '120 ms', startedAt: '09:43:03.910', tokens: 88, linesAdded: 1, linesRemoved: 1 },
};
const data = FOCUS_DATA[focused.id] || FOCUS_DATA['tc-2'];
const t = TOOL_TONES[data.kind];
return (
<aside style={{
width: 320, borderLeft: '0.5px solid var(--border)',
background: 'var(--bg-card)', display: 'flex', flexDirection: 'column',
}}>
{/* Header */}
<div style={{ padding: '14px 16px 10px', borderBottom: '0.5px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<div style={{
width: 24, height: 24, borderRadius: 6,
background: t.tint, color: t.color,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide={t.icon} style={{ width: 13, height: 13 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: t.color,
textTransform: 'uppercase', letterSpacing: '0.05em' }}>{t.label} call</div>
<div style={{ fontSize: 13, fontWeight: 600, fontFamily: 'var(--font-mono)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{data.name}</div>
</div>
<IconBtn icon="x" tooltip="Close inspector" />
</div>
<Tabs value={tab} onChange={setTab} options={[
{ value: 'details', label: 'Details', icon: 'info' },
{ value: 'output', label: 'Output', icon: 'terminal' },
{ value: 'raw', label: 'Raw', icon: 'braces' },
]} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
{tab === 'details' && <InspectorDetails data={data} t={t} />}
{tab === 'output' && <InspectorOutput data={data} t={t} />}
{tab === 'raw' && <InspectorRaw data={data} />}
</div>
{/* Footer */}
<div style={{ padding: '10px 16px', borderTop: '0.5px solid var(--border)',
display: 'flex', gap: 6 }}>
<Btn size="sm" kind="secondary" icon="rotate-cw" fullWidth>Re-run</Btn>
<Btn size="sm" kind="ghost" icon="copy">Copy</Btn>
</div>
</aside>
);
}
function InspectorDetails({ data, t }) {
return (
<div>
<Section title="Status">
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px',
background: 'var(--green-100)', borderRadius: 7,
border: '0.5px solid rgba(42, 168, 118, 0.25)' }}>
<i data-lucide="check-circle-2" style={{ width: 16, height: 16, color: 'var(--green-600)' }}></i>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--green-600)' }}>Completed</div>
<div style={{ fontSize: 11, color: 'var(--fg-muted)' }}>Exit 0 · No errors</div>
</div>
</div>
</Section>
<div style={{ marginTop: 18 }}>
<Section title="Arguments">
<div style={{
background: 'var(--bg-quaternary)', borderRadius: 7, padding: '8px 10px',
fontFamily: 'var(--font-mono)', fontSize: 11.5, lineHeight: 1.5,
color: 'var(--fg)', wordBreak: 'break-all',
}}>{data.arg}</div>
</Section>
</div>
<div style={{ marginTop: 18 }}>
<Section title="Telemetry">
<KV k="Started" v={data.startedAt} mono />
<KV k="Duration" v={data.duration} mono />
<KV k="Tokens" v={data.tokens.toLocaleString()} mono />
{data.exitCode != null && <KV k="Exit code" v={data.exitCode} mono color="var(--green-600)" />}
{data.cwd && <KV k="CWD" v={data.cwd} mono />}
{data.linesAdded != null && (
<KV k="Diff" v={
<span style={{ fontFamily: 'var(--font-mono)' }}>
<span style={{ color: 'var(--green-600)' }}>+{data.linesAdded}</span>
<span style={{ color: 'var(--fg-faint)' }}> / </span>
<span style={{ color: 'var(--red-600)' }}>{data.linesRemoved}</span>
</span>
} />
)}
</Section>
</div>
<div style={{ marginTop: 18 }}>
<Section title="Permissions" hint="Tool gateway policy applied at run time">
<div style={{
background: 'var(--bg-quaternary)', borderRadius: 7, padding: '10px',
fontSize: 12, color: 'var(--fg-muted)', display: 'flex', flexDirection: 'column', gap: 6,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<i data-lucide="shield-check" style={{ width: 13, height: 13, color: 'var(--green-500)' }}></i>
<span>Allowed by <code style={inlineCode}>scarf-default</code> profile</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<i data-lucide="check" style={{ width: 13, height: 13, color: 'var(--green-500)' }}></i>
<span>No human approval required</span>
</div>
</div>
</Section>
</div>
</div>
);
}
function InspectorOutput({ data, t }) {
return (
<div>
<Section title="stdout" right={<KbdKey>C</KbdKey>}>
<div style={{
background: 'var(--gray-900)', color: '#E8E1D2', borderRadius: 7,
padding: '10px 12px', fontFamily: 'var(--font-mono)', fontSize: 11,
lineHeight: 1.6, overflow: 'auto',
}}>
<div><span style={{ color: '#7A7367' }}>$</span> <span style={{ color: '#EFC59E' }}>hermes</span> cron status daily-summary</div>
<div style={{ marginTop: 6 }}>
<span style={{ color: '#2AA876' }}></span> last_run: 2026-04-25T09:28:14Z<br/>
<span style={{ color: '#2AA876' }}></span> duration: 14.2s<br/>
<span style={{ color: '#2AA876' }}></span> exit_code: 0<br/>
<span style={{ color: '#2AA876' }}></span> tokens_used: 1,847<br/>
next_run: 2026-04-26T09:00:00Z<br/>
schedule: 0 9 * * *<br/>
timezone: America/New_York
</div>
</div>
</Section>
<div style={{ marginTop: 16 }}>
<Section title="stderr">
<div style={{ background: 'var(--bg-quaternary)', borderRadius: 7, padding: '10px',
fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--fg-faint)' }}>
(empty)
</div>
</Section>
</div>
</div>
);
}
function InspectorRaw({ data }) {
return (
<div style={{
background: 'var(--gray-900)', color: '#E8E1D2', borderRadius: 7,
padding: '12px', fontFamily: 'var(--font-mono)', fontSize: 11,
lineHeight: 1.55,
}}>
{`{
"id": "${data.kind === 'execute' ? 'tc-2' : 'tc-x'}",
"type": "tool_use",
"name": "${data.name}",
"input": {
"command": "hermes cron status daily-summary",
"cwd": "~/.scarf"
},
"result": {
"exit_code": 0,
"duration_ms": 1402,
"stdout_bytes": 287
}
}`}
</div>
);
}
function KV({ k, v, mono, color }) {
return (
<div style={{ display: 'flex', alignItems: 'center', padding: '5px 0',
borderBottom: '0.5px solid var(--border)' }}>
<span style={{ fontSize: 12, color: 'var(--fg-muted)', flex: '0 0 90px' }}>{k}</span>
<span style={{
fontSize: 12, color: color || 'var(--fg)',
fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', flex: 1, textAlign: 'right',
}}>{v}</span>
</div>
);
}
window.Chat = Chat;
+550
View File
@@ -0,0 +1,550 @@
// Scarf v2 shared components — calmer density, full state matrices.
// Exports to window: Btn, IconBtn, Pill, Dot, Card, StatCard, Section, ContentHeader,
// Field, TextInput, NumberInput, TextArea, Toggle, Checkbox, Radio, RadioGroup,
// Segmented, Select, SettingsGroup, SettingsRow, Tabs, Menu, MenuItem, Divider,
// EmptyState, KbdKey, HelpIcon, Tooltip, Avatar, ProgressBar, Spinner.
const SF = "var(--font-sans)";
// ─────────────── ContentHeader ───────────────
function ContentHeader({ title, subtitle, actions, right, breadcrumb }) {
return (
<div style={{
padding: '24px 32px 22px',
borderBottom: '0.5px solid var(--border)',
background: 'var(--bg-card)',
}}>
{breadcrumb && (
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginBottom: 6 }}>{breadcrumb}</div>
)}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 16 }}>
<div style={{ flex: 1 }}>
<div className="scarf-h2" style={{ marginBottom: subtitle ? 6 : 0 }}>{title}</div>
{subtitle && <div style={{ fontSize: 14, color: 'var(--fg-muted)', maxWidth: 600 }}>{subtitle}</div>}
</div>
{right}
{actions && <div style={{ display: 'flex', gap: 8 }}>{actions}</div>}
</div>
</div>
);
}
// ─────────────── Buttons ───────────────
function Btn({ kind = 'secondary', size = 'md', icon, iconRight, children, onClick, disabled, loading, fullWidth, type = 'button' }) {
const sizes = {
sm: { padding: '5px 11px', fontSize: 12, gap: 5, iconSize: 13 },
md: { padding: '7px 14px', fontSize: 13, gap: 6, iconSize: 14 },
lg: { padding: '10px 18px', fontSize: 14, gap: 7, iconSize: 16 },
};
const kinds = {
primary: { background: 'var(--accent)', color: 'var(--on-accent)', border: '1px solid transparent', shadow: '0 1px 0 rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.18)' },
secondary: { background: 'var(--bg-card)', color: 'var(--fg)', border: '1px solid var(--border-strong)', shadow: 'var(--shadow-sm)' },
ghost: { background: 'transparent', color: 'var(--fg)', border: '1px solid transparent' },
danger: { background: 'var(--bg-card)', color: 'var(--red-600)', border: '1px solid var(--red-500)' },
'danger-solid': { background: 'var(--red-500)', color: '#fff', border: '1px solid transparent' },
accent: { background: 'var(--accent-tint)', color: 'var(--accent-active)', border: '1px solid transparent' },
};
const s = sizes[size];
const k = kinds[kind];
const [hover, setHover] = React.useState(false);
const hoverStyle = !disabled && hover ? {
primary: { background: 'var(--accent-hover)' },
secondary: { background: 'var(--gray-50)', borderColor: 'var(--accent)' },
ghost: { background: 'var(--bg-quaternary)' },
danger: { background: 'var(--red-100)' },
'danger-solid': { background: 'var(--red-600)' },
accent: { background: 'var(--accent-tint-strong)' },
}[kind] : {};
return (
<button type={type} onClick={onClick} disabled={disabled || loading}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
padding: s.padding, fontSize: s.fontSize, gap: s.gap,
...k, ...hoverStyle, boxShadow: k.shadow,
borderRadius: 8, fontFamily: SF, fontWeight: 500,
display: fullWidth ? 'flex' : 'inline-flex', alignItems: 'center', justifyContent: 'center',
cursor: (disabled || loading) ? 'default' : 'pointer',
opacity: disabled ? 0.45 : 1,
width: fullWidth ? '100%' : 'auto',
transition: 'all 120ms var(--ease-smooth)',
whiteSpace: 'nowrap', userSelect: 'none',
}}>
{loading
? <Spinner size={s.iconSize} color={kind === 'primary' ? 'rgba(255,255,255,0.7)' : 'currentColor'} />
: icon && <i data-lucide={icon} style={{ width: s.iconSize, height: s.iconSize }}></i>}
{children}
{iconRight && <i data-lucide={iconRight} style={{ width: s.iconSize, height: s.iconSize, opacity: 0.7 }}></i>}
</button>
);
}
function IconBtn({ icon, onClick, size = 28, tooltip, active, disabled }) {
const [hover, setHover] = React.useState(false);
return (
<button onClick={onClick} disabled={disabled} title={tooltip}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
width: size, height: size, padding: 0, borderRadius: 7,
background: active ? 'var(--accent-tint)' : (hover && !disabled ? 'var(--bg-quaternary)' : 'transparent'),
color: active ? 'var(--accent-active)' : 'var(--fg-muted)',
border: 'none', cursor: disabled ? 'default' : 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
opacity: disabled ? 0.45 : 1, transition: 'background 120ms',
}}>
<i data-lucide={icon} style={{ width: Math.round(size * 0.55), height: Math.round(size * 0.55) }}></i>
</button>
);
}
function Spinner({ size = 14, color = 'currentColor' }) {
return (
<span style={{
display: 'inline-block', width: size, height: size,
border: `2px solid transparent`, borderTopColor: color, borderRightColor: color,
borderRadius: '50%', animation: 'scarfSpin 0.8s linear infinite',
}}></span>
);
}
// ─────────────── Pills / Dots ───────────────
function Pill({ tone = 'gray', dot, icon, children, size = 'md' }) {
const tones = {
gray: { bg: 'var(--bg-quaternary)', fg: 'var(--fg-muted)', dotc: 'var(--gray-500)' },
green: { bg: 'var(--green-100)', fg: 'var(--green-600)', dotc: 'var(--green-500)' },
red: { bg: 'var(--red-100)', fg: 'var(--red-600)', dotc: 'var(--red-500)' },
orange: { bg: 'var(--orange-100)', fg: '#A8741F', dotc: 'var(--orange-500)' },
blue: { bg: 'var(--blue-100)', fg: '#1F70A8', dotc: 'var(--blue-500)' },
accent: { bg: 'var(--accent-tint)', fg: 'var(--accent-active)', dotc: 'var(--accent)' },
amber: { bg: 'var(--orange-100)', fg: '#A8741F', dotc: 'var(--orange-500)' },
purple: { bg: '#EFE0F8', fg: '#5E4080', dotc: '#7E5BA9' },
idle: { bg: 'var(--bg-quaternary)', fg: 'var(--fg-faint)', dotc: 'var(--gray-400)' },
};
const t = tones[tone];
const sizes = { sm: { p: '2px 7px', f: 10 }, md: { p: '3px 9px', f: 11 }, lg: { p: '4px 11px', f: 12 } };
const sz = sizes[size];
return (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
fontSize: sz.f, fontWeight: 600, padding: sz.p, borderRadius: 999,
background: t.bg, color: t.fg, fontFamily: SF, lineHeight: 1.4,
}}>
{dot && <span style={{ width: 6, height: 6, borderRadius: '50%', background: t.dotc }}></span>}
{icon && <i data-lucide={icon} style={{ width: 11, height: 11 }}></i>}
{children}
</span>
);
}
function Dot({ tone = 'gray', size = 8 }) {
const tones = { gray: 'var(--gray-400)', green: 'var(--green-500)', red: 'var(--red-500)',
orange: 'var(--orange-500)', blue: 'var(--blue-500)', accent: 'var(--accent)' };
return <span style={{ width: size, height: size, borderRadius: '50%',
background: tones[tone], display: 'inline-block', flexShrink: 0 }}></span>;
}
// ─────────────── Cards / Sections ───────────────
function Card({ children, padding = 18, style = {}, onClick, interactive }) {
return (
<div onClick={onClick} style={{
background: 'var(--bg-card)', borderRadius: 10,
border: '0.5px solid var(--border)',
boxShadow: 'var(--shadow-sm)',
padding, cursor: onClick || interactive ? 'pointer' : 'default',
transition: 'all 160ms var(--ease-smooth)',
...style,
}}>{children}</div>
);
}
function StatCard({ label, value, sub, accent, icon }) {
return (
<Card padding={16} style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11,
color: 'var(--fg-muted)', fontWeight: 600, marginBottom: 8,
textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{icon && <i data-lucide={icon} style={{ width: 12, height: 12 }}></i>}
{label}
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 600,
color: accent || 'var(--fg)', letterSpacing: '-0.01em', lineHeight: 1.1 }}>{value}</div>
{sub && <div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 6 }}>{sub}</div>}
</Card>
);
}
function Section({ title, hint, right, children, gap = 12 }) {
return (
<div>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: gap, gap: 10 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--fg-muted)',
textTransform: 'uppercase', letterSpacing: '0.06em' }}>{title}</div>
{hint && <div style={{ fontSize: 12, color: 'var(--fg-faint)' }}>{hint}</div>}
<div style={{ marginLeft: 'auto' }}>{right}</div>
</div>
{children}
</div>
);
}
function Divider({ vertical, label }) {
if (vertical) return <div style={{ width: 1, alignSelf: 'stretch', background: 'var(--border)' }}></div>;
if (label) return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, color: 'var(--fg-faint)', margin: '8px 0' }}>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
<span style={{ fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em' }}>{label}</span>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
</div>
);
return <div style={{ height: 1, background: 'var(--border)', margin: '8px 0' }}></div>;
}
// ─────────────── Form fields ───────────────
function Field({ label, hint, error, help, children, required, inline }) {
return (
<label style={{ display: 'flex', flexDirection: inline ? 'row' : 'column',
gap: inline ? 12 : 6, fontFamily: SF, alignItems: inline ? 'center' : 'stretch' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5,
minWidth: inline ? 140 : 0 }}>
<span style={{ fontSize: 13, color: 'var(--fg)', fontWeight: 500 }}>{label}</span>
{required && <span style={{ color: 'var(--red-500)', fontSize: 11 }}>*</span>}
{help && <HelpIcon text={help} />}
</div>
<div style={{ flex: inline ? 1 : 'none', display: 'flex', flexDirection: 'column', gap: 4 }}>
{children}
{error
? <span style={{ fontSize: 11, color: 'var(--red-600)', display: 'flex', alignItems: 'center', gap: 4 }}>
<i data-lucide="alert-circle" style={{ width: 11, height: 11 }}></i>{error}
</span>
: hint && <span style={{ fontSize: 11, color: 'var(--fg-faint)' }}>{hint}</span>
}
</div>
</label>
);
}
function HelpIcon({ text }) {
return (
<span title={text} style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 14, height: 14, borderRadius: '50%', background: 'var(--bg-tertiary)',
color: 'var(--fg-muted)', cursor: 'help',
}}>
<i data-lucide="help-circle" style={{ width: 11, height: 11 }}></i>
</span>
);
}
function inputStyle(invalid) {
return {
fontFamily: SF, fontSize: 13, padding: '7px 11px',
border: `1px solid ${invalid ? 'var(--red-500)' : 'var(--border-strong)'}`,
borderRadius: 7, background: 'var(--bg-card)', color: 'var(--fg)',
outline: 'none', transition: 'all 120ms', width: '100%', boxSizing: 'border-box',
};
}
function TextInput({ value, onChange, placeholder, mono, invalid, leftIcon, rightSlot, type = 'text' }) {
const [v, setV] = React.useState(value ?? '');
React.useEffect(() => setV(value ?? ''), [value]);
const ref = React.useRef();
return (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
{leftIcon && <i data-lucide={leftIcon} style={{
position: 'absolute', left: 10, width: 14, height: 14, color: 'var(--fg-faint)', pointerEvents: 'none'
}}></i>}
<input ref={ref} type={type} value={v}
onChange={e => { setV(e.target.value); onChange && onChange(e.target.value); }}
placeholder={placeholder}
style={{ ...inputStyle(invalid),
fontFamily: mono ? 'var(--font-mono)' : SF,
paddingLeft: leftIcon ? 32 : 11,
paddingRight: rightSlot ? 36 : 11,
}}
onFocus={e => { if (!invalid) { e.target.style.borderColor = 'var(--accent)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}}
onBlur={e => { e.target.style.borderColor = invalid ? 'var(--red-500)' : 'var(--border-strong)'; e.target.style.boxShadow = 'none'; }}
/>
{rightSlot && <div style={{ position: 'absolute', right: 6 }}>{rightSlot}</div>}
</div>
);
}
function TextArea({ value, onChange, placeholder, rows = 3, invalid, mono }) {
const [v, setV] = React.useState(value ?? '');
React.useEffect(() => setV(value ?? ''), [value]);
return (
<textarea value={v} rows={rows} placeholder={placeholder}
onChange={e => { setV(e.target.value); onChange && onChange(e.target.value); }}
style={{ ...inputStyle(invalid), resize: 'vertical', lineHeight: 1.45,
fontFamily: mono ? 'var(--font-mono)' : SF }}
onFocus={e => { if (!invalid) { e.target.style.borderColor = 'var(--accent)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}}
onBlur={e => { e.target.style.borderColor = invalid ? 'var(--red-500)' : 'var(--border-strong)'; e.target.style.boxShadow = 'none'; }}
/>
);
}
function Select({ value, onChange, options }) {
const [v, setV] = React.useState(value ?? options?.[0]?.value ?? '');
React.useEffect(() => setV(value ?? ''), [value]);
return (
<div style={{ position: 'relative', display: 'flex' }}>
<select value={v} onChange={e => { setV(e.target.value); onChange && onChange(e.target.value); }}
style={{ ...inputStyle(), appearance: 'none', paddingRight: 30, cursor: 'pointer' }}>
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
<i data-lucide="chevrons-up-down" style={{
position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
width: 13, height: 13, color: 'var(--fg-muted)', pointerEvents: 'none',
}}></i>
</div>
);
}
// ─────────────── Toggle / Checkbox / Radio ───────────────
function Toggle({ on, onChange, size = 'md', disabled }) {
const sizes = { sm: { w: 28, h: 16, p: 12 }, md: { w: 36, h: 20, p: 16 }, lg: { w: 44, h: 24, p: 20 } };
const s = sizes[size];
return (
<div onClick={() => !disabled && onChange && onChange(!on)} style={{
width: s.w, height: s.h, borderRadius: 999, position: 'relative',
cursor: disabled ? 'default' : 'pointer', flexShrink: 0,
background: on ? 'var(--accent)' : 'var(--gray-300)',
transition: 'background 180ms var(--ease-smooth)',
opacity: disabled ? 0.5 : 1,
}}>
<div style={{
position: 'absolute', top: 2, left: on ? (s.w - s.p - 2) : 2,
width: s.p, height: s.p, borderRadius: '50%', background: '#fff',
boxShadow: '0 1px 3px rgba(0,0,0,0.18), 0 1px 1px rgba(0,0,0,0.06)',
transition: 'left 180ms var(--ease-smooth)',
}}></div>
</div>
);
}
function Checkbox({ checked, onChange, indeterminate, disabled }) {
return (
<div onClick={() => !disabled && onChange && onChange(!checked)} style={{
width: 16, height: 16, borderRadius: 4,
background: checked || indeterminate ? 'var(--accent)' : 'var(--bg-card)',
border: `1px solid ${checked || indeterminate ? 'var(--accent)' : 'var(--border-strong)'}`,
cursor: disabled ? 'default' : 'pointer', flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 120ms', opacity: disabled ? 0.5 : 1,
}}>
{checked && <i data-lucide="check" style={{ width: 12, height: 12, color: '#fff', strokeWidth: 3 }}></i>}
{indeterminate && !checked && <div style={{ width: 8, height: 2, background: '#fff', borderRadius: 1 }}></div>}
</div>
);
}
function Radio({ checked, onChange, disabled }) {
return (
<div onClick={() => !disabled && onChange && onChange(true)} style={{
width: 16, height: 16, borderRadius: '50%',
background: 'var(--bg-card)',
border: `1px solid ${checked ? 'var(--accent)' : 'var(--border-strong)'}`,
cursor: disabled ? 'default' : 'pointer', flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 120ms', opacity: disabled ? 0.5 : 1,
}}>
{checked && <div style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--accent)' }}></div>}
</div>
);
}
// ─────────────── Segmented / Tabs ───────────────
function Segmented({ value, onChange, options, size = 'md' }) {
const padding = size === 'sm' ? '4px 10px' : '6px 14px';
const fontSize = size === 'sm' ? 12 : 13;
return (
<div style={{
display: 'inline-flex', padding: 2, borderRadius: 8,
background: 'var(--bg-quaternary)', border: '0.5px solid var(--border)',
}}>
{options.map(o => {
const active = value === o.value;
return (
<button key={o.value} onClick={() => onChange && onChange(o.value)} style={{
padding, fontSize, fontWeight: active ? 600 : 500, fontFamily: SF,
background: active ? 'var(--bg-card)' : 'transparent',
color: active ? 'var(--fg)' : 'var(--fg-muted)',
border: 'none', borderRadius: 6, cursor: 'pointer',
boxShadow: active ? 'var(--shadow-sm)' : 'none',
transition: 'all 120ms var(--ease-smooth)', display: 'inline-flex', alignItems: 'center', gap: 5,
}}>
{o.icon && <i data-lucide={o.icon} style={{ width: 12, height: 12 }}></i>}
{o.label}
{o.count != null && <span style={{
fontSize: 10, fontFamily: 'var(--font-mono)',
padding: '1px 6px', borderRadius: 999,
background: active ? 'var(--accent-tint)' : 'var(--bg-tertiary)',
color: active ? 'var(--accent-active)' : 'var(--fg-muted)',
}}>{o.count}</span>}
</button>
);
})}
</div>
);
}
function Tabs({ value, onChange, options }) {
return (
<div style={{ display: 'flex', gap: 2, borderBottom: '0.5px solid var(--border)' }}>
{options.map(o => {
const active = value === o.value;
return (
<button key={o.value} onClick={() => onChange && onChange(o.value)} style={{
padding: '10px 14px', fontSize: 13, fontWeight: 500, fontFamily: SF,
background: 'transparent', border: 'none',
color: active ? 'var(--fg)' : 'var(--fg-muted)',
borderBottom: `2px solid ${active ? 'var(--accent)' : 'transparent'}`,
marginBottom: -1, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6,
transition: 'color 120ms',
}}>
{o.icon && <i data-lucide={o.icon} style={{ width: 13, height: 13 }}></i>}
{o.label}
{o.count != null && <span style={{
fontSize: 10, fontFamily: 'var(--font-mono)',
padding: '1px 6px', borderRadius: 999,
background: 'var(--bg-tertiary)', color: 'var(--fg-muted)',
}}>{o.count}</span>}
</button>
);
})}
</div>
);
}
// ─────────────── Settings groups (card-rows) ───────────────
function SettingsGroup({ title, description, children }) {
return (
<div style={{ marginBottom: 28 }}>
{title && <div style={{ marginBottom: 10 }}>
<div style={{ fontSize: 13, fontWeight: 600 }}>{title}</div>
{description && <div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{description}</div>}
</div>}
<div style={{
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
borderRadius: 10, overflow: 'hidden',
}}>{children}</div>
</div>
);
}
function SettingsRow({ title, description, control, icon, last }) {
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
{icon && <div style={{
width: 32, height: 32, borderRadius: 7, background: 'var(--accent-tint)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent)', flexShrink: 0,
}}><i data-lucide={icon} style={{ width: 16, height: 16 }}></i></div>}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{title}</div>
{description && <div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{description}</div>}
</div>
<div style={{ flexShrink: 0 }}>{control}</div>
</div>
);
}
// ─────────────── Menu / dropdown ───────────────
function Menu({ children, anchor = 'bottom-left', style = {} }) {
const positions = {
'bottom-left': { top: '100%', left: 0, marginTop: 4 },
'bottom-right': { top: '100%', right: 0, marginTop: 4 },
'top-left': { bottom: '100%', left: 0, marginBottom: 4 },
};
return (
<div style={{
position: 'absolute', zIndex: 200, ...positions[anchor],
minWidth: 200, padding: 4, background: 'var(--bg-card)',
border: '0.5px solid var(--border)', borderRadius: 9,
boxShadow: 'var(--shadow-lg)', fontFamily: SF, ...style,
}}>
{children}
</div>
);
}
function MenuItem({ icon, label, kbd, onClick, danger, selected, children }) {
const [hover, setHover] = React.useState(false);
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px',
borderRadius: 6, cursor: 'pointer', fontSize: 13,
background: hover ? 'var(--accent-tint)' : 'transparent',
color: danger ? 'var(--red-600)' : (hover ? 'var(--accent-active)' : 'var(--fg)'),
}}>
{icon && <i data-lucide={icon} style={{ width: 14, height: 14 }}></i>}
<span style={{ flex: 1 }}>{label || children}</span>
{selected && <i data-lucide="check" style={{ width: 13, height: 13 }}></i>}
{kbd && <KbdKey>{kbd}</KbdKey>}
</div>
);
}
function KbdKey({ children }) {
return <span style={{
fontFamily: 'var(--font-mono)', fontSize: 10,
padding: '1px 5px', borderRadius: 3,
background: 'var(--bg-quaternary)', border: '0.5px solid var(--border)',
color: 'var(--fg-muted)',
}}>{children}</span>;
}
// ─────────────── Avatar ───────────────
function Avatar({ initials, size = 28, color = 'var(--accent)' }) {
return (
<div style={{
width: size, height: size, borderRadius: '50%', background: color,
color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
fontSize: Math.round(size * 0.4), fontWeight: 600, flexShrink: 0,
}}>{initials}</div>
);
}
// ─────────────── ProgressBar ───────────────
function ProgressBar({ value = 0, color = 'var(--accent)', height = 6 }) {
return (
<div style={{ height, background: 'var(--bg-quaternary)', borderRadius: height / 2, overflow: 'hidden' }}>
<div style={{ width: `${Math.min(100, Math.max(0, value))}%`, height: '100%',
background: color, borderRadius: height / 2, transition: 'width 240ms var(--ease-smooth)' }}></div>
</div>
);
}
// ─────────────── Empty ───────────────
function EmptyState({ icon, title, body, action }) {
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', padding: 80, textAlign: 'center', gap: 12 }}>
<div style={{
width: 64, height: 64, borderRadius: 16, background: 'var(--accent-tint)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--accent)', marginBottom: 4,
}}>
<i data-lucide={icon || 'inbox'} style={{ width: 28, height: 28 }}></i>
</div>
<div style={{ fontSize: 17, fontWeight: 600 }}>{title}</div>
<div style={{ fontSize: 13, color: 'var(--fg-muted)', maxWidth: 380, lineHeight: 1.5 }}>{body}</div>
{action && <div style={{ marginTop: 8 }}>{action}</div>}
</div>
);
}
Object.assign(window, {
ContentHeader, Btn, IconBtn, Spinner, Pill, Dot,
Card, StatCard, Section, Divider,
Field, HelpIcon, TextInput, TextArea, Select,
Toggle, Checkbox, Radio,
Segmented, Tabs,
SettingsGroup, SettingsRow,
Menu, MenuItem, KbdKey,
Avatar, ProgressBar, EmptyState,
});
+165
View File
@@ -0,0 +1,165 @@
// Cron — scheduled agent runs, with run history and a calendar heat strip.
const CRON_JOBS = [
{ id: 'daily-summary', name: 'Daily standup summary', schedule: '0 9 * * 1-5', cronText: 'Weekdays at 9:00am', enabled: true,
lastRun: '2h ago', lastStatus: 'ok', avgDuration: '38s', nextRun: 'tomorrow 9:00am',
personality: 'Hermes', desc: 'Read yesterday\'s commits + Linear updates and post a summary to #standup.', runs7d: 5 },
{ id: 'incident-triage', name: 'Incident triage', schedule: '*/15 * * * *', cronText: 'Every 15 minutes', enabled: true,
lastRun: '3m ago', lastStatus: 'ok', avgDuration: '4.2s', nextRun: 'in 12m',
personality: 'Forge', desc: 'Poll Sentry for unresolved high-severity issues and create Linear tickets.', runs7d: 672 },
{ id: 'design-review', name: 'Friday design review prep', schedule: '0 16 * * 4', cronText: 'Thursdays at 4:00pm', enabled: true,
lastRun: 'yesterday', lastStatus: 'ok', avgDuration: '2m 14s', nextRun: 'Thursday 4:00pm',
personality: 'Atlas', desc: 'Collect new Figma frames + recent PRs, draft an agenda for the design review.', runs7d: 1 },
{ id: 'docs-stale', name: 'Find stale docs', schedule: '0 0 * * 0', cronText: 'Sundays at midnight', enabled: false,
lastRun: '8d ago', lastStatus: 'skipped', avgDuration: '47s', nextRun: 'paused',
personality: 'Hermes', desc: 'Scan the docs site for pages not updated in >90 days; open a checklist.', runs7d: 0 },
{ id: 'release-notes', name: 'Draft release notes', schedule: '0 14 * * 5', cronText: 'Fridays at 2:00pm', enabled: true,
lastRun: '6d ago', lastStatus: 'failed', avgDuration: '1m 03s', nextRun: 'Friday 2:00pm',
personality: 'Atlas', desc: 'Walk merged PRs since last tag; group by area; write user-facing release notes.', runs7d: 1 },
];
const RUN_HISTORY = [
{ when: '2h ago', status: 'ok', duration: '36s', ts: '2026-04-25 09:00:14' },
{ when: 'yesterday', status: 'ok', duration: '41s', ts: '2026-04-24 09:00:08' },
{ when: '2d ago', status: 'ok', duration: '38s', ts: '2026-04-23 09:00:11' },
{ when: '3d ago', status: 'ok', duration: '34s', ts: '2026-04-22 09:00:06' },
{ when: '4d ago', status: 'failed', duration: '12s', ts: '2026-04-21 09:00:09', error: 'github: 502 bad gateway' },
{ when: '5d ago', status: 'ok', duration: '40s', ts: '2026-04-18 09:00:12' },
{ when: '6d ago', status: 'ok', duration: '37s', ts: '2026-04-17 09:00:09' },
];
function Cron() {
const [active, setActive] = React.useState('daily-summary');
const job = CRON_JOBS.find(j => j.id === active);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Cron"
subtitle="Scheduled agent runs. Each job invokes a personality with a fixed prompt."
actions={<><Btn icon="calendar">Timezone: PT</Btn><Btn kind="primary" icon="plus">New cron job</Btn></>} />
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
<div style={{ width: 360, borderRight: '0.5px solid var(--border)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-card)' }}>
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
{CRON_JOBS.map(j => <CronRow key={j.id} j={j} active={j.id === active} onClick={() => setActive(j.id)} />)}
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', background: 'var(--bg)', padding: '24px 32px' }}>
<CronDetail job={job} />
</div>
</div>
</div>
);
}
function CronRow({ j, active, onClick }) {
const [hover, setHover] = React.useState(false);
const tone = j.lastStatus === 'failed' ? 'red' : j.lastStatus === 'skipped' ? 'gray' : 'green';
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
padding: '11px 12px', borderRadius: 7, cursor: 'pointer', marginBottom: 2,
background: active ? 'var(--accent-tint)' : (hover ? 'var(--bg-quaternary)' : 'transparent'),
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
<i data-lucide="clock" style={{ width: 13, height: 13, color: 'var(--fg-muted)', flexShrink: 0 }}></i>
<div style={{ flex: 1, fontSize: 13, fontWeight: 500,
color: active ? 'var(--accent-active)' : 'var(--fg)' }}>{j.name}</div>
{!j.enabled && <Pill tone="gray" size="sm">paused</Pill>}
<Dot tone={tone} />
</div>
<div style={{ display: 'flex', gap: 10, fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>
<span>{j.schedule}</span>
<span style={{ color: 'var(--fg-muted)' }}>· next {j.nextRun}</span>
</div>
</div>
);
}
function CronDetail({ job }) {
return (
<>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, marginBottom: 20 }}>
<div style={{
width: 44, height: 44, borderRadius: 9, background: 'var(--accent-tint)', color: 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide="clock" style={{ width: 22, height: 22 }}></i>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div className="scarf-h2" style={{ fontSize: 22 }}>{job.name}</div>
{job.enabled ? <Pill tone="green" dot>active</Pill> : <Pill tone="gray" dot>paused</Pill>}
</div>
<div style={{ fontSize: 13, color: 'var(--fg-muted)', maxWidth: 520 }}>{job.desc}</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<Btn icon="play">Run now</Btn>
<Toggle on={job.enabled} size="lg" />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 24 }}>
<StatCard label="Schedule" value={job.cronText} sub={job.schedule} />
<StatCard label="Last run" value={job.lastRun} sub={job.lastStatus} />
<StatCard label="Avg duration" value={job.avgDuration} />
<StatCard label="Next run" value={job.nextRun} />
</div>
<SettingsGroup title="Schedule">
<SettingsRow icon="calendar" title="Cron expression"
description={`Parsed as: ${job.cronText} (America/Los_Angeles)`}
control={<TextInput value={job.schedule} mono />} />
<SettingsRow icon="globe" title="Timezone"
description="Job triggers fire in this timezone."
control={<Select value="pt" options={[{ value: 'pt', label: 'America/Los_Angeles' }, { value: 'utc', label: 'UTC' }]} />} />
<SettingsRow icon="hourglass" title="Timeout"
description="Kill the run after this duration."
control={<Select value="5m" options={[
{ value: '1m', label: '1 minute' }, { value: '5m', label: '5 minutes' },
{ value: '15m', label: '15 minutes' }, { value: '1h', label: '1 hour' },
]} />} last />
</SettingsGroup>
<SettingsGroup title="Behavior">
<SettingsRow icon="user-circle" title="Personality"
description={`This job runs as "${job.personality}" with its system prompt + tools.`}
control={<Btn size="sm" icon="external-link">{job.personality}</Btn>} />
<SettingsRow icon="message-square" title="Prompt"
description="The instruction sent to the agent at each scheduled run."
control={<Btn size="sm" icon="edit-3">Edit</Btn>} />
<SettingsRow icon="bell" title="Notify on failure"
description="Send a message to #ops if any run errors out."
control={<Toggle on={true} />} last />
</SettingsGroup>
<SettingsGroup title="Run history" description="Last 7 runs.">
{RUN_HISTORY.map((r, i) => <RunRow key={i} r={r} last={i === RUN_HISTORY.length - 1} />)}
</SettingsGroup>
</>
);
}
function RunRow({ r, last }) {
const tone = r.status === 'failed' ? 'red' : r.status === 'skipped' ? 'gray' : 'green';
const icon = r.status === 'failed' ? 'x' : r.status === 'skipped' ? 'minus' : 'check';
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '10px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<Pill tone={tone} size="sm" icon={icon}>{r.status}</Pill>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12.5, color: 'var(--fg)' }}>{r.when}
<span style={{ color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)', marginLeft: 8, fontSize: 11 }}>{r.ts}</span>
</div>
{r.error && <div style={{ fontSize: 11, color: 'var(--red-500)', fontFamily: 'var(--font-mono)', marginTop: 2 }}>{r.error}</div>}
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)', width: 60, textAlign: 'right' }}>{r.duration}</span>
<Btn size="sm">View log</Btn>
</div>
);
}
window.Cron = Cron;
+117
View File
@@ -0,0 +1,117 @@
// Dashboard — first screen. Mirrors the structure: status header,
// quick stats, recent sessions, recent activity.
function Dashboard() {
return (
<div style={{ padding: '0 0 28px', overflow: 'auto' }}>
<ContentHeader title="Dashboard"
subtitle="At-a-glance status of your Hermes agent"
actions={<><Btn icon="rotate-cw">Refresh</Btn><Btn kind="primary" icon="plus">New Session</Btn></>} />
<div style={{ padding: '20px 28px', display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* Status row */}
<div style={{ display: 'flex', gap: 12 }}>
<StatusCard icon="activity" label="Hermes" value="Running" tone="green" sub="3h 14m uptime" />
<StatusCard icon="cpu" label="Model" value="claude-sonnet-4.5" sub="Anthropic" />
<StatusCard icon="cloud" label="Provider" value="Anthropic" sub="us-east-1 · 18ms" />
<StatusCard icon="network" label="Gateway" value="Connected" tone="green" sub="3 platforms" />
</div>
{/* Stats row */}
<Section title="Last 7 days" right={<Btn size="sm" kind="ghost" icon="bar-chart-3">View Insights</Btn>}>
<div style={{ display: 'flex', gap: 12 }}>
<StatCard label="Sessions" value="847" sub="+12% vs prev" />
<StatCard label="Messages" value="12,394" />
<StatCard label="Tool Calls" value="3,221" />
<StatCard label="Tokens" value="2.4M" sub="1.8M in · 0.6M out" />
<StatCard label="Cost" value="$42.18" accent="var(--accent)" />
</div>
</Section>
{/* Two col */}
<div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: 16 }}>
<Section title="Recent sessions" right={<a style={linkStyle}>View all </a>}>
<Card padding={0}>
<RecentSessionRow project="hermes-blog" message="Draft this week's release notes…" model="haiku-4.5" tokens="1,247" time="14m ago" />
<RecentSessionRow project="scarf" message="Implement the cron diagnostics view" model="sonnet-4.5" tokens="8,392" time="42m ago" />
<RecentSessionRow project="hermes-blog" message="Review the open PRs and summarize" model="sonnet-4.5" tokens="4,108" time="2h ago" />
<RecentSessionRow project="—" message="What model handles function calls best?" model="haiku-4.5" tokens="284" time="3h ago" last />
</Card>
</Section>
<Section title="Recent activity" right={<a style={linkStyle}>View all </a>}>
<Card padding={0}>
<DashActivityRow icon="file-edit" tone="blue" text="Edited cron/jobs.json" sub="hermes-blog · session #3a2f" time="14m" />
<DashActivityRow icon="terminal" tone="orange" text="Ran hermes status" sub="3 platforms healthy" time="42m" />
<DashActivityRow icon="git-branch" tone="green" text="Cron daily-summary completed" sub="14.2s · 1,847 tokens" time="2h" />
<DashActivityRow icon="package" tone="purple" text="Installed template hermes-blog" sub="from awizemann/hermes-blog" time="yesterday" last />
</Card>
</Section>
</div>
</div>
</div>
);
}
const linkStyle = { fontSize: 12, color: 'var(--accent)', cursor: 'pointer', textDecoration: 'none' };
function StatusCard({ icon, label, value, sub, tone }) {
const dotColor = tone === 'green' ? 'var(--green-500)' : 'var(--gray-400)';
return (
<Card padding={14} style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11,
color: 'var(--fg-muted)', fontWeight: 600, marginBottom: 6 }}>
{tone === 'green'
? <span style={{ width: 7, height: 7, borderRadius: '50%', background: dotColor }}></span>
: <i data-lucide={icon} style={{ width: 12, height: 12 }}></i>
}
<span style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>{label}</span>
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 500,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{value}</div>
{sub && <div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 3 }}>{sub}</div>}
</Card>
);
}
function RecentSessionRow({ project, message, model, tokens, time, last }) {
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
cursor: 'pointer', transition: 'background 120ms',
}} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-quaternary)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<Pill tone="accent">{project}</Pill>
<div style={{ flex: 1, fontSize: 13, color: 'var(--fg)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{message}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)' }}>{model}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 70, textAlign: 'right' }}>{tokens}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', width: 60, textAlign: 'right' }}>{time}</div>
</div>
);
}
function DashActivityRow({ icon, tone, text, sub, time, last }) {
const tones = { green: 'var(--green-500)', blue: 'var(--blue-500)', orange: 'var(--orange-500)', purple: 'var(--accent)' };
return (
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 10, padding: '10px 14px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<div style={{
width: 22, height: 22, borderRadius: 5, background: 'var(--bg-quaternary)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: tones[tone], flexShrink: 0,
}}>
<i data-lucide={icon} style={{ width: 12, height: 12 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, color: 'var(--fg)' }}>{text}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 1 }}>{sub}</div>
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)' }}>{time}</div>
</div>
);
}
window.Dashboard = Dashboard;
+111
View File
@@ -0,0 +1,111 @@
// Health — diagnostics report. One-shot health check across services.
const HEALTH_CHECKS = [
{ name: 'Anthropic API', status: 'ok', latency: '124 ms', detail: 'authenticated as Aurora · sonnet-4.5 reachable' },
{ name: 'Local gateway', status: 'ok', latency: '2 ms', detail: 'pid 84021 · uptime 4d 2h · listening :7421' },
{ name: 'Filesystem', status: 'ok', latency: '—', detail: '14.2 GB free of 512 GB' },
{ name: 'GitHub MCP', status: 'ok', latency: '84 ms', detail: 'oauth ok · 18 tools · rate-limit 4500/5000 (warn at 4750)' },
{ name: 'Linear MCP', status: 'ok', latency: '142 ms', detail: 'oauth ok · 9 tools' },
{ name: 'Postgres MCP', status: 'ok', latency: '12 ms', detail: 'stdio · prod read replica' },
{ name: 'Figma MCP', status: 'ok', latency: '210 ms', detail: 'oauth ok · 6 tools' },
{ name: 'Notion MCP', status: 'error', latency: '—', detail: 'TLS handshake failed · 4 retries · backing off 30s' },
{ name: 'Slack MCP', status: 'warn', latency: '—', detail: 'oauth token expired · re-authenticate' },
{ name: 'Sentry MCP', status: 'idle', latency: '—', detail: 'disabled' },
{ name: 'Cron scheduler', status: 'ok', latency: '—', detail: '5 jobs registered · next: incident-triage in 12m' },
{ name: 'Local model cache', status: 'ok', latency: '—', detail: '412 MB · last pruned 2d ago' },
];
function Health() {
const [scanning, setScanning] = React.useState(false);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const ok = HEALTH_CHECKS.filter(c => c.status === 'ok').length;
const warn = HEALTH_CHECKS.filter(c => c.status === 'warn').length;
const err = HEALTH_CHECKS.filter(c => c.status === 'error').length;
function rerun() {
setScanning(true);
setTimeout(() => setScanning(false), 1400);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Health"
subtitle="A diagnostics report across Scarf, the agent, and connected services"
actions={<>
<Btn icon="download">Save report</Btn>
<Btn kind="primary" icon="rotate-cw" loading={scanning} onClick={rerun}>{scanning ? 'Scanning…' : 'Re-run'}</Btn>
</>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
{/* Summary banner */}
<div style={{
background: err > 0 ? 'var(--red-100)' : warn > 0 ? 'var(--orange-100)' : 'var(--green-100)',
border: `0.5px solid ${err > 0 ? 'var(--red-500)' : warn > 0 ? 'var(--orange-500)' : 'var(--green-500)'}`,
borderRadius: 10, padding: 16, marginBottom: 24,
display: 'flex', alignItems: 'center', gap: 14,
}}>
<div style={{
width: 38, height: 38, borderRadius: 9,
background: err > 0 ? 'var(--red-500)' : warn > 0 ? 'var(--orange-500)' : 'var(--green-500)',
color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide={err > 0 ? 'alert-octagon' : warn > 0 ? 'alert-triangle' : 'shield-check'} style={{ width: 20, height: 20 }}></i>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 2 }}>
{err > 0 ? `${err} service${err === 1 ? '' : 's'} unhealthy`
: warn > 0 ? `${warn} warning${warn === 1 ? '' : 's'} to review`
: 'All systems healthy'}
</div>
<div style={{ fontSize: 13, color: 'var(--fg-muted)' }}>
{ok} ok · {warn} warning · {err} error · scanned 2 minutes ago
</div>
</div>
</div>
{/* Checks */}
<SettingsGroup title="Diagnostic checks">
{HEALTH_CHECKS.map((c, i) => <HealthRow key={c.name} c={c} last={i === HEALTH_CHECKS.length - 1} />)}
</SettingsGroup>
<SettingsGroup title="Environment">
<SettingsRow icon="info" title="Scarf version"
description="0.14.2 · 0.15.0 available"
control={<Btn size="sm">Update</Btn>} />
<SettingsRow icon="cpu" title="Platform"
description="macOS 14.4.1 · Apple M3 Pro · 36 GB"
control={<Pill tone="green" dot>supported</Pill>} />
<SettingsRow icon="terminal" title="Shell"
description="/bin/zsh 5.9 · path 47 entries"
control={<Btn size="sm">Inspect</Btn>} last />
</SettingsGroup>
</div>
</div>
);
}
function HealthRow({ c, last }) {
const tones = {
ok: { tone: 'green', icon: 'check-circle' },
warn: { tone: 'amber', icon: 'alert-triangle' },
error: { tone: 'red', icon: 'x-circle' },
idle: { tone: 'gray', icon: 'minus-circle' },
};
const t = tones[c.status];
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<Pill tone={t.tone} icon={t.icon} size="sm">{c.status}</Pill>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{c.name}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg-muted)', marginTop: 2, fontFamily: 'var(--font-mono)' }}>{c.detail}</div>
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 70, textAlign: 'right' }}>{c.latency}</span>
</div>
);
}
window.Health = Health;
+107
View File
@@ -0,0 +1,107 @@
// Insights — usage charts and breakdowns.
function Insights() {
return (
<div style={{ overflow: 'auto', height: '100%' }}>
<ContentHeader title="Insights"
subtitle="Patterns across sessions, models, and tools"
right={<select style={{
fontSize: 12, padding: '5px 10px', border: '1px solid var(--border-strong)',
borderRadius: 6, background: 'var(--bg-card)', fontFamily: 'var(--font-sans)',
}}><option>Last 7 days</option><option>Last 30 days</option><option>This year</option></select>}
actions={<Btn icon="download">Export CSV</Btn>} />
<div style={{ padding: '20px 28px', display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ display: 'flex', gap: 12 }}>
<StatCard label="Sessions" value="847" sub="↗ +12% vs prev" />
<StatCard label="Tokens" value="2.4M" sub="1.8M in · 0.6M out" />
<StatCard label="Tool calls" value="3,221" sub="3.8 avg/session" />
<StatCard label="Avg latency" value="1.2s" accent="var(--accent)" sub="p95 4.1s" />
<StatCard label="Cost" value="$42.18" sub="$0.05 avg/session" />
</div>
<Card>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
<div style={{ flex: 1, fontSize: 13, fontWeight: 600 }}>Token usage</div>
<div style={{ display: 'flex', gap: 12, fontSize: 11, color: 'var(--fg-muted)' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: 'var(--accent)' }}></span>
Input
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: 'var(--brand-200)' }}></span>
Output
</span>
</div>
</div>
<BarChart />
</Card>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<Card>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 14 }}>By model</div>
<BreakdownRow label="claude-sonnet-4.5" value="62%" bar="var(--accent)" sub="$28.41 · 524 sessions" />
<BreakdownRow label="claude-haiku-4.5" value="31%" bar="var(--brand-300)" sub="$10.18 · 263 sessions" />
<BreakdownRow label="claude-opus-4.5" value="5%" bar="var(--brand-700)" sub="$3.40 · 42 sessions" />
<BreakdownRow label="local/llama-3.3" value="2%" bar="var(--gray-400)" sub="$0.00 · 18 sessions" last />
</Card>
<Card>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 14 }}>By tool kind</div>
<BreakdownRow label="read" value="42%" bar="var(--green-500)" sub="1,353 calls" />
<BreakdownRow label="execute" value="24%" bar="var(--orange-500)" sub="773 calls" />
<BreakdownRow label="edit" value="18%" bar="var(--blue-500)" sub="580 calls" />
<BreakdownRow label="fetch" value="11%" bar="var(--purple-tool-500)" sub="354 calls" />
<BreakdownRow label="browser" value="5%" bar="var(--indigo-500)" sub="161 calls" last />
</Card>
</div>
</div>
</div>
);
}
function BarChart() {
// 14 days of data, hand-tuned
const data = [
[120, 40], [80, 32], [180, 60], [240, 90], [200, 75], [60, 22], [40, 15],
[110, 38], [170, 56], [220, 82], [280, 98], [310, 110], [240, 78], [190, 64],
];
const max = 420;
const chartH = 160; // px area for bars
return (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: 184, padding: '0 4px' }}>
{data.map(([inp, outp], i) => {
const inpH = Math.round(inp / max * chartH);
const outpH = Math.round(outp / max * chartH);
return (
<div key={i} style={{
flex: 1, display: 'flex', flexDirection: 'column',
justifyContent: 'flex-end', alignItems: 'stretch', minWidth: 0,
}}>
<div style={{ background: 'var(--brand-200)', height: outpH,
borderRadius: '3px 3px 0 0' }}></div>
<div style={{ background: 'var(--accent)', height: inpH }}></div>
<div style={{ fontSize: 9, color: 'var(--fg-faint)', textAlign: 'center', marginTop: 4,
fontFamily: 'var(--font-mono)', height: 14 }}>{i % 2 === 0 ? `04/${12 + i}` : ''}</div>
</div>
);
})}
</div>
);
}
function BreakdownRow({ label, value, bar, sub, last }) {
return (
<div style={{ marginBottom: last ? 0 : 14 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div style={{ flex: 1, fontFamily: 'var(--font-mono)', fontSize: 12 }}>{label}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600 }}>{value}</div>
</div>
<div style={{ height: 6, background: 'var(--bg-quaternary)', borderRadius: 3, overflow: 'hidden' }}>
<div style={{ width: value, height: '100%', background: bar, borderRadius: 3 }}></div>
</div>
<div style={{ fontSize: 10.5, color: 'var(--fg-faint)', marginTop: 3 }}>{sub}</div>
</div>
);
}
window.Insights = Insights;
+123
View File
@@ -0,0 +1,123 @@
// Logs — streaming monospace surface. Filter pills + a fake live tail.
const LOG_LINES = [
{ ts: '09:42:18.124', level: 'info', source: 'gateway', msg: 'POST /v1/messages → 200 (1.2s, 482 tokens out)' },
{ ts: '09:42:18.066', level: 'debug', source: 'tool', msg: 'tool_call read_file path=src/App.jsx (8.2KB)' },
{ ts: '09:42:17.880', level: 'info', source: 'agent', msg: 'turn 14 started — personality=Forge model=claude-sonnet-4.5' },
{ ts: '09:42:15.341', level: 'warn', source: 'mcp', msg: 'github: rate-limit warning 4500/5000 used this hour' },
{ ts: '09:42:11.012', level: 'info', source: 'tool', msg: 'tool_call execute cmd="npm test -- --watch=false" status=ok 14.2s' },
{ ts: '09:42:01.508', level: 'error', source: 'tool', msg: 'tool_call execute denied: command "rm -rf node_modules" matches deny rule "rm -rf"' },
{ ts: '09:41:58.211', level: 'info', source: 'agent', msg: 'user message received (1.4KB)' },
{ ts: '09:41:42.004', level: 'debug', source: 'memory', msg: 'AGENTS.md hash unchanged (4f02…ab19), skipping reload' },
{ ts: '09:41:30.882', level: 'info', source: 'cron', msg: 'incident-triage finished ok (4.2s)' },
{ ts: '09:41:26.108', level: 'info', source: 'cron', msg: 'incident-triage started' },
{ ts: '09:41:18.443', level: 'info', source: 'mcp', msg: 'linear: tools/list 9 tools (142ms)' },
{ ts: '09:40:54.221', level: 'warn', source: 'gateway', msg: 'approval pending: tool_call execute cmd="git push origin main" (12s)' },
{ ts: '09:40:42.001', level: 'info', source: 'agent', msg: 'turn 13 ended — 2.1s, 7 tool calls, $0.0042' },
{ ts: '09:40:21.778', level: 'debug', source: 'tool', msg: 'tool_call list_files path=ui_kits/scarf-mac (24 entries)' },
{ ts: '09:40:18.422', level: 'error', source: 'mcp', msg: 'notion: TLS handshake failed (timeout 5s) — backing off 30s' },
{ ts: '09:40:02.114', level: 'info', source: 'agent', msg: 'session resumed (idle 14m)' },
];
const LEVEL_TONES = {
debug: '#7C7263', info: 'var(--blue-500)', warn: 'var(--amber-500)', error: 'var(--red-500)',
};
function Logs() {
const [level, setLevel] = React.useState(['info', 'warn', 'error']);
const [source, setSource] = React.useState('all');
const [search, setSearch] = React.useState('');
const [follow, setFollow] = React.useState(true);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const sources = ['all', 'agent', 'tool', 'gateway', 'mcp', 'cron', 'memory'];
const filtered = LOG_LINES.filter(l => {
if (!level.includes(l.level)) return false;
if (source !== 'all' && l.source !== source) return false;
if (search && !l.msg.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Logs"
subtitle="Live tail across the gateway, agent, tools, MCP servers, and cron"
actions={<>
<Btn icon="download">Export</Btn>
<Btn icon={follow ? 'pause' : 'play'} onClick={() => setFollow(!follow)}>
{follow ? 'Pause' : 'Follow'}
</Btn>
</>} />
{/* Toolbar */}
<div style={{
padding: '12px 24px', borderBottom: '0.5px solid var(--border)',
background: 'var(--bg-card)', display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap',
}}>
<TextInput value={search} onChange={setSearch} leftIcon="search" placeholder="Filter messages…" mono width={280} />
<div style={{ display: 'flex', gap: 4 }}>
{['debug', 'info', 'warn', 'error'].map(lv => {
const on = level.includes(lv);
return (
<button key={lv} onClick={() => setLevel(on ? level.filter(x => x !== lv) : [...level, lv])} style={{
padding: '4px 10px', borderRadius: 6, border: '0.5px solid var(--border)',
background: on ? 'var(--bg-tertiary)' : 'var(--bg-card)',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: on ? LEVEL_TONES[lv] : 'var(--fg-faint)',
textTransform: 'uppercase', cursor: 'pointer', letterSpacing: '0.04em',
}}>{lv}</button>
);
})}
</div>
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto' }}>
{sources.map(s => (
<button key={s} onClick={() => setSource(s)} style={{
padding: '4px 10px', borderRadius: 6, border: 'none',
background: source === s ? 'var(--accent-tint)' : 'transparent',
color: source === s ? 'var(--accent-active)' : 'var(--fg-muted)',
fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer',
}}>{s}</button>
))}
</div>
</div>
{/* Tail */}
<div style={{
flex: 1, overflowY: 'auto', background: '#1F1B16', color: '#E8E1D2',
fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.7,
padding: '12px 0',
}}>
{filtered.map((l, i) => <LogRow key={i} l={l} />)}
{follow && (
<div style={{ padding: '6px 24px', display: 'flex', alignItems: 'center', gap: 8,
color: '#A89B82', fontSize: 11 }}>
<span style={{ width: 6, height: 6, borderRadius: 3, background: 'var(--green-500)',
animation: 'pulse 1.4s ease-in-out infinite' }}></span>
following 4 lines/sec
</div>
)}
</div>
<style>{`
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
`}</style>
</div>
);
}
function LogRow({ l }) {
return (
<div style={{
display: 'flex', gap: 14, padding: '1px 24px', alignItems: 'baseline',
}}>
<span style={{ color: '#7C7263', fontSize: 11, width: 100, flexShrink: 0 }}>{l.ts}</span>
<span style={{ color: LEVEL_TONES[l.level], width: 50, flexShrink: 0,
textTransform: 'uppercase', fontSize: 10, fontWeight: 700, letterSpacing: '0.04em' }}>{l.level}</span>
<span style={{ color: '#A89B82', width: 70, flexShrink: 0 }}>{l.source}</span>
<span style={{ color: '#E8E1D2', flex: 1 }}>{l.msg}</span>
</div>
);
}
window.Logs = Logs;
+193
View File
@@ -0,0 +1,193 @@
// MCP Servers — connection list + detail with health, capabilities, and logs.
const MCP_SERVERS = [
{ id: 'github', name: 'GitHub', transport: 'http', url: 'https://mcp.github.com/v1', status: 'connected', tools: 18, prompts: 4, resources: 12, latency: 84, version: '1.4.2', auth: 'oauth', scope: 'org/wizemann' },
{ id: 'linear', name: 'Linear', transport: 'http', url: 'https://mcp.linear.app/sse', status: 'connected', tools: 9, prompts: 0, resources: 6, latency: 142, version: '0.9.1', auth: 'oauth', scope: 'wizemann' },
{ id: 'slack', name: 'Slack', transport: 'http', url: 'https://mcp.slack.com/v1', status: 'auth-required', tools: 0, prompts: 0, resources: 0, latency: null, version: '—', auth: 'oauth', scope: '—' },
{ id: 'postgres-prod', name: 'Postgres (prod, ro)', transport: 'stdio', url: 'mcp-postgres --readonly', status: 'connected', tools: 4, prompts: 0, resources: 28, latency: 12, version: '2.1.0', auth: 'env', scope: 'prod-replica' },
{ id: 'figma', name: 'Figma', transport: 'http', url: 'https://mcp.figma.com/v1', status: 'connected', tools: 6, prompts: 2, resources: 0, latency: 210, version: '0.4.0', auth: 'oauth', scope: 'wizemann-design' },
{ id: 'notion', name: 'Notion', transport: 'http', url: 'https://mcp.notion.so/v1', status: 'error', tools: 0, prompts: 0, resources: 0, latency: null, version: '—', auth: 'oauth', scope: '—', error: 'TLS handshake failed (timeout 5s)' },
{ id: 'sentry', name: 'Sentry', transport: 'http', url: 'https://mcp.sentry.io/v1', status: 'disabled', tools: 0, prompts: 0, resources: 0, latency: null, version: '—', auth: 'token', scope: 'wizemann' },
];
const STATUS_TONES = {
'connected': { tone: 'green', label: 'connected' },
'auth-required': { tone: 'amber', label: 'auth required' },
'error': { tone: 'red', label: 'error' },
'disabled': { tone: 'gray', label: 'disabled' },
};
function MCPServers() {
const [active, setActive] = React.useState('github');
const server = MCP_SERVERS.find(s => s.id === active);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="MCP Servers"
subtitle="Model Context Protocol endpoints — each adds a bundle of tools, prompts, and resources"
actions={<><Btn icon="rotate-cw">Reconnect all</Btn><Btn kind="primary" icon="plus">Add server</Btn></>} />
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
<div style={{ width: 320, borderRight: '0.5px solid var(--border)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-card)' }}>
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
{MCP_SERVERS.map(s => <MCPRow key={s.id} s={s} active={s.id === active} onClick={() => setActive(s.id)} />)}
</div>
<div style={{ padding: 12, borderTop: '0.5px solid var(--border)' }}>
<Btn fullWidth icon="hard-drive">Browse marketplace</Btn>
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', background: 'var(--bg)', padding: '24px 32px' }}>
<MCPDetail server={server} />
</div>
</div>
</div>
);
}
function MCPRow({ s, active, onClick }) {
const status = STATUS_TONES[s.status];
const [hover, setHover] = React.useState(false);
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
padding: '11px 12px', borderRadius: 7, cursor: 'pointer', marginBottom: 2,
background: active ? 'var(--accent-tint)' : (hover ? 'var(--bg-quaternary)' : 'transparent'),
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<ServerGlyph id={s.id} size={22} />
<div style={{ flex: 1, fontSize: 13, fontWeight: 500,
color: active ? 'var(--accent-active)' : 'var(--fg)' }}>{s.name}</div>
<Dot tone={status.tone} />
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{s.transport} · {s.tools} tools · {s.prompts} prompts
</div>
</div>
);
}
function ServerGlyph({ id, size = 22 }) {
const palette = {
github: '#1F1B16', linear: '#5E6AD2', slack: '#611F69',
'postgres-prod': '#336791', figma: '#F24E1E', notion: '#191919', sentry: '#362D59',
};
const letter = id[0].toUpperCase();
return (
<div style={{
width: size, height: size, borderRadius: 5, background: palette[id] || '#888',
color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-display)', fontSize: size * 0.5, fontWeight: 700, flexShrink: 0,
}}>{letter}</div>
);
}
function MCPDetail({ server }) {
const status = STATUS_TONES[server.status];
return (
<>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, marginBottom: 20 }}>
<ServerGlyph id={server.id} size={48} />
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div className="scarf-h2" style={{ fontSize: 22 }}>{server.name}</div>
<Pill tone={status.tone} dot>{status.label}</Pill>
<span style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>v{server.version}</span>
</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>{server.url}</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<Btn icon="rotate-cw">Reconnect</Btn>
<Toggle on={server.status !== 'disabled'} size="lg" />
</div>
</div>
{server.error && (
<div style={{
background: 'var(--red-100)', border: '0.5px solid var(--red-500)',
borderRadius: 9, padding: 12, marginBottom: 20, display: 'flex', gap: 10, alignItems: 'flex-start',
}}>
<i data-lucide="alert-triangle" style={{ width: 16, height: 16, color: 'var(--red-500)', flexShrink: 0, marginTop: 1 }}></i>
<div>
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--red-500)', marginBottom: 2 }}>Connection failed</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>{server.error}</div>
</div>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 24 }}>
<StatCard label="Tools" value={server.tools} />
<StatCard label="Prompts" value={server.prompts} />
<StatCard label="Resources" value={server.resources} />
<StatCard label="Latency" value={server.latency != null ? `${server.latency} ms` : '—'} sub={server.latency != null ? 'p95: ' + Math.round(server.latency * 2.4) + ' ms' : '—'} />
</div>
<SettingsGroup title="Connection">
<SettingsRow icon="link" title="Transport"
description={server.transport === 'http' ? 'HTTP / SSE' : 'Local stdio process'}
control={<Pill>{server.transport}</Pill>} />
<SettingsRow icon="key" title="Auth"
description={server.auth === 'oauth' ? 'OAuth — refreshed automatically' : server.auth === 'env' ? 'Environment variable' : 'Static token'}
control={<Btn size="sm" icon="external-link">Manage</Btn>} />
<SettingsRow icon="shield" title="Scope"
description={`Calls scoped to "${server.scope}".`}
control={<Btn size="sm">Edit</Btn>} last />
</SettingsGroup>
<SettingsGroup title="Capabilities" description="Tools, prompts, and resources advertised by this server.">
<CapRow icon="wrench" name="list_issues" kind="tool" desc="List repository issues with filters" />
<CapRow icon="wrench" name="create_pr" kind="tool" desc="Open a pull request from a branch" />
<CapRow icon="wrench" name="search_code" kind="tool" desc="Full-text search across accessible repos" />
<CapRow icon="message-square" name="review_pr" kind="prompt" desc="Structured PR review prompt" />
<CapRow icon="folder" name="repo://*" kind="resource" desc="Read-only access to repo file trees" last />
</SettingsGroup>
<SettingsGroup title="Activity log" description="Last 5 events from this server.">
<LogLine when="2m ago" level="info" msg="tools/list returned 18 tools (84ms)" />
<LogLine when="14m ago" level="info" msg="github__list_issues invoked (owner=wizemann, state=open)" />
<LogLine when="42m ago" level="warn" msg="rate-limit warning: 4500/5000 used this hour" />
<LogLine when="1h ago" level="info" msg="oauth token refreshed" />
<LogLine when="3h ago" level="info" msg="connection established (TLS 1.3)" last />
</SettingsGroup>
<SettingsGroup title="Danger zone" tone="danger">
<SettingsRow icon="x-circle" title="Disconnect server"
description="Remove this server. Tools it provided will become unavailable."
control={<Btn size="sm" kind="danger">Disconnect</Btn>} last />
</SettingsGroup>
</>
);
}
function CapRow({ icon, name, kind, desc, last }) {
const tones = { tool: 'blue', prompt: 'purple', resource: 'green' };
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '11px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<i data-lucide={icon} style={{ width: 14, height: 14, color: 'var(--fg-muted)', flexShrink: 0 }}></i>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--fg)', minWidth: 140 }}>{name}</div>
<div style={{ flex: 1, fontSize: 12, color: 'var(--fg-muted)' }}>{desc}</div>
<Pill tone={tones[kind]} size="sm">{kind}</Pill>
</div>
);
}
function LogLine({ when, level, msg, last }) {
const tones = { info: 'var(--fg-faint)', warn: 'var(--amber-500)', error: 'var(--red-500)' };
return (
<div style={{
display: 'flex', gap: 12, padding: '8px 18px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<span style={{ color: 'var(--fg-faint)', width: 80 }}>{when}</span>
<span style={{ color: tones[level], textTransform: 'uppercase', width: 44, fontSize: 10, fontWeight: 600, paddingTop: 1 }}>{level}</span>
<span style={{ color: 'var(--fg-muted)', flex: 1 }}>{msg}</span>
</div>
);
}
window.MCPServers = MCPServers;
+134
View File
@@ -0,0 +1,134 @@
// Memory — AGENTS.md editor. Stored instructions the agent reads on every turn.
const MEMORY_FILES = [
{ id: 'global', name: 'AGENTS.md', scope: 'Global', path: '~/.scarf/AGENTS.md', updated: '2 days ago', size: '1.2 KB' },
{ id: 'wizemann', name: 'AGENTS.md', scope: 'Org · Wizemann', path: '~/.scarf/orgs/wizemann/AGENTS.md', updated: '1 week ago', size: '3.4 KB' },
{ id: 'project', name: 'AGENTS.md', scope: 'Project · sera', path: 'sera/AGENTS.md', updated: '14m ago', size: '5.8 KB' },
];
const SAMPLE_AGENTS = `# Sera — agent instructions
You are working on **Sera**, a CLI for building Anthropic-style applications.
The codebase is TypeScript + Bun. Tests live next to source as \`*.test.ts\`.
## Style
- Prefer named exports.
- 2-space indent, no semicolons in TS.
- Avoid default exports except for React components.
- Lowercase filenames except for React components (PascalCase).
## Workflow
- Run \`bun test\` after every meaningful change.
- Open a draft PR early; flip to ready when CI is green.
- Update CHANGELOG.md when changing public API.
## Don't
- Touch \`scripts/release.ts\` — owned by ops.
- Pull in dependencies without flagging it first.
- Push directly to main.
`;
function Memory() {
const [active, setActive] = React.useState('project');
const [draft, setDraft] = React.useState(SAMPLE_AGENTS);
const [dirty, setDirty] = React.useState(false);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const file = MEMORY_FILES.find(f => f.id === active);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Memory" subtitle="AGENTS.md files the agent reads on every turn. Project beats org beats global."
actions={<>
<Btn icon="rotate-ccw" disabled={!dirty}>Discard</Btn>
<Btn kind="primary" icon="check" disabled={!dirty}>Save</Btn>
</>}
/>
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
<div style={{ width: 280, borderRight: '0.5px solid var(--border)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-card)' }}>
<div style={{ padding: '14px 14px 6px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
Memory files
</div>
<div style={{ flex: 1, padding: 8 }}>
{MEMORY_FILES.map(f => {
const a = f.id === active;
return (
<div key={f.id} onClick={() => setActive(f.id)} style={{
padding: '10px 12px', borderRadius: 7, cursor: 'pointer', marginBottom: 2,
background: a ? 'var(--accent-tint)' : 'transparent',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<i data-lucide="file-text" style={{ width: 13, height: 13,
color: a ? 'var(--accent-active)' : 'var(--fg-muted)' }}></i>
<div style={{ fontSize: 13, fontWeight: 500,
color: a ? 'var(--accent-active)' : 'var(--fg)' }}>{f.scope}</div>
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{f.path}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 2 }}>
{f.size} · {f.updated}
</div>
</div>
);
})}
<div style={{ marginTop: 12 }}>
<Btn fullWidth icon="plus" size="sm">Add memory file</Btn>
</div>
</div>
<div style={{ padding: 14, borderTop: '0.5px solid var(--border)' }}>
<div style={{ fontSize: 11, color: 'var(--fg-muted)', lineHeight: 1.5 }}>
<i data-lucide="info" style={{ width: 11, height: 11, verticalAlign: 'text-top', marginRight: 4 }}></i>
Files are loaded in order narrower scopes override broader ones.
</div>
</div>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
<div style={{
padding: '12px 24px', borderBottom: '0.5px solid var(--border)',
background: 'var(--bg-card)', display: 'flex', alignItems: 'center', gap: 12,
}}>
<i data-lucide="file-text" style={{ width: 16, height: 16, color: 'var(--accent)' }}></i>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{file.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>{file.path}</div>
</div>
{dirty
? <Pill tone="amber" dot>unsaved</Pill>
: <Pill tone="green" dot>saved</Pill>}
<div style={{ display: 'flex', gap: 4 }}>
<IconBtn icon="eye" tooltip="Preview" />
<IconBtn icon="more-horizontal" tooltip="More" />
</div>
</div>
<textarea value={draft}
onChange={e => { setDraft(e.target.value); setDirty(true); }}
style={{
flex: 1, padding: '20px 32px', border: 'none', outline: 'none', resize: 'none',
fontFamily: 'var(--font-mono)', fontSize: 13, lineHeight: 1.7,
color: 'var(--fg)', background: 'var(--bg)',
}}
/>
<div style={{
padding: '8px 24px', borderTop: '0.5px solid var(--border)', background: 'var(--bg-card)',
display: 'flex', alignItems: 'center', gap: 16, fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)',
}}>
<span>markdown</span>
<span>·</span>
<span>{draft.split('\n').length} lines</span>
<span>·</span>
<span>{draft.length} chars</span>
<span style={{ marginLeft: 'auto' }}>last loaded: {file.updated}</span>
</div>
</div>
</div>
</div>
);
}
window.Memory = Memory;
+422
View File
@@ -0,0 +1,422 @@
// MoreViews.jsx — Personalities, Quick Commands, Platforms, Credentials,
// Plugins, Webhooks, Profiles, Gateway. Each is a focused list/detail or grid.
// ─────────────── Personalities ───────────────
const PERSONALITIES = [
{ id: 'forge', name: 'Forge', emoji: '⚒', color: '#C25A2A', desc: 'Engineering pair. Refactors, tests, reviews PRs.', model: 'sonnet-4.5', tools: 14, used: '2m ago' },
{ id: 'hermes', name: 'Hermes', emoji: '✉', color: '#7E5BA9', desc: 'Operations. Handles ops scripts, summaries, status.', model: 'haiku-4.5', tools: 8, used: '32m ago' },
{ id: 'atlas', name: 'Atlas', emoji: '◇', color: '#3F6BA9', desc: 'Long-form writer. Spec drafts, release notes, docs.', model: 'opus-4.1', tools: 6, used: 'yesterday' },
{ id: 'vesta', name: 'Vesta', emoji: '✿', color: '#3F8A6E', desc: 'Design partner. Critiques layouts, suggests patterns.', model: 'sonnet-4.5', tools: 4, used: '3 days ago' },
{ id: 'gaia', name: 'Gaia', emoji: '✱', color: '#A8741F', desc: 'Researcher. Web search, summarization, citations.', model: 'sonnet-4.5', tools: 5, used: '1 week ago' },
];
function Personalities() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Personalities"
subtitle="Pre-configured agents — system prompt, model, allowed tools, defaults"
actions={<Btn kind="primary" icon="plus">New personality</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: 32 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 14 }}>
{PERSONALITIES.map(p => <PersonalityCard key={p.id} p={p} />)}
<Card padding={24} interactive style={{ display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', minHeight: 180,
border: '1px dashed var(--border-strong)', background: 'transparent', boxShadow: 'none' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--bg-quaternary)',
display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 8, color: 'var(--fg-muted)' }}>
<i data-lucide="plus" style={{ width: 20, height: 20 }}></i>
</div>
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--fg-muted)' }}>New personality</div>
</Card>
</div>
</div>
</div>
);
}
function PersonalityCard({ p }) {
return (
<Card interactive padding={18}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 12 }}>
<div style={{
width: 38, height: 38, borderRadius: 9, background: p.color, color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-display)', fontSize: 18,
}}>{p.emoji}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 600 }}>{p.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>last used {p.used}</div>
</div>
<IconBtn icon="more-horizontal" size={26} />
</div>
<div style={{ fontSize: 12.5, color: 'var(--fg-muted)', lineHeight: 1.5, marginBottom: 14, minHeight: 36 }}>{p.desc}</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<Pill size="sm">{p.model}</Pill>
<Pill size="sm" icon="wrench">{p.tools} tools</Pill>
</div>
</Card>
);
}
window.Personalities = Personalities;
// ─────────────── Quick Commands ───────────────
const QC = [
{ trigger: '/test', name: 'Run tests', desc: 'Run the project test suite, summarize failures.', personality: 'Forge', uses: 142 },
{ trigger: '/review', name: 'Review PR', desc: 'Walk the diff in a checked-out PR and post review notes.', personality: 'Forge', uses: 38 },
{ trigger: '/standup', name: 'Standup summary', desc: 'Summarize yesterday\'s commits + Linear updates.', personality: 'Hermes', uses: 24 },
{ trigger: '/notes', name: 'Release notes', desc: 'Group merged PRs since last tag into release notes.', personality: 'Atlas', uses: 8 },
{ trigger: '/figma', name: 'Open Figma frame', desc: 'Resolve a Figma URL and import frame metadata.', personality: 'Vesta', uses: 14 },
{ trigger: '/cite', name: 'Cite source', desc: 'Web search + return citations as Markdown footnotes.', personality: 'Gaia', uses: 9 },
];
function QuickCommands() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Quick Commands"
subtitle="Slash-prefixed shortcuts that expand into full prompts"
actions={<Btn kind="primary" icon="plus">New command</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<SettingsGroup>
{QC.map((q, i) => (
<div key={q.trigger} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === QC.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<span style={{
fontFamily: 'var(--font-mono)', fontSize: 12.5, fontWeight: 600,
color: 'var(--accent)', background: 'var(--accent-tint)',
padding: '4px 9px', borderRadius: 6, minWidth: 80, textAlign: 'center',
}}>{q.trigger}</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{q.name}</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{q.desc}</div>
</div>
<Pill size="sm">{q.personality}</Pill>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 70, textAlign: 'right' }}>
{q.uses} uses
</span>
<IconBtn icon="more-horizontal" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.QuickCommands = QuickCommands;
// ─────────────── Platforms ───────────────
const PLATFORMS = [
{ id: 'github', name: 'GitHub', desc: 'Repos, issues, PRs', conn: true, scope: 'org/wizemann · 14 repos' },
{ id: 'linear', name: 'Linear', desc: 'Issues & projects', conn: true, scope: 'wizemann · all teams' },
{ id: 'slack', name: 'Slack', desc: 'Messaging', conn: false, scope: '—' },
{ id: 'notion', name: 'Notion', desc: 'Docs', conn: false, scope: '—' },
{ id: 'figma', name: 'Figma', desc: 'Design files', conn: true, scope: 'wizemann-design' },
{ id: 'sentry', name: 'Sentry', desc: 'Error monitoring', conn: false, scope: '—' },
{ id: 'pagerduty', name: 'PagerDuty', desc: 'On-call', conn: false, scope: '—' },
{ id: 'stripe', name: 'Stripe', desc: 'Payments', conn: false, scope: '—' },
];
function Platforms() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const palette = { github: '#1F1B16', linear: '#5E6AD2', slack: '#611F69', notion: '#191919',
figma: '#F24E1E', sentry: '#362D59', pagerduty: '#06AC38', stripe: '#635BFF' };
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Platforms"
subtitle="Higher-level integrations. Each provides one or more MCP servers and credentials."
actions={<Btn icon="external-link">Browse marketplace</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: 32 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 14 }}>
{PLATFORMS.map(p => (
<Card key={p.id} interactive padding={18}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
<div style={{
width: 38, height: 38, borderRadius: 9, background: palette[p.id] || '#888', color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-display)', fontSize: 18, fontWeight: 700,
}}>{p.name[0]}</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>{p.name}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg-muted)' }}>{p.desc}</div>
</div>
{p.conn && <Pill tone="green" dot size="sm">on</Pill>}
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)', marginBottom: 12,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.scope}</div>
<Btn fullWidth size="sm" kind={p.conn ? 'secondary' : 'primary'}
icon={p.conn ? 'settings' : 'plug'}>
{p.conn ? 'Configure' : 'Connect'}
</Btn>
</Card>
))}
</div>
</div>
</div>
);
}
window.Platforms = Platforms;
// ─────────────── Credentials ───────────────
const CREDS = [
{ name: 'ANTHROPIC_API_KEY', kind: 'api-key', source: 'Keychain', last: '2m ago', scope: 'global', value: 'sk-ant-•••••••••a4f2' },
{ name: 'GITHUB_TOKEN', kind: 'oauth', source: 'OAuth', last: '14m ago', scope: 'global', value: 'gho_•••••••••••3kP9' },
{ name: 'LINEAR_TOKEN', kind: 'oauth', source: 'OAuth', last: '2h ago', scope: 'global', value: 'lin_oauth_•••••8m2x' },
{ name: 'POSTGRES_URL', kind: 'secret', source: 'env (.env)', last: '4h ago', scope: 'project · sera', value: 'postgres://ro@•••' },
{ name: 'OPENAI_API_KEY', kind: 'api-key', source: 'Keychain', last: 'never', scope: 'global', value: 'sk-•••••••••••L7Pw' },
{ name: 'AWS_ACCESS_KEY_ID', kind: 'secret', source: '~/.aws/credentials', last: '1d ago', scope: 'global', value: 'AKIA•••••••••QZX' },
];
function Credentials() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const [reveal, setReveal] = React.useState({});
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Credentials"
subtitle="API keys, OAuth tokens, and secrets the agent can read. Stored in OS keychain by default."
actions={<Btn kind="primary" icon="plus">Add credential</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<div style={{
background: 'var(--accent-tint)', border: '0.5px solid var(--accent)',
borderRadius: 9, padding: 12, marginBottom: 20, display: 'flex', alignItems: 'flex-start', gap: 10,
}}>
<i data-lucide="shield" style={{ width: 16, height: 16, color: 'var(--accent)', marginTop: 1 }}></i>
<div style={{ fontSize: 12.5, color: 'var(--fg)', lineHeight: 1.5 }}>
Credentials are never sent to Anthropic. They're injected into tool calls at the local gateway.
</div>
</div>
<SettingsGroup>
{CREDS.map((c, i) => (
<div key={c.name} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === CREDS.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<i data-lucide={c.kind === 'oauth' ? 'key-round' : c.kind === 'api-key' ? 'key' : 'lock'}
style={{ width: 16, height: 16, color: 'var(--fg-muted)', flexShrink: 0 }}></i>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 500 }}>{c.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 2 }}>
{c.source} · {c.scope} · used {c.last}
</div>
</div>
<code style={{
fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--fg-muted)',
background: 'var(--bg-quaternary)', padding: '3px 8px', borderRadius: 5, width: 220, textAlign: 'center',
}}>
{reveal[c.name] ? c.value.replace(/•+/g, '************') : c.value}
</code>
<IconBtn icon={reveal[c.name] ? 'eye-off' : 'eye'} size={26}
onClick={() => setReveal({ ...reveal, [c.name]: !reveal[c.name] })} />
<IconBtn icon="copy" size={26} />
<IconBtn icon="trash-2" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.Credentials = Credentials;
// ─────────────── Plugins ───────────────
const PLUGINS = [
{ id: 'commit-message', name: 'Smart commits', desc: 'Generate conventional-commit messages from staged changes.', author: 'wizemann', enabled: true, hooks: ['pre-commit'] },
{ id: 'review-helper', name: 'Review helper', desc: 'Auto-tag PR reviewers based on touched paths.', author: 'wizemann', enabled: true, hooks: ['pr-open'] },
{ id: 'todo-extractor', name: 'TODO extractor', desc: 'Surface inline TODOs as a checklist on the dashboard.', author: 'community', enabled: false, hooks: ['session-start'] },
{ id: 'speak', name: 'Speak responses', desc: 'Read agent responses aloud via system TTS.', author: 'community', enabled: false, hooks: ['turn-end'] },
];
function Plugins() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Plugins"
subtitle="Local extensions that hook into agent and editor lifecycle events"
actions={<><Btn icon="external-link">Marketplace</Btn><Btn kind="primary" icon="plus">Install</Btn></>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<SettingsGroup>
{PLUGINS.map((p, i) => (
<div key={p.id} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === PLUGINS.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<div style={{
width: 32, height: 32, borderRadius: 7, background: 'var(--accent-tint)', color: 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide="puzzle" style={{ width: 15, height: 15 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, fontWeight: 500 }}>{p.name}</span>
<span style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>by {p.author}</span>
</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{p.desc}</div>
<div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
{p.hooks.map(h => <Pill key={h} size="sm">{h}</Pill>)}
</div>
</div>
<Toggle on={p.enabled} />
<IconBtn icon="more-horizontal" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.Plugins = Plugins;
// ─────────────── Webhooks ───────────────
const WEBHOOKS = [
{ name: 'PR opened review', url: 'https://hooks.scarf.local/pr-review', events: ['github.pr.opened'], status: 'active', last: '2h ago' },
{ name: 'Sentry → triage', url: 'https://hooks.scarf.local/sentry-triage', events: ['sentry.issue.created', 'sentry.issue.regression'], status: 'active', last: '14m ago' },
{ name: 'Linear cycle → recap', url: 'https://hooks.scarf.local/cycle-recap', events: ['linear.cycle.completed'], status: 'paused', last: '8d ago' },
];
function Webhooks() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Webhooks"
subtitle="External events that trigger an agent run. Each maps an event payload to a personality + prompt."
actions={<Btn kind="primary" icon="plus">New webhook</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<SettingsGroup>
{WEBHOOKS.map((w, i) => (
<div key={w.name} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === WEBHOOKS.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<i data-lucide="webhook" style={{ width: 16, height: 16, color: 'var(--fg-muted)' }}></i>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{w.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)', marginTop: 2,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{w.url}</div>
<div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
{w.events.map(e => <Pill key={e} size="sm">{e}</Pill>)}
</div>
</div>
{w.status === 'active'
? <Pill tone="green" dot>active</Pill>
: <Pill tone="gray" dot>paused</Pill>}
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 80, textAlign: 'right' }}>{w.last}</span>
<IconBtn icon="more-horizontal" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.Webhooks = Webhooks;
// ─────────────── Profiles ───────────────
const PROFILES = [
{ id: 'dev', name: 'Development', desc: 'Permissive — auto-approve writes & execs in dev branches.', active: true, policies: 14 },
{ id: 'review', name: 'Code review', desc: 'Read-only filesystem, no execute, network only via MCP.', active: false, policies: 8 },
{ id: 'prod', name: 'Production', desc: 'All writes & execs require approval. No deletions.', active: false, policies: 22 },
{ id: 'air-gap', name: 'Air-gapped', desc: 'No network. Local tools only. For sensitive code paths.', active: false, policies: 6 },
];
function Profiles() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Profiles"
subtitle="Bundles of policies you switch between per-project or per-task"
actions={<Btn kind="primary" icon="plus">New profile</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: 32 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 }}>
{PROFILES.map(p => (
<Card key={p.id} interactive padding={20}
style={{ borderColor: p.active ? 'var(--accent)' : 'var(--border)',
boxShadow: p.active ? '0 0 0 2px var(--accent-tint)' : 'var(--shadow-sm)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
<i data-lucide="user-cog" style={{ width: 18, height: 18,
color: p.active ? 'var(--accent)' : 'var(--fg-muted)' }}></i>
<div style={{ fontSize: 15, fontWeight: 600, flex: 1 }}>{p.name}</div>
{p.active && <Pill tone="accent" dot>active</Pill>}
</div>
<div style={{ fontSize: 12.5, color: 'var(--fg-muted)', lineHeight: 1.5, marginBottom: 14, minHeight: 36 }}>{p.desc}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>
<i data-lucide="shield" style={{ width: 12, height: 12 }}></i>
{p.policies} policies
<Btn size="sm" style={{ marginLeft: 'auto' }}>{p.active ? 'Edit' : 'Activate'}</Btn>
</div>
</Card>
))}
</div>
</div>
</div>
);
}
window.Profiles = Profiles;
// ─────────────── Gateway ───────────────
function Gateway() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Gateway"
subtitle="Local proxy that routes every model & tool call. Logs, redacts, enforces policies."
actions={<Btn icon="rotate-cw">Restart</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 24 }}>
<StatCard label="Status" value="running" sub="pid 84021 · uptime 4d 2h" accent="var(--green-600)" />
<StatCard label="Listening" value=":7421" sub="loopback only" />
<StatCard label="Calls (24h)" value="1,284" sub="13 denied · 4 errored" />
<StatCard label="Throughput" value="2.4 MB/s" sub="p95: 6.1 MB/s" />
</div>
<SettingsGroup title="Network">
<SettingsRow icon="globe" title="Listen address"
description="The gateway binds to this address. Default loopback only."
control={<TextInput value="127.0.0.1:7421" mono />} />
<SettingsRow icon="lock" title="TLS"
description="Use a self-signed cert for outbound to 127.0.0.1."
control={<Toggle on={true} />} />
<SettingsRow icon="filter" title="Allowed hosts"
description="3 entries — api.anthropic.com, mcp.github.com, mcp.linear.app"
control={<Btn size="sm">Edit</Btn>} last />
</SettingsGroup>
<SettingsGroup title="Logging & redaction">
<SettingsRow icon="file-text" title="Request logging"
description="Persist headers + bodies for 7 days."
control={<Toggle on={true} />} />
<SettingsRow icon="eye-off" title="Redact secrets"
description="Mask values matching credential patterns before logging."
control={<Toggle on={true} />} />
<SettingsRow icon="archive" title="Log retention"
description="Older logs are pruned automatically."
control={<Select value="7d" options={[
{ value: '1d', label: '1 day' }, { value: '7d', label: '7 days' },
{ value: '30d', label: '30 days' }, { value: 'forever', label: 'Forever' },
]} />} last />
</SettingsGroup>
<SettingsGroup title="Performance">
<SettingsRow icon="zap" title="Concurrent requests"
control={<TextInput value="16" mono />} />
<SettingsRow icon="hourglass" title="Per-call timeout"
control={<Select value="60s" options={[
{ value: '30s', label: '30 seconds' }, { value: '60s', label: '60 seconds' },
{ value: '5m', label: '5 minutes' }, { value: '15m', label: '15 minutes' },
]} />} last />
</SettingsGroup>
</div>
</div>
);
}
window.Gateway = Gateway;
+83
View File
@@ -0,0 +1,83 @@
// Projects — list of project folders the agent operates in.
function Projects() {
const projects = [
{ id: 1, name: 'hermes-blog', dir: '~/code/hermes-blog', template: 'awizemann/hermes-blog', sessions: 142, lastRun: '14m ago', cron: 2, status: 'healthy' },
{ id: 2, name: 'scarf', dir: '~/code/scarf', template: '—', sessions: 89, lastRun: '42m ago', cron: 0, status: 'healthy' },
{ id: 3, name: 'inbox-sweep', dir: '~/code/inbox-sweep', template: 'community/inbox-sweep', sessions: 38, lastRun: '3h ago', cron: 1, status: 'healthy' },
{ id: 4, name: 'twitter-recap', dir: '~/code/twitter-recap', template: 'awizemann/twitter-recap', sessions: 14, lastRun: '2d ago', cron: 1, status: 'paused' },
{ id: 5, name: 'pr-watcher', dir: '~/code/pr-watcher', template: 'community/pr-watcher', sessions: 4, lastRun: '5d ago', cron: 1, status: 'errored' },
];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Projects"
subtitle="Each project pins context, AGENTS.md, cron jobs, and session history"
actions={<><Btn icon="folder-plus">Add Existing</Btn><Btn kind="primary" icon="plus">New from Template</Btn></>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '20px 28px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 14 }}>
{projects.map(p => (
<Card key={p.id} padding={16} style={{ display: 'flex', flexDirection: 'column', gap: 10, cursor: 'pointer' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
<div style={{
width: 36, height: 36, borderRadius: 8, background: 'var(--accent-tint)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--accent)', flexShrink: 0,
}}>
<i data-lucide="folder" style={{ width: 18, height: 18 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>{p.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-muted)',
fontFamily: 'var(--font-mono)', overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.dir}</div>
</div>
{p.status === 'healthy' && <Pill tone="green" dot>healthy</Pill>}
{p.status === 'paused' && <Pill tone="gray" dot>paused</Pill>}
{p.status === 'errored' && <Pill tone="red" dot>errored</Pill>}
</div>
{p.template !== '—' && (
<div style={{ fontSize: 11, color: 'var(--fg-muted)',
display: 'flex', alignItems: 'center', gap: 5 }}>
<i data-lucide="package" style={{ width: 11, height: 11 }}></i>
<span style={{ fontFamily: 'var(--font-mono)' }}>{p.template}</span>
</div>
)}
<div style={{ display: 'flex', gap: 16, paddingTop: 8,
borderTop: '0.5px solid var(--border)', fontSize: 11 }}>
<div>
<div style={{ color: 'var(--fg-muted)' }}>Sessions</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, marginTop: 1 }}>{p.sessions}</div>
</div>
<div>
<div style={{ color: 'var(--fg-muted)' }}>Cron jobs</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, marginTop: 1 }}>{p.cron}</div>
</div>
<div style={{ marginLeft: 'auto', textAlign: 'right' }}>
<div style={{ color: 'var(--fg-muted)' }}>Last run</div>
<div style={{ fontSize: 12, marginTop: 1 }}>{p.lastRun}</div>
</div>
</div>
</Card>
))}
<Card padding={16} style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
border: '1px dashed var(--border-strong)', boxShadow: 'none',
background: 'transparent', minHeight: 140, cursor: 'pointer',
color: 'var(--fg-muted)', flexDirection: 'column', gap: 8,
}}>
<i data-lucide="plus" style={{ width: 24, height: 24 }}></i>
<div style={{ fontSize: 13, fontWeight: 500 }}>New project</div>
<div style={{ fontSize: 11, textAlign: 'center', maxWidth: 180 }}>From template, GitHub repo, or empty folder</div>
</Card>
</div>
</div>
</div>
);
}
window.Projects = Projects;
+42
View File
@@ -0,0 +1,42 @@
# Scarf macOS UI Kit
A high-fidelity React recreation of the Scarf macOS app, built against the codebase at `awizemann/scarf` (SwiftUI). It mirrors the real navigation hierarchy from `SidebarView.swift` and the visual rhythm of the actual SwiftUI views (`Dashboard`, `RichChat`, `Sessions`, `Projects`, `Insights`, etc.).
This kit is **cosmetic** — it gets the visuals exactly right but doesn't replicate the Swift business logic. Use it as a starting point for new flows, mocks, or marketing screenshots.
## Run
Open `index.html` in a browser. No build step.
## Components
| File | What it covers |
|---|---|
| `Common.jsx` | `Btn`, `Pill`, `Card`, `StatCard`, `Field`, `TextInput`, `Toggle`, `EmptyState`, `ContentHeader` |
| `Sidebar.jsx` | Sectioned sidebar (Monitor / Projects / Interact / Configure / Manage) — exact section/item list from `SidebarView.swift` |
| `Dashboard.jsx` | Status row, 7-day stats, recent sessions, recent activity |
| `Sessions.jsx` | Filterable, sortable session table |
| `Insights.jsx` | Token-usage chart, by-model and by-tool-kind breakdowns |
| `Projects.jsx` | Project grid with template / cron / health badges |
| `Chat.jsx` | Three-pane Rich Chat — list, transcript with reasoning + tool-call cards, composer |
## Faithful to the source
Replicated 1:1:
- **Sidebar grouping** — five named sections from `SidebarView.swift` in the same order.
- **Tool-kind colors** — `read=green / edit=blue / execute=orange / fetch=purple / browser=indigo / other=gray`, the same tokens used in `ToolCallCard.swift`.
- **Reasoning disclosure** — collapsed orange "REASONING · N tokens" header that expands to italic muted text, matching `RichAssistantMessageView`.
- **Tool-call card chrome** — left tone-rule, monospace name + truncated arg, success/error/spinner trailing, expandable code preview.
- **Status pills** — green/red dot with same word vocabulary (`Running` / `Errored` / `Idle`).
- **Type rhythm** — SwiftUI `largeTitle / title1 / title2 / headline / subhead / body / caption` mapped to `--text-*` tokens.
## Substitutions
- **Icons** — Lucide for the web. SF Symbols aren't redistributable; Lucide is the closest stroked-line set. Documented in `/README.md` → ICONOGRAPHY.
- **Fonts** — system stack first, then Inter (display/text) and JetBrains Mono (mono) loaded from Google Fonts. On macOS users will see SF Pro / SF Mono.
- **Window chrome** — three traffic-light dots painted by hand. The starter `macos-window.jsx` was tried first but its sidebar slot didn't match Scarf's layout, so the chrome is inlined in `index.html`.
## What's intentionally left blank
The placeholder view wired to every sidebar item that isn't one of the five built screens — Activity, Memory, Skills, Platforms, Personalities, Quick Commands, Credentials, Plugins, Webhooks, Profiles, Tools, MCP Servers, Gateway, Cron, Health, Logs, Settings. Each lands on a polite `EmptyState` so navigation is still satisfying. Build any of them by following `Sessions.jsx` (table view) or `Projects.jsx` (card grid) — Scarf is consistent enough that those two patterns cover almost every CRUD pane.
+222
View File
@@ -0,0 +1,222 @@
// Sessions list view — with filters (incl. project filter) and a detail row.
function Sessions() {
const [filter, setFilter] = React.useState('all');
const [project, setProject] = React.useState('all'); // project filter
const [projectMenuOpen, setProjectMenuOpen] = React.useState(false);
const projectMenuRef = React.useRef();
React.useEffect(() => {
function onDoc(e) {
if (projectMenuRef.current && !projectMenuRef.current.contains(e.target)) {
setProjectMenuOpen(false);
}
}
if (projectMenuOpen) {
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}
}, [projectMenuOpen]);
const filters = [
{ id: 'all', label: 'All', count: 847 },
{ id: 'today', label: 'Today', count: 24 },
{ id: 'starred', label: 'Starred', count: 6 },
];
const allRows = [
{ id: 1, project: 'scarf', title: 'Cron diagnostics', model: 'sonnet-4.5', msgs: 14, tokens: '12,847', cost: '$0.04', time: '14m ago', status: 'active' },
{ id: 2, project: 'hermes-blog', title: 'Release notes draft', model: 'haiku-4.5', msgs: 8, tokens: '3,210', cost: '$0.01', time: '42m ago', status: 'idle' },
{ id: 3, project: 'hermes-blog', title: 'PR review summary', model: 'sonnet-4.5', msgs: 22, tokens: '24,108', cost: '$0.08', time: '2h ago', status: 'idle' },
{ id: 4, project: '—', title: 'What model handles function calls best?', model: 'haiku-4.5', msgs: 4, tokens: '284', cost: '$0.00', time: '3h ago', status: 'idle' },
{ id: 5, project: 'scarf', title: 'Memory layout question', model: 'sonnet-4.5', msgs: 11, tokens: '4,892', cost: '$0.02', time: 'yesterday', status: 'idle' },
{ id: 6, project: 'scarf', title: 'Refactor SidebarSection enum', model: 'sonnet-4.5', msgs: 31, tokens: '38,221', cost: '$0.13', time: 'yesterday', status: 'errored' },
{ id: 7, project: 'hermes-blog', title: 'Twitter recap thread', model: 'haiku-4.5', msgs: 6, tokens: '1,247', cost: '$0.00', time: '2 days ago', status: 'idle' },
{ id: 8, project: '—', title: 'Find a good local TTS model', model: 'sonnet-4.5', msgs: 19, tokens: '8,743', cost: '$0.03', time: '3 days ago', status: 'idle' },
];
// Build project facet — counts per project, plus an "Unscoped" bucket.
const projectCounts = allRows.reduce((acc, r) => {
acc[r.project] = (acc[r.project] || 0) + 1;
return acc;
}, {});
const projects = [
{ id: 'all', label: 'All projects', icon: 'layers', count: allRows.length },
...Object.keys(projectCounts).filter(k => k !== '—').sort().map(k => ({
id: k, label: k, icon: 'folder', count: projectCounts[k],
})),
{ id: '—', label: 'Unscoped', icon: 'ghost', count: projectCounts['—'] || 0 },
];
const rows = allRows.filter(r => project === 'all' ? true : r.project === project);
const activeProject = projects.find(p => p.id === project) || projects[0];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Sessions"
subtitle="Every conversation across projects, agents, and models"
actions={<><Btn icon="filter">Filter</Btn><Btn icon="download">Export</Btn></>} />
<div style={{ padding: '14px 28px 0', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
{filters.map(f => (
<div key={f.id} onClick={() => setFilter(f.id)} style={{
padding: '4px 11px', borderRadius: 999, cursor: 'pointer', fontSize: 12,
fontWeight: 500,
background: filter === f.id ? 'var(--accent)' : 'var(--bg-quaternary)',
color: filter === f.id ? '#fff' : 'var(--fg)',
display: 'flex', alignItems: 'center', gap: 5,
}}>{f.label}<span style={{
opacity: 0.7, fontFamily: 'var(--font-mono)',
}}>{f.count}</span></div>
))}
{/* Vertical separator */}
<div style={{ width: 1, height: 18, background: 'var(--border)', margin: '0 4px' }}></div>
{/* Project filter chip — opens a dropdown */}
<div ref={projectMenuRef} style={{ position: 'relative' }}>
<div onClick={() => setProjectMenuOpen(o => !o)} style={{
padding: '4px 6px 4px 11px', borderRadius: 999, cursor: 'pointer', fontSize: 12,
fontWeight: 500,
background: project !== 'all' ? 'var(--accent-tint)' : 'var(--bg-quaternary)',
color: project !== 'all' ? 'var(--accent-active)' : 'var(--fg)',
border: project !== 'all' ? '1px solid var(--accent)' : '1px solid transparent',
display: 'flex', alignItems: 'center', gap: 6,
}}>
<i data-lucide={activeProject.icon}
style={{ width: 12, height: 12 }}></i>
<span>{activeProject.label}</span>
<span style={{ opacity: 0.7, fontFamily: 'var(--font-mono)' }}>{activeProject.count}</span>
{project !== 'all' && (
<i data-lucide="x" onClick={(e) => { e.stopPropagation(); setProject('all'); }}
style={{ width: 12, height: 12, marginLeft: 2, padding: 1, borderRadius: 3 }}></i>
)}
{project === 'all' && (
<i data-lucide="chevron-down" style={{ width: 12, height: 12, marginLeft: 2, opacity: 0.7 }}></i>
)}
</div>
{projectMenuOpen && (
<div style={{
position: 'absolute', top: '100%', left: 0, marginTop: 6, zIndex: 50,
minWidth: 220, padding: 4, background: 'var(--bg-card)',
border: '0.5px solid var(--border)', borderRadius: 9,
boxShadow: 'var(--shadow-lg)', fontFamily: 'var(--font-sans)',
}}>
<div style={{
padding: '6px 10px 4px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em',
}}>Filter by project</div>
{projects.map(p => {
const active = p.id === project;
return (
<div key={p.id} onClick={() => { setProject(p.id); setProjectMenuOpen(false); }}
style={{
display: 'flex', alignItems: 'center', gap: 9, padding: '6px 10px',
borderRadius: 6, cursor: 'pointer', fontSize: 13,
background: active ? 'var(--accent-tint)' : 'transparent',
color: active ? 'var(--accent-active)' : 'var(--fg)',
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--bg-quaternary)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
>
<i data-lucide={p.icon} style={{ width: 13, height: 13,
color: active ? 'var(--accent-active)' : 'var(--fg-muted)' }}></i>
<span style={{ flex: 1,
fontStyle: p.id === '—' ? 'italic' : 'normal',
color: p.id === '—' && !active ? 'var(--fg-muted)' : 'inherit' }}>{p.label}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11,
color: active ? 'var(--accent-active)' : 'var(--fg-faint)' }}>{p.count}</span>
{active && <i data-lucide="check" style={{ width: 13, height: 13 }}></i>}
</div>
);
})}
</div>
)}
</div>
<div style={{ marginLeft: 'auto', position: 'relative' }}>
<i data-lucide="search" style={{
position: 'absolute', left: 8, top: 6, width: 13, height: 13, color: 'var(--fg-faint)'
}}></i>
<input placeholder="Search sessions…" style={{
width: 200, padding: '4px 10px 4px 28px',
border: '1px solid var(--border-strong)', borderRadius: 6,
fontSize: 12, background: 'var(--bg-card)', outline: 'none',
fontFamily: 'var(--font-sans)',
}} />
</div>
</div>
{/* Active filter summary */}
{project !== 'all' && (
<div style={{ padding: '8px 28px 0', fontSize: 12, color: 'var(--fg-muted)' }}>
Showing {rows.length} session{rows.length === 1 ? '' : 's'} from
{' '}<span style={{ color: 'var(--fg)', fontWeight: 500 }}>{activeProject.label}</span>
{' '}·{' '}
<span onClick={() => setProject('all')} style={{
color: 'var(--accent-active)', cursor: 'pointer', textDecoration: 'underline',
textDecorationStyle: 'dotted', textUnderlineOffset: 3,
}}>clear filter</span>
</div>
)}
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 28px 28px' }}>
<Card padding={0}>
<div style={{
display: 'grid', gridTemplateColumns: '120px 1fr 110px 60px 90px 70px 80px 24px',
padding: '8px 14px', borderBottom: '0.5px solid var(--border)',
fontSize: 11, color: 'var(--fg-muted)', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
}}>
<div>Project</div><div>Title</div><div>Model</div>
<div style={{ textAlign: 'right' }}>Msgs</div>
<div style={{ textAlign: 'right' }}>Tokens</div>
<div style={{ textAlign: 'right' }}>Cost</div>
<div style={{ textAlign: 'right' }}>Updated</div>
<div></div>
</div>
{rows.length === 0 && (
<div style={{ padding: 48, textAlign: 'center', color: 'var(--fg-muted)', fontSize: 13 }}>
No sessions match this filter.
</div>
)}
{rows.map(r => (
<div key={r.id} style={{
display: 'grid', gridTemplateColumns: '120px 1fr 110px 60px 90px 70px 80px 24px',
padding: '10px 14px', borderBottom: '0.5px solid var(--border)',
alignItems: 'center', fontSize: 13, cursor: 'pointer', gap: 6,
}} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-quaternary)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<div>
{r.project !== '—'
? <span onClick={(e) => { e.stopPropagation(); setProject(r.project); }}
title={`Filter by ${r.project}`}
style={{ display: 'inline-block' }}>
<Pill tone="accent">{r.project}</Pill>
</span>
: <span style={{ color: 'var(--fg-faint)', fontSize: 11 }}></span>}
</div>
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
display: 'flex', gap: 6, alignItems: 'center' }}>
{r.status === 'active' && <span style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--green-500)' }}></span>}
{r.status === 'errored' && <span style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--red-500)' }}></span>}
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.title}</span>
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)' }}>{r.model}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, textAlign: 'right' }}>{r.msgs}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, textAlign: 'right' }}>{r.tokens}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, textAlign: 'right', color: 'var(--fg-muted)' }}>{r.cost}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', textAlign: 'right' }}>{r.time}</div>
<div style={{ color: 'var(--fg-faint)' }}>
<i data-lucide="chevron-right" style={{ width: 14, height: 14 }}></i>
</div>
</div>
))}
</Card>
</div>
</div>
);
}
window.Sessions = Sessions;
+189
View File
@@ -0,0 +1,189 @@
// Settings — global preferences. One scrollable page with grouped settings.
function Settings() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const [tab, setTab] = React.useState('general');
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Settings" subtitle="Global preferences for Scarf. Per-project overrides live in each project." />
<div style={{ padding: '12px 32px 0', borderBottom: '0.5px solid var(--border)', background: 'var(--bg-card)' }}>
<Tabs value={tab} onChange={setTab} options={[
{ value: 'general', label: 'General', icon: 'sliders-horizontal' },
{ value: 'appearance', label: 'Appearance', icon: 'palette' },
{ value: 'agent', label: 'Agent', icon: 'cpu' },
{ value: 'permissions', label: 'Permissions', icon: 'shield' },
{ value: 'account', label: 'Account', icon: 'user-circle' },
{ value: 'advanced', label: 'Advanced', icon: 'wrench' },
]} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px', maxWidth: 880 }}>
{tab === 'general' && <GeneralTab />}
{tab === 'appearance' && <AppearanceTab />}
{tab === 'agent' && <AgentTab />}
{tab === 'permissions' && <PermissionsTab />}
{tab === 'account' && <AccountTab />}
{tab === 'advanced' && <AdvancedTab />}
</div>
</div>
);
}
function GeneralTab() {
return <>
<SettingsGroup title="Workspace">
<SettingsRow icon="folder" title="Default project location"
description="New projects are created here unless overridden."
control={<Btn size="sm" icon="folder-open">~/Projects</Btn>} />
<SettingsRow icon="terminal" title="Default shell"
description="Used when the agent runs commands."
control={<Select value="zsh" options={[{value:'zsh',label:'/bin/zsh'},{value:'bash',label:'/bin/bash'},{value:'fish',label:'/usr/local/bin/fish'}]} />} />
<SettingsRow icon="globe" title="Locale" description="Affects date and number formatting."
control={<Select value="en-US" options={[{value:'en-US',label:'English (US)'},{value:'en-GB',label:'English (UK)'},{value:'de-DE',label:'Deutsch'}]} />} last />
</SettingsGroup>
<SettingsGroup title="Notifications">
<SettingsRow icon="bell" title="Approval requests" description="Notify when the agent needs permission to run a tool."
control={<Toggle on={true} />} />
<SettingsRow icon="check-circle" title="Run completion" description="Ping when long-running tasks finish."
control={<Toggle on={true} />} />
<SettingsRow icon="alert-triangle" title="Errors only" description="Suppress non-error notifications."
control={<Toggle on={false} />} last />
</SettingsGroup>
<SettingsGroup title="Updates">
<SettingsRow icon="download" title="Auto-update Scarf"
description="Currently on 0.14.2 — 0.15.0 available."
control={<Btn size="sm" kind="primary">Install 0.15.0</Btn>} last />
</SettingsGroup>
</>;
}
function AppearanceTab() {
return <>
<SettingsGroup title="Theme">
<SettingsRow icon="sun" title="Color mode" description="Light is the only mode shipped in this kit."
control={<Segmented value="light" options={[
{ value: 'light', label: 'Light', icon: 'sun' },
{ value: 'dark', label: 'Dark', icon: 'moon' },
{ value: 'auto', label: 'Auto', icon: 'monitor' },
]} />} />
<SettingsRow icon="droplet" title="Accent" description="Scarf uses a warm rust accent across the app."
control={
<div style={{ display: 'flex', gap: 6 }}>
{['#C25A2A','#A8741F','#7E5BA9','#3F8A6E','#3F6BA9','#1F1B16'].map((c,i) =>
<div key={i} style={{ width: 22, height: 22, borderRadius: 11, background: c,
border: i === 0 ? '2px solid var(--fg)' : '0.5px solid var(--border)', cursor: 'pointer' }} />)}
</div>} last />
</SettingsGroup>
<SettingsGroup title="Density & type">
<SettingsRow icon="rows-3" title="UI density"
control={<Segmented value="comfy" options={[
{ value: 'compact', label: 'Compact' }, { value: 'comfy', label: 'Comfortable' }, { value: 'roomy', label: 'Roomy' },
]} />} />
<SettingsRow icon="type" title="Mono font" description="Used in code blocks, logs, and identifiers."
control={<Select value="berkeley" options={[
{ value: 'berkeley', label: 'Berkeley Mono' },
{ value: 'jetbrains', label: 'JetBrains Mono' },
{ value: 'sf-mono', label: 'SF Mono' },
]} />} last />
</SettingsGroup>
</>;
}
function AgentTab() {
return <>
<SettingsGroup title="Default model" description="Used when no personality overrides it.">
<SettingsRow icon="sparkles" title="Model"
control={<Select value="sonnet" options={[
{ value: 'sonnet', label: 'claude-sonnet-4.5' }, { value: 'opus', label: 'claude-opus-4.1' }, { value: 'haiku', label: 'claude-haiku-4.5' },
]} />} />
<SettingsRow icon="thermometer" title="Temperature"
description="Lower is more deterministic. Default 0.4."
control={<TextInput value="0.4" mono />} />
<SettingsRow icon="cpu" title="Max tokens out"
control={<TextInput value="4096" mono />} last />
</SettingsGroup>
<SettingsGroup title="Behavior">
<SettingsRow icon="message-square" title="Stream responses"
control={<Toggle on={true} />} />
<SettingsRow icon="fast-forward" title="Aggressive tool batching"
description="Allow multiple parallel tool calls per turn." control={<Toggle on={true} />} />
<SettingsRow icon="rotate-cw" title="Retry on transient errors"
control={<Toggle on={true} />} last />
</SettingsGroup>
</>;
}
function PermissionsTab() {
return <>
<SettingsGroup title="Defaults" description="Override per-tool in Tools, per-project in each project.">
<SettingsRow icon="book-open" title="Read filesystem"
control={<Pill tone="green" dot>auto</Pill>} />
<SettingsRow icon="file-edit" title="Write filesystem"
control={<Pill tone="amber" dot>approve</Pill>} />
<SettingsRow icon="terminal" title="Execute commands"
control={<Pill tone="amber" dot>approve</Pill>} />
<SettingsRow icon="globe" title="Network access"
control={<Pill tone="green" dot>auto</Pill>} last />
</SettingsGroup>
<SettingsGroup title="Deny rules" description="Patterns the agent can never run.">
<SettingsRow icon="ban" title="rm -rf /"
control={<Btn size="sm">Edit</Btn>} />
<SettingsRow icon="ban" title="git push --force* (origin/main, origin/prod)"
control={<Btn size="sm">Edit</Btn>} />
<SettingsRow icon="plus" title="Add rule" control={<Btn size="sm" kind="primary">New</Btn>} last />
</SettingsGroup>
</>;
}
function AccountTab() {
return <>
<SettingsGroup title="Account">
<SettingsRow icon="user-circle" title="Aurora Wong"
description="aurora@wizemann.com — connected via Anthropic Console"
control={<Btn size="sm">Sign out</Btn>} last />
</SettingsGroup>
<SettingsGroup title="Plan & billing">
<SettingsRow icon="zap" title="Team — 5 seats"
description="Renews May 12. $99/mo."
control={<Btn size="sm" icon="external-link">Manage</Btn>} />
<SettingsRow icon="bar-chart-2" title="Usage this month"
description="$42.18 of $200 cap"
control={<div style={{ width: 140 }}><ProgressBar value={21} /></div>} last />
</SettingsGroup>
<SettingsGroup title="Danger zone">
<SettingsRow icon="trash-2" title="Reset all settings"
description="Returns Scarf to defaults. Projects and history are preserved."
control={<Btn size="sm" kind="danger">Reset</Btn>} />
<SettingsRow icon="x-circle" title="Delete account"
description="Permanently delete this account. This cannot be undone."
control={<Btn size="sm" kind="danger-solid">Delete</Btn>} last />
</SettingsGroup>
</>;
}
function AdvancedTab() {
return <>
<SettingsGroup title="Telemetry">
<SettingsRow icon="bar-chart" title="Anonymous usage data"
description="Helps improve Scarf. No prompts or file contents are sent."
control={<Toggle on={true} />} />
<SettingsRow icon="bug" title="Crash reports"
control={<Toggle on={true} />} last />
</SettingsGroup>
<SettingsGroup title="Experimental">
<SettingsRow icon="flask-conical" title="Multi-agent fan-out"
description="Let one agent spawn focused subagents." control={<Toggle on={false} />} />
<SettingsRow icon="flask-conical" title="Background reasoning"
description="Pre-compute likely next steps while you type." control={<Toggle on={false} />} last />
</SettingsGroup>
<SettingsGroup title="Storage" description="Local-only data on this device.">
<SettingsRow icon="database" title="Project history" description="14.2 GB across 11 projects"
control={<Btn size="sm">Manage</Btn>} />
<SettingsRow icon="hard-drive" title="Cache"
description="412 MB"
control={<Btn size="sm">Clear</Btn>} last />
</SettingsGroup>
</>;
}
window.Settings = Settings;
+95
View File
@@ -0,0 +1,95 @@
// Scarf Sidebar — sectioned nav matching SidebarView.swift
// Sections: Monitor / Projects / Interact / Configure / Manage
const SIDEBAR_SECTIONS = [
{ title: 'Monitor', items: [
{ id: 'dashboard', label: 'Dashboard', icon: 'layout-dashboard' },
{ id: 'insights', label: 'Insights', icon: 'bar-chart-3' },
{ id: 'sessions', label: 'Sessions', icon: 'messages-square' },
{ id: 'activity', label: 'Activity', icon: 'activity' },
]},
{ title: 'Projects', items: [
{ id: 'projects', label: 'Projects', icon: 'folder' },
]},
{ title: 'Interact', items: [
{ id: 'chat', label: 'Chat', icon: 'sparkles' },
{ id: 'memory', label: 'Memory', icon: 'database' },
{ id: 'skills', label: 'Skills', icon: 'wand-2' },
]},
{ title: 'Configure', items: [
{ id: 'platforms', label: 'Platforms', icon: 'cloud' },
{ id: 'personalities', label: 'Personalities', icon: 'user-circle' },
{ id: 'quickCommands', label: 'Quick Commands', icon: 'zap' },
{ id: 'credentialPools', label: 'Credentials', icon: 'key' },
{ id: 'plugins', label: 'Plugins', icon: 'puzzle' },
{ id: 'webhooks', label: 'Webhooks', icon: 'webhook' },
{ id: 'profiles', label: 'Profiles', icon: 'users' },
]},
{ title: 'Manage', items: [
{ id: 'tools', label: 'Tools', icon: 'wrench' },
{ id: 'mcpServers', label: 'MCP Servers', icon: 'server' },
{ id: 'gateway', label: 'Gateway', icon: 'network' },
{ id: 'cron', label: 'Cron', icon: 'clock' },
{ id: 'health', label: 'Health', icon: 'stethoscope' },
{ id: 'logs', label: 'Logs', icon: 'file-text' },
{ id: 'settings', label: 'Settings', icon: 'settings' },
]},
];
function ScarfSidebar({ active, onSelect }) {
return (
<aside style={{
width: 224, height: '100%', display: 'flex', flexDirection: 'column',
background: 'rgba(243, 242, 245, 0.7)',
backdropFilter: 'blur(40px) saturate(180%)',
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
borderRight: '0.5px solid var(--border)',
paddingTop: 38, // space for traffic lights
fontFamily: 'var(--font-sans)',
}}>
<div style={{ padding: '0 16px 12px', display: 'flex', alignItems: 'center', gap: 8 }}>
<img src="../../assets/scarf-app-icon-128.png" width="22" height="22" style={{ borderRadius: 5 }} alt="" />
<div style={{ fontSize: 14, fontWeight: 600, letterSpacing: '-0.01em' }}>Scarf</div>
<div style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>local</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '4px 8px 16px' }}>
{SIDEBAR_SECTIONS.map(sec => (
<div key={sec.title} style={{ marginBottom: 14 }}>
<div style={{
padding: '6px 10px 4px', fontSize: 10.5, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em'
}}>{sec.title}</div>
{sec.items.map(it => {
const isActive = active === it.id;
return (
<div key={it.id} onClick={() => onSelect && onSelect(it.id)}
style={{
display: 'flex', alignItems: 'center', gap: 9,
padding: '5px 10px', borderRadius: 6, cursor: 'pointer',
fontSize: 13, fontWeight: isActive ? 500 : 400,
color: isActive ? 'var(--accent-active)' : 'var(--fg)',
background: isActive ? 'var(--accent-tint)' : 'transparent',
transition: 'background 120ms',
}}>
<i data-lucide={it.icon} style={{ width: 15, height: 15 }}></i>
<span>{it.label}</span>
</div>
);
})}
</div>
))}
</div>
<div style={{
padding: '10px 14px', borderTop: '0.5px solid var(--border)',
display: 'flex', alignItems: 'center', gap: 8, fontSize: 12
}}>
<div style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--green-500)' }}></div>
<span style={{ color: 'var(--fg-muted)' }}>Hermes running</span>
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', color: 'var(--fg-faint)', fontSize: 11 }}>v0.42</span>
</div>
</aside>
);
}
window.ScarfSidebar = ScarfSidebar;
window.SIDEBAR_SECTIONS = SIDEBAR_SECTIONS;
+222
View File
@@ -0,0 +1,222 @@
// Tools — registry of every callable tool, with status, kind, and a
// permission policy. Two-pane: list of tools (left), detail (right).
const TOOL_KIND_TONES = {
read: { color: 'var(--green-500)', tint: 'var(--green-100)', icon: 'book-open' },
edit: { color: 'var(--blue-500)', tint: 'var(--blue-100)', icon: 'file-edit' },
execute: { color: 'var(--orange-500)', tint: 'var(--orange-100)', icon: 'terminal' },
fetch: { color: 'var(--purple-tool-500)', tint: '#EFE0F8', icon: 'globe' },
browser: { color: 'var(--indigo-500)', tint: '#E0E5F8', icon: 'compass' },
mcp: { color: 'var(--accent)', tint: 'var(--accent-tint)',icon: 'server' },
};
const TOOLS_DATA = [
// Built-in
{ id: 'read_file', kind: 'read', source: 'built-in', server: '—', enabled: true, calls7d: 1284, lastUsed: '2m ago', policy: 'auto', desc: 'Read a file from disk by path. Honors hidden-file setting.' },
{ id: 'write_file', kind: 'edit', source: 'built-in', server: '—', enabled: true, calls7d: 412, lastUsed: '14m ago', policy: 'approve-write', desc: 'Write content to a file, creating parent directories as needed.' },
{ id: 'apply_patch', kind: 'edit', source: 'built-in', server: '—', enabled: true, calls7d: 348, lastUsed: '14m ago', policy: 'approve-write', desc: 'Apply a unified-diff patch to existing files.' },
{ id: 'list_files', kind: 'read', source: 'built-in', server: '—', enabled: true, calls7d: 928, lastUsed: '32m ago', policy: 'auto', desc: 'List entries in a directory, optionally recursive.' },
{ id: 'execute', kind: 'execute', source: 'built-in', server: '—', enabled: true, calls7d: 661, lastUsed: '14m ago', policy: 'approve-exec', desc: 'Run a shell command. Subject to gateway approval policy.' },
{ id: 'web_fetch', kind: 'fetch', source: 'built-in', server: '—', enabled: true, calls7d: 184, lastUsed: '1h ago', policy: 'auto', desc: 'Fetch a URL and return the extracted text.' },
{ id: 'web_search', kind: 'fetch', source: 'built-in', server: '—', enabled: true, calls7d: 92, lastUsed: '3h ago', policy: 'auto', desc: 'Search the public web. Returns top 10 results.' },
{ id: 'browser_navigate', kind: 'browser', source: 'built-in', server: '—', enabled: false, calls7d: 0, lastUsed: 'never', policy: 'approve-all', desc: 'Drive a Chromium instance for live page interaction.' },
// MCP
{ id: 'github__list_issues', kind: 'mcp', source: 'mcp', server: 'github', enabled: true, calls7d: 84, lastUsed: '42m ago', policy: 'auto', desc: 'List issues for a GitHub repository the user has access to.' },
{ id: 'github__create_pr', kind: 'mcp', source: 'mcp', server: 'github', enabled: true, calls7d: 12, lastUsed: 'yesterday', policy: 'approve-write', desc: 'Open a pull request from a branch.' },
{ id: 'linear__list_issues', kind: 'mcp', source: 'mcp', server: 'linear', enabled: true, calls7d: 38, lastUsed: '2h ago', policy: 'auto', desc: 'Query Linear issues with filters.' },
{ id: 'slack__send_message', kind: 'mcp', source: 'mcp', server: 'slack', enabled: false, calls7d: 0, lastUsed: 'never', policy: 'approve-all', desc: 'Post a message to a Slack channel as the connected user.' },
{ id: 'postgres__query', kind: 'mcp', source: 'mcp', server: 'postgres-prod', enabled: true, calls7d: 14, lastUsed: '4h ago', policy: 'approve-write', desc: 'Run read-only SQL against the configured database.' },
];
function Tools() {
const [active, setActive] = React.useState('execute');
const [filter, setFilter] = React.useState('all');
const [search, setSearch] = React.useState('');
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const filtered = TOOLS_DATA.filter(t => {
if (filter === 'enabled' && !t.enabled) return false;
if (filter === 'mcp' && t.source !== 'mcp') return false;
if (filter === 'builtin' && t.source !== 'built-in') return false;
if (search && !t.id.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
const tool = TOOLS_DATA.find(t => t.id === active) || TOOLS_DATA[0];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Tools"
subtitle="Every callable tool the agent can use, plus their gateway policy"
actions={<><Btn icon="rotate-cw">Sync</Btn><Btn kind="primary" icon="plus">Register tool</Btn></>} />
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
{/* List */}
<div style={{ width: 380, borderRight: '0.5px solid var(--border)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-card)' }}>
<div style={{ padding: '14px 14px 8px', display: 'flex', flexDirection: 'column', gap: 10 }}>
<TextInput value={search} onChange={setSearch} leftIcon="search" placeholder="Search tools…" mono />
<Segmented value={filter} onChange={setFilter} size="sm" options={[
{ value: 'all', label: 'All', count: TOOLS_DATA.length },
{ value: 'enabled', label: 'Enabled', count: TOOLS_DATA.filter(t => t.enabled).length },
{ value: 'mcp', label: 'MCP', count: TOOLS_DATA.filter(t => t.source === 'mcp').length },
{ value: 'builtin', label: 'Built-in', count: TOOLS_DATA.filter(t => t.source === 'built-in').length },
]} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '0 6px 8px' }}>
<ToolGroupHeader>Built-in</ToolGroupHeader>
{filtered.filter(t => t.source === 'built-in').map(t =>
<ToolRow key={t.id} t={t} active={t.id === active} onClick={() => setActive(t.id)} />
)}
<ToolGroupHeader>MCP servers</ToolGroupHeader>
{filtered.filter(t => t.source === 'mcp').map(t =>
<ToolRow key={t.id} t={t} active={t.id === active} onClick={() => setActive(t.id)} />
)}
</div>
</div>
{/* Detail */}
<div style={{ flex: 1, overflowY: 'auto', background: 'var(--bg)', padding: '24px 32px' }}>
<ToolDetail tool={tool} />
</div>
</div>
</div>
);
}
function ToolGroupHeader({ children }) {
return (
<div style={{
padding: '12px 10px 4px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em',
}}>{children}</div>
);
}
function ToolRow({ t, active, onClick }) {
const tone = TOOL_KIND_TONES[t.kind] || TOOL_KIND_TONES.read;
const [hover, setHover] = React.useState(false);
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
padding: '8px 10px', borderRadius: 7, cursor: 'pointer', marginBottom: 1,
background: active ? 'var(--accent-tint)' : (hover ? 'var(--bg-quaternary)' : 'transparent'),
display: 'flex', alignItems: 'center', gap: 9,
}}>
<div style={{
width: 24, height: 24, borderRadius: 6, background: tone.tint, color: tone.color,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<i data-lucide={tone.icon} style={{ width: 13, height: 13 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500, fontFamily: 'var(--font-mono)',
color: active ? 'var(--accent-active)' : 'var(--fg)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.id}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'var(--fg-faint)', marginTop: 1 }}>
{t.server !== '—' && <span style={{ fontFamily: 'var(--font-mono)' }}>{t.server}</span>}
<span>· {t.calls7d.toLocaleString()} calls</span>
</div>
</div>
{t.enabled
? <Dot tone="green" />
: <Dot tone="gray" />}
</div>
);
}
function ToolDetail({ tool }) {
const tone = TOOL_KIND_TONES[tool.kind] || TOOL_KIND_TONES.read;
return (
<>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, marginBottom: 22 }}>
<div style={{
width: 44, height: 44, borderRadius: 9, background: tone.tint, color: tone.color,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide={tone.icon} style={{ width: 22, height: 22 }}></i>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div className="scarf-h2" style={{ fontFamily: 'var(--font-mono)', fontSize: 22 }}>{tool.id}</div>
{tool.enabled ? <Pill tone="green" dot>enabled</Pill> : <Pill tone="gray" dot>disabled</Pill>}
</div>
<div style={{ fontSize: 13, color: 'var(--fg-muted)', maxWidth: 560 }}>{tool.desc}</div>
</div>
<Toggle on={tool.enabled} size="lg" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 24 }}>
<StatCard label="Calls (7d)" value={tool.calls7d.toLocaleString()} />
<StatCard label="Last used" value={tool.lastUsed} />
<StatCard label="Avg duration" value="142 ms" sub="p95: 920 ms" />
<StatCard label="Error rate" value="0.4%" sub="3 of 661 calls" />
</div>
<SettingsGroup title="Permissions" description="Applied at the gateway. Per-project profiles can override.">
<SettingsRow icon="shield-check" title="Default policy"
description={POLICY_DESC[tool.policy]}
control={<Select value={tool.policy} options={[
{ value: 'auto', label: 'Auto-approve' },
{ value: 'approve-write', label: 'Approve writes' },
{ value: 'approve-exec', label: 'Approve every call' },
{ value: 'approve-all', label: 'Approve every call (strict)' },
{ value: 'deny', label: 'Deny' },
]} />} />
<SettingsRow icon="users" title="Per-project overrides"
description="2 projects override the default policy for this tool."
control={<Btn size="sm" icon="external-link">Manage</Btn>} last />
</SettingsGroup>
<SettingsGroup title="Schema" description="JSON Schema declared by the tool. Read-only.">
<div style={{ background: 'var(--gray-900)', color: '#E8E1D2', padding: 14,
fontFamily: 'var(--font-mono)', fontSize: 11.5, lineHeight: 1.55,
borderRadius: '0 0 10px 10px' }}>
{`{
"name": "${tool.id}",
"input_schema": {
"type": "object",
"properties": {
"command": { "type": "string", "description": "Shell command" },
"cwd": { "type": "string", "default": "$PWD" },
"timeout": { "type": "integer", "default": 60 }
},
"required": ["command"]
}
}`}
</div>
</SettingsGroup>
<SettingsGroup title="Recent calls">
<RecentCallRow when="2m ago" args="hermes cron status daily-summary" status="ok" duration="1.4s" />
<RecentCallRow when="14m ago" args="git log --oneline -n 20" status="ok" duration="86ms" />
<RecentCallRow when="1h ago" args="npm test -- --watch=false" status="ok" duration="14.2s" />
<RecentCallRow when="2h ago" args="rm -rf node_modules" status="denied" duration="—" last />
</SettingsGroup>
</>
);
}
const POLICY_DESC = {
'auto': 'Always invoke without asking.',
'approve-write': 'Pause for approval when the tool changes state.',
'approve-exec': 'Pause for approval before every call.',
'approve-all': 'Pause for approval before every call. Strictest mode.',
'deny': 'Reject the call. Tool appears in lists but cannot be invoked.',
};
function RecentCallRow({ when, args, status, duration, last }) {
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '10px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<span style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)', width: 90 }}>{when}</span>
<span style={{ flex: 1, fontFamily: 'var(--font-mono)', fontSize: 12,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', color: 'var(--fg-muted)' }}>{args}</span>
{status === 'ok' && <Pill tone="green" size="sm" icon="check">ok</Pill>}
{status === 'denied' && <Pill tone="red" size="sm" icon="ban">denied</Pill>}
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 60, textAlign: 'right' }}>{duration}</span>
</div>
);
}
window.Tools = Tools;
File diff suppressed because one or more lines are too long
+141
View File
@@ -0,0 +1,141 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scarf — UI Kit</title>
<link rel="stylesheet" href="../colors_and_type.css">
<style>
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden;
background: linear-gradient(135deg, #EFC59E 0%, #C25A2A 60%, #5C220F 100%); }
@keyframes scarfSpin { to { transform: rotate(360deg); } }
#root { height: 100%; }
.scarf-app {
display: flex; height: 100vh;
background: var(--bg);
overflow: hidden;
}
.scarf-traffic {
position: absolute; top: 14px; left: 18px;
display: flex; gap: 8px; z-index: 100;
}
.scarf-traffic .dot { width: 12px; height: 12px; border-radius: 50%; }
.scarf-traffic .dot.r { background: #FE5F57; }
.scarf-traffic .dot.y { background: #FEBB2E; }
.scarf-traffic .dot.g { background: #28C840; }
.scarf-content {
flex: 1; display: flex; flex-direction: column;
min-width: 0; padding-top: 38px;
background: var(--bg);
}
@keyframes pulseScarf { 0%,100% { opacity:1 } 50% { opacity: 0.3 } }
/* placeholder for contentEditable */
[contenteditable][data-placeholder]:empty:before {
content: attr(data-placeholder); color: var(--fg-faint); pointer-events: none;
}
/* scrollbar tweak */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: rgba(28,26,32,0.15); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(28,26,32,0.25); }
</style>
</head>
<body>
<div id="root"></div>
<template id="__bundler_thumbnail">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#C25A2A"/>
<text x="50" y="62" text-anchor="middle" font-family="Georgia, serif"
font-size="48" font-style="italic" fill="#FAF7F2" font-weight="600">S</text>
</svg>
</template>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<script type="text/babel" src="Common.jsx"></script>
<script type="text/babel" src="Sidebar.jsx"></script>
<script type="text/babel" src="Dashboard.jsx"></script>
<script type="text/babel" src="Sessions.jsx"></script>
<script type="text/babel" src="Insights.jsx"></script>
<script type="text/babel" src="Projects.jsx"></script>
<script type="text/babel" src="Chat.jsx"></script>
<script type="text/babel" src="Settings.jsx"></script>
<script type="text/babel" src="Tools.jsx"></script>
<script type="text/babel" src="MCPServers.jsx"></script>
<script type="text/babel" src="Cron.jsx"></script>
<script type="text/babel" src="Logs.jsx"></script>
<script type="text/babel" src="Memory.jsx"></script>
<script type="text/babel" src="Activity.jsx"></script>
<script type="text/babel" src="Health.jsx"></script>
<script type="text/babel" src="MoreViews.jsx"></script>
<script type="text/babel">
function App() {
const [active, setActive] = React.useState('dashboard');
React.useEffect(() => {
// re-render lucide icons after each route change
requestAnimationFrame(() => window.lucide && window.lucide.createIcons());
}, [active]);
const Views = {
dashboard: Dashboard,
sessions: Sessions,
insights: Insights,
projects: Projects,
chat: Chat,
settings: Settings,
tools: Tools,
mcpServers: MCPServers,
cron: Cron,
logs: Logs,
memory: Memory,
activity: Activity,
health: Health,
personalities: Personalities,
quickCommands: QuickCommands,
platforms: Platforms,
credentialPools: Credentials,
plugins: Plugins,
webhooks: Webhooks,
profiles: Profiles,
gateway: Gateway,
};
const Active = Views[active] || PlaceholderView(active);
return (
<div className="scarf-app" data-screen-label={`Scarf · ${active}`}>
<div className="scarf-traffic">
<span className="dot r"></span><span className="dot y"></span><span className="dot g"></span>
</div>
<ScarfSidebar active={active} onSelect={setActive} />
<div className="scarf-content">
<Active />
</div>
</div>
);
}
function PlaceholderView(name) {
const SIDEBAR_FLAT = SIDEBAR_SECTIONS.flatMap(s => s.items);
const item = SIDEBAR_FLAT.find(i => i.id === name) || { label: name, icon: 'inbox' };
return function Inner() {
return (
<>
<ContentHeader title={item.label} subtitle={`This view isn't fleshed out in the UI kit yet.`} />
<EmptyState icon={item.icon}
title={`${item.label}`}
body={`The Scarf app exposes a dedicated ${item.label} pane here. The kit ships a faithful Dashboard, Sessions, Insights, Projects, and Chat — wire ${item.label} the same way against your data.`}
action={<Btn kind="primary" icon="external-link">Open Scarf docs</Btn>}
/>
</>
);
};
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// Lucide ran once on DOMContentLoaded before React mounted — re-run now that the DOM has icons.
setTimeout(() => window.lucide && window.lucide.createIcons(), 0);
setTimeout(() => window.lucide && window.lucide.createIcons(), 200);
</script>
</body>
</html>
+187
View File
@@ -0,0 +1,187 @@
// MacOS.jsx — Simplified macOS Tahoe (Liquid Glass) window
// Based on the macOS Tahoe UI Kit. No image assets, no dependencies.
// Exports: MacWindow, MacSidebar, MacSidebarItem, MacToolbar, MacGlass, MacTrafficLights
const MAC_FONT = '-apple-system, BlinkMacSystemFont, "SF Pro", "Helvetica Neue", sans-serif';
// ─────────────────────────────────────────────────────────────
// Liquid glass primitive — blur + white tint + inset highlight
// ─────────────────────────────────────────────────────────────
function MacGlass({ children, radius = 296, dark = false, style = {} }) {
return (
<div style={{ position: 'relative', borderRadius: radius, ...style }}>
<div style={{
position: 'absolute', inset: 0, borderRadius: radius,
background: dark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.35)',
backdropFilter: 'blur(40px) saturate(180%)',
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
border: dark ? '0.5px solid rgba(255,255,255,0.12)' : '0.5px solid rgba(255,255,255,0.6)',
boxShadow: dark
? '0 8px 40px rgba(0,0,0,0.2)'
: '0 8px 40px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.4)',
}} />
<div style={{ position: 'relative', zIndex: 1 }}>{children}</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Traffic lights (14px, Tahoe colors)
// ─────────────────────────────────────────────────────────────
function MacTrafficLights({ style = {} }) {
const dot = (bg) => (
<div style={{
width: 14, height: 14, borderRadius: '50%', background: bg,
border: '0.5px solid rgba(0,0,0,0.1)',
}} />
);
return (
<div style={{ display: 'flex', gap: 9, alignItems: 'center', padding: 1, ...style }}>
{dot('#ff736a')}{dot('#febc2e')}{dot('#19c332')}
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Toolbar — title + single glass pill icon
// ─────────────────────────────────────────────────────────────
function MacToolbar({ title = 'Folder' }) {
return (
<div style={{
display: 'flex', gap: 8, alignItems: 'center', padding: 8, flexShrink: 0,
}}>
{/* title */}
<div style={{
fontFamily: MAC_FONT, fontSize: 15, fontWeight: 700,
color: 'rgba(0,0,0,0.85)', whiteSpace: 'nowrap', paddingLeft: 8,
}}>{title}</div>
<div style={{ flex: 1 }} />
{/* single action */}
<MacGlass>
<div style={{
width: 36, height: 36, display: 'flex',
alignItems: 'center', justifyContent: 'center',
}}>
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#4c4c4c', opacity: 0.4 }} />
</div>
</MacGlass>
{/* search */}
<MacGlass>
<div style={{
width: 140, height: 36, display: 'flex', alignItems: 'center',
gap: 6, padding: '0 12px',
}}>
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
<circle cx="5.5" cy="5.5" r="4" stroke="#727272" strokeWidth="1.5"/>
<path d="M8.5 8.5l3 3" stroke="#727272" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span style={{
fontFamily: MAC_FONT, fontSize: 13, fontWeight: 500, color: '#727272',
}}>Search</span>
</div>
</MacGlass>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Sidebar — frosted glass panel floating inside the window
// ─────────────────────────────────────────────────────────────
function MacSidebarItem({ label, selected = false }) {
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
height: 24, padding: '4px 10px 4px 6px', margin: '0 10px',
borderRadius: 8, position: 'relative',
fontFamily: MAC_FONT, fontSize: 11, fontWeight: 500,
}}>
{selected && (
<div style={{
position: 'absolute', inset: 0, borderRadius: 8,
background: 'rgba(0,0,0,0.11)', mixBlendMode: 'multiply',
}} />
)}
<div style={{
width: 14, height: 14, borderRadius: '50%',
background: selected ? '#007aff' : 'rgba(0,0,0,0.4)',
opacity: selected ? 1 : 0.5, flexShrink: 0, position: 'relative',
}} />
<span style={{ color: 'rgba(0,0,0,0.85)', position: 'relative' }}>{label}</span>
</div>
);
}
function MacSidebar({ children }) {
return (
<div style={{
width: 220, height: '100%', padding: 8, flexShrink: 0,
position: 'relative', display: 'flex', flexDirection: 'column',
}}>
{/* glass panel */}
<div style={{
position: 'absolute', inset: 8, borderRadius: 18,
background: 'rgba(210,225,245,0.45)',
backdropFilter: 'blur(50px) saturate(200%)',
WebkitBackdropFilter: 'blur(50px) saturate(200%)',
border: '0.5px solid rgba(255,255,255,0.5)',
boxShadow: '0 8px 40px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.35)',
}} />
{/* content */}
<div style={{
position: 'relative', zIndex: 1, padding: '10px 0',
display: 'flex', flexDirection: 'column', gap: 2,
}}>
{/* window controls + sidebar toggle */}
<div style={{
height: 32, display: 'flex', alignItems: 'center',
justifyContent: 'space-between', padding: '0 10px', marginBottom: 4,
}}>
<MacTrafficLights />
</div>
{children}
</div>
</div>
);
}
function MacSidebarHeader({ title }) {
return (
<div style={{
padding: '14px 18px 5px',
fontFamily: MAC_FONT, fontSize: 11, fontWeight: 700,
color: 'rgba(0,0,0,0.5)',
}}>{title}</div>
);
}
// ─────────────────────────────────────────────────────────────
// Window — r:26, big shadow, sidebar + toolbar + content
// ─────────────────────────────────────────────────────────────
function MacWindow({
width = 900, height = 600, title = 'Folder',
sidebar, children,
}) {
return (
<div style={{
width, height, borderRadius: 26, overflow: 'hidden',
background: '#fff',
boxShadow: '0 0 0 1px rgba(0,0,0,0.23), 0 16px 48px rgba(0,0,0,0.35)',
display: 'flex', position: 'relative',
fontFamily: MAC_FONT,
}}>
<MacSidebar>{sidebar}</MacSidebar>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<MacToolbar title={title} />
<div style={{ flex: 1, overflow: 'auto', padding: '4px 8px' }}>
{children}
</div>
</div>
</div>
);
}
Object.assign(window, {
MacWindow, MacSidebar, MacSidebarItem, MacSidebarHeader,
MacToolbar, MacGlass, MacTrafficLights,
});
@@ -96,6 +96,26 @@ public struct ActivityEntry: Identifiable, Sendable {
public let messageContent: String
public let timestamp: Date?
public init(
id: String,
sessionId: String,
toolName: String,
kind: ToolKind,
summary: String,
arguments: String,
messageContent: String,
timestamp: Date?
) {
self.id = id
self.sessionId = sessionId
self.toolName = toolName
self.kind = kind
self.summary = summary
self.arguments = arguments
self.messageContent = messageContent
self.timestamp = timestamp
}
public var prettyArguments: String {
guard let data = arguments.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data, options: []),
+47
View File
@@ -0,0 +1,47 @@
// swift-tools-version: 6.0
// Scarf Design System typed token bridge + SwiftUI component primitives
// for the macOS and iOS apps.
//
// Ships color tokens (rust/amber brand, surfaces, foregrounds, semantic, tool
// kinds) as an Xcode asset catalog and a thin Swift API
// (`ScarfColor`, `ScarfFont`, `ScarfSpace`, `ScarfRadius`, `ScarfShadow`,
// `ScarfPrimaryButton`, `ScarfCard`, `ScarfBadge`, `ScarfTextField`,
// `ScarfSectionHeader`, `ScarfDivider`).
//
// Both app targets (`scarf`, `scarf mobile`) link this package and `import
// ScarfDesign`. The asset catalog is bundled as a `.process` resource so the
// `Color(name, bundle: .module)` lookups in `ScarfTheme.swift` resolve at
// runtime regardless of which app is hosting the bundle.
//
// Platform minimums match the rest of the workspace: macOS 14 + iOS 18.
import PackageDescription
let package = Package(
name: "ScarfDesign",
defaultLocalization: "en",
platforms: [
.macOS(.v14),
.iOS(.v18),
],
products: [
.library(
name: "ScarfDesign",
targets: ["ScarfDesign"]
),
],
targets: [
.target(
name: "ScarfDesign",
path: "Sources/ScarfDesign",
resources: [
.process("ScarfBrand.xcassets"),
],
swiftSettings: [
// Match ScarfCore / ScarfIOS Swift 5 language mode pending
// the workspace-wide bump to strict Swift 6 concurrency.
.swiftLanguageMode(.v5),
]
),
]
)
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.376",
"green": "0.576",
"red": "0.910"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.475",
"green": "0.659",
"red": "0.941"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.165",
"green": "0.353",
"red": "0.761"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.376",
"green": "0.576",
"red": "0.910"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.078",
"green": "0.180",
"red": "0.478"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.267",
"green": "0.471",
"red": "0.847"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.078",
"green": "0.180",
"red": "0.478"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.118",
"green": "0.282",
"red": "0.651"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.118",
"green": "0.282",
"red": "0.651"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.475",
"green": "0.659",
"red": "0.941"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.482",
"green": "0.522",
"red": "0.549"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.400",
"green": "0.435",
"red": "0.459"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.357",
"green": "0.392",
"red": "0.416"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.573",
"green": "0.612",
"red": "0.639"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.078",
"green": "0.094",
"red": "0.102"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.878",
"green": "0.910",
"red": "0.929"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "1.000",
"green": "1.000",
"red": "1.000"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "1.000",
"green": "1.000",
"red": "1.000"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.310",
"green": "0.325",
"red": "0.851"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.392",
"green": "0.408",
"red": "0.890"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.859",
"green": "0.596",
"red": "0.204"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.890",
"green": "0.686",
"red": "0.357"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.463",
"green": "0.659",
"red": "0.165"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.537",
"green": "0.745",
"red": "0.239"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.306",
"green": "0.678",
"red": "0.941"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.443",
"green": "0.745",
"red": "0.957"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.965",
"green": "0.976",
"red": "0.984"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.059",
"green": "0.075",
"red": "0.082"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "1.000",
"green": "1.000",
"red": "1.000"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.094",
"green": "0.110",
"red": "0.122"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.925",
"green": "0.945",
"red": "0.957"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.106",
"green": "0.125",
"red": "0.137"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.145",
"green": "0.165",
"red": "0.176"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.922",
"green": "0.973",
"red": "1.000"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.145",
"green": "0.165",
"red": "0.176"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.922",
"green": "0.973",
"red": "1.000"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,9 @@
{
"info": {
"author": "xcode",
"version": 1
},
"properties": {
"provides-namespace": true
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.463",
"green": "0.659",
"red": "0.165"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.537",
"green": "0.745",
"red": "0.239"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.859",
"green": "0.596",
"red": "0.204"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.890",
"green": "0.686",
"red": "0.357"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.851",
"green": "0.424",
"red": "0.357"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.890",
"green": "0.549",
"red": "0.494"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.306",
"green": "0.678",
"red": "0.941"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.443",
"green": "0.745",
"red": "0.957"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.788",
"green": "0.357",
"red": "0.557"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.831",
"green": "0.494",
"red": "0.655"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,266 @@
//
// ScarfComponents.swift
// Scarf Design System opinionated SwiftUI component primitives.
//
// These mirror the buttons, cards, badges, and inputs used in the Scarf UI kit.
// Keep them small. Reach for them instead of inlining the same `.padding()
// .background() .clipShape()` chain across screens.
//
import SwiftUI
// MARK: - Buttons
public struct ScarfPrimaryButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.onAccent)
.padding(.horizontal, ScarfSpace.s4)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(configuration.isPressed ? ScarfColor.accentActive : ScarfColor.accent)
)
.scarfShadow(.sm)
.opacity(configuration.isPressed ? 0.95 : 1)
}
}
public struct ScarfSecondaryButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.foregroundPrimary)
.padding(.horizontal, ScarfSpace.s4)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(configuration.isPressed
? ScarfColor.borderStrong
: ScarfColor.backgroundSecondary)
.overlay(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.strokeBorder(ScarfColor.borderStrong, lineWidth: 1)
)
)
}
}
public struct ScarfGhostButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.foregroundPrimary)
.padding(.horizontal, ScarfSpace.s3)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(configuration.isPressed
? ScarfColor.accentTint
: Color.clear)
)
}
}
public struct ScarfDestructiveButton: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.scarfStyle(.bodyEmph)
.foregroundStyle(.white)
.padding(.horizontal, ScarfSpace.s4)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(ScarfColor.danger.opacity(configuration.isPressed ? 0.85 : 1.0))
)
}
}
// MARK: - Card
public struct ScarfCard<Content: View>: View {
let padding: CGFloat
let content: () -> Content
public init(padding: CGFloat = ScarfSpace.s4, @ViewBuilder content: @escaping () -> Content) {
self.padding = padding
self.content = content
}
public var body: some View {
content()
.padding(padding)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
.fill(ScarfColor.backgroundSecondary)
)
.overlay(
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
.strokeBorder(ScarfColor.border, lineWidth: 1)
)
.scarfShadow(.sm)
}
}
// MARK: - Badge / Pill
public enum ScarfBadgeKind {
case neutral, brand, success, danger, warning, info
var fill: Color {
switch self {
case .neutral: return ScarfColor.backgroundTertiary
case .brand: return ScarfColor.accentTint
case .success: return ScarfColor.success.opacity(0.16)
case .danger: return ScarfColor.danger.opacity(0.16)
case .warning: return ScarfColor.warning.opacity(0.18)
case .info: return ScarfColor.info.opacity(0.16)
}
}
var fg: Color {
switch self {
case .neutral: return ScarfColor.foregroundMuted
case .brand: return ScarfColor.accent
case .success: return ScarfColor.success
case .danger: return ScarfColor.danger
case .warning: return ScarfColor.warning
case .info: return ScarfColor.info
}
}
}
public struct ScarfBadge: View {
let text: String
let kind: ScarfBadgeKind
public init(_ text: String, kind: ScarfBadgeKind = .neutral) {
self.text = text
self.kind = kind
}
public var body: some View {
Text(text)
.scarfStyle(.captionStrong)
.foregroundStyle(kind.fg)
.padding(.horizontal, ScarfSpace.s2)
.padding(.vertical, 3)
.background(
Capsule().fill(kind.fill)
)
}
}
// MARK: - Inputs
public struct ScarfTextField: View {
let placeholder: String
@Binding var text: String
public init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
}
public var body: some View {
TextField(placeholder, text: $text)
.textFieldStyle(.plain)
.scarfStyle(.body)
.padding(.horizontal, ScarfSpace.s3)
.padding(.vertical, ScarfSpace.s2)
.background(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.fill(ScarfColor.backgroundSecondary)
)
.overlay(
RoundedRectangle(cornerRadius: ScarfRadius.md, style: .continuous)
.strokeBorder(ScarfColor.borderStrong, lineWidth: 1)
)
}
}
// MARK: - Section header
public struct ScarfSectionHeader: View {
let title: String
let subtitle: String?
public init(_ title: String, subtitle: String? = nil) {
self.title = title
self.subtitle = subtitle
}
public var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(title)
.scarfStyle(.captionUppercase)
.foregroundStyle(ScarfColor.foregroundMuted)
if let subtitle {
Text(subtitle)
.scarfStyle(.footnote)
.foregroundStyle(ScarfColor.foregroundFaint)
}
}
}
}
// MARK: - Divider
public struct ScarfDivider: View {
public init() {}
public var body: some View {
Rectangle()
.fill(ScarfColor.border)
.frame(height: 1)
}
}
// MARK: - Page header
/// Standard page-level title/subtitle/actions header used at the top of
/// every feature route. Mirrors the `ContentHeader` component in the
/// design system's static-site / ui-kit. Drops a hairline divider at the
/// bottom so feature content can flush against it.
public struct ScarfPageHeader<Trailing: View>: View {
let title: String
let subtitle: String?
let trailing: Trailing
public init(_ title: String,
subtitle: String? = nil,
@ViewBuilder trailing: () -> Trailing = { EmptyView() }) {
self.title = title
self.subtitle = subtitle
self.trailing = trailing()
}
public var body: some View {
HStack(alignment: .top, spacing: ScarfSpace.s3) {
VStack(alignment: .leading, spacing: 2) {
Text(title)
.scarfStyle(.title2)
.foregroundStyle(ScarfColor.foregroundPrimary)
if let subtitle {
Text(subtitle)
.scarfStyle(.footnote)
.foregroundStyle(ScarfColor.foregroundMuted)
}
}
Spacer()
trailing
}
.padding(.horizontal, ScarfSpace.s6)
.padding(.top, ScarfSpace.s5)
.padding(.bottom, ScarfSpace.s4)
.overlay(
Rectangle()
.fill(ScarfColor.border)
.frame(height: 1),
alignment: .bottom
)
}
}
@@ -0,0 +1,146 @@
//
// ScarfPreview.swift
// Scarf Design System quick component preview.
//
// Open this file in Xcode and the canvas (P) shows every component at once,
// in light and dark. Use it to sanity-check the bundle works after install.
//
import SwiftUI
public struct ScarfPreviewGallery: View {
@State private var query = ""
@State private var draft = "Hello, Scarf"
public init() {}
public var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: ScarfSpace.s8) {
// Header
VStack(alignment: .leading, spacing: ScarfSpace.s2) {
Text("Scarf Design System")
.scarfStyle(.largeTitle)
.foregroundStyle(ScarfColor.foregroundPrimary)
Text("Component preview — light / dark resolves from the asset catalog.")
.scarfStyle(.subhead)
.foregroundStyle(ScarfColor.foregroundMuted)
}
// Buttons
section("Buttons") {
HStack(spacing: ScarfSpace.s3) {
Button("Primary") {}.buttonStyle(ScarfPrimaryButton())
Button("Secondary") {}.buttonStyle(ScarfSecondaryButton())
Button("Ghost") {}.buttonStyle(ScarfGhostButton())
Button("Delete") {}.buttonStyle(ScarfDestructiveButton())
}
}
// Badges
section("Badges") {
HStack(spacing: ScarfSpace.s2) {
ScarfBadge("Neutral")
ScarfBadge("Brand", kind: .brand)
ScarfBadge("Success", kind: .success)
ScarfBadge("Warning", kind: .warning)
ScarfBadge("Danger", kind: .danger)
ScarfBadge("Info", kind: .info)
}
}
// Inputs
section("Inputs") {
VStack(alignment: .leading, spacing: ScarfSpace.s3) {
ScarfTextField("Search", text: $query)
ScarfTextField("Compose a message", text: $draft)
}
}
// Card
section("Card") {
ScarfCard {
VStack(alignment: .leading, spacing: ScarfSpace.s2) {
ScarfSectionHeader("Connection", subtitle: "anthropic.com")
ScarfDivider()
HStack {
Text("Status")
.scarfStyle(.body)
.foregroundStyle(ScarfColor.foregroundMuted)
Spacer()
ScarfBadge("Connected", kind: .success)
}
HStack {
Text("Last run")
.scarfStyle(.body)
.foregroundStyle(ScarfColor.foregroundMuted)
Spacer()
Text("2 min ago")
.scarfStyle(.bodyEmph)
.foregroundStyle(ScarfColor.foregroundPrimary)
}
}
}
}
// Tool kind swatches (chat)
section("Tool kinds") {
HStack(spacing: ScarfSpace.s3) {
toolSwatch("Bash", ScarfColor.Tool.bash)
toolSwatch("Edit", ScarfColor.Tool.edit)
toolSwatch("Search", ScarfColor.Tool.search)
toolSwatch("Web", ScarfColor.Tool.web)
toolSwatch("Think", ScarfColor.Tool.think)
}
}
// Brand gradient
section("Brand gradient") {
RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous)
.fill(ScarfGradient.brand)
.frame(height: 80)
.overlay(
Text("amber → rust → deep")
.scarfStyle(.subhead)
.foregroundStyle(.white)
)
}
}
.padding(ScarfSpace.s8)
}
.background(ScarfColor.backgroundPrimary.ignoresSafeArea())
}
@ViewBuilder
private func section<Content: View>(_ title: String,
@ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: ScarfSpace.s3) {
Text(title)
.scarfStyle(.captionUppercase)
.foregroundStyle(ScarfColor.foregroundMuted)
content()
}
}
private func toolSwatch(_ name: String, _ color: Color) -> some View {
VStack(spacing: ScarfSpace.s1) {
Circle().fill(color).frame(width: 24, height: 24)
Text(name)
.scarfStyle(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
}
}
}
#Preview("Light") {
ScarfPreviewGallery()
.frame(width: 720, height: 900)
.preferredColorScheme(.light)
}
#Preview("Dark") {
ScarfPreviewGallery()
.frame(width: 720, height: 900)
.preferredColorScheme(.dark)
}
@@ -0,0 +1,154 @@
//
// ScarfTheme.swift
// Scarf Design System Swift token bridge
//
// Mirrors colors_and_type.css. All colors resolve from ScarfBrand.xcassets,
// so light/dark variants come from the asset catalog automatically.
//
// Usage:
// Text("Hello").foregroundStyle(ScarfColor.foregroundPrimary)
// RoundedRectangle(cornerRadius: ScarfRadius.lg)
// .fill(ScarfColor.backgroundSecondary)
// .overlay(RoundedRectangle(cornerRadius: ScarfRadius.lg)
// .strokeBorder(ScarfColor.border, lineWidth: 1))
//
// Drop-in: add this file + ScarfBrand.xcassets to your target. Nothing else.
//
import SwiftUI
// MARK: - Colors
/// All Scarf brand colors. Resolves from ScarfBrand.xcassets (light + dark).
public enum ScarfColor {
fileprivate static func asset(_ name: String) -> Color {
Color(name, bundle: .module)
}
// Brand
public static let brandRust = asset("Brand/BrandRust")
public static let brandRustHover = asset("Brand/BrandRustHover")
public static let brandRustActive = asset("Brand/BrandRustActive")
public static let brandAmber = asset("Brand/BrandAmber")
public static let brandRustDeep = asset("Brand/BrandRustDeep")
/// Semantic alias: the "primary" accent. Use this in component code,
/// not `brandRust` directly it lets you re-skin without a refactor.
public static var accent: Color { brandRust }
public static var accentHover: Color { brandRustHover }
public static var accentActive: Color { brandRustActive }
/// Tinted accent for hover halos, selection backgrounds.
public static var accentTint: Color { brandRust.opacity(0.10) }
public static var accentTintStrong: Color { brandRust.opacity(0.18) }
// Surfaces
public static let backgroundPrimary = asset("Surface/BackgroundPrimary")
public static let backgroundSecondary = asset("Surface/BackgroundSecondary")
public static let backgroundTertiary = asset("Surface/BackgroundTertiary")
/// Use at low alpha (0.040.10) for subtle fills/dividers.
public static var border: Color { asset("Surface/Border").opacity(0.08) }
public static var borderStrong: Color { asset("Surface/BorderStrong").opacity(0.14) }
// Foreground
public static let foregroundPrimary = asset("Foreground/ForegroundPrimary")
public static let foregroundMuted = asset("Foreground/ForegroundMuted")
public static let foregroundFaint = asset("Foreground/ForegroundFaint")
public static let onAccent = asset("Foreground/OnAccent")
// Semantic
public static let success = asset("Semantic/SemanticSuccess")
public static let danger = asset("Semantic/SemanticDanger")
public static let warning = asset("Semantic/SemanticWarning")
public static let info = asset("Semantic/SemanticInfo")
// Tool kinds (chat message decorations)
public enum Tool {
public static let bash = ScarfColor.asset("Tool/ToolBash")
public static let edit = ScarfColor.asset("Tool/ToolEdit")
public static let search = ScarfColor.asset("Tool/ToolSearch")
public static let web = ScarfColor.asset("Tool/ToolWeb")
public static let think = ScarfColor.asset("Tool/ToolThink")
}
}
// MARK: - Gradients
public enum ScarfGradient {
/// Tri-stop amber rust deep rust. Used on app icon, hero buttons, brand splashes.
public static let brand = LinearGradient(
colors: [
Color(red: 0.910, green: 0.576, blue: 0.376), // #E89360
Color(red: 0.761, green: 0.353, blue: 0.165), // #C25A2A
Color(red: 0.478, green: 0.180, blue: 0.078) // #7A2E14
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
/// Soft amber wash for empty states, onboarding moments.
public static let brandSoft = LinearGradient(
colors: [
Color(red: 0.965, green: 0.878, blue: 0.796), // #F6E0CB
Color(red: 0.937, green: 0.773, blue: 0.620) // #EFC59E
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
// MARK: - Radii / spacing / shadow
public enum ScarfRadius {
public static let sm: CGFloat = 4
public static let md: CGFloat = 6
public static let lg: CGFloat = 8
public static let xl: CGFloat = 12
public static let xxl: CGFloat = 14
public static let pill: CGFloat = 999
}
public enum ScarfSpace {
public static let s1: CGFloat = 4
public static let s2: CGFloat = 8
public static let s3: CGFloat = 12
public static let s4: CGFloat = 16
public static let s5: CGFloat = 20
public static let s6: CGFloat = 24
public static let s8: CGFloat = 32
public static let s10: CGFloat = 40
}
public struct ScarfShadow {
public let color: Color
public let radius: CGFloat
public let x: CGFloat
public let y: CGFloat
public static let sm = ScarfShadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
public static let md = ScarfShadow(color: .black.opacity(0.07), radius: 12, x: 0, y: 4)
public static let lg = ScarfShadow(color: .black.opacity(0.10), radius: 24, x: 0, y: 8)
public static let xl = ScarfShadow(color: .black.opacity(0.14), radius: 40, x: 0, y: 16)
}
public extension View {
func scarfShadow(_ s: ScarfShadow) -> some View {
self.shadow(color: s.color, radius: s.radius, x: s.x, y: s.y)
}
}
// MARK: - Motion
public enum ScarfDuration {
public static let fast: Double = 0.12
public static let base: Double = 0.20
public static let slow: Double = 0.30
}
public enum ScarfAnimation {
/// "Smooth" spring matching the cubic-bezier(0.32, 0.72, 0, 1) easing in CSS.
public static let smooth = Animation.spring(response: 0.35, dampingFraction: 0.85)
public static let fast = Animation.easeOut(duration: ScarfDuration.fast)
public static let base = Animation.easeOut(duration: ScarfDuration.base)
}
@@ -0,0 +1,85 @@
//
// ScarfTypography.swift
// Scarf Design System Apple HIG-aligned type scale
//
// Uses SF Pro (system) for UI text and SF Mono for code/transcripts.
// Sizes mirror the CSS tokens (--text-caption ... --text-largeTitle).
//
// Usage:
// Text("Settings").font(ScarfFont.title2)
// Text(message).font(ScarfFont.body)
// Text("v1.2.0").font(ScarfFont.caption).foregroundStyle(ScarfColor.foregroundMuted)
//
import SwiftUI
public enum ScarfFont {
// Display / titles use rounded SF Pro Display (`.default` design + tight tracking).
public static let largeTitle = Font.system(size: 34, weight: .semibold, design: .default)
public static let title1 = Font.system(size: 28, weight: .semibold, design: .default)
public static let title2 = Font.system(size: 22, weight: .semibold, design: .default)
public static let title3 = Font.system(size: 20, weight: .semibold, design: .default)
// Body & labels
public static let headline = Font.system(size: 17, weight: .semibold)
public static let subhead = Font.system(size: 16, weight: .medium)
public static let callout = Font.system(size: 15, weight: .regular)
public static let body = Font.system(size: 14, weight: .regular)
public static let bodyEmph = Font.system(size: 14, weight: .medium)
public static let footnote = Font.system(size: 13, weight: .regular)
public static let caption = Font.system(size: 12, weight: .regular)
public static let captionStrong = Font.system(size: 12, weight: .semibold)
public static let caption2 = Font.system(size: 10, weight: .medium)
// Code / mono for transcripts, command output, file paths.
public static let mono = Font.system(size: 13, weight: .regular, design: .monospaced)
public static let monoSmall = Font.system(size: 12, weight: .regular, design: .monospaced)
}
/// Convenience text styles. Apply with `.scarfStyle(.headline)`.
public enum ScarfTextStyle {
case largeTitle, title1, title2, title3
case headline, subhead, body, bodyEmph, callout, footnote
case caption, captionStrong, captionUppercase
case mono, code
var font: Font {
switch self {
case .largeTitle: return ScarfFont.largeTitle
case .title1: return ScarfFont.title1
case .title2: return ScarfFont.title2
case .title3: return ScarfFont.title3
case .headline: return ScarfFont.headline
case .subhead: return ScarfFont.subhead
case .body: return ScarfFont.body
case .bodyEmph: return ScarfFont.bodyEmph
case .callout: return ScarfFont.callout
case .footnote: return ScarfFont.footnote
case .caption,
.captionUppercase: return ScarfFont.caption
case .captionStrong: return ScarfFont.captionStrong
case .mono, .code: return ScarfFont.mono
}
}
}
public extension View {
/// Apply a Scarf type style. Handles font + tracking + (for `captionUppercase`) text-case.
@ViewBuilder
func scarfStyle(_ style: ScarfTextStyle) -> some View {
switch style {
case .largeTitle:
self.font(style.font).tracking(-0.7)
case .title1:
self.font(style.font).tracking(-0.5)
case .title2, .title3:
self.font(style.font).tracking(-0.3)
case .captionUppercase:
self.font(ScarfFont.captionStrong)
.textCase(.uppercase)
.tracking(0.5)
default:
self.font(style.font)
}
}
}
+19
View File
@@ -10,6 +10,8 @@
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 53SWIFTTERM0001 /* SwiftTerm */; };
53SCARFCORE0010 /* ScarfCore in Frameworks */ = {isa = PBXBuildFile; productRef = 53SCARFCORE0001 /* ScarfCore */; };
53SCARFCORE0011 /* ScarfCore in Frameworks */ = {isa = PBXBuildFile; productRef = 53SCARFCORE0002 /* ScarfCore */; };
53SCARFDESIGN0010 /* ScarfDesign in Frameworks */ = {isa = PBXBuildFile; productRef = 53SCARFDESIGN0001 /* ScarfDesign */; };
53SCARFDESIGN0011 /* ScarfDesign in Frameworks */ = {isa = PBXBuildFile; productRef = 53SCARFDESIGN0002 /* ScarfDesign */; };
53SCARFIOS0010 /* ScarfIOS in Frameworks */ = {isa = PBXBuildFile; productRef = 53SCARFIOS0001 /* ScarfIOS */; };
53SPARKLE00010 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 53SPARKLE00011 /* Sparkle */; };
/* End PBXBuildFile section */
@@ -116,6 +118,7 @@
buildActionMask = 2147483647;
files = (
53SCARFCORE0011 /* ScarfCore in Frameworks */,
53SCARFDESIGN0011 /* ScarfDesign in Frameworks */,
53SCARFIOS0010 /* ScarfIOS in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -140,6 +143,7 @@
files = (
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */,
53SCARFCORE0010 /* ScarfCore in Frameworks */,
53SCARFDESIGN0010 /* ScarfDesign in Frameworks */,
53SPARKLE00010 /* Sparkle in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -208,6 +212,7 @@
name = "scarf mobile";
packageProductDependencies = (
53SCARFCORE0002 /* ScarfCore */,
53SCARFDESIGN0002 /* ScarfDesign */,
53SCARFIOS0001 /* ScarfIOS */,
);
productName = "Scarf iOS";
@@ -279,6 +284,7 @@
packageProductDependencies = (
53SWIFTTERM0001 /* SwiftTerm */,
53SCARFCORE0001 /* ScarfCore */,
53SCARFDESIGN0001 /* ScarfDesign */,
53SPARKLE00011 /* Sparkle */,
);
productName = scarf;
@@ -383,6 +389,7 @@
packageReferences = (
53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
53SCARFCORE0020 /* XCLocalSwiftPackageReference "Packages/ScarfCore" */,
53SCARFDESIGN0020 /* XCLocalSwiftPackageReference "Packages/ScarfDesign" */,
53SCARFIOS0020 /* XCLocalSwiftPackageReference "Packages/ScarfIOS" */,
53SPARKLE00012 /* XCRemoteSwiftPackageReference "Sparkle" */,
);
@@ -1039,6 +1046,10 @@
isa = XCLocalSwiftPackageReference;
relativePath = Packages/ScarfCore;
};
53SCARFDESIGN0020 /* XCLocalSwiftPackageReference "Packages/ScarfDesign" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Packages/ScarfDesign;
};
53SCARFIOS0020 /* XCLocalSwiftPackageReference "Packages/ScarfIOS" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Packages/ScarfIOS;
@@ -1073,6 +1084,14 @@
isa = XCSwiftPackageProductDependency;
productName = ScarfCore;
};
53SCARFDESIGN0001 /* ScarfDesign */ = {
isa = XCSwiftPackageProductDependency;
productName = ScarfDesign;
};
53SCARFDESIGN0002 /* ScarfDesign */ = {
isa = XCSwiftPackageProductDependency;
productName = ScarfDesign;
};
53SCARFIOS0001 /* ScarfIOS */ = {
isa = XCSwiftPackageProductDependency;
productName = ScarfIOS;
@@ -1,11 +1,38 @@
{
"colors" : [
"colors": [
{
"idiom" : "universal"
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.165",
"green": "0.353",
"red": "0.761"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.376",
"green": "0.576",
"red": "0.910"
}
},
"idiom": "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
"info": {
"author": "xcode",
"version": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Some files were not shown because too many files have changed in this diff Show More