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>
@@ -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 }
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 541 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 592 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 274 KiB |
@@ -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); }
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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>
|
||||
@@ -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; }
|
||||
@@ -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 & 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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> {</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> }</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;
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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: []),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.04–0.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 58 KiB |