335 Commits

Author SHA1 Message Date
Alan Wizemann 6a7ac21ebe chore: Bump version to 2.6.5 v2.6.5 2026-05-03 22:15:05 +02:00
Alan Wizemann 5be67282d8 test(layer-b): full Install → Configure → Open → Uninstall journey XCUITest (#73)
Closes the deferred Layer B install-drive that v2.7's smoke test
left as future work. The new test
(`testFullCatalogToInstallToDashboardJourney`) drives the full
install/uninstall pipeline end-to-end and validates 9 assertion
points along the way:

- Window surfaces under `--scarf-test-mode`
- Sidebar navigation to Projects
- Install sheet appears (URL handoff via launch arg)
- Parent-dir field accepts custom path + Continue
- Configure sheet renders + commit clicks
- Confirm Install runs the install pipeline
- Open Project advances to success view
- Project row appears in sidebar with uniquified name
- Right-click Uninstall + confirm Remove + Done removes the row

Runs in ~30s green on the dev Mac.

## What needed wiring up

**SwiftUI Menu / NSToolbarItem accessibility-bridging.** macOS
toolbar Menus don't propagate `.accessibilityIdentifier` through to
XCUITest — neither the menu trigger NOR the popup contents are
queryable by ID. Verified by tree-dump diagnostics. The test
sidesteps this entirely by routing the install URL through a new
`--scarf-test-install-url <https-url>` launch arg that calls
`TemplateURLRouter.shared.handle(scarf://install?url=...)` at App
init, gated on `TestModeFlags.shared.isTestMode`. Production
launches (no flag) untouched.

**Accessibility IDs added** on the new install/uninstall path:
- `templateConfig.commitButton`, `templateConfig.cancelButton`
- `projects.row.<name>`, `sidebar.section.<rawValue>`
- `projects.contextMenu.uninstallTemplate`
- `templateUninstall.confirmRemove`
- `templateInstall.success.openProject`
- `templateUninstall.success.done`

**Sandboxed-runner caveat.** The XCUITest runner's `/tmp` is
sandbox-protected (createDirectory throws EPERM); we use
`NSTemporaryDirectory()` which resolves to the runner's container
tmp (`~/Library/Containers/com.scarfUITests.xctrunner/Data/tmp/`),
which the unsandboxed Scarf app can read since it has full disk
access.

## Known cohabitation hazard (pre-existing uninstaller bug)

If the dev Mac already has a project from the same template
installed, the install pipeline uniquifies the new project's name
("HackerNews Daily Digest 2") but BOTH projects' cron jobs get
registered under the same `[tmpl:awizemann/hackernews-digest] Daily
HN digest` name. `ProjectTemplateUninstaller.loadUninstallPlan`
resolves cron jobs to remove by NAME and can target the wrong
project's job. The Layer B test surfaces this — manifests as: test
passes, the dev's real project's cron job disappears.

**Fix (separate work):** store cron-job IDs in
`<project>/.scarf/template.lock.json` at install time and resolve
by ID at uninstall time. Until then, the test docstring warns
about cohabitation; recovery is `hermes cron create` to recreate
the lost job.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:09:50 +02:00
Alan Wizemann c661945a1f feat(cron): auth-error banner + running indicator + per-job log tail (#72)
Cron rows now surface the same OAuth-refresh-revoked recovery flow as
chat instead of a generic red dot, plus three previously-missing
observability cues:

- ACPErrorHint.classify is reused on `job.lastError`. When it returns
  `oauthRefreshRevoked(provider)` the detail pane shows the human hint
  + a "Re-authenticate" button that drops the user into Credential
  Pools via `coordinator.pendingOAuthReauth = provider` — same wiring
  ChatView's banner uses. Unrecognized errors fall back to the legacy
  red `lastError` text (no regression).
- Row dot turns blue + pulses when `state == "running"` (taking
  precedence over disabled / error / success); the detail header gains
  a `ScarfBadge("running…", kind: .info)` next to active/paused. No new
  polling — `HermesFileWatcher.lastChangeDate` (already wired into
  ActivityView/Logs) drives `CronViewModel.load()` so state flips
  surface within a watcher tick.
- "LAST RUN OUTPUT" replaces the inline `LAST OUTPUT` block with a
  collapsible panel: a one-line summary (`<timestamp> — ok|error|running…`)
  always visible, full monospaced terminal-style scroll view on
  expand, auto-scrolls to bottom when new runs land.

Also fixes a pre-existing bug in `HermesFileService.loadCronOutput`:
Hermes nests per-run output under `~/.hermes/cron/output/<jobId>/<ts>.md`
but the loader treated the dir as flat, so the cron output panel never
rendered any content. The fix walks the per-job subdir + keeps the
legacy flat-file fallback for older Hermes layouts.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:09:21 +02:00
Alan Wizemann f5f8dc30b6 Dogfooding templates: HN Digest + in-app catalog browser + test harness (#71)
* feat(templates): hackernews-digest template + dogfooding test harness

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>

* feat(test-harness): Layer B pivot to real ~/.hermes + a11y IDs + Sparkle gating

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>

* fix(chat): clip placeholder to TextEditor bounds and clear it on focus

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>

* fix(chat): surface OAuth refresh-revoked errors with in-app re-auth

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>

* feat(catalog): in-app template catalog browser + sentinel-marker test isolation

The v2.8 catalog browser surfaces every shipped .scarftemplate from
awizemann.github.io/scarf/templates/catalog.json directly in Scarf.
Users now discover and install templates without leaving the app.
Closes the gap that publishing the catalog updated the website but
nothing inside Scarf.

Architecture mirrors NousModelCatalogService 1:1: cache-first fetch,
24h TTL at ~/.hermes/scarf/catalog_cache.json, result enum (fresh /
cache / fallback) with bundled fallback so a fresh-install / offline
user still sees something. Search + category filter + sort
(awizemann official first). Detail page renders entry.config schema
preview without separate README fetch — what's in catalog.json is
what we render. Install hands the HTTPS URL to the existing
TemplateInstallerViewModel.openRemoteURL flow; nothing about the
installer itself changes.

Files:
- Core/Models/CatalogEntry.swift — Decodable mirror of catalog.json
  per-template shape. Identity-based Equatable/Hashable on `id`.
- Core/Services/CatalogService.swift — fetch + cache + fallback
- Core/Services/InstalledTemplatesIndex.swift — walks projects.json +
  template.lock.json to build [templateId: version] map; classify()
  helper for Installed / Update available / Not installed badges
- Features/Templates/ViewModels/CatalogViewModel.swift — @Observable
- Features/Templates/Views/{CatalogView,CatalogRowView,CatalogDetailView,CatalogCategoryFilter}.swift
- Packages/ScarfCore/.../HermesPathSet.swift — adds catalogCache path
- Features/Projects/Views/ProjectsView.swift — Templates toolbar
  menu now opens with "Browse Catalog…"; sheet binding.

Tests (20 new, all passing in isolation):
- CatalogServiceTests (6) — live catalog.json snapshot, cache lifecycle,
  staleness boundary, schema-version mismatch rejection, bundled fallback
- InstalledTemplatesIndexTests (5) — empty registry, templated project,
  ad-hoc project skip, corrupt lock skip, classify() branches
- CatalogViewModelTests (6) — search filter, category filter, official-first
  sort, deduped categories, install state, install URL pass-through

Accessibility IDs (6, on the catalog path): templates.browseCatalog,
catalog.searchField, catalog.refreshButton, catalog.row.<detailSlug>,
catalog.categoryFilter, catalogDetail.installButton.

## Sentinel-marker hardening on SCARF_HERMES_HOME (incident response)

While iterating on v2.8 tests, the env-var override pattern racing
under Swift Testing's parallel-suite scheduler caused
~/.hermes/scarf/projects.json to be overwritten with fixture data
from ProjectsViewModelTests. Recovered the user's projects from the
on-disk dirs they referenced + cron-job prompt paths (6 projects
restored).

To make this class of incident impossible going forward:
HermesProfileResolver.scarfHermesHomeOverride() now requires the
override path to contain a sentinel marker file
(`.scarf-test-home-marker`). Without the marker, the override is
ignored and Scarf falls through to the real ~/.hermes/. Even if a
test crashes mid-teardown leaving the env var set, even if the var
leaks to a non-test process, even if a misconfigured launchctl plist
exports it — the override only activates against directories that
explicitly opt in by carrying the marker. Tests drop the marker in
their tmpdir setUp; production never carries it.

HermesProfileResolverTests gains overrideIsIgnoredWhenMarkerMissing
which verifies the guard is load-bearing. All test files using
SCARF_HERMES_HOME (CatalogServiceTests, CatalogViewModelTests,
InstalledTemplatesIndexTests, TemplateE2ETests) now drop the marker
before setenv.

Verification: 20/20 v2.8 + v2.7 hardened tests pass; 45/45 adjacent
existing tests pass; ScarfCore package tests pass (221/221); catalog
validator clean (3 templates); wiki secret-scan clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(swift6): retroactive conformance + verbatim help text + xcstrings refresh

Three small Swift 6 compile-cleanups that landed during the
dogfooding-templates iteration:

- MessageSpeechService — drop `@preconcurrency` on the
  AVSpeechSynthesizerDelegate conformance now that the protocol's
  Sendable annotations are upstreamed.
- ChatView — mark `RichChatViewModel.PendingPermission: Identifiable`
  as `@retroactive`. We don't own either the type or the protocol; the
  Swift 6 compiler flags this so downstream breakage is loud if
  ScarfCore ever adds the conformance upstream.
- CredentialPoolsView — wrap the `.help(...)` string in
  `Text(verbatim:)` so the backticks render literally instead of being
  interpreted as markdown inline-code by the LocalizedStringKey
  overload (which `.help(_:)` rejects styled).

Localizable.xcstrings: auto-generated catalog refresh picking up the
new active-profile + chat error-hint strings landed in earlier
commits on this branch (acd3692, 301806d).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(catalog): error logging + MainActor I/O + semver pre-release + decoder fault tolerance

- InstalledTemplatesIndex: replace bare `try?` reads/decodes with logged
  do/catch so corrupt registry/lock files leave a breadcrumb instead of a
  silent nil.
- InstalledTemplatesIndex.isVersionNewer: handle pre-release suffixes per
  semver §11 — `1.0.0-beta` no longer reports as newer than `1.0.0`,
  preventing a ghost "Update available" that would downgrade users.
- CatalogViewModel.refresh: dispatch the synchronous index walk through
  `Task.detached` so registry + N lock-file reads don't run on
  @MainActor.
- Catalog decoder: per-element fault tolerance via custom `init(from:)` —
  one malformed catalog entry is dropped with a logged warning instead
  of failing the whole catalog decode (honors the per-entry doc-comment
  contract).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:04:13 +02:00
Alan Wizemann 34d315793b fix(chat): clip placeholder to TextEditor bounds and clear it on focus
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>
2026-05-03 16:48:32 +02:00
Alan Wizemann acd3692faf fix(profiles): switch-and-relaunch flow + active-profile chip + structured logs
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>
2026-05-03 13:18:10 +02:00
Alan Wizemann ab615f0c28 feat(ios-chat): redesign composer with HIG touch targets and clear disabled state
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>
2026-05-03 13:14:09 +02:00
Alan Wizemann 982ed7da92 chore: bump iOS build to 30 for TestFlight
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).
2026-05-01 16:20:13 +02:00
Alan Wizemann cb164f07f9 fix(ios): lock iPhone to portrait + move chat-start preflight off MainActor
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>
2026-05-01 16:03:28 +02:00
Alan Wizemann 1dbdf9d079 chore: ignore local crashes/ triage directory
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.
2026-05-01 15:57:41 +02:00
Alan Wizemann 101488cd0d docs(readme): bump What's New to v2.6.0 + Hermes v0.12 catch-up
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.
2026-05-01 15:55:16 +02:00
Alan Wizemann 03c996ee80 chore: Bump version to 2.6.0 v2.6.0 2026-05-01 15:42:48 +02:00
Alan Wizemann 8428cbff10 docs(v2.6.0): document post-merge issue fixes in RELEASE_NOTES
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>
2026-05-01 15:41:48 +02:00
Alan Wizemann 381adfd925 fix(acp): bump control-message timeout 30s→60s for db-contended hosts (#61)
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>
2026-05-01 15:40:33 +02:00
Alan Wizemann 254af46e93 feat(chat): per-message TTS playback in assistant bubbles (#66)
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>
2026-05-01 15:38:22 +02:00
Alan Wizemann 596c844da5 feat(chat): notify when Hermes finishes a prompt in the background (#64)
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>
2026-05-01 15:35:55 +02:00
Alan Wizemann ec47d191a1 fix(chat): preserve local user messages across resume cycles (#63)
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>
2026-05-01 15:33:37 +02:00
Alan Wizemann 31e6c31acf fix(chat): scope composer state to active session id (#62)
`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>
2026-05-01 15:28:59 +02:00
Alan Wizemann fcfe1c89d6 fix(chat): stop placeholder ghosting in chat composer (#65)
`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>
2026-05-01 15:27:53 +02:00
Alan Wizemann df1b9caabf fix(chat): scale rich chat content with the font-size slider (#68)
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>
2026-05-01 15:24:45 +02:00
Alan Wizemann a41c81c048 fix(chat): coalesce composer onChange writes to stop typing lag (#67)
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>
2026-05-01 15:20:15 +02:00
Alan Wizemann 88add62997 Merge branch 'v12-updates'
Hermes v2026.4.30 (v0.12.0) compatibility — autonomous Curator (Mac +
iOS), multimodal image input in chat, 5 new inference providers,
Microsoft Teams + Yuanbao gateway platforms, read-only Kanban view,
Skills v0.12 surface (URL install / reload / pin / disable), Cron
--workdir flag, Settings deltas (cache TTL, redaction, runtime footer,
Piper, Vercel), iOS read-only Webhooks/Plugins/Profiles, and a
pre-v0.12 Hermes-version banner. All new surfaces capability-gated so
older Hermes hosts see the v2.5 surface unchanged.

Release notes: releases/v2.6.0/RELEASE_NOTES.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:17:36 +02:00
Alan Wizemann 80589b3f23 chore(i18n): pick up autogenerated v0.12 string keys
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>
2026-05-01 15:17:11 +02:00
Alan Wizemann 13f89e309b docs(claude-md): correct Hermes v0.12 surface drift after review fixes
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>
2026-05-01 14:15:34 +02:00
Alan Wizemann c055081ba3 perf(chat-ios): ingest picker items in parallel via TaskGroup
`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>
2026-05-01 14:12:41 +02:00
Alan Wizemann bd05e01d1c fix(webhooks-ios): surface parse failure in lastError
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>
2026-05-01 14:11:25 +02:00
Alan Wizemann b66ed7e8d7 fix(kanban): show stderr-only in error banner, parse stdout-only as JSON
`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>
2026-05-01 13:29:16 +02:00
Alan Wizemann 46cec816ec fix(cron): allow clearing an existing workdir on edit
`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>
2026-05-01 13:27:49 +02:00
Alan Wizemann 681fa40c3c fix(skills): use ScarfFont token for OFF pill badge
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>
2026-05-01 13:27:07 +02:00
Alan Wizemann 15642d37cf fix(skills): parse equal-indent disabled list in skills config
`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>
2026-05-01 13:23:01 +02:00
Alan Wizemann 33022aeb92 fix(settings): restore flush_memories aux row on pre-v0.12 hosts
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>
2026-05-01 13:22:41 +02:00
Alan Wizemann 4a2ef74b74 fix(cron): gate --workdir flag on hasCronWorkdir capability
`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>
2026-05-01 13:21:35 +02:00
Alan Wizemann 11bb2bd0c3 fix(chat): detach NSOpenPanel image read off MainActor
`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>
2026-05-01 13:20:50 +02:00
Alan Wizemann 3d85b91392 docs(hermes-v12): release notes + CLAUDE.md polish (Phase I)
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>
2026-05-01 13:01:43 +02:00
Alan Wizemann 799332fbcd feat(hermes-v12): iOS catch-up — Webhooks/Plugins/Profiles read-only + version banner (Phase H)
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>
2026-05-01 12:58:28 +02:00
Alan Wizemann 7a833b6c5a feat(hermes-v12): Cron workdir + Microsoft Teams + Yuanbao + read-only Kanban (Phase G)
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>
2026-05-01 12:54:38 +02:00
Alan Wizemann 6954f0276a feat(hermes-v12): Settings deltas — cache TTL, redaction, runtime footer, Piper, Vercel (Phase F)
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>
2026-05-01 12:47:54 +02:00
Alan Wizemann ee3791a1b2 feat(hermes-v12): Skills v0.12 surface — URL install + reload + pin/disable badges (Phase E)
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>
2026-05-01 12:44:15 +02:00
Alan Wizemann 686fb37630 feat(hermes-v12): Curator feature module on Mac + iOS (Phase D)
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>
2026-05-01 12:37:48 +02:00
Alan Wizemann 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>
2026-05-01 12:28:41 +02:00
Alan Wizemann 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>
2026-05-01 12:16:37 +02:00
Alan Wizemann 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>
2026-05-01 12:10:06 +02:00
Alan Wizemann 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>
2026-04-30 17:51:33 +02:00
Alan Wizemann 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>
2026-04-30 17:51:10 +02:00
Alan Wizemann 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>
2026-04-30 16:04:14 +02:00
Alan Wizemann 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>
2026-04-30 14:41:37 +02:00
Alan Wizemann cca99d4e13 chore: Bump version to 2.5.2 v2.5.2 2026-04-29 13:36:53 +02:00
Alan Wizemann 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).
2026-04-29 13:27:25 +02:00
Alan Wizemann 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>
2026-04-29 13:22:51 +02:00
Alan Wizemann 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>
2026-04-29 13:22:51 +02:00