Files
scarf/scarf
Claude 6b731ddfb8 iOS port M5: Chat polish + Memory + Cron + Skills features
Fleshes out the iOS app from "Chat + placeholder Dashboard" into a
real on-the-go Hermes companion: Chat now renders tool calls + tool
results + permission sheets + markdown + chain-of-thought, and the
Dashboard gains three new feature surfaces.

## Chat polish

scarf/Scarf iOS/Chat/ChatView.swift — several new small SwiftUI
view types:

  - ToolCallCard: expandable card for each HermesToolCall on an
    assistant message. Tool-kind icon in the header (from
    HermesToolCall.toolKind.icon), arguments summary collapsed,
    full JSON on tap.
  - ToolResultRow: compact "Tool output" disclosure for messages
    with role == "tool", shown indented beneath the preceding
    assistant bubble.
  - PermissionSheet: SwiftUI .sheet(item:) presentation of
    RichChatViewModel.pendingPermission. Tapping an option
    dispatches ChatController.respondToPermission → ACPClient.
  - ReasoningDisclosure: DisclosureGroup for HermesMessage.reasoning,
    collapsed by default so chatty thinkers don't dominate scroll.

MessageBubble now renders assistant content through
AttributedString(markdown: options: .inlineOnlyPreservingWhitespace).
User messages stay plain Text (no reason to parse what the user
just typed). Unknown markdown falls through as literal text — worst
case, no formatting.

ChatController gains respondToPermission(requestId:optionId:) that
forwards to ACPClient and clears vm.pendingPermission on the
MainActor.

## New feature surfaces

### Memory (read + edit)

ScarfCore/ViewModels/IOSMemoryViewModel.swift:
  - Kind enum (.memory / .user) → maps to paths.memoryMD / .userMD
  - text (mutable) + originalText (pristine) + hasUnsavedChanges
  - load() / save() / revert()
  - async file I/O via ServerContext.readText / writeText — run on
    a detached task so the MainActor doesn't hang on remote SFTP

scarf/Scarf iOS/Memory/:
  - MemoryListView: two-row NavigationLink (MEMORY.md, USER.md)
  - MemoryEditorView: TextEditor bound to vm.text, toolbar Save +
    Revert, "Saved" bottom toast on success.

### Cron (read-only)

ScarfCore/ViewModels/IOSCronViewModel.swift:
  - Loads ~/.hermes/cron/jobs.json via transport.readFile + decodes
    into CronJobsFile (Codable, shipped in M0a)
  - Missing file = empty list (no error — common on fresh installs)
  - Sort: enabled-first, then nextRunAt ascending, disabled last
  - Surfaces decode errors via lastError

scarf/Scarf iOS/Cron/CronListView.swift:
  - Row: state-icon + name + schedule.display + next-run-at.
  - Detail: prompt, schedule, state, delivery route (via
    job.deliveryDisplay), skills, model.

Editing is deferred — needs atomic jobs.json rewrites. Shipped the
read path so users can at least audit their cron config on the go.

### Skills (read-only)

ScarfCore/ViewModels/IOSSkillsViewModel.swift:
  - Scans ~/.hermes/skills/<category>/<name>/ via transport.listDirectory
    + transport.stat for directory-ness
  - Filters dotfiles. Skips empty categories. Swallows per-category
    listing errors (permissions etc.) rather than failing the whole
    load.
  - requiredConfig stays empty — YAML frontmatter parsing deferred
    (would need a parser in ScarfCore; see M5 plan note).

scarf/Scarf iOS/Skills/SkillsListView.swift:
  - Grouped by category, tap → SkillDetailView (path + file list).

## Supporting tweaks

- RichChatViewModel.PendingPermission: fields + public init promoted
  from `let`/internal to `public let` / `public init(...)` so
  PermissionSheet can read title/kind/options and tests can construct
  one directly.

- LocalTransport.writeFile refactored to use Data.write(options: .atomic)
  instead of FileManager.replaceItemAt. replaceItemAt is Apple-only;
  Linux swift-corelibs doesn't fully implement it, which was breaking
  the M5 save-path tests on Linux CI. Data.write(atomic) is cross-
  platform and has identical semantics (temp-file + rename). Also
  auto-creates the parent directory if missing, folding in the one
  bit of the old logic that wasn't atomicity-related.

- DashboardView: single Chat Section → "Surfaces" Section with four
  NavigationLinks (Chat / Memory / Cron / Skills).

## Tests (ScarfCoreTests/M5FeatureVMTests, 10 new)

.serialized suite — tests install a `withLocalTransportFactory`
helper that swaps ServerContext.sshTransportFactory to produce a
LocalTransport against real tmp files (so .ssh contexts in the
test resolve to local FS paths). Restored in defer. Serialized
because the factory is a static.

  - memoryLoadsEmptyWhenFileMissing
  - memoryRoundTripsFileContent  — seed file → load → edit → save
    → reload via fresh VM → confirm persistence
  - memoryRevertRestoresOriginal
  - memoryKindPathRouting        — pin .memory → memoryMD etc.
  - cronEmptyWhenJobsFileMissing — missing file is not an error
  - cronLoadsAndSortsJobs        — 3-job fixture, verify sort:
                                   enabled-before-disabled and
                                   nextRunAt-ascending within
  - cronSurfacesDecodeErrors     — garbage jobs.json
  - skillsEmptyWhenDirMissing
  - skillsScansCategoryAndSkillStructure — 2 categories, dotfile
                                           filter check
  - skillsSkipsEmptyCategories
  - pendingPermissionMemberwise  — SQLite3-gated (RichChatViewModel
                                   is gated)

**108 / 108 passing on Linux** (98 → 108).

## Manual validation needed on Mac

1. Xcode compile clean against M5 source additions.
2. Chat: trigger a tool call + a permission request. Verify cards
   render, options dispatch, markdown looks right.
3. Memory: edit MEMORY.md on phone → save → confirm via `cat` on
   the remote.
4. Cron: existing jobs show sorted + detail view useful.
5. Skills: browse matches `ls ~/.hermes/skills/<cat>/<name>/`.

Updated scarf/docs/IOS_PORT_PLAN.md with M5's scope, rationale
for the LocalTransport.writeFile refactor (Linux CI), and the M6
Settings-blocker (needs YAML parser port).

https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
2026-04-23 17:12:38 +00:00
..