weishu
80948e93c9
merge: B-M2a Android chat pipeline port (48 fixtures green)
2026-08-17 16:21:25 +08:00
weishu
03651984e0
feat(android): chat pipeline port — 48 chat fixtures green (B-M2a)
...
Port web/src/chat/ (normalize → reduce → group) to :core:protocol
app.hapi.protocol.chat, file-for-file:
- ChatTypes (blocks/tool-call/permission/usage; AgentEvent sealed over a
verbatim raw JsonObject so wire events project byte-exact)
- Normalize/NormalizeUser/NormalizeAgent (codex/output/event families,
agy mapping, skip rules, stringify-never-drop fallback)
- Tracer (parentToolUseId grouping, prompt fallback, orphans)
- ReducerEvents/ReducerTools/ReducerCliOutput/ReducerTimeline/Reducer
(usage-limit pipe parsing, dedupe + title echo, api-error folding,
tool pairing, stream coalescing, agent-run cards, turn-duration,
title-changed synthesis, sentinel suppression, pending-permission
synthesis with oldest-visible gate, latestUsage, goal filtering)
- ToolGroups (families, id stability, buildVisibleChatBlocks)
- ToolPresentation (event labels; java.time in place of toLocaleString)
- FixtureProjection + CanonicalJson + ChatFixtureTest: every
shared/fixtures/chat/*.json replayed and compared under canonical
JSON equality, with a loud fixtureVersion gate
TS undefined maps to Kotlin null, TS null to JsonNull; permission
objects carry a presence set so JS spread-merge semantics survive.
:core:protocol:test 223/223 green (48 fixtures), :app:assembleDebug ok.
2026-08-17 16:20:27 +08:00
weishu
dfcf9849b9
merge: B-M2c Android message window store, pagination fixtures green
2026-08-17 15:51:34 +08:00
weishu
99df45e99a
feat(android): message window store port, pagination fixtures green (B-M2c)
...
:core:protocol window/ — pure state machine ported function-for-function from
web/src/lib/message-window-store.ts + messages.ts: merge-by-(id|localId) with
optimistic echo reconciliation, position ordering (invokedAt ?? createdAt, seq,
ASCII id tie-break), trim-preserving-queued with the codex agent-run budget,
epoch reset handling, latest-replace with request-baseline identity
preservation, consumed/cancelled/queued-reconcile transitions, tail/history
modes, hydrate/persist shapes. MessageRetention ports the null-decision tree
of normalizeDecryptedMessage (dedup with the B-M2a pipeline port flagged).
:core:data store/ — per-session MessageWindowStore (StateFlow + Mutex,
single-flight tail sync with trailing drain, fetchOlder with epoch-reset
resync, SSE ingest hooks, optimistic sends, queued-state reconciliation,
seedFrom for resume id changes) + WindowSnapshots (atomic JSON files, LRU 10;
JsonSnapshotStore dedup TODO) + MessageWindowStores registry. Minimal
MessagesApi interface (sealed MessagesQuery) extracted over the two message
endpoints, implemented by HapiApi.
Gate: PaginationFixtureTest replays all 11 shared/fixtures/pagination scripts
against the real store — requests, older-load outcomes, reconcile candidates
and the final window projection all exact — 11/11 green; plus targeted
concurrency/snapshot/seed unit tests. :app:assembleDebug green.
2026-08-17 15:50:47 +08:00
weishu
96d81a3f12
merge: B-M2b Android session/machine stores + session list
...
# Conflicts:
# android/app/build.gradle.kts
2026-08-17 15:48:09 +08:00
weishu
f702cbf844
feat(android): session/machine stores + session list (B-M2b)
...
:core:protocol — summary-side patch path ported from the web reference:
- wire/SummaryPatching.kt: patchSessionSummary port with the summary path's
>= versioned gates (replicated web divergence vs the detail path's strict >,
documented), derived-field recompute (pendingRequestsCount/Kinds/Requests
capped 5, todoProgress), render-irrelevance filter (activeAt-only keep-alive
suppression), toSessionSummary/toSessionSummaryMetadata projection, and the
deprecated canApplyVersionedSummaryPatch legacy gate.
- wire/SessionSorting.kt: exact sortSessionSummaries comparator
(globalPinned > pinned > active > pendingRequestsCount among active >
updatedAt desc; Long-safe, stable).
- SessionMetadata grows the per-flavor agent session id fields the
agentSessionId projection needs.
:core:data store/ — StateFlow stores with per-hub JSON snapshots:
- JsonSnapshotStore: debounced (500 ms) atomic tmp+fsync+rename snapshots,
synchronous cold-start load, corrupt files degrade to null.
- SessionStore: sorted summary list + per-id detail cache (strict-> detail
patching via SessionPatching), full-session upsert preserving hub-computed
scheduled fields, REST fallback for unparseable payloads, coalesced refresh
(16 ms batch like the web), optimistic pin/archive.
- MachineStore: full/patch/null machine-updated decision tree per sse.md.
- LastSeenStore: per-session last-seen watermarks + once-per-scope baseline
seeding + unread derivation (sessionLastSeen.ts/sessionAttention.ts port).
- StoreSyncTargets: SyncEventRouter fan-in — global-scope message-stream
events refresh the list, gap handshakes full-resync.
:app feature/sessions/ — standalone screen + plain-constructor ViewModel:
- SessionListScreen: pinned section, status dot with thinking pulse, flavor/
machine/worktree meta line, pending badge, todo chip, unread dot, machine
filter chips, PullToRefreshBox, offline banner, empty states, long-press
pin/archive sheet; taps emit onOpenSession only (no navigation).
- SessionListViewModel: store combine -> UiState, owns the global SSE
subscription while started (subscribe after collector registration),
entry refresh, error SharedFlow for snackbars.
Tests: SummaryPatching/SessionSorting JVM tests (cases mined from
useSSE.test.ts), store tests (stale/equal/newer patches, full replace,
keep-alive identity, snapshot round-trips, optimistic rollback), ViewModel
combine + handshake tests with fake stores. All of :core:protocol:test,
:core:data:test, :app:testDebugUnitTest, :app:assembleDebug green.
2026-08-17 15:47:05 +08:00
weishu
3cf2a669c3
feat(android): pairing flow, deep link, DI graphs, navigation skeleton (B-M1d)
2026-08-17 15:39:29 +08:00
weishu
3570017743
merge: B-M2d1 Android markdown/code/diff rendering foundation
...
# Conflicts:
# android/gradle/libs.versions.toml
2026-08-17 15:15:10 +08:00
weishu
d39197b97d
feat(android): markdown/code/diff rendering foundation (B-M2d1)
...
:core:protocol (pure JVM, all web-transform ports fixture-style tested):
- markdown/MarkdownTransforms.kt: repairTables (remark-repair-tables port),
disableIndentedCode (enabledBlockTypes minus IndentedCodeBlock == micromark
disable codeIndented) + canonical parser factory (GFM tables, strikethrough
requireTwoTildes, autolink), stripCjkAutolinkArtifacts, detectFilePathLinks/
matchWholeFilePath/rewriteExplicitLinkTarget (remark-file-path-links port)
- markdown/HrefPolicy.kt: Allowed|ConfirmFirst|Blocked scheme policy with
double-percent-decode + control-char-strip bypass hardening (web parity);
scheme-less/SPA/protocol-relative fail closed on native
- git/UnifiedDiffParser.kt: full unified diff -> DiffFile/DiffHunk/DiffLine
(rename/copy/new/deleted/binary/mode/similarity, quoted paths, bare codex
diffs, tolerant of truncated hunks + wrong LLM counts)
- 56 new JVM tests (repair matrix, parser config, CJK, path detection incl.
negatives, href matrix, 11 diff scenarios cross-checked vs web semantics)
:app Compose UI:
- ui/markdown/Markdown.kt: transforms -> commonmark AST walk; paragraphs/
headings/lists (ordered/bullet/task via stripped text markers)/blockquote/
tables (intrinsic-width grid in horizontal scroll)/thematic break/html
monospace fallback; unknown fences (mermaid/math) degrade to CodeBlock;
LinkAnnotation links via LocalMarkdownLinkHandler (onFilePath/onUrl+policy)
- ui/markdown/CodeBlock.kt: header chip + copy, horizontal scroll, highlights
1.0.0 syntax coloring off-main via produceState with 200-entry LRU keyed by
(hash, len, lang, dark), 400-line cap then plain
- ui/components/DiffView.kt: hunk headers, +/- tinted rows, dual line-number
gutters, compact/expand
- ui/theme: HapiExtendedColors (code/diff/table/quote tokens mirroring web
index.css light/dark/OLED) + pure-black OLED scheme, provided by HapiTheme
- MarkdownPreviewParameterProvider + kitchen-sink @Previews (compile-checked)
deps (Maven Central verified): org.commonmark:commonmark 0.24.0 + gfm-tables/
gfm-strikethrough/autolink exts, dev.snipme:highlights 1.0.0 (jvm variant)
Verified: :core:protocol:test 141 green, :app:assembleDebug green
2026-08-17 15:14:01 +08:00
weishu
6f1dc23d77
feat(android): auth + HapiApi transport (B-M1b)
...
:core:data auth + API transport for track B M1:
- auth/: JwtPeek (unverified {uid,ns,exp} peek for proactive refresh),
CredentialStore interface + EncryptedPrefsCredentialStore
(security-crypto) + in-memory impl, HubUrls origin normalization,
HubRegistry (ordered multi-hub roster + active hub behind a storage
seam), AuthInterceptor + single-flight TokenAuthenticator (401 ->
re-exchange -> retry once, Mutex single-flight, terminal AuthEvents,
ensureFreshToken() for SSE pre-connect)
- api/: HapiApi (plain OkHttp + kotlinx.serialization, one suspend fun
per v1 endpoint incl. generated-image bytes + multipart transcription
helper), ApiError with (status, code) per errors.md
- HubSession: per-hub factory wiring clients (timeouts, auth decoration,
256 MB image cache), DI-free
- :core:protocol wire/: request DTOs (ApiRequests.kt) + response DTOs
(auth, health, resume/reopen, cancel/steer/queued-state, spawn,
slash-commands/skills, machine list-directory/paths-exists, uploads,
transcription)
- tests: 45 unit tests (MockWebServer) — refresh/retry-once, concurrent
single-flight, terminal 401 paths, ApiError code mapping, request
shapes (cursors+epoch, deliveryMode, nested answers, explicit nulls),
JwtPeek/HubUrls/HubRegistry
2026-08-17 13:07:44 +08:00
weishu
b09dd67268
feat(android): protocol wire models, catalogs, session patching (B-M1a)
...
:core:protocol, package app.hapi.protocol — pure-JVM foundations for the
Android client, mirrored from shared/src/schemas.ts + sessionSummary.ts and
docs/api/client-contract/{sse,pagination,messages}.md:
- wire/: shared HapiJson config; DecryptedMessage with tri-state invokedAt
(custom serializer — explicit null = queued vs absent = legacy-invoked);
AttachmentMetadata; AgentState + (completed) requests; Session verified
field-by-field against SessionSchema; typed-subset SessionMetadata;
SessionSummary/PendingRequest; SessionPatch with VersionedValue wrappers
and a strict SessionPatches.parse (unknown key / empty / mistyped -> null,
replicating SessionPatchSchema.strict()); Machine family; the 13-type
SyncEvent union behind total SyncEvents.parse (unknown/malformed ->
Unknown, never throws; machine-updated data kept tri-state for the
removed-vs-refetch distinction); MessagesPage + REST envelopes.
- catalog/: AgentFlavor (known + Other(raw)) with capabilities/labels from
flavors.ts; PermissionMode/tone table, per-flavor mode lists, codex
collaboration + copilot agent modes incl. legacy 'fleet' normalization.
- patch/SessionPatching.kt: exact port of web/src/lib/sessionPatch.ts —
strict > version gates, max-monotonic updatedAt, activeAt <60s
render-irrelevance, and deliberately NOT applying activeTurnStartedAt /
scratchlistUpdatedAt (the TS reference never assigns them).
Tests (85 total, all green): golden-fixture decoding over shared/fixtures/
chat/* (fails loudly on zero files or unsupported VERSION), version-gate /
keep-alive cases ported from useSSE.test.ts, full-session + list + messages
page decode, SyncEvent union coverage, catalog pins (TODO(K6): switch to
fixtures/catalogs/modes.json once generated). :app:assembleDebug verified.
2026-08-17 12:06:32 +08:00
weishu
83bd25aa3b
feat(android): scaffold Compose app + core modules + CI (B-M0)
2026-08-17 11:22:35 +08:00