Compare commits

..

24 Commits

Author SHA1 Message Date
Alan Wizemann ef40def4f3 appcast: add v2.8.0 entry 2026-05-09 21:04:37 +02:00
Alan Wizemann b3980e3088 release: v2.7.5 2026-05-08 12:56:11 +02:00
Alan Wizemann 7c9b9461b9 release: v2.7.1 2026-05-07 12:51:55 +02:00
Alan Wizemann bc853ead04 release: v2.7.0 2026-05-05 20:47:20 +02:00
Alan Wizemann 314eab4355 release: v2.6.5 2026-05-03 22:20:31 +02:00
Alan Wizemann 5da174628c catalog: rebuild at 2026-05-03T18:56:56Z 2026-05-03 20:56:56 +02:00
Alan Wizemann bd8774ddb9 release: v2.6.0 2026-05-01 15:48:16 +02:00
Alan Wizemann d3b8971a39 site: rebuild landing page at 2026-04-30T12:33:24Z 2026-04-30 14:33:24 +02:00
Alan Wizemann abdc031d5b site: rebuild landing page at 2026-04-30T12:26:03Z 2026-04-30 14:26:03 +02:00
Alan Wizemann 59316ee08d release: v2.5.2 2026-04-29 13:47:41 +02:00
Alan Wizemann 01e2befc7e release: v2.5.1 2026-04-27 15:38:43 +02:00
Alan Wizemann 5f6c214772 release: v2.5.0 2026-04-25 17:42:48 +02:00
Alan Wizemann bb92164271 release: v2.3.0 2026-04-24 03:20:53 +02:00
Alan Wizemann 77bf8a6432 release: v2.2.1 2026-04-23 22:10:12 +02:00
Alan Wizemann fa51d9546e catalog: rebuild at 2026-04-23T19:43:53Z 2026-04-23 21:43:53 +02:00
Alan Wizemann bfe5823b82 catalog: rebuild at 2026-04-23T17:41:59Z 2026-04-23 19:42:00 +02:00
Alan Wizemann 215aaa7cbe catalog: rebuild at 2026-04-23T16:37:16Z 2026-04-23 18:37:16 +02:00
Alan Wizemann a14b821a0c release: v2.2.0 2026-04-23 18:31:55 +02:00
Alan Wizemann 65237021e3 release: v2.1.0 2026-04-20 18:50:31 -07:00
Alan Wizemann 9529ad5b1f release: v2.0.2 2026-04-20 15:50:03 -07:00
Alan Wizemann 8f13bef875 release: v2.0.0 2026-04-19 13:16:49 -07:00
Alan Wizemann cf8ce21451 release: v1.6.2 2026-04-17 17:22:29 -07:00
Alan Wizemann a98f79cabe release: v1.6.1 2026-04-16 20:03:56 -07:00
Alan Wizemann 3f4ee9fdca chore: Initial gh-pages with empty appcast 2026-04-16 18:09:32 -07:00
721 changed files with 1098 additions and 145332 deletions
-15
View File
@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: awizemann
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-28
View File
@@ -1,28 +0,0 @@
---
name: Bug Report
about: Report a bug in Scarf
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '...'
3. See error
**Expected behavior**
What you expected to happen.
**Screenshots**
If applicable, add screenshots.
**Environment**
- macOS version:
- Xcode version:
- Hermes version:
- Scarf version/commit:
-19
View File
@@ -1,19 +0,0 @@
---
name: Feature Request
about: Suggest an idea for Scarf
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem?**
A clear description of the problem.
**Describe the solution you'd like**
What you want to happen.
**Describe alternatives you've considered**
Other solutions you've thought about.
**Additional context**
Any other context, screenshots, or mockups.
@@ -1,42 +0,0 @@
<!--
Use this template when submitting a new Scarf project template or updating
an existing one. For regular code/docs PRs, delete this template and write
your own summary.
Switch to this template by adding `?template=template-submission.md` to the
compare URL, or let GitHub pick it up automatically when you touch files
under templates/.
-->
## What's in this PR
- [ ] New template: `templates/<your-handle>/<your-template-name>/`
- [ ] Update to existing template: `templates/<author>/<name>/` (which one and why)
## One-line pitch
_What does this template do for its installers? Two sentences max._
## Checklist
- [ ] I wrote this template, or have the author's explicit permission to submit it.
- [ ] `AGENTS.md` is present and tells any cross-agent what the project does and how to run it.
- [ ] `README.md` includes install, customize, and uninstall instructions.
- [ ] The bundle's `template.json` `contents` claim matches what's actually in the zip.
- [ ] Cron jobs (if any) ship paused and use self-contained prompts.
- [ ] No secrets in any file (API keys, tokens, hostnames, IPs, credentials).
- [ ] No writes to `config.yaml`, `auth.json`, or credential paths — v1 installer will refuse.
- [ ] `python3 tools/build-catalog.py --check` passes locally.
- [ ] I installed + uninstalled this template on my machine and verified the `AGENTS.md` contract works end-to-end.
- [ ] I did **not** edit `templates/catalog.json` — the maintainer regenerates it post-merge.
## Testing notes
_What did you run, what did you see? Paste the log output of the cron job
firing once, or the chat transcript of asking the agent to do the main
thing. Reviewers don't have your machine — show, don't tell._
## Screenshots (optional)
_Drop screenshots of the installed dashboard, or the catalog detail page
rendered locally (`./scripts/catalog.sh preview && open /tmp/scarf-catalog-preview/templates/<slug>/index.html`)._
@@ -1,74 +0,0 @@
# Validates `.scarftemplate` bundles on PRs that touch templates/.
#
# Mirrors the invariants `ProjectTemplateService.verifyClaims` enforces at
# install time. Runs the same Python script the maintainer uses locally
# (tools/build-catalog.py --check) so a bundle can't reach main unless the
# validator is happy.
#
# Also runs tools/test_build_catalog.py so drift between the validator and
# its own test suite is caught on the same PR.
name: Validate template submissions
on:
pull_request:
paths:
- 'templates/**'
- 'tools/build-catalog.py'
- 'tools/test_build_catalog.py'
- '.github/workflows/validate-template-pr.yml'
permissions:
contents: read
pull-requests: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full clone so we can diff against the PR base and scope
# --only to just the changed templates if we want to later.
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
# The validator is stdlib-only and tested against 3.9+ (the
# system Python on current macOS, what most maintainers run
# locally). CI uses 3.11 for faster cold-cache times on
# GitHub Actions runners — same stdlib APIs, same code paths.
python-version: '3.11'
- name: Run validator unit tests
run: python3 tools/test_build_catalog.py -v
- name: Validate every template
id: validate
run: |
set -o pipefail
python3 tools/build-catalog.py --check 2>&1 | tee /tmp/validator.log
- name: Post failure comment
if: failure() && steps.validate.outcome == 'failure'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let body = '## Template validation failed\n\n';
try {
const log = fs.readFileSync('/tmp/validator.log', 'utf8');
body += '```\n' + log.slice(-3000) + '\n```\n';
} catch (e) {
body += 'See the failed job log for details.\n';
}
body += '\nFix the issues above and push again — the check reruns automatically.\n';
body += '\nLocal reproduction: `python3 tools/build-catalog.py --check`\n';
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
-68
View File
@@ -1,68 +0,0 @@
# Xcode
build/
.gh-pages-worktree/
.wiki-worktree/
DerivedData/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
*.moved-aside
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# Swift Package Manager
.build/
# `Packages/` is the historical SwiftPM checkout dir for downloaded deps
# (pre-Xcode-14). We keep it ignored — but NOT our local-package checkout
# at scarf/Packages/, which is part of the source tree (ScarfCore, etc.)
# and must ship in the repo.
Packages/
!scarf/Packages/
Package.pins
Package.resolved
*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/
.swiftpm/
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
# IDE
*.swp
*.swo
*~
.idea/
.vscode/
# Claude Code
.claude/
scarf/standards/backups/
# Scarf project dashboards (user-specific)
.scarf/
# Release artifacts — GitHub Releases hosts the binaries; no need to bloat git
# history. RELEASE_NOTES.md stays tracked (committed with the version bump).
releases/v*/*.zip
releases/v*/appcast-entry.xml
# Wiki helper: personal patterns (hostnames, IPs) blocked from the wiki push.
scripts/wiki-blocklist.txt
# TestFlight feedback / crash JSONs downloaded for triage. PII (emails,
# carriers, locales) and never meant for the public repo — kept local
# while a fix round is in progress, deleted afterward.
crashes/
-24
View File
@@ -1,24 +0,0 @@
# Building Scarf
Scarf is a native macOS app built with Xcode. For contributor builds, use the local script:
```bash
./scripts/local-build.sh
```
Requirements:
- macOS 14.6 (Sonoma) or newer at runtime — that's the app's `MACOSX_DEPLOYMENT_TARGET`. Sonoma support is intentional and load-bearing; do not raise this without an explicit decision to drop Sonoma users
- Xcode 16.0 or newer, selected by `xcode-select` (needed for Swift 6 strict-concurrency features the project uses)
- Metal toolchain installed
- Hermes installed at `~/.hermes/` (see the project README for setup)
If the Metal toolchain is missing, the script will offer to install it in interactive shells. You can also install it manually:
```bash
xcodebuild -downloadComponent MetalToolchain
```
`scripts/local-build.sh` resolves Swift package dependencies, detects `arm64` vs `x86_64`, and builds the Debug app unsigned. Signing is intentionally disabled for local Debug builds so contributors do not need the maintainer's Apple Developer account.
Release signing is separate from contributor builds. Maintainers should continue using the existing release process for signed distributable builds.
-323
View File
@@ -1,323 +0,0 @@
# Scarf — macOS GUI for the Hermes AI Agent
## Project Structure
```
scarf/scarf/ Xcode project root (PBXFileSystemSynchronizedRootGroup — auto-discovers files)
scarf/ Main app target source
Core/Services/ HermesDataService, HermesFileService, HermesLogService, ACPClient, HermesFileWatcher
Core/Models/ Plain structs: HermesSession, HermesMessage, HermesConfig, etc.
Features/ MVVM-F feature modules (Dashboard, Sessions, Activity, Chat, Memory, Skills, Cron, Logs, Settings)
Navigation/ AppCoordinator, SidebarView
docs/ PRD, Architecture, Discovery notes
standards/ Copied development standards (read-only reference)
```
## Architecture Rules
- **MVVM-F**: Features never import sibling features. Cross-feature goes through services.
- **AppCoordinator**: Single `@Observable` coordinator for all navigation state, injected via `.environment()`.
- **No external dependencies**: System SQLite3, Foundation JSON, AttributedString markdown.
- **Read-only DB access**: Never write to `~/.hermes/state.db`. Only write to memory files and cron jobs.
- **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).
### iOS Dynamic Type policy
iOS users can scale text via Settings → Accessibility → Display & Text Size. ScarfFont uses fixed point sizes; adopting it blanket on iOS would regress accessibility on `.accessibility2` / `.xSmall` users. iOS-specific rule:
- **Use `ScarfFont` only for**: status badges, chip labels, intentional-display elements (e.g., onboarding step titles, header chrome that's meant to be a fixed visual size).
- **Keep `.font(.headline)` / `.body` / `.caption` semantic tokens for**: list-row primary + secondary text, body copy, error messages, chat content — anything the user reads.
Decision tree per text element: "is this read for content?" → semantic token. "Is this chrome / a label / a badge?" → ScarfFont.
Mac doesn't have this constraint and adopts ScarfFont everywhere. The iOS app already clamps Dynamic Type at the scene root (`ScarfIOSApp.swift`: `.dynamicTypeSize(.xSmall ... .accessibility2)`) — keep that.
### iOS page chrome
Don't retrofit `ScarfPageHeader` over iOS tab roots. iOS uses `.navigationTitle(...)` + `.navigationBarTitleDisplayMode(.large)` as its native page-header pattern; stacking ScarfPageHeader on top creates double titles. Use ScarfPageHeader only on iOS sub-views without a native large-title bar (rare).
iOS button styling: only swap `.borderedProminent``ScarfPrimaryButton`. **Leave `.bordered` native** — it's the iOS convention and inherits rust through `AccentColor.colorset` automatically. Same for `.plain` (used as compact tap targets in lists).
## Key Paths
- Hermes home: `~/.hermes/`
- SQLite DB: `~/.hermes/state.db` (WAL mode, read-only)
- Config: `~/.hermes/config.yaml`
- Memory: `~/.hermes/memories/MEMORY.md`, `~/.hermes/memories/USER.md`
- Sessions: `~/.hermes/sessions/session_*.json`
- Cron: `~/.hermes/cron/jobs.json`
- Logs: `~/.hermes/logs/errors.log`, `~/.hermes/logs/gateway.log`
- ACP: `hermes acp` subprocess (stdio JSON-RPC)
## Build
```bash
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Debug build
```
## Releases
Shipped via a single local script. **Never run manual `xcodebuild archive` / `notarytool` / `gh release create` steps — use the script so nothing is skipped or misordered.**
```bash
./scripts/release.sh <version> # full release: notarize → appcast → gh-pages → tag
./scripts/release.sh <version> --draft # draft: everything builds + notarizes, but appcast/tag are skipped
```
The script bumps version, archives Universal (arm64 + x86_64) + ARM64-only variants, signs with Developer ID, notarizes via `xcrun notarytool` (keychain profile `scarf-notary`), staples, EdDSA-signs the appcast entry with Sparkle's key, pushes the appcast to `gh-pages`, and creates a GitHub release with both zips attached. Draft mode stops after the release is uploaded so the current version stays "latest" until explicitly promoted.
**Release notes convention:** write them to `releases/v<version>/RELEASE_NOTES.md` BEFORE running the script — it's auto-included in the version-bump commit and used as the GitHub release body. If absent, a placeholder is used.
**Canonical prompts (any of these trigger the flow):**
- "Release v1.6.2" — full release
- "Release v1.6.2 as draft" — draft mode
- "Prepare v1.6.2 release notes from recent commits, then release" — generate notes first, then run
**Prerequisites (one-time, already set up on Alan's machine):** Developer ID Application cert in login Keychain (team `3Q6X2L86C4`), notarytool keychain profile `scarf-notary`, Sparkle EdDSA private key in Keychain item `https://sparkle-project.org`, `gh-pages` branch + GitHub Pages enabled. See the header of [scripts/release.sh](scripts/release.sh) and the Releases section in [README.md](README.md) for details.
## Wiki
Public documentation lives in the GitHub wiki at https://github.com/awizemann/scarf/wiki. The wiki is a separate git repo cloned to `.wiki-worktree/` in the repo root (gitignored, sibling to `.gh-pages-worktree/`). Internal dev notes stay in `scarf/docs/`; the wiki is for public-facing reference.
**Update the wiki when:**
- A new feature module is added under `scarf/scarf/scarf/Features/` → extend the relevant User Guide page.
- A new core service is added under `Core/Services/` → extend `Core-Services.md`.
- Architecture changes (AppCoordinator, transport, MVVM-F rule, sandbox) → `Architecture-Overview.md` + the specific sub-page.
- Hermes version bumps in this file → `Hermes-Version-Compatibility.md`.
- `scripts/release.sh` completes a full (non-draft) release → bump latest-version on `Home.md` + append to `Release-Notes-Index.md`.
- Keyboard shortcut or sidebar section changes → `Keyboard-Shortcuts.md` / `Sidebar-and-Navigation.md`.
**Skip for:** bug fixes with no user-observable change, pure refactors, typos, test-only changes, internal cleanups.
```bash
./scripts/wiki.sh pull # always first
# edit .wiki-worktree/*.md with normal tools
./scripts/wiki.sh commit "docs: describe X" # runs secret-scan
./scripts/wiki.sh push # runs secret-scan again, then push
```
**Never** commit API keys, tokens, `.env` files, private keys, or real hostnames/IPs to the wiki. The script's two-pass secret-scan blocks common token patterns and a user-maintained blocklist at `scripts/wiki-blocklist.txt` (gitignored). Do not bypass without explicit approval. Full workflow on the wiki itself at `.wiki-worktree/Wiki-Maintenance.md`.
## Hermes Version
Targets Hermes v2026.5.7 (v0.13.0). Log lines may carry an optional `[session_id]` tag between the level and logger name — `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse.
**Capability gating.** Scarf detects the target's Hermes version once per server connection via [HermesCapabilities](scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesCapabilities.swift) (`hermes --version` → semver + `YYYY.M.D` parse). The resulting `HermesCapabilitiesStore` is injected on `ContextBoundRoot` (Mac) and `ScarfGoTabRoot` (iOS) via `.environment(_:)` and `.hermesCapabilities(_:)`; UI that depends on a release-gated surface reads it through the typed environment key. Pre-target hosts gracefully hide the new affordances rather than throwing on unknown CLI subcommands. Add a new flag at the top of `HermesCapabilities` whenever Scarf gains a release-gated UI surface — group flags by the Hermes release that introduced them (`MARK: v0.13 (v2026.5.7) flags`, etc.).
**v2026.5.7 (v0.13.0)** added (Scarf-relevant subset; full v2.8.0 implementation lands across WS-2 through WS-9):
- **Persistent Goals** — `/goal <text>` slash command locks the agent onto a target across turns. Checkpoints v2 single-store rewrite + auto-resume after gateway restart. Surfaced in Scarf chat as a non-interruptive command + a "🎯 Goal locked: <text>" pill in the chat header. Gated on `HermesCapabilities.hasGoals`.
- **ACP `/queue` slash command** — queues a prompt to run after the current turn completes. Joins `/steer` in `RichChatViewModel.nonInterruptiveCommands` with a transient "Queued" toast. Gated on `hasACPQueue`. `/steer` now also runs as a regular prompt on idle sessions (`hasACPSteerOnIdle`).
- **Kanban v0.13 reliability + recovery UX** — hallucination gate on worker-created cards, generic diagnostics engine (per-task distress signals), per-task `max_retries` override, multiline title/body create, `auto_blocked_reason` rendered in the inspector banner, darwin zombie detection, unify failure counter across spawn/timeout/crash. New fields decode through tolerant `HermesKanbanRun` / `HermesKanbanTaskDetail` extensions; pre-v0.13 hosts ignore unknown keys. Gated on `hasKanbanDiagnostics`.
- **Curator archive + prune** — `hermes curator archive <skill>` + `prune` + `list-archived` subcommands. The synchronous manual `hermes curator run` blocks until done (pre-v0.13 returned immediately). Surfaced as an "Archived" tab in CuratorView with per-row Restore + Prune actions and a destructive prune-confirm sheet. Gated on `hasCuratorArchive`.
- **Messaging Gateway expansion** — Google Chat (20th platform; `hasGoogleChatPlatform`), cross-platform allowlists (`allowed_channels` / `allowed_chats` / `allowed_rooms` per platform; `hasGatewayAllowlists`), per-platform `gateway_restart_notification` (`hasGatewayRestartNotification`), `busy_ack_enabled` toggle (`hasGatewayBusyAckToggle`), slash-command auto-delete TTL, `[[as_document]]` skill media routing directive, `hermes gateway list` cross-profile status verb (`hasGatewayList`).
- **Provider catalog refresh** — new models on Nous Portal + OpenRouter: `deepseek/deepseek-v4-pro`, `x-ai/grok-4.3`, `openrouter/owl-alpha` (free), `tencent/hy3-preview`, `arcee/trinity-large-thinking` (with temperature + compression overrides). `x-ai/grok-4.20-beta` renamed to `x-ai/grok-4.20` — keep alias map. Vercel AI Gateway demoted to bottom of the picker. `image_gen.model` from `config.yaml` now honored by Hermes (was advertised but ignored pre-v0.13); surfaced in `Settings → Auxiliary` (`hasImageGenModel`). OpenRouter response caching toggle (`hasOpenRouterResponseCache`).
- **MCP SSE transport** — MCP servers can be configured with SSE transport + `sse_read_timeout`. Surfaced in MCPServersView add-server flow alongside stdio/pipe. Gated on `hasMCPSSETransport`.
- **Cron `--no-agent` mode** — script-only watchdog jobs that skip the AI call. Surfaced in CronView edit sheet. Gated on `hasCronNoAgent`.
- **Web Tools per-capability backends** — `web_search` and `web_extract` can use distinct backends; SearXNG joined as a search-only backend. Surfaced in the Web Tools settings tab. Gated on `hasWebToolsBackendSplit`.
- **Profiles `--no-skills`** — `hermes profile create --no-skills` for empty-profile creation. Surfaced as a toggle in the create-profile flow. Gated on `hasProfileNoSkills`.
- **CLI / UX additions** — context compression count in the status feed (rendered next to the token count in chat status bar; `hasContextCompressionCount`), `/new <name>` slash-command argument (`hasNewWithSessionName`), `hermes update --yes` non-interactive (`hasUpdateNonInteractive`), `display.language` static-message translation (zh / ja / de / es / fr / uk / tr; `hasDisplayLanguage`), xAI Custom Voices (voice-cloning badge next to xAI TTS provider; `hasXAIVoiceCloning`).
- **Server-side defaults flipped** — secret redaction defaults back to ON in v0.13 (was off by default in v0.12). The Settings redaction toggle remains for opt-out; the default-state hint reflects the v0.13 semantics when the host advertises v0.13+.
- **`video_analyze` tool** — native video understanding on Gemini-class models. Hermes handles transparently inside the agent loop; Scarf has no UI surface yet but `hasVideoAnalyze` is reserved for future widget gating.
- **`transform_llm_output` plugin hook** — plugin-author concern; surfaced indirectly through PluginsView when a plugin advertises the hook. `hasTransformLLMOutputHook` gates the metadata badge.
- **Schema is unchanged from v0.11/v0.12** — same state.db columns. No migration needed.
**v2026.4.30 (v0.12.0)** added (Scarf-relevant subset):
**v2026.4.30 (v0.12.0)** added (Scarf-relevant subset):
- **Autonomous Curator** — `hermes curator` self-prunes / -consolidates the skill library on a 7-day cycle. Reports land at `~/.hermes/logs/curator/run.json` + `REPORT.md`; paths exposed via `HermesPathSet.curatorLogsDir` (`logs/curator`) + `curatorStateFile` (`skills/.curator_state`), with the per-cycle `run.json` / `REPORT.md` resolved at runtime from the `last_report_path` field on the state file. Surfaced in Scarf as a dedicated "Curator" sidebar item under Interact (between Memory and Skills) on Mac, plus a read-mostly iOS panel with Run Now / Pause / Resume actions and inline pin toggles; both gated on `HermesCapabilities.hasCurator`.
- **5 new inference providers** — GMI Cloud, Azure AI Foundry, LM Studio (upgraded to first-class), MiniMax OAuth, Tencent Tokenhub. Mirrored in `ModelCatalogService.overlayOnlyProviders`; the model picker reaches all of them automatically.
- **`flush_memories` aux task removed (server side)** — `auxiliary.flush_memories` is gone from v0.12 Hermes config but remains alive on pre-v0.12 hosts. Scarf preserves `AuxiliarySettings.flushMemories: AuxiliaryModel`, the YAML reader still emits an `aux("flush_memories")` row, and `AuxiliaryTab` only renders the row when `HermesCapabilities.hasFlushMemoriesAux` is `true` (inverse semantics — pre-v0.12 only). v0.12 users never see the row; v0.11 users keep their edit surface.
- **`auxiliary.curator` aux task added** — Curator's review model is configurable independently of the main model. Surfaced in `Settings → Auxiliary` next to the other aux rows.
- **Multimodal ACP `session/prompt`** — ACP advertises and forwards image content blocks. Scarf chat composers (Mac drag/drop + paste; iOS PhotosPicker) attach images that flow through `ACPClient.sendPrompt(sessionId:text:images:)` as `[{"type":"text","text":...}, {"type":"image","data":"<base64>","mimeType":"image/jpeg"}]` — wire shape matches `acp.schema.ImageContentBlock`. `ImageEncoder` downsamples to 1568px long-edge JPEG q=0.85 detached (never blocks MainActor). Gated on `HermesCapabilities.hasACPImagePrompts`.
- **CLI additions:** `hermes -z <prompt>` (non-interactive one-shot), `hermes update --check` (preflight), `hermes fallback` (manage fallback providers), `hermes curator` (status / run / pause / resume / pin / unpin / restore), `hermes kanban` (full 27-verb task-board CLI). All capability-gated. **v2.7.5 lifts Kanban from a read-only list to a full drag-and-drop board.** See the dedicated [Kanban v3](#kanban-v3-drag-and-drop-board--per-project-tenants-v275) section below for the complete architecture.
- **Skills surface:** `hermes skills install <https-url>` direct-URL install (SkillsView "Install from URL…" toolbar button), reload via `hermes skills audit` (Skills "Reload" button — equivalent to the `/reload-skills` slash command for non-ACP contexts), enabled/disabled state read from `skills.disabled` in config.yaml (rendered as strikethrough + "OFF" pill), Curator pin badge from `~/.hermes/skills/.curator_state` (rendered as a pin glyph). The disable-toggle write path is deferred to v2.7 — Hermes only exposes `hermes skills config` as an interactive verb, and Scarf prefers reading accurately to risking a clobbered list.
- **Two new gateway platforms:** Microsoft Teams (19th, plugin-shipped) + Tencent 元宝 / Yuanbao (18th, native). Surfaced in the Mac Platforms tab.
- **Cron upgrades:** per-job `--workdir <abs-path>` (project-aware cwd that pulls AGENTS.md / CLAUDE.md / .cursorrules) is exposed in the editor sheet, gated on `HermesCapabilities.hasCronWorkdir` so pre-v0.12 hosts don't see the field (and a defensive override in `CronView` strips the value before calling `createJob`/`updateJob` even if it was hydrated from a pre-existing job). Pass an empty string on edit to clear an existing workdir, mirroring the `--script` shape. Hermes also added a `context_from` field for chaining cron outputs but only via YAML so far — Scarf reads it (HermesCronJob.contextFrom) but doesn't write it.
- **Settings deltas:** `prompt_caching.cache_ttl` (5m/1h picker), `redaction.enabled` toggle (off-by-default in v0.12 — toggle restores it), `agent.runtime_metadata_footer` toggle, Piper added to TTS provider list, `vercel` added to terminal backend list.
- **Bundled plugins:** Spotify, Google Meet, Langfuse observability, hermes-achievements (visible in Plugins tab).
- **iOS catch-up (Phase H):** read-only Webhooks / Plugins / Profiles tabs (`Scarf iOS/Webhooks/WebhooksView.swift`, `Plugins/PluginsView.swift`, `Profiles/ProfilesView.swift`) parity-match the Mac surfaces but skip mutating CLI verbs. `Scarf iOS/Components/HermesVersionBanner.swift` nudges pre-v0.12 hosts to upgrade (renders only when the connected target is below v0.12).
- **`hermes memory` providers:** honcho, openviking, mem0, hindsight, holographic, retaindb, byterover. `Settings → Memory` lists all providers in the picker; the existing "Run `hermes memory setup` in Terminal" hint stays — `hermes memory setup` is interactive (asks for tokens) so an in-app shellout would surface a frozen UI.
- **Schema is unchanged from v0.11** — same state.db columns (`messages.reasoning_content`, `sessions.api_call_count` introduced in v0.11 remain). No migration needed.
**v2026.4.23 (v0.11.0)** added (historical context, still consumed by Scarf when running against a pre-v0.12 host):
- `/steer <prompt>` — non-interruptive mid-run guidance slash command. Surfaced in Scarf chat menus via `RichChatViewModel.nonInterruptiveCommands`; `ChatViewModel.sendViaACP` (Mac) and `ChatController.send` (iOS) skip the "Agent working…" status flip and show a transient toast instead.
- New CLI subcommands: `hermes plugins` / `profile` / `webhook` / `insights` / `logs` / `memory reset` / `completion` / `dashboard`. Scarf v2.5 adopts **`hermes memory reset`** (toolbar button on MemoryView with destructive confirmation). The other CLIs are documented here for v2.6 — Scarf still reads `~/.hermes/plugins/`, `~/.hermes/profiles/` etc directly today; switching those paths to the canonical CLI is a forward-compatible change to make when bandwidth permits.
- New state.db columns: `messages.reasoning_content` + `sessions.api_call_count`. `HermesDataService.detectSchema` flips `hasV011Schema` only when both are present (partial migrations stay on v0.7 path). Surfaced as the "API" chip on session rows + a network-icon counter in DashboardView. `HermesMessage.preferredReasoning` picks the newer column when both reasoning channels are populated.
- New skills: `design-md` (Google's DESIGN.md authoring; needs `npx`/Node 18+ on host — checked via `SkillPrereqService` and surfaced as a yellow banner) and `spotify` (OAuth via `hermes auth spotify` — driven by `SpotifyAuthFlow` + `SpotifySignInSheet`, mirroring v2.3 Nous Portal pattern).
- Updated skills: `research-paper-writing` 1.1.0 (+SciencePlots dep), `segment-anything-model` (expanded docs), `google-workspace` (gws CLI prefer + granular OAuth scopes), `hermes-agent` (in-tree).
- SKILL.md frontmatter gains `allowed_tools` / `related_skills` / `dependencies` lists. `HermesSkill` carries them as optional fields; `SkillsView` (Mac) + `SkillDetailView` (iOS) render them as chip rows when populated.
v0.10.0 introduced the **Tool Gateway** — paid Nous Portal subscribers route web search, image generation, TTS, and browser automation through their subscription without separate API keys. In Scarf:
- **Provider picker** ([ModelCatalogService.swift](scarf/scarf/Core/Services/ModelCatalogService.swift)) merges Hermes's `HERMES_OVERLAYS` so Nous Portal and other overlay-only providers (OpenAI Codex, Qwen OAuth, Google Gemini CLI, GitHub Copilot ACP, Arcee) appear alongside the models.dev catalog. Subscription-gated providers sort first and render a "Subscription" pill.
- **Subscription detection** ([NousSubscriptionService.swift](scarf/scarf/Core/Services/NousSubscriptionService.swift)) reads `~/.hermes/auth.json``providers.nous`. Read-only; Hermes owns the write path.
- **Per-task routing** (Auxiliary tab) toggles `auxiliary.<task>.provider` between `nous` and `auto`. Hermes derives gateway routing from provider selection — there is no separate `use_gateway` key.
- **Health surface** ([HealthViewModel.swift](scarf/scarf/Features/Health/ViewModels/HealthViewModel.swift)) adds a synthetic "Tool Gateway" section showing subscription state + `platform_toolsets` mappings + which aux tasks are routed through Nous.
- **Scarf's existing `Gateway` feature is renamed to "Messaging Gateway"** everywhere user-facing to disambiguate from the new Tool Gateway. The `SidebarSection.gateway` enum case and `gateway_state.json` / `gateway.log` paths are unchanged (not user-facing strings).
**Keep `ModelCatalogService.overlayOnlyProviders` in sync** with `HERMES_OVERLAYS` in `~/.hermes/hermes-agent/hermes_cli/providers.py`. When Hermes adds a new overlay-only provider, mirror the entry (display name, base URL, auth type, subscription-gated flag, doc URL) or the picker won't reach it.
## Kanban v3: drag-and-drop board + per-project tenants (v2.7.5)
Scarf v2.7.5 promotes Kanban from a read-only list to a full board with drag-and-drop, every Hermes write verb wired up, and per-project boards bound to a Scarf-minted tenant slug. The list view is preserved as a `Board | List` toggle for accessibility / narrow-window fallback.
**Sidebar move.** `.kanban` moved from *Manage**Monitor* in `SidebarView` (between `.activity` and the remaining Monitor entries). Kanban is runtime work-in-progress, not configuration. Position kept inside the same enum case — only the section bucket changed.
**Hermes constraints that drive design.**
1. **No `update` verb.** `priority`, `title`, `body`, `tenant` are write-once at `kanban create`. Mutations after create are state transitions (`assign` / `claim` / `complete` / `block` / `unblock` / `archive`) or new comments. Inline-edit on a card title is impossible at the wire level.
2. **No `project_id` column.** Hermes Kanban is one global SQLite DB at `~/.hermes/kanban.db`. Closest namespace is the optional `tenant TEXT` column. Scarf hijacks it: each project gets a `scarf:<slug>` tenant minted on first kanban interaction.
3. **No within-column position field.** Drag-to-reorder inside a column has no Hermes persistence path and is **disabled** in v2.7.5. Sort key is `priority DESC, created_at DESC` — matches dispatcher's actual run order. Cross-column drag is the only persisted gesture.
4. **No file-watch / webhooks.** Polling at 5s while foregrounded; live `watch` streaming deferred to a later release (a `hasKanbanWatch` flag will gate it).
5. **Status enum has 7 values, board collapses to 5 columns:** Triage / **Up Next** (`todo` + `ready`) / Running / Blocked / Done. Triage hides when empty; Archived hides behind a toolbar toggle.
**Service layer.** [KanbanService](scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift) is a Sendable `actor` in ScarfCore — pure I/O, no UI state. Wraps every v0.12 verb (`list / show / runs / stats / assignees / create / assign / claim / comment / complete / block / unblock / archive / dispatch / link / unlink`). Every method dispatches its CLI invocation through `Task.detached(priority: .utility)`, matching the existing `KanbanViewModel.load` pattern (re: Swift 6 rules in `~/.claude/CLAUDE.md`). Errors land in [KanbanError](scarf/Packages/ScarfCore/Sources/ScarfCore/Models/KanbanError.swift) and surface as inline banners (not modal alerts) since the board is high-frequency. The "no matching tasks" stdout sentinel is normalized to `[]`.
**Drag-drop transition planner.** `KanbanService.plan(for: KanbanTransition)` is a pure function that maps `(from, to)` columns to the right verb sequence — `(.upNext, .running) → [.claim]`, `(.blocked, .running) → [.unblock, .claim]`, etc. Disallowed transitions throw `KanbanError.forbiddenTransition` with a user-facing reason: drop on Done from anywhere triggers "Done is terminal — create a follow-up task to continue work."; drop on Triage from outside triggers "Triage tasks are promoted by a specifier agent." The view's drop handler short-circuits forbidden transitions with red-stroke target feedback.
**Per-project tenant.** [KanbanTenantResolver](scarf/scarf/Core/Services/KanbanTenantResolver.swift) (Mac) mints `scarf:<slug>` on first kanban interaction inside a project, persisting to `<project>/.scarf/manifest.json`'s new optional `kanbanTenant: String?` field. Tenants are **immutable across rename** (existing tasks already carry the old slug). Bare projects (no manifest) get a sentinel manifest written with `id: scarf/<project-id>` + `version: 0.0.0` + just the `kanbanTenant` set; `ProjectAgentContextService` recognizes the sentinel and refuses to surface it as a "Template" line. The cross-platform read-only counterpart is [KanbanTenantReader](scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanTenantReader.swift) in ScarfCore — iOS uses it to filter the per-project board without linking the full manifest model.
**Agent-side tenant injection.** `ProjectAgentContextService.renderBlock` adds a "Kanban tenant" line to the AGENTS.md scarf-managed block whenever a tenant exists. Since `ChatViewModel.startACPSession` calls `refresh(for:)` before opening every project chat, the agent sees the tenant on every session start and is told to pass `--tenant scarf:<slug>` on `hermes kanban create`. Agents are imperfect at flag discipline; misuse just sends the task to the global "Untagged" group on the global board, which is acceptable v2.7.5 behavior. A dedicated retag UX is a follow-up.
**View model.** [KanbanBoardViewModel](scarf/scarf/Features/Kanban/ViewModels/KanbanBoardViewModel.swift) is `@MainActor + @Observable`, holds the column-grouped task array, and applies optimistic-merge logic around drag-drops: an in-flight move records `optimisticOverrides[taskId] = newStatus`, mutates the local array immediately, and clears the override only when the polled response confirms the new status. Without this, a stale poll response can clobber a card the user just dragged. On CLI failure the override is removed and an error message lands in the inline banner.
**Mac surface.** [KanbanBoardView](scarf/scarf/Features/Kanban/Views/KanbanBoardView.swift) is the orchestrator (header + columns + side-pane inspector + create/block/complete sheets). [KanbanColumnView](scarf/scarf/Features/Kanban/Views/KanbanColumnView.swift) owns its `dropDestination(for: KanbanTaskRef.self)`. [KanbanCardView](scarf/scarf/Features/Kanban/Views/KanbanCardView.swift) handles the `.draggable` source, status-specific chrome (running edge accent + shimmer; blocked warning glyph; done dim 0.7/0.55), and a custom drag preview. [KanbanInspectorPane](scarf/scarf/Features/Kanban/Views/KanbanInspectorPane.swift) is a 420pt side-pane (not modal) so the user can keep dragging cards after inspecting one. [KanbanCreateSheet](scarf/scarf/Features/Kanban/Views/KanbanCreateSheet.swift) maps form state to a `KanbanCreateRequest`; the Workspace picker locks to "Project Dir" on per-project boards. [KanbanBlockReasonSheet](scarf/scarf/Features/Kanban/Views/KanbanBlockReasonSheet.swift) and [KanbanCompleteResultSheet](scarf/scarf/Features/Kanban/Views/KanbanCompleteResultSheet.swift) prompt for optional `--reason` / `--result` text on those transitions.
**Per-project surface.** New `DashboardTab.kanban` case in `ProjectsView.swift`, dispatched to [ProjectKanbanTab](scarf/scarf/Features/Projects/Views/ProjectKanbanTab.swift) which mints the tenant on appearance and wraps `KanbanBoardView` with `tenantFilter` + `projectPath` pre-applied. Capability-gated on `HermesCapabilities.hasKanban` so pre-v0.12 hosts don't see a broken destination. Plus a new `kanban_summary` widget — top 3 tasks by priority across `running` + `blocked` + `todo` for the project's tenant, with stats glance footer. Mirror in `tools/widget-schema.json`, `tools/build-catalog.py`, and `site/widgets.js`. Templates can reference it as `{ kind: kanban_summary, max_rows: 3 }` in dashboard.json.
**iOS surface.** Read-only board on the project Kanban tab ([ScarfGoKanbanView](Scarf%20iOS/Kanban/ScarfGoKanbanView.swift) + [ScarfGoKanbanDetailSheet](Scarf%20iOS/Kanban/ScarfGoKanbanDetailSheet.swift)). Renders the 5 columns as a horizontally-paged `Picker` of single-column lists — HIG-friendly on iPhone. No mutations, no drag-drop in v2.7.5 (deferred to a later release). Card titles use semantic `.headline` (not `ScarfFont`) so Dynamic Type works; chrome (badges) keeps `ScarfBadge` for fixed visual weight. Gated on `HermesCapabilities.hasKanban`; pre-v0.12 hosts don't see the segment.
**Capability gating.** Kept the single `HermesCapabilities.hasKanban` flag (`>= 0.12.0`). All 27 verbs shipped together; finer-grained gating is YAGNI. A `hasKanbanWatch` flag will land in a later release if `watch` semantics drift between point releases.
**Don't:** introduce within-column reorder via a client-side ordering sidecar — sort order would diverge from dispatcher's actual run order, which is worse than no manual order. Use `priority` on `kanban create` to set initial order; revisit when Hermes ships an `update --priority` verb. Don't try to mutate `priority` / `title` / `body` post-create — there's no verb. Don't drop cards from `done` into anything — Done is terminal. Don't call `transport.runProcess` directly from view bodies; route through `KanbanService` (the actor) so polling and writes share the same concurrency model.
## Project Templates
Scarf ships a `.scarftemplate` format (v1 as of 2.2.0) for sharing pre-packaged projects across users and machines. A bundle is a zip containing:
- `template.json` — manifest (id, name, version, `contents` claim)
- `README.md` — shown in the install preview sheet
- `AGENTS.md` — required; the [Linux Foundation cross-agent instructions standard](https://agents.md/) — every template is agent-portable out of the box
- `dashboard.json` — copied to `<project>/.scarf/dashboard.json`
- `instructions/…` — optional per-agent shims (`CLAUDE.md`, `GEMINI.md`, `.cursorrules`, `.github/copilot-instructions.md`)
- `skills/<name>/…` — optional; installed to `~/.hermes/skills/templates/<slug>/` (namespaced so uninstall is `rm -rf` on one folder)
- `cron/jobs.json` — optional; registered via `hermes cron create` with a `[tmpl:<id>] …` name prefix and immediately paused
- `memory/append.md` — optional; appended to `~/.hermes/memories/MEMORY.md` between `<!-- scarf-template:<id>:begin/end -->` markers
Key services: [ProjectTemplateService.swift](scarf/scarf/Core/Services/ProjectTemplateService.swift) (inspect + validate + plan), [ProjectTemplateInstaller.swift](scarf/scarf/Core/Services/ProjectTemplateInstaller.swift) (execute a plan), [ProjectTemplateExporter.swift](scarf/scarf/Core/Services/ProjectTemplateExporter.swift) (build a bundle from a project), [ProjectTemplateUninstaller.swift](scarf/scarf/Core/Services/ProjectTemplateUninstaller.swift) (reverse an install using the lock file). UI in [Features/Templates/](scarf/scarf/Features/Templates/). The `scarf://install?url=<https URL>` deep link + `file://` URLs for `.scarftemplate` files are handled by [TemplateURLRouter.swift](scarf/scarf/Core/Services/TemplateURLRouter.swift) and `onOpenURL` in `scarfApp.swift`. A `<project>/.scarf/template.lock.json` uninstall manifest is written after every install and drives the uninstall flow.
**Uninstall semantics:** driven by the lock file. Only files listed in `lock.projectFiles` are removed from the project dir; user-added files (e.g. a `sites.txt` created on first run) are preserved. If every file in the dir was installed by the template, the dir is removed too; otherwise the dir stays with just the user's files. Skills namespace is always removed wholesale (it's isolated). Cron jobs are removed via `hermes cron remove <id>` after resolving each lock-recorded name. Memory block is stripped between the `begin`/`end` markers, leaving the rest of MEMORY.md intact. No "undo" — uninstall is destructive; to re-install, run the install flow again. Uninstall UI lives on the project-list context menu and the dashboard header (only shown when the selected project has a lock file).
**Never** let a template write to `config.yaml`, `auth.json`, sessions, or any credential path — the v1 installer refuses. If you extend the format, treat the preview sheet as load-bearing: the user's only trust boundary is that the sheet is honest about everything that's about to be written.
### Template configuration (v2.3, schemaVersion 2)
Templates can declare a typed configuration schema in `template.json`'s new `config` block. The installer renders a **Configure** step between the parent-directory pick and the preview sheet; values land at `<project>/.scarf/config.json` (non-secret) and in the login Keychain (secret). A post-install **Configuration** button on the dashboard header (shown when `<project>/.scarf/manifest.json` exists) opens the same form pre-filled for editing.
Manifest shape:
```json
{
"schemaVersion": 2,
"contents": { "dashboard": true, "agentsMd": true, "config": 2 },
"config": {
"schema": [
{"key": "site_url", "type": "string", "label": "Site URL", "required": true},
{"key": "api_token", "type": "secret", "label": "API Token", "required": true}
],
"modelRecommendation": {
"preferred": "claude-sonnet-4.5",
"rationale": "Tool-heavy workload — reasoning helps."
}
}
}
```
Supported field types: `string`, `text`, `number`, `bool`, `enum` (with `options: [{value, label}]`), `list` (itemType `"string"` only in v1), `secret`. Type-specific constraints (`pattern`, `min`/`max`, `minLength`/`maxLength`, `minItems`/`maxItems`) are optional. `secret` fields **must not** declare a `default` — the validator refuses.
Key services: [TemplateConfig.swift](scarf/scarf/Core/Models/TemplateConfig.swift) (schema + value models + Keychain ref helpers), [ProjectConfigKeychain.swift](scarf/scarf/Core/Services/ProjectConfigKeychain.swift) (thin `SecItemAdd`/`Copy`/`Delete` wrapper; the only Keychain user in Scarf today), [ProjectConfigService.swift](scarf/scarf/Core/Services/ProjectConfigService.swift) (load/save config.json, resolve secrets, cache manifest, validate schema + values). UI in [Features/Templates/ViewModels/TemplateConfigViewModel.swift](scarf/scarf/Features/Templates/ViewModels/TemplateConfigViewModel.swift) + [Features/Templates/Views/TemplateConfigSheet.swift](scarf/scarf/Features/Templates/Views/TemplateConfigSheet.swift).
**Secret storage.** Keychain service name is `com.scarf.template.<slug>`, account is `<fieldKey>:<project-path-hash-short>`. The path-hash suffix means two installs of the same template in different dirs don't collide on Keychain entries. Values in `config.json` are `"keychain://service/account"` URIs — never plaintext. The bytes hit the Keychain only on form commit, so cancelling never leaves orphan entries.
**Uninstall.** `TemplateLock` v2 gains `config_keychain_items` and `config_fields` arrays. The uninstaller iterates each URI through `SecItemDelete` before removing the lock file. Absent items (user hand-cleaned) are no-ops.
**Exporter.** Carries the *schema* from `<project>/.scarf/manifest.json` through into exported bundles, never values. Exporting never leaks anyone's secrets. `schemaVersion` bumps to 2 only when a schema is forwarded; schema-less exports stay at 1.
**Catalog site.** [tools/build-catalog.py](tools/build-catalog.py) mirrors the Swift schema validator. Each v2 template's `template.json` is copied into `.gh-pages-worktree/templates/<slug>/manifest.json` and the site's `widgets.js` calls `ScarfWidgets.renderConfigSchema` to display the schema on the detail page (display-only — the form lives in-app).
**Schema is Swift-primary.** If `TemplateConfigField.FieldType` gains a new case, update in order: `TemplateConfig.swift` (model + validation), `tools/build-catalog.py` (`SUPPORTED_CONFIG_FIELD_TYPES` + type-specific rules), `widgets.js` (`summariseConstraint`), `TemplateConfigSheet.swift` (new control subview), tests on both sides. Schema drift between validator + installer is the kind of bug users only notice after shipping.
### Project-scoped chat + Scarf-managed AGENTS.md context (v2.3)
v2.3 adds a per-project Sessions tab and a "New Chat" button that spawns `hermes acp` with `cwd = project.path`. Session-to-project attribution is persisted in a Scarf-owned sidecar at `~/.hermes/scarf/session_project_map.json` — the ACP wire protocol has no project-metadata hook (extra params are silently dropped), and `state.db` has no cwd column, so the sidecar is Scarf's source of truth for "which project does this session belong to?" Managed by [SessionAttributionService.swift](scarf/scarf/Core/Services/SessionAttributionService.swift); read by the per-project [ProjectSessionsView.swift](scarf/scarf/Features/Projects/Views/ProjectSessionsView.swift).
**Giving the agent project awareness.** Hermes auto-reads a context file from the session's cwd at startup — priority order `.hermes.md``HERMES.md``AGENTS.md``CLAUDE.md``.cursorrules`, first match wins, 20KB cap. We lean on that by writing a Scarf-managed block into `<project>/AGENTS.md` before opening the session. Service: [ProjectAgentContextService.swift](scarf/scarf/Core/Services/ProjectAgentContextService.swift). Block shape:
```
<!-- scarf-project:begin -->
## Scarf project context
_Auto-generated by Scarf — do not edit between the begin/end markers._
You are operating inside a Scarf project named **"<Project Name>"**. …
- **Project directory:** `<absolute path>`
- **Dashboard:** `<path>/.scarf/dashboard.json`
- **Template:** `<author/id>` v<version> <!-- template-installed only -->
- **Configuration fields:** `field_a`, `field_b (secret — name only, value stored in Keychain)`
- **Registered cron jobs:** `[tmpl:<id>] <name>` — schedule …, currently paused|enabled
- **Uninstall manifest:** `<path>/.scarf/template.lock.json` <!-- when present -->
Any content below this block is template- or user-authored; preserve and defer to it.
<!-- scarf-project:end -->
```
**Invariants.**
- **Secret-safe.** Block surfaces field NAMES, never VALUES. A project with a Keychain-stored secret shows `api_token (secret — name only, …)`; the Keychain ref URI and any plaintext value never appear. Auditable by `refreshListsFieldNamesNotValues` in `ProjectAgentContextServiceTests`.
- **Idempotent.** Two refreshes with unchanged state produce byte-identical output. The write is skipped entirely when no delta, avoiding file-watcher churn.
- **Bounded.** Everything outside the markers is preserved on every refresh. Template-author AGENTS.md content lives safely below the block.
- **Non-fatal.** `ChatViewModel.startACPSession` calls refresh with `try?` + log — a failed write doesn't block the chat from starting; worst case is the session loses project awareness.
- **Refresh timing.** Called BEFORE `client.start()` so the block lands before Hermes's session-boot context scan. Skipping this ordering = the agent sees stale context from the previous refresh (or nothing, on fresh projects).
**Template-author contract.** A template shipped via the catalog should include an `AGENTS.md` with the template's operational instructions. Authors leave the `<!-- scarf-project -->` region alone — Scarf populates it at chat-start time. Everything below is template-owned and preserved.
**Known caveat.** If any parent directory of the project contains `.hermes.md` or `HERMES.md`, those shadow the project's `AGENTS.md` (higher in Hermes's priority order). No fix in v2.3 — deferred to v2.4 pending user input on how to handle authored `.hermes.md` files.
## Template Catalog
Shipped community templates live at `templates/<author>/<name>/` (one level down — `templates/CONTRIBUTING.md` explains the submission flow for authors). The catalog site is generated from this directory and served at `awizemann.github.io/scarf/templates/` alongside the Sparkle appcast — the two coexist on the `gh-pages` branch but touch completely disjoint paths.
Pipeline:
- **Validator + regenerator:** [tools/build-catalog.py](tools/build-catalog.py) is stdlib-only Python (3.9+). It walks `templates/*/*/`, validates every `.scarftemplate` against its manifest claim (mirrors the Swift `ProjectTemplateService.verifyClaims` invariants), enforces a 5 MB bundle-size cap, scans for high-confidence secret patterns, checks `staging/` matches the built bundle byte-for-byte, and emits `templates/catalog.json`. Tested by [tools/test_build_catalog.py](tools/test_build_catalog.py) — 16 tests covering every validation path.
- **Wrapper:** [scripts/catalog.sh](scripts/catalog.sh) mirrors the `scripts/wiki.sh` shape with `check / build / preview / serve / publish` subcommands. `publish` runs a second-pass secret-scan against the rendered site before committing + pushing `gh-pages`.
- **Site source:** `site/index.html.tmpl` + `site/template.html.tmpl` are `{{TOKEN}}`-substitution templates. `site/widgets.js` (~300 lines of vanilla JS) is the dogfood — renders a `ProjectDashboard` JSON into HTML using the same widget vocabulary the Swift app uses, so each template's detail page shows a live preview of its post-install dashboard.
- **Install-URL hosting:** raw-served from `main` at `https://raw.githubusercontent.com/awizemann/scarf/main/templates/<author>/<name>/<name>.scarftemplate`. No per-template Releases ceremony.
- **CI gate:** [.github/workflows/validate-template-pr.yml](.github/workflows/validate-template-pr.yml) runs the Python validator + its own test suite on every PR that touches `templates/`, the validator, or its tests. Failures post a comment on the PR with the last 3 KB of the validator log.
Maintainer workflow on merge to main:
```bash
./scripts/catalog.sh build # regenerate templates/catalog.json + .gh-pages-worktree/templates/
./scripts/catalog.sh publish # secret-scan rendered output + commit + push gh-pages
```
Same cadence as `scripts/release.sh` (manual, auditable, no auto-deploy). Runs stay isolated: release.sh only touches `appcast.xml` on gh-pages; catalog.sh only touches `templates/` on gh-pages. Never push catalog output on a release cadence or vice versa.
**Schema is Swift-primary.** When `ProjectDashboardWidget.type` gains a new case or `ProjectTemplateManifest` adds a field, update Swift first, then mirror into `tools/build-catalog.py` (`SUPPORTED_WIDGET_TYPES`, `_validate_manifest`, `_validate_contents_claim`) so the web validator stays honest. The Python test suite's real-bundle test catches drift on the example template but not on the full widget vocabulary — add a synthetic fixture to `test_build_catalog.py` for any new widget type.
-72
View File
@@ -1,72 +0,0 @@
# Contributing to Scarf
Thanks for your interest in contributing to Scarf.
## Getting Started
1. Fork and clone the repo
2. Open `scarf/scarf.xcodeproj` in Xcode 16.0+
3. Build and run (Scarf runs on macOS 14.6 Sonoma or newer; Hermes must be installed at `~/.hermes/`)
For an unsigned command-line Debug build without an Apple Developer account, run [`./scripts/local-build.sh`](scripts/local-build.sh). See [BUILDING.md](BUILDING.md) for prerequisites.
## Architecture
Scarf uses the MVVM-Feature pattern. Each feature is a self-contained module under `Features/`:
```
Features/FeatureName/
Views/ SwiftUI views
ViewModels/ @Observable view models
```
Rules:
- Features never import sibling features directly
- Cross-feature navigation goes through `AppCoordinator`
- Services in `Core/Services/` are shared across features
- Models in `Core/Models/` are plain structs
## Guidelines
- Keep it simple. Minimal dependencies, no over-engineering.
- No commented-out code, TODOs, or deferred functionality in PRs.
- All code must build with zero warnings.
- Follow existing patterns — look at how similar features are built before adding new ones.
- The app only reads from `~/.hermes/state.db` (never writes). Memory files are the exception.
- Swift 6 strict concurrency: `@MainActor` default isolation, `nonisolated` for service methods.
## Documentation
Public docs live in the [GitHub wiki](https://github.com/awizemann/scarf/wiki). Small fixes (typos, clarifications) can be made via the "Edit" button on any wiki page — you need push access to the main repo. For larger changes, clone the wiki locally (`git clone git@github.com:awizemann/scarf.wiki.git`) or open an issue describing the proposed change.
## Adding a Language
Scarf ships with English + Simplified Chinese, German, French, Spanish, Japanese, and Brazilian Portuguese. To add another locale (or improve an existing one):
1. **Fork** the repo and create a branch.
2. **Add the locale to `knownRegions`** in `scarf/scarf.xcodeproj/project.pbxproj` — follow the existing list (e.g. add `it` after `"pt-BR"`).
3. **Drop a new JSON file at `tools/translations/<locale>.json`** — copy an existing one (say `tools/translations/es.json`) as a starting point. Each entry maps the English source string to your translation. Keys you omit fall back to English at runtime — do that for proper nouns (Scarf, Hermes, Anthropic, OAuth, SSH, …) and for anything technical that shouldn't translate.
4. **Preserve format specifiers exactly**: `%@`, `%lld`, `%d`, positional `%1$@` / `%2$lld`, etc. If word order needs to change in your language, use positional forms (`%1$@ … %2$@`).
5. **Add your locale to `tools/merge-translations.py`'s `LOCALES` list** and run `python3 tools/merge-translations.py` — this writes your translations into `scarf/scarf/Localizable.xcstrings`.
6. **Translate `scarf/scarf/InfoPlist.xcstrings`** (the macOS microphone-permission prompt) for your locale. Add a new `stringUnit` under `localizations`.
7. **Build** (`xcodebuild -project scarf/scarf.xcodeproj -scheme scarf build`) and **sanity-check in Xcode**: Scheme → Run → App Language → your locale. Walk the main views (Dashboard, Chat, Settings) and look for clipping or obvious leaks.
8. **Open a PR** including the new JSON file, the updated catalog, and the pbxproj / script changes. Mention which routes you spot-checked.
AI translation is fine for the first pass — it's how the initial six locales landed. Native-speaker review improves quality and is always welcome, either as a follow-up PR or as review comments on the initial one.
See [scarf/docs/I18N.md](scarf/docs/I18N.md) for deeper context on the String Catalog setup and which strings are intentionally kept verbatim.
## Reporting Issues
Open an issue with:
- What you expected to happen
- What actually happened
- macOS version and Hermes version
- Steps to reproduce
## Pull Requests
- Open an issue first to discuss the change
- One feature or fix per PR
- Include a clear description of what changed and why
- Ensure the project builds with `xcodebuild -project scarf/scarf.xcodeproj -scheme scarf build`
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Alan Wizemann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-495
View File
@@ -1,495 +0,0 @@
<p align="center">
<img src="icon-v2.5.png" width="128" height="128" alt="Scarf app icon">
</p>
<h1 align="center">Scarf</h1>
<p align="center">
A native macOS companion app for the <a href="https://github.com/hermes-ai/hermes-agent">Hermes AI agent</a>.<br>
Full visibility into what Hermes is doing, when, and what it creates.
</p>
<p align="center">
<img src="https://img.shields.io/badge/macOS-14.6+%20Sonoma-blue" alt="macOS">
<img src="https://img.shields.io/badge/Swift-6-orange" alt="Swift">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
<br>
<em>Available in English, 简体中文, Deutsch, Français, Español, 日本語, and Português (Brasil).</em>
<br><br>
<a href="https://www.buymeacoffee.com/awizemann"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" height="28"></a>
</p>
## What's New in 2.7
The biggest release since 2.6 — six weeks of work focused on **remote-context performance**, a **new project authoring flow**, **dashboard widgets**, **OAuth resilience**, and a top-to-bottom **performance instrumentation harness** that drove the bulk of the rest. 36 commits, no schema bump, no Hermes capability bump.
### Remote chats and Activity in seconds, not 30s timeouts
Resuming a chat or opening Activity on a slow remote (a 420ms-RTT droplet, an underprovisioned VPS, a tunnel through 4G) used to fetch the full message column set in one shot, which routinely tripped the 30s SSH timeout on chats with multi-page tool result blobs. v2.7 introduces a **skeleton-then-hydrate pattern** that bounds the wire payload by what the user actually needs to see RIGHT NOW, then fills in the heavy stuff in the background.
- **Chat skeleton** — user + assistant rows only (skips `role='tool'`), `tool_calls` / `reasoning` hard-NULLed at SQL level. Wire payload bounded by conversational text. The chat appears in seconds. Background hydration pages tool calls in 5-id batches; tool-result CONTENT is opt-in (Settings → Display → "Load tool results in past chats", default off) with per-card lazy-fetch in the inspector pane.
- **Activity skeleton** — metadata-only fetch (~3 KB for 50 rows). Placeholder rows render immediately; real per-call entries swap in as paged hydration completes.
- **Single-id whale recovery** — when a 5-id batch trips the 30s timeout (one row carries an oversized `tool_calls` blob), an L1 single-id retry isolates the offender so the rest of the batch still hydrates.
### SSH cancellation that actually cancels
`Task.detached` doesn't inherit cancellation from the awaiting parent. Pre-fix, navigating away from a chat left the underlying ssh subprocess running for the full 30s, pinning a remote sqlite query and a ControlMaster session — the "third chat hangs" / "dashboard spins after rapid switching" symptom. v2.7 wires `withTaskCancellationHandler` through `SSHScriptRunner.run` and `RemoteSQLiteBackend.query`; cancellation now reaches the `Process` within ~100ms.
### New Project from Scratch wizard + Keychain-backed cron secrets
A third project entry point alongside Browse Catalog and Add Existing Project. Scaffolds a Scarf-standard skeleton, registers it, and hands off to a chat session that auto-activates the bundled `scarf-template-author` skill. The skill drives the rest conversationally — widgets, optional config schema, optional cron — and writes the final files itself.
**Cron + Keychain.** Cron prompts that referenced `secret`-typed config fields used to get the literal `keychain://...` URI back, producing 401s. v2.7 mirrors resolved Keychain values into `~/.hermes/.env` under `$SCARF_<UPPER_SLUG>_<UPPER_FIELD>` env vars. Hermes already reloads `.env` per cron tick — credential rotation is automatic.
### Project dashboards — file-reading widgets, sparklines, typed status
Five new widget types and project-wide auto-refresh. **Backwards-compatible** — every existing `dashboard.json` renders byte-identically.
- **`markdown_file`** / **`log_tail`** / **`cron_status`** / **`image`** / **`status_grid`** — file-reading widgets that auto-refresh when the underlying file changes. By convention, place files inside `<project>/.scarf/`.
- **`stat` widget gains inline sparklines** via optional `sparkline: [Number]`. SVG-only render; dozens per dashboard cost nothing.
- **Typed status badges** with lenient decode (`ok`/`up` → success, `down`/`error` → danger). Unknown strings render as plain text rather than crashing.
- **Structured widget error card** replaces the legacy "Unknown: \<type\>" placeholder.
### OAuth resilience + Credential Pools
- **Daily OAuth keepalive cron** prevents Anthropic OAuth refresh tokens from expiring after weeks of inactivity.
- **Remote re-auth** unblocked — OAuth flow drives a remote `hermes auth add` correctly with stdin forwarded.
- **OAuth remove button** + auto-refresh of Credential Pools on `auth.json` change.
- **`resolve_provider_client` errors** (auxiliary task references an unauthenticated provider) classified into a clear hint with a one-click jump to Settings → Aux Models.
- **Model/provider mismatch banner** detects when `model.default` carries a `<provider>/...` prefix that disagrees with `model.provider`, with one-click fix in either direction.
### ScarfMon — performance instrumentation harness
The diagnostic surface that drove the bulk of the v2.7 perf work. Off by default; signpost-only mode (Instruments-friendly) is free; Full mode keeps a 4096-entry in-memory ring buffer you can copy as JSON for paste-into-issue diagnosis. Wiki: [Performance-Monitoring](https://github.com/awizemann/scarf/wiki/Performance-Monitoring).
See the full [v2.7.0 release notes](https://github.com/awizemann/scarf/releases/tag/v2.7.0) for the complete list (36 commits, including: in-flight coalescing for `loadRecentSessions`, snapshot pipeline rewrite from `sqlite3 .backup` to direct SSH-streamed queries [#74](https://github.com/awizemann/scarf/issues/74), per-message TTS, window-position persistence, sidebar reorder, and many other fixes).
**Previous releases:** see the [Release Notes Index](https://github.com/awizemann/scarf/wiki/Release-Notes-Index) on the wiki for v2.6, v2.5, v2.3, v2.2, v2.0, v1.6, and earlier.
## ScarfGo — the iPhone companion
Same Hermes server you've been running on your Mac — reachable from your phone over SSH. Multi-server, project-scoped chat, session resume, memory editor, cron list, skills tree, settings (read), all native iOS. Pure-Swift SSH (Citadel under the hood — no `ssh` binary needed on iOS). Per-project chat writes the same Scarf-managed `AGENTS.md` block the Mac app does, so the agent boots with the same project context regardless of which client opened the session.
**[Join the public TestFlight](https://testflight.apple.com/join/qCrRpcTz)** — the link is live now but only accepts new beta testers once Apple's Beta Review approves the first build. If you hit a "not accepting testers" splash, bookmark it and try again in 2448h.
<p align="center">
<a href="assets/screenshots/scarfgo-servers.png"><img src="assets/screenshots/scarfgo-servers.png" alt="ScarfGo — Servers list" width="140"></a>
<a href="assets/screenshots/scarfgo-chat.png"><img src="assets/screenshots/scarfgo-chat.png" alt="ScarfGo — Chat with Hermes" width="140"></a>
<a href="assets/screenshots/scarfgo-project-dashboard.png"><img src="assets/screenshots/scarfgo-project-dashboard.png" alt="ScarfGo — Project dashboard" width="140"></a>
<a href="assets/screenshots/scarfgo-skills.png"><img src="assets/screenshots/scarfgo-skills.png" alt="ScarfGo — Skills browser" width="140"></a>
<a href="assets/screenshots/scarfgo-system.png"><img src="assets/screenshots/scarfgo-system.png" alt="ScarfGo — System tab" width="140"></a>
</p>
<p align="center"><sub><em>Tap any thumbnail to view full size. Servers list · Chat · Project dashboard (Site Status Checker template) · Skills browser · System tab.</em></sub></p>
See the [ScarfGo wiki page](https://github.com/awizemann/scarf/wiki/ScarfGo) for the full feature tour, [ScarfGo Onboarding](https://github.com/awizemann/scarf/wiki/ScarfGo-Onboarding) for the SSH-key setup walkthrough, and [Platform Differences](https://github.com/awizemann/scarf/wiki/Platform-Differences) for what is and isn't shared between Mac and iOS.
## Connect ScarfGo to your Hermes server
ScarfGo speaks SSH directly — no companion service, no developer-controlled server in between. Onboarding takes about a minute:
1. **Install via TestFlight.** Open the [public TestFlight link](https://testflight.apple.com/join/qCrRpcTz) on your phone, accept the invite, install ScarfGo from TestFlight (just like any other beta).
2. **Tap Add Server.** Enter the host (IP or DNS), SSH user, port (default 22), and an optional nickname. Same details you'd type into `ssh user@host`.
3. **Generate Key.** ScarfGo creates a fresh Ed25519 keypair on the device. The private half lives in the iOS Keychain (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`) and is excluded from iCloud sync — it never leaves the phone.
4. **Add the public key to your Hermes host.** Tap **Copy public key**, then on the host run:
```bash
cat >> ~/.ssh/authorized_keys <<'EOF'
<paste the line ScarfGo showed you>
EOF
chmod 600 ~/.ssh/authorized_keys
```
This is its own line per device — the convention any second SSH client uses. Mac (Scarf) keeps using your existing ssh-agent / `~/.ssh/config` and is unaffected.
5. **Tap Test connection.** ScarfGo opens an SSH session, probes for the `hermes` binary, and saves the server on success. If it can't find `hermes`, see the [troubleshooting section](https://github.com/awizemann/scarf/wiki/ScarfGo-Onboarding#troubleshooting) — it's almost always a `PATH` quirk on non-interactive SSH.
Done. Open the Dashboard tab and tap any session to resume it; tap the **+** in Chat to start a new project-scoped session.
## Multi-server, one window per server
Scarf 2.0 is a multi-window app. Each window is bound to exactly one Hermes server — your local `~/.hermes/` is synthesized automatically, and you can add remotes via **File → Open Server…** → **Add Server** (host, user, port, optional identity file). Open a second window for a different server and the two run side-by-side with independent state.
Remote Hermes is reached over system SSH — the same `~/.ssh/config`, ssh-agent, ProxyJump, and ControlMaster pooling your terminal uses. File I/O flows through `scp`/`sftp`; SQLite is served from atomic `sqlite3 .backup` snapshots cached under `~/Library/Caches/scarf/snapshots/<server-id>/`; chat (ACP) tunnels as `ssh -T host -- hermes acp` with JSON-RPC over stdio end-to-end. Everything in the feature list below works against remote identically to local.
### Remote setup requirements
The remote host must have:
1. **SSH access** — key-based auth via your local ssh-agent. Scarf never prompts for passphrases; run `ssh-add` once in Terminal before connecting.
2. **`sqlite3`** on the remote `$PATH` — needed for the atomic DB snapshots. Install on the remote with `apt install sqlite3` (Ubuntu/Debian), `yum install sqlite` (RHEL/Fedora), or `apk add sqlite` (Alpine).
3. **`pgrep`** on the remote `$PATH` — used by the Dashboard "is Hermes running" check. Standard on every distro; install `procps` if missing.
4. **`~/.hermes/` readable by the SSH user**. When Hermes runs as a separate user (systemd service, Docker container), the SSH user needs read access to `config.yaml` and `state.db`. Either (a) SSH as the Hermes user, (b) `chmod` Hermes's home to be group-readable and add your SSH user to that group, or (c) set the **Hermes data directory** field when adding the server to point at the right location (e.g. `/var/lib/hermes/.hermes`).
### Troubleshooting remote connections
If the connection pill is green but the Dashboard shows "Stopped", "unknown", or empty values, the SSH user can't read the Hermes state files. Open **Manage Servers → 🩺 Run Diagnostics** (or click the yellow "Can't read Hermes state" pill in the toolbar). The diagnostics sheet runs fourteen checks in one SSH session — connectivity, `sqlite3` presence, read access to `config.yaml` and `state.db`, the effective non-login `$PATH` — and tells you exactly which one fails and why, with remediation hints for each. Use the **Copy Full Report** button to paste the full output into a bug report.
For the common "Hermes isn't at the default path" case (systemd services, Docker), **Test Connection** in the Add Server sheet now probes `/var/lib/hermes/.hermes`, `/opt/hermes/.hermes`, `/home/hermes/.hermes`, and `/root/.hermes` when it can't find `state.db` at `~/.hermes/`, and offers a one-click fill if it finds any of them.
## Features
Scarf mirrors Hermes's surface area through a sidebar-based UI. Sections below map 1:1 to the app's sidebar.
### Monitor
- **Dashboard** — System health, token usage, cost tracking, recent sessions with live refresh
- **Insights** — Usage analytics with token breakdown (including reasoning tokens), cost tracking, model/platform stats, top tools bar chart, activity heatmaps, notable sessions, and time period filtering (7/30/90 days or all time)
- **Sessions Browser** — Full conversation history with message rendering, model reasoning/thinking display, tool call inspection, full-text search, rename, delete, and JSONL export. Subagent sessions are filtered from the main list and accessible via parent session drill-down
- **Activity Feed** — Recent tool execution log with filtering by kind and session, detail inspector with pretty-printed arguments and tool output display
### Interact
- **Live Chat** — Two modes: **Rich Chat** streams responses in real-time via the Agent Client Protocol (ACP) with iMessage-style bubbles, markdown rendering, tool call visualization, thinking/reasoning display, permission request dialogs, and a one-click `/compress` focus sheet (when Hermes advertises the command); **Terminal** runs `hermes chat` with full ANSI color and Rich formatting via [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm). Both modes support session persistence, resume/continue previous sessions, auto-reconnection with session recovery, and voice mode controls
- **Memory Viewer/Editor** — View and edit Hermes's MEMORY.md and USER.md with live file-watcher refresh, external memory provider awareness (Honcho, Supermemory, etc.), and profile-scoped memory support with profile picker
- **Skills Browser** — Browse installed skills by category with file content viewer and required config warnings. **New in 1.6:** Browse the Skills Hub, search by registry (official, skills.sh, well-known, GitHub, ClawHub, LobeHub), install, check for updates, and uninstall — all from the app
### Configure *(new in 1.6)*
- **Platforms** — Native GUI setup for all 13 messaging platforms (Telegram, Discord, Slack, WhatsApp, Signal, Email, Matrix, Mattermost, Feishu, iMessage, Home Assistant, Webhook, CLI). Per-platform forms write credentials to `~/.hermes/.env` and behavior toggles to `~/.hermes/config.yaml`. WhatsApp and Signal pairing use an inline SwiftTerm terminal for QR scan and signal-cli daemon management
- **Personalities** — List defined personalities, pick the active one, and edit `SOUL.md` inline with markdown preview
- **Quick Commands** — Editor for custom `/command_name` shell shortcuts with dangerous-pattern detection (`rm -rf`, `mkfs`, etc.)
- **Credential Pools** — Per-provider credential rotation with a fixed OAuth flow (URL extraction + browser open + code paste) and proper `--type api-key` handling. API keys never stored in UI state — only last-4 preview. Strategy picker (fill_first / round_robin / least_used / random)
- **Plugins** — Install via Git URL or `owner/repo`, update, remove, enable/disable. Reads `~/.hermes/plugins/` directly for reliable state
- **Webhooks** — Create, list, test-fire, and remove webhook subscriptions. Detects the "platform not enabled" state and links to gateway setup
- **Profiles** — Switch between multiple isolated Hermes instances. Create, rename, delete, export (zip), import. Safe-switch warning reminds users to restart Scarf after activating a different profile
### Manage
- **Tools** — Enable/disable toolsets per platform with a connectivity-aware platform menu (green/orange/grey/red dots for connected/configured/offline/error). **Fixed in 1.6:** all 13 platforms now appear (was previously stuck on CLI)
- **MCP Servers** — Manage Model Context Protocol servers Hermes connects to. Add via curated presets (GitHub, Linear, Notion, Sentry, Stripe, and more) or fully custom (stdio command + args, or HTTP URL with optional bearer auth). Per-server detail view with enable/disable toggle, environment variable + header editor, tool-include/exclude filters, resources/prompts toggles, request and connect timeouts, OAuth token detection + clearing, and one-click "Test Connection" that runs `hermes mcp test` and surfaces the discovered tool list. Gateway-restart banner appears after config changes that require a reload
- **Gateway Control** — Start/stop/restart the messaging gateway, view platform connection status, manage user pairing (approve/revoke)
- **Cron Manager** — View scheduled jobs with pre-run scripts, delivery failure tracking, timeout info, and `[SILENT]` job indicators. **New in 1.6:** full write support — create, edit, pause, resume, run-now, and delete jobs from the app
- **Health** — Component-level status and diagnostics. **New in 1.6:** inline "Run Dump" and "Share Debug Report" buttons (the latter with an upload-confirmation dialog before sending to Nous support)
- **Log Viewer** — Real-time log tailing for agent.log, errors.log, and gateway.log with level filtering, component filter (Gateway / Agent / Tools / CLI / Cron), clickable session-ID pills that filter to a single session, and text search
- **Settings** — **Restructured in 1.6** into a 10-tab layout: General, Display, Agent, Terminal, Browser, Voice, Memory, Aux Models, Security, Advanced. Exposes ~60 previously hidden config fields including all 8 auxiliary model tasks, container limits, full TTS/STT provider settings, human-delay simulation, compression thresholds, logging rotation, checkpoints, website blocklist, Tirith sandbox, and delegation. One-click **Backup & Restore** via `hermes backup` / `hermes import`. Model picker replaces the old free-text model field, backed by the models.dev cache (111 providers, all major models) with a "Custom…" escape hatch
### Project Dashboards
Custom, agent-generated dashboards for any project. Define stat boxes, charts, tables, progress bars, checklists, rich text, and embedded web views in a simple JSON file — Scarf renders them with live refresh. Let your Hermes agent build and maintain project-specific visualizations automatically. See [Project Dashboards](#project-dashboards-1) below for the full schema.
### System
- **Hermes Process Control** — Start, stop, and restart the Hermes agent directly from Scarf
- **Menu Bar** — Status icon showing Hermes running state with quick actions
## Requirements
- macOS 14.6+ (Sonoma) for Scarf
- iOS 18.0+ for [ScarfGo](https://github.com/awizemann/scarf/wiki/ScarfGo) (the iPhone companion, public TestFlight from v2.5)
- Xcode 16.0+ to build from source
- [Hermes agent](https://github.com/hermes-ai/hermes-agent) v0.6.0+ installed at `~/.hermes/` on each target host (v0.12.0+ recommended for full v2.6 feature support — autonomous Curator, multimodal image input, 5 new providers, Microsoft Teams + Yuanbao gateways, Kanban, Skills v0.12 surface, cron `--workdir`, prompt-cache TTL, Piper TTS, Vercel terminal)
- For remote servers: SSH access (key-based), `sqlite3` on the remote (for atomic DB snapshots), and the `hermes` CLI resolvable from the remote user's `PATH` or at a path you specify per server. ScarfGo requires the same on every Hermes host it connects to.
### Compatibility
Scarf reads Hermes's SQLite database and parses CLI output from `hermes status`, `hermes doctor`, `hermes tools`, `hermes sessions`, `hermes gateway`, and `hermes pairing`. Automatic schema detection provides backward compatibility with older databases while supporting new features in newer Hermes versions.
| Hermes Version | Status |
|----------------|--------|
| v0.6.0 (2026-03-30) | Verified |
| v0.7.0 (2026-04-03) | Verified |
| v0.8.0 (2026-04-08) | Verified |
| v0.9.0 (2026-04-13) | Verified |
| v0.10.0 (2026-04-16) | Verified (Tool Gateway introduced) |
| v0.11.0 (2026-04-23) | Verified |
| v0.12.0 (2026-04-30) | **Verified — current target (recommended for full v2.6 feature support)** |
Scarf 2.6 targets Hermes v0.12.0 for the autonomous Curator, multimodal ACP image content blocks, the 5 new inference providers, Microsoft Teams + Yuanbao gateways, the read-only Kanban view, the Skills v0.12 surface (URL install / reload / disable badges / curator pin), cron `--workdir`, `auxiliary.curator`, `prompt_caching.cache_ttl`, the redaction toggle, the runtime metadata footer, Piper TTS, and the Vercel terminal backend. Every v0.12 surface is **capability-gated** — Scarf detects the host's Hermes version once per server connection (`hermes --version` → semver + `YYYY.M.D` parse) and hides v0.12-only UI on older hosts. v0.11.0 hosts keep the full v2.5 surface (`/steer`, `messages.reasoning_content`, `sessions.api_call_count`, design-md/spotify skills, SKILL.md frontmatter chips, `hermes memory reset`). Earlier Hermes versions remain supported for monitoring, sessions, file-based features, and ACP chat; new behavior degrades gracefully on older agents.
If a Hermes update changes the database schema or CLI output format, Scarf may need to be updated. Check the [Health](#features) view for compatibility warnings.
## Install
### Pre-built Binary (no Xcode required)
Download the latest build from [Releases](https://github.com/awizemann/scarf/releases):
- `Scarf-vX.X.X-Universal.zip` — Apple Silicon + Intel (recommended)
- `Scarf-vX.X.X-ARM64.zip` — Apple Silicon only (smaller download)
1. Unzip and drag **Scarf.app** to Applications
2. Launch normally — builds are Developer ID signed and notarized, so Gatekeeper accepts them on first launch
Scarf checks for updates automatically on launch via [Sparkle](https://sparkle-project.org) and daily thereafter. You can disable automatic checks or trigger a manual check from **Settings → General → Updates** or the menu bar icon.
#### "Scarf.app is damaged" on first launch
If Gatekeeper rejects the app on first launch (occasionally happens on macOS 14+ for zip-distributed apps depending on extraction tool + quarantine state), the bundle itself is fine — every release is verified to pass `codesign --verify --strict --deep` and `spctl --assess --type execute` before it ships. The fix is to **only remove the quarantine attribute**, never strip all xattrs or re-sign:
```bash
# Recommended — non-destructive
xattr -d com.apple.quarantine /Applications/Scarf.app
# Or extract with ditto instead of double-clicking the zip:
ditto -xk ~/Downloads/Scarf-vX.X.X-Universal.zip ~/Downloads/
```
**Do not run `xattr -rc /Applications/Scarf.app`** — it strips codesign-related extended attributes and can break the bundle's seal. **Do not run `codesign --force --deep --sign - /Applications/Scarf.app`** — `--deep` ad-hoc re-signing is incompatible with Sparkle.framework's nested XPC services and `Updater.app` sub-bundle, and will corrupt the framework signature even if the outer app appears intact afterward. If a clean re-download + `xattr -d com.apple.quarantine` doesn't resolve the issue, please open an issue with `codesign --verify --verbose=4 --strict /Applications/Scarf.app` output captured **before** any mitigation attempts.
### Build from Source
```bash
git clone https://github.com/awizemann/scarf.git
cd scarf/scarf
open scarf.xcodeproj
```
Or from the command line:
```bash
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Release -arch arm64 -arch x86_64 ONLY_ACTIVE_ARCH=NO build
```
For an unsigned local Debug build without an Apple Developer account (handy for contributors), use [`./scripts/local-build.sh`](scripts/local-build.sh) — see [BUILDING.md](BUILDING.md) for prerequisites.
## Architecture
Scarf follows the **MVVM-Feature** pattern with zero external dependencies beyond SwiftTerm:
```
scarf/
Core/
Models/ Plain data structs (HermesSession, HermesMessage, HermesConfig, etc.)
Services/ Data access (SQLite reader, file I/O, log tailing, file watcher)
Features/ Self-contained feature modules
Dashboard/ System overview and stats
Insights/ Usage analytics and activity patterns
Sessions/ Conversation browser with rename, delete, export
Activity/ Tool execution feed with inspector
Projects/ Agent-generated project dashboards with widget rendering
Chat/ Rich ACP chat and embedded terminal with voice controls
Memory/ Memory viewer and editor
Skills/ Skill browser by category
Tools/ Toolset management per platform
MCPServers/ MCP server registry, presets, OAuth, tool filters, test runner
Gateway/ Messaging gateway control and pairing
Cron/ Scheduled job viewer
Logs/ Real-time log viewer
Settings/ Structured config editor
Navigation/ AppCoordinator + SidebarView
```
### Data Sources
Scarf reads Hermes data directly from `~/.hermes/`:
| Source | Format | Access |
|--------|--------|--------|
| `state.db` | SQLite (WAL mode) | Read-only |
| `config.yaml` | YAML | Read-only |
| `memories/*.md` | Markdown | Read/Write |
| `cron/jobs.json` | JSON | Read-only |
| `logs/*.log` | Text | Read-only |
| `gateway_state.json` | JSON | Read-only |
| `skills/` | Directory tree | Read-only |
| `hermes acp` | ACP subprocess (JSON-RPC stdio) | Real-time chat |
| `hermes chat` | Terminal subprocess | Interactive |
| `hermes tools` | CLI commands | Enable/Disable |
| `hermes sessions` | CLI commands | Rename/Delete/Export |
| `hermes gateway` | CLI commands | Start/Stop/Restart |
| `hermes pairing` | CLI commands | Approve/Revoke |
| `hermes mcp` | CLI commands | Add/Remove/Test MCP servers |
| `mcp-tokens/*.json` | JSON (per-server OAuth) | Detect/Delete |
| `.scarf/dashboard.json` | JSON (per-project) | Read-only |
| `scarf/projects.json` | JSON (registry) | Read/Write |
The app opens `state.db` in read-only mode to avoid WAL contention with Hermes. Management actions (tool toggles, session rename/delete/export) go through the Hermes CLI.
### Dependencies
| Package | Purpose |
|---------|---------|
| [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm) | Terminal emulator for the Chat feature |
| [Sparkle](https://github.com/sparkle-project/Sparkle) | Auto-updates from the GitHub-hosted appcast |
Everything else uses system frameworks: SQLite3 C API, Foundation JSON, AttributedString markdown, SwiftUI Charts, GCD file watching.
## How It Works
Scarf watches `~/.hermes/` for file changes and queries the SQLite database for sessions, messages, and analytics. Views refresh automatically when Hermes writes new data.
The Chat tab has two modes. **Rich Chat** communicates with Hermes via the Agent Client Protocol (ACP) — a JSON-RPC connection over stdio — streaming responses in real-time with automatic reconnection and session recovery on connection loss. **Terminal** mode spawns `hermes chat` in a pseudo-terminal for the full interactive CLI experience with proper ANSI rendering. Sessions persist across navigation in both modes — switch tabs and come back without losing your conversation.
Management actions (renaming sessions, toggling tools, editing memory) call the Hermes CLI or write directly to the appropriate files, keeping Scarf and Hermes in sync.
The app sandbox is disabled because Scarf needs direct access to `~/.hermes/` and the ability to spawn the Hermes binary.
## Project Dashboards
Project Dashboards turn Scarf into a customizable monitoring hub for all your projects. You define a simple JSON file in your project folder describing what to display — stat boxes, charts, tables, progress bars, checklists, rich text, and embedded web views — and Scarf renders it as a live-updating dashboard. Your Hermes agent can generate and maintain these dashboards automatically.
### What You Can Build
- **Development dashboards** — test coverage, build status, open issues, sprint progress
- **Data project trackers** — pipeline metrics, data quality scores, processing throughput
- **Deployment monitors** — deploy history tables, uptime stats, error rate charts
- **Research dashboards** — experiment results, key findings, paper status checklists
- **Agent activity views** — cron job results, content generation stats, task completion rates
- **Embedded web apps** — local dev servers, HTML reports, Grafana dashboards, any web-based tool your agent generates
- **Any project status** — if your agent can measure it, Scarf can display it
### Quick Start
**1. Create the dashboard file**
Create `.scarf/dashboard.json` in any project folder:
```json
{
"version": 1,
"title": "My Project",
"description": "Project status at a glance",
"sections": [
{
"title": "Overview",
"columns": 3,
"widgets": [
{
"type": "stat",
"title": "Test Coverage",
"value": "87%",
"icon": "checkmark.shield",
"color": "green",
"subtitle": "+2.1% this week"
},
{
"type": "progress",
"title": "Sprint Progress",
"value": 0.73,
"label": "73% complete",
"color": "blue"
},
{
"type": "list",
"title": "Tasks",
"items": [
{ "text": "Write unit tests", "status": "done" },
{ "text": "Update API docs", "status": "active" },
{ "text": "Deploy to prod", "status": "pending" }
]
}
]
}
]
}
```
**2. Register your project**
In Scarf, go to **Projects** in the sidebar and click the **+** button to add your project folder. Or have your agent add it directly to the registry at `~/.hermes/scarf/projects.json`:
```json
{
"projects": [
{ "name": "my-project", "path": "/Users/you/Developer/my-project" }
]
}
```
**3. View in Scarf**
Select your project in the Projects sidebar — the dashboard renders immediately. Scarf watches the file for changes and refreshes automatically whenever the JSON is updated.
### Widget Types
| Type | Description | Key Fields |
|------|-------------|------------|
| `stat` | Key metric with large value display | `value`, `icon`, `color`, `subtitle` |
| `progress` | Progress bar with label | `value` (0.01.0), `label`, `color` |
| `text` | Rich text block | `content`, `format` ("markdown" or "plain") |
| `table` | Data table with headers | `columns`, `rows` |
| `chart` | Line, bar, or pie chart | `chartType`, `series` (each with `name`, `color`, `data`) |
| `list` | Checklist with status indicators | `items` (each with `text`, `status`: done/active/pending) |
| `webview` | Embedded web browser | `url`, `height` (default 400) |
The `webview` widget embeds a live web browser directly in your dashboard — perfect for displaying local dev servers, HTML reports, or any web-based tool your agent generates.
When a dashboard includes a webview widget, Scarf adds a tabbed interface: **Dashboard** shows your normal widgets, **Site** shows the web content full-canvas with clean margins — using the entire available space in the app. This gives you the best of both worlds: compact metrics at a glance, and a full embedded browser when you need it.
```json
{
"type": "webview",
"title": "Project Report",
"url": "http://localhost:8000/dashboard",
"height": 500
}
```
- `url`: Any URL — typically a local server (`http://localhost:...`) or file path
- `height`: Height in points when displayed as an inline widget card (default: 400). The Site tab always uses full available space regardless of this setting.
**Colors**: red, orange, yellow, green, blue, purple, pink, teal, indigo, mint, brown, gray
**Icons**: Any [SF Symbol](https://developer.apple.com/sf-symbols/) name (e.g., `checkmark.shield`, `cpu`, `doc.text`, `chart.bar`)
### Agent-Generated Dashboards
The real power is letting your Hermes agent build and update dashboards automatically. Add instructions like this to your agent's context:
> Analyze this project and create a `.scarf/dashboard.json` dashboard with relevant metrics and status. Use stat widgets for key numbers, charts for trends, tables for structured data, lists for task tracking, and a webview widget if the project has a local web server or HTML reports. Register the project in `~/.hermes/scarf/projects.json` if not already registered.
Your agent can update the dashboard as part of cron jobs, after builds, or whenever project state changes. Since Scarf watches the file, updates appear in real-time.
### Dashboard Schema Reference
```json
{
"version": 1,
"title": "Required — dashboard title",
"description": "Optional — subtitle text",
"updatedAt": "Optional — ISO 8601 timestamp",
"sections": [
{
"title": "Section Name",
"columns": 3,
"widgets": [{ "type": "...", "title": "..." }]
}
]
}
```
Each section defines a grid with 14 columns. Widgets flow left-to-right, wrapping to new rows. See [DASHBOARD_SCHEMA.md](scarf/docs/DASHBOARD_SCHEMA.md) for the full schema reference with examples of every widget type.
## Releases
Scarf ships through GitHub releases — the App Store is not supported because Scarf spawns the user-installed `hermes` binary and reads `~/.hermes/` directly, both of which App Sandbox forbids.
Each release goes through a single local script: [scripts/release.sh](scripts/release.sh). The script archives a universal binary, signs it with the Developer ID Application cert, submits to `notarytool`, staples the ticket, produces the distribution zip, signs an appcast entry with Sparkle's EdDSA key, pushes an updated `appcast.xml` to the `gh-pages` branch, creates the GitHub release, and tags `main`.
The Sparkle appcast is served from [awizemann.github.io/scarf/appcast.xml](https://awizemann.github.io/scarf/appcast.xml).
Signing prerequisites (one-time):
- `Developer ID Application` certificate in the login Keychain
- `scarf-notary` keychain profile registered via `xcrun notarytool store-credentials`
- Sparkle EdDSA private key in Keychain item `https://sparkle-project.org` (back this up — without it, shipped apps can never receive updates)
## Template Catalog
Community-contributed Scarf project templates live under [`templates/`](templates/) in this repo and are browsable at **[awizemann.github.io/scarf/templates/](https://awizemann.github.io/scarf/templates/)** with live dashboard previews and one-click `scarf://install?url=…` links.
- **Install from the web** — click "Install with Scarf" on any template's detail page; the app takes over from there.
- **Install from a local file** — Scarf → Projects → Templates → Install from File…, or double-click any `.scarftemplate` in Finder.
- **Author a template** — see [`templates/CONTRIBUTING.md`](templates/CONTRIBUTING.md) for the full walkthrough. Fork, drop a template under `templates/<your-github-handle>/<your-name>/`, open a PR; CI validates the bundle automatically.
The catalog's site is a static HTML + vanilla JS build generated by [`tools/build-catalog.py`](tools/build-catalog.py) and driven by [`scripts/catalog.sh`](scripts/catalog.sh) (check / build / preview / publish). Appcast and main landing page are independent — updating the catalog never disturbs Sparkle.
## Contributing
Contributions are welcome. Please open an issue to discuss what you'd like to change before submitting a PR.
1. Fork the repo
2. Create your feature branch (`git checkout -b feature/my-feature`)
3. Commit your changes (`git commit -m 'Add my feature'`)
4. Push to the branch (`git push origin feature/my-feature`)
5. Open a Pull Request
For template submissions, see [`templates/CONTRIBUTING.md`](templates/CONTRIBUTING.md) — same flow, with a catalog-specific checklist + automated CI validation.
## Support
If you find Scarf useful, consider buying me a coffee.
<a href="https://www.buymeacoffee.com/awizemann"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" height="40"></a>
## License
[MIT](LICENSE)
View File
+785
View File
@@ -0,0 +1,785 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Scarf Updates</title>
<link>https://awizemann.github.io/scarf/appcast.xml</link>
<description>Scarf macOS app updates</description>
<language>en</language>
<item>
<title>Version 2.8.0</title>
<sparkle:version>35</sparkle:version>
<sparkle:shortVersionString>2.8.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 09 May 2026 19:02:51 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.8.0</h2>
<p>A coordinated catch-up release adopting Hermes v0.13.0 (v2026.5.7) — &quot;The Tenacity Release&quot; — across Scarf&#x27;s full surface area. v2.8.0 ships <strong>Persistent Goals</strong>, <strong>ACP <code>/queue</code></strong>, <strong>Kanban diagnostics + recovery UX</strong>, <strong>Curator archive/prune</strong>, <strong>Google Chat (20th platform) + cross-platform allowlists</strong>, a refreshed <strong>provider catalog</strong> with five new models, and a slate of settings + UX polish — all behind capability flags so pre-v0.13 hosts continue to render the v2.7.5 surface unchanged.</p>
<p>No data migrations, no schema changes. <code>~/.hermes/state.db</code> columns are unchanged from v0.11/v0.12. Existing <code>~/.hermes/scarf/</code> sidecars are untouched. Sparkle picks the update up automatically.</p>
<h3>New features</h3>
<h4>Persistent Goals + ACP <code>/queue</code> (chat)</h4>
<ul>
<li><strong><code>/goal &lt;text&gt;</code> slash command</strong> — locks the agent on a target that persists across turns. Surfaced via the chat slash menu (gated on <code>HermesCapabilities.hasGoals</code>) and rendered as an <code>info</code>-tinted &quot;Goal locked: …&quot; pill in the chat header. The pill exposes a &quot;Clear goal&quot; context-menu item that dispatches <code>/goal --clear</code>. Optimistic local mirror — Hermes is the authoritative owner; Scarf paints the pill the moment the user sends <code>/goal …</code> so the affordance feels instant.</li>
<li><strong><code>/queue &lt;text&gt;</code> slash command</strong> — queues a prompt to run after the current turn completes. Joins <code>/steer</code> and <code>/goal</code> in <code>RichChatViewModel.nonInterruptiveCommands</code> (the chat keeps &quot;Agent working…&quot; off when sent). A header chip shows the queued count; tap opens a popover listing prompts + relative timestamps. Per-entry deletion isn&#x27;t exposed (Hermes has no remove-by-id verb), and the popover header makes that explicit so users understand the local mirror&#x27;s role.</li>
<li><strong><code>/steer</code> on idle</strong> — pre-v0.13 was a no-op when no turn was in flight; v0.13 runs it as a regular prompt. The composer&#x27;s slash button now greys <code>/steer</code> only on pre-v0.13 hosts (gated on <code>hasACPSteerOnIdle</code>).</li>
<li><strong>Static slash-menu fallbacks</strong> — pre-session, the menu surfaces <code>/new</code> (with optional <code>[&lt;name&gt;]</code> argument hint on v0.13). Active-session-only fallbacks (<code>/clear</code>, <code>/compact</code>, <code>/cost</code>, <code>/model</code>, <code>/tools</code>, <code>/reload-skills</code>, <code>/help</code>, <code>/exit</code>) round out resumed sessions where Hermes ACP doesn&#x27;t re-emit <code>available_commands_update</code> after <code>session/load</code>. Deduped against the ACP-advertised set so the canonical entry always wins once a session opens.</li>
</ul>
<h4>Kanban v0.13 diagnostics + recovery UX</h4>
<ul>
<li><strong>Hallucination-gate verify / reject</strong> — worker-created cards land with <code>hallucination_gate_status: pending</code>. The inspector renders a yellow banner (&quot;Created by a worker — verify before running&quot;) with a Verify and Reject button. Cards in pending state dim 0.6 with a yellow ⚠ glyph in the title row.</li>
<li><strong>Diagnostics rendering</strong> — new typed-mirror enum <code>KanbanDiagnosticKind</code> with severity (info / warning / critical). Per-task and per-run diagnostics surface in the inspector Runs tab as chip-lists; auto-block reasons render verbatim in the existing red banner. Darwin zombie detections show as a distinct <code>darwin_zombie_detected</code> kind.</li>
<li><strong>Per-task <code>max_retries</code></strong> — added to the create sheet (default 3) and shown as a header chip in the inspector. Write-once at create time, matching Hermes&#x27;s pattern.</li>
<li><strong>Multiline title/body</strong> — the create sheet&#x27;s Title field accepts multiline input, capped to four visible rows.</li>
<li><strong>Tolerant decoding</strong> — every new field uses <code>decodeIfPresent</code>. Pre-v0.13 JSON parses cleanly with the new fields defaulting to nil, and the v2.7.5 board surface is unchanged on older hosts.</li>
</ul>
<h4>Curator archive + prune</h4>
<ul>
<li><strong>Archived skills section</strong> in <code>CuratorView</code> showing <code>hermes curator list-archived</code> output. Each row exposes Restore (returns to the active leaderboard) and Prune (destructive — opens a custom confirm sheet matching the template-uninstall pattern, with <code>ScarfDestructiveButton</code> &quot;Prune permanently&quot; and Cancel as the default keyboard action).</li>
<li><strong>Bulk prune</strong> — a header action (gated on archived list non-empty) that enumerates every archived skill in the confirm sheet before a single-tap destructive action. Per-skill prune buttons are present per row when Hermes supports <code>prune &lt;name&gt;</code>; otherwise only the bulk action is exposed.</li>
<li><strong>Synchronous &quot;Run Now&quot;</strong> — v0.13 <code>hermes curator run</code> blocks until done. The Run Now button shows a progress affordance for the duration; pre-v0.13 falls back to fire-and-forget.</li>
<li><strong>New <code>CuratorService</code> actor</strong> in ScarfCore (<a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/CuratorService.swift">scarf/Packages/ScarfCore/Sources/ScarfCore/Services/CuratorService.swift</a>) — pure-I/O Sendable actor mirroring <code>KanbanService</code>&#x27;s shape, with defensive <code>--json</code> retry-without-flag fallback for verbs that may not support it on all v0.13 patch releases.</li>
<li>The legacy <code>CuratorRestoreSheet</code> flow (SAFE-list-restore for v0.12) is preserved; it predates the v0.13 archive surface and serves a distinct case.</li>
</ul>
<h4>Messaging Gateway expansion</h4>
<ul>
<li><strong>Google Chat</strong> — 20th platform. New entry in the Mac Platforms tab, gated on <code>HermesCapabilities.hasGoogleChatPlatform</code>.</li>
<li><strong>Cross-platform allowlists</strong> — per-platform editor for <code>allowed_channels</code> (Slack / Mattermost / Google Chat), <code>allowed_chats</code> (Telegram / WhatsApp), and <code>allowed_rooms</code> (Matrix / DingTalk). New <code>AllowlistEditor</code> component plus the <code>GatewayAllowlistKind</code> / <code>GatewayPlatformSettings</code> ScarfCore types. Persisted to <code>~/.hermes/config.yaml</code> via a new <code>GatewayConfigWriter</code> since <code>hermes config set</code> doesn&#x27;t write list blocks.</li>
<li><strong>Per-platform behavior toggles</strong> — <code>busy_ack_enabled</code> (suppress per-message &quot;agent is working…&quot; acks), <code>gateway_restart_notification</code> (post a &quot;Gateway restarted&quot; notice on boot), and a slash-command auto-delete TTL (seconds, 0 to disable). Each appears in the new <code>GatewayBehaviorSection</code> component.</li>
<li><strong><code>hermes gateway list</code> cross-profile digest</strong> — inline status row in <code>MessagingGatewayView</code> showing which profile is running which platform across all profiles. New <code>HermesGatewayListService</code> actor parses <code>hermes gateway list --json</code>. Hidden when the verb fails (pre-v0.13 hosts) or no profiles are registered.</li>
<li><strong><code>MessagingGatewayViewModel</code></strong> — internal rename from <code>GatewayViewModel</code> to disambiguate from the v0.10 Tool Gateway feature. The user-facing label was already &quot;Messaging Gateway&quot; since v0.10.</li>
<li><strong><code>[[as_document]]</code> hint</strong> — informational tooltip in skill detail surfaces explaining the new media-routing directive for skills that reference it.</li>
</ul>
<h4>Provider catalog refresh</h4>
<ul>
<li><strong>Five new models</strong> — <code>deepseek/deepseek-v4-pro</code>, <code>x-ai/grok-4.3</code>, <code>openrouter/owl-alpha</code> (free tier), <code>tencent/hy3-preview</code>, and <code>arcee/trinity-large-thinking</code> (with temperature + compression overrides). Surfaced through <code>models_dev_cache.json</code>; no manual entries required.</li>
<li><strong>Grok rename</strong> — <code>x-ai/grok-4.20-beta</code> → <code>x-ai/grok-4.20</code>. Implemented via read-time alias resolution in <code>ModelCatalogService.modelAliases</code> so existing user configs with the <code>-beta</code> suffix keep validating without YAML rewrites. Three composite-keyed aliases cover the openrouter / xai / vercel routes.</li>
<li><strong>Vercel AI Gateway demoted</strong> — sort comparator change in <code>loadProviders()</code> puts Vercel last, after the alphabetical group.</li>
<li><strong><code>image_gen.model</code> honored</strong> — pre-v0.13 the key was advertised but ignored; v0.13 actually drives the image-generation path. Surfaced in <code>Settings → Auxiliary</code> with a curated picker (<code>OpenAI gpt-image-1</code>, <code>Imagen 3/4</code>, <code>Stable Image Ultra</code>, <code>FLUX 1.1 Pro</code>, <code>DALL·E 3</code>); free-form entry is also accepted. Gated on <code>hasImageGenModel</code>.</li>
<li><strong>OpenRouter response caching</strong> — toggle in <code>Settings → Auxiliary</code> writing <code>openrouter.response_cache.enabled</code> to <code>config.yaml</code>. Off by default in Scarf&#x27;s parser. Gated on <code>hasOpenRouterResponseCache</code>.</li>
</ul>
<h4>Settings tab additions</h4>
<ul>
<li><strong>MCP SSE transport</strong> — MCP add-server flow gains a Transport picker (<code>stdio</code> / <code>http</code> / <code>sse</code>) with <code>sse_read_timeout</code> field for SSE servers. The YAML round-trip preserves OAuth + headers identically to the existing <code>.http</code> shape. Gated on <code>hasMCPSSETransport</code>.</li>
<li><strong>Cron <code>--no-agent</code> watchdog mode</strong> — toggle in the Cron edit sheet that maps to <code>hermes cron create/update --no-agent</code>. When ON, the prompt + context fields hide (the AI call is skipped). Defensive write-path strips the flag on pre-v0.13 hosts mirroring the <code>--workdir</code> pattern. New <code>HermesCronJob.noAgent: Bool</code> field with <code>decodeIfPresent</code> so pre-v0.13 reads keep parsing. Gated on <code>hasCronNoAgent</code>.</li>
<li><strong>Web Tools per-capability backends</strong> — new <code>Settings → Web Tools</code> tab with separate pickers for <code>web_search</code> and <code>web_extract</code>. SearXNG appears in the search picker only. The legacy single <code>web_tools.backend</code> is still readable for round-trip safety on mixed-version installs. Gated on <code>hasWebToolsBackendSplit</code>.</li>
<li><strong>Profiles <code>--no-skills</code></strong> — &quot;Empty profile (no skills)&quot; toggle in the create-profile flow that appends <code>--no-skills</code> to <code>hermes profile create</code>. Disabled when &quot;Clone all&quot; is on (mutually exclusive). Gated on <code>hasProfileNoSkills</code>.</li>
</ul>
<h4>UX polish</h4>
<ul>
<li><strong>Context compression count</strong> in the chat status bar. v0.13 emits the count alongside the token tally on the <code>session/prompt</code> response; Scarf renders a <code>🗜 ×N</code> chip next to the token count when <code>count &gt; 0</code>. Gated on <code>hasContextCompressionCount</code>.</li>
<li><strong><code>/new &lt;name&gt;</code> argument hint</strong> — bracket-aware so v0.13 hosts show <code>[&lt;name&gt;]</code> and pre-v0.13 hosts show no hint.</li>
<li><strong><code>HermesUpdaterCommandBuilder</code></strong> — forward-compat plumbing for <code>hermes update --yes</code>. No in-app surface in v2.8.0 (Scarf doesn&#x27;t currently expose a &quot;Run hermes update&quot; command); the builder is wired so a future Settings affordance can opt in cleanly.</li>
<li><strong>Redaction default-flip awareness</strong> — the existing <code>Settings → Advanced → Redaction</code> toggle hint copy now branches on <code>HermesCapabilities.isV013OrLater</code>. v0.13+ hosts read &quot;Recommended: ON. Hermes v0.13 defaults to redacting secrets unless you opt out&quot;; pre-v0.13 keeps the v2.7 hint.</li>
<li><strong><code>display.language</code> picker</strong> — new <code>Settings → General → Locale</code> row. 8 options: default, zh, ja, de, es, fr, uk, tr. Hermes does the actual translation; Scarf just persists <code>display.language</code> to <code>config.yaml</code>. Gated on <code>hasDisplayLanguage</code>.</li>
<li><strong>xAI Custom Voices badge</strong> — <code>Settings → Voice</code> shows a &quot;Cloning supported&quot; <code>ScarfBadge</code> next to the xAI TTS provider entry. Informational only; voice management itself happens via <code>hermes voice</code> CLI. Gated on <code>hasXAIVoiceCloning</code>.</li>
</ul>
<h4>ScarfGo iOS catch-up (read-only)</h4>
<p>Following the Phase H precedent, iOS mirrors selected v2.8 surfaces as read-only — write parity is deferred to v2.8.x.</p>
<ul>
<li><strong>Goal pill + queue chip</strong> in the iOS chat header (<code>projectContextBar</code>). Tap is a no-op; the Mac app owns mutations.</li>
<li><strong>Kanban v0.13 diagnostics</strong> in <code>ScarfGoKanbanDetailSheet</code> — <code>retries: N</code> chip, &quot;Worker-created — verify on Mac&quot; hallucination badge, red <code>auto_blocked_reason</code> banner, tappable diagnostics chip-lists with severity-tinted badges and a new <code>DiagnosticDetailSheet</code> (replacing Mac&#x27;s <code>.help()</code> tooltip on touch).</li>
<li><strong>Curator Archived list</strong> in <code>Scarf iOS/Curator/CuratorView.swift</code> — read-only, with footer pointing users to the Mac app for Restore / Prune actions.</li>
<li><strong>Settings → Platforms extension</strong> — Google Chat status row, busy-ack and restart-notification summary rows across <code>gatewayPlatforms</code> (handles disagreement with &quot;mixed (N platforms)&quot;), allowlist DisclosureGroups with monospaced &quot;platform: id&quot; entries when expanded.</li>
<li><strong>&quot;v0.13 features active&quot; badge</strong> in iOS Settings (gated on <code>caps.isV013OrLater</code>). Tap presents <code>V013FeaturesSheet</code> listing the new affordances.</li>
</ul>
<h3>Capability gating</h3>
<p>v2.8.0 adds 22 new flags on <code>HermesCapabilities</code> (each gating one v0.13 surface), plus an <code>isV013OrLater</code> convenience predicate. Every new affordance is gated; pre-v0.13 hosts see the v2.7.5 surface byte-identical to before. The HermesVersionBanner threshold remains pre-v0.12 — v0.12 → v0.13 nudging happens via the iOS Settings badge (positive surface) rather than a global yellow banner (which was reserved for &quot;missing every new feature&quot; cases).</p>
<h3>Bug fixes uncovered during v0.13.0 dogfooding</h3>
<ul>
<li><strong>Dashboard flicker on v0.13 hosts</strong> — Hermes v0.13 writes to <code>state.db-wal</code> and rotating logs at ~10 Hz during gateway activity. Each FSEvents fire ticked <code>lastChangeDate</code>, every observing view re-fired its load handler against it, and on Local hosts the dashboard stacked 5+ concurrent <code>dashboardSnapshot</code> calls in 200 ms — sqlite contention on the read-only handle surfaced as <code>BackendError error 3</code>, plus visible flicker. Two-part fix: <code>HermesFileWatcher.scheduleCoalescedTick</code> coalesces FSEvents into one observable mutation per 500 ms quiet window with a 1.5 s max-wait floor (so a coincident <code>gateway_state.json</code> Start/Stop touch can&#x27;t be starved indefinitely under sustained WAL writes); <code>DashboardViewModel.load()</code> holds a single in-flight <code>Task&lt;Void, Never&gt;</code> handle so concurrent triggers await the in-flight load instead of stacking.</li>
<li><strong>Sparse slash menu on resumed sessions</strong> — Hermes ACP only emits <code>available_commands_update</code> after <code>session/new</code>, not after <code>session/load</code>. Combined with <code>RichChatViewModel.reset()</code> clearing <code>acpCommands</code> on every session switch, resumed sessions landed at a 4-command fallback even though the agent identity hadn&#x27;t changed. Fix: stop wiping <code>acpCommands</code> in <code>reset()</code> (they&#x27;re agent-level, not session-level), and add an active-session-only static fallback set covering the standard agent commands so cold-start LOAD users see a rich menu immediately.</li>
</ul>
<h3>Migrating from 2.7.5</h3>
<p>Sparkle delivers the update automatically. No config migration, no schema changes — same <code>~/.hermes/state.db</code> columns as v0.11/v0.12, same Scarf-owned sidecars at <code>~/.hermes/scarf/</code>. Existing v2.7.5 Kanban tenants stay valid; existing project manifests are unchanged. Settings tabs grow new rows; existing rows render identically.</p>
<p>If you&#x27;re connecting to a Hermes v0.13.0 host for the first time after this update, the new surfaces light up automatically — no flag flip in the app. Pre-v0.13 hosts continue to render the v2.7.5 surface; nothing breaks if you upgrade Scarf before upgrading Hermes.</p>
<h3>Known limitations</h3>
<ul>
<li><strong>iOS write surfaces</strong> (Verify hallucination gate, Reject, Curator archive/prune actions, allowlist editor, <code>/goal</code> send, <code>/queue</code> send) are explicitly out of scope for v2.8.0 and slated for v2.8.x. iOS surfaces are read-only mirrors per the Phase H precedent.</li>
<li><strong>Auto-resumed-from-checkpoint indicator</strong> — Hermes v0.13&#x27;s &quot;auto-resume after gateway restart&quot; feature is server-side; whether the ACP adapter advertises a Scarf-visible signal is unclear pending live host verification. Deferred to v2.8.1.</li>
<li><strong>xAI voice cloning management UX</strong> — only the &quot;Cloning supported&quot; badge ships in v2.8.0. A full voice-management surface is a follow-up.</li>
<li><strong>Bulk re-tag for legacy NULL-tenant Kanban tasks</strong> — carryover from v2.7.5; Hermes still has no <code>tenant</code> mutation verb post-create.</li>
<li><strong>Cluster A wire-shape TODOs</strong> — 25 <code>// TODO(WS-N-Q&lt;n&gt;)</code> markers across the codebase flag fields and CLI flags whose exact shape couldn&#x27;t be verified from release notes alone. Each has a tolerant-decode default that fails closed (hides the affordance rather than throwing); a pre-merge sweep on a v0.13 host can confirm or fix each in seconds.</li>
</ul>
<h3>Acknowledgements</h3>
<p>v2.8.0 was driven by a 9-stream coordinated multi-agent build: WS-1 capability flag foundation through WS-9 iOS catch-up, with planning artifacts archived under <a href="scarf/docs/v2.8/">scarf/docs/v2.8/</a> for future reference. Bug fixes for the dashboard flicker and sparse-slash-menu issues were caught during a fresh end-to-end dogfood pass against a live Hermes v0.13.0 install — the kind of surface-level UX bugs that only show up under real-world <code>state.db-wal</code> write rates and real-world resume flows. As always, real bugs come from doing instead of speculating.</p>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.8.0/Scarf-v2.8.0-Universal.zip"
sparkle:edSignature="m5HQUKgxfWa5u88gEVCGWMIKaogBIsjPspQG97y1KcrW1w6S5XF1s0v1oRaRWMyIlj46BD+937Inu2Ii5TbXAg=="
length="19771149"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.7.5</title>
<sparkle:version>34</sparkle:version>
<sparkle:shortVersionString>2.7.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 08 May 2026 10:56:09 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.5</h2>
<p>A feature release that lifts Scarf&#x27;s Kanban surface from a read-only list (the v2.6 placeholder shipped while upstream Kanban was still mid-rework) to a full drag-and-drop board with the complete Hermes v0.12 mutation surface wired up — plus per-project boards bound to a Scarf-minted tenant slug, and a read-only board on iOS for at-a-glance status from your phone. No data migrations, no schema changes; pre-v0.12 hosts gracefully hide the surface.</p>
<h3>New features</h3>
<h4>Mac</h4>
<ul>
<li><strong>Drag-and-drop Kanban board</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanBoardView.swift">scarf/Features/Kanban/Views/KanbanBoardView.swift</a>). Five visible columns — Triage / Up Next (<code>todo</code> + <code>ready</code>) / Running / Blocked / Done — collapsing Hermes&#x27;s seven status values into a layout that doesn&#x27;t waste space on <code>ready</code>, which the dispatcher only ever holds for a few seconds. Triage hides itself when empty; archived hides behind a header toggle. Drop a card onto a column and Scarf maps the gesture to the right Hermes verbs through a pure transition planner: drop-on-Running fires <code>kanban dispatch</code> (the dispatcher then spawns a worker), drop-on-Blocked opens a sheet asking for a reason and calls <code>kanban block</code>, drop-on-Done opens a result sheet and calls <code>kanban complete</code>, blocked → running chains <code>unblock</code> + <code>dispatch</code>. Forbidden transitions (anything dropped on Done; anything dragged out of Triage) reject with a red drop-target stroke and a tooltip explaining why — Done is terminal, Triage is promoted by a specifier worker, neither has a CLI verb that maps cleanly. Optimistic local updates apply on drop and revert on CLI failure with a toast, so the UI feels instant.</li>
</ul>
<ul>
<li><strong>Side-pane inspector</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanInspectorPane.swift">KanbanInspectorPane.swift</a>). Click a card and a 420 px pane slides in from the trailing edge. Not a modal sheet — modal would block triaging the next card after closing. Header carries the status, an inline assignee menu (more on that below), workspace kind, and tenant; below that, four tabs render <code>hermes kanban show &lt;id&gt;</code> data: <strong>Comments</strong> (with an inline composer that calls <code>kanban comment</code>), <strong>Events</strong> (the <code>task_events</code> log with per-kind glyphs), <strong>Runs</strong> (one row per attempt with outcome badge + summary + error), and <strong>Log</strong> — the worker&#x27;s captured stdout/stderr from <code>hermes kanban log &lt;id&gt;</code>, polled every 2 s while the task is running with a &quot;● streaming&quot; indicator and auto-scroll to the latest line, snapshot-only with a refresh button when the task is in a terminal state. The action bar at the bottom has all the per-status verbs — Start (which is <code>claim</code> rebranded as a user-visible action), Complete, Block, Unblock, Archive — every one with a help tooltip explaining what it does and what Hermes verb it invokes. The &quot;Archive&quot; tooltip explicitly notes Hermes has no hard-delete: archived tasks remain in <code>~/.hermes/kanban.db</code> and are recoverable via the &quot;Show archived&quot; toggle until <code>hermes kanban gc</code> runs.</li>
</ul>
<ul>
<li><strong>Inspector auto-refresh.</strong> While the inspector is open, the detail (header, action buttons, comments, events, runs) re-fetches every 5 s on the same cadence as the board itself, so a worker transition (e.g. running → done elsewhere) is reflected without the user having to close + reopen. The Log tab&#x27;s 2 s poll runs separately and self-cancels the moment the task transitions out of <code>running</code>.</li>
</ul>
<ul>
<li><strong>Inline assignee picker on the inspector header.</strong> The assignee badge is a clickable menu — set means a <code>.brand</code> (rust) chip, unassigned means a <code>.warning</code> (yellow) chip so the eye catches it instantly. Tapping opens a menu of every known profile (union of <code>~/.hermes/profiles/</code>, current task assignees, and the active local profile from <code>HermesProfileResolver</code>) plus an &quot;Unassigned&quot; option. Selection routes through <code>kanban assign</code> and immediately follows with <code>kanban dispatch</code> so the task gets picked up promptly. Solves the &quot;I assigned a profile but nothing happened&quot; gap end-to-end without the user touching a terminal.</li>
</ul>
<ul>
<li><strong>Health banner in the inspector.</strong> Surfaces two conditions that previously left users staring at a stuck task with no explanation. <strong>Yellow</strong> when the task is unassigned in <code>ready</code> / <code>todo</code>: <em>&quot;Won&#x27;t run automatically — Hermes&#x27;s dispatcher silently skips tasks with no assignee.&quot;</em> The dispatcher&#x27;s own <code>--json</code> output literally lists these under <code>skipped_unassigned</code>; we now surface that to the human. <strong>Red</strong> when the most-recently-completed run ended in a non-success outcome (<code>stale_lock</code> / <code>crashed</code> / <code>gave_up</code> / <code>timed_out</code> / <code>spawn_failed</code> / <code>reclaimed</code> / <code>failed</code>): banner displays the outcome label + the raw <code>error</code> field from the run record, so you don&#x27;t have to dig into the Runs tab to discover it. The red banner is suppressed while a fresh attempt is running — once status flips back to <code>running</code>, the previous outcome is stale signal and the Log tab&#x27;s live stream is the right thing to look at.</li>
</ul>
<ul>
<li><strong>Card-level signals.</strong> Cards in <code>running</code> get a 2 px <code>ScarfColor.info</code> left edge + a subtle title shimmer so live work is obvious at a glance. Blocked cards get a 2 px <code>ScarfColor.warning</code> left edge + a ⚠ glyph next to the title. Done cards dim to 0.7 opacity in light mode, 0.55 in dark, with a green ✓ in the title row. Cards in <code>ready</code> / <code>todo</code> with no assignee get a yellow ⚠ glyph in the title row with a tooltip explaining the dispatcher won&#x27;t pick them up — same signal as the inspector banner, just at the board level so triage is one keypress away.</li>
</ul>
<ul>
<li><strong><code>Board | List</code> toggle at the top of the route.</strong> The v2.6 read-only list view is preserved in <code>KanbanListView.swift</code> and surfaced via a segmented picker, so users on narrow windows or anyone who prefers a flat sortable list can opt in. Choice persists across launches via <code>@AppStorage</code>.</li>
</ul>
<ul>
<li><strong>New Task sheet</strong> (<a href="scarf/scarf/Features/Kanban/Views/KanbanCreateSheet.swift">KanbanCreateSheet.swift</a>). Title, body (markdown supported), assignee (defaults to <code>HermesProfileResolver.activeProfileName()</code> so newly-created tasks actually run), workspace kind (segmented <code>Scratch / Worktree / Project Dir</code>; locked to Project Dir on per-project boards), priority slider, comma-separated skills with autocomplete from <code>~/.hermes/skills/</code>, optional tenant (hidden on per-project boards — the slug is implicit), and a &quot;Send to triage&quot; toggle. Submit fires <code>kanban create --json</code> and immediately follows with <code>kanban dispatch</code> so an assigned task transitions <code>ready</code> → <code>running</code> within seconds rather than waiting for the gateway dispatcher&#x27;s internal cycle.</li>
</ul>
<ul>
<li><strong>Kanban moved from Manage → Monitor in the sidebar.</strong> It&#x27;s runtime work-in-progress, not configuration. Sits between Activity and the rest of Manage so users see &quot;what&#x27;s happening right now&quot; at a glance.</li>
</ul>
<h4>Per-project Kanban</h4>
<ul>
<li><strong><code>DashboardTab.kanban</code> on every project</strong>, capability-gated on <code>HermesCapabilities.hasKanban</code>. Renders a project-scoped <code>KanbanBoardView</code> filtered to the project&#x27;s tenant slug. Workspace defaults in the New Task sheet are pre-pinned to <code>dir:&lt;project.path&gt;</code>. Empty state explains the project doesn&#x27;t have any tasks yet and offers a &quot;New Task&quot; CTA — the empty board IS the discovery surface.</li>
</ul>
<ul>
<li><strong>Tenant minting via <a href="scarf/scarf/Core/Services/KanbanTenantResolver.swift">KanbanTenantResolver</a>.</strong> Each Scarf project gets a stable <code>scarf:&lt;slug&gt;</code> tenant minted on first kanban interaction and persisted to <code>&lt;project&gt;/.scarf/manifest.json</code> (new optional <code>kanbanTenant</code> field on <code>ProjectTemplateManifest</code>). Slug rules: lowercased, hyphenated, ≤ 48 chars, <code>scarf:</code> prefix to avoid collision with hand-typed tenants. Once minted, the tenant is <strong>immutable across rename</strong> — tasks already on the board carry the original slug, so renaming the project doesn&#x27;t orphan them. Bare projects (no manifest) get a sentinel manifest written with <code>id: scarf/&lt;project-id&gt;</code> + <code>version: 0.0.0</code> + just the <code>kanbanTenant</code> set; the <code>ProjectAgentContextService</code> reader recognizes the sentinel and refuses to surface it as a &quot;Template&quot; line in the AGENTS.md block, so the project doesn&#x27;t suddenly start advertising a fake template to the agent.</li>
</ul>
<ul>
<li><strong>Agent-side tenant injection.</strong> <a href="scarf/scarf/Core/Services/ProjectAgentContextService.swift">ProjectAgentContextService.renderBlock</a> emits a &quot;Kanban tenant&quot; line inside the <code>&lt;!-- scarf-project --&gt;</code> markers in <code>&lt;project&gt;/AGENTS.md</code> whenever a tenant exists, instructing the agent to pass <code>--tenant scarf:&lt;slug&gt;</code> on <code>hermes kanban create</code>. <code>ChatViewModel.startACPSession</code> already calls <code>refresh(for:)</code> before opening every project chat, so the agent reads a fresh tenant on every session start with no extra wiring. Agents are imperfect at flag discipline; a forgotten <code>--tenant</code> lands the task in the global &quot;Untagged&quot; group rather than failing — acceptable v2.7.5 behavior.</li>
</ul>
<ul>
<li><strong><code>kanban_summary</code> dashboard widget</strong> (<a href="scarf/scarf/Features/Projects/Views/Widgets/KanbanSummaryWidgetView.swift">KanbanSummaryWidgetView.swift</a>). New widget kind for project dashboards: shows the top three <code>running</code> / <code>blocked</code> / <code>todo</code> tasks for the project&#x27;s tenant by priority, plus a glance footer (<code>&quot;12 todo · 3 running · 5 blocked&quot;</code>) sourced from <code>kanban stats</code>. Polls every 10 s while the dashboard is foregrounded. Widget vocabulary registered in <a href="tools/widget-schema.json">tools/widget-schema.json</a> and rendered on the catalog site via <a href="site/widgets.js">site/widgets.js</a>; template authors can drop a <code>{ kind: kanban_summary, max_rows: 3 }</code> block into <code>dashboard.json</code>.</li>
</ul>
<h4>iOS / iPadOS</h4>
<ul>
<li><strong>Read-only Kanban tab on <code>ProjectDetailView</code></strong> (<a href="scarf/Scarf%20iOS/Kanban/ScarfGoKanbanView.swift">Scarf iOS/Kanban/ScarfGoKanbanView.swift</a>). Same five-column collapse rendered as a horizontally-paged segmented <code>Picker</code> of single-column lists — HIG-friendly on iPhone where a 5-column grid forces unreadable card widths. Pulls live status, assignee, workspace, skills, priority chips. Tap a card → modal <code>NavigationStack</code> detail sheet (<a href="scarf/Scarf%20iOS/Kanban/ScarfGoKanbanDetailSheet.swift">ScarfGoKanbanDetailSheet.swift</a>) with the same Comments / Events / Runs tabs the Mac inspector has. Read-only in v2.7.5 — mutations + drag-drop on iPad land in v2.8 once the Mac flow is fully shaken out. Card titles use semantic <code>.headline</code> (not <code>ScarfFont</code>) so Dynamic Type works; chrome (badges) stays on <code>ScarfBadge</code> for fixed visual weight per the project&#x27;s iOS conventions.</li>
</ul>
<h4>ScarfCore</h4>
<ul>
<li><strong><code>KanbanService</code> actor</strong> (<a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift">Packages/ScarfCore/Sources/ScarfCore/Services/KanbanService.swift</a>) — pure-I/O Sendable actor wrapping every Hermes v0.12 verb (<code>list / show / runs / stats / assignees / create / assign / claim / comment / complete / block / unblock / archive / dispatch / link / unlink / log</code>). Dispatches each CLI invocation through <code>Task.detached(priority: .utility)</code> matching the existing concurrency conventions. Errors land in <a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Models/KanbanError.swift">KanbanError</a> and surface as inline banners (not modal alerts) since the board is high-frequency. The &quot;no matching tasks&quot; stdout sentinel is normalized to <code>[]</code> rather than thrown.</li>
</ul>
<ul>
<li><strong>Pure transition planner.</strong> <code>KanbanService.plan(for: KanbanTransition)</code> is a synchronous function that maps a <code>(from, to)</code> column pair to the right verb sequence — <code>(.upNext, .running) → [.dispatch]</code>, <code>(.blocked, .running) → [.unblock, .dispatch]</code>, etc. Disallowed transitions throw <code>KanbanError.forbiddenTransition</code> with a user-actionable reason. The planner is fully tested in <code>KanbanModelsTests.swift</code>. Critically: <code>dispatch</code> (not <code>claim</code>) is the verb used for Up-Next → Running. Hermes&#x27;s <code>claim</code> is documented as &quot;manual alternative to the dispatcher&quot; and assumes the caller spawns the worker themselves — Scarf doesn&#x27;t, so calling <code>claim</code> from drag-drop reserved tasks but never spawned work, and the dispatcher reclaimed them ~15 minutes later (<code>stale_lock</code>). <code>dispatch</code> is the right primitive for a GUI client.</li>
</ul>
<ul>
<li><strong>Cross-platform <a href="scarf/Packages/ScarfCore/Sources/ScarfCore/Services/KanbanTenantReader.swift">KanbanTenantReader</a>.</strong> Read-only projection over <code>&lt;project&gt;/.scarf/manifest.json</code>&#x27;s <code>kanbanTenant</code> field. The full <code>ProjectTemplateManifest</code> type lives in the Mac target; this lightweight reader gives iOS a way to filter the per-project board by tenant without linking the full manifest model.</li>
</ul>
<ul>
<li><strong>Timestamp decoding tolerates both shapes.</strong> Hermes emits <code>created_at</code> / <code>started_at</code> / <code>completed_at</code> / <code>last_heartbeat_at</code> etc. as Unix integer seconds (its SQLite columns are INTEGER), but earlier wire docs implied ISO-8601 strings. The decoder now accepts either an integer or a string and normalizes to ISO-8601 so downstream code only handles one type. Locked in by <code>decodeUnixIntegerTimestamps</code> in <code>KanbanModelsTests</code>.</li>
</ul>
<ul>
<li><strong><code>KanbanBoardViewModel</code> optimistic merge.</strong> Holds <code>optimisticOverrides: [taskId: status]</code> for in-flight drags; the polled response merges with optimistic state until the server confirms the new status, so a stale poll arriving milliseconds after a drop can&#x27;t snap the card back to its old column. On CLI failure the override is removed and the message lands in the inline banner.</li>
</ul>
<h3>Dispatch + assignee fixes</h3>
<p>A diagnostic round driving real tasks end-to-end exposed a connected bug pattern that the polish pass closed:</p>
<ul>
<li><strong>Hermes&#x27;s dispatcher silently skips unassigned tasks</strong> — its <code>kanban dispatch --json</code> output literally lists them under a <code>skipped_unassigned</code> key and moves on. Tasks created without an assignee sat in <code>ready</code> indefinitely and the user had no signal anything was wrong. The New Task sheet now defaults to the active Hermes profile, the inspector header shows a yellow &quot;Unassigned&quot; chip + warning banner, every <code>ready</code> / <code>todo</code> card without an assignee gets a ⚠ glyph + tooltip, and the inspector&#x27;s inline assignee picker fixes it in one click.</li>
</ul>
<ul>
<li><strong>Drag-to-Running used to call <code>claim</code></strong>, which is a manual alternative to the dispatcher. Status flipped to <code>running</code>, but no worker spawned (Scarf doesn&#x27;t host workers), and 15 minutes later the dispatcher reclaimed the task with a <code>stale_lock</code> outcome. Replaced with <code>dispatch</code> end-to-end so the gateway-running dispatcher actually does the spawning.</li>
</ul>
<ul>
<li><strong><code>hermes kanban assignees</code> empty-state was leaking into the picker.</strong> The CLI prints a literal sentinel <code>(no assignees — create a profile with hermes -p &lt;name&gt; setup)</code> when the table is empty; the parser was tokenizing it on whitespace and offering <code>(no</code> as a profile in the menu. Parser now skips the sentinel, validates each candidate against <code>^[a-zA-Z0-9_-]+$</code>, and falls back cleanly to the active local profile when the table is empty.</li>
</ul>
<ul>
<li><strong><code>spawn_failed</code> from &quot;executable not found on PATH&quot;</strong> — most subtle of the lot. macOS GUI apps inherit a launch-services PATH (<code>/usr/bin:/bin:/usr/sbin:/sbin</code>) that doesn&#x27;t include <code>~/.local/bin</code> (where pipx installs <code>hermes</code>) or <code>/opt/homebrew/bin</code>. Scarf was finding <code>hermes</code> for its own invocation via the absolute-path resolver in <code>HermesPathSet.hermesBinaryCandidates</code>, but when the dispatcher then spawned a worker process, that worker inherited Scarf&#x27;s GUI PATH and couldn&#x27;t find <code>hermes</code> by name — recording an <code>outcome=spawn_failed</code> run with the exact &quot;executable not found on PATH&quot; message. <code>LocalTransport</code> now grows an <code>environmentEnricher</code> static (mirroring <code>SSHTransport.environmentEnricher</code>) wired by <code>scarfApp.swift</code> to the same <code>HermesFileService.enrichedEnvironment()</code> login-shell probe the SSH transport uses. Every local subprocess Scarf spawns now sees the user&#x27;s full PATH and credential env, so a spawned-from-Scarf hermes can spawn its children by name without reaching for absolute paths. Defense-in-depth: <code>subprocessEnvironment(forExecutable:)</code> also unconditionally prepends the executable&#x27;s parent directory to PATH, so the fix works even if the enricher hasn&#x27;t been wired (early startup, tests).</li>
</ul>
<h3>Migrating from 2.7.1</h3>
<p>Sparkle will offer the update automatically. No config migration, no schema changes — <code>~/.hermes/kanban.db</code> is shared across all Hermes clients and Scarf only reads/writes through the documented CLI surface. Existing Scarf projects pick up the new project Kanban tab on first open; the tenant slug is minted lazily on first kanban interaction inside the project, so projects with no kanban activity stay byte-identical until the user opens the tab.</p>
<p>If you have an existing project with a Scarf-managed <code>manifest.json</code>, the new optional <code>kanbanTenant</code> field is added on next mint and lives alongside any template-author config schema without touching it. Templates do not ship <code>kanbanTenant</code> (it&#x27;s user-machine-scoped state); the export pipeline strips it.</p>
<p>If you&#x27;ve been running tasks via the v2.6 read-only list and your Hermes host already runs the gateway dispatcher, your existing kanban tasks should appear on the board automatically — there&#x27;s no migration step. Tasks created without an assignee in v2.6 will now show the yellow &quot;Unassigned&quot; warning until you fix them through the inline picker.</p>
<h3>Known limitations</h3>
<ul>
<li><strong>Within-column reorder is not supported.</strong> Hermes has no <code>update</code> verb and no <code>position</code> column on the tasks table — <code>priority</code> is write-once at create time. Sort order inside each column is <code>priority DESC, created_at DESC</code>, matching the dispatcher&#x27;s actual run order. We considered a client-side ordering sidecar; rejected because the on-screen order would diverge from what runs next, which is worse than no manual order. Will revisit if Hermes ships an <code>update --priority</code> verb.</li>
</ul>
<ul>
<li><strong>No live <code>watch</code> streaming yet.</strong> The board polls every 5 s; the inspector polls detail on the same cadence and the Log tab on a 2 s cadence while running. <code>hermes kanban watch --json</code> event streaming + reconnect-with-backoff lands in v2.8 along with iOS write surfaces.</li>
</ul>
<ul>
<li><strong>No bulk re-tag for legacy NULL-tenant tasks.</strong> Tasks created before this release (assignee or no assignee) appear in the global &quot;Untagged&quot; group on the global board. Hermes has no <code>tenant</code> mutation verb post-create, so retagging would be archive + recreate — too destructive to ship in this release.</li>
</ul>
<h3>Acknowledgements</h3>
<ul>
<li>Driven end-to-end against a fresh local Hermes v0.12.0 install with the gateway dispatcher running. Real bug surface mostly came from doing instead of speculating: the <code>claim</code> vs <code>dispatch</code> distinction, the silent <code>skipped_unassigned</code> behavior, the <code>(no</code> parse leak, the integer-vs-ISO timestamp shape, and the stale &quot;Last run&quot; banner during a fresh attempt all surfaced from driving real tasks and watching what actually happened.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.5/Scarf-v2.7.5-Universal.zip"
sparkle:edSignature="6QLmonqLdavcU3+u7NE3oYL4Iui4wZZ0r9OWM+kj0uJ3tM32C14N35g7kXmADvo50YONIAmxqfkYWo/AIX70AA=="
length="19346988"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.7.1</title>
<sparkle:version>33</sparkle:version>
<sparkle:shortVersionString>2.7.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 07 May 2026 10:51:54 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.1</h2>
<p>A patch release covering three bug reports filed against 2.7.0, plus follow-up cleanups in the same neighborhood. No data migrations, no UI surface changes — drop-in replacement for 2.7.0 on Mac.</p>
<h3>Bug fixes</h3>
<h4>Mac</h4>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/77">#77</a> — Sessions screen renders empty even when Dashboard reports sessions exist.</strong> v2.7.0 folded the Sessions tab&#x27;s two SQL queries (sessions list + previews) into a single batched SSH round-trip for perf. The combined wire payload for any user with ~150+ sessions crossed macOS&#x27;s 1664 KB pipe-buffer threshold; without a concurrent reader draining the pipe, the remote <code>sqlite3 -json</code> blocked, the script never finished, our 30-second timeout fired, and the call returned an empty result. <code>SSHScriptRunner</code> now drains stdout/stderr concurrently with the running process via <code>FileHandle.readabilityHandler</code>, so the kernel pipe never fills. Same fix applied to the local-execution path. New regression test pushes 256 KB of synthetic output through the runner and asserts full delivery — would have wedged pre-fix.</li>
</ul>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/78">#78</a> — Skills &quot;What&#x27;s New&quot; pill contradicts the Updates sub-tab.</strong> The pill at the top of the Skills page was rendering on every sub-tab, including Updates. It counts <strong>local</strong> file deltas since the user last clicked &quot;Mark as seen&quot; (e.g. &quot;18 new&quot; = 18 skills landed on disk that you haven&#x27;t acknowledged), while the Updates body runs <code>hermes skills check</code> to find skills with newer <strong>upstream</strong> versions available — a different concept. Two surfaces using the word &quot;update&quot; for two different things made the screen contradict itself. Two changes: the pill now renders only on the Installed sub-tab (Mac and ScarfGo), and its label says &quot;X <strong>changed</strong> since you last looked&quot; instead of &quot;X updated&quot; so the local-file vocabulary doesn&#x27;t collide with upstream-update vocabulary anywhere on the page.</li>
</ul>
<ul>
<li><strong><a href="https://github.com/awizemann/scarf/issues/79">#79</a> — Skills hub search returns nothing for terms visible in Browse.</strong> With the source picker on &quot;All Sources&quot;, <code>hermes skills search &lt;query&gt;</code> (no <code>--source</code> flag) routes through Hermes&#x27;s centralized index and skips external API sources (skills-sh, github, clawhub, lobehub, well-known) — but Browse still aggregates from those sources, so a skill like <code>honcho</code> would show up in Browse and disappear in search. Same picker, same query, contradictory results. Rather than chase Hermes&#x27;s index gaps, &quot;All Sources&quot; search now means &quot;filter what you can already see&quot;: Scarf caches the most recent Browse payload and runs a client-side substring filter (case-insensitive against name, description, and identifier) against it, instantly. Source-specific searches still shell out to <code>hermes skills search --source &lt;s&gt;</code> for full upstream search semantics. Five new tests cover the filter behavior.</li>
</ul>
<ul>
<li><strong><code>hermesPIDResult()</code> — narrow the Hermes &quot;is it running?&quot; probe to the gateway.</strong> Previously <code>pgrep -f hermes</code>, which matched any process with &quot;hermes&quot; in its argv: chat sessions Scarf itself spawns, <code>hermes -z</code> one-shots, log tails, even the README in an editor. The Dashboard &quot;Hermes is running&quot; badge could read true even when the gateway daemon was down. Tightened to a regex that matches only the gateway shape — <code>python -m hermes_cli.main gateway run …</code> and <code>/path/to/hermes gateway run …</code>. All callers (DashboardViewModel, HealthViewModel, SettingsViewModel, scarfApp, stopHermes) want the gateway PID specifically. Cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a> — thanks to <a href="https://github.com/unixwzrd">@unixwzrd</a> for the diagnosis and regex.</li>
</ul>
<ul>
<li><strong><code>HealthViewModel.stopDashboard()</code> — stop the dashboard by port, not <code>pkill -f</code>.</strong> External-instance fallback used to be <code>pkill -f &quot;hermes dashboard&quot;</code>, broad enough to match shell history, log tails, README readers — anything with the substring in its argv. Now <code>lsof -tiTCP:&lt;port&gt; -sTCP:LISTEN</code> resolves the PID actually bound to the dashboard port and only that one process gets <code>SIGTERM</code>. Trusting the port is correct here: Scarf owns the configured port and the user-visible intent is &quot;stop the thing on this port.&quot; Direction cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a>; the <code>-c hermes</code> filter from the original was dropped because Hermes installs as a Python shebang script and the kernel COMM is <code>python</code>, not <code>hermes</code> — <code>-c hermes</code> would silently miss every standard install.</li>
</ul>
<h3>Documentation + tooling</h3>
<ul>
<li><strong><code>scripts/local-build.sh</code> + <code>BUILDING.md</code> for contributor builds.</strong> New unsigned single-arch Debug build script for contributors without an Apple Developer account. Detects arm64 / x86_64, verifies xcode-select / xcrun / xcodebuild, probes the Metal toolchain (offers an interactive install on TTY, errors cleanly on CI), resolves Swift packages, builds Debug with signing disabled. Optional one-touch <code>ditto</code> to <code>/Applications/scarf.app</code> on explicit y/N. The canonical Release universal CLI in <code>README.md</code> is unchanged — <code>local-build.sh</code> is an alternative for contributors, not a replacement for the shipping build. Cherry-picked from <a href="https://github.com/awizemann/scarf/pull/76">#76</a>.</li>
</ul>
<ul>
<li><strong><code>BUILDING.md</code> + <code>CONTRIBUTING.md</code> — restored Sonoma compatibility messaging.</strong> The runtime min is <strong>macOS 14.6 (Sonoma)</strong> — that&#x27;s the <code>MACOSX_DEPLOYMENT_TARGET</code> on the main <code>scarf</code> target and is intentional. Build min is <strong>Xcode 16.0</strong> (needed for Swift 6 strict-concurrency features). The legacy CONTRIBUTING.md line had drifted to &quot;Xcode 26.3+ / macOS 26.2+&quot;, which would have steered Sonoma contributors and users away from a build that actually runs on their box. Corrected, with a load-bearing-callout in BUILDING.md so future doc edits don&#x27;t silently raise the floor again.</li>
</ul>
<h3>Migrating from 2.7.0</h3>
<p>Sparkle will offer the update automatically. No config migration, no schema changes. Existing sessions, skills, and projects are untouched.</p>
<p>If you&#x27;ve been working around #77 by collapsing the sidebar or restarting Scarf to repopulate the Sessions list, you can stop — sessions should load reliably now.</p>
<h3>Acknowledgements</h3>
<ul>
<li><a href="https://github.com/bricelb">@bricelb</a> for the three v2.7.0 bug reports (<a href="https://github.com/awizemann/scarf/issues/77">#77</a>, <a href="https://github.com/awizemann/scarf/issues/78">#78</a>, <a href="https://github.com/awizemann/scarf/issues/79">#79</a>) — well-instrumented reproductions including screenshots and environment details made the diagnosis straightforward.</li>
<li><a href="https://github.com/unixwzrd">@unixwzrd</a> for <a href="https://github.com/awizemann/scarf/pull/76">#76</a> — the gateway-pgrep tighten, the <code>pkill -f &quot;hermes dashboard&quot;</code> direction, and the <code>local-build.sh</code> contributor flow are all cherry-picked from that PR.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.1/Scarf-v2.7.1-Universal.zip"
sparkle:edSignature="P9whuwJ264TN1XLaUNvzQK+yKmK2fiyccgKa6TixK3mkIPyVl2XrwPhyHgoMFOQ/c14l+4sizWolx67BSwXBAg=="
length="18806277"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.7.0</title>
<sparkle:version>32</sparkle:version>
<sparkle:shortVersionString>2.7.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Tue, 05 May 2026 18:47:18 +0000</pubDate>
<description><![CDATA[
<!DOCTYPE html><html><head><meta charset="utf-8"><style>body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #1d1d1f;
margin: 0;
padding: 0 4px;
}
h2 {
font-size: 17px;
margin: 16px 0 6px 0;
border-bottom: 1px solid #e5e5e7;
padding-bottom: 3px;
}
h3 {
font-size: 14px;
margin: 14px 0 4px 0;
color: #424245;
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 10px 0 2px 0;
}
p { margin: 6px 0; }
ul { margin: 6px 0; padding-left: 20px; }
li { margin: 3px 0; }
code {
background: #f5f5f7;
border-radius: 3px;
padding: 1px 4px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
pre {
background: #f5f5f7;
border-radius: 5px;
padding: 8px 10px;
overflow-x: auto;
font-size: 12px;
}
pre code { background: transparent; padding: 0; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
hr {
border: none;
border-top: 1px solid #e5e5e7;
margin: 16px 0;
}
strong { color: #1d1d1f; }
@media (prefers-color-scheme: dark) {
body { color: #f5f5f7; background: #1c1c1e; }
h2 { border-bottom-color: #38383a; }
h3 { color: #c7c7cc; }
code, pre { background: #2c2c2e; }
hr { border-top-color: #38383a; }
a { color: #4499ff; }
strong { color: #f5f5f7; }
}
</style></head><body>
<h2>What&#x27;s in 2.7.0</h2>
<p>The biggest release since 2.6.0 — a six-week stretch covering <strong>remote-context performance</strong>, a <strong>new project authoring flow</strong>, <strong>dashboard widgets</strong>, <strong>OAuth resilience</strong>, and a top-to-bottom <strong>performance instrumentation harness</strong> that drove the bulk of the rest. 36 commits, no schema bump, no Hermes capability bump.</p>
<p>The throughline: Scarf got materially faster and more honest on slow remote SSH links, where 30-second sqlite timeouts and silently-empty UI used to be common. The skeleton-then-hydrate pattern, SSH cancellation propagation, and ScarfMon-driven diagnosis are the shape of how that work gets done now.</p>
<hr>
<h3>Remote-context performance — chats and Activity in seconds, not 30s timeouts</h3>
<p>Resuming a chat on a slow remote (a 420ms-RTT droplet, an underprovisioned VPS, a tunnel through 4G) used to fetch the full message column set in one shot, which routinely tripped the 30s SSH timeout on chats with multi-page tool result blobs. The 160-message session was broken; the 30-message session was broken too. Activity didn&#x27;t load at all.</p>
<p>v2.7 introduces a <strong>skeleton-then-hydrate pattern</strong> that bounds the wire payload by what the user actually needs to see RIGHT NOW, then fills in the heavy stuff in the background:</p>
<ul>
<li><strong>Chat skeleton.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift"><code>fetchSkeletonMessages</code></a> selects user + assistant rows only (skips <code>role=&#x27;tool&#x27;</code>) with <code>tool_calls</code> / <code>reasoning</code> / <code>reasoning_content</code> hard-NULLed at the SQL level. Wire payload bounded by conversational text alone — typically a few KB. The chat appears in seconds. Background <code>startToolHydration</code> pages through <code>hydrateAssistantToolCalls</code> in 5-id batches to splice tool calls in. Tool-result CONTENT is <strong>opt-in</strong> via Settings → Display → &quot;Load tool results in past chats&quot; (default off); the inspector pane lazy-fetches per-result content via <code>fetchToolResult(callId:)</code> when you open a card.</li>
<li><strong>Activity skeleton.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesDataService.swift"><code>fetchRecentToolCallSkeleton</code></a> returns metadata-only rows (id + session_id + role + timestamp; everything else NULLed). Activity opens in &lt;1s on remote with placeholder rows; real per-call entries swap in as paged hydration completes. New &quot;Loading tool details…&quot; pill in the page header surfaces hydration progress.</li>
<li><strong>Single-id whale recovery.</strong> When a 5-id batch trips the 30s timeout (one row carries an oversized <code>tool_calls</code> blob — a long Edit&#x27;s args, a big diff), an L1 single-id retry isolates the offending row so the rest of the batch still hydrates. Whale row stays bare; assistant message stays readable.</li>
<li><strong>Lazy tool result loading in the inspector.</strong> Default-off avoids the bulk fetch. When you focus a tool call card, ChatInspectorPane fires <code>loadToolResultIfMissing(callId:)</code> which splices a single result into the message stream without re-fetching anything else.</li>
</ul>
<p>Effect: a 160-message thinking-model session that used to time out at exactly 30s now opens in under 2 seconds with placeholder cards filling in over the next few. Activity loads in 500-800ms.</p>
<h4>SSH cancellation that actually cancels</h4>
<p><code>Task.detached { … }</code> doesn&#x27;t inherit cancellation from the awaiting parent, and <code>Task&lt;…&gt; { … }</code> (unstructured) also drops the signal. Without explicit bridging, cancelling a chat-load Task only unwinds Swift state — the underlying ssh subprocess kept running for the full 30s, pinning a remote sqlite query and a ControlMaster session slot. This produced the &quot;third chat hangs&quot; / &quot;dashboard spins after rapid switching&quot; symptom.</p>
<p>v2.7 wires <code>withTaskCancellationHandler</code> through <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Transport/SSHScriptRunner.swift"><code>SSHScriptRunner.run</code></a> and <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/Backends/RemoteSQLiteBackend.swift"><code>RemoteSQLiteBackend.query</code></a> so parent cancellation reaches the <code>Process</code> and calls <code>proc.terminate()</code> within 100ms. New <code>ssh.cancelled</code> ScarfMon event surfaces this.</p>
<h4>In-flight coalescing for <code>loadRecentSessions</code></h4>
<p>File-watcher deltas during an active stream used to stack 2-3 parallel sessions-list reload tasks (the 500ms <code>scheduleSessionsRefresh</code> debounce only suppresses a pending tick, not one already executing). Subsequent callers now await the in-flight load instead of spawning a parallel SSH subprocess. New <code>mac.loadRecentSessions.coalesced</code> event tracks dedup hits.</p>
<h4>Loading-state UX hardening</h4>
<p>The Mac chat sidebar greys out and disables row taps the moment a session-switch is initiated (synchronously, before <code>client.start()</code> returns), with a floating ProgressView showing the current phase: <strong>&quot;Spawning hermes acp…&quot;</strong> → <strong>&quot;Authenticating…&quot;</strong> → <strong>&quot;Loading session…&quot;</strong> → <strong>&quot;Loading history…&quot;</strong> → <strong>&quot;Ready&quot;</strong>. Pre-fix the sidebar looked engageable while the 5-7 second SSH+ACP boot was still in flight, and the user could queue up a second session-switch behind the first. New <code>isStartingSession</code> flag flips on user click for instant feedback.</p>
<h4>Partial-result + mismatch + pinned-model banners</h4>
<ul>
<li><strong>Partial-result banner.</strong> When the skeleton fetch trips an SSH transport failure (rather than a clean empty result), the chat surfaces &quot;Couldn&#x27;t load full chat history — the connection to <em>server</em> timed out&quot; through the existing <code>acpError</code> triplet, plus forces <code>hasMoreHistory = true</code> so the &quot;Load earlier&quot; affordance shows up. Replaces the pre-fix silent empty transcript.</li>
<li><strong>Model/provider mismatch banner.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/ModelPreflight.swift"><code>ModelPreflight.detectMismatch</code></a> recognizes when <code>model.default</code> carries a <code>&lt;provider&gt;/...</code> prefix that disagrees with <code>model.provider</code> (e.g. <code>anthropic/claude-sonnet-4.6</code> + <code>provider: nous</code> after switching OAuth via Credential Pools). Banner offers one-click fix in either direction.</li>
<li><strong>Pinned-model failure hint.</strong> ACP error classifier now recognizes <code>model_not_found</code> / <code>404 messages</code> / <code>model is not available</code> and surfaces &quot;This session was created with a model the provider no longer offers — start a new chat to use your current model&quot; so the pinned-model failure mode has a clear recovery path.</li>
<li><strong>OAuth-completion provider swap.</strong> After a successful OAuth in Credential Pools, if the just-authed provider differs from <code>model.provider</code>, surface &quot;Switch active provider to <em>name</em>?&quot; with [Switch] / [Keep current] instead of auto-dismissing.</li>
</ul>
<hr>
<h3>New Project from Scratch wizard + Keychain-backed cron secrets</h3>
<p>A <strong>third project entry point</strong> alongside Browse Catalog and Add Existing Project: a wizard that scaffolds a Scarf-standard project skeleton (<code>&lt;project&gt;/.scarf/dashboard.json</code> + AGENTS.md marker block), registers it, and hands off to a chat session that auto-activates the bundled <code>scarf-template-author</code> skill. The skill drives the rest conversationally — widgets, optional config schema, optional cron — and writes the final files itself. Wizard stays minimal because the agent does configuration better than a multi-step form. The skill ships bundled inside <code>Scarf.app/Contents/Resources/BuiltinSkills.bundle/</code> and copies into <code>~/.hermes/skills/</code> on launch (idempotent + version-gated).</p>
<p><strong>Cron + Keychain — <code>$SCARF_&lt;SLUG&gt;_&lt;FIELD&gt;</code> env vars.</strong> Cron prompts that referenced <code>secret</code>-typed config fields used to get the literal <code>keychain://...</code> URI back when reading <code>config.json</code>, producing 401s. v2.7 mirrors resolved Keychain values into <code>~/.hermes/.env</code> under a marker-bounded block keyed by template slug:</p>
<pre><code># scarf-secrets:begin local-news-aggregator
SCARF_LOCAL_NEWS_AGGREGATOR_API_TOKEN=actual-value
SCARF_LOCAL_NEWS_AGGREGATOR_RSS_URL=https://example.com/feed
# scarf-secrets:end local-news-aggregator</code></pre>
<p>Hermes already reloads <code>~/.hermes/.env</code> per cron tick, so credential rotation is automatic — just edit the value in Configuration → next tick sees it. The mirror runs at every state-change point: install, post-install Configuration save, uninstall, &quot;Remove from List&quot;, and on app launch (reconciliation pass over registered projects). Source of truth stays in the Keychain — <code>config.json</code> keeps <code>keychain://</code> URIs unchanged. Mode 0600 enforced on <code>~/.hermes/.env</code>.</p>
<p>Cron prompts now reference these env vars directly:</p>
<pre><code>{
&quot;prompt&quot;: &quot;Use the terminal: curl -sS -H \&quot;Authorization: Bearer $SCARF_LOCAL_NEWS_AGGREGATOR_API_TOKEN\&quot; \&quot;$SCARF_LOCAL_NEWS_AGGREGATOR_RSS_URL\&quot; -o {{PROJECT_DIR}}/.scarf/feed.xml&quot;
}</code></pre>
<p><strong>Migration.</strong> First launch of v2.7 walks the project registry and writes the managed block per schemaful project — automatic. Existing cron prompts you wrote against the old (broken) <code>config.json</code> pattern still need updating: open the cron job in Scarf&#x27;s Cron sidebar and edit the prompt, or ask the agent in chat (&quot;Update my Local News cron job&#x27;s prompt to use the new env var convention&quot;) — the bundled <code>scarf-template-author</code> skill (now v1.1.0) documents the convention with worked examples.</p>
<p>Also fixes <a href="https://github.com/awizemann/scarf/issues/75">#75</a> — <code>_NSDetectedLayoutRecursion</code> on the Configuration form for projects whose form transitioned between stages with different intrinsic heights.</p>
<hr>
<h3>Project dashboards — file-reading widgets, sparklines, typed status</h3>
<p>Five new widget types, project-wide auto-refresh, and a structured error card for unknown widgets. Backwards-compatible — every existing <code>dashboard.json</code> renders byte-identically.</p>
<ul>
<li><strong>Project-wide auto-refresh.</strong> <a href="https://github.com/awizemann/scarf/blob/main/scarf/scarf/Core/Services/HermesFileWatcher.swift"><code>HermesFileWatcher</code></a> used to watch each project&#x27;s <code>dashboard.json</code> specifically. v2.7 promotes that to a watch on the entire <code>&lt;project&gt;/.scarf/</code> directory. A <code>markdown_file</code> or <code>log_tail</code> widget pointing at <code>&lt;project&gt;/.scarf/reports/foo.md</code> refreshes the moment a cron job rewrites the file. <strong>By convention, place files the dashboard reads inside <code>.scarf/</code></strong> so the watch picks them up.</li>
<li><strong><code>markdown_file</code></strong> — renders a markdown file from disk through the same <code>MarkdownContentView</code> pipeline used by inline <code>text</code> widgets.</li>
<li><strong><code>log_tail</code></strong> — last <code>lines</code> of a file (default 20, max 200), monospaced, ANSI codes stripped.</li>
<li><strong><code>cron_status</code></strong> — last run / next run / state for one Hermes cron job by <code>jobId</code>, plus a small inline log tail. Read-only — Run/Pause/Resume controls stay on the Cron tab.</li>
<li><strong><code>image</code></strong> — local file (<code>path</code> relative to project root) or remote <code>url</code>. Optional <code>height</code> cap. Useful for matplotlib/Plotly PNGs the cron job generates.</li>
<li><strong><code>status_grid</code></strong> — compact NxM grid of colored cells, one per service / item, with hover labels.</li>
<li><strong><code>stat</code> widget gains inline sparklines.</strong> Optional <code>sparkline: [Number]</code> field. SVG-only render, dozens per dashboard cost nothing.</li>
<li><strong>Typed status badges.</strong> <code>list</code> items and <code>status_grid</code> cells share a typed enum (<code>success</code>, <code>warning</code>, <code>danger</code>, <code>info</code>, <code>pending</code>, <code>done</code>, <code>neutral</code>) with lenient decode for synonyms (<code>ok</code>/<code>up</code> → success, <code>down</code>/<code>error</code> → danger). Unknown strings render as plain text.</li>
<li><strong>Structured widget error card.</strong> Replaces the legacy &quot;Unknown: \&lt;type\&gt;&quot; placeholder with a card surfacing the title, specific reason, and a hint.</li>
<li><strong>Schema mirror.</strong> The widget vocabulary lives once at <a href="https://github.com/awizemann/scarf/blob/main/tools/widget-schema.json"><code>tools/widget-schema.json</code></a>; the catalog validator reads from it and enforces per-type required fields.</li>
</ul>
<hr>
<h3>OAuth resilience + Credential Pools</h3>
<ul>
<li><strong>Daily OAuth keepalive cron.</strong> Prevents Anthropic OAuth refresh tokens from expiring after weeks of inactivity. New cron job <code>[scarf:oauth-keepalive]</code> (managed by Scarf) pings Hermes on a daily cadence; the in-app Refresh All Sessions action mirrors the same path on demand.</li>
<li><strong>Remote re-auth.</strong> Re-authenticating against a remote droplet&#x27;s OAuth provider used to be blocked by the lack of a stdin path through SSHTransport. The OAuth flow now drives a remote <code>hermes auth add</code> correctly with stdin forwarded.</li>
<li><strong>OAuth remove button.</strong> Per-provider remove action in Credential Pools (auth.json edit), with confirmation dialog. Companion auto-refresh of the view when <code>auth.json</code> changes externally (file-watcher).</li>
<li><strong><code>resolve_provider_client</code> error classification.</strong> When an auxiliary task references a provider whose credentials aren&#x27;t loaded, Hermes prints <code>resolve_provider_client: &lt;name&gt; requested but &lt;Display Name&gt; not configured</code> to stderr — pre-fix this surfaced in chat as the opaque <code>-32603 Internal error</code> with no actionable detail. Now classified into a clear hint pointing at Settings → Aux Models.</li>
<li><strong>Aux Tab unknown-task surface.</strong> When <code>config.yaml</code> has an <code>auxiliary.&lt;task&gt;</code> block for a task Scarf doesn&#x27;t know about (newer Hermes added it; Scarf hasn&#x27;t caught up), render it as a plain row with the raw provider/model values instead of dropping it silently.</li>
<li><strong>Credential Pools refresh after OAuth sheet dismiss.</strong> Closing the OAuth sheet after a successful add now refreshes the list immediately instead of leaving the just-added pool hidden until the next file-watcher tick.</li>
</ul>
<hr>
<h3>ScarfMon — performance instrumentation harness</h3>
<p>The diagnostic surface that drove the bulk of the v2.7 perf work. Off by default; signpost-only mode (Instruments-friendly) is free; Full mode (4096-entry in-memory ring buffer + os.Logger) is a click away in Settings → Diagnostics → Performance. Wiki: https://github.com/awizemann/scarf/wiki/Performance-Monitoring</p>
<ul>
<li><strong>Phases 1-3</strong> built the core: dispatcher + ring buffer + 3 backends, chat / transport / sqlite measure points, diagnostic counters for chat-render bursts, finalize-burst dampening.</li>
<li><strong>Tier A + B</strong> added per-feature instrumentation: iOS file watcher, sessions list, model catalog, dashboard widgets, image encoder, message hydration.</li>
<li><strong>Nous picker investigation</strong> localized a 60s + 120s beach-ball to a specific path (Nous catalog <code>readCache</code>), then killed the 120s one with dedupe + 5s timeout.</li>
<li><strong>Tier C catch-up</strong> (this release): instrumented Memory / Skills / Cron / Curator load paths so future captures show how often these tabs cost multiple sequential SFTP RTTs on remote.</li>
<li><strong>Per-call bytes recorded</strong> on transport + sqlite events so captures show payload sizes alongside latencies.</li>
<li><strong><code>mac.emptyAssistantTurn</code> event</strong> documents the Nous quirk where the model returns a thought stream with no body (the bubble looks like Hermes is &quot;still thinking&quot; but the turn already finished).</li>
</ul>
<p>Adding a new measure point is two lines. The harness covers Mac and iOS uniformly. The &quot;Copy as JSON&quot; button exports the ring buffer for paste-into-issue diagnosis.</p>
<hr>
<h3>Other fixes + polish</h3>
<ul>
<li><strong>Sessions sidebar reload debounce</strong> — file-watcher deltas during streaming used to flicker the sessions list. Coalesced into one trailing fetch ~500ms after the last tick.</li>
<li><strong>Session-load pagination + race guard</strong> — switching to a small chat while a larger one is mid-fetch could last-write-wins the small chat away. Three race-checks against <code>self.sessionId</code> prevent the stale fetch from overwriting.</li>
<li><strong>Sessions + previews batched</strong> — two separate SSH calls folded into one <code>queryBatch</code> round trip, halving the round-trips for every sidebar refresh.</li>
<li><strong>Remote SQLite query timeout</strong> bumped 15→30s to better tolerate slow links; in-flight query coalescing dedupes concurrent identical queries.</li>
<li><strong><code>Thread.sleep</code> spin replaced</strong> with a kernel-wait via <code>DispatchGroup</code> for <code>runLocal</code> timeout; under concurrent SSH load the old loop accumulated spin-blocked threads and produced 7-second outliers in <code>loadRecentSessions</code>.</li>
<li><strong>Window position + size</strong> persists across launches.</li>
<li><strong>Sidebar reorder</strong> — Projects promoted to first section; profile chip moved under server name.</li>
<li><strong><code>stop</code> badge suppressed</strong> on metadata footer for normal turn ends (it was firing for every clean completion, looking like an error).</li>
<li><strong>Nous picker search field</strong> + <code>model-picker</code> filter for the long Nous overlay model list.</li>
<li><strong><code>oauth-keepalive</code> cron create</strong> — drop the <code>--silent</code> flag Hermes doesn&#x27;t accept.</li>
<li><strong>Snapshot pipeline rewritten</strong> — replaced the <code>sqlite3 .backup</code>-then-download pipeline with direct SSH-streamed query execution (issue <a href="https://github.com/awizemann/scarf/issues/74">#74</a>). Eliminates the multi-minute snapshot wait on multi-GB state.db files. Companion fix: pre-expand <code>~/</code> in Swift via <code>resolvedUserHome</code> so sqlite3 finds the DB without depending on the remote shell&#x27;s tilde expansion.</li>
<li><strong>Aux nested-YAML parser</strong> — corrected the parser so the unknown-task surface works on remote (was previously dropping aux blocks whose <code>provider:</code> value lived on a separate line).</li>
<li><strong><code>ModelPreflight</code> newline trim bug</strong> — <code>.whitespaces</code> doesn&#x27;t strip newlines; switched both trims to <code>.whitespacesAndNewlines</code> so a stray <code>\n</code> in a hand-edited config.yaml doesn&#x27;t false-positive the mismatch banner.</li>
</ul>
<hr>
<h3>What&#x27;s measured today</h3>
<p>321 ScarfCore tests pass (302 prior + 19 new ModelPreflight). New ScarfMon events documented in the <a href="https://github.com/awizemann/scarf/wiki/Performance-Monitoring">Performance-Monitoring wiki</a>.</p>
<h3>Compatibility</h3>
<ul>
<li>macOS 14+ (unchanged).</li>
<li>Hermes target: still <strong>v2026.4.30 (v0.12.0)</strong>. No new Hermes capability gates added.</li>
<li>Existing <code>dashboard.json</code> files render unchanged.</li>
<li>Existing <code>.scarftemplate</code> bundles install unchanged. Catalog manifest schemaVersion stays at 1/2/3 — no bump.</li>
<li>Existing <code>~/.hermes/.env</code> content is preserved byte-identically — Scarf only writes inside its <code># scarf-secrets:begin &lt;slug&gt;</code> / <code># scarf-secrets:end &lt;slug&gt;</code> regions.</li>
<li>The skeleton-then-hydrate chat loader and SSH cancellation propagation are <strong>Mac-only</strong> in this release; ScarfGo (iOS) keeps its existing chat path.</li>
</ul>
<h3>What&#x27;s deferred</h3>
<ul>
<li><strong>Per-widget data sources + per-widget refresh granularity.</strong> The general &quot;widget points at a typed data source&quot; abstraction is the next-largest win in dashboards but materially expands the model + JS mirror + validator surface. The project-wide watch covers the common cron-driven workflow without it.</li>
<li><strong>Cross-project health digest sidebar rollup.</strong> Counting attention-needed projects across the registry — scoped but didn&#x27;t pull its weight. The typed status enum makes it cheap to add later.</li>
<li><strong>Automatic cron-prompt rewriter on upgrade.</strong> Heuristic rewrites of free-form prompts are risky; the docs + agent-assisted path ships in v2.7. Revisit a &quot;scan + fix&quot; UI in v2.8 if real users miss the migration.</li>
<li><strong>iOS New Project wizard + iOS Keychain-env mirror.</strong> ScarfGo&#x27;s project surface is read-only; the wizard&#x27;s chat-handoff pattern depends on Mac-only ACP plumbing.</li>
<li><strong>iOS skeleton-then-hydrate loaders.</strong> Same data-service surfaces are public, but the iOS chat lifecycle is structured differently. Defer until iOS dogfooding shows the same payload-size pain.</li>
<li><strong>Tier C redesigns (Memory/Skills/Cron/Curator).</strong> Instrumented in v2.7; redesign waits for capture data showing which path actually needs the skeleton-then-hydrate treatment.</li>
</ul>
</body></html>
]]></description>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.7.0/Scarf-v2.7.0-Universal.zip"
sparkle:edSignature="JfHK1sKbP3ubbznXx3uY/a3kY2szkWpTUQmtHJE54Uv970T5PxgIHCTwsimqtZCPepUTv2qK4TRQfqHJBKUmCA=="
length="18793474"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.6.5</title>
<sparkle:version>31</sparkle:version>
<sparkle:shortVersionString>2.6.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sun, 03 May 2026 20:20:29 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.6.5/Scarf-v2.6.5-Universal.zip"
sparkle:edSignature="2hYRNX/z+mQ7TjlHfDAiTHiP/Rk1BoJRjjefPvutooiptwOK6EyYq/9crnHYZYe8xvEQTDMmAvsPz4vYz823Cg=="
length="18257858"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.6.0</title>
<sparkle:version>29</sparkle:version>
<sparkle:shortVersionString>2.6.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 01 May 2026 13:48:15 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.6.0/Scarf-v2.6.0-Universal.zip"
sparkle:edSignature="gI1uwJAvmwdlvngLcsSQFtMDsHyI6+DWN23ZjOX/BYmX+BqKu0XLuAXL1tztZV9RF1l5PG+vNVWGnq6ivqVQCQ=="
length="18031081"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.5.2</title>
<sparkle:version>28</sparkle:version>
<sparkle:shortVersionString>2.5.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Wed, 29 Apr 2026 11:47:40 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.2/Scarf-v2.5.2-Universal.zip"
sparkle:edSignature="tPgE0ajkNs+OYuYGgB8jVLtY/tGTaUJlrJCf5I5AbwCU2R+Zu08D+pLB17q01A3AgwYfYTLZbnoTG+xO4ys8DQ=="
length="17367761"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.5.1</title>
<sparkle:version>27</sparkle:version>
<sparkle:shortVersionString>2.5.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Mon, 27 Apr 2026 13:38:41 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.1/Scarf-v2.5.1-Universal.zip"
sparkle:edSignature="OArax2dY25Q7ZRFYGcviaGmCQCJsIugcBdjTET//mJ4XTT/FnnPQoSTIYaQrsV+mwFZU//G75q9PwPtnNsPiCA=="
length="17090983"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.5.0</title>
<sparkle:version>26</sparkle:version>
<sparkle:shortVersionString>2.5.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 25 Apr 2026 15:42:47 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.5.0/Scarf-v2.5.0-Universal.zip"
sparkle:edSignature="YnHpGMIiL8jyDn3+h8B7Gqzrlz8SXDSyiUXGm9DD6BIkRfYfzi3AVatkxfBLMvoVhlqPGKIhKsB8ybqopgjpCw=="
length="16994785"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.3.0</title>
<sparkle:version>25</sparkle:version>
<sparkle:shortVersionString>2.3.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 24 Apr 2026 01:20:51 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.3.0/Scarf-v2.3.0-Universal.zip"
sparkle:edSignature="vae/hUTU7UOSY/LYU/pt1A9wnbKgvp22+e2peGA/clmloaA22gxCnBX5JALT1w93eHYMLOtvdSf5OrNHyogHDQ=="
length="14783787"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.2.1</title>
<sparkle:version>24</sparkle:version>
<sparkle:shortVersionString>2.2.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 23 Apr 2026 20:10:10 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.2.1/Scarf-v2.2.1-Universal.zip"
sparkle:edSignature="nFdO9t2wWWKeXehQCm7btr7kzCtmDDg4xsvrm4Z24fqOB+Y9ffYcIBr+e9pqnLSIJJ6r/lYcyz5FlkUMjYXyCw=="
length="17868308"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.2.0</title>
<sparkle:version>23</sparkle:version>
<sparkle:shortVersionString>2.2.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Thu, 23 Apr 2026 16:31:53 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.2.0/Scarf-v2.2.0-Universal.zip"
sparkle:edSignature="mGuKLJbcugMTKdSlrkgYKjpVAaXY8CsMVhCiAo/O7b4K8S/fRaK7ZZNdbLtfSGndrbVWcSU0IIhaB1rBGaPOBQ=="
length="17867934"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.1.0</title>
<sparkle:version>22</sparkle:version>
<sparkle:shortVersionString>2.1.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Tue, 21 Apr 2026 01:50:30 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.1.0/Scarf-v2.1.0-Universal.zip"
sparkle:edSignature="kllR3yC/Cze1W9fSM+WRIE5YVObubEGmV629hAvxzVhvVIJ9n+qa00WOAC3YakZLEKX46DmowEMQf5ikqQGODQ=="
length="17243337"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.0.2</title>
<sparkle:version>21</sparkle:version>
<sparkle:shortVersionString>2.0.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Mon, 20 Apr 2026 22:50:02 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.0.2/Scarf-v2.0.2-Universal.zip"
sparkle:edSignature="3BF7PzLqO835wr+cCEA0Ls4kfp2hNwgDmM8YlvUKM9R/GwTs+tgtSPLTwfr1UyXNH/83uNRuSeZM8iNgV4dPDA=="
length="17063811"
type="application/octet-stream" />
</item>
<item>
<title>Version 2.0.0</title>
<sparkle:version>19</sparkle:version>
<sparkle:shortVersionString>2.0.0</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sun, 19 Apr 2026 20:11:43 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v2.0.0/Scarf-v2.0.0-Universal.zip"
sparkle:edSignature="QTgPwFgHk5SbUYKrgg412o/Yc7ZhSiow/33EoWnN+julyEVw/0MwemjHNcwcX+aVQJV4arMp7xGVMzaVV/j1CQ=="
length="16906164"
type="application/octet-stream" />
</item>
<item>
<title>Version 1.6.2</title>
<sparkle:version>18</sparkle:version>
<sparkle:shortVersionString>1.6.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Sat, 18 Apr 2026 00:22:28 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v1.6.2/Scarf-v1.6.2-Universal.zip"
sparkle:edSignature="wtf0QjTKCvYq8BZW1meeWdWk8GMsbYopfM/DNiYw8ImdgmX6X8jN5+bIG9KvzYv0VgZ0la8ssSliiz7zdJA2CQ=="
length="16570465"
type="application/octet-stream" />
</item>
<item>
<title>Version 1.6.1</title>
<sparkle:version>17</sparkle:version>
<sparkle:shortVersionString>1.6.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
<pubDate>Fri, 17 Apr 2026 02:11:53 +0000</pubDate>
<enclosure url="https://github.com/awizemann/scarf/releases/download/v1.6.1/Scarf-v1.6.1-Universal.zip"
sparkle:edSignature="hoYDb7VRQ+YDNUox1kf7eYbhckOJWYLEi8ZPfBZG59qK4L/5N2mmgV7jOCLriHkNx0F4mvM9UK8UQZIkxsX1DA=="
length="16566934"
type="application/octet-stream" />
</item>
</channel>
</rss>

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 391 KiB

Before

Width:  |  Height:  |  Size: 632 KiB

After

Width:  |  Height:  |  Size: 632 KiB

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Before

Width:  |  Height:  |  Size: 429 KiB

After

Width:  |  Height:  |  Size: 429 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 514 KiB

Before

Width:  |  Height:  |  Size: 472 KiB

After

Width:  |  Height:  |  Size: 472 KiB

Before

Width:  |  Height:  |  Size: 750 KiB

After

Width:  |  Height:  |  Size: 750 KiB

Before

Width:  |  Height:  |  Size: 473 KiB

After

Width:  |  Height:  |  Size: 473 KiB

Before

Width:  |  Height:  |  Size: 539 KiB

After

Width:  |  Height:  |  Size: 539 KiB

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Before

Width:  |  Height:  |  Size: 750 KiB

After

Width:  |  Height:  |  Size: 750 KiB

Before

Width:  |  Height:  |  Size: 473 KiB

After

Width:  |  Height:  |  Size: 473 KiB

Before

Width:  |  Height:  |  Size: 412 KiB

After

Width:  |  Height:  |  Size: 412 KiB

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Before

Width:  |  Height:  |  Size: 553 KiB

After

Width:  |  Height:  |  Size: 553 KiB

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Before

Width:  |  Height:  |  Size: 501 KiB

After

Width:  |  Height:  |  Size: 501 KiB

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Before

Width:  |  Height:  |  Size: 790 KiB

After

Width:  |  Height:  |  Size: 790 KiB

Before

Width:  |  Height:  |  Size: 488 KiB

After

Width:  |  Height:  |  Size: 488 KiB

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 577 KiB

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 KiB

Before

Width:  |  Height:  |  Size: 591 KiB

After

Width:  |  Height:  |  Size: 591 KiB

@@ -1,11 +0,0 @@
{
"images" : [
{
"filename" : "Scarf-AppIcon-iOS-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

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

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

-193
View File
@@ -1,193 +0,0 @@
/* Scarf Design System — colors + type tokens. v2 (amber→rust)
*
* Light/dark via [data-theme="dark"] override on a parent. Default light.
*
* v2 changes: brand shifted from purple to a tri-stop amber→rust gradient.
* Neutrals warmed (yellow undertone). Semantic green/blue/red/orange preserved
* — those still mean success/info/danger and remain the tool-kind colors in chat.
*/
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root {
/* ───── Brand — amber → rust ───── */
--brand-50: #FBF1E8;
--brand-100: #F6E0CB;
--brand-200: #EFC59E; /* highlight stop in tri-gradient */
--brand-300: #E89360; /* gradient start */
--brand-400: #D87844;
--brand-500: #C25A2A; /* primary accent — Scarf Rust */
--brand-600: #A6481E;
--brand-700: #7A2E14; /* gradient end */
--brand-800: #5C220F;
--brand-900: #3B1608;
/* ───── Neutrals (warm, slight amber tint) ───── */
--gray-0: #FFFFFF;
--gray-50: #FBF9F6;
--gray-100: #F4F1EC;
--gray-200: #EAE5DD;
--gray-300: #D8D1C5;
--gray-400: #B5ABA0;
--gray-500: #8C857B;
--gray-600: #6A645B;
--gray-700: #4A463F;
--gray-800: #2D2A25;
--gray-900: #1A1814;
--gray-950: #100E0B;
/* ───── Semantic palette ───── */
--green-500: #2AA876;
--green-600: #1F7F5A;
--green-100: #D8F0E5;
--red-500: #D9534F;
--red-600: #B83C38;
--red-100: #F8DAD8;
--orange-500: #F0AD4E; /* reasoning / warning — distinct from brand rust */
--orange-100: #FCEAD0;
--blue-500: #3498DB;
--blue-100: #D8ECF8;
--indigo-500: #5B6CD9;
--purple-tool-500: #8E5BC9;
/* ───── Surfaces (light) ───── */
--fg: var(--gray-900);
--fg-muted: var(--gray-600);
--fg-faint: var(--gray-500);
--bg: var(--gray-50);
--bg-card: var(--gray-0);
--bg-quaternary: rgba(45, 42, 37, 0.04);
--bg-tertiary: rgba(45, 42, 37, 0.07);
--border: rgba(45, 42, 37, 0.08);
--border-strong: rgba(45, 42, 37, 0.14);
/* ───── Brand tokens (semantic) ───── */
--accent: var(--brand-500);
--accent-hover: var(--brand-600);
--accent-active: var(--brand-700);
--accent-tint: rgba(194, 90, 42, 0.10);
--accent-tint-strong: rgba(194, 90, 42, 0.18);
--on-accent: #FFFFFF;
/* ───── Type stacks ───── */
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
--font-display: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", "Segoe UI", sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
/* ───── Type scale ───── */
--text-caption2: 10px;
--text-caption: 12px;
--text-footnote: 13px;
--text-body: 14px;
--text-callout: 15px;
--text-subhead: 16px;
--text-headline: 17px;
--text-title3: 20px;
--text-title2: 22px;
--text-title1: 28px;
--text-largeTitle: 34px;
--leading-tight: 1.2;
--leading-snug: 1.35;
--leading-normal: 1.5;
--leading-relaxed: 1.6;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* ───── Radii / spacing / shadow ───── */
--r-sm: 4px;
--r-md: 6px;
--r-lg: 8px;
--r-xl: 12px;
--r-2xl: 14px;
--r-pill: 999px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--shadow-sm: 0 1px 2px rgba(45, 42, 37, 0.05);
--shadow-md: 0 1px 2px rgba(45, 42, 37, 0.04), 0 4px 12px rgba(45, 42, 37, 0.04);
--shadow-lg: 0 2px 4px rgba(45, 42, 37, 0.06), 0 8px 24px rgba(45, 42, 37, 0.07);
--shadow-xl: 0 4px 8px rgba(45, 42, 37, 0.08), 0 16px 40px rgba(45, 42, 37, 0.10);
--shadow-focus: 0 0 0 3px rgba(194, 90, 42, 0.28);
--gradient-brand: linear-gradient(135deg, #E89360 0%, #C25A2A 50%, #7A2E14 100%);
--gradient-brand-soft: linear-gradient(135deg, #F6E0CB 0%, #EFC59E 100%);
--ease-smooth: cubic-bezier(0.32, 0.72, 0, 1);
--dur-fast: 120ms;
--dur-base: 200ms;
--dur-slow: 300ms;
}
[data-theme="dark"] {
--fg: #EDE8E0;
--fg-muted: #A39C92;
--fg-faint: #756F66;
--bg: #15130F;
--bg-card: #1F1C18;
--bg-quaternary: rgba(255, 248, 235, 0.05);
--bg-tertiary: rgba(255, 248, 235, 0.08);
--border: rgba(255, 248, 235, 0.08);
--border-strong: rgba(255, 248, 235, 0.14);
--accent: #E89360;
--accent-hover: #F0A879;
--accent-active: #D87844;
--accent-tint: rgba(232, 147, 96, 0.14);
--accent-tint-strong: rgba(232, 147, 96, 0.24);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
--shadow-md: 0 1px 2px rgba(0, 0, 0, 0.35), 0 4px 12px rgba(0, 0, 0, 0.35);
--shadow-lg: 0 2px 4px rgba(0, 0, 0, 0.45), 0 8px 24px rgba(0, 0, 0, 0.45);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--fg: #EDE8E0;
--fg-muted: #A39C92;
--fg-faint: #756F66;
--bg: #15130F;
--bg-card: #1F1C18;
--bg-quaternary: rgba(255, 248, 235, 0.05);
--bg-tertiary: rgba(255, 248, 235, 0.08);
--border: rgba(255, 248, 235, 0.08);
--border-strong: rgba(255, 248, 235, 0.14);
--accent: #E89360;
--accent-hover: #F0A879;
--accent-active: #D87844;
--accent-tint: rgba(232, 147, 96, 0.14);
--accent-tint-strong: rgba(232, 147, 96, 0.24);
}
}
/* ───── Semantic type rules ───── */
body, .scarf-body {
font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--leading-normal);
color: var(--fg);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
.scarf-h1 { font-family: var(--font-display); font-size: var(--text-largeTitle); font-weight: 600; line-height: 1.2; letter-spacing: -0.02em; }
.scarf-h2 { font-family: var(--font-display); font-size: var(--text-title1); font-weight: 600; line-height: 1.2; letter-spacing: -0.015em; }
.scarf-h3 { font-family: var(--font-display); font-size: var(--text-title2); font-weight: 600; line-height: 1.35; letter-spacing: -0.01em; }
.scarf-headline { font-family: var(--font-sans); font-size: var(--text-headline); font-weight: 600; line-height: 1.35; }
.scarf-subhead { font-family: var(--font-sans); font-size: var(--text-subhead); font-weight: 500; line-height: 1.35; }
.scarf-body-text { font-family: var(--font-sans); font-size: var(--text-body); line-height: 1.5; }
.scarf-caption { font-family: var(--font-sans); font-size: var(--text-caption); line-height: 1.5; color: var(--fg-muted); }
.scarf-caption-strong { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-muted); }
.scarf-mono { font-family: var(--font-mono); font-size: 0.92em; }
.scarf-code { font-family: var(--font-mono); font-size: 0.9em; background: var(--bg-quaternary); padding: 1px 5px; border-radius: var(--r-sm); color: var(--fg); }
-382
View File
@@ -1,382 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scarf Design System</title>
<link rel="stylesheet" href="colors_and_type.css">
<link rel="icon" type="image/png" href="assets/scarf-app-icon-256.png">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
min-height: 100vh;
background:
radial-gradient(ellipse 100% 60% at 50% -10%, rgba(232, 147, 96, 0.18), transparent 60%),
var(--bg);
color: var(--fg);
font-family: var(--font-sans);
}
.wrap { max-width: 1080px; margin: 0 auto; padding: 80px 32px 120px; }
header { display: flex; align-items: center; gap: 20px; margin-bottom: 56px; }
.icon-tile {
width: 88px; height: 88px;
border-radius: 22px;
background-image: url('assets/scarf-app-icon-256.png');
background-size: cover;
box-shadow: var(--shadow-lg);
}
h1 {
font-family: var(--font-display);
font-size: 44px;
font-weight: 600;
letter-spacing: -0.02em;
margin: 0 0 6px;
line-height: 1.1;
}
.tagline {
font-size: 17px;
color: var(--fg-muted);
line-height: 1.5;
max-width: 56ch;
margin: 0;
}
.section-label {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--fg-faint);
margin: 64px 0 20px;
}
/* Big feature card */
.hero-card {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 0;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 18px;
overflow: hidden;
box-shadow: var(--shadow-md);
margin-bottom: 40px;
}
.hero-card .text {
padding: 36px 36px 32px;
display: flex; flex-direction: column;
justify-content: center;
}
.hero-card .preview {
background: var(--gradient-brand);
position: relative;
min-height: 320px;
display: flex; align-items: center; justify-content: center;
}
.hero-card .preview img {
width: 60%; max-width: 240px;
filter: drop-shadow(0 14px 40px rgba(60, 18, 6, 0.35));
}
.hero-card h2 {
font-family: var(--font-display);
font-size: 28px;
font-weight: 600;
letter-spacing: -0.015em;
margin: 0 0 10px;
}
.hero-card p {
font-size: 15px;
color: var(--fg-muted);
line-height: 1.55;
margin: 0 0 24px;
}
.hero-card .cta {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
background: var(--accent);
color: var(--on-accent);
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
align-self: flex-start;
transition: background 120ms ease;
}
.hero-card .cta:hover { background: var(--accent-hover); }
.hero-card .cta svg { width: 16px; height: 16px; }
/* Token grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.tile {
display: block;
text-decoration: none;
color: inherit;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px 20px;
transition: transform 160ms var(--ease-smooth), border-color 160ms ease, box-shadow 160ms ease;
}
.tile:hover {
transform: translateY(-2px);
border-color: var(--border-strong);
box-shadow: var(--shadow-md);
}
.tile .kicker {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--fg-faint);
margin-bottom: 6px;
}
.tile h3 {
margin: 0 0 4px;
font-size: 16px;
font-weight: 600;
color: var(--fg);
}
.tile p {
margin: 0;
font-size: 13px;
color: var(--fg-muted);
line-height: 1.45;
}
.swatches {
display: flex; gap: 4px; margin-top: 14px;
}
.sw {
flex: 1; height: 22px; border-radius: 4px;
border: 1px solid rgba(0,0,0,0.05);
}
/* Group titles */
.group-title {
font-family: var(--font-display);
font-size: 22px;
font-weight: 600;
letter-spacing: -0.01em;
margin: 0 0 16px;
}
.group-blurb {
font-size: 14px;
color: var(--fg-muted);
margin: 0 0 24px;
line-height: 1.5;
max-width: 60ch;
}
footer {
margin-top: 80px;
padding-top: 28px;
border-top: 1px solid var(--border);
font-size: 13px;
color: var(--fg-faint);
display: flex; justify-content: space-between; align-items: center;
}
footer a { color: var(--fg-muted); text-decoration: none; }
footer a:hover { color: var(--accent); }
@media (max-width: 760px) {
.hero-card { grid-template-columns: 1fr; }
.hero-card .preview { min-height: 200px; order: -1; }
h1 { font-size: 36px; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="icon-tile" role="img" aria-label="Scarf app icon"></div>
<div>
<h1>Scarf Design System</h1>
<p class="tagline">A native macOS &amp; iOS companion for the Hermes AI agent — calm, confident, and rust-warm. This site documents the palette, type, components, and screens.</p>
</div>
</header>
<!-- UI Kit hero -->
<div class="section-label">UI Kit</div>
<a href="ui-kit/index.html" class="hero-card" style="text-decoration: none; color: inherit;">
<div class="text">
<h2>Interactive macOS app</h2>
<p>Click through every screen — Dashboard, Sessions, Insights, Projects, Chat, Settings, Tools, MCP servers, Cron, Logs, Memory, Activity, Health and more. Faithful to the real Scarf macOS app, with a working sidebar and the rust palette throughout.</p>
<span class="cta">
Open the kit
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</span>
</div>
<div class="preview">
<img src="assets/scarf-app-icon-1024.png" alt="">
</div>
</a>
<!-- Tokens & components -->
<div class="section-label">Tokens &amp; components</div>
<h2 class="group-title">Foundations</h2>
<p class="group-blurb">Each tile opens a single design-system card. They're sized for ~700px wide and render one concept at a time.</p>
<div class="grid">
<a class="tile" href="tokens/colors-brand.html">
<div class="kicker">Color</div>
<h3>Brand — amber → rust</h3>
<p>The 9-step rust ramp. Primary accent is <code>#C25A2A</code>.</p>
<div class="swatches">
<div class="sw" style="background:#FBF1E8"></div>
<div class="sw" style="background:#EFC59E"></div>
<div class="sw" style="background:#E89360"></div>
<div class="sw" style="background:#C25A2A"></div>
<div class="sw" style="background:#7A2E14"></div>
<div class="sw" style="background:#3B1608"></div>
</div>
</a>
<a class="tile" href="tokens/colors-neutrals.html">
<div class="kicker">Color</div>
<h3>Warm neutrals</h3>
<p>Slight amber undertone — never cool grey. 11 steps for surfaces and text.</p>
<div class="swatches">
<div class="sw" style="background:#FBF9F6"></div>
<div class="sw" style="background:#EAE5DD"></div>
<div class="sw" style="background:#B5ABA0"></div>
<div class="sw" style="background:#6A645B"></div>
<div class="sw" style="background:#2D2A25"></div>
<div class="sw" style="background:#100E0B"></div>
</div>
</a>
<a class="tile" href="tokens/colors-semantic.html">
<div class="kicker">Color</div>
<h3>Semantic palette</h3>
<p>Success, danger, warning, info — preserved from system conventions.</p>
<div class="swatches">
<div class="sw" style="background:#2AA876"></div>
<div class="sw" style="background:#D9534F"></div>
<div class="sw" style="background:#F0AD4E"></div>
<div class="sw" style="background:#3498DB"></div>
</div>
</a>
<a class="tile" href="tokens/colors-tool-kinds.html">
<div class="kicker">Color</div>
<h3>Tool-kind palette</h3>
<p>Bash, edit, search, web, think — the per-tool decorations in chat.</p>
<div class="swatches">
<div class="sw" style="background:#2AA876"></div>
<div class="sw" style="background:#3498DB"></div>
<div class="sw" style="background:#5B6CD9"></div>
<div class="sw" style="background:#8E5BC9"></div>
<div class="sw" style="background:#F0AD4E"></div>
</div>
</a>
<a class="tile" href="tokens/type-display.html">
<div class="kicker">Type</div>
<h3>Display scale</h3>
<p>Large titles &amp; headlines — SF Pro Display, tight tracking.</p>
</a>
<a class="tile" href="tokens/type-body.html">
<div class="kicker">Type</div>
<h3>Body scale</h3>
<p>14px base, the working text of the app.</p>
</a>
<a class="tile" href="tokens/type-mono.html">
<div class="kicker">Type</div>
<h3>Mono</h3>
<p>SF Mono — for transcripts, paths, command output.</p>
</a>
<a class="tile" href="tokens/spacing-scale.html">
<div class="kicker">Layout</div>
<h3>Spacing scale</h3>
<p>4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 — that's the whole grid.</p>
</a>
<a class="tile" href="tokens/spacing-radii.html">
<div class="kicker">Layout</div>
<h3>Radii</h3>
<p>4 / 6 / 8 / 12 / 14 / pill — tuned for native macOS controls.</p>
</a>
<a class="tile" href="tokens/spacing-shadows.html">
<div class="kicker">Layout</div>
<h3>Shadows</h3>
<p>Four elevation tiers, all on a warm-black tint.</p>
</a>
<a class="tile" href="tokens/iconography.html">
<div class="kicker">Brand</div>
<h3>Iconography</h3>
<p>Lucide icons at 16/18/20/24, 1.6px stroke, currentColor.</p>
</a>
<a class="tile" href="tokens/brand-mark.html">
<div class="kicker">Brand</div>
<h3>App mark</h3>
<p>The flowing-silk icon — preferred backgrounds &amp; minimum sizes.</p>
</a>
</div>
<h2 class="group-title" style="margin-top: 56px;">Components</h2>
<p class="group-blurb">Composable pieces lifted directly from the macOS app's surfaces.</p>
<div class="grid">
<a class="tile" href="tokens/components-buttons.html">
<div class="kicker">Component</div>
<h3>Buttons</h3>
<p>Primary / secondary / ghost / destructive — three sizes each.</p>
</a>
<a class="tile" href="tokens/components-forms.html">
<div class="kicker">Component</div>
<h3>Forms</h3>
<p>Text fields, toggles, selects — with focus &amp; error states.</p>
</a>
<a class="tile" href="tokens/components-sidebar.html">
<div class="kicker">Component</div>
<h3>Sidebar</h3>
<p>Section headers, items, active state, count pills.</p>
</a>
<a class="tile" href="tokens/components-stat-cards.html">
<div class="kicker">Component</div>
<h3>Stat cards</h3>
<p>Number-forward dashboard tiles.</p>
</a>
<a class="tile" href="tokens/components-status-cards.html">
<div class="kicker">Component</div>
<h3>Status cards</h3>
<p>Connection / health / run cards with semantic dots.</p>
</a>
<a class="tile" href="tokens/components-chat-bubbles.html">
<div class="kicker">Component</div>
<h3>Chat bubbles</h3>
<p>User &amp; agent rich messages, avatars, timestamps.</p>
</a>
<a class="tile" href="tokens/components-composer.html">
<div class="kicker">Component</div>
<h3>Composer</h3>
<p>Multiline input with attachments &amp; tool toggles.</p>
</a>
<a class="tile" href="tokens/components-tool-call.html">
<div class="kicker">Component</div>
<h3>Tool-call card</h3>
<p>Inline transcript card showing what the agent did.</p>
</a>
</div>
<footer>
<span>Scarf Design System · v2 (rust)</span>
<span><a href="ui-kit/index.html">UI kit</a> · <a href="tokens/colors-brand.html">First token</a></span>
</footer>
</div>
</body>
</html>
-44
View File
@@ -1,44 +0,0 @@
/* Shared styling for design-system preview cards.
Each card is sized for ~700px wide and renders one focused concept. */
@import url('../colors_and_type.css');
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
}
.card-root {
padding: 20px 24px;
min-height: 110px;
display: flex;
flex-direction: column;
gap: 12px;
}
.row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.col { display: flex; flex-direction: column; gap: 8px; }
.label { font-size: 11px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
.mono { font-family: var(--font-mono); font-size: 11px; color: var(--fg-muted); }
/* swatches */
.swatch {
width: 92px;
height: 64px;
border-radius: 8px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 6px 8px;
position: relative;
overflow: hidden;
}
.swatch .name { font-size: 10px; font-weight: 600; }
.swatch .hex { font-family: var(--font-mono); font-size: 10px; opacity: 0.85; }
.swatch.dark-text { color: var(--gray-900); }
.swatch.light-text { color: #fff; }
-13
View File
@@ -1,13 +0,0 @@
<!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>
@@ -1,17 +0,0 @@
<!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>
@@ -1,21 +0,0 @@
<!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>
@@ -1,19 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Semantic colors</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root">
<div class="label">Semantic · status &amp; feedback</div>
<div class="row">
<div class="swatch light-text" style="background:#2AA876"><div class="name">success</div><div class="hex">#2AA876</div></div>
<div class="swatch light-text" style="background:#D9534F"><div class="name">danger</div><div class="hex">#D9534F</div></div>
<div class="swatch dark-text" style="background:#F0AD4E"><div class="name">warning</div><div class="hex">#F0AD4E</div></div>
<div class="swatch light-text" style="background:#3498DB"><div class="name">info</div><div class="hex">#3498DB</div></div>
</div>
<div class="row" style="gap:8px;margin-top:4px">
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#D8F0E5;color:#1F7F5A;font-weight:600">● Running</span>
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#F8DAD8;color:#B83C38;font-weight:600">● Error</span>
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#FCEAD0;color:#A8741F;font-weight:600">● Reasoning</span>
<span style="font-size:11px;padding:3px 9px;border-radius:999px;background:#D8ECF8;color:#1F70A8;font-weight:600">● Model</span>
</div>
</div>
</body></html>
@@ -1,16 +0,0 @@
<!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>
@@ -1,31 +0,0 @@
<!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>
@@ -1,15 +0,0 @@
<!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>
@@ -1,12 +0,0 @@
<!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>
@@ -1,26 +0,0 @@
<!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>
@@ -1,25 +0,0 @@
<!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>
@@ -1,18 +0,0 @@
<!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>
@@ -1,19 +0,0 @@
<!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>
@@ -1,31 +0,0 @@
<!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>
@@ -1,24 +0,0 @@
<!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>
@@ -1,14 +0,0 @@
<!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>
@@ -1,16 +0,0 @@
<!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>
@@ -1,13 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Shadows</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="background:var(--bg)">
<div class="label">Shadows · two-layer Apple style</div>
<div class="row" style="gap:24px;padding:12px 4px">
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 1px 2px rgba(28,26,32,.05)"></div><div class="mono">sm · subtle lift</div></div>
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 1px 2px rgba(28,26,32,.04),0 4px 12px rgba(28,26,32,.04)"></div><div class="mono">md · cards</div></div>
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 2px 4px rgba(28,26,32,.06),0 8px 24px rgba(28,26,32,.07)"></div><div class="mono">lg · hover</div></div>
<div class="col" style="align-items:center;gap:8px"><div style="width:120px;height:60px;background:var(--bg-card);border-radius:8px;box-shadow:0 4px 8px rgba(28,26,32,.08),0 16px 40px rgba(28,26,32,.10)"></div><div class="mono">xl · sheet</div></div>
</div>
</div>
</body></html>
-11
View File
@@ -1,11 +0,0 @@
<!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>
@@ -1,11 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Type · display</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="gap:14px">
<div class="label">Display · SF Pro Display / Inter</div>
<div style="font-family:var(--font-display);font-size:34px;font-weight:600;letter-spacing:-0.02em;line-height:1.15">Make the complex simple</div>
<div style="font-family:var(--font-display);font-size:28px;font-weight:600;letter-spacing:-0.015em;line-height:1.2">Recent sessions</div>
<div style="font-family:var(--font-display);font-size:22px;font-weight:600;letter-spacing:-0.01em">Activity patterns</div>
<div class="mono">largeTitle 34 / title1 28 / title2 22 — used for view titles only</div>
</div>
</body></html>
-15
View File
@@ -1,15 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Type · mono</title>
<link rel="stylesheet" href="_preview.css"></head>
<body>
<div class="card-root" style="gap:10px">
<div class="label">Mono · SF Mono / JetBrains Mono</div>
<div style="font-family:var(--font-mono);font-size:14px;font-weight:500">claude-haiku-4-5</div>
<div style="font-family:var(--font-mono);font-size:13px;color:var(--fg-muted)">~/.hermes/state.db · 14.2 MB</div>
<div style="font-family:var(--font-mono);font-size:12px">{ "tokens": 2384, "model": "claude-haiku-4-5" }</div>
<div class="row" style="gap:6px">
<span style="font-family:var(--font-mono);font-size:11px;background:var(--bg-quaternary);padding:2px 8px;border-radius:4px">v2.3.0</span>
<span style="font-family:var(--font-mono);font-size:11px;background:var(--bg-quaternary);padding:2px 8px;border-radius:4px">2,847 tokens</span>
<span style="font-family:var(--font-mono);font-size:11px;background:var(--bg-quaternary);padding:2px 8px;border-radius:4px">$0.0421</span>
</div>
</div>
</body></html>
-98
View File
@@ -1,98 +0,0 @@
// Activity chronological feed of everything that happened recently across
// all projects, sessions, cron, and tools. Day-grouped, filterable.
const ACTIVITY_GROUPS = [
{ day: 'Today', items: [
{ time: '09:42', icon: 'message-square', tone: 'accent', title: 'Sera — chat session resumed', sub: 'Forge · 14 turns · refactored CronRunner', proj: 'sera' },
{ time: '09:30', icon: 'clock', tone: 'green', title: 'incident-triage ran', sub: 'cron · ok in 4.2s · 0 issues created', proj: '—' },
{ time: '09:00', icon: 'clock', tone: 'green', title: 'daily-summary ran', sub: 'cron · ok in 36s · posted to #standup', proj: '—' },
{ time: '08:42', icon: 'git-pull-request', tone: 'blue', title: 'PR #284 opened', sub: 'sera · "Switch to AbortController for cron timeouts"', proj: 'sera' },
{ time: '08:14', icon: 'shield', tone: 'amber', title: 'Approval: execute git push origin main', sub: 'sera · approved by Aurora · 3.2s wait', proj: 'sera' },
]},
{ day: 'Yesterday', items: [
{ time: '17:22', icon: 'check-circle', tone: 'green', title: 'release-notes generated', sub: 'cron · ok in 1m 03s · draft saved', proj: '—' },
{ time: '15:08', icon: 'plug', tone: 'accent', title: 'MCP server connected — Figma', sub: '6 tools, 2 prompts available', proj: '—' },
{ time: '14:31', icon: 'message-square', tone: 'accent', title: 'Hermes — onboarding draft', sub: '8 turns · drafted welcome email', proj: 'hermes' },
{ time: '11:02', icon: 'alert-triangle', tone: 'red', title: 'Tool denied — rm -rf node_modules', sub: 'sera · matched deny rule "rm -rf"', proj: 'sera' },
{ time: '09:00', icon: 'clock', tone: 'green', title: 'daily-summary ran', sub: 'cron · ok in 41s', proj: '—' },
]},
{ day: 'Mon, Apr 21', items: [
{ time: '16:48', icon: 'user-plus', tone: 'accent', title: 'New personality — Atlas', sub: 'Created by Aurora · long-form writing model', proj: '—' },
{ time: '14:00', icon: 'database', tone: 'blue', title: 'Postgres (prod, ro) reconfigured', sub: 'switched to read replica', proj: '—' },
{ time: '09:00', icon: 'clock', tone: 'red', title: 'daily-summary failed', sub: 'cron · github 502 bad gateway · retried ok at 09:14', proj: '—' },
]},
];
const ACT_TONES = {
accent: { bg: 'var(--accent-tint)', fg: 'var(--accent)' },
green: { bg: 'var(--green-100)', fg: 'var(--green-600)' },
blue: { bg: 'var(--blue-100)', fg: 'var(--blue-500)' },
amber: { bg: 'var(--orange-100)', fg: 'var(--orange-500)' },
red: { bg: 'var(--red-100)', fg: 'var(--red-500)' },
};
function Activity() {
const [filter, setFilter] = React.useState('all');
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Activity"
subtitle="Everything Scarf has done recently — sessions, cron, tools, MCP, approvals"
actions={<Btn icon="filter">Filter</Btn>}
right={
<Segmented value={filter} onChange={setFilter} size="sm" options={[
{ value: 'all', label: 'All' },
{ value: 'sessions', label: 'Sessions' },
{ value: 'cron', label: 'Cron' },
{ value: 'tools', label: 'Tools' },
]} />
} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
{ACTIVITY_GROUPS.map(g => (
<div key={g.day} style={{ marginBottom: 28 }}>
<div style={{
fontSize: 11, fontWeight: 600, color: 'var(--fg-muted)',
textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8,
padding: '0 4px',
}}>{g.day}</div>
<div style={{
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
borderRadius: 10, overflow: 'hidden',
}}>
{g.items.map((it, i) => <ActivityRow key={i} it={it} last={i === g.items.length - 1} />)}
</div>
</div>
))}
</div>
</div>
);
}
function ActivityRow({ it, last }) {
const tone = ACT_TONES[it.tone];
const [hover, setHover] = React.useState(false);
return (
<div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
background: hover ? 'var(--bg-quaternary)' : 'transparent', cursor: 'pointer',
}}>
<span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--fg-faint)', width: 44 }}>{it.time}</span>
<div style={{
width: 26, height: 26, borderRadius: 6, background: tone.bg, color: tone.fg,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<i data-lucide={it.icon} style={{ width: 14, height: 14 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{it.title}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg-muted)', marginTop: 1 }}>{it.sub}</div>
</div>
{it.proj !== '—' && <Pill size="sm">{it.proj}</Pill>}
<i data-lucide="chevron-right" style={{ width: 14, height: 14, color: 'var(--fg-faint)' }}></i>
</div>
);
}
window.Activity = Activity;
-787
View File
@@ -1,787 +0,0 @@
// Chat three-pane: session list / transcript / inspector.
// Inspector defaults to ToolCall details for the focused tool call; falls
// back to session-level metadata. Transcript supports reasoning, multi-step
// tool calls, file diffs, and a slash-command palette in the composer.
const TOOL_TONES = {
read: { color: 'var(--green-500)', tint: 'var(--green-100)', icon: 'book-open', label: 'Read' },
edit: { color: 'var(--blue-500)', tint: 'var(--blue-100)', icon: 'file-edit', label: 'Edit' },
execute: { color: 'var(--orange-500)', tint: 'var(--orange-100)', icon: 'terminal', label: 'Execute' },
fetch: { color: 'var(--purple-tool-500)', tint: '#EFE0F8', icon: 'globe', label: 'Fetch' },
browser: { color: 'var(--indigo-500)', tint: '#E0E5F8', icon: 'compass', label: 'Browser' },
search: { color: 'var(--accent)', tint: 'var(--accent-tint)',icon: 'search', label: 'Search' },
};
// Top-level Chat
function Chat() {
const [active, setActive] = React.useState('s1');
const [focused, setFocused] = React.useState({ kind: 'tool', id: 'tc-2' }); // inspector subject
const [composerOpen, setComposerOpen] = React.useState(false); // slash menu
React.useEffect(() => {
requestAnimationFrame(() => window.lucide && window.lucide.createIcons());
});
const sessions = [
{ id: 's1', title: 'Cron diagnostics', project: 'scarf', preview: 'The daily-summary job ran 14 minutes ago…', time: '14m', model: 'sonnet-4.5', unread: 0, pinned: true, status: 'live' },
{ id: 's2', title: 'Release notes draft', project: 'hermes-blog', preview: 'Pulled the merged PRs from this week…', time: '42m', model: 'haiku-4.5', unread: 2, status: 'idle' },
{ id: 's3', title: 'PR review summary', project: 'hermes-blog', preview: 'Three PRs are ready for review.', time: '2h', model: 'sonnet-4.5', status: 'idle' },
{ id: 's4', title: 'Function calling models', project: '—', preview: 'Sonnet handles structured tool use…', time: '3h', model: 'haiku-4.5', status: 'idle' },
{ id: 's5', title: 'Memory layout question', project: 'scarf', preview: 'The shared memory keys live at…', time: 'yesterday', model: 'sonnet-4.5', status: 'idle' },
{ id: 's6', title: 'Catalog publish flow', project: 'hermes-blog', preview: 'Walked through the .scarftemplate bundle…', time: 'yesterday', model: 'sonnet-4.5', status: 'idle' },
{ id: 's7', title: 'SSH tunnel debug', project: 'scarf-remote', preview: 'Connection drops after ~90s of idle…', time: 'Mon', model: 'sonnet-4.5', status: 'error' },
];
return (
<div style={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
<ChatList sessions={sessions} active={active} setActive={setActive} />
<Transcript focused={focused} setFocused={setFocused} composerOpen={composerOpen} setComposerOpen={setComposerOpen} />
<Inspector focused={focused} setFocused={setFocused} />
</div>
);
}
// Pane 1 session list
function ChatList({ sessions, active, setActive }) {
const [filter, setFilter] = React.useState('all');
return (
<div style={{
width: 264, borderRight: '0.5px solid var(--border)',
background: 'var(--gray-50)', display: 'flex', flexDirection: 'column'
}}>
<div style={{ padding: '14px 14px 8px', display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ flex: 1, fontFamily: 'var(--font-display)', fontSize: 17, fontWeight: 600 }}>Chats</div>
<IconBtn icon="search" tooltip="Search ⌘F" />
<Btn size="sm" kind="primary" icon="plus">New</Btn>
</div>
<div style={{ padding: '0 12px 8px' }}>
<Segmented value={filter} onChange={setFilter} size="sm" options={[
{ value: 'all', label: 'All', count: sessions.length },
{ value: 'live', label: 'Live', count: 1 },
{ value: 'pinned', label: 'Pinned', count: 1 },
]} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '0 6px 8px' }}>
<SessionGroupHeader>Today</SessionGroupHeader>
{sessions.slice(0, 4).map(s => <SessionRow key={s.id} s={s} active={active === s.id} onClick={() => setActive(s.id)} />)}
<SessionGroupHeader>Earlier</SessionGroupHeader>
{sessions.slice(4).map(s => <SessionRow key={s.id} s={s} active={active === s.id} onClick={() => setActive(s.id)} />)}
</div>
<div style={{ padding: '8px 14px', borderTop: '0.5px solid var(--border)',
display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--fg-muted)' }}>
<i data-lucide="message-square" style={{ width: 12, height: 12 }}></i>
<span>{sessions.length} chats</span>
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 10 }}>1.2 MB · state.db</span>
</div>
</div>
);
}
function SessionGroupHeader({ children }) {
return (
<div style={{
padding: '10px 10px 4px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em',
}}>{children}</div>
);
}
function SessionRow({ s, active, onClick }) {
const [hover, setHover] = React.useState(false);
const statusColor = s.status === 'live' ? 'var(--green-500)' : s.status === 'error' ? 'var(--red-500)' : 'var(--gray-400)';
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
padding: '8px 10px', borderRadius: 7, cursor: 'pointer', marginBottom: 1,
background: active ? 'var(--accent-tint)' : (hover ? 'var(--bg-quaternary)' : 'transparent'),
position: 'relative',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
{s.status === 'live'
? <span style={{ width: 7, height: 7, borderRadius: '50%', background: statusColor,
boxShadow: '0 0 0 2px rgba(42,168,118,0.20)' }}></span>
: <span style={{ width: 6, height: 6, borderRadius: '50%', background: statusColor }}></span>}
{s.pinned && <i data-lucide="pin" style={{ width: 11, height: 11, color: 'var(--accent)' }}></i>}
<div style={{ flex: 1, fontSize: 13, fontWeight: 500,
color: active ? 'var(--accent-active)' : 'var(--fg)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.title}</div>
<div style={{ fontSize: 10, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>{s.time}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4, paddingLeft: 14 }}>
{s.project !== '—' && <span style={{
fontSize: 10, fontWeight: 500, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)',
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
padding: '0 5px', borderRadius: 4,
}}>{s.project}</span>}
<div style={{ flex: 1, fontSize: 11, color: 'var(--fg-muted)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.preview}</div>
{s.unread > 0 && <span style={{
fontSize: 9, fontWeight: 700, fontFamily: 'var(--font-mono)',
padding: '1px 5px', borderRadius: 999, background: 'var(--accent)', color: '#fff', minWidth: 14, textAlign: 'center',
}}>{s.unread}</span>}
</div>
</div>
);
}
// Pane 2 transcript
function Transcript({ focused, setFocused, composerOpen, setComposerOpen }) {
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0,
background: 'var(--bg)' }}>
<TranscriptHeader />
<div style={{ flex: 1, overflowY: 'auto', padding: '20px 28px 8px',
display: 'flex', flexDirection: 'column', gap: 16, scrollBehavior: 'smooth' }}>
<DateMarker>Today · 9:42 AM</DateMarker>
<UserMsg time="9:42 AM">What's the status of the daily-summary cron job? I need to know if it's healthy before I push the new schedule changes.</UserMsg>
<AssistantMsg time="9:42 AM" tokens={284} model="sonnet-4.5" durationMs={2140}>
<Reasoning tokens={127} preview="Check the registry first, then the most recent execution." />
<ToolCall id="tc-1" kind="read" name="read_file" arg="~/.scarf/cron/jobs.json" duration="86 ms" focus={focused} setFocus={setFocused} />
<ToolCall id="tc-2" kind="execute" name="execute" arg='hermes cron status daily-summary' duration="1.4 s" focus={focused} setFocus={setFocused} expanded />
<p style={msgPara}>
The <code style={inlineCode}>daily-summary</code> job ran <strong>14 minutes ago</strong> and completed
successfully in 14.2 s, using 1,847 tokens. Next run is scheduled for tomorrow at 09:00 safe to ship the schedule changes.
</p>
<MsgFooter />
</AssistantMsg>
<UserMsg time="9:43 AM">Show me what it produced.</UserMsg>
<AssistantMsg time="9:43 AM" tokens={612} model="sonnet-4.5" inProgress durationMs={4280}>
<ToolCall id="tc-3" kind="read" name="read_file" arg="~/.scarf/cron/output/daily-summary.md" duration="42 ms" focus={focused} setFocus={setFocused} />
<p style={msgPara}>The latest summary covers <strong>April 24, 2026</strong>. Highlights:</p>
<ul style={{ ...msgPara, paddingLeft: 18, margin: '4px 0' }}>
<li>3 PRs merged across <code style={inlineCode}>hermes</code> and <code style={inlineCode}>scarf</code></li>
<li>2 cron failures auto-recovered (gateway timeouts)</li>
<li>Token spend down 8% week-over-week</li>
</ul>
<ToolCall id="tc-4" kind="edit" name="apply_patch" arg="~/.scarf/cron/jobs.json" duration="120 ms" diff focus={focused} setFocus={setFocused} />
</AssistantMsg>
<SuggestedReplies items={['Schedule a dry run', 'Show last 5 runs', 'Disable daily-summary']} />
</div>
<Composer open={composerOpen} setOpen={setComposerOpen} />
</div>
);
}
function TranscriptHeader() {
return (
<div style={{
padding: '14px 24px', borderBottom: '0.5px solid var(--border)',
display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-card)',
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<i data-lucide="pin" style={{ width: 13, height: 13, color: 'var(--accent)' }}></i>
<div style={{ fontSize: 14, fontWeight: 600 }}>Cron diagnostics</div>
<Pill tone="green" dot size="sm">live</Pill>
</div>
<div style={{ fontSize: 11, color: 'var(--fg-muted)', display: 'flex', gap: 10, marginTop: 3, alignItems: 'center', flexWrap: 'wrap' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<i data-lucide="folder" style={{ width: 11, height: 11, color: 'var(--accent)' }}></i>
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>scarf</span>
</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>claude-sonnet-4.5</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span>14 messages</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>12,847 tok</span>
<span style={{ color: 'var(--fg-faint)' }}>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>$0.0421</span>
</div>
</div>
<Btn size="sm" kind="ghost" icon="git-branch">Branch</Btn>
<Btn size="sm" kind="secondary" icon="share">Share</Btn>
<IconBtn icon="more-horizontal" tooltip="More" />
</div>
);
}
function DateMarker({ children }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, color: 'var(--fg-faint)' }}>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
<span style={{ fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em' }}>{children}</span>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
</div>
);
}
const msgPara = { fontSize: 14, lineHeight: 1.55, color: 'var(--fg)', margin: '6px 0' };
const inlineCode = { fontFamily: 'var(--font-mono)', fontSize: 12.5,
background: 'var(--bg-quaternary)', padding: '1px 5px', borderRadius: 4 };
function UserMsg({ time, children }) {
return (
<div style={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{
maxWidth: '76%', padding: '10px 14px', borderRadius: 14, borderBottomRightRadius: 4,
background: 'var(--accent)', color: 'var(--on-accent)', fontSize: 14, lineHeight: 1.5,
boxShadow: '0 1px 0 rgba(0,0,0,0.06)',
}}>{children}</div>
<div style={{ fontSize: 10, color: 'var(--fg-faint)', marginTop: 4, marginRight: 4,
display: 'flex', gap: 6, alignItems: 'center' }}>
<i data-lucide="check-check" style={{ width: 11, height: 11, color: 'var(--green-500)' }}></i>
<span>{time}</span>
</div>
</div>
);
}
function AssistantMsg({ time, tokens, model, inProgress, durationMs, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', maxWidth: '88%', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, width: '100%' }}>
<div style={{
width: 26, height: 26, borderRadius: 7, marginTop: 2, flexShrink: 0,
background: 'var(--gradient-brand)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff',
boxShadow: '0 1px 2px rgba(122, 46, 20, 0.25)',
}}>
<i data-lucide="sparkles" style={{ width: 14, height: 14 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
background: 'var(--bg-card)', borderRadius: 12,
border: '0.5px solid var(--border)',
padding: '12px 14px', boxShadow: 'var(--shadow-sm)',
}}>{children}</div>
<div style={{ fontSize: 10, color: 'var(--fg-faint)', marginTop: 4, marginLeft: 4,
display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
{inProgress && <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<span style={{
width: 7, height: 7, borderRadius: '50%', background: 'var(--accent)',
animation: 'pulseScarf 1.4s ease-in-out infinite',
}}></span>
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>thinking</span>
</span>}
<span style={{ fontFamily: 'var(--font-mono)' }}>{model}</span>
<span>·</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>{tokens} tok</span>
<span>·</span>
<span>{(durationMs / 1000).toFixed(1)}s</span>
<span>·</span>
<span>{time}</span>
</div>
</div>
</div>
</div>
);
}
function MsgFooter() {
const Btnn = ({ icon, label }) => {
const [hover, setHover] = React.useState(false);
return (
<button onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
padding: '3px 7px', fontSize: 11, color: hover ? 'var(--fg)' : 'var(--fg-muted)',
background: hover ? 'var(--bg-quaternary)' : 'transparent',
border: 'none', borderRadius: 5, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 4, fontFamily: 'var(--font-sans)',
}}>
<i data-lucide={icon} style={{ width: 11, height: 11 }}></i>{label}
</button>
);
};
return (
<div style={{ display: 'flex', gap: 2, marginTop: 6, paddingTop: 6, borderTop: '0.5px solid var(--border)' }}>
<Btnn icon="copy" label="Copy" />
<Btnn icon="thumbs-up" label="" />
<Btnn icon="thumbs-down" label="" />
<Btnn icon="rotate-cw" label="Retry" />
<div style={{ flex: 1 }}></div>
<Btnn icon="pin" label="Pin" />
</div>
);
}
// Reasoning disclosure
function Reasoning({ tokens, preview, children }) {
const [open, setOpen] = React.useState(false);
return (
<div style={{ marginBottom: 8, background: 'var(--orange-100)', borderRadius: 7,
padding: '6px 10px', border: '0.5px solid rgba(240, 173, 78, 0.3)' }}>
<div onClick={() => setOpen(!open)} style={{
cursor: 'pointer', fontSize: 11, fontWeight: 600,
display: 'flex', alignItems: 'center', gap: 5, color: '#A8741F',
}}>
<i data-lucide="brain" style={{ width: 12, height: 12 }}></i>
<span style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Reasoning</span>
<span style={{ color: 'var(--fg-faint)', fontWeight: 500, fontFamily: 'var(--font-mono)' }}>· {tokens} tok</span>
<span style={{ flex: 1 }}></span>
<i data-lucide={open ? 'chevron-down' : 'chevron-right'} style={{ width: 12, height: 12 }}></i>
</div>
{!open && preview && (
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 3,
fontStyle: 'italic', lineHeight: 1.5,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{preview}</div>
)}
{open && (
<div style={{ fontSize: 12.5, color: 'var(--fg-muted)', lineHeight: 1.55,
padding: '6px 0 2px', fontStyle: 'italic' }}>
The user wants the status of a specific cron job named "daily-summary".
I should check the cron registry first, then look at the most recent execution
via <code style={inlineCode}>hermes cron status</code>. If exit_code is 0,
the job is healthy and the schedule push is safe.
</div>
)}
</div>
);
}
// ToolCall card
function ToolCall({ id, kind, name, arg, duration, expanded: initial, diff, focus, setFocus }) {
const [open, setOpen] = React.useState(initial || false);
const t = TOOL_TONES[kind] || TOOL_TONES.read;
const isFocused = focus.kind === 'tool' && focus.id === id;
return (
<div style={{ marginBottom: 5 }}>
<div onClick={() => { setOpen(!open); setFocus({ kind: 'tool', id }); }} style={{
background: isFocused ? t.tint : 'var(--bg-quaternary)',
border: `0.5px solid ${isFocused ? t.color : 'var(--border)'}`,
outline: isFocused ? `1px solid ${t.color}` : 'none', outlineOffset: '-1px',
borderRadius: 7, padding: '6px 10px',
display: 'flex', alignItems: 'center', gap: 9,
fontSize: 12, cursor: 'pointer', transition: 'all 120ms',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<i data-lucide={t.icon} style={{ width: 12, height: 12, color: t.color }}></i>
<span style={{ fontSize: 10, fontWeight: 700, color: t.color,
textTransform: 'uppercase', letterSpacing: '0.04em' }}>{t.label}</span>
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600, color: 'var(--fg)' }}>{name}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--fg-muted)', flex: 1, minWidth: 0,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{arg}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-faint)' }}>{duration}</span>
<i data-lucide="check-circle-2" style={{ width: 13, height: 13, color: 'var(--green-500)' }}></i>
<i data-lucide={open ? 'chevron-down' : 'chevron-right'} style={{ width: 12, height: 12, color: 'var(--fg-faint)' }}></i>
</div>
{open && (
diff
? <DiffPreview />
: <ToolOutput kind={kind} />
)}
</div>
);
}
function ToolOutput({ kind }) {
if (kind === 'execute') {
return (
<div style={{
background: 'var(--gray-900)', color: '#E8E1D2', borderRadius: 7,
padding: '10px 12px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
marginTop: 6, lineHeight: 1.55, overflow: 'auto',
border: '1px solid var(--gray-800)',
}}>
<div><span style={{ color: '#7A7367' }}>$</span> <span style={{ color: '#EFC59E' }}>hermes</span> cron status daily-summary</div>
<div style={{ marginTop: 4 }}>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>last_run</span>: <span>2026-04-25T09:28:14Z</span><br/>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>duration</span>: <span>14.2s</span><br/>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>exit_code</span>: <span>0</span><br/>
<span style={{ color: '#2AA876' }}></span> <span style={{ color: '#A39C92' }}>tokens_used</span>: <span>1,847</span><br/>
<span style={{ color: '#A39C92' }}>next_run</span>: <span>2026-04-26T09:00:00Z</span>
</div>
</div>
);
}
// read
return (
<div style={{
background: 'var(--bg-card)', borderRadius: 7,
padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
marginTop: 6, lineHeight: 1.6, color: 'var(--fg-muted)',
border: '0.5px solid var(--border)', maxHeight: 120, overflow: 'auto',
}}>
<div><span style={{ color: 'var(--fg-faint)' }}>1</span> &#123;</div>
<div><span style={{ color: 'var(--fg-faint)' }}>2</span> "name": "daily-summary",</div>
<div><span style={{ color: 'var(--fg-faint)' }}>3</span> "schedule": "0 9 * * *",</div>
<div><span style={{ color: 'var(--fg-faint)' }}>4</span> "enabled": true</div>
<div><span style={{ color: 'var(--fg-faint)' }}>5</span> &#125;</div>
</div>
);
}
function DiffPreview() {
return (
<div style={{
background: 'var(--bg-card)', borderRadius: 7,
padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
marginTop: 6, lineHeight: 1.6, color: 'var(--fg)',
border: '0.5px solid var(--border)',
}}>
<div><span style={{ color: 'var(--fg-faint)', display: 'inline-block', width: 22 }}>3</span><span> "schedule": "0 9 * * *",</span></div>
<div style={{ background: 'rgba(217, 83, 79, 0.10)' }}>
<span style={{ color: 'var(--red-600)', display: 'inline-block', width: 22 }}>-</span>
<span> "timezone": "UTC",</span>
</div>
<div style={{ background: 'rgba(42, 168, 118, 0.10)' }}>
<span style={{ color: 'var(--green-600)', display: 'inline-block', width: 22 }}>+</span>
<span> "timezone": "America/New_York",</span>
</div>
<div><span style={{ color: 'var(--fg-faint)', display: 'inline-block', width: 22 }}>5</span><span> "enabled": true</span></div>
</div>
);
}
// Suggested replies
function SuggestedReplies({ items }) {
return (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 4, paddingLeft: 36 }}>
{items.map(s => (
<button key={s} style={{
fontSize: 12, padding: '5px 10px', borderRadius: 999,
background: 'var(--bg-card)', border: '0.5px solid var(--border-strong)',
color: 'var(--fg)', fontFamily: 'var(--font-sans)', cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 4,
}}>
<i data-lucide="sparkles" style={{ width: 11, height: 11, color: 'var(--accent)' }}></i>
{s}
</button>
))}
</div>
);
}
// Composer
const SLASH_COMMANDS = [
{ cmd: 'compress', desc: 'Compress conversation context', icon: 'minimize-2' },
{ cmd: 'clear', desc: 'Clear and start fresh', icon: 'trash-2' },
{ cmd: 'model', desc: 'Switch model', icon: 'cpu' },
{ cmd: 'project', desc: 'Change project', icon: 'folder' },
{ cmd: 'memory', desc: 'Edit AGENTS.md', icon: 'database' },
{ cmd: 'cost', desc: 'Show token / cost report', icon: 'circle-dollar-sign' },
];
function Composer({ open, setOpen }) {
const [text, setText] = React.useState('');
const onChange = e => {
const v = e.currentTarget.innerText;
setText(v);
setOpen(v.trim().startsWith('/'));
};
return (
<div style={{
borderTop: '0.5px solid var(--border)', padding: '12px 24px 14px',
background: 'var(--bg-card)', position: 'relative',
}}>
{open && (
<div style={{
position: 'absolute', bottom: 'calc(100% - 4px)', left: 24, right: 24,
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
borderRadius: 9, boxShadow: 'var(--shadow-lg)', padding: 4, maxWidth: 360,
}}>
<div style={{ padding: '4px 8px 6px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
Slash commands
</div>
{SLASH_COMMANDS.map((c, i) => (
<div key={c.cmd} style={{
display: 'flex', alignItems: 'center', gap: 9, padding: '6px 8px',
borderRadius: 6, fontSize: 13, cursor: 'pointer',
background: i === 0 ? 'var(--accent-tint)' : 'transparent',
color: i === 0 ? 'var(--accent-active)' : 'var(--fg)',
}}>
<i data-lucide={c.icon} style={{ width: 14, height: 14 }}></i>
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>/{c.cmd}</span>
<span style={{ flex: 1, color: 'var(--fg-muted)', fontSize: 12 }}>{c.desc}</span>
{i === 0 && <KbdKey></KbdKey>}
</div>
))}
</div>
)}
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
border: `1px solid ${open ? 'var(--accent)' : 'var(--border-strong)'}`,
borderRadius: 12, padding: '10px 12px',
background: 'var(--bg-card)',
boxShadow: open ? 'var(--shadow-focus)' : 'none',
transition: 'box-shadow 120ms, border-color 120ms',
}}>
{/* Attached context chips */}
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<ContextChip icon="folder" label="scarf" tone="accent" />
<ContextChip icon="file-text" label="cron/jobs.json" />
<ContextChip icon="plus" label="Add context" muted />
</div>
{/* Input */}
<div contentEditable suppressContentEditableWarning onInput={onChange}
style={{
fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none',
color: 'var(--fg)', padding: '2px 0', minHeight: 22, maxHeight: 160, overflowY: 'auto',
lineHeight: 1.5,
}}
data-placeholder="Message Hermes… / for commands · @ for files"></div>
{/* Footer row */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<ComposerChip icon="paperclip" label="" />
<ComposerChip icon="at-sign" label="@" />
<ComposerChip icon="image" label="" />
<Divider vertical />
<ComposerChip icon="cpu" label="sonnet-4.5" />
<ComposerChip icon="folder" label="scarf" />
<div style={{ flex: 1 }}></div>
<span style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>
send · newline
</span>
<button style={{
width: 30, height: 30, borderRadius: 8, background: 'var(--accent)',
color: '#fff', border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 1px 2px rgba(122, 46, 20, 0.3)',
}}>
<i data-lucide="arrow-up" style={{ width: 15, height: 15 }}></i>
</button>
</div>
</div>
</div>
);
}
function ContextChip({ icon, label, tone, muted }) {
return (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
padding: '2px 8px', borderRadius: 999, fontSize: 11, fontWeight: 500,
background: tone === 'accent' ? 'var(--accent-tint)' : 'var(--bg-quaternary)',
color: tone === 'accent' ? 'var(--accent-active)' : (muted ? 'var(--fg-muted)' : 'var(--fg)'),
fontFamily: tone === 'accent' ? 'var(--font-sans)' : 'var(--font-mono)',
border: muted ? '0.5px dashed var(--border-strong)' : 'none',
cursor: muted ? 'pointer' : 'default',
}}>
<i data-lucide={icon} style={{ width: 11, height: 11 }}></i>{label}
</div>
);
}
function ComposerChip({ icon, label }) {
const [hover, setHover] = React.useState(false);
return (
<button onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
display: 'inline-flex', alignItems: 'center', gap: 4,
padding: label ? '3px 7px' : '4px', borderRadius: 6, fontSize: 12,
background: hover ? 'var(--bg-quaternary)' : 'transparent',
color: 'var(--fg-muted)', border: 'none', cursor: 'pointer',
fontFamily: 'var(--font-mono)',
}}>
<i data-lucide={icon} style={{ width: 13, height: 13 }}></i>{label}
</button>
);
}
// Pane 3 Inspector
function Inspector({ focused }) {
const [tab, setTab] = React.useState('details');
// Find the focused tool call. For demo, hard-code tc-2 details.
const FOCUS_DATA = {
'tc-1': { kind: 'read', name: 'read_file', arg: '~/.scarf/cron/jobs.json',
duration: '86 ms', startedAt: '09:42:18.214', tokens: 412 },
'tc-2': { kind: 'execute', name: 'execute', arg: 'hermes cron status daily-summary',
duration: '1.4 s', startedAt: '09:42:18.302', tokens: 86,
cwd: '~/.scarf', exitCode: 0 },
'tc-3': { kind: 'read', name: 'read_file', arg: '~/.scarf/cron/output/daily-summary.md',
duration: '42 ms', startedAt: '09:43:01.190', tokens: 1284 },
'tc-4': { kind: 'edit', name: 'apply_patch', arg: '~/.scarf/cron/jobs.json',
duration: '120 ms', startedAt: '09:43:03.910', tokens: 88, linesAdded: 1, linesRemoved: 1 },
};
const data = FOCUS_DATA[focused.id] || FOCUS_DATA['tc-2'];
const t = TOOL_TONES[data.kind];
return (
<aside style={{
width: 320, borderLeft: '0.5px solid var(--border)',
background: 'var(--bg-card)', display: 'flex', flexDirection: 'column',
}}>
{/* Header */}
<div style={{ padding: '14px 16px 10px', borderBottom: '0.5px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<div style={{
width: 24, height: 24, borderRadius: 6,
background: t.tint, color: t.color,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide={t.icon} style={{ width: 13, height: 13 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: t.color,
textTransform: 'uppercase', letterSpacing: '0.05em' }}>{t.label} call</div>
<div style={{ fontSize: 13, fontWeight: 600, fontFamily: 'var(--font-mono)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{data.name}</div>
</div>
<IconBtn icon="x" tooltip="Close inspector" />
</div>
<Tabs value={tab} onChange={setTab} options={[
{ value: 'details', label: 'Details', icon: 'info' },
{ value: 'output', label: 'Output', icon: 'terminal' },
{ value: 'raw', label: 'Raw', icon: 'braces' },
]} />
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
{tab === 'details' && <InspectorDetails data={data} t={t} />}
{tab === 'output' && <InspectorOutput data={data} t={t} />}
{tab === 'raw' && <InspectorRaw data={data} />}
</div>
{/* Footer */}
<div style={{ padding: '10px 16px', borderTop: '0.5px solid var(--border)',
display: 'flex', gap: 6 }}>
<Btn size="sm" kind="secondary" icon="rotate-cw" fullWidth>Re-run</Btn>
<Btn size="sm" kind="ghost" icon="copy">Copy</Btn>
</div>
</aside>
);
}
function InspectorDetails({ data, t }) {
return (
<div>
<Section title="Status">
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px',
background: 'var(--green-100)', borderRadius: 7,
border: '0.5px solid rgba(42, 168, 118, 0.25)' }}>
<i data-lucide="check-circle-2" style={{ width: 16, height: 16, color: 'var(--green-600)' }}></i>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--green-600)' }}>Completed</div>
<div style={{ fontSize: 11, color: 'var(--fg-muted)' }}>Exit 0 · No errors</div>
</div>
</div>
</Section>
<div style={{ marginTop: 18 }}>
<Section title="Arguments">
<div style={{
background: 'var(--bg-quaternary)', borderRadius: 7, padding: '8px 10px',
fontFamily: 'var(--font-mono)', fontSize: 11.5, lineHeight: 1.5,
color: 'var(--fg)', wordBreak: 'break-all',
}}>{data.arg}</div>
</Section>
</div>
<div style={{ marginTop: 18 }}>
<Section title="Telemetry">
<KV k="Started" v={data.startedAt} mono />
<KV k="Duration" v={data.duration} mono />
<KV k="Tokens" v={data.tokens.toLocaleString()} mono />
{data.exitCode != null && <KV k="Exit code" v={data.exitCode} mono color="var(--green-600)" />}
{data.cwd && <KV k="CWD" v={data.cwd} mono />}
{data.linesAdded != null && (
<KV k="Diff" v={
<span style={{ fontFamily: 'var(--font-mono)' }}>
<span style={{ color: 'var(--green-600)' }}>+{data.linesAdded}</span>
<span style={{ color: 'var(--fg-faint)' }}> / </span>
<span style={{ color: 'var(--red-600)' }}>{data.linesRemoved}</span>
</span>
} />
)}
</Section>
</div>
<div style={{ marginTop: 18 }}>
<Section title="Permissions" hint="Tool gateway policy applied at run time">
<div style={{
background: 'var(--bg-quaternary)', borderRadius: 7, padding: '10px',
fontSize: 12, color: 'var(--fg-muted)', display: 'flex', flexDirection: 'column', gap: 6,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<i data-lucide="shield-check" style={{ width: 13, height: 13, color: 'var(--green-500)' }}></i>
<span>Allowed by <code style={inlineCode}>scarf-default</code> profile</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<i data-lucide="check" style={{ width: 13, height: 13, color: 'var(--green-500)' }}></i>
<span>No human approval required</span>
</div>
</div>
</Section>
</div>
</div>
);
}
function InspectorOutput({ data, t }) {
return (
<div>
<Section title="stdout" right={<KbdKey>C</KbdKey>}>
<div style={{
background: 'var(--gray-900)', color: '#E8E1D2', borderRadius: 7,
padding: '10px 12px', fontFamily: 'var(--font-mono)', fontSize: 11,
lineHeight: 1.6, overflow: 'auto',
}}>
<div><span style={{ color: '#7A7367' }}>$</span> <span style={{ color: '#EFC59E' }}>hermes</span> cron status daily-summary</div>
<div style={{ marginTop: 6 }}>
<span style={{ color: '#2AA876' }}></span> last_run: 2026-04-25T09:28:14Z<br/>
<span style={{ color: '#2AA876' }}></span> duration: 14.2s<br/>
<span style={{ color: '#2AA876' }}></span> exit_code: 0<br/>
<span style={{ color: '#2AA876' }}></span> tokens_used: 1,847<br/>
next_run: 2026-04-26T09:00:00Z<br/>
schedule: 0 9 * * *<br/>
timezone: America/New_York
</div>
</div>
</Section>
<div style={{ marginTop: 16 }}>
<Section title="stderr">
<div style={{ background: 'var(--bg-quaternary)', borderRadius: 7, padding: '10px',
fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--fg-faint)' }}>
(empty)
</div>
</Section>
</div>
</div>
);
}
function InspectorRaw({ data }) {
return (
<div style={{
background: 'var(--gray-900)', color: '#E8E1D2', borderRadius: 7,
padding: '12px', fontFamily: 'var(--font-mono)', fontSize: 11,
lineHeight: 1.55,
}}>
{`{
"id": "${data.kind === 'execute' ? 'tc-2' : 'tc-x'}",
"type": "tool_use",
"name": "${data.name}",
"input": {
"command": "hermes cron status daily-summary",
"cwd": "~/.scarf"
},
"result": {
"exit_code": 0,
"duration_ms": 1402,
"stdout_bytes": 287
}
}`}
</div>
);
}
function KV({ k, v, mono, color }) {
return (
<div style={{ display: 'flex', alignItems: 'center', padding: '5px 0',
borderBottom: '0.5px solid var(--border)' }}>
<span style={{ fontSize: 12, color: 'var(--fg-muted)', flex: '0 0 90px' }}>{k}</span>
<span style={{
fontSize: 12, color: color || 'var(--fg)',
fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)', flex: 1, textAlign: 'right',
}}>{v}</span>
</div>
);
}
window.Chat = Chat;
-550
View File
@@ -1,550 +0,0 @@
// Scarf v2 shared components calmer density, full state matrices.
// Exports to window: Btn, IconBtn, Pill, Dot, Card, StatCard, Section, ContentHeader,
// Field, TextInput, NumberInput, TextArea, Toggle, Checkbox, Radio, RadioGroup,
// Segmented, Select, SettingsGroup, SettingsRow, Tabs, Menu, MenuItem, Divider,
// EmptyState, KbdKey, HelpIcon, Tooltip, Avatar, ProgressBar, Spinner.
const SF = "var(--font-sans)";
// ContentHeader
function ContentHeader({ title, subtitle, actions, right, breadcrumb }) {
return (
<div style={{
padding: '24px 32px 22px',
borderBottom: '0.5px solid var(--border)',
background: 'var(--bg-card)',
}}>
{breadcrumb && (
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginBottom: 6 }}>{breadcrumb}</div>
)}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 16 }}>
<div style={{ flex: 1 }}>
<div className="scarf-h2" style={{ marginBottom: subtitle ? 6 : 0 }}>{title}</div>
{subtitle && <div style={{ fontSize: 14, color: 'var(--fg-muted)', maxWidth: 600 }}>{subtitle}</div>}
</div>
{right}
{actions && <div style={{ display: 'flex', gap: 8 }}>{actions}</div>}
</div>
</div>
);
}
// Buttons
function Btn({ kind = 'secondary', size = 'md', icon, iconRight, children, onClick, disabled, loading, fullWidth, type = 'button' }) {
const sizes = {
sm: { padding: '5px 11px', fontSize: 12, gap: 5, iconSize: 13 },
md: { padding: '7px 14px', fontSize: 13, gap: 6, iconSize: 14 },
lg: { padding: '10px 18px', fontSize: 14, gap: 7, iconSize: 16 },
};
const kinds = {
primary: { background: 'var(--accent)', color: 'var(--on-accent)', border: '1px solid transparent', shadow: '0 1px 0 rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.18)' },
secondary: { background: 'var(--bg-card)', color: 'var(--fg)', border: '1px solid var(--border-strong)', shadow: 'var(--shadow-sm)' },
ghost: { background: 'transparent', color: 'var(--fg)', border: '1px solid transparent' },
danger: { background: 'var(--bg-card)', color: 'var(--red-600)', border: '1px solid var(--red-500)' },
'danger-solid': { background: 'var(--red-500)', color: '#fff', border: '1px solid transparent' },
accent: { background: 'var(--accent-tint)', color: 'var(--accent-active)', border: '1px solid transparent' },
};
const s = sizes[size];
const k = kinds[kind];
const [hover, setHover] = React.useState(false);
const hoverStyle = !disabled && hover ? {
primary: { background: 'var(--accent-hover)' },
secondary: { background: 'var(--gray-50)', borderColor: 'var(--accent)' },
ghost: { background: 'var(--bg-quaternary)' },
danger: { background: 'var(--red-100)' },
'danger-solid': { background: 'var(--red-600)' },
accent: { background: 'var(--accent-tint-strong)' },
}[kind] : {};
return (
<button type={type} onClick={onClick} disabled={disabled || loading}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
padding: s.padding, fontSize: s.fontSize, gap: s.gap,
...k, ...hoverStyle, boxShadow: k.shadow,
borderRadius: 8, fontFamily: SF, fontWeight: 500,
display: fullWidth ? 'flex' : 'inline-flex', alignItems: 'center', justifyContent: 'center',
cursor: (disabled || loading) ? 'default' : 'pointer',
opacity: disabled ? 0.45 : 1,
width: fullWidth ? '100%' : 'auto',
transition: 'all 120ms var(--ease-smooth)',
whiteSpace: 'nowrap', userSelect: 'none',
}}>
{loading
? <Spinner size={s.iconSize} color={kind === 'primary' ? 'rgba(255,255,255,0.7)' : 'currentColor'} />
: icon && <i data-lucide={icon} style={{ width: s.iconSize, height: s.iconSize }}></i>}
{children}
{iconRight && <i data-lucide={iconRight} style={{ width: s.iconSize, height: s.iconSize, opacity: 0.7 }}></i>}
</button>
);
}
function IconBtn({ icon, onClick, size = 28, tooltip, active, disabled }) {
const [hover, setHover] = React.useState(false);
return (
<button onClick={onClick} disabled={disabled} title={tooltip}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
width: size, height: size, padding: 0, borderRadius: 7,
background: active ? 'var(--accent-tint)' : (hover && !disabled ? 'var(--bg-quaternary)' : 'transparent'),
color: active ? 'var(--accent-active)' : 'var(--fg-muted)',
border: 'none', cursor: disabled ? 'default' : 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
opacity: disabled ? 0.45 : 1, transition: 'background 120ms',
}}>
<i data-lucide={icon} style={{ width: Math.round(size * 0.55), height: Math.round(size * 0.55) }}></i>
</button>
);
}
function Spinner({ size = 14, color = 'currentColor' }) {
return (
<span style={{
display: 'inline-block', width: size, height: size,
border: `2px solid transparent`, borderTopColor: color, borderRightColor: color,
borderRadius: '50%', animation: 'scarfSpin 0.8s linear infinite',
}}></span>
);
}
// Pills / Dots
function Pill({ tone = 'gray', dot, icon, children, size = 'md' }) {
const tones = {
gray: { bg: 'var(--bg-quaternary)', fg: 'var(--fg-muted)', dotc: 'var(--gray-500)' },
green: { bg: 'var(--green-100)', fg: 'var(--green-600)', dotc: 'var(--green-500)' },
red: { bg: 'var(--red-100)', fg: 'var(--red-600)', dotc: 'var(--red-500)' },
orange: { bg: 'var(--orange-100)', fg: '#A8741F', dotc: 'var(--orange-500)' },
blue: { bg: 'var(--blue-100)', fg: '#1F70A8', dotc: 'var(--blue-500)' },
accent: { bg: 'var(--accent-tint)', fg: 'var(--accent-active)', dotc: 'var(--accent)' },
amber: { bg: 'var(--orange-100)', fg: '#A8741F', dotc: 'var(--orange-500)' },
purple: { bg: '#EFE0F8', fg: '#5E4080', dotc: '#7E5BA9' },
idle: { bg: 'var(--bg-quaternary)', fg: 'var(--fg-faint)', dotc: 'var(--gray-400)' },
};
const t = tones[tone];
const sizes = { sm: { p: '2px 7px', f: 10 }, md: { p: '3px 9px', f: 11 }, lg: { p: '4px 11px', f: 12 } };
const sz = sizes[size];
return (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
fontSize: sz.f, fontWeight: 600, padding: sz.p, borderRadius: 999,
background: t.bg, color: t.fg, fontFamily: SF, lineHeight: 1.4,
}}>
{dot && <span style={{ width: 6, height: 6, borderRadius: '50%', background: t.dotc }}></span>}
{icon && <i data-lucide={icon} style={{ width: 11, height: 11 }}></i>}
{children}
</span>
);
}
function Dot({ tone = 'gray', size = 8 }) {
const tones = { gray: 'var(--gray-400)', green: 'var(--green-500)', red: 'var(--red-500)',
orange: 'var(--orange-500)', blue: 'var(--blue-500)', accent: 'var(--accent)' };
return <span style={{ width: size, height: size, borderRadius: '50%',
background: tones[tone], display: 'inline-block', flexShrink: 0 }}></span>;
}
// Cards / Sections
function Card({ children, padding = 18, style = {}, onClick, interactive }) {
return (
<div onClick={onClick} style={{
background: 'var(--bg-card)', borderRadius: 10,
border: '0.5px solid var(--border)',
boxShadow: 'var(--shadow-sm)',
padding, cursor: onClick || interactive ? 'pointer' : 'default',
transition: 'all 160ms var(--ease-smooth)',
...style,
}}>{children}</div>
);
}
function StatCard({ label, value, sub, accent, icon }) {
return (
<Card padding={16} style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11,
color: 'var(--fg-muted)', fontWeight: 600, marginBottom: 8,
textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{icon && <i data-lucide={icon} style={{ width: 12, height: 12 }}></i>}
{label}
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 600,
color: accent || 'var(--fg)', letterSpacing: '-0.01em', lineHeight: 1.1 }}>{value}</div>
{sub && <div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 6 }}>{sub}</div>}
</Card>
);
}
function Section({ title, hint, right, children, gap = 12 }) {
return (
<div>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: gap, gap: 10 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--fg-muted)',
textTransform: 'uppercase', letterSpacing: '0.06em' }}>{title}</div>
{hint && <div style={{ fontSize: 12, color: 'var(--fg-faint)' }}>{hint}</div>}
<div style={{ marginLeft: 'auto' }}>{right}</div>
</div>
{children}
</div>
);
}
function Divider({ vertical, label }) {
if (vertical) return <div style={{ width: 1, alignSelf: 'stretch', background: 'var(--border)' }}></div>;
if (label) return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, color: 'var(--fg-faint)', margin: '8px 0' }}>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
<span style={{ fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em' }}>{label}</span>
<div style={{ flex: 1, height: 1, background: 'var(--border)' }}></div>
</div>
);
return <div style={{ height: 1, background: 'var(--border)', margin: '8px 0' }}></div>;
}
// Form fields
function Field({ label, hint, error, help, children, required, inline }) {
return (
<label style={{ display: 'flex', flexDirection: inline ? 'row' : 'column',
gap: inline ? 12 : 6, fontFamily: SF, alignItems: inline ? 'center' : 'stretch' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5,
minWidth: inline ? 140 : 0 }}>
<span style={{ fontSize: 13, color: 'var(--fg)', fontWeight: 500 }}>{label}</span>
{required && <span style={{ color: 'var(--red-500)', fontSize: 11 }}>*</span>}
{help && <HelpIcon text={help} />}
</div>
<div style={{ flex: inline ? 1 : 'none', display: 'flex', flexDirection: 'column', gap: 4 }}>
{children}
{error
? <span style={{ fontSize: 11, color: 'var(--red-600)', display: 'flex', alignItems: 'center', gap: 4 }}>
<i data-lucide="alert-circle" style={{ width: 11, height: 11 }}></i>{error}
</span>
: hint && <span style={{ fontSize: 11, color: 'var(--fg-faint)' }}>{hint}</span>
}
</div>
</label>
);
}
function HelpIcon({ text }) {
return (
<span title={text} style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 14, height: 14, borderRadius: '50%', background: 'var(--bg-tertiary)',
color: 'var(--fg-muted)', cursor: 'help',
}}>
<i data-lucide="help-circle" style={{ width: 11, height: 11 }}></i>
</span>
);
}
function inputStyle(invalid) {
return {
fontFamily: SF, fontSize: 13, padding: '7px 11px',
border: `1px solid ${invalid ? 'var(--red-500)' : 'var(--border-strong)'}`,
borderRadius: 7, background: 'var(--bg-card)', color: 'var(--fg)',
outline: 'none', transition: 'all 120ms', width: '100%', boxSizing: 'border-box',
};
}
function TextInput({ value, onChange, placeholder, mono, invalid, leftIcon, rightSlot, type = 'text' }) {
const [v, setV] = React.useState(value ?? '');
React.useEffect(() => setV(value ?? ''), [value]);
const ref = React.useRef();
return (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
{leftIcon && <i data-lucide={leftIcon} style={{
position: 'absolute', left: 10, width: 14, height: 14, color: 'var(--fg-faint)', pointerEvents: 'none'
}}></i>}
<input ref={ref} type={type} value={v}
onChange={e => { setV(e.target.value); onChange && onChange(e.target.value); }}
placeholder={placeholder}
style={{ ...inputStyle(invalid),
fontFamily: mono ? 'var(--font-mono)' : SF,
paddingLeft: leftIcon ? 32 : 11,
paddingRight: rightSlot ? 36 : 11,
}}
onFocus={e => { if (!invalid) { e.target.style.borderColor = 'var(--accent)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}}
onBlur={e => { e.target.style.borderColor = invalid ? 'var(--red-500)' : 'var(--border-strong)'; e.target.style.boxShadow = 'none'; }}
/>
{rightSlot && <div style={{ position: 'absolute', right: 6 }}>{rightSlot}</div>}
</div>
);
}
function TextArea({ value, onChange, placeholder, rows = 3, invalid, mono }) {
const [v, setV] = React.useState(value ?? '');
React.useEffect(() => setV(value ?? ''), [value]);
return (
<textarea value={v} rows={rows} placeholder={placeholder}
onChange={e => { setV(e.target.value); onChange && onChange(e.target.value); }}
style={{ ...inputStyle(invalid), resize: 'vertical', lineHeight: 1.45,
fontFamily: mono ? 'var(--font-mono)' : SF }}
onFocus={e => { if (!invalid) { e.target.style.borderColor = 'var(--accent)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}}
onBlur={e => { e.target.style.borderColor = invalid ? 'var(--red-500)' : 'var(--border-strong)'; e.target.style.boxShadow = 'none'; }}
/>
);
}
function Select({ value, onChange, options }) {
const [v, setV] = React.useState(value ?? options?.[0]?.value ?? '');
React.useEffect(() => setV(value ?? ''), [value]);
return (
<div style={{ position: 'relative', display: 'flex' }}>
<select value={v} onChange={e => { setV(e.target.value); onChange && onChange(e.target.value); }}
style={{ ...inputStyle(), appearance: 'none', paddingRight: 30, cursor: 'pointer' }}>
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
<i data-lucide="chevrons-up-down" style={{
position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
width: 13, height: 13, color: 'var(--fg-muted)', pointerEvents: 'none',
}}></i>
</div>
);
}
// Toggle / Checkbox / Radio
function Toggle({ on, onChange, size = 'md', disabled }) {
const sizes = { sm: { w: 28, h: 16, p: 12 }, md: { w: 36, h: 20, p: 16 }, lg: { w: 44, h: 24, p: 20 } };
const s = sizes[size];
return (
<div onClick={() => !disabled && onChange && onChange(!on)} style={{
width: s.w, height: s.h, borderRadius: 999, position: 'relative',
cursor: disabled ? 'default' : 'pointer', flexShrink: 0,
background: on ? 'var(--accent)' : 'var(--gray-300)',
transition: 'background 180ms var(--ease-smooth)',
opacity: disabled ? 0.5 : 1,
}}>
<div style={{
position: 'absolute', top: 2, left: on ? (s.w - s.p - 2) : 2,
width: s.p, height: s.p, borderRadius: '50%', background: '#fff',
boxShadow: '0 1px 3px rgba(0,0,0,0.18), 0 1px 1px rgba(0,0,0,0.06)',
transition: 'left 180ms var(--ease-smooth)',
}}></div>
</div>
);
}
function Checkbox({ checked, onChange, indeterminate, disabled }) {
return (
<div onClick={() => !disabled && onChange && onChange(!checked)} style={{
width: 16, height: 16, borderRadius: 4,
background: checked || indeterminate ? 'var(--accent)' : 'var(--bg-card)',
border: `1px solid ${checked || indeterminate ? 'var(--accent)' : 'var(--border-strong)'}`,
cursor: disabled ? 'default' : 'pointer', flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 120ms', opacity: disabled ? 0.5 : 1,
}}>
{checked && <i data-lucide="check" style={{ width: 12, height: 12, color: '#fff', strokeWidth: 3 }}></i>}
{indeterminate && !checked && <div style={{ width: 8, height: 2, background: '#fff', borderRadius: 1 }}></div>}
</div>
);
}
function Radio({ checked, onChange, disabled }) {
return (
<div onClick={() => !disabled && onChange && onChange(true)} style={{
width: 16, height: 16, borderRadius: '50%',
background: 'var(--bg-card)',
border: `1px solid ${checked ? 'var(--accent)' : 'var(--border-strong)'}`,
cursor: disabled ? 'default' : 'pointer', flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 120ms', opacity: disabled ? 0.5 : 1,
}}>
{checked && <div style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--accent)' }}></div>}
</div>
);
}
// Segmented / Tabs
function Segmented({ value, onChange, options, size = 'md' }) {
const padding = size === 'sm' ? '4px 10px' : '6px 14px';
const fontSize = size === 'sm' ? 12 : 13;
return (
<div style={{
display: 'inline-flex', padding: 2, borderRadius: 8,
background: 'var(--bg-quaternary)', border: '0.5px solid var(--border)',
}}>
{options.map(o => {
const active = value === o.value;
return (
<button key={o.value} onClick={() => onChange && onChange(o.value)} style={{
padding, fontSize, fontWeight: active ? 600 : 500, fontFamily: SF,
background: active ? 'var(--bg-card)' : 'transparent',
color: active ? 'var(--fg)' : 'var(--fg-muted)',
border: 'none', borderRadius: 6, cursor: 'pointer',
boxShadow: active ? 'var(--shadow-sm)' : 'none',
transition: 'all 120ms var(--ease-smooth)', display: 'inline-flex', alignItems: 'center', gap: 5,
}}>
{o.icon && <i data-lucide={o.icon} style={{ width: 12, height: 12 }}></i>}
{o.label}
{o.count != null && <span style={{
fontSize: 10, fontFamily: 'var(--font-mono)',
padding: '1px 6px', borderRadius: 999,
background: active ? 'var(--accent-tint)' : 'var(--bg-tertiary)',
color: active ? 'var(--accent-active)' : 'var(--fg-muted)',
}}>{o.count}</span>}
</button>
);
})}
</div>
);
}
function Tabs({ value, onChange, options }) {
return (
<div style={{ display: 'flex', gap: 2, borderBottom: '0.5px solid var(--border)' }}>
{options.map(o => {
const active = value === o.value;
return (
<button key={o.value} onClick={() => onChange && onChange(o.value)} style={{
padding: '10px 14px', fontSize: 13, fontWeight: 500, fontFamily: SF,
background: 'transparent', border: 'none',
color: active ? 'var(--fg)' : 'var(--fg-muted)',
borderBottom: `2px solid ${active ? 'var(--accent)' : 'transparent'}`,
marginBottom: -1, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6,
transition: 'color 120ms',
}}>
{o.icon && <i data-lucide={o.icon} style={{ width: 13, height: 13 }}></i>}
{o.label}
{o.count != null && <span style={{
fontSize: 10, fontFamily: 'var(--font-mono)',
padding: '1px 6px', borderRadius: 999,
background: 'var(--bg-tertiary)', color: 'var(--fg-muted)',
}}>{o.count}</span>}
</button>
);
})}
</div>
);
}
// Settings groups (card-rows)
function SettingsGroup({ title, description, children }) {
return (
<div style={{ marginBottom: 28 }}>
{title && <div style={{ marginBottom: 10 }}>
<div style={{ fontSize: 13, fontWeight: 600 }}>{title}</div>
{description && <div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{description}</div>}
</div>}
<div style={{
background: 'var(--bg-card)', border: '0.5px solid var(--border)',
borderRadius: 10, overflow: 'hidden',
}}>{children}</div>
</div>
);
}
function SettingsRow({ title, description, control, icon, last }) {
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
{icon && <div style={{
width: 32, height: 32, borderRadius: 7, background: 'var(--accent-tint)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent)', flexShrink: 0,
}}><i data-lucide={icon} style={{ width: 16, height: 16 }}></i></div>}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{title}</div>
{description && <div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{description}</div>}
</div>
<div style={{ flexShrink: 0 }}>{control}</div>
</div>
);
}
// Menu / dropdown
function Menu({ children, anchor = 'bottom-left', style = {} }) {
const positions = {
'bottom-left': { top: '100%', left: 0, marginTop: 4 },
'bottom-right': { top: '100%', right: 0, marginTop: 4 },
'top-left': { bottom: '100%', left: 0, marginBottom: 4 },
};
return (
<div style={{
position: 'absolute', zIndex: 200, ...positions[anchor],
minWidth: 200, padding: 4, background: 'var(--bg-card)',
border: '0.5px solid var(--border)', borderRadius: 9,
boxShadow: 'var(--shadow-lg)', fontFamily: SF, ...style,
}}>
{children}
</div>
);
}
function MenuItem({ icon, label, kbd, onClick, danger, selected, children }) {
const [hover, setHover] = React.useState(false);
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px',
borderRadius: 6, cursor: 'pointer', fontSize: 13,
background: hover ? 'var(--accent-tint)' : 'transparent',
color: danger ? 'var(--red-600)' : (hover ? 'var(--accent-active)' : 'var(--fg)'),
}}>
{icon && <i data-lucide={icon} style={{ width: 14, height: 14 }}></i>}
<span style={{ flex: 1 }}>{label || children}</span>
{selected && <i data-lucide="check" style={{ width: 13, height: 13 }}></i>}
{kbd && <KbdKey>{kbd}</KbdKey>}
</div>
);
}
function KbdKey({ children }) {
return <span style={{
fontFamily: 'var(--font-mono)', fontSize: 10,
padding: '1px 5px', borderRadius: 3,
background: 'var(--bg-quaternary)', border: '0.5px solid var(--border)',
color: 'var(--fg-muted)',
}}>{children}</span>;
}
// Avatar
function Avatar({ initials, size = 28, color = 'var(--accent)' }) {
return (
<div style={{
width: size, height: size, borderRadius: '50%', background: color,
color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
fontSize: Math.round(size * 0.4), fontWeight: 600, flexShrink: 0,
}}>{initials}</div>
);
}
// ProgressBar
function ProgressBar({ value = 0, color = 'var(--accent)', height = 6 }) {
return (
<div style={{ height, background: 'var(--bg-quaternary)', borderRadius: height / 2, overflow: 'hidden' }}>
<div style={{ width: `${Math.min(100, Math.max(0, value))}%`, height: '100%',
background: color, borderRadius: height / 2, transition: 'width 240ms var(--ease-smooth)' }}></div>
</div>
);
}
// Empty
function EmptyState({ icon, title, body, action }) {
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', padding: 80, textAlign: 'center', gap: 12 }}>
<div style={{
width: 64, height: 64, borderRadius: 16, background: 'var(--accent-tint)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--accent)', marginBottom: 4,
}}>
<i data-lucide={icon || 'inbox'} style={{ width: 28, height: 28 }}></i>
</div>
<div style={{ fontSize: 17, fontWeight: 600 }}>{title}</div>
<div style={{ fontSize: 13, color: 'var(--fg-muted)', maxWidth: 380, lineHeight: 1.5 }}>{body}</div>
{action && <div style={{ marginTop: 8 }}>{action}</div>}
</div>
);
}
Object.assign(window, {
ContentHeader, Btn, IconBtn, Spinner, Pill, Dot,
Card, StatCard, Section, Divider,
Field, HelpIcon, TextInput, TextArea, Select,
Toggle, Checkbox, Radio,
Segmented, Tabs,
SettingsGroup, SettingsRow,
Menu, MenuItem, KbdKey,
Avatar, ProgressBar, EmptyState,
});
-165
View File
@@ -1,165 +0,0 @@
// Cron scheduled agent runs, with run history and a calendar heat strip.
const CRON_JOBS = [
{ id: 'daily-summary', name: 'Daily standup summary', schedule: '0 9 * * 1-5', cronText: 'Weekdays at 9:00am', enabled: true,
lastRun: '2h ago', lastStatus: 'ok', avgDuration: '38s', nextRun: 'tomorrow 9:00am',
personality: 'Hermes', desc: 'Read yesterday\'s commits + Linear updates and post a summary to #standup.', runs7d: 5 },
{ id: 'incident-triage', name: 'Incident triage', schedule: '*/15 * * * *', cronText: 'Every 15 minutes', enabled: true,
lastRun: '3m ago', lastStatus: 'ok', avgDuration: '4.2s', nextRun: 'in 12m',
personality: 'Forge', desc: 'Poll Sentry for unresolved high-severity issues and create Linear tickets.', runs7d: 672 },
{ id: 'design-review', name: 'Friday design review prep', schedule: '0 16 * * 4', cronText: 'Thursdays at 4:00pm', enabled: true,
lastRun: 'yesterday', lastStatus: 'ok', avgDuration: '2m 14s', nextRun: 'Thursday 4:00pm',
personality: 'Atlas', desc: 'Collect new Figma frames + recent PRs, draft an agenda for the design review.', runs7d: 1 },
{ id: 'docs-stale', name: 'Find stale docs', schedule: '0 0 * * 0', cronText: 'Sundays at midnight', enabled: false,
lastRun: '8d ago', lastStatus: 'skipped', avgDuration: '47s', nextRun: 'paused',
personality: 'Hermes', desc: 'Scan the docs site for pages not updated in >90 days; open a checklist.', runs7d: 0 },
{ id: 'release-notes', name: 'Draft release notes', schedule: '0 14 * * 5', cronText: 'Fridays at 2:00pm', enabled: true,
lastRun: '6d ago', lastStatus: 'failed', avgDuration: '1m 03s', nextRun: 'Friday 2:00pm',
personality: 'Atlas', desc: 'Walk merged PRs since last tag; group by area; write user-facing release notes.', runs7d: 1 },
];
const RUN_HISTORY = [
{ when: '2h ago', status: 'ok', duration: '36s', ts: '2026-04-25 09:00:14' },
{ when: 'yesterday', status: 'ok', duration: '41s', ts: '2026-04-24 09:00:08' },
{ when: '2d ago', status: 'ok', duration: '38s', ts: '2026-04-23 09:00:11' },
{ when: '3d ago', status: 'ok', duration: '34s', ts: '2026-04-22 09:00:06' },
{ when: '4d ago', status: 'failed', duration: '12s', ts: '2026-04-21 09:00:09', error: 'github: 502 bad gateway' },
{ when: '5d ago', status: 'ok', duration: '40s', ts: '2026-04-18 09:00:12' },
{ when: '6d ago', status: 'ok', duration: '37s', ts: '2026-04-17 09:00:09' },
];
function Cron() {
const [active, setActive] = React.useState('daily-summary');
const job = CRON_JOBS.find(j => j.id === active);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Cron"
subtitle="Scheduled agent runs. Each job invokes a personality with a fixed prompt."
actions={<><Btn icon="calendar">Timezone: PT</Btn><Btn kind="primary" icon="plus">New cron job</Btn></>} />
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
<div style={{ width: 360, borderRight: '0.5px solid var(--border)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-card)' }}>
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
{CRON_JOBS.map(j => <CronRow key={j.id} j={j} active={j.id === active} onClick={() => setActive(j.id)} />)}
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', background: 'var(--bg)', padding: '24px 32px' }}>
<CronDetail job={job} />
</div>
</div>
</div>
);
}
function CronRow({ j, active, onClick }) {
const [hover, setHover] = React.useState(false);
const tone = j.lastStatus === 'failed' ? 'red' : j.lastStatus === 'skipped' ? 'gray' : 'green';
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
padding: '11px 12px', borderRadius: 7, cursor: 'pointer', marginBottom: 2,
background: active ? 'var(--accent-tint)' : (hover ? 'var(--bg-quaternary)' : 'transparent'),
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
<i data-lucide="clock" style={{ width: 13, height: 13, color: 'var(--fg-muted)', flexShrink: 0 }}></i>
<div style={{ flex: 1, fontSize: 13, fontWeight: 500,
color: active ? 'var(--accent-active)' : 'var(--fg)' }}>{j.name}</div>
{!j.enabled && <Pill tone="gray" size="sm">paused</Pill>}
<Dot tone={tone} />
</div>
<div style={{ display: 'flex', gap: 10, fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>
<span>{j.schedule}</span>
<span style={{ color: 'var(--fg-muted)' }}>· next {j.nextRun}</span>
</div>
</div>
);
}
function CronDetail({ job }) {
return (
<>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, marginBottom: 20 }}>
<div style={{
width: 44, height: 44, borderRadius: 9, background: 'var(--accent-tint)', color: 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide="clock" style={{ width: 22, height: 22 }}></i>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div className="scarf-h2" style={{ fontSize: 22 }}>{job.name}</div>
{job.enabled ? <Pill tone="green" dot>active</Pill> : <Pill tone="gray" dot>paused</Pill>}
</div>
<div style={{ fontSize: 13, color: 'var(--fg-muted)', maxWidth: 520 }}>{job.desc}</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<Btn icon="play">Run now</Btn>
<Toggle on={job.enabled} size="lg" />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 24 }}>
<StatCard label="Schedule" value={job.cronText} sub={job.schedule} />
<StatCard label="Last run" value={job.lastRun} sub={job.lastStatus} />
<StatCard label="Avg duration" value={job.avgDuration} />
<StatCard label="Next run" value={job.nextRun} />
</div>
<SettingsGroup title="Schedule">
<SettingsRow icon="calendar" title="Cron expression"
description={`Parsed as: ${job.cronText} (America/Los_Angeles)`}
control={<TextInput value={job.schedule} mono />} />
<SettingsRow icon="globe" title="Timezone"
description="Job triggers fire in this timezone."
control={<Select value="pt" options={[{ value: 'pt', label: 'America/Los_Angeles' }, { value: 'utc', label: 'UTC' }]} />} />
<SettingsRow icon="hourglass" title="Timeout"
description="Kill the run after this duration."
control={<Select value="5m" options={[
{ value: '1m', label: '1 minute' }, { value: '5m', label: '5 minutes' },
{ value: '15m', label: '15 minutes' }, { value: '1h', label: '1 hour' },
]} />} last />
</SettingsGroup>
<SettingsGroup title="Behavior">
<SettingsRow icon="user-circle" title="Personality"
description={`This job runs as "${job.personality}" with its system prompt + tools.`}
control={<Btn size="sm" icon="external-link">{job.personality}</Btn>} />
<SettingsRow icon="message-square" title="Prompt"
description="The instruction sent to the agent at each scheduled run."
control={<Btn size="sm" icon="edit-3">Edit</Btn>} />
<SettingsRow icon="bell" title="Notify on failure"
description="Send a message to #ops if any run errors out."
control={<Toggle on={true} />} last />
</SettingsGroup>
<SettingsGroup title="Run history" description="Last 7 runs.">
{RUN_HISTORY.map((r, i) => <RunRow key={i} r={r} last={i === RUN_HISTORY.length - 1} />)}
</SettingsGroup>
</>
);
}
function RunRow({ r, last }) {
const tone = r.status === 'failed' ? 'red' : r.status === 'skipped' ? 'gray' : 'green';
const icon = r.status === 'failed' ? 'x' : r.status === 'skipped' ? 'minus' : 'check';
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '10px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<Pill tone={tone} size="sm" icon={icon}>{r.status}</Pill>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12.5, color: 'var(--fg)' }}>{r.when}
<span style={{ color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)', marginLeft: 8, fontSize: 11 }}>{r.ts}</span>
</div>
{r.error && <div style={{ fontSize: 11, color: 'var(--red-500)', fontFamily: 'var(--font-mono)', marginTop: 2 }}>{r.error}</div>}
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)', width: 60, textAlign: 'right' }}>{r.duration}</span>
<Btn size="sm">View log</Btn>
</div>
);
}
window.Cron = Cron;
-117
View File
@@ -1,117 +0,0 @@
// Dashboard first screen. Mirrors the structure: status header,
// quick stats, recent sessions, recent activity.
function Dashboard() {
return (
<div style={{ padding: '0 0 28px', overflow: 'auto' }}>
<ContentHeader title="Dashboard"
subtitle="At-a-glance status of your Hermes agent"
actions={<><Btn icon="rotate-cw">Refresh</Btn><Btn kind="primary" icon="plus">New Session</Btn></>} />
<div style={{ padding: '20px 28px', display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* Status row */}
<div style={{ display: 'flex', gap: 12 }}>
<StatusCard icon="activity" label="Hermes" value="Running" tone="green" sub="3h 14m uptime" />
<StatusCard icon="cpu" label="Model" value="claude-sonnet-4.5" sub="Anthropic" />
<StatusCard icon="cloud" label="Provider" value="Anthropic" sub="us-east-1 · 18ms" />
<StatusCard icon="network" label="Gateway" value="Connected" tone="green" sub="3 platforms" />
</div>
{/* Stats row */}
<Section title="Last 7 days" right={<Btn size="sm" kind="ghost" icon="bar-chart-3">View Insights</Btn>}>
<div style={{ display: 'flex', gap: 12 }}>
<StatCard label="Sessions" value="847" sub="+12% vs prev" />
<StatCard label="Messages" value="12,394" />
<StatCard label="Tool Calls" value="3,221" />
<StatCard label="Tokens" value="2.4M" sub="1.8M in · 0.6M out" />
<StatCard label="Cost" value="$42.18" accent="var(--accent)" />
</div>
</Section>
{/* Two col */}
<div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: 16 }}>
<Section title="Recent sessions" right={<a style={linkStyle}>View all </a>}>
<Card padding={0}>
<RecentSessionRow project="hermes-blog" message="Draft this week's release notes…" model="haiku-4.5" tokens="1,247" time="14m ago" />
<RecentSessionRow project="scarf" message="Implement the cron diagnostics view" model="sonnet-4.5" tokens="8,392" time="42m ago" />
<RecentSessionRow project="hermes-blog" message="Review the open PRs and summarize" model="sonnet-4.5" tokens="4,108" time="2h ago" />
<RecentSessionRow project="—" message="What model handles function calls best?" model="haiku-4.5" tokens="284" time="3h ago" last />
</Card>
</Section>
<Section title="Recent activity" right={<a style={linkStyle}>View all </a>}>
<Card padding={0}>
<DashActivityRow icon="file-edit" tone="blue" text="Edited cron/jobs.json" sub="hermes-blog · session #3a2f" time="14m" />
<DashActivityRow icon="terminal" tone="orange" text="Ran hermes status" sub="3 platforms healthy" time="42m" />
<DashActivityRow icon="git-branch" tone="green" text="Cron daily-summary completed" sub="14.2s · 1,847 tokens" time="2h" />
<DashActivityRow icon="package" tone="purple" text="Installed template hermes-blog" sub="from awizemann/hermes-blog" time="yesterday" last />
</Card>
</Section>
</div>
</div>
</div>
);
}
const linkStyle = { fontSize: 12, color: 'var(--accent)', cursor: 'pointer', textDecoration: 'none' };
function StatusCard({ icon, label, value, sub, tone }) {
const dotColor = tone === 'green' ? 'var(--green-500)' : 'var(--gray-400)';
return (
<Card padding={14} style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11,
color: 'var(--fg-muted)', fontWeight: 600, marginBottom: 6 }}>
{tone === 'green'
? <span style={{ width: 7, height: 7, borderRadius: '50%', background: dotColor }}></span>
: <i data-lucide={icon} style={{ width: 12, height: 12 }}></i>
}
<span style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>{label}</span>
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 500,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{value}</div>
{sub && <div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 3 }}>{sub}</div>}
</Card>
);
}
function RecentSessionRow({ project, message, model, tokens, time, last }) {
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
cursor: 'pointer', transition: 'background 120ms',
}} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-quaternary)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<Pill tone="accent">{project}</Pill>
<div style={{ flex: 1, fontSize: 13, color: 'var(--fg)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{message}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)' }}>{model}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 70, textAlign: 'right' }}>{tokens}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', width: 60, textAlign: 'right' }}>{time}</div>
</div>
);
}
function DashActivityRow({ icon, tone, text, sub, time, last }) {
const tones = { green: 'var(--green-500)', blue: 'var(--blue-500)', orange: 'var(--orange-500)', purple: 'var(--accent)' };
return (
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 10, padding: '10px 14px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<div style={{
width: 22, height: 22, borderRadius: 5, background: 'var(--bg-quaternary)',
display: 'flex', alignItems: 'center', justifyContent: 'center', color: tones[tone], flexShrink: 0,
}}>
<i data-lucide={icon} style={{ width: 12, height: 12 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, color: 'var(--fg)' }}>{text}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 1 }}>{sub}</div>
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)' }}>{time}</div>
</div>
);
}
window.Dashboard = Dashboard;
-111
View File
@@ -1,111 +0,0 @@
// Health diagnostics report. One-shot health check across services.
const HEALTH_CHECKS = [
{ name: 'Anthropic API', status: 'ok', latency: '124 ms', detail: 'authenticated as Aurora · sonnet-4.5 reachable' },
{ name: 'Local gateway', status: 'ok', latency: '2 ms', detail: 'pid 84021 · uptime 4d 2h · listening :7421' },
{ name: 'Filesystem', status: 'ok', latency: '—', detail: '14.2 GB free of 512 GB' },
{ name: 'GitHub MCP', status: 'ok', latency: '84 ms', detail: 'oauth ok · 18 tools · rate-limit 4500/5000 (warn at 4750)' },
{ name: 'Linear MCP', status: 'ok', latency: '142 ms', detail: 'oauth ok · 9 tools' },
{ name: 'Postgres MCP', status: 'ok', latency: '12 ms', detail: 'stdio · prod read replica' },
{ name: 'Figma MCP', status: 'ok', latency: '210 ms', detail: 'oauth ok · 6 tools' },
{ name: 'Notion MCP', status: 'error', latency: '—', detail: 'TLS handshake failed · 4 retries · backing off 30s' },
{ name: 'Slack MCP', status: 'warn', latency: '—', detail: 'oauth token expired · re-authenticate' },
{ name: 'Sentry MCP', status: 'idle', latency: '—', detail: 'disabled' },
{ name: 'Cron scheduler', status: 'ok', latency: '—', detail: '5 jobs registered · next: incident-triage in 12m' },
{ name: 'Local model cache', status: 'ok', latency: '—', detail: '412 MB · last pruned 2d ago' },
];
function Health() {
const [scanning, setScanning] = React.useState(false);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const ok = HEALTH_CHECKS.filter(c => c.status === 'ok').length;
const warn = HEALTH_CHECKS.filter(c => c.status === 'warn').length;
const err = HEALTH_CHECKS.filter(c => c.status === 'error').length;
function rerun() {
setScanning(true);
setTimeout(() => setScanning(false), 1400);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Health"
subtitle="A diagnostics report across Scarf, the agent, and connected services"
actions={<>
<Btn icon="download">Save report</Btn>
<Btn kind="primary" icon="rotate-cw" loading={scanning} onClick={rerun}>{scanning ? 'Scanning…' : 'Re-run'}</Btn>
</>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
{/* Summary banner */}
<div style={{
background: err > 0 ? 'var(--red-100)' : warn > 0 ? 'var(--orange-100)' : 'var(--green-100)',
border: `0.5px solid ${err > 0 ? 'var(--red-500)' : warn > 0 ? 'var(--orange-500)' : 'var(--green-500)'}`,
borderRadius: 10, padding: 16, marginBottom: 24,
display: 'flex', alignItems: 'center', gap: 14,
}}>
<div style={{
width: 38, height: 38, borderRadius: 9,
background: err > 0 ? 'var(--red-500)' : warn > 0 ? 'var(--orange-500)' : 'var(--green-500)',
color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide={err > 0 ? 'alert-octagon' : warn > 0 ? 'alert-triangle' : 'shield-check'} style={{ width: 20, height: 20 }}></i>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 2 }}>
{err > 0 ? `${err} service${err === 1 ? '' : 's'} unhealthy`
: warn > 0 ? `${warn} warning${warn === 1 ? '' : 's'} to review`
: 'All systems healthy'}
</div>
<div style={{ fontSize: 13, color: 'var(--fg-muted)' }}>
{ok} ok · {warn} warning · {err} error · scanned 2 minutes ago
</div>
</div>
</div>
{/* Checks */}
<SettingsGroup title="Diagnostic checks">
{HEALTH_CHECKS.map((c, i) => <HealthRow key={c.name} c={c} last={i === HEALTH_CHECKS.length - 1} />)}
</SettingsGroup>
<SettingsGroup title="Environment">
<SettingsRow icon="info" title="Scarf version"
description="0.14.2 · 0.15.0 available"
control={<Btn size="sm">Update</Btn>} />
<SettingsRow icon="cpu" title="Platform"
description="macOS 14.4.1 · Apple M3 Pro · 36 GB"
control={<Pill tone="green" dot>supported</Pill>} />
<SettingsRow icon="terminal" title="Shell"
description="/bin/zsh 5.9 · path 47 entries"
control={<Btn size="sm">Inspect</Btn>} last />
</SettingsGroup>
</div>
</div>
);
}
function HealthRow({ c, last }) {
const tones = {
ok: { tone: 'green', icon: 'check-circle' },
warn: { tone: 'amber', icon: 'alert-triangle' },
error: { tone: 'red', icon: 'x-circle' },
idle: { tone: 'gray', icon: 'minus-circle' },
};
const t = tones[c.status];
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<Pill tone={t.tone} icon={t.icon} size="sm">{c.status}</Pill>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{c.name}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg-muted)', marginTop: 2, fontFamily: 'var(--font-mono)' }}>{c.detail}</div>
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 70, textAlign: 'right' }}>{c.latency}</span>
</div>
);
}
window.Health = Health;
-107
View File
@@ -1,107 +0,0 @@
// Insights usage charts and breakdowns.
function Insights() {
return (
<div style={{ overflow: 'auto', height: '100%' }}>
<ContentHeader title="Insights"
subtitle="Patterns across sessions, models, and tools"
right={<select style={{
fontSize: 12, padding: '5px 10px', border: '1px solid var(--border-strong)',
borderRadius: 6, background: 'var(--bg-card)', fontFamily: 'var(--font-sans)',
}}><option>Last 7 days</option><option>Last 30 days</option><option>This year</option></select>}
actions={<Btn icon="download">Export CSV</Btn>} />
<div style={{ padding: '20px 28px', display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ display: 'flex', gap: 12 }}>
<StatCard label="Sessions" value="847" sub="↗ +12% vs prev" />
<StatCard label="Tokens" value="2.4M" sub="1.8M in · 0.6M out" />
<StatCard label="Tool calls" value="3,221" sub="3.8 avg/session" />
<StatCard label="Avg latency" value="1.2s" accent="var(--accent)" sub="p95 4.1s" />
<StatCard label="Cost" value="$42.18" sub="$0.05 avg/session" />
</div>
<Card>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
<div style={{ flex: 1, fontSize: 13, fontWeight: 600 }}>Token usage</div>
<div style={{ display: 'flex', gap: 12, fontSize: 11, color: 'var(--fg-muted)' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: 'var(--accent)' }}></span>
Input
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<span style={{ width: 9, height: 9, borderRadius: 2, background: 'var(--brand-200)' }}></span>
Output
</span>
</div>
</div>
<BarChart />
</Card>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<Card>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 14 }}>By model</div>
<BreakdownRow label="claude-sonnet-4.5" value="62%" bar="var(--accent)" sub="$28.41 · 524 sessions" />
<BreakdownRow label="claude-haiku-4.5" value="31%" bar="var(--brand-300)" sub="$10.18 · 263 sessions" />
<BreakdownRow label="claude-opus-4.5" value="5%" bar="var(--brand-700)" sub="$3.40 · 42 sessions" />
<BreakdownRow label="local/llama-3.3" value="2%" bar="var(--gray-400)" sub="$0.00 · 18 sessions" last />
</Card>
<Card>
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 14 }}>By tool kind</div>
<BreakdownRow label="read" value="42%" bar="var(--green-500)" sub="1,353 calls" />
<BreakdownRow label="execute" value="24%" bar="var(--orange-500)" sub="773 calls" />
<BreakdownRow label="edit" value="18%" bar="var(--blue-500)" sub="580 calls" />
<BreakdownRow label="fetch" value="11%" bar="var(--purple-tool-500)" sub="354 calls" />
<BreakdownRow label="browser" value="5%" bar="var(--indigo-500)" sub="161 calls" last />
</Card>
</div>
</div>
</div>
);
}
function BarChart() {
// 14 days of data, hand-tuned
const data = [
[120, 40], [80, 32], [180, 60], [240, 90], [200, 75], [60, 22], [40, 15],
[110, 38], [170, 56], [220, 82], [280, 98], [310, 110], [240, 78], [190, 64],
];
const max = 420;
const chartH = 160; // px area for bars
return (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: 184, padding: '0 4px' }}>
{data.map(([inp, outp], i) => {
const inpH = Math.round(inp / max * chartH);
const outpH = Math.round(outp / max * chartH);
return (
<div key={i} style={{
flex: 1, display: 'flex', flexDirection: 'column',
justifyContent: 'flex-end', alignItems: 'stretch', minWidth: 0,
}}>
<div style={{ background: 'var(--brand-200)', height: outpH,
borderRadius: '3px 3px 0 0' }}></div>
<div style={{ background: 'var(--accent)', height: inpH }}></div>
<div style={{ fontSize: 9, color: 'var(--fg-faint)', textAlign: 'center', marginTop: 4,
fontFamily: 'var(--font-mono)', height: 14 }}>{i % 2 === 0 ? `04/${12 + i}` : ''}</div>
</div>
);
})}
</div>
);
}
function BreakdownRow({ label, value, bar, sub, last }) {
return (
<div style={{ marginBottom: last ? 0 : 14 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div style={{ flex: 1, fontFamily: 'var(--font-mono)', fontSize: 12 }}>{label}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600 }}>{value}</div>
</div>
<div style={{ height: 6, background: 'var(--bg-quaternary)', borderRadius: 3, overflow: 'hidden' }}>
<div style={{ width: value, height: '100%', background: bar, borderRadius: 3 }}></div>
</div>
<div style={{ fontSize: 10.5, color: 'var(--fg-faint)', marginTop: 3 }}>{sub}</div>
</div>
);
}
window.Insights = Insights;
-123
View File
@@ -1,123 +0,0 @@
// Logs streaming monospace surface. Filter pills + a fake live tail.
const LOG_LINES = [
{ ts: '09:42:18.124', level: 'info', source: 'gateway', msg: 'POST /v1/messages → 200 (1.2s, 482 tokens out)' },
{ ts: '09:42:18.066', level: 'debug', source: 'tool', msg: 'tool_call read_file path=src/App.jsx (8.2KB)' },
{ ts: '09:42:17.880', level: 'info', source: 'agent', msg: 'turn 14 started — personality=Forge model=claude-sonnet-4.5' },
{ ts: '09:42:15.341', level: 'warn', source: 'mcp', msg: 'github: rate-limit warning 4500/5000 used this hour' },
{ ts: '09:42:11.012', level: 'info', source: 'tool', msg: 'tool_call execute cmd="npm test -- --watch=false" status=ok 14.2s' },
{ ts: '09:42:01.508', level: 'error', source: 'tool', msg: 'tool_call execute denied: command "rm -rf node_modules" matches deny rule "rm -rf"' },
{ ts: '09:41:58.211', level: 'info', source: 'agent', msg: 'user message received (1.4KB)' },
{ ts: '09:41:42.004', level: 'debug', source: 'memory', msg: 'AGENTS.md hash unchanged (4f02…ab19), skipping reload' },
{ ts: '09:41:30.882', level: 'info', source: 'cron', msg: 'incident-triage finished ok (4.2s)' },
{ ts: '09:41:26.108', level: 'info', source: 'cron', msg: 'incident-triage started' },
{ ts: '09:41:18.443', level: 'info', source: 'mcp', msg: 'linear: tools/list 9 tools (142ms)' },
{ ts: '09:40:54.221', level: 'warn', source: 'gateway', msg: 'approval pending: tool_call execute cmd="git push origin main" (12s)' },
{ ts: '09:40:42.001', level: 'info', source: 'agent', msg: 'turn 13 ended — 2.1s, 7 tool calls, $0.0042' },
{ ts: '09:40:21.778', level: 'debug', source: 'tool', msg: 'tool_call list_files path=ui_kits/scarf-mac (24 entries)' },
{ ts: '09:40:18.422', level: 'error', source: 'mcp', msg: 'notion: TLS handshake failed (timeout 5s) — backing off 30s' },
{ ts: '09:40:02.114', level: 'info', source: 'agent', msg: 'session resumed (idle 14m)' },
];
const LEVEL_TONES = {
debug: '#7C7263', info: 'var(--blue-500)', warn: 'var(--amber-500)', error: 'var(--red-500)',
};
function Logs() {
const [level, setLevel] = React.useState(['info', 'warn', 'error']);
const [source, setSource] = React.useState('all');
const [search, setSearch] = React.useState('');
const [follow, setFollow] = React.useState(true);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const sources = ['all', 'agent', 'tool', 'gateway', 'mcp', 'cron', 'memory'];
const filtered = LOG_LINES.filter(l => {
if (!level.includes(l.level)) return false;
if (source !== 'all' && l.source !== source) return false;
if (search && !l.msg.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Logs"
subtitle="Live tail across the gateway, agent, tools, MCP servers, and cron"
actions={<>
<Btn icon="download">Export</Btn>
<Btn icon={follow ? 'pause' : 'play'} onClick={() => setFollow(!follow)}>
{follow ? 'Pause' : 'Follow'}
</Btn>
</>} />
{/* Toolbar */}
<div style={{
padding: '12px 24px', borderBottom: '0.5px solid var(--border)',
background: 'var(--bg-card)', display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap',
}}>
<TextInput value={search} onChange={setSearch} leftIcon="search" placeholder="Filter messages…" mono width={280} />
<div style={{ display: 'flex', gap: 4 }}>
{['debug', 'info', 'warn', 'error'].map(lv => {
const on = level.includes(lv);
return (
<button key={lv} onClick={() => setLevel(on ? level.filter(x => x !== lv) : [...level, lv])} style={{
padding: '4px 10px', borderRadius: 6, border: '0.5px solid var(--border)',
background: on ? 'var(--bg-tertiary)' : 'var(--bg-card)',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: on ? LEVEL_TONES[lv] : 'var(--fg-faint)',
textTransform: 'uppercase', cursor: 'pointer', letterSpacing: '0.04em',
}}>{lv}</button>
);
})}
</div>
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto' }}>
{sources.map(s => (
<button key={s} onClick={() => setSource(s)} style={{
padding: '4px 10px', borderRadius: 6, border: 'none',
background: source === s ? 'var(--accent-tint)' : 'transparent',
color: source === s ? 'var(--accent-active)' : 'var(--fg-muted)',
fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer',
}}>{s}</button>
))}
</div>
</div>
{/* Tail */}
<div style={{
flex: 1, overflowY: 'auto', background: '#1F1B16', color: '#E8E1D2',
fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.7,
padding: '12px 0',
}}>
{filtered.map((l, i) => <LogRow key={i} l={l} />)}
{follow && (
<div style={{ padding: '6px 24px', display: 'flex', alignItems: 'center', gap: 8,
color: '#A89B82', fontSize: 11 }}>
<span style={{ width: 6, height: 6, borderRadius: 3, background: 'var(--green-500)',
animation: 'pulse 1.4s ease-in-out infinite' }}></span>
following 4 lines/sec
</div>
)}
</div>
<style>{`
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
`}</style>
</div>
);
}
function LogRow({ l }) {
return (
<div style={{
display: 'flex', gap: 14, padding: '1px 24px', alignItems: 'baseline',
}}>
<span style={{ color: '#7C7263', fontSize: 11, width: 100, flexShrink: 0 }}>{l.ts}</span>
<span style={{ color: LEVEL_TONES[l.level], width: 50, flexShrink: 0,
textTransform: 'uppercase', fontSize: 10, fontWeight: 700, letterSpacing: '0.04em' }}>{l.level}</span>
<span style={{ color: '#A89B82', width: 70, flexShrink: 0 }}>{l.source}</span>
<span style={{ color: '#E8E1D2', flex: 1 }}>{l.msg}</span>
</div>
);
}
window.Logs = Logs;
-193
View File
@@ -1,193 +0,0 @@
// MCP Servers connection list + detail with health, capabilities, and logs.
const MCP_SERVERS = [
{ id: 'github', name: 'GitHub', transport: 'http', url: 'https://mcp.github.com/v1', status: 'connected', tools: 18, prompts: 4, resources: 12, latency: 84, version: '1.4.2', auth: 'oauth', scope: 'org/wizemann' },
{ id: 'linear', name: 'Linear', transport: 'http', url: 'https://mcp.linear.app/sse', status: 'connected', tools: 9, prompts: 0, resources: 6, latency: 142, version: '0.9.1', auth: 'oauth', scope: 'wizemann' },
{ id: 'slack', name: 'Slack', transport: 'http', url: 'https://mcp.slack.com/v1', status: 'auth-required', tools: 0, prompts: 0, resources: 0, latency: null, version: '—', auth: 'oauth', scope: '—' },
{ id: 'postgres-prod', name: 'Postgres (prod, ro)', transport: 'stdio', url: 'mcp-postgres --readonly', status: 'connected', tools: 4, prompts: 0, resources: 28, latency: 12, version: '2.1.0', auth: 'env', scope: 'prod-replica' },
{ id: 'figma', name: 'Figma', transport: 'http', url: 'https://mcp.figma.com/v1', status: 'connected', tools: 6, prompts: 2, resources: 0, latency: 210, version: '0.4.0', auth: 'oauth', scope: 'wizemann-design' },
{ id: 'notion', name: 'Notion', transport: 'http', url: 'https://mcp.notion.so/v1', status: 'error', tools: 0, prompts: 0, resources: 0, latency: null, version: '—', auth: 'oauth', scope: '—', error: 'TLS handshake failed (timeout 5s)' },
{ id: 'sentry', name: 'Sentry', transport: 'http', url: 'https://mcp.sentry.io/v1', status: 'disabled', tools: 0, prompts: 0, resources: 0, latency: null, version: '—', auth: 'token', scope: 'wizemann' },
];
const STATUS_TONES = {
'connected': { tone: 'green', label: 'connected' },
'auth-required': { tone: 'amber', label: 'auth required' },
'error': { tone: 'red', label: 'error' },
'disabled': { tone: 'gray', label: 'disabled' },
};
function MCPServers() {
const [active, setActive] = React.useState('github');
const server = MCP_SERVERS.find(s => s.id === active);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="MCP Servers"
subtitle="Model Context Protocol endpoints — each adds a bundle of tools, prompts, and resources"
actions={<><Btn icon="rotate-cw">Reconnect all</Btn><Btn kind="primary" icon="plus">Add server</Btn></>} />
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
<div style={{ width: 320, borderRight: '0.5px solid var(--border)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-card)' }}>
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
{MCP_SERVERS.map(s => <MCPRow key={s.id} s={s} active={s.id === active} onClick={() => setActive(s.id)} />)}
</div>
<div style={{ padding: 12, borderTop: '0.5px solid var(--border)' }}>
<Btn fullWidth icon="hard-drive">Browse marketplace</Btn>
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', background: 'var(--bg)', padding: '24px 32px' }}>
<MCPDetail server={server} />
</div>
</div>
</div>
);
}
function MCPRow({ s, active, onClick }) {
const status = STATUS_TONES[s.status];
const [hover, setHover] = React.useState(false);
return (
<div onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
padding: '11px 12px', borderRadius: 7, cursor: 'pointer', marginBottom: 2,
background: active ? 'var(--accent-tint)' : (hover ? 'var(--bg-quaternary)' : 'transparent'),
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<ServerGlyph id={s.id} size={22} />
<div style={{ flex: 1, fontSize: 13, fontWeight: 500,
color: active ? 'var(--accent-active)' : 'var(--fg)' }}>{s.name}</div>
<Dot tone={status.tone} />
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{s.transport} · {s.tools} tools · {s.prompts} prompts
</div>
</div>
);
}
function ServerGlyph({ id, size = 22 }) {
const palette = {
github: '#1F1B16', linear: '#5E6AD2', slack: '#611F69',
'postgres-prod': '#336791', figma: '#F24E1E', notion: '#191919', sentry: '#362D59',
};
const letter = id[0].toUpperCase();
return (
<div style={{
width: size, height: size, borderRadius: 5, background: palette[id] || '#888',
color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-display)', fontSize: size * 0.5, fontWeight: 700, flexShrink: 0,
}}>{letter}</div>
);
}
function MCPDetail({ server }) {
const status = STATUS_TONES[server.status];
return (
<>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, marginBottom: 20 }}>
<ServerGlyph id={server.id} size={48} />
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<div className="scarf-h2" style={{ fontSize: 22 }}>{server.name}</div>
<Pill tone={status.tone} dot>{status.label}</Pill>
<span style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>v{server.version}</span>
</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>{server.url}</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<Btn icon="rotate-cw">Reconnect</Btn>
<Toggle on={server.status !== 'disabled'} size="lg" />
</div>
</div>
{server.error && (
<div style={{
background: 'var(--red-100)', border: '0.5px solid var(--red-500)',
borderRadius: 9, padding: 12, marginBottom: 20, display: 'flex', gap: 10, alignItems: 'flex-start',
}}>
<i data-lucide="alert-triangle" style={{ width: 16, height: 16, color: 'var(--red-500)', flexShrink: 0, marginTop: 1 }}></i>
<div>
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--red-500)', marginBottom: 2 }}>Connection failed</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>{server.error}</div>
</div>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 24 }}>
<StatCard label="Tools" value={server.tools} />
<StatCard label="Prompts" value={server.prompts} />
<StatCard label="Resources" value={server.resources} />
<StatCard label="Latency" value={server.latency != null ? `${server.latency} ms` : '—'} sub={server.latency != null ? 'p95: ' + Math.round(server.latency * 2.4) + ' ms' : '—'} />
</div>
<SettingsGroup title="Connection">
<SettingsRow icon="link" title="Transport"
description={server.transport === 'http' ? 'HTTP / SSE' : 'Local stdio process'}
control={<Pill>{server.transport}</Pill>} />
<SettingsRow icon="key" title="Auth"
description={server.auth === 'oauth' ? 'OAuth — refreshed automatically' : server.auth === 'env' ? 'Environment variable' : 'Static token'}
control={<Btn size="sm" icon="external-link">Manage</Btn>} />
<SettingsRow icon="shield" title="Scope"
description={`Calls scoped to "${server.scope}".`}
control={<Btn size="sm">Edit</Btn>} last />
</SettingsGroup>
<SettingsGroup title="Capabilities" description="Tools, prompts, and resources advertised by this server.">
<CapRow icon="wrench" name="list_issues" kind="tool" desc="List repository issues with filters" />
<CapRow icon="wrench" name="create_pr" kind="tool" desc="Open a pull request from a branch" />
<CapRow icon="wrench" name="search_code" kind="tool" desc="Full-text search across accessible repos" />
<CapRow icon="message-square" name="review_pr" kind="prompt" desc="Structured PR review prompt" />
<CapRow icon="folder" name="repo://*" kind="resource" desc="Read-only access to repo file trees" last />
</SettingsGroup>
<SettingsGroup title="Activity log" description="Last 5 events from this server.">
<LogLine when="2m ago" level="info" msg="tools/list returned 18 tools (84ms)" />
<LogLine when="14m ago" level="info" msg="github__list_issues invoked (owner=wizemann, state=open)" />
<LogLine when="42m ago" level="warn" msg="rate-limit warning: 4500/5000 used this hour" />
<LogLine when="1h ago" level="info" msg="oauth token refreshed" />
<LogLine when="3h ago" level="info" msg="connection established (TLS 1.3)" last />
</SettingsGroup>
<SettingsGroup title="Danger zone" tone="danger">
<SettingsRow icon="x-circle" title="Disconnect server"
description="Remove this server. Tools it provided will become unavailable."
control={<Btn size="sm" kind="danger">Disconnect</Btn>} last />
</SettingsGroup>
</>
);
}
function CapRow({ icon, name, kind, desc, last }) {
const tones = { tool: 'blue', prompt: 'purple', resource: 'green' };
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, padding: '11px 18px',
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<i data-lucide={icon} style={{ width: 14, height: 14, color: 'var(--fg-muted)', flexShrink: 0 }}></i>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12.5, color: 'var(--fg)', minWidth: 140 }}>{name}</div>
<div style={{ flex: 1, fontSize: 12, color: 'var(--fg-muted)' }}>{desc}</div>
<Pill tone={tones[kind]} size="sm">{kind}</Pill>
</div>
);
}
function LogLine({ when, level, msg, last }) {
const tones = { info: 'var(--fg-faint)', warn: 'var(--amber-500)', error: 'var(--red-500)' };
return (
<div style={{
display: 'flex', gap: 12, padding: '8px 18px', fontFamily: 'var(--font-mono)', fontSize: 11.5,
borderBottom: last ? 'none' : '0.5px solid var(--border)',
}}>
<span style={{ color: 'var(--fg-faint)', width: 80 }}>{when}</span>
<span style={{ color: tones[level], textTransform: 'uppercase', width: 44, fontSize: 10, fontWeight: 600, paddingTop: 1 }}>{level}</span>
<span style={{ color: 'var(--fg-muted)', flex: 1 }}>{msg}</span>
</div>
);
}
window.MCPServers = MCPServers;
-134
View File
@@ -1,134 +0,0 @@
// Memory AGENTS.md editor. Stored instructions the agent reads on every turn.
const MEMORY_FILES = [
{ id: 'global', name: 'AGENTS.md', scope: 'Global', path: '~/.scarf/AGENTS.md', updated: '2 days ago', size: '1.2 KB' },
{ id: 'wizemann', name: 'AGENTS.md', scope: 'Org · Wizemann', path: '~/.scarf/orgs/wizemann/AGENTS.md', updated: '1 week ago', size: '3.4 KB' },
{ id: 'project', name: 'AGENTS.md', scope: 'Project · sera', path: 'sera/AGENTS.md', updated: '14m ago', size: '5.8 KB' },
];
const SAMPLE_AGENTS = `# Sera — agent instructions
You are working on **Sera**, a CLI for building Anthropic-style applications.
The codebase is TypeScript + Bun. Tests live next to source as \`*.test.ts\`.
## Style
- Prefer named exports.
- 2-space indent, no semicolons in TS.
- Avoid default exports except for React components.
- Lowercase filenames except for React components (PascalCase).
## Workflow
- Run \`bun test\` after every meaningful change.
- Open a draft PR early; flip to ready when CI is green.
- Update CHANGELOG.md when changing public API.
## Don't
- Touch \`scripts/release.ts\` — owned by ops.
- Pull in dependencies without flagging it first.
- Push directly to main.
`;
function Memory() {
const [active, setActive] = React.useState('project');
const [draft, setDraft] = React.useState(SAMPLE_AGENTS);
const [dirty, setDirty] = React.useState(false);
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const file = MEMORY_FILES.find(f => f.id === active);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Memory" subtitle="AGENTS.md files the agent reads on every turn. Project beats org beats global."
actions={<>
<Btn icon="rotate-ccw" disabled={!dirty}>Discard</Btn>
<Btn kind="primary" icon="check" disabled={!dirty}>Save</Btn>
</>}
/>
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
<div style={{ width: 280, borderRight: '0.5px solid var(--border)',
display: 'flex', flexDirection: 'column', background: 'var(--bg-card)' }}>
<div style={{ padding: '14px 14px 6px', fontSize: 10, fontWeight: 600,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
Memory files
</div>
<div style={{ flex: 1, padding: 8 }}>
{MEMORY_FILES.map(f => {
const a = f.id === active;
return (
<div key={f.id} onClick={() => setActive(f.id)} style={{
padding: '10px 12px', borderRadius: 7, cursor: 'pointer', marginBottom: 2,
background: a ? 'var(--accent-tint)' : 'transparent',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<i data-lucide="file-text" style={{ width: 13, height: 13,
color: a ? 'var(--accent-active)' : 'var(--fg-muted)' }}></i>
<div style={{ fontSize: 13, fontWeight: 500,
color: a ? 'var(--accent-active)' : 'var(--fg)' }}>{f.scope}</div>
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{f.path}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 2 }}>
{f.size} · {f.updated}
</div>
</div>
);
})}
<div style={{ marginTop: 12 }}>
<Btn fullWidth icon="plus" size="sm">Add memory file</Btn>
</div>
</div>
<div style={{ padding: 14, borderTop: '0.5px solid var(--border)' }}>
<div style={{ fontSize: 11, color: 'var(--fg-muted)', lineHeight: 1.5 }}>
<i data-lucide="info" style={{ width: 11, height: 11, verticalAlign: 'text-top', marginRight: 4 }}></i>
Files are loaded in order narrower scopes override broader ones.
</div>
</div>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
<div style={{
padding: '12px 24px', borderBottom: '0.5px solid var(--border)',
background: 'var(--bg-card)', display: 'flex', alignItems: 'center', gap: 12,
}}>
<i data-lucide="file-text" style={{ width: 16, height: 16, color: 'var(--accent)' }}></i>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{file.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>{file.path}</div>
</div>
{dirty
? <Pill tone="amber" dot>unsaved</Pill>
: <Pill tone="green" dot>saved</Pill>}
<div style={{ display: 'flex', gap: 4 }}>
<IconBtn icon="eye" tooltip="Preview" />
<IconBtn icon="more-horizontal" tooltip="More" />
</div>
</div>
<textarea value={draft}
onChange={e => { setDraft(e.target.value); setDirty(true); }}
style={{
flex: 1, padding: '20px 32px', border: 'none', outline: 'none', resize: 'none',
fontFamily: 'var(--font-mono)', fontSize: 13, lineHeight: 1.7,
color: 'var(--fg)', background: 'var(--bg)',
}}
/>
<div style={{
padding: '8px 24px', borderTop: '0.5px solid var(--border)', background: 'var(--bg-card)',
display: 'flex', alignItems: 'center', gap: 16, fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)',
}}>
<span>markdown</span>
<span>·</span>
<span>{draft.split('\n').length} lines</span>
<span>·</span>
<span>{draft.length} chars</span>
<span style={{ marginLeft: 'auto' }}>last loaded: {file.updated}</span>
</div>
</div>
</div>
</div>
);
}
window.Memory = Memory;
-422
View File
@@ -1,422 +0,0 @@
// MoreViews.jsx Personalities, Quick Commands, Platforms, Credentials,
// Plugins, Webhooks, Profiles, Gateway. Each is a focused list/detail or grid.
// Personalities
const PERSONALITIES = [
{ id: 'forge', name: 'Forge', emoji: '⚒', color: '#C25A2A', desc: 'Engineering pair. Refactors, tests, reviews PRs.', model: 'sonnet-4.5', tools: 14, used: '2m ago' },
{ id: 'hermes', name: 'Hermes', emoji: '✉', color: '#7E5BA9', desc: 'Operations. Handles ops scripts, summaries, status.', model: 'haiku-4.5', tools: 8, used: '32m ago' },
{ id: 'atlas', name: 'Atlas', emoji: '◇', color: '#3F6BA9', desc: 'Long-form writer. Spec drafts, release notes, docs.', model: 'opus-4.1', tools: 6, used: 'yesterday' },
{ id: 'vesta', name: 'Vesta', emoji: '✿', color: '#3F8A6E', desc: 'Design partner. Critiques layouts, suggests patterns.', model: 'sonnet-4.5', tools: 4, used: '3 days ago' },
{ id: 'gaia', name: 'Gaia', emoji: '✱', color: '#A8741F', desc: 'Researcher. Web search, summarization, citations.', model: 'sonnet-4.5', tools: 5, used: '1 week ago' },
];
function Personalities() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Personalities"
subtitle="Pre-configured agents — system prompt, model, allowed tools, defaults"
actions={<Btn kind="primary" icon="plus">New personality</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: 32 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 14 }}>
{PERSONALITIES.map(p => <PersonalityCard key={p.id} p={p} />)}
<Card padding={24} interactive style={{ display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', minHeight: 180,
border: '1px dashed var(--border-strong)', background: 'transparent', boxShadow: 'none' }}>
<div style={{ width: 40, height: 40, borderRadius: 10, background: 'var(--bg-quaternary)',
display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 8, color: 'var(--fg-muted)' }}>
<i data-lucide="plus" style={{ width: 20, height: 20 }}></i>
</div>
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--fg-muted)' }}>New personality</div>
</Card>
</div>
</div>
</div>
);
}
function PersonalityCard({ p }) {
return (
<Card interactive padding={18}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 12 }}>
<div style={{
width: 38, height: 38, borderRadius: 9, background: p.color, color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-display)', fontSize: 18,
}}>{p.emoji}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 600 }}>{p.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>last used {p.used}</div>
</div>
<IconBtn icon="more-horizontal" size={26} />
</div>
<div style={{ fontSize: 12.5, color: 'var(--fg-muted)', lineHeight: 1.5, marginBottom: 14, minHeight: 36 }}>{p.desc}</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<Pill size="sm">{p.model}</Pill>
<Pill size="sm" icon="wrench">{p.tools} tools</Pill>
</div>
</Card>
);
}
window.Personalities = Personalities;
// Quick Commands
const QC = [
{ trigger: '/test', name: 'Run tests', desc: 'Run the project test suite, summarize failures.', personality: 'Forge', uses: 142 },
{ trigger: '/review', name: 'Review PR', desc: 'Walk the diff in a checked-out PR and post review notes.', personality: 'Forge', uses: 38 },
{ trigger: '/standup', name: 'Standup summary', desc: 'Summarize yesterday\'s commits + Linear updates.', personality: 'Hermes', uses: 24 },
{ trigger: '/notes', name: 'Release notes', desc: 'Group merged PRs since last tag into release notes.', personality: 'Atlas', uses: 8 },
{ trigger: '/figma', name: 'Open Figma frame', desc: 'Resolve a Figma URL and import frame metadata.', personality: 'Vesta', uses: 14 },
{ trigger: '/cite', name: 'Cite source', desc: 'Web search + return citations as Markdown footnotes.', personality: 'Gaia', uses: 9 },
];
function QuickCommands() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Quick Commands"
subtitle="Slash-prefixed shortcuts that expand into full prompts"
actions={<Btn kind="primary" icon="plus">New command</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<SettingsGroup>
{QC.map((q, i) => (
<div key={q.trigger} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === QC.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<span style={{
fontFamily: 'var(--font-mono)', fontSize: 12.5, fontWeight: 600,
color: 'var(--accent)', background: 'var(--accent-tint)',
padding: '4px 9px', borderRadius: 6, minWidth: 80, textAlign: 'center',
}}>{q.trigger}</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{q.name}</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{q.desc}</div>
</div>
<Pill size="sm">{q.personality}</Pill>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 70, textAlign: 'right' }}>
{q.uses} uses
</span>
<IconBtn icon="more-horizontal" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.QuickCommands = QuickCommands;
// Platforms
const PLATFORMS = [
{ id: 'github', name: 'GitHub', desc: 'Repos, issues, PRs', conn: true, scope: 'org/wizemann · 14 repos' },
{ id: 'linear', name: 'Linear', desc: 'Issues & projects', conn: true, scope: 'wizemann · all teams' },
{ id: 'slack', name: 'Slack', desc: 'Messaging', conn: false, scope: '—' },
{ id: 'notion', name: 'Notion', desc: 'Docs', conn: false, scope: '—' },
{ id: 'figma', name: 'Figma', desc: 'Design files', conn: true, scope: 'wizemann-design' },
{ id: 'sentry', name: 'Sentry', desc: 'Error monitoring', conn: false, scope: '—' },
{ id: 'pagerduty', name: 'PagerDuty', desc: 'On-call', conn: false, scope: '—' },
{ id: 'stripe', name: 'Stripe', desc: 'Payments', conn: false, scope: '—' },
];
function Platforms() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const palette = { github: '#1F1B16', linear: '#5E6AD2', slack: '#611F69', notion: '#191919',
figma: '#F24E1E', sentry: '#362D59', pagerduty: '#06AC38', stripe: '#635BFF' };
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Platforms"
subtitle="Higher-level integrations. Each provides one or more MCP servers and credentials."
actions={<Btn icon="external-link">Browse marketplace</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: 32 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 14 }}>
{PLATFORMS.map(p => (
<Card key={p.id} interactive padding={18}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
<div style={{
width: 38, height: 38, borderRadius: 9, background: palette[p.id] || '#888', color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-display)', fontSize: 18, fontWeight: 700,
}}>{p.name[0]}</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>{p.name}</div>
<div style={{ fontSize: 11.5, color: 'var(--fg-muted)' }}>{p.desc}</div>
</div>
{p.conn && <Pill tone="green" dot size="sm">on</Pill>}
</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)', marginBottom: 12,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.scope}</div>
<Btn fullWidth size="sm" kind={p.conn ? 'secondary' : 'primary'}
icon={p.conn ? 'settings' : 'plug'}>
{p.conn ? 'Configure' : 'Connect'}
</Btn>
</Card>
))}
</div>
</div>
</div>
);
}
window.Platforms = Platforms;
// Credentials
const CREDS = [
{ name: 'ANTHROPIC_API_KEY', kind: 'api-key', source: 'Keychain', last: '2m ago', scope: 'global', value: 'sk-ant-•••••••••a4f2' },
{ name: 'GITHUB_TOKEN', kind: 'oauth', source: 'OAuth', last: '14m ago', scope: 'global', value: 'gho_•••••••••••3kP9' },
{ name: 'LINEAR_TOKEN', kind: 'oauth', source: 'OAuth', last: '2h ago', scope: 'global', value: 'lin_oauth_•••••8m2x' },
{ name: 'POSTGRES_URL', kind: 'secret', source: 'env (.env)', last: '4h ago', scope: 'project · sera', value: 'postgres://ro@•••' },
{ name: 'OPENAI_API_KEY', kind: 'api-key', source: 'Keychain', last: 'never', scope: 'global', value: 'sk-•••••••••••L7Pw' },
{ name: 'AWS_ACCESS_KEY_ID', kind: 'secret', source: '~/.aws/credentials', last: '1d ago', scope: 'global', value: 'AKIA•••••••••QZX' },
];
function Credentials() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
const [reveal, setReveal] = React.useState({});
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Credentials"
subtitle="API keys, OAuth tokens, and secrets the agent can read. Stored in OS keychain by default."
actions={<Btn kind="primary" icon="plus">Add credential</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<div style={{
background: 'var(--accent-tint)', border: '0.5px solid var(--accent)',
borderRadius: 9, padding: 12, marginBottom: 20, display: 'flex', alignItems: 'flex-start', gap: 10,
}}>
<i data-lucide="shield" style={{ width: 16, height: 16, color: 'var(--accent)', marginTop: 1 }}></i>
<div style={{ fontSize: 12.5, color: 'var(--fg)', lineHeight: 1.5 }}>
Credentials are never sent to Anthropic. They're injected into tool calls at the local gateway.
</div>
</div>
<SettingsGroup>
{CREDS.map((c, i) => (
<div key={c.name} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === CREDS.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<i data-lucide={c.kind === 'oauth' ? 'key-round' : c.kind === 'api-key' ? 'key' : 'lock'}
style={{ width: 16, height: 16, color: 'var(--fg-muted)', flexShrink: 0 }}></i>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 500 }}>{c.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', marginTop: 2 }}>
{c.source} · {c.scope} · used {c.last}
</div>
</div>
<code style={{
fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--fg-muted)',
background: 'var(--bg-quaternary)', padding: '3px 8px', borderRadius: 5, width: 220, textAlign: 'center',
}}>
{reveal[c.name] ? c.value.replace(/•+/g, '************') : c.value}
</code>
<IconBtn icon={reveal[c.name] ? 'eye-off' : 'eye'} size={26}
onClick={() => setReveal({ ...reveal, [c.name]: !reveal[c.name] })} />
<IconBtn icon="copy" size={26} />
<IconBtn icon="trash-2" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.Credentials = Credentials;
// Plugins
const PLUGINS = [
{ id: 'commit-message', name: 'Smart commits', desc: 'Generate conventional-commit messages from staged changes.', author: 'wizemann', enabled: true, hooks: ['pre-commit'] },
{ id: 'review-helper', name: 'Review helper', desc: 'Auto-tag PR reviewers based on touched paths.', author: 'wizemann', enabled: true, hooks: ['pr-open'] },
{ id: 'todo-extractor', name: 'TODO extractor', desc: 'Surface inline TODOs as a checklist on the dashboard.', author: 'community', enabled: false, hooks: ['session-start'] },
{ id: 'speak', name: 'Speak responses', desc: 'Read agent responses aloud via system TTS.', author: 'community', enabled: false, hooks: ['turn-end'] },
];
function Plugins() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Plugins"
subtitle="Local extensions that hook into agent and editor lifecycle events"
actions={<><Btn icon="external-link">Marketplace</Btn><Btn kind="primary" icon="plus">Install</Btn></>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<SettingsGroup>
{PLUGINS.map((p, i) => (
<div key={p.id} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === PLUGINS.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<div style={{
width: 32, height: 32, borderRadius: 7, background: 'var(--accent-tint)', color: 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<i data-lucide="puzzle" style={{ width: 15, height: 15 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, fontWeight: 500 }}>{p.name}</span>
<span style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>by {p.author}</span>
</div>
<div style={{ fontSize: 12, color: 'var(--fg-muted)', marginTop: 2 }}>{p.desc}</div>
<div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
{p.hooks.map(h => <Pill key={h} size="sm">{h}</Pill>)}
</div>
</div>
<Toggle on={p.enabled} />
<IconBtn icon="more-horizontal" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.Plugins = Plugins;
// Webhooks
const WEBHOOKS = [
{ name: 'PR opened → review', url: 'https://hooks.scarf.local/pr-review', events: ['github.pr.opened'], status: 'active', last: '2h ago' },
{ name: 'Sentry → triage', url: 'https://hooks.scarf.local/sentry-triage', events: ['sentry.issue.created', 'sentry.issue.regression'], status: 'active', last: '14m ago' },
{ name: 'Linear cycle → recap', url: 'https://hooks.scarf.local/cycle-recap', events: ['linear.cycle.completed'], status: 'paused', last: '8d ago' },
];
function Webhooks() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Webhooks"
subtitle="External events that trigger an agent run. Each maps an event payload to a personality + prompt."
actions={<Btn kind="primary" icon="plus">New webhook</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<SettingsGroup>
{WEBHOOKS.map((w, i) => (
<div key={w.name} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
borderBottom: i === WEBHOOKS.length - 1 ? 'none' : '0.5px solid var(--border)',
}}>
<i data-lucide="webhook" style={{ width: 16, height: 16, color: 'var(--fg-muted)' }}></i>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{w.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)', marginTop: 2,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{w.url}</div>
<div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
{w.events.map(e => <Pill key={e} size="sm">{e}</Pill>)}
</div>
</div>
{w.status === 'active'
? <Pill tone="green" dot>active</Pill>
: <Pill tone="gray" dot>paused</Pill>}
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-faint)', width: 80, textAlign: 'right' }}>{w.last}</span>
<IconBtn icon="more-horizontal" size={26} />
</div>
))}
</SettingsGroup>
</div>
</div>
);
}
window.Webhooks = Webhooks;
// Profiles
const PROFILES = [
{ id: 'dev', name: 'Development', desc: 'Permissive — auto-approve writes & execs in dev branches.', active: true, policies: 14 },
{ id: 'review', name: 'Code review', desc: 'Read-only filesystem, no execute, network only via MCP.', active: false, policies: 8 },
{ id: 'prod', name: 'Production', desc: 'All writes & execs require approval. No deletions.', active: false, policies: 22 },
{ id: 'air-gap', name: 'Air-gapped', desc: 'No network. Local tools only. For sensitive code paths.', active: false, policies: 6 },
];
function Profiles() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Profiles"
subtitle="Bundles of policies you switch between per-project or per-task"
actions={<Btn kind="primary" icon="plus">New profile</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: 32 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 }}>
{PROFILES.map(p => (
<Card key={p.id} interactive padding={20}
style={{ borderColor: p.active ? 'var(--accent)' : 'var(--border)',
boxShadow: p.active ? '0 0 0 2px var(--accent-tint)' : 'var(--shadow-sm)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
<i data-lucide="user-cog" style={{ width: 18, height: 18,
color: p.active ? 'var(--accent)' : 'var(--fg-muted)' }}></i>
<div style={{ fontSize: 15, fontWeight: 600, flex: 1 }}>{p.name}</div>
{p.active && <Pill tone="accent" dot>active</Pill>}
</div>
<div style={{ fontSize: 12.5, color: 'var(--fg-muted)', lineHeight: 1.5, marginBottom: 14, minHeight: 36 }}>{p.desc}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--fg-faint)', fontFamily: 'var(--font-mono)' }}>
<i data-lucide="shield" style={{ width: 12, height: 12 }}></i>
{p.policies} policies
<Btn size="sm" style={{ marginLeft: 'auto' }}>{p.active ? 'Edit' : 'Activate'}</Btn>
</div>
</Card>
))}
</div>
</div>
</div>
);
}
window.Profiles = Profiles;
// Gateway
function Gateway() {
React.useEffect(() => { requestAnimationFrame(() => window.lucide && window.lucide.createIcons()); });
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Gateway"
subtitle="Local proxy that routes every model & tool call. Logs, redacts, enforces policies."
actions={<Btn icon="rotate-cw">Restart</Btn>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 24 }}>
<StatCard label="Status" value="running" sub="pid 84021 · uptime 4d 2h" accent="var(--green-600)" />
<StatCard label="Listening" value=":7421" sub="loopback only" />
<StatCard label="Calls (24h)" value="1,284" sub="13 denied · 4 errored" />
<StatCard label="Throughput" value="2.4 MB/s" sub="p95: 6.1 MB/s" />
</div>
<SettingsGroup title="Network">
<SettingsRow icon="globe" title="Listen address"
description="The gateway binds to this address. Default loopback only."
control={<TextInput value="127.0.0.1:7421" mono />} />
<SettingsRow icon="lock" title="TLS"
description="Use a self-signed cert for outbound to 127.0.0.1."
control={<Toggle on={true} />} />
<SettingsRow icon="filter" title="Allowed hosts"
description="3 entries — api.anthropic.com, mcp.github.com, mcp.linear.app"
control={<Btn size="sm">Edit</Btn>} last />
</SettingsGroup>
<SettingsGroup title="Logging & redaction">
<SettingsRow icon="file-text" title="Request logging"
description="Persist headers + bodies for 7 days."
control={<Toggle on={true} />} />
<SettingsRow icon="eye-off" title="Redact secrets"
description="Mask values matching credential patterns before logging."
control={<Toggle on={true} />} />
<SettingsRow icon="archive" title="Log retention"
description="Older logs are pruned automatically."
control={<Select value="7d" options={[
{ value: '1d', label: '1 day' }, { value: '7d', label: '7 days' },
{ value: '30d', label: '30 days' }, { value: 'forever', label: 'Forever' },
]} />} last />
</SettingsGroup>
<SettingsGroup title="Performance">
<SettingsRow icon="zap" title="Concurrent requests"
control={<TextInput value="16" mono />} />
<SettingsRow icon="hourglass" title="Per-call timeout"
control={<Select value="60s" options={[
{ value: '30s', label: '30 seconds' }, { value: '60s', label: '60 seconds' },
{ value: '5m', label: '5 minutes' }, { value: '15m', label: '15 minutes' },
]} />} last />
</SettingsGroup>
</div>
</div>
);
}
window.Gateway = Gateway;
-83
View File
@@ -1,83 +0,0 @@
// Projects list of project folders the agent operates in.
function Projects() {
const projects = [
{ id: 1, name: 'hermes-blog', dir: '~/code/hermes-blog', template: 'awizemann/hermes-blog', sessions: 142, lastRun: '14m ago', cron: 2, status: 'healthy' },
{ id: 2, name: 'scarf', dir: '~/code/scarf', template: '—', sessions: 89, lastRun: '42m ago', cron: 0, status: 'healthy' },
{ id: 3, name: 'inbox-sweep', dir: '~/code/inbox-sweep', template: 'community/inbox-sweep', sessions: 38, lastRun: '3h ago', cron: 1, status: 'healthy' },
{ id: 4, name: 'twitter-recap', dir: '~/code/twitter-recap', template: 'awizemann/twitter-recap', sessions: 14, lastRun: '2d ago', cron: 1, status: 'paused' },
{ id: 5, name: 'pr-watcher', dir: '~/code/pr-watcher', template: 'community/pr-watcher', sessions: 4, lastRun: '5d ago', cron: 1, status: 'errored' },
];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<ContentHeader title="Projects"
subtitle="Each project pins context, AGENTS.md, cron jobs, and session history"
actions={<><Btn icon="folder-plus">Add Existing</Btn><Btn kind="primary" icon="plus">New from Template</Btn></>} />
<div style={{ flex: 1, overflowY: 'auto', padding: '20px 28px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 14 }}>
{projects.map(p => (
<Card key={p.id} padding={16} style={{ display: 'flex', flexDirection: 'column', gap: 10, cursor: 'pointer' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
<div style={{
width: 36, height: 36, borderRadius: 8, background: 'var(--accent-tint)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--accent)', flexShrink: 0,
}}>
<i data-lucide="folder" style={{ width: 18, height: 18 }}></i>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>{p.name}</div>
<div style={{ fontSize: 11, color: 'var(--fg-muted)',
fontFamily: 'var(--font-mono)', overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.dir}</div>
</div>
{p.status === 'healthy' && <Pill tone="green" dot>healthy</Pill>}
{p.status === 'paused' && <Pill tone="gray" dot>paused</Pill>}
{p.status === 'errored' && <Pill tone="red" dot>errored</Pill>}
</div>
{p.template !== '—' && (
<div style={{ fontSize: 11, color: 'var(--fg-muted)',
display: 'flex', alignItems: 'center', gap: 5 }}>
<i data-lucide="package" style={{ width: 11, height: 11 }}></i>
<span style={{ fontFamily: 'var(--font-mono)' }}>{p.template}</span>
</div>
)}
<div style={{ display: 'flex', gap: 16, paddingTop: 8,
borderTop: '0.5px solid var(--border)', fontSize: 11 }}>
<div>
<div style={{ color: 'var(--fg-muted)' }}>Sessions</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, marginTop: 1 }}>{p.sessions}</div>
</div>
<div>
<div style={{ color: 'var(--fg-muted)' }}>Cron jobs</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, marginTop: 1 }}>{p.cron}</div>
</div>
<div style={{ marginLeft: 'auto', textAlign: 'right' }}>
<div style={{ color: 'var(--fg-muted)' }}>Last run</div>
<div style={{ fontSize: 12, marginTop: 1 }}>{p.lastRun}</div>
</div>
</div>
</Card>
))}
<Card padding={16} style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
border: '1px dashed var(--border-strong)', boxShadow: 'none',
background: 'transparent', minHeight: 140, cursor: 'pointer',
color: 'var(--fg-muted)', flexDirection: 'column', gap: 8,
}}>
<i data-lucide="plus" style={{ width: 24, height: 24 }}></i>
<div style={{ fontSize: 13, fontWeight: 500 }}>New project</div>
<div style={{ fontSize: 11, textAlign: 'center', maxWidth: 180 }}>From template, GitHub repo, or empty folder</div>
</Card>
</div>
</div>
</div>
);
}
window.Projects = Projects;
-42
View File
@@ -1,42 +0,0 @@
# 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.

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