mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
cecc1060c6229c026149c17d2fe7723fd3a2eab1
201 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3420abae74 |
M2 follow-up: Citadel 0.12.1 (current), pre-built Assets.xcassets
Two follow-ups per review:
## Citadel: current stable
Citadel is at 0.12.1, not 0.9.x as I'd been writing against. Bumped
the pin from `from: "0.7.0"` to `.upToNextMinor(from: "0.12.0")`
— tight because Citadel's pre-1.0 authentication-method variants
have shifted between minor releases (0.7 → 0.9 → 0.12), so
explicit bump-and-review is safer than letting the version float.
Downloaded Citadel 0.12.1's source and verified every API call in
CitadelSSHService against it:
- SSHAuthenticationMethod.ed25519(username:, privateKey:) ✓
- SSHClientSettings(host:, authenticationMethod:, hostKeyValidator:) ✓
- SSHHostKeyValidator.acceptAnything() ✓
- SSHClient.connect(to: settings) ✓
- client.executeCommand(_:) -> ByteBuffer ✓
- client.close() async throws ✓
Dropped the "FIXME — may need adjustment" disclaimer in the file
header; replaced with a "verified against 0.12.1" note that says
re-verify if the pin bumps to 0.13+. Same change in SETUP.md
troubleshooting.
## Assets.xcassets (app icon + accent color)
scarf/scarf-ios/Assets.xcassets/ now exists with:
- AppIcon.appiconset/
AppIcon-1024.png (1024×1024, copied from the Mac app's
icon set — same art)
Contents.json (idiom: universal, platform: ios,
size: 1024x1024 — iOS 14+ renders all
smaller sizes from this automatically)
- AccentColor.colorset/
Contents.json (Scarf teal: sRGB 0.227/0.525/0.722
light, 0.400/0.690/0.902 dark)
- Contents.json (root, empty — just version metadata)
SETUP.md updated:
- Instructs Alan to delete Xcode's scaffolded Assets.xcassets AND
import ours, not the other way around.
- Notes the accent color values so a different palette choice is
a one-file edit.
- Removes the obsolete "drop your icon asset" step.
No functional code changes; tests still 88/88 on Linux.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
ba368d2f6d |
iOS port M2: iOS app skeleton — onboarding, Citadel wrapper, Keychain, Dashboard
First iOS phase. Delivers all the code needed to build + TestFlight a
functional v1 iOS app (onboarding with SSH-key generate / import +
real Citadel-backed connection test; persistent Keychain key +
UserDefaults server config; placeholder Dashboard) — but NOT the
scarf-ios.xcodeproj. Creating that from scratch by hand is too risky
without an iOS SDK to build against, so Alan creates it in Xcode's UI
following scarf/scarf-ios/SETUP.md (~5 minutes, one-time).
## ScarfCore additions (all Linux-testable)
Packages/ScarfCore/Sources/ScarfCore/Security/:
- SSHKey.swift — SSHKeyBundle + SSHKeyStore protocol
+ InMemorySSHKeyStore test actor
- IOSServerConfig.swift — IOSServerConfig + store protocol + mock;
toServerContext(id:) bridges to the
existing ServerContext so all ScarfCore
services work against an iOS config
- OnboardingState.swift — OnboardingStep enum + pure validators
(host, port, PEM shape, public-key parse)
- SSHConnectionTester.swift — protocol + error enum + mock
- OnboardingViewModel.swift — @Observable @MainActor state machine,
fully dependency-injected (key store /
config store / tester / generator closure)
## New Packages/ScarfIOS local SPM package
Depends on ScarfCore + Citadel (from: "0.7.0").
- KeychainSSHKeyStore.swift — real iOS Keychain storage
(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, no iCloud
sync). Gated on canImport(Security) for Linux skip.
- UserDefaultsIOSServerConfigStore.swift — JSON-encoded single-key
persistence of IOSServerConfig.
- Ed25519KeyGenerator.swift — CryptoKit-backed Ed25519 minting.
Emits standard OpenSSH public-key lines (authorized_keys-ready).
Stores the private half in a compact SCARF ED25519 PRIVATE KEY
PEM shape that CitadelSSHService decodes back into a
Curve25519.Signing.PrivateKey. Non-interop with OpenSSH's
`BEGIN OPENSSH PRIVATE KEY` envelope — export flow for sharing
keys is deferred to a later phase.
- CitadelSSHService.swift — SSHConnectionTester conformance +
key-generation wrapper. Runs `echo scarf-ok` over a one-shot
Citadel exec for the onboarding connection test. One FIXME on
buildClientSettings because Citadel 0.7→0.9 shifted the
`.ed25519(...)` authentication-method variant name; every other
line is Citadel-version-independent. Gated on
canImport(Citadel) && canImport(CryptoKit).
## scarf/scarf-ios/ app source tree
- App/ScarfIOSApp.swift — @main, RootModel routes to
onboarding or dashboard based on
stored state.
- Onboarding/OnboardingRootView.swift — 8 sub-views, one per
OnboardingStep. Validated
server-details form, key-source
picker, generate / show-public
/ import / test / retry /
connected.
- Dashboard/DashboardView.swift — M2 placeholder: connected host
details + Disconnect button.
M3 replaces with real data.
## scarf/scarf-ios/SETUP.md
Step-by-step Xcode project creation:
- iOS 18 / iPhone-only / team 3Q6X2L86C4 / Bundle ID
com.scarf.scarf-ios / Swift 5 language mode.
- Wire Packages/ScarfCore + Packages/ScarfIOS (Citadel resolves
transitively).
- Replace Xcode's default scaffolded files with this source tree.
- Smoke-test procedure (simulator → physical iPhone).
- TestFlight upload steps.
- Troubleshooting for the known Citadel-variant-name drift.
## Test coverage (Linux, `swift test`)
M2OnboardingTests, 26 new tests (ScarfCore):
- SSHKeyBundle memberwise + display fingerprint
- InMemorySSHKeyStore + InMemoryIOSServerConfigStore round-trips
- IOSServerConfig.toServerContext bridging (with + without
remoteHome override)
- All OnboardingLogic validators (empty / whitespace / port range /
legacy-RSA rejection / public-key line parser)
- MockSSHConnectionTester scripting (success + failure)
- 10 OnboardingViewModel end-to-end paths: happy-path
save-and-test, invalid-host blocks advance, connection-failure
routes to .testFailed (and crucially does NOT save config),
retry-after-failure-works, import-happy, import-rejects-bad-PEM,
reset clears all state
ScarfIOSSmokeTests, 3 tests (Apple-only, won't run on Linux):
- Ed25519KeyGenerator bundle shape + base64 wire format
- OpenSSH public-key line byte-length pinned at 51 bytes
- Corrupted PEM rejection on round-trip decode
Running
docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work swift:6.0 swift test
reports **88 / 88 passing** (62 pre-M2 + 26 new).
## Real bug caught in development
First pass of OnboardingViewModel had `confirmPublicKeyAdded()` set
`isWorking=true`, then call `runConnectionTest()` which bailed on
`!isWorking` — meaning the connection probe never ran and the config
was never saved. Caught by the end-to-end test. Fixed by extracting
the shared probe body into `performConnectionTest()` and letting
both entry points own their own `isWorking` transition.
## Manual validation still needed on Mac
1. Xcode project creation per SETUP.md — confirm the resulting
project builds cleanly.
2. Citadel 0.9.x authentication-method variant — verify the one
FIXME line in buildClientSettings.
3. End-to-end onboarding: simulator against `localhost:22` (or a
test host), then TestFlight → physical iPhone → real SSH host
with the shown public key in authorized_keys.
Updated scarf/docs/IOS_PORT_PLAN.md with M2's shipped scope, the
scope decision about NOT generating the xcodeproj, and the list of
rules M3+ can rely on (Citadel transport dispatch, ChannelFactory
hook, single-server invariant).
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
bdf31d6781 |
iOS port M1: decouple ACPClient from Process via ACPChannel protocol
Introduces the key architectural abstraction that lets iOS share the
ACP state machine with Mac in M4+. ACPClient no longer touches
`Process`, `Pipe`, file descriptors, or SSH sessions directly — it
reads / writes line-oriented JSON-RPC through an `ACPChannel`.
New in ScarfCore/ACP/:
- ACPChannel.swift (protocol + ACPChannelError enum)
- ProcessACPChannel.swift (Mac + Linux; `#if !os(iOS)` guard —
iOS can't spawn subprocesses). Wraps the Process + Pipe +
raw POSIX write(2) code that used to live inline inside
ACPClient: SIGPIPE-ignore, partial-write loops, EPIPE →
`.writeEndClosed`, graceful SIGINT + 2s SIGKILL watchdog.
Uses `canImport(Darwin)` / `canImport(Glibc)` for the
platform-specific `write(2)` binding.
- ACPClient.swift (moved from scarf/Core/Services and refactored).
Process/Pipe/stdinFd/Darwin.write state replaced with a single
`channel: any ACPChannel` reference. Construction takes a
`ChannelFactory = @Sendable (ServerContext) async throws -> any ACPChannel`
closure — Mac wires ProcessACPChannel, iOS will wire a Citadel
SSHExecACPChannel in M4.
Mac-side glue (stays in main target):
- scarf/Core/Services/ACPClient+Mac.swift (new) carries the
`ACPClient.forMacApp(context:)` factory. Internally spawns
`hermes acp` locally or `ssh -T host -- hermes acp` remotely
via SSHTransport.makeProcess, passing the enriched shell env
(local: full PATH + credentials; remote: just SSH_AUTH_SOCK
+ SSH_AGENT_PID) with TERM stripped. Behaviour identical to
pre-M1.
- ChatViewModel updated at 3 sites from `ACPClient(context:)`
to `ACPClient.forMacApp(context:)`.
Public API change callers need to know about:
- `ACPClient.respondToPermission(requestId:optionId:)` is now
`async`. ChatViewModel already `await`ed it, so that upgrade
is a no-op; no other callers.
Also deleted scarf/Core/Services/ACPClient.swift (605 lines;
replaced by ScarfCore version).
Test coverage (M1ACPTests, 10 tests):
Using a MockACPChannel actor to script JSON-RPC deterministically,
not a real subprocess:
- ACPChannel protocol (mock send/receive, write-after-close,
error descriptions).
- ACPClient initial state.
- start() sends initialize and flips isConnected on reply.
- RPC error reply surfaces as ACPClientError.rpcError.
- Mid-flight channel close → pending request resolves with
.processTerminated, isConnected flips false.
- session/update notification routes into the `events` stream
as .messageChunk.
- Stderr lines feed the recentStderr ring buffer.
- ACPErrorHint.classify across credential / missing-binary /
rate-limit / unknown cases.
`swift test` on Linux now reports 62 / 62 passing.
Updated scarf/docs/IOS_PORT_PLAN.md with M1's shipped state, the
behavior-preservation rationale for the Mac factory, and the
iOS hook point M2–M4 will plug into.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
920c86b4f8 |
M0 verification: fix two real regressions before starting M1
Two bugs caught by a post-M0d audit, both of which would have bitten
users before any test exercised them on Mac:
1. GatewayViewModel.swift lost its `import ScarfCore` during the
M0d revert (when I moved it back to the Mac target after finding it
wasn't portable). The file references ServerContext everywhere and
wouldn't compile in Xcode without the import. Added back.
2. SSHTransport.sshSubprocessEnvironment() regressed in M0b.
The original Mac code ran HermesFileService.enrichedEnvironment(),
which tries `zsh -l -i` (login + interactive, with prompt-framework
defangs) FIRST, then falls back to `zsh -l`. Most users with
1Password / Secretive / manual ssh-add export SSH_AUTH_SOCK from
their `.zshrc` (interactive shell init), NOT `.zprofile`. My M0b
replacement used `zsh -l` only — so it would have silently failed
to find their ssh-agent socket, and SSH auth would break with
"Permission denied" (exit 255) for everyone who set up their
agent the normal way.
Fix is a dependency-inversion injection point instead of a local
shell probe: SSHTransport.environmentEnricher is a `(@Sendable () ->
[String: String])?` static that the Mac target wires at launch to
HermesFileService.enrichedEnvironment(). Same exact code path
executed as before M0b; no duplication; iOS leaves it `nil` and
falls back to ProcessInfo.processInfo.environment (Citadel will
own the SSH agent on iOS in M4+, not the login shell). Tests can
set a stub closure.
scarfApp.init() now sets `SSHTransport.environmentEnricher = {
HermesFileService.enrichedEnvironment() }` right before the
existing warm-up Task.
Test coverage: M0b suite gains `sshTransportEnvironmentEnricherInjection`,
which pins the injection-point shape so a future refactor can't
silently drop it.
Audit results (for confidence before M1):
- Exhaustive grep of every moved type across main target → 0 files
reference ScarfCore types without `import ScarfCore` (after the
GatewayVM fix).
- `scarf.xcodeproj/project.pbxproj` has no stale path references
(PBXFileSystemSynchronizedRootGroup auto-discovers).
- `xcshareddata/xcschemes/*.xcscheme` has no stale path references.
- `.build/` correctly gitignored.
- Zero leftover temp scripts / `.orig` / `.bak` files.
`swift test`: 52 / 52 passing on Linux.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
8bd4b9282a |
iOS port M0d: extract 6 portable ViewModels to ScarfCore
Fourth and final M0 sub-PR. Wraps up the ScarfCore extraction with the
ViewModels that have no dependency on Mac-target services or AppKit.
Views deliberately stay in the Mac target — see plan for rationale.
Moved (6 VMs):
ActivityViewModel.swift — HermesDataService consumer, SQLite3-gated
ConnectionStatusViewModel.swift — @MainActor heartbeat for remote SSH
InsightsViewModel.swift — HermesDataService aggregator, SQLite3-gated
(+ InsightsPeriod, ModelUsage, PlatformUsage,
ToolUsage, NotableSession types; exports
free functions formatDuration/formatTokens)
LogsViewModel.swift — HermesLogService consumer, fully portable
(+ nested LogFile / LogComponent enums)
ProjectsViewModel.swift — ProjectDashboardService wrapper, portable
RichChatViewModel.swift — ~700 lines of ACP-event + message-group
handling, SQLite3-gated
(+ ChatDisplayMode, MessageGroup types)
Reverted in-flight:
GatewayViewModel.swift — my audit missed that it calls
`context.runHermes(...)`, a Mac-target-only extension. Not portable
without moving HermesFileService too. Left in the Mac target.
Platform guards applied:
- `#if canImport(SQLite3)` wraps entire files for ActivityVM, InsightsVM,
and RichChatVM (they transitively depend on HermesDataService).
- `#if canImport(Darwin)` around LocalizedStringResource displayName
in LogsViewModel's nested LogFile and LogComponent enums.
- `#if canImport(os)` around the unused Logger in
ConnectionStatusViewModel (kept the field for future use).
Swift 6 / Observation notes:
- `import Observation` explicitly added to each @Observable file.
Mac target gets Observation via SwiftUI; ScarfCore doesn't import
SwiftUI, so it needs the explicit module import. Observation ships
in the Swift 5.9+ standard library on every platform.
- Nested enums' `var id: String { rawValue }` had to be manually
promoted to `public var id` since my sed only touches 4-space-indent
declarations and the nested enum's members are at 8-space indent.
- Two accidentally-publicized function-local `let` variables in
InsightsViewModel reverted back to internal.
- Sed adjustment: an earlier pattern was producing `@Observable public`
which is a Swift syntax error. Fixed post-hoc by stripping the
stray trailing `public` after the attribute; noted in the plan file
as a checklist item for M1+ sed work.
Consumer import sweeps:
4 Mac-target files gained `import ScarfCore` for the moved VM types:
ContentView.swift, ChatView.swift, RichChatView.swift, and
ConnectionStatusPill.swift.
Test coverage (M0dViewModelsTests): 14 new tests.
- ConnectionStatusViewModel: local-always-connected, remote idle-start,
Status Equatable pinning.
- LogsViewModel: init defaults, filteredEntries across level / search /
component filters, nested enum Identifiable ids and loggerPrefix.
- ProjectsViewModel: .local context binding.
- (SQLite3-gated, Apple-only):
ActivityVM construction, InsightsVM period defaults and sinceDate
ordering, ChatDisplayMode case coverage, RichChatVM empty-state
invariants, MessageGroup derived properties.
Running `docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work
swift:6.0 swift test` now reports 51 / 51 passing on Linux
(M0a 16 + M0b 18 + M0c 8 + M0d 9 + smoke 1 − 5 SQLite3-gated).
Apple-target CI should see 56 / 56 with the 5 gated tests added in.
Updated scarf/docs/IOS_PORT_PLAN.md with M0d's shipped state, the
Views-stay-Mac-only scope decision, and the sed-gotcha checklist
future phases should watch for.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
27dc694aeb |
iOS port M0c: extract portable Services to ScarfCore
Third of four M0 sub-PRs. Moves the four Services that have no dependency
on Mac-target code or AppKit into ScarfCore, so the Mac + (future) iOS
targets can share them.
Files moved (4):
scarf/Core/Services/HermesDataService.swift (658 lines, SQLite reader + SnapshotCoordinator actor)
scarf/Core/Services/HermesLogService.swift (log tail + parse, LogEntry + LogLevel)
scarf/Core/Services/ModelCatalogService.swift (models.dev JSON reader, HermesModelInfo + HermesProviderInfo)
scarf/Core/Services/ProjectDashboardService.swift (per-project dashboard I/O)
Not moved, with reason:
HermesFileService.swift — carries the big shell-enrichment logic; a
later phase can port once iOS has a clearer env story for ACP spawns.
HermesEnvService.swift — depends on HermesFileService.
HermesFileWatcher.swift — depends on HermesFileService.
ACPClient.swift — M1's job (the ACPChannel refactor).
UpdaterService.swift — wraps Sparkle, stays Mac-only forever.
Platform guards:
HermesDataService.swift is wrapped in `#if canImport(SQLite3) ... #endif`
for the whole file. SQLite3 isn't a system module on Linux
swift-corelibs-foundation. Apple platforms compile unchanged. Linux
builds skip the file entirely; nothing in ScarfCore references
HermesDataService from outside the file, so there's no downstream
fallout.
ModelCatalogService `import os` / Logger definition / call site all
guarded with `#if canImport(os)`. Linux gets silent logging.
HermesLogService + ProjectDashboardService use only Foundation —
no guards needed.
Other fixes:
- Features/Settings/Views/Components/ModelPickerSheet.swift (the one
remaining consumer) gains `import ScarfCore`.
- Self-referential `import ScarfCore` stripped from each moved file.
Test coverage: 8 new tests in ScarfCoreTests/M0cServicesTests.swift:
- HermesLogService.parseLine exercised via readLastLines on a real
tmp file with three formats — v0.9.0+ with session tag, older
without, and garbage fallback. Pins CLAUDE.md's optional-session-tag
invariant.
- LogLevel SwiftUI colour strings pinned.
- HermesModelInfo.contextDisplay across 1M / 200K / 500 / nil cases;
costDisplay with and without costs.
- ModelCatalogService load path end-to-end against a synthetic
models_dev_cache.json lookalike — providers sorted, models
filtered, provider(for:) resolves both full-scan and slash-prefixed
IDs.
- Malformed + missing catalog files return empty, no crash.
- ProjectDashboardService round-trips ProjectRegistry + reads a
synthetic .scarf/dashboard.json.
Running `docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work
swift:6.0 swift test` now reports 42 / 42 passing (M0a 16 + M0b 18 +
M0c 8).
Updated scarf/docs/IOS_PORT_PLAN.md progress log with the shipped M0c
state and the SQLite3-gating pattern future phases should reuse.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
0fd2ceb9fc |
iOS port M0b: extract Transport + ServerContext to ScarfCore
Second of four M0 sub-PRs. Moves the remaining cross-cutting
infrastructure — the ServerTransport protocol and its two implementations
(LocalTransport, SSHTransport), plus ServerContext and its helpers —
into ScarfCore so both Mac and (future) iOS targets share one codebase.
Files moved (5):
- scarf/Core/Transport/ServerTransport.swift (+ FileStat, ProcessResult, WatchEvent)
- scarf/Core/Transport/LocalTransport.swift
- scarf/Core/Transport/SSHTransport.swift
- scarf/Core/Transport/TransportErrors.swift
- scarf/Core/Models/ServerContext.swift (+ SSHConfig, ServerKind, ServerID, UserHomeCache)
Split out of ServerContext.swift into a new Mac-target sibling file
scarf/Core/Models/ServerContext+Mac.swift:
- runHermes(_:timeout:stdin:) — depends on HermesFileService
- openInLocalEditor(_:) — depends on AppKit.NSWorkspace
These methods can't live in ScarfCore itself because ScarfCore must not
depend on main-target services or AppKit. iOS will provide a sibling
ServerContext+iOS.swift in M2+.
Removed: scarf/Core/Models/HermesPaths+Deprecated.swift.
Zero callers in-tree; its only justification was that ServerContext
used to be in the Mac target. With ServerContext in ScarfCore now,
the deprecated forwarders are both unreachable AND dead code.
Breaking the ScarfCore → main-target circular dep in SSHTransport:
The old SSHTransport.sshSubprocessEnvironment() called
HermesFileService.enrichedEnvironment() to harvest SSH_AUTH_SOCK from
the user's login shell. Replaced with a local #if os(macOS) helper
SSHTransport.macLoginShellSSHAgent() that probes /bin/zsh for only
the two SSH agent vars (no PATH/credentials — that's still in
HermesFileService for ACP subprocess use). Behavior-identical on
macOS; no-op on iOS/Linux.
Platform guards added in ScarfCore (runtime targets still macOS/iOS):
- `#if canImport(os)` around os.Logger (definition + every call site,
except the large Darwin-dependent ensureControlDir block).
- `#if canImport(Darwin)` around LocalTransport.watchPaths (FSEvents)
and SSHTransport.ensureControlDir (Darwin.stat/lstat). Linux gets
a no-op empty stream and a best-effort FileManager.createDirectory
fallback — neither is exercised at runtime on Linux, only compiled.
- `#if canImport(SwiftUI)` around ServerContext's EnvironmentKey.
- `#if canImport(AppKit)` inside the new ServerContext+Mac.swift
extension.
Bug fixed: M0a's sed transform accidentally added `public` to protocol
requirements in ServerTransport.swift, e.g. `public nonisolated var
contextID: ServerID { get }`. Swift forbids access modifiers on
protocol requirements — stripped.
54 additional consumer files in the Mac target gained `import ScarfCore`.
Test coverage: 18 new tests in ScarfCoreTests/M0bTransportTests.swift.
Runs on Linux via
docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work swift:6.0 swift test
Total suite: 34 / 34 passing (M0a's 16 + M0b's 18).
Updated scarf/docs/IOS_PORT_PLAN.md progress log with the shipped M0b
state and the Platform-guard patterns future phases should reuse.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
f6f31cabe4 |
M0a fixup: unignore local Packages/, add missing files, make Linux CI pass
The initial M0a commit was incomplete: .gitignore's `Packages/` rule
(meant for the legacy pre-Xcode-14 SwiftPM checkout dir) silently
swallowed three new files that SHOULD have been committed:
- scarf/Packages/ScarfCore/Package.swift
- scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConstants.swift
- scarf/Packages/ScarfCore/Tests/ScarfCoreTests/ScarfCoreSmokeTests.swift
The 12 moved models slipped through because `git mv` preserves tracking
across gitignored destinations, but new files in that tree did not.
Fix: add `!scarf/Packages/` override so our local SPM package is always
tracked; keep the top-level `Packages/` ignore for the historical case.
Also verified M0a builds + tests green on Linux via
`docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work swift:6.0 swift test`.
To make that work, two small, Apple-platform-preserving guards:
- `sqliteTransient` in HermesConstants.swift wrapped in
`#if canImport(SQLite3)` — SQLite3 is not a system module on Linux
swift-corelibs-foundation. Apple builds compile unchanged.
- `ToolKind.displayName` and `MCPTransport.displayName` wrapped in
`#if canImport(Darwin)` — `LocalizedStringResource` is Apple-only.
Apple builds compile unchanged.
Additionally:
- Package.swift pinned to Swift 5 language mode, matching the Mac app's
`SWIFT_VERSION = 5.0`. Two types (`ACPEvent.availableCommands` and
`ACPToolCallEvent.rawInput`) claim `Sendable` while carrying
`[String: Any]` — strict Swift 6 rejects that. Comment in Package.swift
flags this for a future typed-payloads cleanup + bump to `.v6`.
- ScarfCoreSmokeTests now contains 16 tests exercising every M0a
`public init` so parameter drift fails CI instead of a reviewer.
- IOS_PORT_PLAN.md updated with what actually shipped, the Linux-CI
guards + patterns future phases should reuse, and the Sendable
follow-up flagged under "Rules next phases can rely on".
Test results (Linux, Swift 6.0.3):
Suite M0aPublicInitTests: 15 tests passed
Suite ScarfCoreSmokeTests: 1 test passed
Total: 16 / 16 passed
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
bb5045c10f |
iOS port M0a: extract 13 leaf Models to new ScarfCore local SPM package
First of four M0 sub-PRs that carve a platform-neutral ScarfCore package
out of the Mac app, in preparation for an iOS target. This PR is
Mac-only — no iOS target yet, no behavior changes expected.
What moves to ScarfCore:
- 13 leaf model files (HermesSession, HermesMessage, HermesConfig and
its 19 nested Settings structs, HermesCronJob, HermesMCPServer,
HermesSkill, HermesSlashCommand, HermesTool + KnownPlatforms,
HermesPathSet, MCPServerPreset, ProjectDashboard family, ACPMessages).
- Portable half of HermesConstants.swift (sqliteTransient, QueryDefaults,
FileSizeUnit). The deprecated HermesPaths enum stays in main target
as HermesPaths+Deprecated.swift since it references ServerContext.
What stays in the Mac target:
- ServerContext.swift (moves in M0b alongside Transport — depends on
LocalTransport/SSHTransport + HermesFileService).
- HermesPaths+Deprecated.swift (dead forwarders, zero callers in-tree;
kept for safety until M0b can clean them up).
Mechanics:
- New Packages/ScarfCore/Package.swift targeting macOS 14 / iOS 18,
Swift 6 language mode.
- Every moved type and member marked public; explicit public memberwise
init added to every struct (Swift's synthesized memberwise init is
internal and would break cross-module construction).
- Xcode project references the package via XCLocalSwiftPackageReference
and links ScarfCore into the scarf target.
- 49 consumer files get `import ScarfCore` added.
See scarf/docs/IOS_PORT_PLAN.md for the full multi-phase plan, locked
decisions (iOS 18, iPhone only, no APNs v1), and the M0b–M6 roadmap.
Manual verification checklist:
- Open scarf.xcodeproj in Xcode and build the scarf scheme — should
resolve the local package and compile with no new errors.
- Run scarfTests — should pass (tests don't touch moved types).
- Smoke-run the app: Dashboard, Sessions, Chat, Memory should render
with identical data to pre-PR.
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
|
||
|
|
3e0d2db4c7 |
fix(catalog): accept git worktrees for gh-pages check
`need_ghpages` was testing `[[ -d "$GHPAGES_DIR/.git" ]]` — "is .git a directory?". That's true for a regular clone but FALSE for a `git worktree add` worktree, where `.git` is a pointer file (contains `gitdir: …/main-repo/.git/worktrees/<name>`) rather than the directory itself. `release.sh` creates the gh-pages worktree as part of its flow; after release the worktree persists with a `.git` file but `catalog.sh publish` would then refuse to run because of the dir-only check. Switched to `-e` (exists, either file or directory). Updated the surrounding comment so the next poor soul doesn't delete the worktree on the script's own (wrong) advice. Caught when publishing the v2.2.0 template catalog — error told the user to re-create a worktree that was already there and valid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2b25a9da71 | chore: Bump version to 2.2.0 v2.2.0 | ||
|
|
5fb9620631 |
Merge branch 'project-sharing': v2.2.0 — templates + configuration + catalog
Brings in 22 commits delivering the full v2.2.0 scope:
- Project Templates: .scarftemplate bundle format (install, uninstall,
export, URL router) + install preview sheet + cross-agent AGENTS.md
- Template Configuration (schemaVersion 2): typed schema with 7 field
types, Keychain-backed secrets, Configure step in install flow,
post-install Configuration editor, model recommendations
- Template Catalog: gh-pages site generated from templates/<author>/<name>/,
stdlib-only Python validator mirroring Swift invariants, PR CI gate,
install-URL hosting from raw main
- Example template: awizemann/site-status-checker (config + cron + Site
tab webview updates)
- Site tab: webview widget in any dashboard exposes a second tab
- UX: Remove from List vs. Uninstall Template clarification, preserved-
files banner, Run Now no longer blocks on long agent runs, markdown
in install sheet, install-time {{PROJECT_DIR}} token substitution
Release notes at releases/v2.2.0/RELEASE_NOTES.md (94 lines).
Wiki page at https://github.com/awizemann/scarf/wiki/Project-Templates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
de5b278da4 |
docs: expand v2.2.0 release notes + README for full 2.2 scope
The pre-existing release notes and README "What's New in 2.2" block
only covered the original Project Templates feature. This expands
both to reflect everything that's actually shipping in 2.2:
- Template Configuration (schemaVersion 2): typed schema, 7 field
types, Keychain-backed secrets, configure step in install flow,
post-install Configuration editor, model recommendations.
- Template Catalog: gh-pages site with live dashboard previews,
stdlib-only Python validator mirroring Swift invariants, PR CI
gate, install-URL hosting from raw main.
- Example template `awizemann/site-status-checker` exercising every
v2.2 surface — config form, cron, Site tab webview, dashboard
updates.
- Site tab — a webview widget in any dashboard exposes a second
tab next to Dashboard, rendering a live URL.
- UX clarifications: Remove from List (keep files) vs. Uninstall
Template (remove installed files), preserved-files banner on
uninstall success, Run Now no longer blocks on long agent runs.
- Install-time {{PROJECT_DIR}} / {{TEMPLATE_ID}} / {{TEMPLATE_SLUG}}
token substitution in cron prompts.
Release-notes link + wiki link surfaced at the bottom of the README
block so readers have a jump to full details.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
fb7a80f191 |
fix: Run Now agent-run timing + non-404 webview placeholder
Two independent fixes that both blocked the "install → Run Now → see the Site tab render" loop. 1. CronViewModel.runNow stopped blocking on `cron tick`. Previously the UI waited up to 60 s on the tick before deciding whether the job succeeded, so any agent run that did real work (an LLM call + a few HTTP GETs + a file write = easily 90 s+) surfaced a false "Run failed" toast while the job kept running in the background. Dashboard updates landed minutes later, confusing the user. New shape: show "Agent started — dashboard will update when it finishes" the instant `cron run` queues the job, then call `cron tick` with a 300 s timeout to force execution. Tick failures are logged but don't overwrite the started-toast — HermesFileWatcher picks up the dashboard.json rewrite automatically when the agent finishes. 2. site-status-checker's webview widget pointed at `github.com/awizemann/scarf/tree/main/templates/awizemann/...`. The templates/ path only exists on project-sharing, not main, so GitHub returned 404 in the Site tab until the first cron run replaced the URL with the user's configured site. Switched the placeholder to `awizemann.github.io/scarf/` which always renders. Bundle + catalog rebuilt against the updated dashboard.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
18640293f7 |
fix(projects): clarify remove-vs-uninstall UX
Three UX changes addressing user feedback that "Remove from Scarf" and "Uninstall Template…" looked interchangeable, and that users were surprised when uninstall left the project folder behind. - Rename sidebar menu entries: "Uninstall Template…" → "Uninstall Template (remove installed files)…" "Remove from Scarf" → "Remove from List (keep files)…" The expanded labels carry the scope difference at the point of click. - Add a confirmation dialog for Remove from List. The sidebar's "-" button and the context-menu entry both route through it. Dialog copy explicitly spells out "Nothing on disk is touched — the folder, cron job, skills, and memory block all stay. To actually remove installed files, use 'Uninstall Template…' instead." Sidebar "-" also gains a help tooltip saying the same thing. - Post-uninstall preserved-files banner. When the uninstaller keeps the project directory (because the cron wrote a status-log.md or the user dropped files in there), the success view now shows an orange banner listing up to 8 preserved paths with a "+N more…" tail, plus a one-line explanation and a pointer to delete the folder from Finder if the user doesn't want those files. VM captures the preservation shape before nil'ing `plan` on success. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
19750597cd |
feat(site-status-checker): add Live Site Preview webview for Site tab
A Scarf project dashboard that includes at least one webview widget automatically exposes a Site tab next to the Dashboard tab. Adding a "Live Site Preview" section with a webview widget gives this template that tab out of the box. The cron job + AGENTS.md now tell the agent to rewrite the webview's `url` field to the first entry in `values.sites` on each run, so the Site tab renders whatever the user actually configured instead of the GitHub placeholder. If `values.sites` is empty, the webview URL is left untouched. Swift example test updated to assert 4 sections (was 3) plus the new webview widget's presence + title; bundle + catalog rebuilt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
69e9cc6c7b |
fix(cron): Run now now actually runs + markdown rendering in install sheet
Two fixes chained from manually testing site-status-checker v1.1.0.
---
Cron Run now was a no-op when the Hermes gateway scheduler wasn't
already running. `hermes cron run <id>` only marks a job as due on
the next scheduler tick — it doesn't execute. During dev or right
after install (gateway stopped, as the logs the user pasted showed),
the user's click resulted in nothing happening: job queued, tick
never comes, zero agent sessions, zero output, dashboard never
updates. Exactly the failure mode they hit.
Fix: CronViewModel.runNow now calls `hermes cron run <id>` followed
by `hermes cron tick` after a short delay. `tick` runs all due jobs
once and exits — so the just-queued job actually executes, and
exits cleanly whether the scheduler is running or not. Redundant
(not duplicative) when the gateway is live. The user sees a status
message whether it succeeded or failed instead of silent nothing.
---
Markdown rendering in install-sheet screens. Template READMEs,
manifest descriptions, field help text, and cron prompts all
reasonably contain markdown — but the install preview sheet was
rendering everything as plain text, so `[Create one](https://…)`
would appear verbatim instead of as a link, `# Site Status Checker`
as a literal pound sign, etc.
New Features/Templates/Views/TemplateMarkdown.swift — a tiny,
dependency-free markdown renderer scoped to what template authors
actually write:
- Headings (#..######) → larger bold Text with vertical spacing
- Bullet and numbered lists → hanging-indent rows with •/1. prefix
- Fenced code blocks (```) → monospaced with quaternary background
- Paragraphs → regular Text, with inline formatting via SwiftUI's
built-in AttributedString(markdown:) so **bold**, *italic*,
`code`, and [links](urls) work
- Blank lines separate blocks
Two entry points: `TemplateMarkdown.render(_ source:)` returns a
View for multi-block content (README preview), and
`TemplateMarkdown.inlineText(_ source:)` returns a Text for
one-line strings where block structure doesn't apply (field
descriptions, manifest tagline).
Wired into:
- TemplateInstallSheet.readmeSection — was plain Text(readme), now
renders the full README with structure.
- TemplateInstallSheet.manifestHeader description — inline-only
(taglines rarely have block structure).
- TemplateInstallSheet.cronSection — new DisclosureGroup per cron
job exposes the full prompt with markdown rendering. Users can
now verify what the installer will register with Hermes before
clicking Install. {{PROJECT_DIR}} / {{TEMPLATE_ID}} tokens show
unresolved here; they get substituted when the installer calls
hermes cron create.
- TemplateConfigSheet field descriptions — inline markdown so
`[Create a token](https://...)`-style links render as real links.
Not a full CommonMark implementation — no tables, no blockquotes,
no images, no HTML passthrough. Those can evolve as templates need
them. Safe with untrusted input: never executes scripts or renders
raw HTML.
Scope stays tight: 57/57 Swift tests + 24/24 Python tests still pass.
No new tests for the markdown helper itself — rendering is visual,
hard to unit-test meaningfully without snapshot-testing infra, and
the surface is small enough that changes would be caught by the
visual regression of any template install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
03bf5262bb |
feat(templates): install-time {{PROJECT_DIR}} substitution in cron prompts
Hermes doesn't set a working directory when firing cron jobs, so any
relative path in a template's cron prompt (`.scarf/config.json`,
`status-log.md`, etc.) resolves against whatever dir Hermes happens
to be in — NOT the installed project. Practical effect: site-status-
checker's cron job fires, agent runs with relative paths, finds
nothing to read, silently bails. User sees "Run now" complete with
zero output and nothing updated on disk.
Fix: the installer now substitutes template-author placeholders in
cron prompts at install time, before calling `hermes cron create`.
The registered cron job carries a fully-qualified, CWD-independent
prompt.
Supported tokens (deliberately few — each is part of the template
format contract from now on):
- `{{PROJECT_DIR}}` — absolute path of the installed project dir.
The one that was motivating this fix; required for any cron prompt
that reads or writes project files.
- `{{TEMPLATE_ID}}` — the `owner/name` from the manifest, for
templates that want to tag delivery payloads or log lines.
- `{{TEMPLATE_SLUG}}` — the sanitised slug used by the installer for
dir name + skills namespace, for templates that want to reference
their skills install path.
Implemented as a static `ProjectTemplateInstaller.substituteCronTokens`
so it's testable as a pure function. Unsupported placeholders pass
through verbatim — template authors notice in testing that their
token didn't get replaced and either use a supported one or file
a request.
Site Status Checker v1.1.0 updated to use the tokens:
- cron/jobs.json prompt now opens with "Run the site status check
for the Scarf project at {{PROJECT_DIR}}" and references
{{PROJECT_DIR}}/.scarf/config.json, {{PROJECT_DIR}}/status-log.md,
and {{PROJECT_DIR}}/.scarf/dashboard.json explicitly.
- AGENTS.md gains a note explaining that the cron-registered prompt
carries absolute paths (installer substitutes at install time),
while interactive-chat agents can keep using relative paths.
- bundle rebuilt, catalog regenerated.
templates/CONTRIBUTING.md documents the three supported tokens under
the cron/jobs.json bullet so future authors don't have to discover
this by hitting the same CWD bug.
Tests:
- ProjectTemplateExampleTemplateTests.siteStatusCheckerParsesAndPlans
extended to assert the bundled prompt contains {{PROJECT_DIR}}
UNRESOLVED. If someone accidentally bakes an absolute path into
the template (their install dir), every user of that template
would get the wrong path — this test catches that.
- Four new substitution tests in ProjectTemplateInstallerTests:
resolves PROJECT_DIR / resolves ID + SLUG / leaves unknown tokens
untouched / substitutes repeated occurrences. All go through the
static helper directly; no install round-trip needed.
57/57 Swift tests + 24/24 Python tests pass. Catalog check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3af99d9d9c |
fix(templates): site-status-checker dashboard no longer lies before first run
The template's dashboard shipped with two hardcoded example URLs (https://example.com + https://example.org) baked into a "Configured Sites" list widget, and the widget title still said "from sites.txt" — stale from the v1.0.0 layout before we moved to config.json. After the v1.1.0 configure-on-install flow lands, the user fills in a real sites list through the Configure form (which correctly lands in `.scarf/config.json` — the editor modal confirms that), but the dashboard still rendered the baked-in example URLs. The agent would overwrite them on the first cron run, but until then the dashboard misrepresents reality. Two orthogonal paths to fix this — populate the dashboard's items from config.json at install time (requires Scarf-side template-value interpolation, which is a v2.3.1 feature), or ship a dashboard that clearly advertises "nothing has run yet." Taking the second path for v1.1.0: replace the example URLs with a single placeholder row with status "pending" pointing the user at running the check. The agent replaces the row with real data on the first cron run. Also: widget title fixed ("Watched Sites (populated after first run)" instead of the stale sites.txt reference), top-of-dashboard description updated, and the Quick Start text now mentions the Configuration button as the way to set sites, not the long-gone sites.txt. Bundle + catalog rebuilt; ProjectTemplateExampleTemplateTests still passes (it asserts against cron prompt + schema shape, not dashboard content, so the dashboard edit doesn't affect it). --- Secondary fix: test deflake from the saveRegistry throw change. Making saveRegistry throw exposed a pre-existing parallel-test race: three suites (ProjectTemplateInstallerTests, ProjectTemplateUninstallerTests, ProjectTemplateConfigInstallTests) all write to the real `~/.hermes/scarf/projects.json`. Swift Testing's `.serialized` trait only serializes within a single suite — multiple suites still run in parallel. Before, writes silently failed on the racing-loser side and tests passed by accident; now the loser's test throws "couldn't be saved in the folder 'scarf'". Added TestRegistryLock — a module-level NSLock that all three suites' snapshotRegistry/restoreRegistry helpers share. acquireAndSnapshot() locks + reads; restore(_:) writes + unlocks. The paired snapshot-in-test-body / defer-restore pattern keeps acquire + release balanced. Replaced the three per-suite copies of the helpers with thin delegates to the shared lock. Verified by running the full test suite 3 consecutive times: 53/53 tests pass each run, no flakes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3bd95de8f4 |
fix(config): install sheet silently closed after Continue in config step
Two bugs chained into the observed "install completed but project didn't show up" report. Either one would have been enough on its own; both are here so both are fixed. Primary bug: TemplateConfigSheet's Cancel + Continue buttons each called `@Environment(\.dismiss)` after their state-update callbacks. That was fine when the sheet is presented standalone (the post-install Configuration button uses it this way and wants dismissal), but Phase C also INLINED the same view inside TemplateInstallSheet.configureView for the install flow's .awaitingConfig stage — there's no intermediate .sheet() presenter there, so `dismiss()` resolved to the OUTER install sheet. Clicking Continue → configure form's `onCommit` fired `installerViewModel.submitConfig(values:)` which advanced stage to .planned, then the dismiss() closed the whole install sheet before the preview ever rendered. install() was never called. Fix: remove both dismiss() calls from TemplateConfigSheet. Dismissal is now the caller's responsibility. ConfigEditorSheet (standalone mode) already calls `dismiss()` inside its own onCancel closure and lets the .succeeded state's Done button handle commit-dismissal, so nothing breaks there. The install flow's state machine advances to the preview stage where the existing Install/Cancel buttons drive everything from there. Secondary bug (latent, same class): ProjectDashboardService.saveRegistry swallowed both directory-creation and file-write errors with `try?`. If the `~/.hermes/scarf/` dir creation or projects.json write ever failed for any reason (permissions, readonly filesystem, sandbox), the installer's registerProject returned a valid-looking ProjectEntry while the registry on disk never received the row. Same symptom surface as the primary bug: install "succeeds," project invisible. Fix: saveRegistry now throws. Updated all four callers: - ProjectTemplateInstaller.registerProject: `try` — a registry write failure aborts install with a user-visible failure screen. This is the critical path; silent success on a destructive op is the exact failure mode we want to eliminate. - ProjectTemplateUninstaller: `do/catch` + logger.warning — we're at the final step of uninstall after every other side effect has already completed (files removed, skills removed, cron removed, memory stripped, Keychain cleared). Leaving a stale registry row pointing at a deleted project is cosmetic and easy to fix from the sidebar minus button. - ProjectsViewModel.addProject + removeProject: `do/catch` + logger.error. The VM doesn't currently have a surface for user-visible errors (no toast/alert on this view), but the failure now at least lands in the unified log instead of disappearing. Proper in-UI error surface is tracked as follow-up. - ProjectDashboardService.loadRegistry: switched its stale `print` to `logger.error` while I was in the file. Tests: added TemplateInstallerViewModelTests suite (3 tests) covering the install VM's configure-step state transitions: - submitConfigStashesValuesAndTransitionsToPlanned — .awaitingConfig → .planned + configValues stash on the plan. The exact transition that the dismiss() bug tore down mid-flight. - cancelConfigReturnsToAwaitingParentDirectory — back-button behaviour with plan preserved so re-entry doesn't re-run buildPlan. - submitConfigNoOpWhenPlanIsNil — defensive guard. These won't catch a view-level regression (Swift Testing doesn't do UI tests in this project), but they lock in the VM state-machine contract so the next refactor can't silently break submitConfig or cancelConfig without failing CI. 53/53 Swift tests + 24/24 Python tests + catalog validator clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
81e8da91d6 |
feat(templates): upgrade site-status-checker to v1.1.0 with config schema
First real exercise of the v2.3 configuration feature. The template no longer asks the agent to bootstrap sites.txt on first run — instead, users enter their list of URLs through the Configure form during install, and change them later via the dashboard's Configuration button. This makes the template a complete round-trip test of the new feature end-to-end. Schema (manifest.config.schema): - `sites` — list<string>, required, 1–25 items, default two example URLs. This is the list the cron job hits. - `timeout_seconds` — number, 1–60, default 10. Per-URL HTTP timeout. - `modelRecommendation.preferred = claude-haiku-4` — rationale: simple tool-use task, Haiku is cost-effective for daily cron. Manifest bumped: schemaVersion 1 → 2, version 1.0.0 → 1.1.0, minScarfVersion 2.2.0 → 2.3.0, contents.config = 2. AGENTS.md rewritten for the config-driven flow: - Reads values from `.scarf/config.json` at run time (values.sites + values.timeout_seconds). No more sites.txt bootstrap. - "Add a site" / "Remove a site" no longer mean the agent edits a file — they mean "open the Configuration button on the dashboard." The agent points the user there rather than trying to mutate config.json itself. A future Scarf release may expose a tool for agents to write config programmatically; until then, config is strictly a user action. - First-run bootstrap now only creates status-log.md (if absent). README.md rewritten to walk users through the new form-based flow, explain the Configuration button, and document the model recommendation. Uninstall instructions point at the right-click Uninstall Template action rather than manual steps. Cron prompt updated to reference config.json (values.sites, values.timeout_seconds) instead of sites.txt. ProjectTemplateExampleTemplateTests.siteStatusCheckerParsesAndPlans extended with v2-specific assertions: manifest.schemaVersion == 2, contents.config == 2, schema.fields.count == 2, per-field constraints (sites type/itemType/minItems/maxItems, timeout min/max), modelRecommendation.preferred, plan.configSchema + plan.manifestCachePath are populated, plan.projectFiles includes both config.json + manifest.json destinations. Cron-prompt assertion swapped from sites.txt to config.json/values.sites. Three suites that touch ~/.hermes/scarf/projects.json now carry .serialized — the new Phase B install-with-config tests stressed the parallel-execution race in the snapshot/restore helpers. Serializing within each suite deflakes without any architectural change. Swift 50/50, Python 24/24, catalog validator accepts the upgraded bundle. Site detail page now has manifest.json for renderConfigSchema to pick up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb750e237e |
docs: CLAUDE.md — add Template Configuration section
Documents the v2.3 configuration feature for future agent sessions: manifest schemaVersion 2 shape, supported field types, Keychain storage conventions (service/account naming with project-path hash suffix), the uninstaller's config-items cleanup path, exporter behaviour (schema forwarded, values stripped), and the catalog site's schema display. Includes the "Schema is Swift-primary" note so future edits to TemplateConfigField.FieldType go through the right order of updates — Swift first, then Python mirror, then widgets.js, then UI controls, then tests on both sides. Schema drift between Swift + Python validator would accept bundles the app later refuses at install time, which is a catastrophic UX failure for the catalog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
68f6b98fcf |
feat(catalog-config): mirror manifest v2 schema in validator + site
Phase D of v2.3 template configuration — closes the loop between the Swift app and the catalog pipeline. Authors can now ship schemaful bundles; the Python validator enforces the same invariants the Swift installer does; the catalog site displays the schema so visitors see what they'll need to configure before installing. Python validator (tools/build-catalog.py): - SUPPORTED_SCHEMA_VERSIONS accepts both 1 and 2 (v1 bundles are unchanged; v2 adds optional manifest.config). - New _validate_config_schema function mirrors the Swift ProjectConfigService.validateSchema rules: unique keys, supported types, enum option presence + unique values, list itemType == "string", secret-field cannot declare a default, modelRecommendation.preferred non-empty when present. - _validate_contents_claim cross-checks contents.config (field count) against config.schema actual length — mismatch refused. - TemplateRecord.to_catalog_entry exposes `config` in catalog.json so the site can render the schema. - render_site copies each bundle's template.json to the detail dir as manifest.json (only when the manifest has a config block — keeps the served tree lean and makes "no manifest.json" a meaningful 404 signal in the frontend). - catalog.json's own schemaVersion stays at 1 (independent of per- template manifest schemaVersion). Python tests (tools/test_build_catalog.py): 8 new cases in a new ConfigSchemaValidationTests suite — accepts schemaful bundle, rejects duplicate keys, rejects secret-with-default, rejects enum-without- options, rejects unsupported field type, rejects contents.config count mismatch, rejects unsupported list itemType, legacy v1 manifests pass unchanged. 24/24 Python tests total. Site (site/widgets.js): - New renderConfigSchema(container, config) — mirrors the display on the Scarf install preview. Renders each field as a <dt>/<dd> pair with type + required badges; enum shows choice labels; list fields show min/max bounds; string fields show pattern/length; secret fields get a "Stored in Keychain" reassurance. Optional modelRecommendation panel at the bottom with preferred + rationale + alternatives. - The renderer is display-only — the site never collects values; that's the Scarf app's job. template.html.tmpl adds a #config-schema <section>. The inline script fetches manifest.json from the detail dir; on success hands the config block to ScarfWidgets.renderConfigSchema; on 404 (schema-less templates) silently leaves the section empty. CSS in styles.css adds a config-schema panel matching the accent-green aesthetic. 24/24 Python + 50/50 Swift tests pass. site-status-checker still renders correctly (schema-less; manifest.json isn't copied for it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f8c086ee7a |
feat(config): configure-step UI + post-install Configuration editor
Adds the user-facing side of v2.3 template configuration. Install-time flow: templates with a non-empty config.schema get a Configure step between the parent-directory pick and the preview sheet. Post-install flow: a Configuration button on the dashboard header + a context-menu entry on the project list opens the same form pre-filled with current values. New files: - Features/Templates/ViewModels/TemplateConfigViewModel.swift — drives the form. Keeps freshly-entered secret bytes in `pendingSecrets` in-memory until commit() succeeds, then calls ProjectConfigService.storeSecret for each one. Cancelling never leaves orphan Keychain entries — the form is transactional. Validates via ProjectConfigService.validateValues on commit and populates per-field `errors` the sheet surfaces inline. Two modes: .install (needs a project passed at commit time) and .edit(project:) (VM already holds the target). - Features/Templates/Views/TemplateConfigSheet.swift — the form. One row per field with a control dispatched by type: TextField (string), TextEditor (text), number input, Toggle (bool), segmented/dropdown Picker (enum, picks form by option count), add/remove list editor, SecureField with show/hide toggle (secret). Required-field asterisk + per-field error display. Optional modelRecommendation panel at the bottom — informational badge; no auto-switch. - Features/Templates/ViewModels/TemplateConfigEditorViewModel.swift — loads <project>/.scarf/manifest.json + config.json, hands a TemplateConfigViewModel to the sheet, writes edited values back on commit. Has a .notConfigurable stage for projects without a manifest cache (hand-added projects, schema-less templates). - Features/Templates/Views/ConfigEditorSheet.swift — thin wrapper that owns the editor VM and routes its stages to loading / form / saving / success / error / not-configurable views. Wiring: - TemplateInstallerViewModel gains an .awaitingConfig stage between .awaitingParentDirectory and .planned. pickParentDirectory() now inspects plan.configSchema and either routes to .awaitingConfig (non-empty schema) or straight to .planned (schema-less). New submitConfig(values:) stashes finalized values in plan.configValues and advances; cancelConfig() returns to .awaitingParentDirectory. - TemplateInstallSheet renders a new `configureView` that inlines TemplateConfigSheet into the install flow for .awaitingConfig. The existing preview (.planned) gains a new "Configuration" section listing each field + its display value (secrets shown as "•••••• (Keychain)", lists shown as "first + N more", "(not set)" for missing values). - ProjectsView adds an isConfigurable(_:) check (transport.fileExists on .scarf/manifest.json), a new @State configEditorProject for sheet presentation, a new "Configuration…" context-menu entry on project list rows (for configurable projects), and a new slider.horizontal.3 button on the dashboard header next to the existing Uninstall button. 50/50 tests still pass. This commit is UI-only — no new Phase C tests (sheet behaviour is hard to unit-test without UI automation and the underlying VM logic is exercised by Phase A/B's config-round-trip tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eb34aec1f1 |
feat(config): template-config UI forms (configure sheet + editor)
Introduces the TemplateConfigSheet form and its view models, plus the install-flow integration points: a new .awaitingConfig stage in TemplateInstallerViewModel, the configureView step in the install sheet, and the dashboard-header Configuration button wiring in ProjectsView. This is the schemaful-template v2.3 UI that every subsequent config commit builds on. Originally landed alongside scaffolding for an iOS target in b289a83; this is the split that keeps the template-config work and drops the iOS scaffolding (the real iOS port is on scarf-mobile-development). |
||
|
|
97e9beea5f |
refactor(settings): remove unused providers list
The hardcoded `providers` array in SettingsViewModel was never referenced — no view reads `viewModel.providers`; the Model picker uses the models.dev catalog via `ModelCatalogService.loadProviders()` and Provider is shown as a `ReadOnlyRow` in the General tab. Leaving the dead list around makes issues like #33 look plausible (users reasonably guess a stale enum is normalising `openai-codex` → `openai` on save, which the code does not actually do). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7a99547b22 |
fix: address code-review findings from Apr 22 commits
Three follow-ups from reviewing |
||
|
|
64b7d3beaf |
feat(config): manifest schemaVersion 2 + installer/uninstaller/exporter wiring
Extends the template format to schemaVersion 2 (schema-less bundles at v1 keep working unchanged) and threads TemplateConfigSchema through inspect → buildPlan → install → uninstall → export end-to-end. Model additions (ProjectTemplate.swift): - ProjectTemplateManifest gains optional `config: TemplateConfigSchema?`. - TemplateContents gains optional `config: Int?` claim (field count) cross-checked against the schema by `verifyClaims` so a manifest can't hide its configuration from the preview sheet. - TemplateInstallPlan gains `configSchema`, `configValues` (populated by the VM just before install()), and `manifestCachePath`. New fields also feed totalWriteCount so the preview footer is honest. - TemplateLock gains optional `configKeychainItems: [String]?` and `configFields: [String]?`. Optional so pre-2.3 lock files still uninstall cleanly — Codable's default decoding skips missing fields. Service changes: - ProjectTemplateService.inspect now accepts schemaVersion 1 or 2. When the manifest declares a config block, the service validates it immediately via ProjectConfigService.validateSchema and fails the install with a manifestParseFailed before the preview sheet ever renders. verifyClaims cross-checks contents.config count against the actual schema length. - ProjectTemplateService.buildPlan populates configSchema and queues two new entries in projectFiles: .scarf/config.json (synthesized by the installer from configValues at write time, using an empty sourceRelativePath sentinel) and .scarf/manifest.json (copy of the bundle's template.json so the post-install Configuration editor can render offline). - ProjectTemplateInstaller.createProjectFiles now special-cases the empty-source sentinel: for .scarf/config.json, it encodes plan.configValues into a ProjectConfigFile on the fly. Secrets in that file are keychain:// refs — the raw bytes were routed into the Keychain by the VM before install() was called. - ProjectTemplateInstaller.writeLockFile records every keychainRef URI from configValues in lock.configKeychainItems and the schema field keys in lock.configFields. - ProjectTemplateUninstaller.uninstall adds a new step 4a: iterate lock.configKeychainItems, parse each URI into a TemplateKeychainRef, SecItemDelete each one. Absent items are no-ops (the Keychain wrapper already handles errSecItemNotFound silently). - ProjectTemplateExporter now reads the source project's .scarf/manifest.json (if present) and forwards the SCHEMA through to the exported bundle while zeroing values. schemaVersion bumps to 2 only when a schema is carried; schema-less exports stay at 1 for byte-compatibility with v2.2 catalog validators. Tests (ProjectTemplateTests.swift): 5 new tests in 1 new suite. - inspectAcceptsSchemaV2Bundle: v2 manifest unpacks cleanly. - buildPlanSurfacesSchemaAndQueuesConfigFiles: plan carries the schema; projectFiles contains both config.json + manifest.json. - verifyClaimsRejectsConfigCountMismatch: a manifest lying about contents.config vs. schema.fields.count is refused at inspect. - installWritesConfigJsonAndManifestCache: install round-trip writes config.json (with non-secret values inline + secret as keychainRef), manifest.json cache, and lock with configKeychainItems + configFields. Real Keychain is exercised; the test cleans up the single item it creates. - uninstallDeletesKeychainItemsViaLock: install + then uninstall, verify the Keychain entry is gone via SecItemCopyMatching. sampleManifest test helper gains `configFieldCount` and `configSchema` params so tests that want schemaful bundles don't need to rebuild the whole manifest record. schemaVersion auto-bumps to 2 when a schema is present so the fixture mirrors real bundle shape. 50/50 tests in 13 suites pass; pre-existing 45 from v2.2 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
385c3a2e4d |
feat(config): template-config models + Keychain wrapper + ProjectConfigService
Groundwork for v2.3 template configuration. No user-visible behaviour yet — this commit adds the data structures, storage layer, and validation rules that the installer/uninstaller/UI will integrate with in the next two commits. Models (Core/Models/TemplateConfig.swift): - TemplateConfigSchema + TemplateConfigField for the author-declared manifest.config block. 7 field types: string, text, number, bool, enum, list, secret. Type-specific constraints (pattern, min/max, min/maxLength, min/maxItems, enum options) are all optional and the validator enforces only those applicable to the field's type. - TemplateModelRecommendation for the author's model-of-choice hint (preferred + rationale + alternatives). Purely advisory — Scarf never auto-switches the active model. - TemplateConfigValue enum: string / number / bool / list / keychainRef. Custom Codable preserves keychain:// refs on round-trip — a round through save/load never demotes a secret ref to plaintext. - ProjectConfigFile is the on-disk shape at <project>/.scarf/config.json. - TemplateKeychainRef: derives (service, account) from templateSlug + fieldKey + project-path hash. The 32-bit FNV-1a suffix prevents two installs of the same template in different dirs from colliding in the login Keychain. uri <-> parse round-trips losslessly. Keychain layer (Core/Services/ProjectConfigKeychain.swift): - Thin wrapper over kSecClassGenericPassword. set() tries update-first then add-if-missing so we don't trip "already exists" on a race. - kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: no iCloud sync, but cron triggers can still read after the user's first unlock. - testServiceSuffix lets unit tests route items under a distinct service prefix so nothing leaks into the user's real Keychain. Service layer (Core/Services/ProjectConfigService.swift): - load/save for <project>/.scarf/config.json through the ServerContext transport (so remote-ready for when installer goes remote). - cacheManifest/loadCachedManifest: the installer copies template.json into <project>/.scarf/manifest.json so the post-install "Configuration" button can render the form offline. - resolveSecret / storeSecret / deleteSecrets: the three Keychain paths any caller needs. Non-secret values never pass through these. - validateSchema: author-facing invariants (unique keys, known types, enum opts present/unique, no defaults on secrets, non-empty model preferred). Called by ProjectTemplateService during inspect. - validateValues: user-facing invariants (required, pattern, numeric range, list bounds, enum membership). Returns one error per problem so the UI can surface them inline with the offending field. Tests (scarfTests/TemplateConfigTests.swift): 23 tests in 5 suites. - Schema validation: happy path + every rejection rule. - Value validation: required, pattern, numeric range, list bounds, enum membership, secret-via-keychain-ref acceptance. - Keychain ref: uri round-trip, parse rejection of malformed input, path-hash differs across project dirs but is stable for same path. - ProjectConfigFile round-trips non-secret values cleanly AND preserves keychain:// refs (the bug that would silently demote secrets to plaintext if the Codable were wrong). - Real Keychain integration: store+resolve+delete, set overwrites, delete of missing item is a no-op, bulk delete clears all. Tests use unique testServiceSuffix per run so no cross-contamination. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e76fbf9937 |
chore: audit follow-ups from plan review
Four small fixes surfaced by a side-by-side plan-vs-shipped pass: - README.md: adds the Template Catalog section the plan called out — links to the live site URL, the install flows (web / file / Finder), and templates/CONTRIBUTING.md for authors. Placed right before the existing Contributing section, with a catalog-specific cross-link at the end of that section too. - CLAUDE.md: adds the Template Catalog section so future agent sessions know the regenerator pipeline exists, how it relates to release.sh + wiki.sh, and what the schema-sync rule is when DashboardWidget or ProjectTemplateManifest change. - scarf/scarfTests/ProjectTemplateTests.swift: fixes the stale ProjectTemplateExampleTemplateTests docstring still referencing `examples/templates/` (the example moved to `templates/awizemann/` in 70f7cea). - .github/workflows/validate-template-pr.yml: untangles the self- contradictory Python-version comment. The validator is 3.9+ compatible; CI uses 3.11 for faster runner caching. Same stdlib surface, same code paths — just clearer about why. All tests still green: 22 Swift tests in 7 suites, 16 Python tests, catalog check passes on the site-status-checker example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c9b8da9ec5 |
feat(ci): validate template submissions on PR + tailored checklist
Adds the CI gate that runs on every PR touching templates/, the catalog validator, or its tests. The Action: - runs tools/test_build_catalog.py (catches drift between validator + its own test suite on the same PR that introduces the drift) - runs tools/build-catalog.py --check (validates every shipped .scarftemplate against the same invariants ProjectTemplateService.verifyClaims enforces at install time) - posts a PR comment with the last 3 KB of the validator log on failure, so contributors see the specific mismatch without hunting through the Actions UI .github/PULL_REQUEST_TEMPLATE/template-submission.md is the author-facing checklist that mirrors templates/CONTRIBUTING.md. Opt-in via the ?template=template-submission.md compare URL (documented in the contribution guide). CONTRIBUTING.md now links both the PR template and the workflow file so authors know what to expect. Phase 4 closes the community loop — from this commit on, a stranger can fork the repo, follow templates/CONTRIBUTING.md, push a PR, and get deterministic green/red feedback before a maintainer ever looks at it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6175bee27d |
feat(site): dogfood the Scarf dashboard format as the catalog website
Adds site/ with vanilla HTML + CSS + ~300 lines of JavaScript that
renders ProjectDashboard JSON directly in the browser. Each template's
detail page shows a live preview of the exact dashboard the user will
get post-install — the catalog IS the dogfood.
site/widgets.js mirrors the Swift widget dispatcher:
- stat (big number + colored icon + optional subtitle)
- progress (0..1 bar)
- text with inline markdown subset (headings, bold/italic, inline code,
code fences, bullet + numbered lists, links)
- table (plain HTML)
- list (with up/down/unknown status badges)
- chart (SVG line + bar — no Chart.js dependency)
- webview (sandboxed iframe)
- unknown (placeholder so the page doesn't silently omit widgets)
Plus the renderMarkdown helper used by the template detail page to
display the bundle's README.
site/index.html.tmpl + site/template.html.tmpl are substitution-only —
the Python regenerator swaps {{CARDS}}, {{COUNT}}, {{COUNT_PLURAL}},
{{NAME}}, {{DESC}}, {{VERSION}}, {{AUTHOR_HTML}}, {{TAGS_HTML}},
{{INSTALL_URL_ENCODED}}, {{SCARF_INSTALL_URL}}. The detail page fetches
dashboard.json + README.md at page load and hands them to widgets.js.
No client-side framework, no bundler, no npm.
site/styles.css: minimal CSS with scarf green accent, prefers-color-
scheme dark support, responsive at 680px. One file, ~280 lines.
build-catalog.py extended to copy dashboard.json + README.md out of each
bundle into its detail dir so widgets.js can fetch them without
reaching across directories (and so gh-pages doesn't need to serve zip
contents at request time).
Two new Python tests: end-to-end site rendering (both cards, install
URL wiring, static asset copy, per-template dashboard + README copy)
and the {{COUNT_PLURAL}} singular-vs-plural flip. 16/16 Python tests
green.
Smoke-tested locally with python3 -m http.server: every endpoint
(index, catalog.json, detail HTML, per-template dashboard.json + README,
widgets.js) returns 200. The .gh-pages-worktree/appcast.xml +
.gh-pages-worktree/index.html are untouched — the catalog is purely
additive under /templates/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
11732baa3c |
feat(catalog): stdlib-only Python validator + regenerator for templates/
Adds the catalog pipeline without introducing any external dependencies. tools/build-catalog.py walks templates/<author>/<name>/, validates every shipped .scarftemplate against its manifest (same invariants Swift's ProjectTemplateService.verifyClaims enforces at install time), and emits templates/catalog.json for the frontend to read. Validator invariants: - Required bundle files: template.json, README.md, AGENTS.md, dashboard.json - contents claim cross-checked against actual zip entries (instructions, skills, cron count, memory appendix) - dashboard.json widget types restricted to the vocabulary the Swift renderer knows - Manifest id author component must match the template directory - 5 MB bundle-size cap on submissions (installer's own cap is 50 MB) - High-confidence secret patterns (private keys, GitHub PATs, Slack tokens, AWS access keys, OpenAI/Anthropic keys) block the bundle - staging/ source tree must match the built bundle byte-for-byte — catches the common failure mode of editing staging/ but forgetting to rebuild scripts/catalog.sh wraps the Python script with check/build/preview/serve/ publish subcommands, mirroring the scripts/wiki.sh shape. publish adds a second-pass hard-pattern secret scan on the rendered gh-pages output so template prose can't leak credentials even if the Python scan missed them. tools/test_build_catalog.py has 14 unit tests covering the main validator paths (minimal-valid, missing-AGENTS, content-claim mismatch, author mismatch, oversized bundle, unknown widget type, secret detection, staging-drift detection, missing bundle, catalog.json shape, and a real- bundle end-to-end check against templates/awizemann/site-status-checker). Python 3.9 compatible (Xcode's bundled python3), so no runtime needs installing. templates/catalog.json committed as the first generated aggregate index; maintainers regenerate on merge by running `./scripts/catalog.sh build`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d8a0a89db2 |
feat(templates): promote examples/ to templates/<author>/<name>/ catalog layout
Set up the catalog directory structure this branch will fill with community templates. The existing site-status-checker example moves from examples/templates/ to templates/awizemann/site-status-checker/ (tracked by git as a rename so history is preserved). The examples/ directory is removed. New top-level docs: - templates/README.md — landing for folks browsing the catalog on github.com. Lists the current templates and points at the live site. - templates/CONTRIBUTING.md — author-facing submission walkthrough. Requires AGENTS.md, pre-flight with tools/build-catalog.py --check (added in the next commit), one template per PR, don't edit catalog.json (maintainer regenerates it post-merge). ProjectTemplateExampleTemplateTests.locateExample updated to search templates/<author>/<name>/ instead of examples/templates/ — the test still walks up from #filePath to find the repo root so it works in both xcodebuild and Xcode IDE test runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
38c075d61d |
docs: ship site-status-checker example template + v2.2.0 release notes
First installable template demonstrating the format: - Dashboard with stat widgets (up/down/last-checked) + configured-sites list + quick-start markdown. - Cross-agent AGENTS.md with the full cron-prompt contract so any agent that reads agents.md (Claude Code, Cursor, Codex, Aider, Jules, Copilot, Zed, …) picks up the behavior on first run. - Cron job (0 9 * * *) that ships paused with the [tmpl:…] tag, pinging a user-editable sites.txt and writing results to status-log.md. - First-run bootstrap logic in AGENTS.md: if sites.txt doesn't exist yet the agent creates it with two placeholder URLs, then proceeds. Plus examples/templates/README.md explaining the staging/ layout, authoring conventions, and how to rebuild a bundle after editing. CI validates the bundle via ProjectTemplateExampleTemplateTests so drift between staging/ and the built .scarftemplate fails on every build. v2.2.0 release notes cover the full feature surface including the install preview sheet, scarf:// + file:// URL handling, skills namespacing, cron-job tagging, memory-block markers, and the lock-driven uninstall flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c800b93804 |
feat: project templates v1 (install + uninstall + export + URL handler)
Shareable `.scarftemplate` bundle format lets users package a project's dashboard, cross-agent AGENTS.md, optional per-agent instruction shims, optional namespaced skills, optional tagged cron jobs, and an optional memory appendix into a single zip that anyone can install with one click. Core: - Bundle format + manifest schema v1 (template.json with contents claim cross-checked against zip entries to prevent hidden files). - ProjectTemplateService inspects + validates + builds an install plan. - ProjectTemplateInstaller executes plans with transport-routed I/O so the v1 local-only flow extends cleanly to remote ServerContexts later. - ProjectTemplateExporter builds bundles from existing projects with user-selected skills + cron jobs. - ProjectTemplateUninstaller reverses installs using template.lock.json. Only lock-tracked files are removed; user-added files are preserved. UI: - Templates menu in Projects toolbar: Install from File, Install from URL, Export as Template. - Preview-and-confirm sheets for install, uninstall, and export with full diff of what will be written/removed before anything runs. - Right-click context menu on project list + dashboard header button for uninstall (only shown when template.lock.json exists). Deep link + file associations: - scarf:// URL scheme registered; onOpenURL in scarfApp.swift routes scarf://install?url=https://... and file:// URLs for .scarftemplate files to the install sheet. - Custom UTType com.scarf.template registered so Finder shows the file with a Scarf icon and double-click opens the install preview. - Cold-launch race fix: .task picks up any URL staged on the router before the onChange observer was installed. Safety: - Never writes to config.yaml, auth.json, sessions, or credentials. - Cron jobs ship paused with a [tmpl:<id>] name prefix. - Skills install to a namespaced ~/.hermes/skills/templates/<slug>/ dir so they never collide with user-authored skills. - Memory appendix is wrapped in scarf-template:<id>:begin/end markers for clean removal during uninstall. - Download cap: 50 MB for URL-fetched templates, enforced on the actual on-disk file size after download so chunked transfers can't bypass it. Tests: 22 tests in 7 suites cover manifest parsing, claim verification, URL routing (scarf:// + file://), end-to-end install and uninstall against a minimal bundle (projects registry is snapshotted + restored), user-added file preservation, and exporter round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7311320bfd |
Merge pull request #30 from awizemann/claude/issue-26-default-server
Let users pick the default server opened on launch (#26) |
||
|
|
4663697942 |
Merge pull request #29 from awizemann/claude/issue-26-sidebar-width
Persist sidebar width across launches (#26) |
||
|
|
41635955b0 |
feat: let users pick the default server opened on launch (#26)
Repurposes the previously-unused ServerEntry.openOnLaunch flag so users can nominate Local or any registered remote as the server Scarf opens into when a fresh window has no prior binding (first launch or File → New Window). - ServerRegistry gains `defaultServerID` (returns the flagged entry's ID or falls back to Local) and `setDefaultServer(_:)` (flips the flag on the named entry and clears it elsewhere, then persists). - ScarfApp's WindowGroup defaultValue closure now returns `registry.defaultServerID` instead of hardcoded `ServerContext.local.id`. - ManageServersView gains a Local row at the top of the list plus a star button per row: filled yellow on the current default, outline on the others. Click to promote. Backward compatible: the openOnLaunch field was already in the persisted schema (default false), so existing servers.json files load unchanged — Local remains the default until the user picks otherwise. Refs #26 |
||
|
|
1989feee22 |
feat: persist sidebar width across launches (#26)
Wire an NSSplitView autosave name into NavigationSplitView's underlying AppKit split view so the sidebar's drag-to-resize position is remembered in UserDefaults and restored on next launch. SplitViewAutosave.swift installs an invisible NSViewRepresentable that walks up the view hierarchy from the sidebar, finds the enclosing NSSplitView, and assigns autosaveName = "ScarfMainSidebar". AppKit handles persistence from there — no manual UserDefaults or @AppStorage plumbing needed. ContentView also gets navigationSplitViewColumnWidth(min:ideal:max:) bounds so first-launch (before any autosave exists) lands at a sensible 240pt ideal within a 180–360pt range. Refs #26 |
||
|
|
8773254d11 |
chore: accept safe parts of Xcode recommended-settings migration
Xcode 26.x suggested an upgrade pass that included a critical regression: ENABLE_APP_SANDBOX = YES on the main app, which would silently break every view that reads ~/.hermes/ (state.db, config.yaml, memory files, skills, logs). Scarf is architected sandbox-off per CLAUDE.md — reverted. Kept the benign pieces: - DEAD_CODE_STRIPPING = YES on all targets (stock modern optimization) - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES at project level — static analyzer warning for un-localizable call sites; directly relevant to the i18n work in 2.1.0 and will flag regressions of the exact patterns just cleaned up - STRING_CATALOG_GENERATE_SYMBOLS = YES hoisted to project level (was already set at target level; hoisting is a no-op functional change but Xcode prefers it inherited) - Scheme file LastUpgradeVersion bumped to 2620 to match current Xcode Rejected: - ENABLE_APP_SANDBOX = YES (critical — would break app file access) - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT / RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION build settings (Xcode's new form replacing the entitlements file; keeping the entitlements file as the single source of truth since every release 1.x → 2.1.0 shipped and notarized with that form) - LastUpgradeCheck = 2620 (Xcode dropped 2630 → 2620; cosmetic revert) v2.1.0 was released before this Xcode pass so no rebuild needed — the downloaded zips and Sparkle appcast entry are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a1aa653a33 | chore: Bump version to 2.1.0 v2.1.0 | ||
|
|
e256196397 |
chore: commit shared Xcode scheme
The scarf scheme existed in every local Xcode session (Xcode auto-creates it from xcschememanagement's ^#shared#^ entry on first open), but was never actually committed to the repo. Release v2.1.0 hit the resulting "project contains no schemes" error on headless xcodebuild archive after the build/ cache was cleaned. Committing the scheme itself so future headless builds work from a fresh clone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
50880efe81 |
docs: prep v2.1.0 release notes + README language badge
Pre-release prep so that when `./scripts/release.sh 2.1.0` runs on main, the notes file is already in tree (script's `git add` is then a no-op, bump commit contains only the pbxproj version change). - README gains a 2.1 "What's New" section covering translations + the chat slash-menu; 2.0 moves down to "Previously". - Badge row gains a language list line. - Full release notes at releases/v2.1.0/RELEASE_NOTES.md — covers the three stacked i18n PRs (infra, audit burn-down, translations) and the chat slash-menu work merged in parallel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b1bc7e8494 |
Merge pull request #25 from awizemann/translations-initial
feat(i18n): initial translations for 6 languages + contributor workflow |
||
|
|
f47034d4ad |
fix(i18n): localize sidebar, settings tabs, and settings section titles
Three connected bugs where the Label/SettingsSection APIs took a `String`, which routes through the StringProtocol overloads and bypasses localization entirely. Identified by the user after testing zh-Hans / de / fr — the sidebar menu items, Settings tab bar, and Settings section headers all remained English under any App Language override. - SidebarSection now exposes displayName: LocalizedStringResource; SidebarView builds Label via the Text/Image builders so the catalog key is actually used. - SettingsTab gets the same displayName treatment; the .tabItem Label builds through the Text/Image builder too. - SettingsSection.title changes from String → LocalizedStringKey so literal call sites (all ~20 of them) now extract into the catalog. Two call sites that were passing String variables (PlatformsView, CredentialPoolsView) are wrapped via LocalizedStringKey(...) — brand/provider names fall through to English as before. AuxiliaryTab's static task list gets a LocalizedStringKey column so its section titles extract too. This change newly extracts 65 previously-invisible section-title keys into the catalog; translations added for all six locales. Catalog: 575 → 644 source keys, each locale translated for 583 of them (brand names / protocol names / format-only keys intentionally fall through). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1726a613a5 |
feat(i18n): add translations for zh-Hans, de, fr, es, ja, pt-BR
Ships first-pass AI translations for six locales on top of the existing English base, plus a simple JSON-per-locale contributor workflow so new languages can land as a single PR. - 518 keys translated per locale (proper nouns / brand names / format- only strings left to fall back to English by design — see the "Non-blocking (intentional verbatim)" section of scarf/docs/I18N.md). - Per-locale source-of-truth lives in tools/translations/<locale>.json; tools/merge-translations.py writes them into Localizable.xcstrings and is idempotent (re-runnable as translators iterate). - InfoPlist.xcstrings (macOS microphone permission prompt) translated for all six locales. - knownRegions expanded: zh-Hans, de, fr now join by es, ja, pt-BR. - CONTRIBUTING.md gains an "Adding a Language" section documenting the fork → JSON → merge → PR flow. Native-speaker reviews welcome. Closes #13 (the original ask: Simplified Chinese support). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
de34a80807 |
Merge pull request #24 from awizemann/multi-language
feat(i18n): close silently un-localizable sites (Phase 1b) |
||
|
|
d9a25b3997 |
Merge pull request #22 from awizemann/claude/pedantic-kare-1edf13
feat(i18n): enable String Catalog + locale-aware numeric formatters |
||
|
|
b40182f2da |
feat(i18n): close silently un-localizable sites from the audit
Burns down the follow-ups tracked in scarf/docs/I18N.md so that future
translation passes (Phase 2+) don't see English leak through ternary UI
copy, enum rawValue displays, or fixed-format strings.
- Ternary status copy: Text(cond ? "A" : "B") → cond ? Text("A") : Text("B")
(each branch routes through LocalizedStringKey). Covers Health, Chat
(voice/TTS/recording/ACP status), Profiles, MCPServer test result,
SignalSetup, QuickCommands header.
- Enum .rawValue displays: LogFile, LogComponent, DashboardTab, Skills.Tab,
InsightsPeriod, ToolKind, AuthType each expose a
displayName: LocalizedStringResource. LogEntry.LogLevel stays verbatim
(technical jargon — DEBUG/INFO/ERROR/… are industry-standard).
- displayName passthroughs: HermesToolPlatform, ServerRegistry.Entry,
MCPServerPreset wrapped with Text(verbatim:) at call sites (brand names
and user data, not UI chrome). MCPTransport.displayName promoted to
LocalizedStringResource.
- Composite format strings: ModelPickerSheet "ctx" suffix, InsightsView
"tokens" suffix and MCPServerTestResultView "%.1fs · %d tools" rewritten
as Text("\(arg) suffix") LocalizedStringKey. Percent display uses
.formatted(.percent) after /100.
- Day-of-week chart now sources from Calendar.current.shortWeekdaySymbols,
re-indexed for the existing Mon=0 data model.
- ConnectionStatusPill's label + tooltip return Text (not String) so the
.help(Text) / direct-render paths localize correctly.
- Catalog re-synced: 545 → 575 keys (+30 from new ternary branches and
enum displayName values).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|