When an OAuth provider's refresh token was revoked, Hermes printed
"Refresh session has been revoked. Run `hermes model` to re-authenticate."
to stderr but Scarf swallowed it — the user saw a typing indicator that
silently disappeared with no banner, no system message, no actionable
hint. The error classifier had no pattern for OAuth revocation.
- `ACPErrorHint.classify` now returns a `Classification` struct
carrying the hint plus an optional `oauthProvider` name. New
patterns match "Refresh session has been revoked", "re-authenticate",
and 401-with-OAuth-provider-name (whole-word so `anthropicapi`
doesn't false-match `anthropic`). Provider extraction lets the UI
dispatch the right re-auth flow.
- Chat error banner ([ChatView.swift]) gains a "Re-authenticate" button
when an OAuth provider was identified — sets
`AppCoordinator.pendingOAuthReauth` and routes to Credential Pools.
- Credential Pools view consumes the hand-off slot to auto-present
AddCredentialSheet seeded with the affected provider, AND adds a
per-row "Re-authenticate" button on every OAuth provider so users
who go straight there don't have to retype the provider name.
- `AddCredentialSheet` accepts an optional `initialProvider` that
pre-fills providerID + authType=.oauth; the existing Nous-vs-PKCE-
vs-CLI gate dispatches re-auth identically to first-time setup —
reuses the same `OAuthFlowController` / `NousSignInSheet` plumbing,
no new flow code.
Verification: ScarfCore 221/221 (incl. new
errorHintsClassifyOAuthRefreshRevoked covering the four patterns +
word-boundary guard); Mac app builds clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs in the Mac chat composer's placeholder overlay:
* The "Message Hermes… / for commands · drag images to attach" hint had
no width constraint, so on narrower window geometries it visibly
overflowed past the rounded TextEditor boundary. Add `lineLimit(1)`,
`truncationMode(.tail)`, and `frame(maxWidth: .infinity, alignment:
.leading)` so it ellipsizes inside the field instead.
* The opacity formula `text.isEmpty ? 1 : 0` only hid the placeholder
once content was typed, not when the field gained focus. Standard
NSTextField / UITextField semantics clear the placeholder on focus.
Switch to `(text.isEmpty && !isFocused) ? 1 : 0` so the hint
disappears the moment the user clicks into the field.
The opaque-background ghosting mitigation from #65 is preserved
unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovered during Layer B work that XCUITest runners are sandboxed:
they can read ~/.hermes/ but writes throw NSFileWriteNoPermissionError.
That kills the SCARF_HERMES_HOME-based isolation pattern for UI tests —
snapshot/restore from inside the runner can't work. Pivot:
- Layer B drives the real ~/.hermes the dev Mac is already running
against. The harness assumes a working Hermes install (XCTSkip if
the binary isn't there). Cleanup is via the app's own UI flows
(which have full disk access), not direct file I/O. Layer A keeps
its env-var seam — those tests run inside the host app's address
space and write freely.
- SwiftUI's WindowGroup(for: ServerID.self) doesn't auto-surface a
window on a fresh XCUIApplication.launch(). The harness sends ⌘1
(the "Open Server → Local" menu shortcut wired in scarfApp.swift's
OpenServerCommands) to take the same code path real users hit via
Dock click.
- Real user home resolved via getpwuid(getuid()) rather than
NSHomeDirectory(), which inside the sandboxed runner returns
~/Library/Containers/com.scarfUITests.xctrunner/Data.
- 8 accessibility IDs added on the install path so the next iteration
can drive the full Templates → Install from URL → Parent dir →
Confirm Install flow without depending on view-tree label scraping:
templates.toolbar.menu, templates.installFromFile,
templates.installFromURL, templates.installURL.field,
templates.installURL.confirm, templateInstall.parentDir.field,
templateInstall.parentDir.continue, templateInstall.confirmInstall.
- TestModeFlags.shared.isTestMode now gates UpdaterService —
--scarf-test-mode launches Sparkle inert so update prompts don't
pop on top of an XCUITest-driven window. Production launches
unchanged.
FixtureHermesHome.swift removed — the fixture-tmpdir approach is
abandoned in favour of using the real installation. Layer A's
SCARF_HERMES_HOME tests still pass; they just don't need a populated
home to exercise path derivation.
Verification: scarfTests 124/124, ScarfCore 220/220, Layer B smoke
1/1 (after fresh build — XCUITest is sensitive to stale binaries).
catalog.py --check still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First pass of the dogfooding-templates initiative. Each pre-release cycle
ships one new official `.scarftemplate` and uses installing/exercising
that template as the regression test. v1 lands the harness scaffolding
plus the first template under it.
- HackerNews Daily Digest template (`templates/awizemann/hackernews-digest/`):
config-driven (min_score / max_items / topics) cron-only template.
No secrets — keeps the harness minimal until the fake-Keychain shim
lands. Bundle validates against `tools/build-catalog.py`; entry added
to `templates/catalog.json`.
- `SCARF_HERMES_HOME` env-var override at `HermesProfileResolver` —
the seam every Layer-B test relies on to drive Scarf against an
isolated Hermes home. Bypasses cache + active_profile lookup; rejects
relative paths. 5 unit tests + 3 ServerContext integration tests.
- `TestModeFlags.shared.isTestMode` — reads `--scarf-test-mode` once
from `CommandLine.arguments`. Wiring only; gating sites (Sparkle,
capability probe, first-run walkthrough) land as Layer-B exercises
them.
- Layer A (`scarf/scarfTests/TemplateE2ETests.swift`): parses + plans
the shipped HN bundle the way the app does at install time;
asserts manifest, config schema, dashboard widgets, and cron prompt
contract. Mirrors the existing site-status-checker coverage.
- Layer B scaffold (`scarf/scarfUITests/TemplateInstallUITests.swift`):
proves the launch-arg + env-var plumbing reaches Scarf. Full install
click-through deferred until fixture-Hermes-home and accessibility
IDs land.
Wiki pages added separately on the `.wiki-worktree` branch:
- `Template-Ideas.md` — backlog of 9 v1-feasible templates +
full-spec v3 epic for Project-Site-as-Living-Surface (eBay listings
use case).
- `Test-Harness.md` — contributor guide for extending the harness.
Verification: scarfTests 124/124, ScarfCore 220/220, new Layer A 3/3,
Layer B scaffold 1/1, build-catalog.py + its 28 unit tests all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Profile selection had no apparent effect on Webhooks/Sessions/SOUL.md/Memory
even after restart in some user setups. The path-resolution code reads
~/.hermes/active_profile correctly on paper, so the failure mode is likely
environment-specific (HERMES_HOME exported in the shell, in-process state
that didn't reset on what the user perceived as a restart, etc). Layer a
defense that's correct regardless of root cause:
* New AppRelauncher helper spawns a fresh `open -n <bundleURL>` and asks
the current process to terminate after a 250ms delay. Refuses to fire
from Xcode/DerivedData (the .debugBuild guard) so debug sessions don't
lose their attached debugger.
* ProfilesViewModel.switchAndRelaunch runs `hermes profile use`, calls
HermesProfileResolver.invalidateCache(), then relaunches via the helper.
Existing switchTo() also gains the cache-invalidation step so the
context-menu "Set Active (no relaunch)" path stays self-consistent.
* ProfilesView replaces the passive "Restart Scarf after switching" text
with a confirmation-gated `Switch & Relaunch` primary button on the
detail pane plus the same item in each row's context menu. Confirmation
dialog flags that all Scarf windows will close.
* SidebarView header gains a brand-tinted ScarfBadge showing the
currently-active profile on local contexts. Click to jump to the
Profiles tab. The chip refreshes on `selectedSection` change so a
terminal-side `hermes profile use` is visible after the next nav.
* HermesProfileResolver success logs gain `name=…, home=…, source=…`
key=value structure across all three resolution paths (file / file-default /
default-no-file). `log show … | grep ProfileResolver` now answers
"what did the resolver decide?" unambiguously for support requests.
Closes#70
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Send button is now a 44pt circular target with an explicit color swap
(rust accent → background-tertiary) on disable, instead of relying on
SwiftUI's default opacity dim — addresses the "first tap doesn't
register" complaint by making the inactive state visibly different in
both light and dark mode. Paperclip and text field both gain a 44pt
minimum height so the row feels modern and roomy.
The text field swaps `.roundedBorder` for a plain field with a
ScarfRadius.xl rounded fill (ScarfColor.backgroundSecondary) and a
borderStrong stroke. Outer paddings and HStack spacing migrate from
magic numbers to ScarfSpace tokens.
Preserves verbatim: the `.toolbar { ToolbarItemGroup(placement: .keyboard) }`
keyboard-dismiss chevron (issue #51), draft persistence, .submitLabel,
@FocusState, photo-picker wiring, attachment-strip rendering, and every
.disabled() predicate.
Closes#69
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS-only patch carrying the rotation lock + chat-start preflight
off-MainActor fixes from cb164f0. Mac side stays on the v2.6.0
binary already shipped (build 29 archive); this build number bump
only affects future Mac archives, not the one already notarized.
Uploaded to App Store Connect via altool — Apple processing now,
will land in TestFlight once the binary clears the post-upload
scan (typically 5–15 min).
Two iOS-specific crash classes from the v2.5.1 TestFlight feedback
round:
**Rotation crash** — locked the iPhone target to
`UIInterfaceOrientationPortrait` only (was Portrait + LandscapeLeft
+ LandscapeRight). The phone can't rotate the app at all anymore,
so any layout path that wasn't audited for size-class transitions
is no longer reachable. iPad orientation list left alone (target
device family is iPhone-only anyway).
**"Crash while typing" / "trying to continue an existing
conversation"** — `ChatController.passModelPreflight()` was doing
a synchronous SSH read (`context.readText(configYAML)`) on
`@MainActor` during chat-start. On a remote ScarfGo context that
blocks the main thread for seconds; iOS's non-responsive-app
watchdog kills the process around 10s. To the user this surfaces
as a "crash" while they're typing — they kept tapping the keyboard
while the connect was hung. Move the read to `Task.detached` and
await it; the UI stays responsive while the SSH I/O drains. Three
callers (`start`, `start(projectPath:)`, `startResuming`) updated
to `await passModelPreflight(...)` — they were already async.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TestFlight feedback / crash JSONs land here while we're working
through an iOS fix round. They carry tester PII (emails, carriers,
locales) and aren't meant for the public repo. Kept local-only;
deleted after the round closes.
Replaces the 2.5 "What's New" block with a 2.6 summary that
covers the Hermes v0.12 surfaces (Curator, multimodal images, 5
new providers, Teams + Yuanbao, Kanban, Skills v0.12, cron
--workdir, settings deltas, ScarfGo Webhooks/Plugins/Profiles)
and the post-merge chat fix round (#67/#68/#65/#62/#63/#64/#66/
#61). Verified-versions table gains v0.12.0 as the current target;
recommended-Hermes line points at v0.12.0+ for full feature
support. ScarfGo block kept but de-emphasised since it shipped
in 2.5.
Adds a "Chat composer + transcript (post-merge round)" subsection
to the bug-fixes block covering #67, #68, #65, #62, #63, #64,
#66, and the partial #61 ACP-timeout bump. The pre-merge
test-target / iOS-build fixes stay grouped under "Pre-merge".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Field-reported (#61): under realistic concurrency where the
Hermes gateway is also running, state.db lock contention
(Discord sync / skill registration / cron scheduling all
holding write locks) stalls ACP's `initialize` / `session/new` /
`session/load` past the previous 30s watchdog, surfacing as
"Starting…" indefinitely or an opaque timeout error.
SQLite contention on a healthy host clears in seconds, so 60s
gives the lock-resolution path room to breathe while still
surfacing genuinely broken transports promptly. `session/prompt`
remains untimed (it streams events and can run for minutes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small speaker glyph to the metadata footer of each settled
assistant bubble. Tap to read the reply aloud through
`AVSpeechSynthesizer`; tap again (or any other bubble's button) to
stop. Picks up the user's macOS Spoken Content default voice
automatically — no Hermes dependency, works offline.
- New `MessageSpeechService` (`Core/Services/`) — shared
`@Observable` synthesizer; `playingMessageId` drives icon
state. Markdown control characters (asterisks, backticks,
link syntax) are stripped before speech so the user doesn't
hear "asterisk asterisk bold".
- `SpeakMessageButton` lives outside `RichMessageBubble.==` so
the bubble's Equatable short-circuit doesn't freeze the icon
when playback flips between messages.
The full Hermes-provider TTS pipeline (Edge / ElevenLabs /
OpenAI / NeuTTS / Piper from Settings → Voice) is a much bigger
follow-up — wiring per-provider audio fetching, caching, and
streamed playback is its own quarter. v2.6.0 ships the immediate
"listen while doing something else" affordance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sending a long prompt and switching to other work — the canonical
async-agent flow — required polling the chat to know when the
response landed. Wire a local UNUserNotificationCenter notification
to fire when an ACP prompt completes while Scarf isn't the
foreground app.
- New `ChatNotificationService` (Core/Services) handles lazy
authorization, foreground gating, and post.
- `ChatViewModel.sendViaACP` calls it on successful prompt
completion with the assistant's first-line preview and the
active session title.
- Settings → Display → Feedback adds a "Notify when Hermes finishes"
toggle, default on. Skipped for `/steer`-style mid-run sends —
those don't end a turn.
Dock badges and per-session unread state from the issue are
worthwhile follow-ups but out of scope for v2.6.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a user sent a prompt and immediately switched to a different
session before Hermes flushed the row to state.db, `resumeSession`
ran `reset()` (which clears `messages`) and then
`loadSessionHistory` read the un-persisted DB and replaced the
array with an empty result. The user's bubble came back blank or
disappeared on return.
Hold local-only user messages (negative ids) in a per-session
cache that survives `reset()`. `loadSessionHistory` re-injects any
still-pending entries for the loaded session, dedups against any
DB row that finally caught up (matching content with persisted id
≥ 0), and clears the cache as the DB confirms each entry.
Cache is bounded by sessions sent-in during one app run; entries
clean themselves out as Hermes persists, and orphaned entries
(deleted sessions etc.) are tiny and never re-surface since
session ids are unique per session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`RichChatInputBar`'s `@State` `text` and `attachments` survived
session switches because the surrounding view tree is structurally
identical across sessions — SwiftUI happily reused the same
instance and leaked the previous session's unsent draft into the
new one.
Bind the composer's identity to `richChat.sessionId` so SwiftUI
rebuilds the view (and its `@State`) on session change. A stable
fallback string covers the brief "no session selected" window;
using `UUID()` here would mint a fresh id on every render and
trash the composer per body re-eval.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`TextEditor`'s NSTextView surfaces a typed glyph one frame before
the SwiftUI binding propagates, so the bare `if text.isEmpty`
overlay rendered the translucent placeholder text directly on top
of the just-typed character — the "behind or around" ghost the
reporter described.
Two mitigations:
- Pin an opaque `ScarfColor.backgroundSecondary` rect behind the
placeholder Text. During any single-frame binding lag the user
now sees a clean placeholder rather than layered glyphs.
- Switch the conditional to `.opacity(text.isEmpty ? 1 : 0)` so the
view tree stays stable per keystroke. Pairs with the composer
perf fix from #67.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chat font-size slider only set `\.dynamicTypeSize` on the chat
root, but ScarfFont tokens are fixed-point (`Font.system(size: 14, …)`)
so dynamic type didn't reach bubble text, reasoning, tool chips, code
blocks, or markdown headings. Slider moved between 85%–130% with
little visible effect.
Plumb a separate `\.chatFontScale: Double` env value from
`RichChatView` and have the chat content views read it:
- `RichMessageBubble` — user bubble body, reasoning (disclosure +
inline), REASONING label, token chip, tool-chip name, metadata
footer.
- `MarkdownContentView` — paragraphs (now pinned to a scaled body
font instead of inheriting), headings (1..5), inline-rendered code
blocks, code-language label.
- `CodeBlockView` — code body and language label.
`ChatFontScale.{body, callout, caption, captionStrong, caption2,
mono, monoSmall, codeBlock, codeInline}(_ scale:)` helpers mirror
`ScarfFont`'s base sizes so scale = 1.0 is byte-for-byte identical
to today's UI; the slider now actually moves the visible chat text.
Other surfaces (settings, sidebar, etc.) still use the static
ScarfFont tokens — chat scaling stays scoped to the chat surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typing in the chat composer became unusably laggy because
`updateMenuState()` ran on every keystroke and unconditionally wrote
both `showMenu` and `selectedIndex`. Two state writes inside one
`onChange(of: text)` handler tripped SwiftUI's "action tried to
update multiple times per frame" warning, and each redundant write
forced a full body re-eval — visible as the slow-HID stalls and the
main-thread layout churn the reporter captured in sampling.
Two changes:
- Compute the new selection up front and write only the deltas. Same
semantics; no spurious mutations.
- Short-circuit the whole handler when the user is composing normal
text (no `/` prefix) and the menu is already hidden — the common
case. Stops paying for `SlashCommandMenu.filter` on every keystroke
of regular prose.
- Replace `.onChange(of: commands.map(\.id))` with
`.onChange(of: commands.count)`. The mapped form allocated a fresh
`[String]` on every body re-eval; counting is one int read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Xcode-autogenerated strings for the v12 surface — curator chip labels,
image attachment button + counter, archived-skill banner — that the
extractor produced while the v12-updates branch was being authored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLAUDE.md was rewritten in 3d85b91 to describe the new v0.12 surfaces
but several claims drifted from what actually shipped (or have since
walked back during the review-fix pass):
- Curator iOS panel was described as "read-only"; it ships Run Now /
Pause / Resume actions and inline pin toggles.
- Curator path symbols were named `curatorReportJSON` / `curatorReportMD`;
the actual additions to `HermesPathSet` are `curatorLogsDir` and
`curatorStateFile`, with the per-cycle `run.json` / `REPORT.md`
resolved at runtime via the state file's `last_report_path`.
- The `flush_memories` bullet claimed Scarf had dropped the field; it's
preserved on pre-v0.12 hosts via `hasFlushMemoriesAux` (restored in
commit 33022ae).
- The cron `--workdir` bullet didn't mention the capability gating that
landed in commit 4a2ef74, nor the empty-string clear gesture from
commit 46cec81.
- The v0.12 surface list omitted the iOS Phase H catch-up
(Webhooks/Plugins/Profiles read-only tabs + HermesVersionBanner)
shipped in commit 799332f.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ingestPickerItems` ran loadTransferable + encode sequentially per
selected image. PhotosPickerItem.loadTransferable is async and hops
off MainActor (nonisolated), but for 5+ iCloud-backed PHAssets the
sequential pipeline meant five round-trips back-to-back instead of
five concurrent ones.
Switched to `withTaskGroup` keyed by selection index so:
- Slot cap is computed once up front and items past the cap are
dropped (previously we mid-loop-broke after the first overage).
- Each item's loadTransferable + ImageEncoder runs concurrently.
- Results land back in selection order via index sort, so the
attachment chip row matches what the user picked.
Errors carry a Sendable `String` message rather than the raw `Error`,
which isn't Sendable under strict concurrency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-load assignment was a true no-op:
`self.lastError = parsed.isEmpty && !result.isEmpty ? nil : nil` —
both ternary branches assigned `nil`. The intent (visible from the
condition shape) was to set an error message when the CLI returned
text but the parser produced no webhooks.
Now that branch sets a "Couldn't parse webhook list output" message
which the existing banner at line 33 renders. Normal flow (parse
succeeds, or empty output) still clears the error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`KanbanViewModel.load` previously assigned the combined stdout+stderr
output of `runHermesCLI` into both the JSON-parse `data` and the
`stderr` slot of its result tuple. Two consequences:
- On non-zero exit, the error banner showed combined output (often
stdout usage text concatenated with the actual error), reducing the
signal-to-noise ratio when troubleshooting.
- On non-zero exit with mixed output, JSON decoding could fail because
stderr text was prepended to the JSON body.
Added `HermesFileService.runHermesCLISplit` — a sibling of `runHermesCLI`
that returns `(exitCode, stdout, stderr)` separately, leaning on the
already-separated `stdoutString` / `stderrString` from the transport
layer. KanbanViewModel now uses it: stdout is the JSON parse target,
stderr is the error-banner source. Existing `runHermesCLI` callers are
untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`updateJob` only emitted `--workdir <path>` when the value was non-empty,
so once a workdir was set on a job, the user had no way to remove it
through Scarf — clearing the TextField and saving was a silent no-op.
Hermes' `cron edit --workdir` argparse documents passing an empty string
as the explicit clear gesture (mirroring the existing `--script` shape,
which already passes empty through here). Drop the `!isEmpty` predicate
so a non-nil value — including "" — reaches the CLI.
The previous capability gate keeps this safe on pre-v0.12 hosts: CronView
passes `workdir: nil` there, so the flag is omitted and v0.11 argparse
is never asked about an unknown arg.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The disabled-skill row's "OFF" pill used `.font(.system(size: 9, weight:
.semibold))`, which the project CLAUDE.md flags as a code smell ("bypass
the type scale… is a code smell"). The design system documents
`scarfStyle(.captionUppercase)` as the canonical badge font; switching
to it picks up the matching tracking + uppercase casing as a bonus.
The pin glyph above (`Image(systemName: "pin.fill").font(.system(size:
9))`) is left as-is — that's intentional glyph sizing on an `Image`,
which the design rule explicitly excludes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`readDisabledSkillNames` broke out of the loop on `leading <= baseIndent`,
but PyYAML's default `yaml.dump` (what Hermes uses to write the disabled
list) emits list items at the SAME indent as the parent key:
skills:
disabled:
- foo
- bar
Here `disabled:` is at indent 2 and `- foo` is also at indent 2, so the
old check terminated before any item was appended — every disabled skill
written by Hermes would have appeared enabled in the UI.
Now the loop only breaks when the indent is strictly shallower than the
`disabled:` line, or when a same-indent line isn't a list item (sibling
key — that's still the end of the block). The deeper-indent layout still
parses correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B removed the `flushMemories` field from `AuxiliarySettings`,
the `aux("flush_memories")` reader from the YAML parser, and the
"Flush Memories" row from `AuxiliaryTab.tasks` outright. But
`HermesCapabilities.hasFlushMemoriesAux` still claims (with inverse
semantics) that the row should stay visible on pre-v0.12 hosts where
the task is alive. Project CLAUDE.md documents the same contract.
Restored:
- `AuxiliarySettings.flushMemories: AuxiliaryModel` (and `.empty`).
- `aux("flush_memories")` in both YAML readers
(`HermesConfig+YAML.swift` and the `HermesFileService` mirror).
- `AuxiliaryTab.tasks` appends the Flush Memories row when
`hasFlushMemoriesAux` is true, mirroring how `curator` is appended
on the v0.12+ branch.
On v0.12+ hosts the flag is `false` so the field stays `.empty` and
the row is hidden — no behaviour change for current users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`HermesCapabilities.hasCronWorkdir` was added but never consumed: the
editor sheet always rendered the Workdir TextField and the view model
unconditionally appended `--workdir <path>` whenever the field was
non-empty. On a pre-v0.12 host argparse rejects the unknown flag and
the entire `cron create`/`cron edit` call fails.
Two-layer gate:
- CronJobEditor takes a `supportsWorkdir` flag and hides the field on
pre-v0.12 hosts.
- CronView reads `\.hermesCapabilities` and forces the workdir argument
to "" / nil when the capability is absent, so an editing-an-existing-
job path that hydrates `form.workdir` from a pre-existing value can't
smuggle the flag through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`presentImagePicker()` ran `Data(contentsOf: url)` synchronously on
MainActor inside the URL loop before the detached `encode()`. A 24 MP
HEIC at 8-15 MB stalled the chat composer per file. The drag/drop and
paste paths already read off-main via `loadObject`/`loadDataRepresentation`
callbacks; this brings the open-panel branch in line by capturing the
URLs into a `Task.detached` and reading bytes there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds releases/v2.6.0/RELEASE_NOTES.md covering every Phase A-H surface
(Curator, multimodal image input, 5 new providers, Skills v0.12,
Settings deltas, Cron workdir, Teams + Yuanbao, read-only Kanban, iOS
read-only Webhooks/Plugins/Profiles, version banner, internal
capability detector). Drops a paragraph at the top noting Hermes
v0.11 hosts continue to work — every new surface is gated on
HermesCapabilities so v2.6 against v0.11 looks identical to v2.5.2
against v0.11.
Polishes CLAUDE.md inaccuracies introduced in Phase A's first pass:
- ACP image wire shape: corrected to {"type":"image","data":...,"mimeType":...}
(matches acp.schema.ImageContentBlock); previous Anthropic-style
source: {type: base64, ...} sketch was wrong.
- Cron --context-from: clarified that Hermes hasn't exposed it as a
CLI flag yet (read-only via HermesCronJob.contextFrom), only
--workdir is writable.
- hermes memory setup: noted that the interactive verb stays in
Terminal (no in-app shellout); Settings → Memory just exposes the
provider picker.
- Skills surface: more precise about which CLI verbs back the Mac UI
affordances and why the disable-toggle is deferred to v2.7.
215 ScarfCore tests green; both Mac and iOS schemes build clean. Wiki
update + the actual release.sh ship are deferred to the user's
typical release-prep flow (the wiki repo is a separate worktree
that needs scripts/wiki.sh pull/commit/push, and release.sh expects
a clean working tree pointed at main).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the iOS read-only inspection gap on three CLI-driven Hermes
surfaces and adds a Hermes-version banner so mobile users on remote
v0.11 hosts see the upgrade nudge inline.
Components:
- Scarf iOS/Components/HermesVersionBanner.swift — yellow banner shown
on the Dashboard when the active server's HermesCapabilities returns
detected==true && hasCurator==false. One-tap session dismiss; comes
back on next app open. Lists the v0.12 capabilities the user is
missing out on (curator, multimodal, new providers).
- Scarf iOS/Webhooks/WebhooksView.swift — read-only list rendered from
`hermes webhook list`. Tolerant block parser mirrors the Mac
WebhooksViewModel shape so future drift fixes in one canonical place
if/when promoted into ScarfCore. Detects the "platform not enabled"
state and shows a setup-required pane instead of synthesizing rows
from instructional text.
- Scarf iOS/Plugins/PluginsView.swift — filesystem-first scan over
`~/.hermes/plugins/<name>/` with plugin.json / plugin.yaml manifest
reads (mirrors the Mac VM). Enabled/disabled badge, version, source.
Uses HermesYAML.parseNestedYAML / stripYAMLQuotes from ScarfCore
(already public).
- Scarf iOS/Profiles/ProfilesView.swift — `hermes profile list` text
parser with active-profile highlighting from
`~/.hermes/active_profile`. Defensively handles both Rich box-drawn
table output and plain-text fallback.
ScarfGoTabRoot's System tab gains an "Inspect" section with the three
new NavigationLinks. None are capability-gated — the underlying
list verbs exist on both v0.11 and v0.12, so the read views work
against either Hermes version without surprises.
Tests: 215 ScarfCore tests pass; both Mac and iOS schemes build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mac-only Phase G surfaces. Three additions:
Cron — `--workdir` flag (v0.12+):
- HermesCronJob carries `workdir: String?` and `contextFrom: [String]?`
fields (the latter is read-only from CLI today; YAML-only chaining).
- FormState.workdir; CronJobEditor adds an absolute-path field;
CronViewModel.createJob/updateJob forward `--workdir` when set,
omit it when blank so v0.11 hosts (which don't know the flag) keep
working unchanged.
Platforms — Microsoft Teams + Yuanbao (v0.12+):
- KnownPlatforms gains the two new platform identifiers + icons.
- PlatformsView adds inline read-only setup panels for each since the
full setup flow lives outside Scarf (OAuth dance for Yuanbao, plugin
install for Teams). Both panels surface the type, the recommended
setup command, and the current configured/connected status the
existing connectivity probe already understands.
Kanban — read-only list (v0.12+):
- HermesKanbanTask Sendable Codable model mirroring
`_task_to_dict` in hermes_cli/kanban.py.
- KanbanViewModel polls `hermes kanban list --json` every 5s while the
view is foregrounded; status filter dropdown maps to `--status`.
Empty list and "no matching tasks" text outputs both render the
empty state cleanly.
- KanbanView: page header + status badges + meta chips
(id/assignee/workspace/skills) per row. No create/claim/dispatch UI
— multi-profile collaboration was reverted upstream while the
design is reworked, so v2.6 ships read-only and defers the editor
to v2.7+.
- AppCoordinator.SidebarSection.kanban + ContentView routing.
SidebarView's capability-aware `sections` filters out the row when
`HermesCapabilities.hasKanban` is false.
Tests: 215 ScarfCore tests pass; both Mac and iOS schemes build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the v0.12 config knobs that landed without their own dedicated
UI elsewhere:
- prompt_caching.cache_ttl picker (5m default, 1h opt-in) — reduces
cache writes on long agent loops with stable system prompts.
- redaction.enabled toggle — Hermes flipped this off by default in
v0.12 because the substitution corrupted patches; security-sensitive
users can flip it back on here.
- agent.runtime_metadata_footer toggle — opt-in compact footer on each
final reply (provider/model/cost/turn count).
- TTS provider list gains "piper" — native local TTS engine new in
v0.12.
- Terminal backend list gains "vercel" — Vercel Sandbox backend for
execute_code/terminal added in v0.12.
The new "Caching & Redaction" section in AdvancedTab is gated on
HermesCapabilities.hasPromptCacheTTL — pre-v0.12 hosts don't see
toggles that would write keys Hermes ignores. The Piper + Vercel
options ride along unconditionally because Hermes silently accepts
unknown values and falls back to safe defaults.
Model + parser:
- HermesConfig grows three optional scalar fields (cacheTTL: String,
redactionEnabled: Bool, runtimeMetadataFooter: Bool). All three
have init defaults so existing call sites — including
HermesConfig.empty — keep compiling.
- Both YAML readers (HermesFileService for Mac, HermesConfig+YAML for
the package) now parse the new keys with v0.12-defaults.
Tests: 215 ScarfCore tests pass; both Mac and iOS schemes build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hermes v0.12 added three skills surfaces Scarf can now reach:
- Direct-URL install: `hermes skills install <https://...>` lets users
pull a one-off skill without going through a registry. Mac SkillsView
grew an "Install from URL…" toolbar button (capability-gated on
HermesCapabilities.hasSkillURLInstall) opening a sheet with the URL
field plus optional --category / --name overrides.
- Reload: `hermes skills audit` rescans `~/.hermes/skills/` and refreshes
the agent's view of available skills without restarting. Wired to a
"Reload" toolbar button next to the install button on Mac.
- Enabled state: skills.disabled in config.yaml is now read at scan time
(SkillsViewModel.readDisabledSkillNames). Disabled skills render
strikethrough + an "OFF" pill on Mac and iOS rows so users see what
Hermes won't load. iOS detail view explains the state in plain text.
- Curator pin badge: pinned-skill names from
`~/.hermes/skills/.curator_state` (SkillsViewModel.readPinnedSkillNames)
surface as a pin glyph on each row. Mac sidebar + iOS list both show
it; iOS detail view explains "pinned by curator — won't auto-archive."
Model + scanner:
- HermesSkill gains `enabled: Bool` (default true) and `pinned: Bool`
(default false). Both default to backwards-compatible values so
unmodified call sites keep compiling.
- SkillsScanner.scan now takes optional `disabledNames` and
`pinnedNames` sets and applies them per skill at scan time.
- SkillsViewModel.load auto-fetches both sets internally so Mac/iOS
callers don't have to plumb curator state manually; an opt-in
`pinnedNames` override is available for the Curator screen which
has a fresher snapshot in hand.
Tests: 215 ScarfCore tests pass; both Mac and iOS schemes build clean.
Note: the disable-toggle path (writing the array back into
config.yaml) is deferred to v2.7 — Hermes ships
`hermes skills config` as an interactive verb only, and we'd rather
read accurately than risk clobbering the user's list with a
half-tested write path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hermes v0.12 ships an autonomous Curator that prunes / consolidates
agent-created skills on a 7-day cycle. This phase brings that surface
into Scarf so users can see status, trigger runs, pin protected skills,
and restore archived ones.
Pipeline:
- HermesCuratorStatus + HermesCuratorSkillRow: Sendable value types for
parsed status + per-skill leaderboard rows.
- HermesCuratorStatusParser: pure text parser for `hermes curator status`
stdout (no `--json` flag exists upstream). Tolerates Hermes's
whitespace-padded leaderboard layout (`activity= 0` with N spaces
between `=` and the value) by slicing between known key positions
rather than splitting on whitespace. State-file JSON overrides
text-parsed values for last_run_at / last_run_summary /
last_report_path because the file carries full ISO timestamps the
text output may have rounded.
- CuratorViewModel: @Observable @MainActor, drives the CLI verbs
(status / run / pause / resume / pin / unpin / restore) via
transport.runProcess so it works equally over local and Citadel SSH.
- HermesPathSet: adds curatorLogsDir + curatorStateFile (the latter
is `.curator_state` with no extension despite holding JSON).
Mac:
- Features/Curator/Views/CuratorView.swift — page-header + status card
+ skill counts + pinned chips + 3 leaderboard tables (least recent,
most active, least active) with inline pin toggles and a
per-skill counter chip row. "Run Now" button + a kebab menu for
Pause/Resume + Restore Archived.
- Features/Curator/Views/CuratorRestoreSheet.swift — name-entry sheet
for `hermes curator restore <skill>`. Free-form text field; Hermes
doesn't ship a `curator list-archived` yet so we don't synthesize a
picker.
- Sidebar: AppCoordinator + SidebarView gain a `.curator` case under
Interact (between Memory and Skills); the row is filtered out by
SidebarView's capability-aware `sections` computed property when
`HermesCapabilities.hasCurator` is false. ContentView routes
`.curator` to CuratorView. Pre-v0.12 hosts see the v0.11 sidebar
unchanged.
iOS:
- Scarf iOS/Curator/CuratorView.swift — read-mostly List with the same
status / skill counts / pinned / leaderboards + inline pin toggles.
Run Now / Pause / Resume actions in the section footer.
- ScarfGoTabRoot's System tab gains a Curator NavigationLink under
Features, gated on `hasCurator`. Uses a stable
`systemTabContextID` so the SSH transport pool reuses the cached
Citadel connection keyed by that id.
Tests: 6 new parser tests (215 total, all green). Locks the empty-state
output captured from a real v0.12.0 install + paused-state + state-file
override + multi-word-name-row parsing. Both Mac and iOS schemes build
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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).
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>
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>
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>