Commit Graph
14 Commits
Author SHA1 Message Date
weishuandClaude Opus 5.5 64e062fb84 feat(ios): search sessions, unread-only lens and mark all read
The iOS session list could only filter by machine. Web also has text
search, an unread-only filter and Mark All as Read.

- Search: a .searchable field ranks matches with the web relevance
  rules, ported from sessionListSearch.ts via Android as
  SessionListSearch. It searches title, worktree, summary, agent,
  machine, path and id; every word must match, with */? wildcards.
  Results keep the global/project pin sections, so the Pinned header
  still applies.
- Filters menu:
  - It now holds an Unread Only toggle that keeps the iPad's selected
    chat listed.
  - The machine section appears only with two or more machines, as
    before; the menu itself shows whenever the hub has sessions.
  - Mark All as Read advances the local last-seen watermarks after a
    confirmation, as on web.
- Empty list: an empty search or filter result gets its own empty state
  instead of "No sessions yet".

getSessionTitle moves into HapiKit so search and the rows share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishuandClaude Opus 5.5 5942b427a9 feat(ios): rename, delete and reopen sessions
The API wrappers existed but iOS had no UI for these actions, while
web and Android did. The session row's context menu and the chat's …
menu now offer Rename for every session. Active sessions keep Archive.
Inactive sessions get Reopen and a confirmed Delete instead of
Archive, which the hub rejects with 409 for them.

The store applies renames and deletes optimistically. A failed rename
refetches server state; a failed delete restores the row and the
cached detail. Reopen marks the returned session active right away.
When reopen returns a superseding id, the chat seeds the new window,
moves the draft and navigates to it, as send-resume already does. A
reopen from the list also moves the draft and opens the session. A
422 shows the hub's message with the missing fields.

On iPhone, an authoritative removal (delete, or archive elsewhere) now
pops that chat, matching the iPad split, which already clears the
selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
weishu c6fae86c42 feat(ios): adapt layout for iPhone Duo
- Use the split navigation on foldable iPhones once UIHingeInteraction
  (iOS 27.1+) reports a hinge: unfolded shows sidebar + chat, folded
  collapses to the phone stack without recreating the chat.
- Side-by-side sidebar: drop the truncated inline "Sessions" title and
  move the filter beside the hub menu (also fixes iPad).
- Folded outer screen: drop the home title so the empty bar disappears,
  use the leading-aligned system title/subtitle in chat, minimize the
  chat title on scroll, and let the composer sit 16pt closer to the home
  indicator while the text field is unfocused.
- Qualify Markdown.Document, which is ambiguous with the iOS 27.1 SDK.
- test-transcript.sh picks the newest runtime that supports the device
  type (iOS 27.1 only ships iPhone Duo); split tests run on iPhone Duo.
2026-10-28 10:32:21 +08:00
weishu 16ba8fdaa9 feat(ios): add adaptive iPad navigation 2026-09-14 18:10:59 +08:00
weishu 68fe5d976e feat(ios): streamline home machine filtering 2026-09-11 20:30:31 +08:00
weishu e289a0a776 feat(ios): push — APNs registration, E2E notification service extension, actions (P3) 2026-08-18 21:07:15 +08:00
weishu 3300be929d fix(ios): hide steady connection chip; drop blank empty-header section band (device feedback) 2026-08-18 16:42:27 +08:00
weishu 6066f20c20 feat(ios): zh-CN localization + catalog dedupe (A-M5)
Part 1 — Simplified Chinese localization of the iOS app layer:
- ios/Hapi/Resources/Localizable.xcstrings: hand-authored String Catalog
  (395 keys, zh-Hans; en implicit as source). Terminology mined from
  web/src/lib/locales/zh-CN.ts (会话/新建会话/权限模式/允许/拒绝/工作树/
  机器/语音输入/用量/草稿夹 …).
- Mechanical edits only: merged multi-part string concatenations into
  single LocalizedStringKey literals, converted ternary/plain-String user
  copy to String(localized:), switched helper params (detailRow,
  DashboardCard, optionPicker, notice) to LocalizedStringKey.
