mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
ws-7-settings-v0.13
75 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
adcc984091 |
feat(kanban): full read/write board with per-project tenants
Lifts Scarf's Kanban surface from the v2.6 read-only list to a drag-and-drop board with the complete Hermes v0.12 mutation surface wired up, plus per-project boards bound to a Scarf-minted tenant slug and a read-only board on iOS. Why now: the v2.6 list was a placeholder shipped while upstream Kanban collab was still mid-rework. v0.12 stabilized the 27-verb CLI; this release makes Scarf a real GUI client for it. Driving real tasks end-to-end exposed and closed a connected bug pattern (claim vs dispatch, silent skipped_unassigned, integer-vs-ISO timestamps, parser-leaked "(no" sentinel) that would have shipped as latent UX papercuts otherwise. ScarfCore: KanbanService actor (Sendable, pure I/O) wrapping every verb; KanbanTenantReader cross-platform manifest projection; eight new model types (TaskDetail, Comment, Event, Run, Stats, Assignee, CreateRequest, Filters); KanbanError; pure transition planner that maps drag-drop column changes to verb sequences, tested against canonical Hermes JSON fixtures. Mac: KanbanBoardView orchestrator with five-column drag-drop layout, optimistic-merge state, KanbanInspectorPane side-pane (Comments / Events / Runs / Log tabs, Log streams worker stdout every 2s while running), inline assignee picker, health banner for unassigned and last-failed-run states. New Task sheet defaults to active profile and auto-fires kanban dispatch on submit. Sidebar moved Kanban from Manage to Monitor. Read-only KanbanListView preserved as Board|List toggle for narrow windows / accessibility. Per-project: DashboardTab.kanban tab on every project gated on hasKanban; KanbanTenantResolver mints scarf:<slug> tenants on first interaction and persists to .scarf/manifest.json (immutable across rename); ProjectAgentContextService surfaces the tenant in the AGENTS.md scarf-managed block so agents pass --tenant <slug> on kanban create. New kanban_summary dashboard widget; vocabulary mirrored in tools/widget-schema.json and site/widgets.js. iOS: read-only board on the project tab via paged single-column Picker, modal detail sheet with Comments / Events / Runs. Mutations + drag-drop deferred to v2.8. Tests: 19 new pure-logic tests covering decoding, planner verb mapping, argv assembly, glance string formatting, and parser rejection of the kanban assignees empty-state sentinel. All 348 ScarfCore tests pass. Constraints documented in CLAUDE.md: no within-column reorder (Hermes has no update --priority verb); no live watch streaming yet (5s polling for board, 2s for log); no bulk re-tag for legacy NULL-tenant tasks. Pre-v0.12 Hermes hosts gracefully hide the surface end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3a3c87e033 |
fix(skills): scope What's New pill to Installed tab + reword updated→changed
Issue #78 — The "What's New" pill at the top of the Skills page announced "18 new, 3 updated since you last looked" while the Updates sub-tab simultaneously said "No Updates / All skills are up to date." Two surfaces measuring two different things both used the word "update": the pill counts local file deltas since the user last clicked "Mark as seen", while the Updates body runs `hermes skills check` to find skills with newer upstream versions available. From the user's seat the screen contradicted itself. Two changes: 1. Render the pill only on the Installed sub-tab (Mac + ScarfGo). Local file deltas are contextually meaningful only on the tab that surfaces installed skills; showing them above Browse Hub or Updates was misleading. 2. Reword the pill: "X updated since you last looked" → "X changed since you last looked". Keeps `SkillSnapshotDiff.updatedCount` as the field name (it's still about file changes, not version bumps); only the user-visible string changes. Removes the vocabulary collision with the Updates tab's separate upstream-update check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6b66b1c96f |
perf(ios): wire v2.7 perf parity — instrument iOS-only VMs + surface hydration banner + opt-in toggle
Most of the v2.7 perf work was already covered on iOS via shared code in ScarfCore — `RichChatViewModel.loadSessionHistory` (and its skeleton-then-hydrate path), `hydrateAssistantToolCalls`, `fetchSkeletonMessages`, `fetchRecentToolCallSkeleton`, `ModelPreflight.detectMismatch`, and the `RemoteSQLiteBackend` cancellation handler all flow through to the ScarfGo chat unchanged. `CitadelServerTransport.streamScript` already honors `Task.isCancelled` correctly via `withThrowingTaskGroup` + `Task.checkCancellation()`, so the SSH-cancellation-on-nav-away chain works on iOS without the Mac-side `SSHScriptRunner` fix. Three iOS-specific gaps closed: * IOSCronViewModel.load + IOSMemoryViewModel.load wrapped in `ScarfMon.measureAsync(.diskIO, "ios.cron.load")` / `"ios.memory.load"` — parity with the Mac `cron.load` / `memory.load` events. `ios.memory.load.bytes` records the payload size for the loaded file. * iOS Settings → "Chat (Scarf)" section gains a toggle bound to `RichChatViewModel.loadHistoricalToolResultsKey` so iOS users can opt into Phase 2b bulk tool-result hydration, same as the Mac DisplayTab. The shared key means the gate inside `startToolHydration` reads the right value automatically — no extra plumbing needed. * iOS ChatView surfaces `isHydratingTools` as a "Loading tool details…" connection banner (mirrors the Mac toolbar pill added in v2.7 perf work). Sits between the existing "Thinking…" banner and the empty-view fallback so chat status is always honest about what the agent and Scarf are doing. Both Mac and iOS targets build clean; all 321 ScarfCore tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9ff9a018e7 |
feat(scarfmon,chat): Phase 3b — dampen finalize bursts + Thinking… status + wider loadConfig stack
Three targeted fixes from the Phase 3a baseline. Bubble-burst dampening (Phase 3b-1): - RichChatViewModel.finalizeStreamingMessage wraps both the streaming-id rewrite and the empty-finalize remove() in a no-animation Transaction. The id flip from 0 → permanent value was the load-bearing trigger of the 5–8 RichMessageBubble.body fires we were seeing 1–2 ms after every `finalizeStreamingMessage` interval; SwiftUI ran an animated diff against neighbors and re-evaluated their bodies. The new message is content-equal to the streaming one — there is no animation worth running. Thinking… status promotion (Phase 3b-2): - RichChatViewModel exposes `isStreamingThoughtsOnly` — true while a turn is in flight, has emitted thought-stream bytes, and has not yet produced any visible assistant text. The Phase 3a baseline showed this is where most of the user-perceived "feels slow" lives: reasoning models commonly take 3–8 s before producing visible output, and Scarf surfaced no specific signal during that window. - Mac ChatView.displayedStatus promotes the toolbar pill to "Thinking…" when the flag is true. - iOS connectionBanner gains a transient "Thinking…" strip with spinner, same trigger condition. Phase 3a fix-up: - HermesFileService.loadConfig stack-trace logging widened from one frame to a 10-frame window prefixed with "#N", so the actual caller is visible past inlined ScarfMon wrappers (the prior log surfaced ScarfMon.measure itself, not the loadConfig caller). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3126c34561 |
feat(scarfmon): chat + transport + sqlite measure points (Phase 2)
Wires ScarfMon measure points into the chat hot path on both targets, plus the underlying SSH transport and remote-SQLite backend. All callsites are surgical adds — no behavior change. Cost when ScarfMon is in `.signpostOnly` (default) is one os_signpost emit per call, elided by the runtime outside an Instruments session. In `.full` mode the same callsites also push samples into the in-memory ring buffer. Render counters (event): - mac.ChatView.body / ios.ChatView.body — full transcript pane re-evals - mac.RichMessageBubble.body / ios.MessageBubble.body — per-bubble re-evals Stream + session (event + interval): - mac.sendViaACP, mac.sendPrompt — user tap → first-byte - mac.acpEvent, mac.handleACPEvent — per-event delivery + handle cost - mac.startACPSession — session boot - ios.send, ios.startResuming — same shape on iOS - ios.acpEvent, ios.handleACPEvent — same per-event split on iOS Transport + SQLite (interval, with byte counts on rows): - ssh.streamScript (Citadel iOS) — SSH round-trip - ssh.run (SSHScriptRunner Mac) — SSH round-trip - sqlite.query, sqlite.queryBatch — Remote SQLite per-call - sqlite.query.rows — row count + stdout bytes per query Disk I/O (interval): - diskIO.loadConfig — config.yaml read + parse - diskIO.loadCronJobs — cron jobs.json decode Body counters use the `let _: Void = ScarfMon.event(...)` pattern at the top of `body` — works inside `@ViewBuilder` and fires on every re-eval, which is exactly the signal we want. To use: Mac: Settings → Advanced → Performance Diagnostics → Full iOS: Settings → Diagnostics → Performance → Full Both panels auto-aggregate by (category, name), surface top 20 by p95, and offer Copy as JSON for sharing in feedback threads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6cf59c8a44 |
feat(scarfmon): perf instrumentation plumbing for iOS + Mac (Phase 1)
ScarfMon lands the always-on perf instrumentation harness. Phase 1 ships the plumbing only; Phase 2 wires the chat measure points. Core (ScarfCore/Diagnostics/): - ScarfMon — public API: measure / measureAsync / event with @inline(__always) short-circuit when the backend set is empty so the off path is one branch + return. Categories are an enum, names are StaticString so user content cannot leak through metric tags. - ScarfMonRingBuffer — fixed-capacity (4096) lock-protected ring; one os_unfair_lock per record; summary() aggregates by (category, name) with nearest-rank p50/p95; exportJSON() emits a one-line-per-sample dump for the Copy as JSON button. - ScarfMonSignpostBackend — emits os_signpost into a dedicated com.scarf.mon subsystem so Instruments → Points of Interest shows Scarf's own measure points without a debug build. - ScarfMonLoggerBackend — Logger(.debug) sink for users running `log stream --predicate 'subsystem == \"com.scarf.mon\"'`. - ScarfMonBoot — three modes (off / signpostOnly / full); persists the user's choice in UserDefaults under ScarfMonMode; configure() is idempotent and replaces the active backend set atomically. Tests: 11 cases covering ring ordering / wrap / reset, summary aggregation, p95 percentiles, event vs interval semantics, install / isActive, measure + measureAsync (including the throw path), boot mode transitions, and JSON export round-trip. @Suite(.serialized) because the suite mutates process-wide backend state. App wiring: - ScarfIOSApp.init + ScarfApp.init call ScarfMonBoot.configure(mode:) with the persisted mode (default .signpostOnly). - iOS Settings → Diagnostics → Performance row leads to a list-style panel with the segmented mode picker, top-20 stat rows by p95, Copy as JSON, and Reset. - Mac Settings → Advanced gains a ScarfMonDiagnosticsSection with the same shape (NSPasteboard for copy). Open-source by design — no remote upload, no analytics. The ring buffer never leaves the device unless the user explicitly taps Copy as JSON. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c7bcfd8655 |
feat(dashboards): v2.7 widget catalog — file-reading widgets, sparkline, typed status, project-wide watch
Major project-dashboard release. Five new widget types (markdown_file, log_tail, cron_status, image, status_grid), inline sparkline on stat, typed status enum shared by list + status_grid, structured WidgetErrorCard, and a project-wide .scarf/ directory watch that picks up files cron jobs write next to dashboard.json. - ProjectDashboard: extend DashboardWidget with path/lines/jobId/cells/gridColumns/sparkline; add StatusGridCell + ListItemStatus (lenient parse with synonyms) - HermesFileWatcher: watch each project's .scarf/ dir alongside dashboard.json (local FSEvents + remote SSH mtime poll); updateProjectWatches signature now takes dashboardPaths + scarfDirs - New widget views: CronStatus, Image, LogTail, MarkdownFile, StatusGrid, plus WidgetErrorCard for structured failure messaging; legacy "Unknown" placeholder replaced everywhere - WidgetPathResolver: project-root-anchored path resolution that rejects absolute paths + ".." escapes pre and post canonicalization - Stat widget gains optional inline sparkline (pure SwiftUI Path, no Charts dep); list widget rows route through typed status with semantic icons + ScarfColor tints - iOS list widget + unsupported card adopt typed status + warning-toned error card (parity with Mac error styling); new widget types remain Mac-only - Site mirror: widgets.js renders all five new types (file-reading widgets show annotated catalog placeholders), sparkline SVG, status-grid grid; styles.css adds typed-status palette + error-card + sparkline + grid styles - Catalog validator: tools/widget-schema.json is the single source of truth; build-catalog.py loads it and enforces per-type required fields. 8 new test cases in test_build_catalog.py covering schema load, v2.7 additions, and missing-required rejection - Template-author skill (SKILL.md) gains v2.7 Widget Catalog section + canonical status guidance; CONTRIBUTING.md points authors at widget-schema.json; template-author bundle rebuilt - Localizable.xcstrings picks up auto-extracted strings for the previously-shipped OAuth keepalive feature - Release notes drafted at releases/v2.7.0/RELEASE_NOTES.md Backwards compatible — existing dashboard.json renders byte-identically, status synonyms (ok/up/down/active/etc.) keep working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
2aab9dac07 |
feat: chat-start preflight, Nous catalog, remote-aware admin sheets
Three feature batches that were in progress on chat-resilience — all aligned with v2.5.2's remote-context theme. ## Chat-start model preflight When a chat-start hits a server whose config.yaml has no model.default / model.provider, the upstream provider returns an opaque "Model parameter is required" 400 only AFTER the user types a prompt and hits send. New ModelPreflight in ScarfCore catches the missing keys before any ACP work; ChatView presents the existing ModelPickerSheet via a thin ChatModelPreflightSheet wrapper so the picker / validation / Nous-catalog branch stay single-sourced. ChatViewModel persists the selection via `hermes config set` and replays the original startACPSession arguments — the chat the user originally opened lands without re-clicking the project row. ## Nous Portal live catalog NousModelCatalogService fetches `GET /v1/models` from inference-api.nousresearch.com using the bearer token in `auth.json`, caches to `~/.hermes/scarf/nous_models_cache.json` (new path on HermesPathSet) with a 24h TTL. Picker's nous-overlay detail switches from a free-form TextField to a real model list, with a "Custom…" escape hatch (nousManualEntry) for IDs not yet in the API response. ## Remote-aware admin sheets (mirror of #54's pattern) The Add Project sheet got context-aware Verify in v2.5.1 (#54); this batch extends the same shape to three more sheets: - Profiles: remote import/export. ProfilesView gains showRemoteImportSheet + pendingRemoteExport state; reuses the same path-input + verify + run-via-hermes pattern from AddProjectSheet. Drives `hermes profile import <zip>` / `hermes profile export <name> <zip>` over SSH. - Backup restore (Settings → Advanced): pickLocalBackupZip + new RemoteBackupPathSheet so the Restore action picks a local zip on local contexts and verifies a remote path on remote contexts. - Template install destination: TemplateInstallSheet's parent- directory picker now branches on context. ParentDirectoryStep with browseLocalDirectory + verifyRemotePath + RemoteVerification — same UX vocabulary as AddProjectSheet, applied to where the template gets installed. Plus a `runHermesWithStdin` helper on HermesFileService for the profile import flow (passing zip bytes through stdin rather than landing them on the remote disk first), and ProjectTemplateInstaller gains a remote-path-aware code path for the install destination. ## Localizations Localizable.xcstrings adds strings for all the new copy across seven supported locales (en, zh-Hans, de, fr, es, ja, pt-BR). |
||
|
|
c31dfccb9b |
fix(ios-chat): move keyboard-dismiss chevron to leading edge (#57)
The keyboard accessory dismiss button added in #51 was placed at the trailing edge of the keyboard toolbar (Spacer before Button), which sits directly above the trailing-edge send button in the composer below. Two near-identical-shape controls visually stack on the right edge of the screen, confusing users about which is which. Move the Spacer() to AFTER the Button so the chevron lives at the leading edge of the keyboard accessory bar — visually separated from the send button below, and matches the iOS convention (Notes, Mail, Reminders all put accessory dismiss on the leading side). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
511726e2c0 |
feat(chat-resilience): iOS reconnect + snapshot fallback + paging + pill fix
Brings iOS chat to parity with Mac's reconnect behavior so a session survives phone-sleep, network handoffs, and SSH socket drops without losing the agent's work — Hermes already persists messages to state.db in real-time, the iOS app just had no resync path. Core changes (shared between Mac and iOS via ScarfCore): - ServerTransport.cachedSnapshotPath: fall back to the cached state.db snapshot when a fresh pull fails. HermesDataService surfaces this via isUsingStaleSnapshot + lastSnapshotMtime so views can render "Last updated X ago." Default opt-in via refresh(forceFresh: false); chat history reload passes forceFresh: true to refuse stale data. - HermesDataService.fetchMessages(sessionId:limit:before:): bounded pagination by id desc. Legacy unbounded overload deprecated. New HistoryPageSize constants centralize the budget. - RichChatViewModel.loadEarlier(): pages back through the current session via oldestLoadedMessageID + hasMoreHistory. iOS-only: - ChatController gains the Mac reconnect machinery: 5-attempt exponential backoff (1→16s) via session/resume → session/load, reconcileWithDB on success, "Resynced N new messages" toast. startACPEventLoop + startHealthMonitor extracted as helpers. - New NetworkReachabilityService (NWPathMonitor singleton). Suspends reconnect attempts while offline; kicks a fresh cycle on link-up. - ScarfGoCoordinator + ScarfGoTabRoot funnel scenePhase transitions to ChatController.handleScenePhase. On .active we verify channel health and reconnect if dead. - Draft persistence: UserDefaults keyed by (serverID, sessionID) survives force-quit. 7-day janitor at app launch. - Connection-state banner: .reconnecting and .offline render slim ScarfDesign-tinted strips above the message list. .failed keeps using the existing full-screen overlay. Bonus fix: - ConnectionStatusViewModel tier-2 probe now checks state.db instead of config.yaml. Hermes v0.11+ doesn't materialize config.yaml until the user changes a setting, so a freshly-installed working Hermes was being marked "degraded — config missing" indefinitely. state.db is the file Scarf actually depends on. Out of scope (deferred): APNs push notifications, BGTaskScheduler- based extended-background keepalive, offline write queue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4776119e07 |
fix(ios-onboarding): hide Cancel on first-run onboarding (#55)
App Store Connect feedback: "Cancel button not working" on the
"Connect to Hermes" onboarding screen.
Confirmed root cause in RootModel.cancelOnboarding:
state = servers.isEmpty
? .onboarding(forNewServer: ServerID())
: .serverList
When the user has zero configured servers (the first-run case),
the conditional re-presented a fresh onboarding view. The button
fired, the state mutated, but the visible result was "tap Cancel,
get an identical screen" — indistinguishable from a dead button.
The defensive intent ("don't strand the user on an empty server
list") was reasonable, but the UX-as-shipped is worse than the
strand it tried to prevent — first-run TestFlight users see a
seemingly broken app.
Fix at the right layer: don't show Cancel when there's nowhere
to go.
- New `canCancel: Bool` parameter on OnboardingRootView (default
true). When false, the leading toolbar slot omits the Cancel
button entirely.
- RootView passes `canCancel: !model.servers.isEmpty`.
- RootModel.cancelOnboarding simplified — drops the defensive
`.isEmpty` re-loop branch, asserts the invariant in debug, and
in release still routes to `.serverList` (which renders an
empty-state with the "+ Add server" toolbar button) rather than
re-presenting onboarding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
afb1356b27 |
feat(ios-keychain): opt-in iCloud Keychain sync for SSH keys (#52)
Reddit-reported friction: every iOS device needed its own SSH key because Scarf hardcoded kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + kSecAttrSynchronizable=false on every Keychain write. Pairing iPhone + iPad meant onboarding twice and editing authorized_keys per device. Add an opt-in toggle in System tab → Security: - New SSHKeyICloudPreference (UserDefaults wrapper, default false so existing installs see no change on update). - KeychainSSHKeyStore.writeBundle now consults the preference: when on, items use kSecAttrAccessibleAfterFirstUnlock (no ThisDeviceOnly suffix — required for iCloud Keychain sync) + kSecAttrSynchronizable=true. - All read / list / delete queries unconditionally pass kSecAttrSynchronizable=kSecAttrSynchronizableAny so they match items regardless of sync state. Without this a flipped write would orphan items at the next read. - Public migrateAllItems(toICloudSync:) reads every stored bundle, deletes with Any, re-saves with target attributes. Idempotent. System tab Security section toggle: - Live migration on flip with a "Updating Keychain..." progress row. - Failure path reverts the toggle + surfaces the error inline rather than silently leaving the state inconsistent. - Footer copy explains the tradeoff (E2EE via iCloud Keychain; Advanced Data Protection keeps encryption keys on device). Out of scope: per-server-key sync override (M9 multi-server keys all sync or none); in-app key export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f9a288ac6c |
fix(ios-chat): dismissable keyboard via swipe + toolbar button (#51)
Pre-fix the iOS composer's TextField had no keyboard dismissal: no @FocusState, no scrollDismissesKeyboard, no keyboard accessory. With axis: .vertical + submitLabel: .send the Return key inserts a newline rather than committing, so once the keyboard rose it stayed up — hiding the top-trailing toolbar button on small phones. Three additive changes: - @FocusState private var composerFocused on ChatView, bound to the TextField via .focused($composerFocused). - .scrollDismissesKeyboard(.interactively) on the message list ScrollView so dragging the messages downward collapses the keyboard with the gesture (the standard iOS chat pattern the reporter explicitly named — "swipe away"). - ToolbarItemGroup(placement: .keyboard) accessory with a keyboard.chevron.compact.down "Done" button so dismissal is also available without a scrollable area (e.g. fresh empty-state chat before any messages exist). ScarfGo iOS only. Mac unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
558970a09a |
perf(chat-ios): mirror Mac equatable short-circuit on ScarfGo bubbles (#46)
ScarfGo's chat is a separate rendering path: LazyVStack + ForEach(controller.vm.messages) with a private MessageBubble struct (not the shared MessageGroupView/RichMessageBubble used on Mac). The Mac fix's Equatable conformances therefore didn't propagate. Without short-circuiting, every visible bubble re-evaluates body on each streamed ACP chunk because the @Observable VM's `messages` mutation invalidates anyone reading it — and each bubble's `ChatContentFormatter.segments` + `AttributedString(markdown:)` are both O(content) per render. LazyVStack already keeps off-screen bubbles dormant on iOS, but the 5–10 visible bubbles re-parsing on every chunk is enough to bog down a long turn on phone hardware. Add Equatable to MessageBubble (id-keyed, with content/reasoning/ toolCalls.count compared only for the streaming bubble id==0) and apply .equatable() at the ForEach call site. Settled bubbles short- circuit body re-eval; the streaming bubble still redraws per chunk. Note: the trailing-group patch helper (Mac fix part 2) already benefits iOS as a side effect — buildMessageGroups() is no longer called per chunk, and even though iOS doesn't read messageGroups directly, the elided rebuild is still wasted work avoided. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
87fcbad1ac |
fix(ios): App Store validation — strip 1024 alpha + drop iCloud entitlements
Apple's TestFlight upload validator rejected v2.5 with two errors; fixing both for the next archive. 1. **Invalid large app icon** (alpha channel). `AppIcon.appiconset/AW Mac OS Applications-macOS-Default-1024x1024@1x.png` was RGBA — Apple rejects any 1024×1024 marketing icon with an alpha channel. Composited the icon onto a solid white background via PIL and resaved as RGB PNG. `sips -g hasAlpha` now reports `no`. The file's design is solid edge-to-edge, so the white-fill is invisible — no visual change. 2. **Invalid Code Signing Entitlements** (`com.apple.developer.icloud-container-environment` empty string). `Scarf_iOS.entitlements` had `com.apple.developer.icloud-services = [CloudKit]` + `com.apple.developer.icloud-container-identifiers = []`. Xcode's signing phase synthesises `com.apple.developer.icloud-container-environment` from this combo, and with no container identifier the value lands as empty — which Apple's validator rejects. Per the privacy policy I drafted in v2.5 ("no iCloud Keychain sync, no cloud accounts") Scarf doesn't actually use iCloud, so removing the iCloud entries is the correct fix. Dropped both `com.apple.developer.icloud-services` and `com.apple.developer.icloud-container-identifiers`. Kept `aps-environment = development` — push capability stays declared but gated off via `NotificationRouter.apnsEnabled = false` until the cert + Hermes-side sender land. iOS scheme builds clean post-fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
850fa7a697 |
fix(ios): preserve hermes output on non-zero exit + extend remote PATH
Two related fixes that together restore Skills hub Browse / Search on iOS over Citadel SSH. CitadelServerTransport.asyncRunProcess was using `executeCommand`, which throws `CommandFailed` and discards the captured ByteBuffer when the remote process exits non-zero. `hermes skills browse` happens to print its full table and then exit non-zero on some hosts, so iOS got nothing while Mac (Foundation Process) got the full output with exitCode=1. Drive `executeCommandStream` directly so stdout + stderr are drained regardless of outcome, then catch `SSHClient.CommandFailed` to recover the actual exit code. Network/channel-level failures still report -1 so callers can distinguish them from a clean non-zero remote exit. Citadel's raw exec channel also doesn't source the user's shell rc files, so non-interactive sessions land with a stripped PATH (typically just /usr/bin:/bin). pipx installs `hermes` at `~/.local/bin/hermes`, and many of hermes's sub-tools (git, curl, python) live in homebrew prefixes that the remote sshd would otherwise add via login-shell init. Mac's OpenSSH sshd handles this transparently; Citadel does not. Inline PATH=$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH on every runProcess invocation so bare `hermes` resolves AND any subprocess it spawns can still find its tools. SkillsViewModel.finishBrowse now surfaces the actual stderr/stdout snippet when the CLI exits non-zero, instead of a canned "Browse failed" banner. ANSI-stripped + box-drawing-stripped so the message stays readable in the one-line banner. Made diagnosing the underlying PATH issue much easier and is a net UX improvement going forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
21e3cc9361 |
feat(ios): rust page background + dashboard switch-server button
Sweeps the rust ScarfDesign page background onto the screens that were still rendering against the iOS default: Skills/Hub, Skills/Updates, all three project sub-views, and Skill Detail. Lists adopt .scrollContentBackground(.hidden) + ScarfColor.backgroundPrimary, with .listRowBackground(ScarfColor.backgroundSecondary) on rows so the Mac-style elevated-card density carries through. Adds a "Switch server" toolbar button to Dashboard's top-right, threaded through ScarfGoTabRoot from the connected-server host. One tap soft- disconnects and returns to the server list — same code path the System tab already exposes, just reachable without first navigating away from Dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
295f2dfefc |
feat(ios): Mac-style page backgrounds + Dashboard + Chat redesign
iOS now uses ScarfColor.backgroundPrimary throughout instead of the default iOS systemGroupedBackground. List-based screens add .scrollContentBackground(.hidden) + the rust background underneath; list rows use ScarfColor.backgroundSecondary as their card surface. Applied to: Projects, Memory, Cron, Settings, Skills/Installed, and the Servers root. Dashboard rewritten in Mac-style cards (no more native iOS list): - ScrollView + VStack with rust background - Activity stat grid (2-col LazyVGrid) with bordered rust-tinted cards: Sessions / Messages / Tool Calls / Tokens (with in/out sub- label). - Recent sessions card — bordered, ScarfColor.backgroundSecondary, inline session rows with 1px dividers, "See all" nav to Sessions sub-tab. - Error banner styled with ScarfColor.warning tinted card per Mac. - Sessions sub-tab keeps a List view but renders against rust background with ScarfColor.backgroundSecondary row backgrounds. Chat redesigned to match the Mac chat reference: - User bubble: rust accent fill with ScarfColor.onAccent text and uneven rounded corners (top/bottom-leading + top-trailing 14px; bottom-trailing 4px) — visually pinches to the sender side, same as Mac. - Assistant bubble: rust gradient sparkles avatar tile (24x24) alongside a ScarfColor.backgroundSecondary bordered card. - ToolCallCard: kind-tinted border + uppercase tracked label (READ/EDIT/EXECUTE/FETCH/BROWSER) using ScarfColor.success/info/ warning/Tool.web/Tool.search; expanded JSON in a bordered ScarfColor.backgroundSecondary panel. - ReasoningDisclosure: warning-tinted card with REASONING uppercase label. Both Mac (scarf) and iOS (scarf mobile) schemes build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
de611c5343 |
feat(ios): adopt ScarfDesign across the iOS app
AccentColor.colorset repointed to BrandRust hex (light + dark) so the tab bar, every .tint, every default button, and every navigation accent across all 5 tabs read rust automatically. Single-line fix, biggest visible change. ScarfDesign now imported across all 27 iOS view files. Color sweep applies the same patterns as the Mac side, with two iOS-specific deviations documented in CLAUDE.md: - ScarfPageHeader is NOT retrofitted onto iOS tab roots. iOS uses .navigationTitle(...) + .navigationBarTitleDisplayMode(.large) as its native page-header pattern; stacking ScarfPageHeader on top creates double titles. ScarfPageHeader is reserved for sub-views without a native large-title bar. - Only .borderedProminent → ScarfPrimaryButton. .bordered and .plain stay native because .bordered is the iOS convention for non-primary buttons and inherits rust through AccentColor automatically. Dynamic Type policy (locked + documented in CLAUDE.md): preserve .font(.headline)/.body/.caption semantic tokens for body copy, list rows, error messages, and chat content (anything read for content). Use ScarfFont only for status badges, chip labels, intentional fixed- size display elements. Mass-swapping ScarfFont on iOS would regress accessibility for users on .accessibility2 / .xSmall. Files touched (27 view files + AccentColor + CLAUDE.md): - Color sweep: .foregroundStyle(.secondary) → ScarfColor.foregroundMuted, Color(.secondarySystemBackground) → ScarfColor.backgroundSecondary, status colors (.orange/.green/.red) → ScarfColor.warning/success/danger in: Dashboard, Skills (root + Installed + Hub + Updates + Detail), Projects (root + Detail + Sessions + Site + 8 widgets), Memory (List + Editor), Cron, Settings (root + Editor), Servers, Chat (root + Picker + Slash browser), Onboarding. - Primary button swap (5 files): Chat, Projects/Sessions, Skills/ Updates, Skills/Hub, Onboarding. - CLAUDE.md: appended "iOS Dynamic Type policy" + "iOS page chrome" guidance under the existing Design System section. Both Mac (scarf) and iOS (scarf mobile) schemes build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
26c034ea6f |
feat(ios-skills): port v0.11 features to new file structure (Phase D)
Re-port the four v0.11 iOS Skills features that lived in the now- deleted Skills/SkillsListView.swift into the new Installed/SkillDetailView + Skills/SkillsView surfaces. iOS Components/FlowLayout.swift (NEW, promoted helper): - 50-line struct conforming to SwiftUI's Layout protocol; wraps subviews onto multiple lines on overflow. Built-in API, no third- party dep. Originally inline in the deleted SkillsListView; promoted so multiple iOS views can share without duplicating ~30 lines. iOS Skills/Installed/SkillDetailView.swift (extend): - design-md npx prereq banner: yellow "Prerequisite missing" section triggered by .task(id: skill.id) probing `which npx` via SkillPrereqService when the selected skill is design-md. - Spotify info row: green "Authentication" section pointing users at the Mac sheet or shell for OAuth — phone OAuth flows are deferred in v2.5. - SKILL.md frontmatter chip rows: three sections (Allowed tools / Related skills / Dependencies) using a chipRow helper backed by the shared FlowLayout. Each section hides itself when its HermesSkill field is nil — old skills without v0.11 frontmatter show none of these. iOS Skills/SkillsView.swift (extend): - "What's New" pill at the top of the tab (above the sub-tab picker). Driven by SkillSnapshotService.diff() against the per- server last-seen snapshot. First-load primes silently so users don't see "everything is new!" noise on day one. - Recomputes on .task and .refreshable. - "Seen" button persists the current set + dismisses. Verified: iOS build succeeds. The chip-row data path is now end-to-end (SkillsScanner → HermesSkill → SkillDetailView chips) and the snapshot pill matches the Mac SkillsView placement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3d4a6a3a75 |
Merge branch 'claude/pedantic-mcnulty-bac7cc' (iOS UI refactor)
Brings the major iOS UI refactor into scarf-mobile-development on top
of the v0.11 work that landed after the merge base (commit
|
||
|
|
a73025aba0 |
feat(ios): 5-tab nav + Projects/Skills feature parity with Mac
Major iOS UI refactor that brings ScarfGo to feature parity with the Mac app for Projects + Skills, on top of a ScarfCore consolidation that unifies the view-model + scanner/parser layer between platforms. Layout (ScarfGoTabRoot): - Old: Chat / Dashboard / Memory / More (4 tabs). - New: Dashboard / Projects / Chat / Skills / System (5 tabs, Chat centered). Memory + Cron + Settings consolidate under System. Projects (NEW iOS feature): - ProjectsListView, ProjectDetailView, ProjectSessionsView_iOS, ProjectSiteView. - Widgets/ subdir: 7 widget views (Chart, List, Progress, Stat, Table, Text, Webview) + WidgetHelpers + DashboardWidgetsView. - Tied to chat via ScarfGoCoordinator.startChatInProject() which sets pendingProjectChat + flips selectedTab to .chat. Skills (NEW iOS surface): - SkillsView is a 3-sub-tab switcher (Installed / Browse Hub / Updates). - Installed/: InstalledSkillsListView, SkillDetailView, SkillEditorSheet. - Hub/HubBrowseView for the skills hub catalog. - Updates/UpdatesView for hermes skills check / update. ScarfCore consolidation: - SkillsViewModel and ProjectSessionsViewModel lift from Mac target into ScarfCore so iOS and Mac share one type. - New SkillsScanner walks ~/.hermes/skills/ once for both platforms via the supplied transport. - New SkillFrontmatterParser handles required_config: parsing. - New HermesSkillsHubParser for the hub catalog format. - Tests for both new parsers. Mac touchpoints: - Features/Skills/Views/SkillsView.swift: .onAppear wraps the now- async load() in a Task. - Old Mac-target SkillsViewModel and ProjectSessionsViewModel deleted (replaced by ScarfCore). Coordinator + chat: - ScarfGoCoordinator gains pendingProjectChat: String? + startChatInProject(path:) helper. - iOS ChatView consumes pendingProjectChat (mirrors the existing pendingResumeSessionID pattern); resolves path → ProjectEntry via registry, falls back to a synthesized entry on miss. Tests: - M5FeatureVMTests renames 3 IOSSkillsViewModel references to the shared SkillsViewModel. - New SkillFrontmatterParserTests + SkillsHubParserTests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
99f734bf0b |
feat(ios-memory): hermes memory reset on iOS too (cross-platform parity)
Mac shipped the toolbar Reset button in Phase 5; iOS gets it in the final verification pass for parity. iOS MemoryListView: - Toolbar button (counterclockwise icon) opens a destructive confirmation dialog matching the Mac copy. - resetMemory() shells out via context.makeTransport().runProcess, using the same PATH-prefix trick IOSSettingsViewModel.saveValue uses so non-interactive remote shells find hermes in ~/.local/bin / /opt/homebrew/bin / ~/.hermes/bin. - Success and failure both surface alerts (success message confirms the wipe; failure surfaces stderr+stdout combined). Verified: iOS build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8057beb001 |
feat(state.db): reasoning_content + api_call_count (Phase 4)
Hermes v2026.4.23 added two columns to state.db: - messages.reasoning_content — newer richer reasoning channel some providers emit alongside the legacy messages.reasoning blob. - sessions.api_call_count — distinct from tool_call_count; counts per-turn API round-trips so the user can see the cost breakdown. ScarfCore models: - HermesMessage gains reasoningContent: String? + computed preferredReasoning + updated hasReasoning to consider both channels. - HermesSession gains apiCallCount: Int (default 0 for old hosts). ScarfCore HermesDataService: - hasV011Schema flag detects both new columns via PRAGMA table_info; only flips true when BOTH are present (partial migrations stay on the v0.7 path to avoid runtime "no such column" errors). - sessionColumns / messageColumns / searchMessages SELECT lists conditionally append the new columns. - sessionFromRow / messageFromRow read them defensively (column index 20 / 11 respectively when v0.11 schema is on). UI surfacing: - Mac SessionDetailView shows "<N> API" label next to msgs/tools when apiCallCount > 0. - Mac Dashboard SessionRow + iOS Dashboard sessionRow add a network-icon chip with the API call count. - Mac RichMessageBubble + iOS MessageBubble switch to message.preferredReasoning for the disclosure body. Verified: ScarfCore + Mac + iOS build. 179/179 ScarfCore tests pass unchanged (existing tests didn't construct sessions/messages with the new fields; defaults preserve behaviour). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
751c9e6778 |
feat(skills): SkillSnapshotService + 'What's New' pill (Phase 3.4)
Per-server snapshot of skill signatures so the Skills tab can show "2 new, 4 updated since you last looked" — same pattern Hermes's `hermes skills update` CLI shows on the host. ScarfCore SkillSnapshotService: - [skillId: signature] map, signature is `<fileCount>:<sorted-files>`. New / removed / files-changed all show up as a delta. - diff(against:) returns SkillSnapshotDiff with counts + a label string for the pill. - markSeen(_:) persists the current set. - Backend abstraction: file-based on Mac, UserDefaults on iOS, in-memory for tests. - previousSnapshotEmpty silently primes first-load so users don't see "everything is new!" noise. Mac SkillsView: - whatsNewPill(diff:) tinted pill at the top with "Mark as seen". - recomputeSnapshotDiff() on .task and on totalSkillCount change. iOS SkillsListView: - Same pill rendered as a Section row with "Seen" button. - Recompute on .task + .refreshable. Verified: Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5c08c09dde |
feat(skills): SKILL.md frontmatter v0.11 fields (Phase 3.3)
Hermes v2026.4.23 SKILL.md files carry richer YAML frontmatter: allowed_tools, related_skills, dependencies. Surface them as chip rows in the skill detail view on both platforms. ScarfCore HermesSkill: - Three new optional fields: allowedTools, relatedSkills, dependencies. Default-nil so older skills (no SKILL.md, or SKILL.md without these fields) load unchanged. Mac HermesFileService.parseSkillFrontmatter: - Reads `<skill>/SKILL.md`, splits at `---` markers, parses the frontmatter via HermesYAML.parseNestedYAML, and extracts the three list fields. Tuple-of-optionals return; nil-everything when the file is absent or has no frontmatter. iOS IOSSkillsViewModel.parseFrontmatter: - Mirror with the iOS transport (over SFTP). Same parser, same return shape. Mac SkillsView: - skillChipSection(title:items:) helper renders a labelled chip row. Three rows added between the existing missing-config / Spotify / npx surfaces and the file list — only shown when the corresponding field is non-empty. iOS SkillDetailView: - chipRow(_:) helper using a small in-file FlowLayout (built-in Layout protocol, no third-party dep) so the chips wrap onto multiple lines on iPhone-narrow screens. Three sections matching Mac. Verified: ScarfCore + Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7ec7282f36 |
feat(skills): design-md npx prereq check (Phase 3.2)
design-md (Hermes v2026.4.23) requires `npx` (Node.js 18+) on the host to invoke `npx @google/design.md`. Probe the host's PATH when the skill is selected; surface a yellow banner with an install hint when missing. ScarfCore SkillPrereqService: - probe(binary:installHint:) async -> Status — runs `/usr/bin/env which <binary>` via the transport with a 4s timeout. Returns .present / .missing(hint) / .unknown(reason). - installHints table for npx / node / gws / ffmpeg with terse per-OS install guidance. Skills can pass custom hints if their install path is more involved. Mac SkillsView: - @State designMdNpxStatus + .onChange(of: selectedSkill.name) triggers the probe whenever the user lands on the design-md skill. Banner renders only on .missing — present and unknown cases stay silent (avoids false-alarm noise on transient SSH errors). iOS SkillDetailView: - @State npxStatus + .task(id: skill.id) per-skill probe. - Same banner with the same hint copy; no install button (user is already on iPhone, fixing the host needs a shell anyway). Verified: ScarfCore + Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
97aa988762 |
feat(skills): Spotify auth flow + sign-in sheet (Phase 3.1)
Hermes v2026.4.23 ships a `spotify` skill that needs OAuth via `hermes auth spotify`. Mirror the v2.3 Nous Portal in-app sign-in pattern so users don't have to drop to a shell. Mac (full sign-in flow): - SpotifyAuthFlow.swift in Core/Services — @Observable @MainActor, five-state machine (idle → starting → waitingForApproval(URL) → verifying → success | failure). Spawns `hermes auth spotify` via the transport, regex-detects the `https://accounts.spotify.com/authorize?...` URL on stdout/stderr, auto-opens it via NSWorkspace, and on subprocess exit polls `~/.hermes/auth.json` to confirm `providers.spotify.access_token` actually landed (exit code alone isn't proof). - SpotifySignInSheet.swift in Features/Skills/Views — five sub-views matching the state machine (starting / waiting / verifying / success / failure with retry). Auto-dismisses 1.2s after success. Mirrors NousSignInSheet shape. - SkillsView surfaces a "Sign in to Spotify" row in the skill detail pane when the selected skill is the spotify one. iOS (read-only documentation): - SkillsListView's SkillDetailView gains a "Authentication" section on the spotify skill explaining that OAuth needs to happen from Mac (or a shell). The credential lands in ~/.hermes/auth.json and ScarfGo picks it up automatically once the agent uses the skill. Editor sheet UX deferred to v2.6 — multi-line OAuth flows on iPhone are a separate UX problem. Verified: Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
64bcea35a0 |
feat(chat): git branch indicator in chat header (Phase 2.4)
Hermes v2026.4.23's TUI shows the project's current git branch as a sidebar pill. Mirror it in the chat header on both platforms. ScarfCore GitBranchService: - branch(at projectPath: String) async -> String? — runs `git -C <path> rev-parse --abbrev-ref HEAD` via the transport (works on local + remote SSH projects). Returns nil for non-git dirs, missing git, detached HEAD, or transport errors. No throwing — chat header omits the chip on any failure. Mac: - ChatViewModel.currentGitBranch populated alongside currentProjectPath in startACPSession's resolution branch. - SessionInfoBar gains gitBranch: String? — renders a tinted `arrow.triangle.branch` chip after the project chip when set. - RichChatView wires chatViewModel.currentGitBranch through. iOS: - ChatController.currentGitBranch on the same lifecycle hooks (resetAndStartInProject + startResuming + cleared on resetAndStartNewSession). - projectContextBar renders the chip inline next to the project name. Verified: ScarfCore + Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1fcd963019 |
feat(chat): numbered shortcuts on permission sheet (Phase 2.3)
Hermes v2026.4.23's TUI rewrite added 1-9 numbered shortcuts on approval prompts so power users approve/deny without reaching for the mouse. Mirror the pattern in Scarf: Mac PermissionApprovalView: - Each option button gets a "1. ", "2. ", … prefix on its label. - New private View extension `applyingNumberShortcut(index:)` binds the digit `idx + 1` (no modifiers) via .keyboardShortcut. Capped at 9; extra options stay tappable but unbound. iOS PermissionSheet: - Each row gets a monospaced "1." / "2." prefix as a hierarchy hint. - No keyboard binding (phones don't have hardware keyboards), but the numbering matches the Mac pattern so users transitioning between platforms see the same visual structure. Verified: Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
70d4c97a6c |
feat(chat): per-turn stopwatch on assistant bubbles (Phase 2.2)
Wall-clock duration of each agent turn renders as a compact pill in the message metadata footer (Mac) / below the bubble (iOS). Mirrors the per-turn stopwatch Hermes v2026.4.23's TUI rewrite ships. ScarfCore RichChatViewModel: - currentTurnStart: Date? captured in addUserMessage when entering a fresh turn (skipped for /steer-style mid-run sends so the duration reflects the FULL turn). - turnDurations: [Int: TimeInterval] keyed by finalised assistant message id; populated in finalizeStreamingMessage and cleared on reset(). - formatTurnDuration(_:) static — "0.8s" / "4.2s" / "1m 12s". Mac: - RichMessageBubble gains turnDuration: TimeInterval?; renders via formatTurnDuration in the existing metadata footer. - RichChatMessageList + MessageGroupView thread the durations dict through; RichChatView wires richChat.turnDurations. iOS: - MessageBubble gains turnDuration parameter; renders below the bubble for assistant messages only. - ChatView's ForEach passes controller.vm.turnDuration(forMessageId:). Verified: Mac + iOS builds clean. Resumed sessions (loaded from state.db) show no pill — turnDurations only populates for live ACP turns, which is the correct behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a9bd51bf05 |
feat(chat): /steer non-interruptive support (Phase 2.1)
Hermes v2026.4.23 introduces /steer — mid-run guidance the agent applies after the next tool call without interrupting the current turn. Surface it as a first-class slash command in both Mac and iOS chat menus with non-interruptive send semantics. ScarfCore RichChatViewModel: - nonInterruptiveCommands static (currently just /steer) merged into availableCommands at the end of the menu. - HermesSlashCommand.Source.acpNonInterruptive case carries the flag through to the menu UI. - transientHint: String? property for short-lived composer toasts. - isNonInterruptiveSlash(_ text: String) -> Bool helper for the send paths to detect /steer-shaped invocations. Mac ChatViewModel.sendViaACP: - /steer-shaped sends skip the "Agent working..." status update (the agent is already on its current turn) and set a 4-second transientHint "Guidance queued — applies after the next tool call." Mac RichChatView: - New steeringToast() above the input bar renders the hint when set; tinted pill with arrow icon, opacity transition. iOS ChatController.send + ChatView: - Same isNonInterruptiveSlash check surfaces the toast above the composer; auto-clears via the same 4s Task pattern. - steeringToast() helper view in ChatView. Verified: ScarfCore + Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7f5ff1946e |
feat(slash-commands): ScarfGo read-only browser sheet (Phase 1.7)
Read-only surface in iOS for browsing project-scoped slash commands. Editing on phones is its own UX problem (multi-line markdown + keyboard ergonomics) — Mac stays the canonical authoring surface in v2.5; iOS browses + invokes. When a project chat has at least one slash command loaded, projectContextBar grows a tinted "<N> slash" chip on the right side. Tapping opens ProjectSlashCommandsBrowser: - List of every command with /<name>, description, argument hint, optional model-override badge. - Tap a row → CommandDetailSheet with the full prompt-template body rendered in a monospaced block (text-selection enabled), plus metadata rows for argumentHint / model / tags. - Footer points authors back to Mac for editing. Verified: iOS build succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8a87ff1922 |
feat(slash-commands): list project commands in AGENTS.md block (Phase 1.5)
The chat layer client-side-expands /<name> args, but the agent still needs to know what commands exist so it can answer "what slash commands does this project have?" and recognise the <!-- scarf-slash:<name> --> marker prepended to expanded prompts. ProjectContextBlock.renderMinimalBlock(...) gains an optional slashCommandNames parameter; when non-empty, a new "Project slash commands" bullet lists the names as backticked /<name> entries. Mac's ProjectAgentContextService.renderBlock(for:) reads the names via ProjectSlashCommandService.loadCommands(at:).map(\.name) and emits the same bullet, keeping Mac and iOS block output aligned where the content overlaps. iOS chat resetAndStartInProject splits the slash-command load into a synchronous read on a detached task BEFORE writing the block — needed because the block has to land on disk before `hermes acp` boots, and the async load that populates the chat menu would lose the race. Verified: ScarfCore, Mac, iOS all build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6808adfa98 |
feat(slash-commands): portable project-scoped slash commands (Phase 1.1-1.4)
Net-new Scarf primitive — Hermes has no project-scoped slash command
concept. Commands live at <project>/.scarf/slash-commands/<name>.md as
Markdown files with YAML frontmatter; Scarf intercepts the chat slash
menu, expands {{argument}} substitution client-side, and sends the
expanded prompt as a normal user message. Works uniformly on Mac + iOS,
local + remote SSH, against any Hermes version (no upstream dep).
Lands the model + service + chat wiring; editor UI (Mac), read-only
browser (iOS), AGENTS.md block extension, .scarftemplate format
extension, and tests follow in subsequent commits.
What this commit ships:
- ScarfCore Models/ProjectSlashCommand.swift — Sendable struct
carrying name + description + argumentHint? + model? + tags? + body
+ sourcePath. Validates name shape (lowercase, hyphens, starts with
letter, ≤64 chars).
- ScarfCore Services/ProjectSlashCommandService.swift — transport-
based loadCommands(at:), loadCommand(at:), save(_:at:),
delete(named:at:), expand(_:withArgument:). Markdown-with-
frontmatter parser reuses HermesYAML so no new dep. Substitution
supports `{{argument}}` and `{{argument | default: "..."}}`.
- HermesSlashCommand.Source gains .projectScoped (full payload looked
up in RichChatViewModel by name) and .acpNonInterruptive (reserved
for /steer in Phase 2.1).
- RichChatViewModel.projectScopedCommands + projectScopedCommand(named:)
+ loadProjectScopedCommands(at:); availableCommands precedence is
ACP > project-scoped > quick_commands, all de-duped by name.
- Mac ChatViewModel: expandIfProjectScoped(_:) helper called in
sendViaACP; loads commands when currentProjectPath is set in
startACPSession's resolution branch.
- iOS ChatController: same pattern in send(); loads commands in both
resetAndStartInProject and startResuming(sessionID:); resume now
resolves both path AND name so we can read the slash-commands dir.
Verified: ScarfCore + Mac + iOS all build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4fc12ca790 |
fix(ios-notifications): feature-gate Approve/Deny stub actions
Push Notifications capability is disabled in the iOS target, so the APNS code path can't fire today — but the `SCARF_PENDING_PERMISSION` category was registered unconditionally, exposing the stub-only `APPROVE_PERMISSION` / `DENY_PERMISSION` action handlers as a route iOS could surface action buttons on if a notification ever slipped through. Add `NotificationRouter.apnsEnabled` (=`false`) and gate `registerCategories()` behind it. While `false`, the category is explicitly cleared so iOS has no path to route a tap to the stubs. The gate is the single switch — flipping it requires the capability + sender + real handler implementations to all land together. Verified: iOS build succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3da3d3ce5e |
fix(ios-rootmodel): surface store failures (A.3 + A.4 bundled)
Bundled because the fixes are coherent — they all add the same mechanism (`lastError` + `os.Logger`) to the same model. A.3 — Distinguish "no servers" from "Keychain unreachable": - `RootModel.connect(to:)` previously used `try?` on `keyStore.load(for:)`. A biometric cancel or device-locked Keychain read returned nil → the app dropped the user into fresh onboarding, destroying the existing server's host/user/port. Now we catch the throw, log via os.Logger, set `lastError`, and stay on `.serverList`. The user sees a banner + Dismiss button instead of being kicked back to onboarding. - `RootModel.load()` now logs the corrupted-blob path via os.Logger and sets `lastError` before falling through to onboarding (recovery is necessary, but the user gets context now). A.4 — Surface delete failures in `forget()` and `disconnect()`: - Both used `try?` on every store delete. On partial failure the in-memory dict was wiped while orphan Keychain entries lingered. Now each delete is `do/catch` with logging, failures collected into `lastError`. The in-memory state is reloaded from disk so it tracks what's actually persisted (covers the partial-failure case). ServerListView gains an inline error banner above the list that reads `model.lastError`, with a Dismiss button calling `clearLastError()`. Verified: iOS build succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
48e99f2c43 |
fix(ios-chat): surface project context block write failures
ChatController.resetAndStartInProject swallowed the SFTP write of the Scarf-managed AGENTS.md block via `try?` inside `Task.detached`. On failure (permission denied, SFTP error, malformed path) the user saw no feedback while the UI continued claiming the session was project-scoped — but the agent never received the project context, leading to silently degraded chat quality. Replace the `try? + fire-and-forget` with a `Result`-returning detached task. On `.failure`, log the underlying error via `os.Logger` and route it to the existing ACP error banner (`acpError` / `acpErrorHint` / `acpErrorDetails`) with a friendly "Project context not written — agent will proceed without it" payload. Session still starts; only the context-augmentation step is reported as missing. The session-attribution write at the same flow stays fire-and-forget by design — `SessionAttributionService.persist` already logs failures internally, and a missed attribution is purely cosmetic (Dashboard project-badge cosmetics, not chat function). Replaced the comment to make that intent explicit so future readers don't accidentally "fix" it by promoting attribution failures to the chat banner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
54a0797334 |
M9 #4.6 (pass-2): Dashboard Overview/Sessions split + chat project bar
Pass-2 feedback bundled into one architectural commit:
1. **Project indicator moved out of the nav-bar principal slot.** The
iPhone nav bar's .principal area gets squeezed to icon-only when
adjacent toolbar buttons exist — the result was a folder icon with
no project-name text, which is worse than no indicator at all. New
`projectContextBar` renders a full-width tinted strip BELOW the
nav bar when a session is project-attributed: "Project chat"
caption + folder icon + full project name. Scrolls away with the
message list. Pattern cribbed from Slack's channel-topic header
and Apple Mail's sender strip.
2. **Dashboard split into Overview + Sessions sub-tabs.** Segmented
picker at the top. Overview = stats + 5 most-recent sessions for
at-a-glance; Sessions = the deeper 25-session list with a project
filter. `See all` button on Overview's Recent Sessions header
switches tabs. Addresses pass-2 complaint: "The dashboard might
need tabs to break it down better."
3. **Project filter on the Sessions sub-tab.** Menu picker (scales
to N projects; segmented doesn't). "All projects" clears; each
project entry filters to sessions attributed there. Uses the same
attribution map loaded once in `IOSDashboardViewModel.load()`, so
filtering is an O(n) in-memory pass over 25 sessions — no extra
SFTP traffic. Addresses pass-2 complaint: "we should add a filter
to the sessions selector in the dash to see by project."
4. **`IOSDashboardViewModel` exposes the wider surface:**
- `allSessions` (25-session window, feeds the Sessions tab)
- `allProjects` (project registry, drives the filter menu)
- `sessions(filteredBy: String?)` helper — accepts a project name
(nil = all), returns filtered subset.
Mac parity note from the earlier commit message still stands — Mac's
global Sessions list doesn't currently filter by project either.
That's a parallel post-TestFlight followup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9a4473333b |
M7 #17 (pass-2): empty-transcript UX + defensive project chip
Pass-2 observations:
1. Resumed sessions from Dashboard loaded into chat but showed no
message history.
2. On sessions WITH a project badge, the chat nav-bar chip rendered
the folder icon but no project name.
**Root cause for (1)** — not actually an iOS bug. ACP-native sessions
(the kind ScarfGo starts) don't persist their transcript to the
client-visible `state.db` — only CLI/terminal sessions leave
history there. Confirmed by direct SQLite inspection: the session
IDs in Dashboard's Recent Sessions show `message_count = 0`; the
sessions with lots of messages are all older CLI sessions. The Mac
has this same limitation — just less visible because Mac's Sessions
list surfaces CLI sessions preferentially.
What we fix on the UX side: a friendlier empty state when a resumed
session has no persisted transcript. Replaces the blank canvas with
an icon + "Session resumed" + explanatory caption ("Hermes has the
context for this session, but the transcript isn't cached locally.
Send a message to continue.") Nudges the user toward the right
mental model instead of leaving them wondering why their history
vanished. Gated on `sessionId != nil` so fresh-chat empty state
stays the same.
**Root cause for (2)** — `ProjectEntry.name` shouldn't be empty, but
a defensive treatment avoids ever surfacing a folder-only chip on
edge cases (registry race, partial JSON decode). startResuming now:
- Clears `currentProjectName` eagerly at the start of the resume
flow so a lingering name from a prior session doesn't flash onto
the new header.
- Treats empty strings as nil when the lookup returns one.
And the toolbar renderer adds a `!projectName.isEmpty` guard so an
unexpected empty string never produces an icon-only chip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3b3c037fce |
M9 #4.5 (pass-2): project context surfaced in Chat nav + Dashboard rows
Pass-2 UX feedback: "When selecting a per-project chat, we should update the chat interface to show that we are 'in a project' — and label them in the sessions list so the user can see the session and understand what project it belongs to." Two related changes: **In-chat indicator** — ChatController gains `currentProjectName`, set by `resetAndStartInProject` (direct: we have the ProjectEntry) and by `startResuming` (resolved via SessionAttributionService + project registry lookup). ChatView's toolbar uses a `.principal` ToolbarItem with a VStack: "Chat" title on top, `Label(name, systemImage: "folder.fill")` subtitle underneath when attributed. Mirrors Mac's SessionInfoBar project-chip pattern but fits the iOS nav-bar real estate instead of eating a full-width horizontal row. **Dashboard row labels** — `IOSDashboardViewModel.load()` now does one additional SFTP read per refresh: pulls the session→project sidecar + project registry, maps session id → project display name into `sessionProjectNames`. Row renders a small tinted folder capsule when attributed. Batched so row renders are O(1) dict lookups — no extra SFTP traffic per cell. Silent on failure (attribution is cosmetic). Not in scope for this commit: Mac's global Sessions list doesn't currently show project attribution either — that gap exists on both platforms, but wiring Mac's ProjectsSidebar + SessionsView for per-row labels is a bigger surgery. Scoped as a post-TestFlight followup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |