mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
9384ce031b9cea30c19cccf0ced97e116ee3fdbb
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
16ba8fdaa9 | feat(ios): add adaptive iPad navigation | ||
|
|
ee46cd6fea | feat(ios): integrate scratchlist with the chat composer | ||
|
|
b41b9293e9 |
fix(web): render cold sessions from a small initial page (#1809)
* fix(web): speed up cold session first paint * test(web): update pagination fixtures for cold page * test(web): wait for cold page before loading history * feat(native): match cold session pagination * test(native): cover cached re-entry page size |
||
|
|
c2e3d16b7e |
feat(native): improve anchored chat scrolling and history loading
Add viewport-driven paging with layout acknowledgements, bounded retries, cancellation gates, and epoch-safe history retention. Preserve transcript anchors and expansion state, fix tool-group identity collisions, and serialize Android history coordination on Main. Reduce per-scroll composition and layout work; add native regression tests, CI coverage, and profiling guidance. |
||
|
|
f0e5ba9c0f |
feat(codex): mid-turn Steer via app-server turn/steer (#888) (#1606)
* feat(shared): steer capability gates and live steered signal schemas - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which agents can deliver queued messages into the active turn (pi, codex, cursor ACP; legacy stream-json cursor excluded) - AgentState.steeringActive, DecryptedMessage.steered and messages-consumed live signal (never persisted by the hub) * feat(cli): queue reservations and steered messages-consumed option - MessageQueue2 gains takeByLocalId/restoreReservation/ beginReservationDispatch/commitReservation so an async steer can reserve a queued row without racing the main loop's turn/start drain - emitMessagesConsumed accepts steered: true to mark mid-turn delivery * feat(codex): mid-turn steer via app-server turn/steer (#888) - CodexAppServerClient.steerTurn + TurnSteerParams/Response types - CodexRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, validates it against the active turn (no control commands, matching mode hash), injects via turn/steer with an epoch guard that invalidates in-flight steers on abort/cleanup - steeringActive agent state tracks the active-turn window - hub syncEngine gate opens to codex; messages-consumed relays steered * feat(web): Steered badge and steer gating for codex sessions - HappyUserMessage shows a ↳ Steered badge fed by the live messages-consumed steered signal, preserved across server echoes and refetches (mergeMessages carries the optimistic marker) - SessionChat gates canSteer via isSteeringSupportedForSession instead of the pi-only check - clearStaleQueuedStatus normalizes a queued status on an invoked message - fix(web): drop duplicate showSessionSummaryInChat in markdown test (upstream typecheck breakage) * fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise codex and pi only; cursor joins when its soft-steer handler lands (#1609) - turn/steer now splits dispatch (stdin accepted) from completion (turn finished): the hub RPC acks once dispatch succeeds — never on the concurrent turn's completion, which can exceed the 30s RPC window - queue row commits only after the turn settles; a rejected/aborted steer restores the row so the message still delivers via turn/start, and a dispatched steer is never restored (no duplicate delivery) - steer carries clientUserMessageId (echoed as userMessage.clientId) so ambiguous transport failures can reconcile the thread later - client tests cover dispatch/complete split and stdin-write failure * fix(codex): reconcile dispatched steers before restoring; align error copy - A dispatched turn/steer whose completion fails (disconnect / protocol error) is now reconciled via thread/read by clientUserMessageId before the queued row is restored — the instruction is only re-delivered by turn/start when the thread never received it - Reconcile targets the pinned steer thread, not whichever turn is current when completion fails - syncEngine unsupported-flavor error now matches the capability gate (Pi and Codex only until the cursor handler lands) - launcher tests cover steer success (ack on dispatch), reconcile-accepted and reconcile-rejected outcomes * fix(codex): consume the row at dispatch; drop background reconcile - The hub RPC acks and the queue row is consumed as soon as stdin accepts turn/steer; completion is background-only logging. A dispatched steer is never restored, so the same localId cannot be re-delivered via turn/start after the caller was told the steer succeeded - Dispatch failure (stdin write error) still restores the row and reports failure - steer.completed rejection is always handled (no unhandled rejection on the dispatch-failure path) - tests updated: completion failure after dispatch keeps the row consumed; dispatch failure restores it * fix(codex): distinguish definite rejection from indeterminate completion - Transport-level failures (timeout, abort, disconnect, spawn, protocol) carry an indeterminate marker; explicit JSON-RPC error responses do not - After a dispatched steer, turn completion resolves → commit + consumed; a definite app-server rejection restores the row (instruction was never accepted, so turn/start cannot duplicate it); an indeterminate outcome leaves the row reserved so it can never be delivered twice - Completion handling registers before awaiting dispatch so the dispatch-failure path cannot leak an unhandled rejection - client/launcher tests cover explicit rejection (restore), indeterminate outcome (row stays reserved) and dispatch failure * fix(codex): reconcile indeterminate steers instead of a permanent reservation - After an indeterminate completion (disconnect/protocol), reconcile the thread by clientUserMessageId immediately: accepted → commit + consumed, provably rejected → restore, still unreadable → keep the reservation and retry from the main-loop top on later passes (post-reconnect) - A row never sits in dispatching forever: the hub cannot stamp it invoked while the instruction may never have been accepted - tests: indeterminate keeps reserved while thread unreadable; accepted reconciliation consumes; rejected path restores * fix(codex): accept all thread item shapes; retry reconcile; ack through abort - Reconcile matcher accepts userMessage/user_message with clientId/ client_id, matching the shapes the thread parser supports — an accepted steer can no longer be misclassified as rejected - A pending reconciliation schedules a wakeLoop retry, so a temporary app-server outage cannot strand the reservation behind waitForTurnOrRecovery - The success-path ACK no longer checks the steer epoch: the hub already reported steered on dispatch, so commit + messages-consumed must reach it even when an abort resets the queue in between * fix(codex): reinit reconnected app-server; keep reconcile retries alive - thread/read after a disconnect auto-connects a fresh app-server, which must be initialized before any request — reconcile now ensures connect + initialize (isConnected getter added to the client) - every still-unknown loop-top reconciliation schedules the next retry, so recovery without external traffic is eventually observed - launcher mock gains isConnected * fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK - Reconciliation runs on a self-rescheduling 1s timer independent of the main loop (wakes it too), so idle loops and waitForTurnOrRecovery still observe app-server recovery; abort clears nothing implicitly — the ACK path commits and consumes even when the reservation was cancelled - Absence of a durable client id is ambiguous: unmatched reads stay 'unknown' and keep retrying instead of restoring the row - CodexAppServerClient tracks initialized state (reset on disconnect/exit) so ensureAppServerInitialized re-initializes a fresh process before thread/read; initialize failures leave the flag false for the next retry - tests: accepted reconciliation via scheduled timer, indeterminate keeps reserved, explicit rejection restores * fix(codex): bind reconciliation to the launcher lifecycle - runSteerReconciliation clears any armed retry timer on entry and never installs a second one, so loop-top and timer-driven passes cannot multiply - shuttingDown is set when the main loop ends: timers are cleared and the pending map is dropped, so an unresolved steer can never respawn an app-server after cleanup (remote-to-local switch included) * fix(codex): report steered only after app-server acceptance - The handler now awaits steer.completed (the inject-acceptance response): an explicit JSON-RPC rejection surfaces as failed and restores the row for the normal turn/start path instead of a false steered - Transport failure after dispatch reports 'Steer outcome is being reconciled' and keeps the row reserved while the timer-driven thread reconciliation runs - dispatch-failure path also swallows the paired completion rejection * fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait - MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching steer reservation: the hub neither deletes the row nor stamps invoked_at (new CancelMessageResponse 'busy' status; web restores the optimistic row); pushIsolateAndClear and reset/close share cancelReservations so /clear-style commands cannot have a rejected steer resurrect a discarded prompt - turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a lost response is indeterminate and funnels into thread reconciliation instead of stranding the reservation - tests updated for the tri-state cancel contract * fix(codex,web): busy-aware edit flow; bound reconciliation reads - QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it never prefills the composer when the row is inside an async steer, so a second client cannot send a duplicate - reconcileSteerByClientId bounds thread/read with a 5s timeout so a connected-but-silent app-server cannot hold the reservation in-flight indefinitely * fix(steer): inFlight-dominated cancel acks; bounded reconciliation - hub cancel-queued-message acks check inFlight before removed: a stale duplicate socket reporting removed can no longer delete the durable row while another socket is dispatching the steer - reconciliation entries expire after 60s and mark delivered: after the rejection window, a dispatched steer that the app-server never proved (client ids dropped on restart) is committed instead of polling thread/read forever - pre-dispatch failures (abort before write included) never enter reconciliation — they restore the row and report failure * fix(steer): persist indeterminate outcomes without replay * fix(steer): make ambiguous delivery restart-safe * fix(steer): recover crash-held rows and preserve retry dedup * fix(steer): ack retries and bound stdin dispatch * fix(steer): reconcile indeterminate dispatches and serialize retries * fix(codex): classify stdin callback failures as indeterminate * fix(steer): recheck indeterminate cancels after ACK * fix(steer): close retry and abort races * fix(steer): serialize live retries and abort admission * fix(steer): distinguish live dispatching from unknown * fix(steer): keep ACK failures held and reconcile busy cancel * fix(steer): distinguish held cancel from removal * fix(store): combine schema v24 migrations * fix(store): reserve schema v25 for steer delivery state * fix(steer): keep held cancel state and notify requeue * fix(steer): release explicitly cancelled unknown reservations * fix(codex): reject cancelled reservations before native steer * fix(codex): make reservation restore atomic with state * fix(codex): terminate abandoned transport writes * fix(steer): own abandoned app-server lifecycle and consume races * fix(codex): confirm dispatch and recover abandoned turns * test(codex): mock abandoned transport callback * fix(codex): clear visible turn state on transport loss * fix(steer): claim retries and cover native delivery state * fix(native): preserve indeterminate state on Android hydration * fix(steer): make retry claims single-winner * fix(steer): serialize concurrent retry claims * fix(socket): tolerate missing steer-state ACK callbacks * fix(native): serialize retry operations * docs(web): document unknown steer delivery and retry controls * fix(steer): handle retry failures and abort-before-connect * fix(steer): reinitialize after transport loss and finish iOS retry errors * fix(steer): preserve indeterminate rows across reconnect gaps * test(web): mock indeterminate queued recovery state * fix(steer): recover consumed ACK tombstones * fix(steer): expose consumed cancel tombstones |
||
|
|
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
|
||
|
|
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). |
||
|
|
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.
|
||
|
|
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. |
||
|
|
3f497d5669 |
merge: A-M2d iOS message window store + pagination fixture harness
# Conflicts: # ios/README.md |
||
|
|
7712a92f51 |
feat(ios): message window store port + pagination fixture harness (A-M2d)
HapiProtocol/Window/ — pure port of web/src/lib/message-window-store.ts + messages.ts, mirroring the Android reference port 1:1: - MessageWindowState: constants (400/600/800/800/200), MessagePosition, OlderLoadOutcome, full InternalState fields, persisted v2 snapshot shape - MessageWindowLogic: every transition (merge/trim preserving queued rows, latest replace with request-baseline identity preservation, tail sync begin/apply/finish, older pages + epoch-mismatch reset, enterTailMode that deliberately keeps requiresLatestReset, activate, SSE ingest with hidden-row cursor advance, markConsumed stamping server rows to 'sent', optimistic lifecycle, queued reconcile, hydrate/persist, seededState) - MessageMerge: position comparator (ASCII tie-break), localId echo replacement preserving status/invokedAt, 10s sent-dedup fallback - MessageRetention: calls the fixtures-green chat pipeline's normalizeDecryptedMessage directly (no hand-mirrored tree to drift) - WindowMessage: identity-carrying final class (web's !== baseline classification), tri-state invokedAt, buildOptimisticMessage HapiClient/Stores/ — async half: - MessagesProviding seam (three-variant MessagesPageQuery; APIClient conforms via its existing endpoints) - MessageWindowController actor: single-flight tail controller with trailing drain and a synchronous generation bump before the first await (the web runs to its first suspension; Android used UNDISPATCHED), fetchOlder with onBeforeApply veto, SSE hooks, optimistic append/status/ cancel-invoked, queued-state reconciliation in 1000-id batches, seedFrom - WindowSnapshotStore: per-session JSON snapshots, LRU 10 (dedup TODO with the session-list package's cache) + MessageWindowControllers registry Tests (swift-testing): - PaginationFixtureTests: parameterized replay of shared/fixtures/ pagination/*.json against the real controller via a scripted provider; asserts expectedRequests (canonical JSON incl. explicit-null untils), expectedOutcome, expectedCandidates, and the final expectedState projection with per-op labels and first-differing-line diffs - MessageWindowControllerTests: tail-sync coalescing + trailing drain, concurrent-SSE preservation across a reset replace, cursor-no-advance guard, snapshot round-trip/LRU, seedFrom All 11 fixtures verified green against a line-by-line Python mirror of these exact algorithms (no local Swift toolchain; CI runs the real suite). |
||
|
|
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.
|