- LocalizedNoticeMapper (app layer): display-point translation of the 27
  known HapiKit-emitted strings (ChatInteractor notices, dictation errors,
  window-sync warnings, files fallbacks, worktree-name validation) with
  verbatim passthrough for server-originated text; package stays
  language-free.
- Deliberately untranslated (web parity / non-copy): event rows
  (EventPresentation — web renders presentation.ts verbatim), catalog
  option labels (Default/Auto/Sonnet/permission modes — web shows them
  verbatim), tool names, code-like titles (grep(pattern:), MCP:, Skill,
  Task), unit suffixes (B/KB/MB, m/h/d/w), decorative separators.
- Language row wired: AppLanguage gains .system (follow system, new
  default); explicit picks write the AppleLanguages override, Follow
  system removes it; footer notes a relaunch applies it (no supported
  in-place SwiftUI locale swap).
- project.pbxproj: zh-Hans added to knownRegions.

Part 2 — #39 catalog dedupe (HapiProtocol):
- NewSessionCatalogs.claudeModels/claudeEfforts now derive from
  ClaudeModels/ClaudeEfforts (single source shared with ModelCatalog);
  codexReasoningEfforts stays own data (web CODEX_REASONING_EFFORT_OPTIONS
  minus max); effortLabel delegates to ModelCatalog.capitalizedFirst.
- New CatalogTests lock test for the derived option lists.

Gate: bash ios/scripts/linux-test.sh — 459 tests green.
2026-08-18 16:04:03 +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 a4355d1837 feat(ios): read-only chat assembly (A-M2f)
Wires the merged M2 pieces into a usable pair -> list -> live chat flow:

- pbxproj: link HapiUI into the app target (UUIDs C7/C8, mirroring the
  existing HapiProtocol/HapiClient product references; only project change).
- HubSession: MessageWindowControllers registry + per-chat ChatSession
  factory with in-memory per-session SSE resume cursors and scene-phase
  forwarding to the active chat.
- ChatSession (app): session-scope SSEClient while the chat is open;
  window opened/activated before subscribing; a single consume task awaits
  every event into the window actor, giving Android-channel-equivalent
  arrival-order ingestion; session-updated and friends route through the
  shared SyncEventRouter (detail patching), session-removed clears the
  window, a gap handshake triggers full resync + detail refetch + catch-up
  tail sync; stop hands the cursor back for seamless reopen.
- ChatPipeline (HapiClient): actor running the reduction path off-main -
  queued-row filter, normalization memoized by row instance identity,
  reduce + buildVisibleChatBlocks with previousGroups-stable group ids.
- ChatModel: window state + detail agentState + machine labels -> serial
  ~100 ms-coalesced pipeline loop (first run immediate, publishes strictly
  ordered), header title cascade, loading/empty/error states, loadOlder /
  retry, last-seen stamping on every update.
- ChatView: bottom-anchored ScrollView/LazyVStack (defaultScrollAnchor +
  scrollPosition(id:)), auto-stick at bottom, new-messages pill, top
  sentinel paging with documented scroll re-anchoring, degraded banners.
- Block views: user bubble w/ attachment chips, agent markdown, collapsed
  reasoning, tool cards (knownTools.tsx-parity presentation, status chip,
  read-only permission row w/ pending banner, per-tool bodies: terminal,
  before/after edits, write content, unified-diff detection -> DiffTextView,
  checklists, read-only questions, pretty-JSON fallback, children rail),
  tool groups, centered event rows (presentation.ts port), cli output,
  generated images (authed bytes + in-memory cache + full-screen viewer),
  codex review verdict card.
- Navigation/links: session list now pushes ChatView; app-level
  \.hapiOpenURL handler (https/http -> SFSafariViewController, custom
  schemes confirm first, hapi-file:// -> M4 placeholder) + theme injection.
- Tests: ChatPipelineTests drive the runner with fixture-derived windows
  (stable ids, memo-stable recomputes, queued filter, group-id stability
  across an older-page arrival).
- README: M2f app-layer description.
2026-08-17 20:55:36 +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