Files
scarf/scarf
Claude bd6e722029 iOS port M4: Chat via SSHExecACPChannel (Citadel exec bidirectional)
First real interactive iOS feature. Streams JSON-RPC over a
Citadel 8-bit-safe exec channel to a remote `hermes acp` process.
Reuses ScarfCore's `RichChatViewModel` state machine (from M0d)
+ `ACPClient` (from M1) unchanged — the only new code is the iOS-
specific channel + factory + SwiftUI view.

## SSHExecACPChannel

Packages/ScarfIOS/Sources/ScarfIOS/SSHExecACPChannel.swift
(iOS counterpart to Mac's ProcessACPChannel)

Uses Citadel's `SSHClient.withExec(_:perform:)`:
  - RFC 4254 exec channel, no PTY, binary-clean stdin/stdout for
    JSON-RPC bytes.
  - Bidirectional: `TTYStdinWriter` for our `send(_:)` writes,
    `TTYOutput` stream for stdout/stderr.
  - withExec's closure-scoped lifecycle handled by running it in
    a detached Task. A per-actor pending-waiters queue lets the
    first `send(_:)` block until the writer is handed over (one-
    time RTT); subsequent sends are instant.
  - `close()` cancels the Task, which drops the `withExec`
    closure, which triggers Citadel to close the SSH channel.
    Clean teardown.
  - Line framing via `Data` accumulators for stdout + stderr
    separately — Citadel yields bytes in arbitrary chunk sizes,
    we only push complete (newline-terminated) lines into the
    ACPChannel streams.

## ACPClient+iOS

Packages/ScarfIOS/Sources/ScarfIOS/ACPClient+iOS.swift
(Sibling to Mac's ACPClient+Mac.swift)

Exposes `ACPClient.forIOSApp(context:keyProvider:)`. Opens a
dedicated `SSHClient` per ACP session — NOT reusing the
`CitadelServerTransport` client. Rationale: ACP sessions can
run for minutes/hours of streaming chat, and OpenSSH caps
concurrent channels per connection at ~10. Two separate
connections (transport + ACP) stay well under.

SSH auth: ed25519 via the Keychain-stored bundle, same
`SSHAuthenticationMethod.ed25519(...)` path as
CitadelServerTransport.

## iOS Chat view

scarf/Scarf iOS/Chat/ChatView.swift + embedded ChatController
(@Observable @MainActor). Minimal v1 UX:

  - Three-state lifecycle: .connecting / .ready / .failed(reason)
  - Auto-scrolling message list
  - SwiftUI composer (multi-line TextField + Send button)
  - Toolbar "+" for a fresh session (stop → reset → start)
  - Message bubble (user: accent; agent: secondary background)

Deferred to M5: tool-call cards, permission request sheets,
markdown rendering, voice.

scarf/Scarf iOS/Dashboard/DashboardView.swift gains a
NavigationLink into Chat.

## Small public-API tweak

`RichChatViewModel.sessionId` promoted from `private(set)` to
`public private(set)` — ChatController reads it to route
`sendPrompt`. Same pattern as earlier M3 public-nits patches.

## Tests: 2 new in M4ACPIOSTests (now 98/98 on Linux)

Deliberately focused — M1's 10-test MockACPChannel suite already
covers the full ACPClient state machine. These two pin the
patterns iOS's new SSHExecACPChannel exercises:

  - streamingPromptDeliversChunksAndCompletes: full handshake +
    session/new + streamed agent_message_chunk notifications +
    session/prompt response. Verifies chunks arrive as
    .messageChunk events and prompt resolves with correct usage
    tokens.
  - permissionRequestYieldsEventAndRespondSends: remote
    session/request_permission request → .permissionRequest
    event → respondToPermission writes correct JSON back on the
    channel with matching id + outcome.

Running `docker run --rm -v $PWD/Packages/ScarfCore:/work
-w /work swift:6.0 swift test` now reports 98 / 98.

## Manual validation needed on Mac

1. Xcode compile of scarf mobile target against the merged
   pbxproj (target reconciliation shipped in the previous commit
   on this branch).
2. Chat end-to-end against a real Hermes host. From Dashboard,
   tap Chat → type "hello" → streaming response. Test "+" for
   new session. Verify no leaked SSH connections across
   Disconnect + re-onboard.
3. If your Hermes enables tools: verify tool_call_update
   notifications come through (won't render with fancy cards
   yet — that's M5 polish).

Updated scarf/docs/IOS_PORT_PLAN.md with M4's shipped state, the
"two separate SSH clients" rule, and the M5 polish backlog
(tool cards, permissions, markdown, voice).

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