Commit Graph
9 Commits
Author SHA1 Message Date
weishu 01dabad204 fix(sessions): stabilize ordering by latest user input
Persist and backfill the last user-message timestamp, and use it for deterministic session ordering across web and native clients while preserving manual pins.

Remove the fixed-order setting and guard ordering against transcript echoes, replayed messages, and stale REST/SSE updates. Add regression coverage and regenerate the SSE fixture.
2026-10-29 12:47:52 +08:00
weishuandClaude Opus 5.5 132d256171 feat(ios): fork, rewind and export conversations
The hub's fork, rewind and export routes had no iOS UI. The chat's
menu now offers:

- Fork Conversation: when the agent advertises forkCurrent, the
  session is live and no Codex turn is running (the web gate). The new
  session opens in place of this chat.
- Rewind or Fork…: a sheet listing loaded user messages that have a
  native point in metadata.conversationHistoryPoints. Each row offers a
  confirmed Fork from Here or Rewind to Here, gated like web: forks
  need an idle turn except on Codex, and rewinds always need one. The
  actions live in a sheet, not on the transcript bubbles, because those
  keep text selection and cached row heights. After a rewind the tail
  resyncs with a history reset. An ambiguous native boundary (409
  ambiguous_native_boundary_fork_safe) offers a fork from the same
  point instead.
- Export Conversation…: saves the hub's lossless JSON payload through
  the Files save panel, using the web download's filename. A large
  export asks before retrying with force; a 413 explains the limit.
  Markdown export stays web-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:00:00 +08:00
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 16ba8fdaa9 feat(ios): add adaptive iPad navigation 2026-09-14 18:10:59 +08:00
weishu 93e9c5ac65 fix: restore Codex titles and iOS collaboration controls 2026-09-14 08:44:02 +08:00
weishu 3510a0f4f1 fix(ios): Linux compile + full package test round — fixtures green locally (A-CI-local)
ios/scripts/linux-test.sh stages HapiKit + shared/fixtures at repo depth
into a persistent tmp dir and runs swift test in swift:6.1-noble; the
manifest drops HapiUI (SwiftUI/swift-markdown/Highlightr) under
#if os(Linux). 458 tests green, including all 48 chat and 11 pagination
golden fixtures.

Fixes that fell out of the first real compile of the blind-written port:
- ChatTypes.CodexReview.wireValue + SummaryPatching legacy id fallback:
  split expressions that exceeded the Swift 6 type-checker budget
- FileEndpointsTests: raw string containing "# terminated the literal
  early (never compiled anywhere) — now ##-delimited
- SSEClient.backoffSleep: Task { try? ... } inferred Task<()?, Never>
- Darwin gates: Security/Keychain behind canImport(Security) (tests use
  InMemoryCredentialStore via CredentialStoring), CryptoKit digest with
  FNV-1a filename fallback, FoundationNetworking imports for URLSession
  types, corelibs URLCache diskPath: initializer, get-only
  waitsForConnectivity, delegate-based SSE transport where
  URLSession.bytes(for:) does not exist
2026-08-18 11:51:57 +08:00
weishu 6c7746d2b0 feat(ios): scratchlist (A-M4b)
Per-session parked notes mirroring the Android B-M4d feature:

- HapiProtocol Models/ScratchlistApi.swift: wire types for entries CRUD
  (idempotent create via client entryId+createdAt), attachment metadata,
  limits (defaults from shared/src/scratchlistAttachments.ts), upload
  envelope, and the typed error codes (scratchlist_at_cap,
  scratchlist_attachment_too_large, scratchlist_attachment_in_use, ...).
- Endpoints/ScratchlistEndpoints.swift: GET/POST/PUT/DELETE entries,
  limits, base64 upload, raw-bytes attachment fetch, attachment delete.
- Stores/ScratchlistStore.swift: @MainActor @Observable per-session cache
  behind the SessionScratchlistStoring seam - open/release observation,
  16 ms-coalesced refetch on the scratchlistUpdatedAt SSE signal,
  optimistic create/update/delete with surgical entryId reconcile +
  rollback (refresh preserves in-flight optimistic creates), 200-entry cap
  pre-check + hub 409 verdict, uploadsInFlight, cached limits with
  offline defaults, UTF-16 text clamp at 10000.
- Stores/ScratchlistAttachmentGuard.swift: pure Fits/Downscale/Reject
  budget verdicts ported verbatim.
- SessionListStore: onScratchlistInvalidation callback fired when a
  session patch carries scratchlistUpdatedAt (the seam the M4b comment
  reserved); HubSession wires it into the store and injects the store
  into ChatInteractor.
- ChatInteractor (additive tail section): scratchlist store property,
  scratchlistCount badge seam, insertComposerText, parkComposerDraft
  (composer clears only after the hub accepts; at-cap/failed keep the
  draft).
- Features/Scratchlist/: sheet off the chat toolbar's note icon with
  count badge - entry cards (4-line preview, relative age, authed
  thumbnails via NSCache loader, filename chips), edit sheet
  (PhotosPicker -> guard -> JPEG downscale -> upload spinner tile,
  remove, delete/save), full-screen viewer (GeneratedImage pattern),
  per-entry To composer, and Park current draft in the screen header (a
  deliberate placement divergence from Android's composer button - the
  composer UI is owned by the concurrent attachments package). iOS-only
  import step transcodes disallowed-but-decodable rasters (HEIC) to JPEG
  before the guard.
- Tests (36, all transcribed from or mirroring the Android suites): store
  CRUD optimistic/rollback/at-cap/invalidation/upload/limits with
  canonical wire-body asserts + the SessionListStore seam test, 9 guard
  verdicts, 9 interactor park/insert/badge seam tests over a fake store.

Verified on Linux via a dockerized Swift 6.0 scratch copy (FIFO HTTP
performer): full HapiClient build under strict concurrency, 36/36 new
tests green, SessionListStore 17/17 and ChatInteractor 29/29 suites
green; app-side screen model + loader typechecked against stub
frameworks; SwiftUI views and the CG-based import parse-checked (macOS
CI compiles them).
2026-08-18 11:38:37 +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