weishu
50a42ecd6d
merge: A-M3f iOS composer attachments + dictation
...
# Conflicts:
# ios/README.md
2026-08-18 11:28:35 +08:00
weishu
05d063cc33
feat(ios): composer attachments + dictation (A-M3f)
...
Attachments (Android B-M3f semantics ported verbatim):
- HapiClient/Attachments/AttachmentPolicy — pure plan matrix (>4 MB
recompressible image -> 2048 px JPEG q85 with .jpg rename, 50 MB hard
reject, 192 MB image read cap, 512 px q80 previewUrl data-URL thumbs,
data-URL parse/round-trip).
- HapiClient/Attachments/ComposerAttachments — upload-on-pick tray over an
AttachmentUploading seam (APIClient conforms): uploading/ready/failed
chips, retained payload for retry, remove -> best-effort delete,
mid-upload removal deletes the orphan on completion, consume() ->
AttachmentMetadata with JPEG data-URL previewUrl, discardAllDetached +
deinit orphan cleanup (Android onCleared analogue).
- ChatInteractor: tray ownership, unsettled chips refuse the send with a
notice, attachments-only sends post empty text, optimistic rows carry
the metadata, appendDictatedText/postNotice/discardAttachments.
- App: AttachmentPreparer (capped security-scoped reads, ImageIO
downscale/encode with EXIF transform, HEIC-undecodable fallback),
PhotosPicker multi (videos via FileRepresentation temp files),
UIImagePickerController camera capture, fileImporter; composer chip row
(thumb/spinner/tap-to-retry/remove) with attachment-aware send gating;
user bubbles upgrade chips to off-main-decoded previewUrl thumbnails
(web-sent attachments included).
Dictation (Android B-M3ce port):
- HapiProtocol/Models/VoiceApi — TranscriptionResponse,
TranscriptionProvidersResponse, TranscriptionProviderInfo.
- HapiClient/Endpoints/VoiceEndpoints — GET
/api/voice/transcription/providers + multipart POST
/api/voice/transcription (file/provider/mode/language, Android part
order) over MultipartFormData; DictationTranscribing conformance.
- HapiClient/Voice/DictationController — idle/starting/recording/
transcribing, transcribed/noProvider/error events, provider memoized
(first standard-capable entry), appendTranscript port.
- App: AVAudioRecorderDictation (m4a/AAC mono 44.1 kHz 96 kbps, session
activate/deactivate), mic button + recording chip (elapsed + cancel),
record-permission request via AVAudioApplication.
Info.plist gains NSMicrophoneUsageDescription; the camera string now
covers attachment capture (modern PhotosPicker needs no photo-library
permission). Tests: policy matrix, tray over the real client with exact
base64 upload bodies + gated in-flight scenarios, dictation controller
suite with fake recorder/transport, voice endpoint request shapes, and
the interactor attachment-send matrix transcribed from the Android VM
tests (wire bodies byte-for-byte).
2026-08-18 11:27:28 +08:00
weishu
8795bab4da
feat(ios): usage/storage dashboards + settings (A-M4de)
2026-08-18 11:18:50 +08:00
weishu
ed310fcb9b
merge: A-M3ab iOS composer + permission actions + session config
...
# Conflicts:
# ios/Packages/HapiKit/Sources/HapiProtocol/Models/ApiResponses.swift
# ios/README.md
2026-08-18 10:35:36 +08:00
weishu
eed3dddb94
feat(ios): composer, permission actions, session config (A-M3ab)
...
Interaction layer turning the read-only chat into a working remote control,
mirroring the merged Android B-M3ab feature-for-feature (web authority where
the ports disagreed):
- ChatInteractor (HapiClient, fully swift-test covered): optimistic composer
sends (appendOptimistic -> POST -> status settle), queue-by-default with a
long-press Send&Steer intent while a turn is active, tap-to-retry on failed
rows (steer retries degrade to queue), per-session drafts
(UserDefaultsChatDrafts, hub-scoped keys, debounced + flushed on close).
- session_inactive (409) recovery: one POST /resume (current permissionMode)
then retry; a superseding session id seeds the new window
(MessageWindowControllers.seed), migrates the draft, retargets the
optimistic row and emits sessionSuperseded -- ChatView/HomeView replace the
navigation entry in place.
- Queued bar: uninvoked sends in web sort order with Cancel (optimistic
DELETE; invoked-race ingests the authoritative row as sent), Edit
(cancel + composer prefill, newer-draft guard) and Steer (invoked answers
reconcile a missed consume); single-flight per-row op guard.
reconcileQueuedState now runs on chat open and on session-pipe gap.
- Permission actions: flavor-exact bodies mirroring PermissionFooter.tsx --
claude {} / allowTools (Bash(cmd)) / mode:acceptEdits, codex-family
decision approved / approved_for_session / abort via deny -- plus
AskUserQuestion flat answers (option cards, Other free text, no-questions
fallback, cursor stable ids) and request_user_input nested answers
(user_note suffix, required validation); optimistic Resolving /
AlreadyHandled (404/409) overrides settled by the agentState patch.
ChatPipeline now re-attaches the window row's client status so failed
user rows actually render the retry affordance (web normalize.ts parity;
the Android reference misses this overlay).
- Session config sheet (toolbar gear): catalog-driven permission-mode picker
with tones, claude static model/effort catalogs (ModelCatalog port), codex
models via new GET /sessions/:id/codex-models endpoint + wire types with
per-model reasoning efforts; optimistic detail updates
(SessionListStore.updateDetailLocal, new) rolled forward to server truth
on error.
- Lifecycle: VisibilityReporter posts POST /api/visibility per tracked
handshake subscriptionId on scene-phase flips (404 prunes); ChatSession
exposes its subscriptionId and feeds the reporter; the global SSE pipe was
already HubSession-lifetime on iOS.
- Tests: ChatInteractorTests transcribes the Android interaction suite
against the real APIClient/AuthManager/SessionListStore/window registry
with only HTTP scripted -- canonical JSON bodies asserted byte-for-byte
(send/approve/deny/config), optimistic send happy/fail/retry, 409 resume
both id paths, cancel invoked-race, steer reconcile, edit prefill,
override lifecycle, config optimistic + rollback, drafts, abort.
2026-08-18 10:33:04 +08:00
weishu
fa91f06662
feat(ios): new session flow (A-M3c)
...
Port of the tested Android NEW SESSION reference (B-M3d) to iOS:
- HapiProtocol: NewSessionCatalogs (static claude models/efforts + codex
reasoning-effort fallback, exact Android data) in Catalog/; CodexModelSummary
+ CodexModelsResponse wire types (shared/src/apiTypes.ts).
- HapiClient: machineCodexModels endpoint (GET /api/machines/:id/codex-models,
rpc_target_missing surfaces as APIError); NewSession/NewSessionForm.swift —
typed Codable draft (tolerant decode) + NewSessionLogic: exact spawn body
per SpawnSessionRequestSchema (yolo incl. false for non-grok/non-codex-family,
permissionMode incl. 'default' for grok+codex-family, sessionType always,
trimmed-or-absent worktreeName, serviceTier only while fast tier visible,
collaborationMode only when plan, model only claude/codex), parent-path
derivation, suggestion filtering, recent-path LRU(8), worktree-name
validation, codex catalog helpers, draft sanitization.
- App: Features/NewSession (NewSessionModel @Observable orchestration —
machine preselect last-used, 250 ms debounced list-directory autocomplete
with per-parent cache, exists probe with worktree-blocking / simple
two-tap-create, codex catalog fetch + selection reconcile, UserDefaults
draft/prefs per hub; NewSessionView Form UI with per-flavor option matrix);
session-list "+" toolbar button on HomeView presents the sheet, success
dismisses and pushes the chat.
- Tests: spawn-body exactness (4 configs, canonical JSON), parent query,
suggestions, LRU, worktree validation, fast-tier detection, reasoning-effort
normalization, draft sanitize + tolerant decode; codex-models endpoint
request/error construction.
2026-08-18 10:03:05 +08:00
weishu
67b7fe7304
feat(ios): session list store + UI (A-M2a)
...
HapiProtocol (pure, mirroring the Android reference port):
- SummaryPatching: sessionSummary.ts derivations (pending requests
cap-5/oldest-first/id-tiebreak, kinds, todo progress), toSessionSummary
projection with the per-flavor agentSessionId resolution (legacy chain
omits piSessionId, replicated), applySessionSummaryPatch with the
deliberate >= version gates (vs the detail path's strict >), the
keep-alive render-irrelevance filter (pendingRequests compare ignores
'since'; metadata compare ignores hapiMcpUrl), max-monotonic updatedAt,
post-patch updatedAt as fallback 'since', and the deprecated legacy
detail-required gate kept for rule pinning.
- SessionSorting: exact list comparator (globalPinned > pinned > active >
pendingRequestsCount desc among active > updatedAt desc; stable).
- SessionMetadata: per-flavor session-id fields (claude/codex/gemini/
opencode/grok/agy/cursor/kimi/copilot/pi).
HapiClient stores (@MainActor @Observable, per hub):
- DiskCache: 500 ms debounced atomic JSON snapshots + SnapshotLocations.
- SessionListStore: sorted summaries + detail cache; full-session upsert
preserving hub-computed scheduled fields; strict-> detail vs >= summary
patch paths; keep-alive identity preservation (listRevision); empty-{}
and unparseable payloads take the REST fallback; 16 ms coalesced
refresh; optimistic pin (roll-forward) and archive (restore).
- MachineStore: the exact machine-updated decision tree.
- LastSeenStore: monotonic unread watermarks + per-scope baseline.
- SyncEventRouter: SyncEvent fan-out to the stores, global-scope message
events refresh the list, gap handshake triggers the full resync.
App:
- HubSession owns the stores + router (replaces the TODO(M2) routing);
background flushes snapshots.
- Features/Sessions: SessionListView + SessionListModel (status dot with
thinking pulse, title cascade, flavor·machine·worktree meta, relative
age on a minute timeline, pending/todo badges, unread dots, pinned
section, machine filter chips >= 2, pull-to-refresh, empty/loading/
offline states, long-press pin/archive context menu) + an M2f chat
placeholder pushed on row tap.
- HomePlaceholderView -> HomeView hosting the list (hub switcher +
connection dot kept in the toolbar).
Tests transcribe the Android suites: SummaryPatchingTest (18),
SessionSortingTest (6), SessionStoreTest, MachineStoreTest,
LastSeenStoreTest, JsonSnapshotStoreTest, StoreSyncTargetsTest — using
the existing HTTPPerforming recording stub plus a path-routing performer
for concurrent refetches.
2026-08-17 20:17:02 +08:00
weishu
1f0ec6184d
feat(ios): HapiClient API transport + auth (A-M1b)
...
APIClient (final class, Sendable) with typed endpoints for the M2/M3 REST
surface behind an HTTPPerforming seam; actor AuthManager with single-flight
JWT refresh (POST /api/auth), proactive refresh 10 min before exp, terminal
authFailed state, and 401 -> refresh -> retry-once wiring per
docs/api/client-contract/auth.md; Keychain credential store (per-hub records
under run.hapi.companion) with CredentialStoring seam + in-memory double;
HubRegistry (ordered hubs + active hub in injectable UserDefaults, origin
normalization); payload-only JWT decoding; APIError {status, code, body}
parsing per errors.md; minimal multipart builder for M4c dictation; 256 MB
URLCache session for generated images.
Request/response wire models (SendMessageRequest, PermissionApproveRequest,
SpawnRequest/SpawnResponse, MessagesQuery, envelopes, RPC-wrapped shapes)
join HapiProtocol/Models mirroring shared/src/apiTypes.ts.
swift-testing coverage: JWT decode (padding/garbage/hostile exp), auth
single-flight (8 concurrent callers -> one exchange), 401 retry-once and
terminal paths, APIError body parsing, endpoint request construction
byte-checks (cursor queries, deliveryMode body, answers formats, explicit
null model reset) via a recording performer, hub URL normalization,
multipart bytes.
2026-08-17 15:20:03 +08:00
weishu
1e80327d76
feat(ios): HapiProtocol wire models, catalogs, session patching (A-M1a)
2026-08-17 12:31:34 +08:00
weishu
e0e7be39c3
feat(ios): scaffold SwiftUI app + HapiKit package + CI (A-M0)
2026-08-17 11:18:50 +08:00