mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
13545689927cda6d60a2d4e8767d67986d5443b4
296 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1354568992 |
feat(hermes-v12): ACP multimodal image input on Mac + iOS (Phase C)
Hermes v0.12 advertises `prompt_capabilities.image = true` and accepts
image content blocks in `session/prompt`. This wires a producer flow on
both targets so users can attach images alongside text and have them
routed to the vision-capable model automatically.
Pipeline:
- ChatImageAttachment: Sendable value type holding base64 payload +
thumbnail, MIME type, source filename, and approximate byte count.
- ImageEncoder: detached-only Sendable service that downsamples to
Anthropic's 1568px long-edge cap, JPEG-encodes at q=0.85, and
produces a small inline thumbnail for composer chips. Cross-platform
(NSImage on Mac, UIImage on iOS, JPEG-passthrough on Linux/CI).
- ACPClient.sendPrompt(sessionId:text:images:) overload emits a content
array `[{type: "text"...}, {type: "image", data, mimeType}]` matching
the wire shape in hermes-agent/acp_adapter/server.py. The
zero-arg-images convenience overload preserves the v0.11 wire shape
for any unmodified callers.
Mac UI:
- RichChatInputBar grew an `attachments: [ChatImageAttachment]` state
array, a paperclip button (NSOpenPanel multi-pick), drag-drop and
paste handlers, and a horizontal preview chip strip. The "send"
callback's signature is `(String, [ChatImageAttachment]) -> Void`
threaded through RichChatView -> ChatTranscriptPane -> ChatView ->
ChatViewModel.sendText(text, images:). Image-only prompts are
permitted ("describe this") once at least one attachment is queued.
iOS UI:
- ChatView's composer adopts a paperclip + PhotosPicker flow with the
same chip strip and 5-attachment cap. Attachments live on
ChatController so they survive across PhotosPicker presentations.
loadTransferable(type: Data.self) feeds raw bytes into the same
ImageEncoder; encode work runs detached so MainActor stays
responsive on cellular.
Capability gating:
- Both composers hide the entire attachment surface when
HermesCapabilities.hasACPImagePrompts is false (pre-v0.12 hosts).
No paperclip button, no drop target, no paste accept — the input bar
is byte-for-byte the v0.11 surface against an older Hermes.
Tests: 209 ScarfCore tests pass; both Mac and iOS schemes build clean.
The encoder's pixel work is hard to unit-test at the package level
(no NSImage/UIImage in plain Swift CI) — manual end-to-end testing
is the verification path here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
da721fa276 |
feat(hermes-v12): provider catalog + auxiliary swap (Phase B)
Adds the five v0.12 inference providers to ModelCatalogService.overlayOnlyProviders so the model picker reaches them. IDs match HERMES_OVERLAYS verbatim: - gmi → GMI Cloud (api_key) - azure-foundry → Azure AI Foundry (api_key) - lmstudio → LM Studio (api_key, promoted from custom-endpoint alias) - minimax-oauth → MiniMax (OAuth, oauth_external) - tencent-tokenhub → Tencent TokenHub (api_key) Auxiliary tasks: drop the `flush_memories` row (Hermes removed it entirely in v0.12) and add `auxiliary.curator` so users can configure the model the autonomous curator's review fork uses. The Curator row is gated on HermesCapabilities.hasCuratorAux, so v0.11 hosts don't see a control that writes a key Hermes ignores. AuxiliarySettings, the YAML parser, and HealthViewModel's Tool Gateway breakdown are all updated. Side fixes: - CredentialPoolsGatingTests was missing `import ScarfCore` after ModelCatalogService moved to the package (broke the test target's compile against pure-Mac scarf). - Promoted `ModelCatalogService.overlayOnlyProviders` to public so the new `v012OverlayProvidersCarryCorrectAuthTypes` lock-in test can reach it. Tests: 14 ToolGateway tests pass; 209 ScarfCore tests pass; both Mac and iOS schemes build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a90a29add8 |
feat(hermes-v12): version-aware capability detection (Phase A)
Introduces `HermesCapabilities` (parsed from `hermes --version`) and a
per-server `HermesCapabilitiesStore` injected into Mac `ContextBoundRoot`
and iOS `ScarfGoTabRoot` via `.environment(_:)` and `.hermesCapabilities`.
Subsequent v0.12-targeted UI (Curator, Kanban, ACP image input,
auxiliary.curator, prompt cache TTL, etc.) can branch on these flags so
older Hermes installs degrade silently instead of throwing on unknown CLI
subcommands.
Adds `curatorReportJSON` / `curatorReportMD` paths to `HermesPathSet`.
Bumps the Hermes version target in CLAUDE.md from v2026.4.23 (v0.11.0) to
v2026.4.30 (v0.12.0) and lists the v0.12 surfaces Scarf will consume.
Side fixes:
- `M5FeatureVMTests.ScriptedTransport` was missing
`cachedSnapshotPath` after that property was added in 7b864d7;
added `URL? { nil }` stub.
- `M0dViewModelsTests` referenced `.degraded(reason:)` after the case
gained `hint` + `cause`; updated.
- `RemoteBackupService.zipDirectory` and `RemoteRestoreService.unzipArchive`
used `Foundation.Process` unconditionally, breaking the iOS build
(Process is unavailable on iOS). Wrapped in `#if !os(iOS)` with iOS
stubs that throw — the backup/restore flow is Mac-only by design.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
421e6030df |
fix(dashboard): shadow Hermes-home consolidation actually clears the warning
The "Project-local Hermes home shadowing global setup" banner has a "Copy fix command" button that produced a one-liner the user could paste on the remote. The old command only `cp`'d the project's `auth.json` into the global `~/.hermes/`; it never touched the project-local `.hermes/` directory. Hermes' CLI binds to the *closest* `.hermes/` as `$HERMES_HOME`, so the directory still being there meant it still shadowed — the detector's `fileExists(<project>/.hermes)` correctly kept returning true and the warning didn't go away after the user "fixed" it. They got stuck. Fix: rename the project-local `.hermes/` to `.hermes.scarf-bak.<UTC-stamp>/` after the auth copy. Hermes scans for a directory literally named `.hermes`, so the rename is enough to stop binding without losing user data — `state.db`, sessions, skills all survive untouched in the renamed folder. The user can inspect / delete the `.bak` later when confident. `mv` over `rm -rf` because a project's shadow can hold uncommitted session history; deletion would be unrecoverable, the rename is reversible. Also removes the `if shadow.hasAuthJSON` gate around the "Copy fix command" button — a state-only shadow (no creds, just `state.db`) still binds as `$HERMES_HOME` and needs the same rename to clear the warning. The button now always shows; the help-tooltip text branches on `hasAuthJSON` to describe what the command will do. Help-text now spells out the rename so the user knows where their data went before they paste anything. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b864d77d5 |
feat(servers): backup + restore for any Scarf server
Adds an end-to-end "back up this server's full Hermes state" flow
with a verifiable archive format and a matching restore that pushes
it onto a fresh droplet. Tested against a 570 MB local Hermes home
+ 5 projects, then iterated against a real DigitalOcean droplet.
Architecture
- `.scarfbackup` is a ZIP containing `manifest.json` (schema v1,
source server + hermes version + per-tarball SHA-256), one
`hermes.tar.gz` (gzipped tar of `~/.hermes/`), and one
`projects/<id>.tar.gz` per registered project. Streams via
`tar -czf - …` over SSH; never buffers a full archive in memory.
- New `streamRawBytes(executable:args:)` on `ServerTransport`
(Local + SSH impls) yields binary `Data` chunks. `streamLines`
splits on `\n` and would corrupt tar output — needed a
binary-safe sibling.
- `RemoteBackupService` runs preflight (resolves $HOME, probes
hermes version, enumerates projects via the existing
`ProjectDashboardService`, sizes each via `du -sb`, checks for
`sqlite3`), optionally runs `PRAGMA wal_checkpoint(TRUNCATE)`
to quiesce state.db, streams each tarball with incremental
SHA-256, then ZIP-bundles via `/usr/bin/zip`. Atomic
temp-then-rename so a partial archive never appears at the
user-chosen destination.
- `RemoteRestoreService` unzips into a temp dir, validates the
manifest's `kind` magic + `schemaVersion`, hash-verifies every
inner tarball BEFORE pushing any bytes to the target, then
streams each tarball into `tar -xzf - -C …` over SSH stdin.
Post-restore: rewrites `~/.hermes/scarf/projects.json` with
source→target path mappings via a small `python3 -c` script,
and pauses every cron job (`enabled: false`) so restored jobs
don't surprise-fire on a fresh droplet.
Defaults + safety
- Excluded from the backup unless explicitly opted in:
`auth.json` (provider creds), `mcp-tokens/` (per-host OAuth),
`logs/`. Always excluded: `state.db-{wal,shm}`,
`gateway_state.json`, and standard project junk
(`node_modules`, `.venv`, `.git/objects`, `__pycache__`,
`.next`, `dist`).
- Manifest records `options.includeAuth/includeMcpTokens/
includeLogs/checkpointedWAL` honestly so restore can warn
the user about what they'll need to re-establish manually.
- All paths are tilde-expanded against the resolved remote
`$HOME` before being passed to `tar`/`sqlite3`.
`tar -C '~/projects'` would otherwise fail with
"No such file or directory" because `shellQuote` wraps the
path in single quotes and tar doesn't expand tildes itself.
UI
- Per-row ellipsis menu on `ManageServersView` consolidates
Back Up… / Restore from Backup… / Diagnostics… / Remove…
Keeps the row visually clean as actions grow. Local server
gets Back Up + Restore (no Remove or Diagnostics).
- `BackupServerSheet` walks loading → ready (size + project
list + auth/logs toggles) → running (byte-counter progress
per stage) → done (Show in Finder) | failed (Try again).
- `RestoreServerSheet` walks awaitingFile → inspecting →
ready (source-vs-target preview, projects-root chooser,
cron-pause toggle, "auth was excluded" notes) → running →
done | failed.
- Both view models use a `WeakBox` two-step capture pattern so
the @Sendable progress callback hops back into MainActor
without the Swift 6 var-self warning on nested closures.
Cleanup folded in
- Drops two no-op `await`s on sync `startReaders()` in
`ProcessACPChannel` (warning surfaced after the Phase 1 ACP
changes; cleanest to fix in the same Transport-layer touch).
Verified
- Local round-trip via a Swift CLI harness:
preflight → backup → unzip listing matches manifest →
on-disk SHA-256 matches manifest claim for every tarball.
- Real DigitalOcean droplet: backup completes after the
tilde-expansion fix; restore preserves projects + sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
11946aad67 |
feat(remote): legible SSH/ACP failures + servers.json export/import
A vanished or misconfigured remote surfaced as an opaque 30s "ACP request 'initialize' timed out" because the channel's EOF fired with no exit code or stderr context, and `sh -c` on the remote couldn't find pipx-installed `hermes` on PATH. This makes remote failure modes immediately legible and adds a recovery path for the server registry itself. - `ACPClientError.processTerminated` now carries exit code + stderr tail; `performDisconnectCleanup` reads them from the channel before failing pending requests, and `ACPErrorHint.classify` recognises Connection refused, Operation timed out, Permission denied (publickey), Host key verification failed, Could not resolve hostname, and exit 127 / command not found. - `ProcessACPChannel.terminationHandler` closes the stdout read end the moment the OS reaps the child so disconnect cleanup fires within ~1s instead of waiting on `availableData`. `lastExitCode` reads `Process.terminationStatus` directly to avoid an actor-handshake race. - `SSHTransport.makeProcess` / `streamLines` switch from `sh -c` to `bash -lc` so non-interactive SSH shells source the user's profile and pick up pipx (`~/.local/bin`), Linuxbrew, asdf, and conda PATH entries. - New `ServerRegistry.exportFile()` / `importEntries(from:)` with a `.scarfservers` JSON envelope (schema v1, dedupe by UUID, default-server flag preserved). UI in `ManageServersView`'s header menu surfaces Export… / Import… via NSSave/OpenPanel. No secrets travel — `identityFile` is a path string and SSH keys live in `~/.ssh/`, not in `servers.json`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4140983866 |
feat(site): marketing landing page for Mac + ScarfGo
Replace the gh-pages root placeholder with a real landing page that sells both apps. Sources live at site/landing/ and publish through a new scripts/site.sh that mirrors scripts/catalog.sh and scripts/wiki.sh (check / build / preview / serve / publish, two-pass secret-scan, only touches root files + assets/ on gh-pages so appcast.xml and templates/ stay disjoint). Page is rust-palette tokens mapped from ScarfDesign, semantic HTML, SEO + AEO infra (OpenGraph, Twitter cards, JSON-LD SoftwareApplication + MobileApplication + FAQPage, llms.txt, sitemap, manifest), 12-entry FAQ, light/dark via prefers-color-scheme + manual toggle that swaps both site chrome and screenshot variants. tools/og-image.html renders the 1200x630 OG / 1200x600 Twitter cards via headless Chromium. Real captures from the live Mac app (9 surfaces x light + dark) + existing ScarfGo screenshots round out the imagery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cca99d4e13 | chore: Bump version to 2.5.2 v2.5.2 | ||
|
|
2aab9dac07 |
feat: chat-start preflight, Nous catalog, remote-aware admin sheets
Three feature batches that were in progress on chat-resilience — all aligned with v2.5.2's remote-context theme. ## Chat-start model preflight When a chat-start hits a server whose config.yaml has no model.default / model.provider, the upstream provider returns an opaque "Model parameter is required" 400 only AFTER the user types a prompt and hits send. New ModelPreflight in ScarfCore catches the missing keys before any ACP work; ChatView presents the existing ModelPickerSheet via a thin ChatModelPreflightSheet wrapper so the picker / validation / Nous-catalog branch stay single-sourced. ChatViewModel persists the selection via `hermes config set` and replays the original startACPSession arguments — the chat the user originally opened lands without re-clicking the project row. ## Nous Portal live catalog NousModelCatalogService fetches `GET /v1/models` from inference-api.nousresearch.com using the bearer token in `auth.json`, caches to `~/.hermes/scarf/nous_models_cache.json` (new path on HermesPathSet) with a 24h TTL. Picker's nous-overlay detail switches from a free-form TextField to a real model list, with a "Custom…" escape hatch (nousManualEntry) for IDs not yet in the API response. ## Remote-aware admin sheets (mirror of #54's pattern) The Add Project sheet got context-aware Verify in v2.5.1 (#54); this batch extends the same shape to three more sheets: - Profiles: remote import/export. ProfilesView gains showRemoteImportSheet + pendingRemoteExport state; reuses the same path-input + verify + run-via-hermes pattern from AddProjectSheet. Drives `hermes profile import <zip>` / `hermes profile export <name> <zip>` over SSH. - Backup restore (Settings → Advanced): pickLocalBackupZip + new RemoteBackupPathSheet so the Restore action picks a local zip on local contexts and verifies a remote path on remote contexts. - Template install destination: TemplateInstallSheet's parent- directory picker now branches on context. ParentDirectoryStep with browseLocalDirectory + verifyRemotePath + RemoteVerification — same UX vocabulary as AddProjectSheet, applied to where the template gets installed. Plus a `runHermesWithStdin` helper on HermesFileService for the profile import flow (passing zip bytes through stdin rather than landing them on the remote disk first), and ProjectTemplateInstaller gains a remote-path-aware code path for the install destination. ## Localizations Localizable.xcstrings adds strings for all the new copy across seven supported locales (en, zh-Hans, de, fr, es, ja, pt-BR). |
||
|
|
c31dfccb9b |
fix(ios-chat): move keyboard-dismiss chevron to leading edge (#57)
The keyboard accessory dismiss button added in #51 was placed at the trailing edge of the keyboard toolbar (Spacer before Button), which sits directly above the trailing-edge send button in the composer below. Two near-identical-shape controls visually stack on the right edge of the screen, confusing users about which is which. Move the Spacer() to AFTER the Button so the chevron lives at the leading edge of the keyboard accessory bar — visually separated from the send button below, and matches the iOS convention (Notes, Mail, Reminders all put accessory dismiss on the leading side). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
61e61f556a |
feat(chat): hideable sessions + inspector panes for the Mac chat (#58)
The 3-pane layout (264px sessions list + transcript + 320px inspector)
ate ~584px of horizontal space on every chat window — squeezing the
actual transcript on smaller windows AND keeping the "No tool selected"
empty-state visible even when irrelevant. User reported that as
"reasoning, in/out, hard to read because of the tool selected box
taking so much space".
Add toolbar toggles + Settings parity to hide either side pane:
- Two new @AppStorage keys in ChatDensitySettings:
scarf.chat.showSessionsList (default true)
scarf.chat.showInspector (default true)
- ChatView toolbar gains two buttons next to the View picker:
sidebar.left toggles the sessions list, sidebar.right toggles the
inspector. Both highlight in accent color when visible. Hidden when
in terminal mode (the 3-pane layout doesn't apply there).
- RichChatView body conditionally renders each side pane and its
divider, with .transition(.move + .opacity) and a 180ms easeInOut
animation so the transcript reflows smoothly rather than snapping.
- Auto-show inspector when a tool card is focused so a click never
silently dies — onChange of focusedToolCallId flips
showInspector back on if it was off. The slide-in animation
covers the visual transition.
- DisplayTab → Chat density gains parity Toggle rows for "Sessions
list" and "Tool inspector" — same group as the existing density
pickers from #47/#48 so the settings home is consistent.
Defaults match today's behavior so existing users see no change
until they opt out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
424711c3d9 |
fix(ios-snapshot): harden Citadel state.db snapshot path (#56)
Reported on iOS: dashboard shows "Connection issue / Citadel.SSH
Client.CommandFailed error 1", memory files (USER.md, SOUL.md) load
fine but Sessions / Activity / Tool Calls all show 0. The snapshot
operation that pulls ~/.hermes/state.db over SFTP via `sqlite3
.backup` was failing on the remote, but the iOS user got zero
actionable context.
Two latent bugs in CitadelServerTransport.asyncSnapshotSQLite —
both fixed in v2.5.0 for asyncRunProcess but missed on this path:
1. `executeCommand` throws CommandFailed on non-zero exit AND
discards the captured stderr buffer. So when sqlite3 is missing
(slim Docker images, statically-linked installs) or state.db
doesn't exist, the user only saw "error 1" and a generic
connection-issue banner with no remediation.
2. No `PATH=...` prefix. asyncRunProcess inline-prepends
`PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"`
so bare command resolution works on Citadel's stripped-PATH
exec channel; the snapshot path didn't, so any sqlite3 install
outside /usr/bin failed at exit 127 ("command not found").
Mirror the asyncRunProcess hardening on the snapshot path:
- Prepend the same PATH prefix so sqlite3 resolves on hosts where
it lives at /usr/local/bin or /opt/homebrew/bin.
- Drive `executeCommandStream` instead of `executeCommand`.
Capture stdout + stderr regardless of exit code.
- On non-zero exit, throw an NSError carrying the real stderr (or
stdout if stderr is empty — sqlite3 sometimes errors via stdout
depending on the remote shell). HermesDataService.humanize
already keys off "sqlite3: command not found" /
"permission denied" / "no such file" substrings, so once the
real message reaches it the dashboard banner becomes actionable
("sqlite3 is not installed on <host>. Install with apt install
sqlite3..." instead of the generic CommandFailed error).
- When the stream itself fails to start (network/auth-level), throw
with a "Failed to start snapshot stream" message so the connect-
level error path is distinguishable from the remote-exec failure.
iOS-only — Mac path was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
067aeda878 |
fix(catalog): async catalog reads — unfreezes Model + Credential sheets (#59)
Two views called ModelCatalogService.loadProviders() synchronously
from .onAppear on the MainActor:
- ModelPickerSheet (Settings → Model)
- AddCredentialSheet (Credential Pools → +)
loadProviders() walks loadCatalog() → transport.readFile() of
~/.hermes/models_dev_cache.json — a multi-megabyte JSON with ~1500
models across ~110 providers. On a remote SSH context that's a
synchronous SSH file read on the main thread; the user's reported
1–2 minute UI freeze on first open is exactly that. Even on local
contexts the JSONDecoder pass on the main thread is a noticeable
hiccup. Direct violation of CLAUDE.md's rule against sync I/O on
@MainActor.
Compound case: ModelPickerSheet.loadModelsForSelection() did the
same sync read every time the user clicked a different provider in
the picker — re-froze the UI per click.
Fix:
- Add async wrappers on the service:
loadProvidersAsync() -> [HermesProviderInfo]
loadModelsAsync(for:) -> [HermesModelInfo]
Each await Task.detached { sync method }.value. Existing sync
methods stay for tests and any non-View consumers.
- ModelPickerSheet: replace .onAppear with .task; await both async
calls. Same conversion for loadModelsForSelection() — renamed to
loadModelsForSelectionAsync() and called from the provider-list
selection binding via Task { ... }. Subscription state load also
routed through Task.detached since it's another auth.json read
that's tiny on local but SSH-backed on remote.
- AddCredentialSheet (CredentialPoolsView): same .onAppear → .task
conversion with isLoadingProviders @State driving an overlay
ProgressView "Loading providers..." while the read is in flight.
No behavior or data-shape change; pure I/O dispatch fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
389620059c |
fix(credentials): recognize OAuth providers; warn on project-shadowed Hermes
Three related fixes for the "I authed Nous but Scarf doesn't see it" bug: 1. `hasAnyAICredential()` (HermesFileService) only probed the `credential_pool.<provider>` shape in auth.json. OAuth-authed providers land under `providers.<name>.access_token` instead — Nous, Spotify, GH Copilot ACP, Qwen, Gemini all use that path. The chat banner kept showing "No AI provider credentials" even after a successful Nous sign-in. Now we probe both shapes; refresh-only entries (pre-mint OAuth flows) also count. 2. `CredentialPoolsViewModel` decoded only `credential_pool.*` and ignored `providers.*` entirely. New `oauthProviders` array surfaces them in a parallel "OAuth providers" section above the rotation pools — read-only, with token tail, expiry badge, portal URL, and "managed by `hermes auth add`" footnote so users know where the write path lives. 3. New `ProjectHermesShadowDetector` (ScarfCore) probes each registered project for a `<project>/.hermes/` directory. Hermes' CLI binds to the closest `.hermes/` as `$HERMES_HOME` when run from inside such a project — `hermes auth add nous` lands in the project's auth.json instead of `~/.hermes/auth.json` and Scarf's global probes never see it. Surfaced as a yellow Dashboard banner listing affected projects with badges for `auth.json` / `state.db` presence and a "Copy fix command" button that emits a one-liner consolidating auth.json into the global home. Read-only — no auto-migration; the user decides what to keep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4ffd353835 |
fix(diagnostics): treat config.yaml absence as informational, not failure
Same root cause as the connection-pill fix in
|
||
|
|
511726e2c0 |
feat(chat-resilience): iOS reconnect + snapshot fallback + paging + pill fix
Brings iOS chat to parity with Mac's reconnect behavior so a session survives phone-sleep, network handoffs, and SSH socket drops without losing the agent's work — Hermes already persists messages to state.db in real-time, the iOS app just had no resync path. Core changes (shared between Mac and iOS via ScarfCore): - ServerTransport.cachedSnapshotPath: fall back to the cached state.db snapshot when a fresh pull fails. HermesDataService surfaces this via isUsingStaleSnapshot + lastSnapshotMtime so views can render "Last updated X ago." Default opt-in via refresh(forceFresh: false); chat history reload passes forceFresh: true to refuse stale data. - HermesDataService.fetchMessages(sessionId:limit:before:): bounded pagination by id desc. Legacy unbounded overload deprecated. New HistoryPageSize constants centralize the budget. - RichChatViewModel.loadEarlier(): pages back through the current session via oldestLoadedMessageID + hasMoreHistory. iOS-only: - ChatController gains the Mac reconnect machinery: 5-attempt exponential backoff (1→16s) via session/resume → session/load, reconcileWithDB on success, "Resynced N new messages" toast. startACPEventLoop + startHealthMonitor extracted as helpers. - New NetworkReachabilityService (NWPathMonitor singleton). Suspends reconnect attempts while offline; kicks a fresh cycle on link-up. - ScarfGoCoordinator + ScarfGoTabRoot funnel scenePhase transitions to ChatController.handleScenePhase. On .active we verify channel health and reconnect if dead. - Draft persistence: UserDefaults keyed by (serverID, sessionID) survives force-quit. 7-day janitor at app launch. - Connection-state banner: .reconnecting and .offline render slim ScarfDesign-tinted strips above the message list. .failed keeps using the existing full-screen overlay. Bonus fix: - ConnectionStatusViewModel tier-2 probe now checks state.db instead of config.yaml. Hermes v0.11+ doesn't materialize config.yaml until the user changes a setting, so a freshly-installed working Hermes was being marked "degraded — config missing" indefinitely. state.db is the file Scarf actually depends on. Out of scope (deferred): APNs push notifications, BGTaskScheduler- based extended-background keepalive, offline write queue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
587c6c36c8 |
fix(diagnostics): sqlite3 probe with login-PATH + candidate fallback (#19)
@cmalpass's April 25 follow-up on #19: diagnostics reported "sqlite3 not installed or on system PATH" while sqlite3 was actually installed and Hermes was using it fine. Same false-negative class the `hermes` probe pre-fix had — a bare `command -v sqlite3` in the non-login SSH shell misses installs at /opt/homebrew/bin or /usr/local/bin when the user's PATH export lives in .zprofile (the typical Homebrew setup). The hermes probe was upgraded to source rc files + walk a candidate list; sqlite3 wasn't. Mirror the same pattern: - Move the sqlite3 detection AFTER the rc-source loop so the login PATH is in scope. - Add a standard-location fallback list: /usr/bin/sqlite3, /usr/local/bin/sqlite3, /opt/homebrew/bin/sqlite3, /opt/local/bin/sqlite3. - Use the resolved sqlite3 binary explicitly in the sqlite3CanOpenStateDB probe so it doesn't re-fail-by-PATH when the binary is at e.g. /opt/homebrew/bin. Falls back to bare `sqlite3` so the FAIL detail line still carries the real error. Hermes non-login probe stays as-is — that semantic ("is hermes on the un-enriched PATH?") is meaningful and we don't want to muddle it. Failure-hint copy on sqlite3Installed updated to spell out the new fallback behavior so users who still see FAIL get accurate guidance (install via package manager, OR symlink an existing binary into a location the probe checks). Closes the third and last open layer of #19. Layer 1 (104-byte ControlMaster path) was fixed in v2.0.2; layer 2 (pill / diagnostics disagreement) was fixed in v2.5.1 (#44). Ships in v2.5.2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
50fbbc6af6 | chore: Bump version to 2.5.1 v2.5.1 | ||
|
|
4776119e07 |
fix(ios-onboarding): hide Cancel on first-run onboarding (#55)
App Store Connect feedback: "Cancel button not working" on the
"Connect to Hermes" onboarding screen.
Confirmed root cause in RootModel.cancelOnboarding:
state = servers.isEmpty
? .onboarding(forNewServer: ServerID())
: .serverList
When the user has zero configured servers (the first-run case),
the conditional re-presented a fresh onboarding view. The button
fired, the state mutated, but the visible result was "tap Cancel,
get an identical screen" — indistinguishable from a dead button.
The defensive intent ("don't strand the user on an empty server
list") was reasonable, but the UX-as-shipped is worse than the
strand it tried to prevent — first-run TestFlight users see a
seemingly broken app.
Fix at the right layer: don't show Cancel when there's nowhere
to go.
- New `canCancel: Bool` parameter on OnboardingRootView (default
true). When false, the leading toolbar slot omits the Cancel
button entirely.
- RootView passes `canCancel: !model.servers.isEmpty`.
- RootModel.cancelOnboarding simplified — drops the defensive
`.isEmpty` re-loop branch, asserts the invariant in debug, and
in release still routes to `.serverList` (which renders an
empty-state with the "+ Add server" toolbar button) rather than
re-presenting onboarding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f72bf6e30b |
fix(connection-pill): unify pill probe with diagnostics over raw ssh (#44)
Issue #44: pill stuck on "Connected — can't read Hermes state" while Run Diagnostics shows 14/14 passing. Both code paths probe the same question (`[ -r ~/.hermes/config.yaml ]`) yet disagreed. Root cause: the pill called `transport.runProcess(executable: "/bin/sh", args: ["-c", script])` which routes through SSHTransport.remotePathArg quoting. That quoting double-quotes every argument to rewrite `~/` → `$HOME/`, mangling multi-line shell scripts containing `"$VAR"` references and nested quotes — the remote received a scrambled `if`-test and `$H/config.yaml` evaluated to `"/config.yaml"` (or worse), so tier-2 always read as failed. `RemoteDiagnosticsViewModel` already documented this exact bug and worked around it locally: invoke `/usr/bin/ssh ... -- /bin/sh -s` directly and pipe the script via stdin so it travels as opaque bytes. The pill never got the same treatment, hence the silent disagreement. The #53 granular-cause script I added a few commits back made the mangling worse — more $VARs, more `[ ! -e ]` tests, more nested quoting, all things that increase the runProcess quoting attack surface. Move the diagnostics workaround into shared ScarfCore code as `SSHScriptRunner.run(script:context:timeout:)`. Both the pill probe and the diagnostics view now use it, so they always see the same remote shell state. macOS-only via `#if os(macOS)` (Foundation.Process isn't on iOS); iOS callers never reach this surface anyway — ScarfGo uses Citadel-based SSH transports for its own flows. Other tidy-ups: - `ConnectionStatusViewModel` no longer holds a `transport` instance — the field was only used by the now-replaced runProcess path. - `RemoteDiagnosticsViewModel` loses ~120 lines of duplicated `runOverSSH` / `runLocally` / `controlDirPath` helpers; calls into `SSHScriptRunner.run` directly. Risk: low. The SSH path is the same shape that's been shipping in the diagnostics view since #19. The pill's 15s heartbeat gains a small forking-an-ssh-process overhead vs the ControlMaster- multiplexed runProcess, which is invisible at that cadence and amortized by ssh's own ControlMaster (the `-o ControlMaster=auto` options match SSHTransport's, so the multiplex socket is shared). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0bfae1227a |
fix(projects): context-aware Add Project sheet on remote servers (#54)
Pre-fix `AddProjectSheet` always rendered a Browse button backed by
NSOpenPanel — a Mac-local Finder dialog. On a remote SSH server
context, users would pick a Mac path (`/Users/alan/code/...`), the
path would land in the projects registry as the project's "remote"
working directory, and tool calls would fail at runtime because
that path doesn't exist on the Linux server.
Tier-1 fix:
- Pass active ServerContext into AddProjectSheet (was context-blind).
- Local context: Browse button unchanged. Pixel-identical to today.
- Remote context: hide Browse, surface a hint "Path on <server> —
must already exist on the server", add a Verify button that runs
context.makeTransport().stat(path) over the existing SSH transport
and renders inline:
spinner → checking
green ✓ → directory exists
yellow ⚠ → missing / file-not-dir / unreadable
- Path field's onChange resets stale verification so users don't see
a green check for a path they've since edited.
Tier 2 (full remote SFTP-backed picker that lets users navigate the
remote filesystem) is deferred — separate larger feature, ~200-300
lines and its own UX. Tier 1 unblocks remote project creation now,
which was the blocking bug.
Other 5 NSOpenPanel call sites audited — `TemplateInstallSheet:423`
likely has the same class of bug for template install destinations
on remote contexts; flagged in the issue body for a follow-up. The
other 4 (template-file picker, key-file picker, etc.) all pick
Mac-local artifacts and are correct as-is.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c312a565b6 |
fix(connection-pill): granular degraded reasons + inline hint popover (#53)
Pre-fix the connection-status pill collapsed every config.yaml read
failure to "Connected — can't read Hermes state", forcing users into
the heavy 14-probe Remote Diagnostics sheet to learn why. Multiple
distinct causes (Hermes not installed, not yet set up, permission
denied, profile mismatch) all read identically.
Probe script now emits granular `TIER2:1:<cause>` codes:
- no-home: ~/.hermes itself missing
- missing: config.yaml absent (typically pre-`hermes setup`)
- perm: file exists but unreadable by the SSH user
- profile:<name>: config missing AND ~/.hermes/active_profile points
at a non-default profile, so Scarf is reading the wrong directory
Status.degraded now carries (reason, hint, cause) instead of just a
short reason. The pill label shows the specific reason
("Hermes profile coder is active", "Hermes hasn't been set up yet",
etc.); clicking opens an inline popover with:
- A one-paragraph actionable hint
- A "Run diagnostics" button (existing path) and a "Retry" button
- For the profile case: a copy-paste affordance for
`hermes profile use default` to revert
Backwards-compatible: a remote that emits the legacy binary
`TIER2:1` parses to `.unknown` with the prior generic copy. No probe
script breakage on older Hermes installs.
Cross-link with #50 (local profile awareness) — this fix surfaces
the profile-mismatch class of bug for remote contexts. A proper
remote-side profile fix (HermesPathSet.defaultRemoteHome respecting
active_profile) is filed separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
afb1356b27 |
feat(ios-keychain): opt-in iCloud Keychain sync for SSH keys (#52)
Reddit-reported friction: every iOS device needed its own SSH key because Scarf hardcoded kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + kSecAttrSynchronizable=false on every Keychain write. Pairing iPhone + iPad meant onboarding twice and editing authorized_keys per device. Add an opt-in toggle in System tab → Security: - New SSHKeyICloudPreference (UserDefaults wrapper, default false so existing installs see no change on update). - KeychainSSHKeyStore.writeBundle now consults the preference: when on, items use kSecAttrAccessibleAfterFirstUnlock (no ThisDeviceOnly suffix — required for iCloud Keychain sync) + kSecAttrSynchronizable=true. - All read / list / delete queries unconditionally pass kSecAttrSynchronizable=kSecAttrSynchronizableAny so they match items regardless of sync state. Without this a flipped write would orphan items at the next read. - Public migrateAllItems(toICloudSync:) reads every stored bundle, deletes with Any, re-saves with target attributes. Idempotent. System tab Security section toggle: - Live migration on flip with a "Updating Keychain..." progress row. - Failure path reverts the toggle + surfaces the error inline rather than silently leaving the state inconsistent. - Footer copy explains the tradeoff (E2EE via iCloud Keychain; Advanced Data Protection keeps encryption keys on device). Out of scope: per-server-key sync override (M9 multi-server keys all sync or none); in-app key export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f9a288ac6c |
fix(ios-chat): dismissable keyboard via swipe + toolbar button (#51)
Pre-fix the iOS composer's TextField had no keyboard dismissal: no @FocusState, no scrollDismissesKeyboard, no keyboard accessory. With axis: .vertical + submitLabel: .send the Return key inserts a newline rather than committing, so once the keyboard rose it stayed up — hiding the top-trailing toolbar button on small phones. Three additive changes: - @FocusState private var composerFocused on ChatView, bound to the TextField via .focused($composerFocused). - .scrollDismissesKeyboard(.interactively) on the message list ScrollView so dragging the messages downward collapses the keyboard with the gesture (the standard iOS chat pattern the reporter explicitly named — "swipe away"). - ToolbarItemGroup(placement: .keyboard) accessory with a keyboard.chevron.compact.down "Done" button so dismissal is also available without a scrollable area (e.g. fresh empty-state chat before any messages exist). ScarfGo iOS only. Mac unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb33a39b42 |
fix(profiles): respect Hermes v0.11 active_profile (#50)
Hermes v0.11's `hermes profile` feature gives each profile its own
HERMES_HOME directory: the default profile is ~/.hermes, named
profiles live at ~/.hermes/profiles/<name>/. Each has its own
state.db, sessions/, config.yaml, .env, memories/, cron/, etc.
The active profile is recorded in ~/.hermes/active_profile.
Pre-fix Scarf hardcoded ~/.hermes and ignored active_profile, so
`hermes profile use coder` followed by a Scarf relaunch left Scarf
reading the wrong state.db — the new profile's chat sessions
silently never appeared.
Add HermesProfileResolver in ScarfCore that reads active_profile
and returns the effective home path. HermesPathSet.defaultLocalHome
becomes a static var backed by the resolver; every derived path
(stateDB, sessionsDir, configYAML, memoriesDir, cron paths, plugins,
gateway state, auth.json, etc.) automatically follows the active
profile through the existing `home + suffix` plumbing — no
downstream call sites need to change.
Resolver semantics:
- Absent / empty / "default" file → ~/.hermes (today's behavior)
- Valid profile name pointing to an existing dir → that dir
- Invalid name OR missing target → fall back to ~/.hermes with a
one-line os.Logger warning (so worst case is "Scarf shows what
it always showed")
Validation regex mirrors Hermes's hermes_cli/profiles.py exactly
([a-z0-9][a-z0-9_-]{0,63}). 5-second cache via OSAllocatedUnfairLock
keeps hot-path filesystem hits negligible.
SessionInfoBar gains a leftmost profile chip when not "default" so
users can see which profile Scarf is reading from. Tooltip explains
how to switch (`hermes profile use <name>` + relaunch).
Out of scope (deferred):
- In-app profile picker that writes to active_profile. Switching
mid-session is messy (open ACP processes are bound to whichever
HERMES_HOME spawned them); the reporter's "switch + restart" flow
is what we fix here.
- Remote SSH profile awareness. defaultRemoteHome stays "~/.hermes"
— remote profile selection is a separate, larger feature needing
its own UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e828538a2d |
docs(privacy): correct sandbox claim — Scarf macOS is unsandboxed by design
The privacy policy claimed "the macOS app is sandboxed where possible" and that uninstall removes "~/Library/Containers/com.scarf". Both wrong: - Per scarf/CLAUDE.md "Sandbox disabled. Scarf needs to read ~/.hermes/ directly." Scarf cannot ship App-Sandboxed because it needs direct filesystem access to ~/.hermes/ and the ability to spawn the hermes CLI — both forbidden by the App Sandbox. - ~/Library/Containers/com.scarf doesn't exist for an unsandboxed app; data lives at ~/Library/Caches/scarf/, ~/Library/Preferences/com.scarf.app.plist, and ~/Library/Application Support/com.scarf/. Replaced both with accurate text. Also clarified that ScarfGo on iOS DOES run inside the standard iOS sandbox — no special entitlements beyond Keychain. The wiki mirror at .wiki-worktree/Privacy-Policy.md got the same fix in the corresponding wiki audit commit. Caught during the v2.5 wiki audit pass. Will re-publish to gh-pages in v2.5.1 alongside other queued doc updates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
051f3bf80c |
feat(chat): density preferences for tool cards, reasoning, font (#47, #48)
Three Scarf-local @AppStorage-backed preferences in
Settings → Display → Chat density. All defaults match today's UI;
existing users see no change until they opt in.
- Tool calls: Full card (today) / Compact chip / Hidden
- Compact: one-line tappable chip per call (icon + name + status
dot). Tap focuses the call so the right-pane inspector opens
with full args + result, same as today's inline expand.
- Hidden: per-call rows skipped entirely. The MessageGroupView
toolSummary pill ("Used 5 tools (3 read, 2 edit)") becomes
the only chrome AND becomes tappable — clicking focuses the
first call so per-call duration / exit code remain reachable
via the inspector. Pill is now shown for any call count > 0
in hidden mode (was > 1) so the inspector path is always
available. Issue #47.
- Reasoning: Disclosure box (today) / Inline (italic) / Hidden
- Inline: italic foregroundFaint caption inline above the reply
with a 9pt brain prefix. No box, no border. Same data, far
less vertical space.
- Hidden: reasoning text not rendered. Per-message tokenCount
(which the disclosure label was duplicating) stays in the
metadataFooter so token telemetry isn't lost. Issue #48.
- Chat font size: 85%–130% slider (5% step) applied via
.environment(\.dynamicTypeSize, ...) on RichChatView's root,
scaling message list / input bar / session info bar / inspector
pane together. Reset button restores 100%. Issue #48.
Telemetry preservation (the user-stated constraint):
- Per-turn stopwatch, per-message tokenCount, finish reason, and
message timestamp remain in the bubble metadataFooter in every
mode.
- SessionInfoBar input/output/reasoning tokens, cost USD, model,
project, git branch, and started-at relative time are unchanged
by every density setting.
- Per-call duration + exit code stay reachable via the inspector
pane in compact and hidden modes.
Out of scope (called out in the plan):
- Context-fill widget — Hermes v0.11 doesn't expose context_used
/ context_total per session. Approximating from messages.tokenCount
+ a static window table would be wrong-on-purpose; defer until
Hermes ships the canonical field.
- iOS — ScarfGo already renders both surfaces compactly. Both
issues reference Mac.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
558970a09a |
perf(chat-ios): mirror Mac equatable short-circuit on ScarfGo bubbles (#46)
ScarfGo's chat is a separate rendering path: LazyVStack + ForEach(controller.vm.messages) with a private MessageBubble struct (not the shared MessageGroupView/RichMessageBubble used on Mac). The Mac fix's Equatable conformances therefore didn't propagate. Without short-circuiting, every visible bubble re-evaluates body on each streamed ACP chunk because the @Observable VM's `messages` mutation invalidates anyone reading it — and each bubble's `ChatContentFormatter.segments` + `AttributedString(markdown:)` are both O(content) per render. LazyVStack already keeps off-screen bubbles dormant on iOS, but the 5–10 visible bubbles re-parsing on every chunk is enough to bog down a long turn on phone hardware. Add Equatable to MessageBubble (id-keyed, with content/reasoning/ toolCalls.count compared only for the streaming bubble id==0) and apply .equatable() at the ForEach call site. Settled bubbles short- circuit body re-eval; the streaming bubble still redraws per chunk. Note: the trailing-group patch helper (Mac fix part 2) already benefits iOS as a side effect — buildMessageGroups() is no longer called per chunk, and even though iOS doesn't read messageGroups directly, the elided rebuild is still wasted work avoided. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8d9de4c576 |
perf(chat): stop O(n)-per-token re-render of settled bubbles (#46)
Long chats progressively bog down and eventually crash because every streamed ACP token triggers a full messageGroups rebuild plus a body re-evaluation of every MessageGroupView and RichMessageBubble — even the n-1 settled groups that haven't changed. Three changes cap per-chunk work at "patch the trailing group + re-render the streaming bubble": - MessageGroupView and RichMessageBubble are now Equatable, applied via .equatable() in the ForEach. Settled groups (no streaming message inside) short-circuit body re-evaluation entirely; the streaming group compares content/reasoning/toolCalls.count so it still redraws on every chunk. - RichChatViewModel.upsertStreamingMessage no longer calls buildMessageGroups() per chunk. New patchTrailingGroupForStreaming mutates only the trailing group's assistant entry in place. The 9 other call sites of buildMessageGroups() are untouched — they cover structural events (user message, tool-call complete, finalize, session resume) where group boundaries can actually change, and a full rebuild is correct there. - MessageGroup.toolKindCounts is now a model property (was a MessageGroupView computed prop that re-walked O(m × k) per body render). Lives behind the Equatable short-circuit. - ToolCallCard.formatJSON cached via .task(id: call.callId) so JSON pretty-printing runs once per card lifetime instead of on every expand/collapse + every neighbour's re-render. Seeded with raw arguments to avoid a first-frame empty-text flicker. - ToolResultContent.lines/preview cached via .task(id: content) — the prior pair of computed properties split content on \n twice per render, expensive on long command/file output. Skipped from the original plan: the per-message parse cache (rendered moot once Equatable already short-circuits settled bubbles) and the LazyVStack switch (deferred — RichChatMessageList comments flag scroll-anchor regression risk; revisit separately if needed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e0f0fad192 |
fix(release): post-package verification + non-destructive recovery docs
Add codesign --verify --strict --deep + spctl --assess on the extracted distribution zip inside build_variant() so any seal regression introduced by ditto / staple / future pipeline tweaks fails the release before users see "damaged" errors. Document the non-destructive recovery path in README and explicitly warn against `xattr -rc` and `codesign --force --deep --sign -` (issue #49 — both corrupt Sparkle.framework's nested XPC service / Updater.app signatures even when the outer app remains intact). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
80a4d23974 |
docs(readme): shrink ScarfGo gallery thumbs 180->140px so 5 fit in one row
GitHub's README content column is ~770px wide. 180px x 5 + spacing overflowed and wrapped 4+1 (the System tab dropped to its own line), breaking the gallery's "thumbnail strip" reading. 140px x 5 lands at ~700px including spacing, comfortably within the column. No content change to the screenshots or paths — just the width attr. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d95ef61e13 |
docs(readme): ScarfGo screenshot gallery under the v2.5 What's New section
Five 1284x2778 simulator captures from the iPhone 17 Pro Max stock sim, dropped in at assets/screenshots/scarfgo-*.png. The README gallery is HTML inside the existing Markdown — five thumbnails at 180px wide, centered, each wrapped in an <a href> pointing back at the same file so a click opens the full-resolution PNG via GitHub's asset viewer (the closest thing the README format supports to a lightbox). Order matches the user flow: Servers list -> Chat with Hermes -> Project dashboard (Site Status Checker template, dogfooding the catalog) -> Skills browser -> System tab. One italic caption underneath labels the screens in order. 3.4 MB total. iPhone 17 Pro Max is the canonical capture device for v2.5; the App Store listing will use the same shots once they need cropping/framing for Apple's screenshot specs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
988ce5df5a |
docs(readme): rename hero icon to bust GitHub's raw-asset CDN cache
The previous commit replaced icon.png on disk with the rust v2.5 artwork, but GitHub's raw-asset CDN was still serving the cached purple PNG to README viewers (~5 min TTL — but in practice longer under sustained traffic). Renaming the asset forces a fresh fetch on every README render, which is the reliable cache-bust. icon-v2.5.png is bit-identical to the prior icon.png (md5 match against the Mac app icon set's 512x512). The version in the filename is intentional — when v2.6 ships with a different icon, we'll cycle to icon-v2.6.png and the same cache-bust applies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3bca8a6e55 |
docs(readme): swap home-page hero icon for the v2.5 rust app icon
icon.png at the repo root drives the centered hero block on the GitHub README. It was still the pre-rust design from v2.0; replaced with the rust ScarfDesign 512x512 sourced from the Mac app icon set so the home page matches the in-app branding now that v2.5.0 has shipped. Also bumps the source resolution from 256x256 to 512x512 — the README displays it at 128x128, so retina + HiDPI displays now render crisply without losing the asset's intent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b5f4f65ffe | chore: Bump version to 2.5.0 v2.5.0 | ||
|
|
b474286bfe |
build(ios): iPhone-only — drop iPad, macCatalyst, Designed-for-iPhone-iPad
Locks the `scarf mobile` target to iPhone before TestFlight submission: - TARGETED_DEVICE_FAMILY 1,2 -> 1 (iPhone only) - SUPPORTED_PLATFORMS constrained to iphoneos + iphonesimulator - SUPPORTS_MACCATALYST = NO - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO Applied symmetrically to Debug + Release configs. iPad layout via .tabViewStyle(.sidebarAdaptable) hasn't been smoke-tested and was explicitly out of scope for v2.5; flipping the device-family flag prevents Apple's review tooling from picking up an unsupported device class. Mac and visionOS are similarly excluded — ScarfGo is an iPhone-only companion in v2.5; the iPad / Catalyst story is its own future release. Both targets build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b1e2fc5dcd |
docs(v2.5): home-page focus + RELEASE_NOTES under-the-hood + App Store metadata
README: strip the "Previously, in 2.3" subsection per release direction — the home page is now a single-version forward-looking surface with prior releases linked off to the wiki Release Notes Index. Promote the ScarfGo TestFlight callout to its own subsection with the public link (testflight.apple.com/join/qCrRpcTz) embedded inline. Add a "Connect ScarfGo to your Hermes server" five-step walkthrough between What's New and Multi-server, mirroring OnboardingRootView's state machine so users can follow it cold without opening the wiki first. RELEASE_NOTES: extend the Under-the-hood section with the iOS-side maintenance work that landed in the last 48h — Citadel executeCommandStream rewrite (preserves stdout on non-zero remote exit; was eating Skills hub Browse output), inline PATH= prepend on every iOS runProcess (Citadel's raw exec channel doesn't source shell rc files), fd-leak cleanup across the three transports + ProcessACPChannel, ServerLiveStatus 10/30/60/120/300s exponential backoff for unreachable remotes, and the print() -> os.Logger sweep. APP_STORE_METADATA.md: full App Store Connect copy ready for paste — app name, subtitle, promotional text (153/170 chars), 2873/4000-char description in three paragraphs (what / features / privacy), brand-safe keywords (85/100 chars; no competitor product names), support / marketing / privacy URLs, category, age rating, 1150/4000-char "What's New" text. Screenshots flagged as out-of-scope for this prep pass — user captures from the simulator before App Store submission. TestFlight checklist remains the canonical doc for the in-flight beta submission. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
87fcbad1ac |
fix(ios): App Store validation — strip 1024 alpha + drop iCloud entitlements
Apple's TestFlight upload validator rejected v2.5 with two errors; fixing both for the next archive. 1. **Invalid large app icon** (alpha channel). `AppIcon.appiconset/AW Mac OS Applications-macOS-Default-1024x1024@1x.png` was RGBA — Apple rejects any 1024×1024 marketing icon with an alpha channel. Composited the icon onto a solid white background via PIL and resaved as RGB PNG. `sips -g hasAlpha` now reports `no`. The file's design is solid edge-to-edge, so the white-fill is invisible — no visual change. 2. **Invalid Code Signing Entitlements** (`com.apple.developer.icloud-container-environment` empty string). `Scarf_iOS.entitlements` had `com.apple.developer.icloud-services = [CloudKit]` + `com.apple.developer.icloud-container-identifiers = []`. Xcode's signing phase synthesises `com.apple.developer.icloud-container-environment` from this combo, and with no container identifier the value lands as empty — which Apple's validator rejects. Per the privacy policy I drafted in v2.5 ("no iCloud Keychain sync, no cloud accounts") Scarf doesn't actually use iCloud, so removing the iCloud entries is the correct fix. Dropped both `com.apple.developer.icloud-services` and `com.apple.developer.icloud-container-identifiers`. Kept `aps-environment = development` — push capability stays declared but gated off via `NotificationRouter.apnsEnabled = false` until the cert + Hermes-side sender land. iOS scheme builds clean post-fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
63c5d13bec |
chore: fd-leak cleanup, os.Logger conversion, status-poll backoff
Pre-release maintenance pass picked up across both targets while
debugging the iOS Browse fix:
- LocalTransport / SSHTransport: close the parent's copy of every
pipe write end after spawn so EOF reaches the reader once the child
exits, and explicitly close read ends after draining. Was leaking
one fd per `runProcess`/`streamLines` invocation under load.
- ProcessACPChannel: also close stdout/stderr write-end fds on
channel teardown — same pattern, ACP sessions can churn on long
reconnect loops.
- HermesDataService / HermesLogService / ProjectDashboardService:
replace remaining `print("[Scarf] ...")` debug statements with
os.Logger calls (subsystem="com.scarf"), matching the global rule
that production code uses Logger and `print()` is reserved for
previews + test helpers.
- ProjectDashboardService / IOSCronViewModel: drop redundant
`fileExists` guards before `createDirectory` — the operation is
already mkdir -p across every transport, so the extra round-trip
was pure latency on remote hosts.
- scarfApp.swift: server-status sidebar probe now uses an exponential
backoff (10s → 30s → 60s → 120s → 300s) when consecutive probes
fail, resetting on the first full success. Previously a registered
remote going unreachable hammered pgrep + gateway_state.json every
10s indefinitely; now offline servers settle to a 5-min cadence
while live servers stay snappy.
- Localizable.xcstrings: routine .strings catalog refresh — stale
entries for removed UI strings, picked up new "Stored under
quick_commands:..." subtitle wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
850fa7a697 |
fix(ios): preserve hermes output on non-zero exit + extend remote PATH
Two related fixes that together restore Skills hub Browse / Search on iOS over Citadel SSH. CitadelServerTransport.asyncRunProcess was using `executeCommand`, which throws `CommandFailed` and discards the captured ByteBuffer when the remote process exits non-zero. `hermes skills browse` happens to print its full table and then exit non-zero on some hosts, so iOS got nothing while Mac (Foundation Process) got the full output with exitCode=1. Drive `executeCommandStream` directly so stdout + stderr are drained regardless of outcome, then catch `SSHClient.CommandFailed` to recover the actual exit code. Network/channel-level failures still report -1 so callers can distinguish them from a clean non-zero remote exit. Citadel's raw exec channel also doesn't source the user's shell rc files, so non-interactive sessions land with a stripped PATH (typically just /usr/bin:/bin). pipx installs `hermes` at `~/.local/bin/hermes`, and many of hermes's sub-tools (git, curl, python) live in homebrew prefixes that the remote sshd would otherwise add via login-shell init. Mac's OpenSSH sshd handles this transparently; Citadel does not. Inline PATH=$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH on every runProcess invocation so bare `hermes` resolves AND any subprocess it spawns can still find its tools. SkillsViewModel.finishBrowse now surfaces the actual stderr/stdout snippet when the CLI exits non-zero, instead of a canned "Browse failed" banner. ANSI-stripped + box-drawing-stripped so the message stays readable in the one-line banner. Made diagnosing the underlying PATH issue much easier and is a net UX improvement going forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
21e3cc9361 |
feat(ios): rust page background + dashboard switch-server button
Sweeps the rust ScarfDesign page background onto the screens that were still rendering against the iOS default: Skills/Hub, Skills/Updates, all three project sub-views, and Skill Detail. Lists adopt .scrollContentBackground(.hidden) + ScarfColor.backgroundPrimary, with .listRowBackground(ScarfColor.backgroundSecondary) on rows so the Mac-style elevated-card density carries through. Adds a "Switch server" toolbar button to Dashboard's top-right, threaded through ScarfGoTabRoot from the connected-server host. One tap soft- disconnects and returns to the server list — same code path the System tab already exposes, just reachable without first navigating away from Dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
295f2dfefc |
feat(ios): Mac-style page backgrounds + Dashboard + Chat redesign
iOS now uses ScarfColor.backgroundPrimary throughout instead of the default iOS systemGroupedBackground. List-based screens add .scrollContentBackground(.hidden) + the rust background underneath; list rows use ScarfColor.backgroundSecondary as their card surface. Applied to: Projects, Memory, Cron, Settings, Skills/Installed, and the Servers root. Dashboard rewritten in Mac-style cards (no more native iOS list): - ScrollView + VStack with rust background - Activity stat grid (2-col LazyVGrid) with bordered rust-tinted cards: Sessions / Messages / Tool Calls / Tokens (with in/out sub- label). - Recent sessions card — bordered, ScarfColor.backgroundSecondary, inline session rows with 1px dividers, "See all" nav to Sessions sub-tab. - Error banner styled with ScarfColor.warning tinted card per Mac. - Sessions sub-tab keeps a List view but renders against rust background with ScarfColor.backgroundSecondary row backgrounds. Chat redesigned to match the Mac chat reference: - User bubble: rust accent fill with ScarfColor.onAccent text and uneven rounded corners (top/bottom-leading + top-trailing 14px; bottom-trailing 4px) — visually pinches to the sender side, same as Mac. - Assistant bubble: rust gradient sparkles avatar tile (24x24) alongside a ScarfColor.backgroundSecondary bordered card. - ToolCallCard: kind-tinted border + uppercase tracked label (READ/EDIT/EXECUTE/FETCH/BROWSER) using ScarfColor.success/info/ warning/Tool.web/Tool.search; expanded JSON in a bordered ScarfColor.backgroundSecondary panel. - ReasoningDisclosure: warning-tinted card with REASONING uppercase label. Both Mac (scarf) and iOS (scarf mobile) schemes build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
de611c5343 |
feat(ios): adopt ScarfDesign across the iOS app
AccentColor.colorset repointed to BrandRust hex (light + dark) so the tab bar, every .tint, every default button, and every navigation accent across all 5 tabs read rust automatically. Single-line fix, biggest visible change. ScarfDesign now imported across all 27 iOS view files. Color sweep applies the same patterns as the Mac side, with two iOS-specific deviations documented in CLAUDE.md: - ScarfPageHeader is NOT retrofitted onto iOS tab roots. iOS uses .navigationTitle(...) + .navigationBarTitleDisplayMode(.large) as its native page-header pattern; stacking ScarfPageHeader on top creates double titles. ScarfPageHeader is reserved for sub-views without a native large-title bar. - Only .borderedProminent → ScarfPrimaryButton. .bordered and .plain stay native because .bordered is the iOS convention for non-primary buttons and inherits rust through AccentColor automatically. Dynamic Type policy (locked + documented in CLAUDE.md): preserve .font(.headline)/.body/.caption semantic tokens for body copy, list rows, error messages, and chat content (anything read for content). Use ScarfFont only for status badges, chip labels, intentional fixed- size display elements. Mass-swapping ScarfFont on iOS would regress accessibility for users on .accessibility2 / .xSmall. Files touched (27 view files + AccentColor + CLAUDE.md): - Color sweep: .foregroundStyle(.secondary) → ScarfColor.foregroundMuted, Color(.secondarySystemBackground) → ScarfColor.backgroundSecondary, status colors (.orange/.green/.red) → ScarfColor.warning/success/danger in: Dashboard, Skills (root + Installed + Hub + Updates + Detail), Projects (root + Detail + Sessions + Site + 8 widgets), Memory (List + Editor), Cron, Settings (root + Editor), Servers, Chat (root + Picker + Slash browser), Onboarding. - Primary button swap (5 files): Chat, Projects/Sessions, Skills/ Updates, Skills/Hub, Onboarding. - CLAUDE.md: appended "iOS Dynamic Type policy" + "iOS page chrome" guidance under the existing Design System section. Both Mac (scarf) and iOS (scarf mobile) schemes build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
23dd8becb9 |
polish: tokenize remaining sheets, page headers, and widgets
Phase 1 — Page headers for the 9 non-mockup feature views: Skills, Gateway, Platforms, Personalities, QuickCommands, CredentialPools, Plugins, Webhooks, Profiles. Each now ships a ScarfPageHeader with title + subtitle + tokenized trailing actions (ScarfPrimary / Secondary / Ghost buttons), wrapped in .fixedSize so labels can't wrap at narrow widths. Outer .background(ScarfColor.backgroundPrimary). Phase 2 — Modal sheets: ModelPickerSheet, NousSignInSheet, RenameProjectSheet, MoveToFolderSheet, the five Templates sheets (TemplateInstall / TemplateConfig / TemplateExport / TemplateUninstall / ConfigEditorSheet), three MCPServer sheets (AddCustom / Editor / PresetPicker), AddServerSheet, ManageServersView, MissingServerView. .font(.headline) -> .scarfStyle(.headline); .buttonStyle(.borderedProminent) -> ScarfPrimaryButton(); raw text fields where touched -> ScarfTextField; cancel buttons -> ScarfGhostButton. Phase 3 — All 12 platform setup views (Discord / Email / Feishu / HomeAssistant / IMessage / Matrix / Mattermost / Signal / Slack / Telegram / Webhook / WhatsApp). Connect buttons swapped to ScarfPrimaryButton. Phase 4 — All 7 project dashboard widgets (Chart / List / Progress / Stat / Table / Text / Webview). .font(.caption) -> .scarfStyle(.caption); .background(.quaternary.opacity(0.5)) -> ScarfColor.backgroundSecondary; RoundedRectangle(cornerRadius: 8) -> ScarfRadius.lg. Phase 5 — Project sub-views: ProjectSessionsView, ProjectsSidebar, ProjectSlashCommandsView. Same token sweep. Phase 6 — Common chrome: - LoadingOverlay: .font(.callout/caption) -> .scarfStyle; secondary foreground -> ScarfColor.foregroundMuted; window-background -> ScarfColor.backgroundPrimary. - ServerSwitcherToolbar: status dot + label tokenized. - ConnectionStatusPill: status colors -> ScarfColor.success/warning/ danger; error sheet header -> ScarfPrimaryButton retry. Build green on both Mac (scarf) and iOS (scarf mobile) schemes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
41769e289c |
feat(chat): port the 3-pane chat layout + ScarfDesign telemetry
Sessions list (264 px) | transcript (flex) | inspector (320 px) per design/static-site/ui-kit/Chat.jsx and the ScarfChatView reference. Built over the real ChatViewModel + RichChatViewModel — live ACP streaming pipeline untouched. HermesToolCall gains optional duration / exitCode / startedAt fields (backwards-compatible, nil defaults; not Codable). RichChatViewModel populates them on ACP toolCallStart / toolCallUpdate; mutates the streaming entry before finalize so the persisted call carries telemetry. Sessions loaded from state.db gracefully render "—" when nil. ChatViewModel gains focusedToolCallId + a focusedToolCall computed helper. ToolCallCard takes onFocus / isFocused — single click both focuses the inspector and toggles inline expansion (two paths to the same data per locked decision). Border weight + tint bump signal the focused card. Sessions pane: project filter (matches Sessions feature semantics), search field, project chips per row, right-click rename + delete via hermes sessions rename / delete --yes. Recent-sessions limit bumped 10 -> 50 so the project filter has data. loadRecentSessions commits all four observables in a single MainActor batch to eliminate the brief flash on session switch. ChatView toolbar's redundant Session menu trimmed (left pane is canonical). ChatTranscriptPane wraps existing SessionInfoBar + RichChatMessageList + RichChatInputBar without owning new state. RichChatView body becomes a fixed 3-pane HStack — ViewThatFits was downgrading to transcript-only when transcript content widened mid-load. Inspector: STATUS / ARGUMENTS / TELEMETRY / PERMISSIONS in the Details tab; STDOUT in dark mono panel under Output; full JSON envelope under Raw. Footer Re-run is stubbed (TODO: /retry path); Copy puts the raw JSON envelope on the clipboard. ProjectSlashCommandsView: empty-state ContentUnavailableView now centers in the full pane via .frame(maxWidth/maxHeight: .infinity). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8a2d89654b |
feat(design): adopt ScarfDesign system across Mac UI
Add a typed design-system package (Packages/ScarfDesign) with rust-tone color tokens, type scale, spacing/radius tokens, ScarfPageHeader and component primitives (ScarfCard, ScarfBadge, ScarfTextField, ScarfSectionHeader, ScarfDivider, four button styles). Both Mac and iOS targets `import ScarfDesign`. Sidebar redesigned per design/static-site/ui-kit/Sidebar.jsx — glassy translucent background, 224 px width, app-icon header with server pill, custom tokenized rows with rust accent-tint when active, footer with live Hermes-running indicator (wired to ServerLiveStatusRegistry). 14 mockup-backed feature screens redesigned: Settings, Dashboard, Sessions, Memory, Chat (visual sweep), Activity, Cron, Insights, MCPServers, Health, Logs, Tools (full); Projects light-touch. Non-mockup features inherit rust through AccentColor.colorset repoint. Mac AppIcon.appiconset replaced with the rust set. AccentColor.colorset repointed to BrandRust hex (light + dark variants). Visual sweep: every multi-button page-header / action-bar cluster now wraps in .fixedSize(horizontal: true, vertical: false) so labels can't wrap letter-by-letter at narrow widths (regression seen on the MCP detail pane with 4 buttons). Follow-ups landed: - Sidebar Hermes-running probe wired to per-window ServerLiveStatusRegistry (no more placeholder green). - Sessions: today filter predicate (isDateInToday(startedAt)); pill count reflects real count. Starred stays a no-op pending an upstream pinned/starred field on HermesSession. - Dashboard: Recent activity column rendered alongside Recent sessions in a ViewThatFits 2-col grid. Populated from HermesDataService.fetchRecentToolCalls(limit:) flattened to ActivityEntry. ActivityEntry gains a public memberwise init. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f04d95c960 |
refactor(mac-skills): delegate loadSkills to shared SkillsScanner (Phase E)
Cleanup pass: HermesFileService.loadSkills() was duplicating walk logic that the new ScarfCore SkillsScanner now owns. Replaced the ~38-line implementation with a one-line delegation. Removed: - HermesFileService.loadSkills() walk body (38 lines). - HermesFileService.parseSkillFrontmatter (24 lines, supersedes by SkillFrontmatterParser.parseV011Fields). - HermesFileService.parseSkillRequiredConfig (24 lines, superseded by SkillFrontmatterParser.parseRequiredConfig). The remaining HermesFileService surface (loadSkillContent, saveSkillContent, isValidSkillPath) is unchanged — those are Mac- target-specific guards on file paths that don't fit ScarfCore. Tab enum audit: searched for orphan `.memory` / `.more` references under Scarf iOS/. None found — the worktree refactor cleanly migrated every selectedTab assignment to the new 5-tab vocabulary. Verified: ScarfCore 197 tests + 28 catalog tests + Mac + iOS builds all green (Phase F gate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
26c034ea6f |
feat(ios-skills): port v0.11 features to new file structure (Phase D)
Re-port the four v0.11 iOS Skills features that lived in the now- deleted Skills/SkillsListView.swift into the new Installed/SkillDetailView + Skills/SkillsView surfaces. iOS Components/FlowLayout.swift (NEW, promoted helper): - 50-line struct conforming to SwiftUI's Layout protocol; wraps subviews onto multiple lines on overflow. Built-in API, no third- party dep. Originally inline in the deleted SkillsListView; promoted so multiple iOS views can share without duplicating ~30 lines. iOS Skills/Installed/SkillDetailView.swift (extend): - design-md npx prereq banner: yellow "Prerequisite missing" section triggered by .task(id: skill.id) probing `which npx` via SkillPrereqService when the selected skill is design-md. - Spotify info row: green "Authentication" section pointing users at the Mac sheet or shell for OAuth — phone OAuth flows are deferred in v2.5. - SKILL.md frontmatter chip rows: three sections (Allowed tools / Related skills / Dependencies) using a chipRow helper backed by the shared FlowLayout. Each section hides itself when its HermesSkill field is nil — old skills without v0.11 frontmatter show none of these. iOS Skills/SkillsView.swift (extend): - "What's New" pill at the top of the tab (above the sub-tab picker). Driven by SkillSnapshotService.diff() against the per- server last-seen snapshot. First-load primes silently so users don't see "everything is new!" noise on day one. - Recomputes on .task and .refreshable. - "Seen" button persists the current set + dismisses. Verified: iOS build succeeds. The chip-row data path is now end-to-end (SkillsScanner → HermesSkill → SkillDetailView chips) and the snapshot pill matches the Mac SkillsView placement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
84b033814b |
feat(scarfcore): SkillsScanner populates v0.11 frontmatter (Phase C)
Post-merge follow-up: the new SkillsScanner constructed HermesSkill with `requiredConfig` only — leaving `allowedTools` / `relatedSkills` / `dependencies` (added in my v0.11 Phase 3.3) as nil. Detail-view chip rows would render empty. SkillFrontmatterParser: - New `parseV011Fields(_:) -> (allowedTools:, relatedSkills:, dependencies:)` reader. Reuses HermesYAML.parseNestedYAML to extract the three lists from the SKILL.md frontmatter region between `---` markers. Returns nil-everything when the file has no frontmatter or the fields are absent / empty — chip rows hide. - Existing `parseRequiredConfig(_:)` unchanged. SkillsScanner: - Reads `<skill>/SKILL.md` opportunistically (after the `<skill>/skill.yaml` read), parses v0.11 frontmatter, passes the three optional arrays into the HermesSkill constructor. - Old skills without SKILL.md or without frontmatter keep nil and scan keeps working. Tests: - 5 new SkillFrontmatterParserTests cases covering happy path, partial fields, no frontmatter, empty fields, empty input. - 10 total tests for the parser; all green. Verified: ScarfCore builds clean. The chip-row data path is now end-to-end (scan → HermesSkill → detail view) for both Mac and iOS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3d4a6a3a75 |
Merge branch 'claude/pedantic-mcnulty-bac7cc' (iOS UI refactor)
Brings the major iOS UI refactor into scarf-mobile-development on top
of the v0.11 work that landed after the merge base (commit
|