Commit Graph
12 Commits
Author SHA1 Message Date
weishu 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.
2026-09-10 16:22:36 +08:00
weishu 641ae3bf36 feat(android): zh-CN localization + language switching (B-M5a)
Extraction sweep: every user-visible hardcoded English string in
android/app moved to values/strings.xml with feature-prefixed keys
(sessions_/chat_/files_/scratchlist_/pairing_/new_session_/tool_...);
values-zh-rCN/strings.xml translates all 448 keys, terminology aligned
with web/src/lib/locales/zh-CN.ts.

ViewModels stay string-free: transient notices became semantic sealed
types resolved at the UI layer (ChatNotice, ScratchlistNotice +
ScratchlistImportRejection, PairingError, DictationErrorKind,
SessionListError.DeleteFailed.stillActive); ViewModels that genuinely
compose display text take small Strings seams with English defaults
for JVM tests (FilesStrings, FileViewerStrings, NewSessionStrings);
toolCardPresentation gains a Resources parameter.

Language switching: LanguagePrefs gains SYSTEM (follow system, the new
default); Settings applies AppCompatDelegate.setApplicationLocales
immediately; MainActivity now extends AppCompatActivity over
Theme.AppCompat.DayNight.NoActionBar with autoStoreLocales +
android:localeConfig; FCM/WorkManager surfaces wrap the application
context via localizedForAppLanguage (per-app locales miss non-activity
contexts below API 33).

Verification: :app:assembleDebug green, :app:lintDebug 0 errors
(MissingTranslation clean), full JVM test gate green (176 app tests;
notice assertions updated to the semantic types).
2026-08-18 15:54:48 +08:00
weishu bf63d2ad7b feat(android): FCM push + notification actions (B-M4a)
Gradle/Firebase: firebase-messaging + work-runtime-ktx in the catalog and
:app; com.google.gms.google-services applied CONDITIONALLY (only when
app/google-services.json exists) so the repo builds green without any
Firebase config; committed google-services.json.example + README section
(CI-injected official builds / self-build drop-in / v1.x runtime
FirebaseOptions path); real config gitignored. push/PushBinding.kt is the
availability seam: no Firebase -> every push path no-ops.

Registration (:core:data push/DeviceRegistrar): stable DataStore UUID
deviceId; POST /api/devices/register {token, platform:'phone', deviceId}
fanned out to EVERY paired hub on app start, on pairing (roster addition),
and on onNewToken; transient failures retry via a per-hub WorkManager
unique work item; best-effort DELETE on sign-out while the hub's JWT still
works.

Service (fcm/HapiFirebaseMessagingService): data-only contract v1 decoding
in :core:data push/PushPayload — channels permission_requests(HIGH) /
ready / task_notifications (created on app start), type-<sessionId>
coalescing tags, severity accent colors, notifySummary-driven ready
bodies, unknown type/contractVersion degrade to plain title/body (default
channel, no actions). Suppress-when-open: foreground + that session's
chat composed -> skip (SSE already shows it). Tap -> internal MainActivity
intent route (no public URI) -> existing navigation opens the chat.

Actions: permission-request Allow/Deny and ready/task RemoteInput Reply +
Dismiss -> NotificationActionReceiver -> expedited CoroutineWorkers
(approve/deny {} bodies, reply {text, localId}) through on-demand
HubSessions built from stored credentials (HapiWorkerFactory +
Configuration.Provider + on-demand WorkManager init — no HubGraph needed
in background); notification updates pending -> done / "Already handled"
(404 request-gone) / inactive / failed. Multi-hub: payload has no hub URL,
so workers try the ACTIVE hub first, then other paired hubs on 404
session-miss (single-hub users always hit first try).

Hub check: android.priority=HIGH is already set unconditionally for every
FCM message (hub/src/fcm/fcmService.ts) — no hub change needed.

Tests (34 new, :core:data): payload keys/severities/unknown contract
version, channel routing, suppress-when-open; registrar fan-out /
addition-only / retry-on-transient / null-token no-op via fake seams;
action wire bodies + Bearer header + multi-hub resolution through a real
HubSession against two MockWebServers. Full gate green with AND without
google-services.json (protocol 227, data 182, app 96 tests; debug +
release/R8/lintVital assemble).
2026-08-18 10:50:07 +08:00
weishu 78a5ff9961 feat(android): composer, permission actions, session config (B-M3ab)
Interaction layer turning the read-only chat into a working remote control:

- Composer: multiline input bar with optimistic sends (appendOptimistic ->
  POST -> status settle), queue-by-default delivery with a long-press
  Send&Steer intent while a turn is active, abort button during thinking,
  tap-to-retry on failed rows, per-session drafts (DataStore, hub-scoped
  keys), attachment chip seam for M4.
- session_inactive (409) recovery: one POST /resume then retry; a
  superseding session id seeds the new window, migrates the draft and
  emits SessionSuperseded for renavigation (web resolveSessionId parity).
- Queued bar: uninvoked sends with Cancel (DELETE; invoked-race ingests the
  authoritative row as sent), Edit (cancel + composer prefill, draft-kept
  guard) and Steer (POST steer; invoked answers reconcile a missed consume).
  reconcileQueuedState now runs on chat open and on session-pipe gap.
- Permission actions: flavor-exact bodies mirroring PermissionFooter.tsx --
  claude {} / allowTools / mode:acceptEdits, codex-family decision:
  approved / approved_for_session / abort -- plus AskUserQuestion flat
  answers and request_user_input nested answers forms; optimistic
  Resolving/AlreadyHandled overrides settled by the agentState patch.
- Session config sheet: catalog-driven permission-mode picker, claude
  static model/effort catalogs (ported to :core:protocol catalog), codex
  models via GET /codex-models (new HapiApi endpoint + wire types) with
  per-model reasoning efforts; optimistic detail updates rolled back to
  server truth on error.
- Lifecycle: ProcessLifecycleOwner -> SseEngine.setLifecycleForeground +
  POST /api/visibility per subscription (VisibilityReporter fed by the new
  SyncTargets.onHandshake hook); the global SSE pipe moved from the session
  list VM to HubGraph lifetime (GlobalSsePipe) so queued/consumed
  bookkeeping and list badges stay fresh while a chat is open.
- Tests: VM-level interaction suite (optimistic send/failure/retry,
  inactive-resume both id paths, cancel invoked-race, steer, exact
  approve/deny body JSON incl. both answers formats, config optimistic +
  rollback, drafts) + GlobalSsePipe tests; full gate green (554 tests,
  assembleDebug).
2026-08-17 21:01:32 +08:00
weishu b23afe24fe feat(android): read-only chat screen + store wiring (B-M2d2) 2026-08-17 17:02:37 +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 f275bae4cb merge: B-M1c Android SSE engine + reconnect state machine
# Conflicts:
#	android/README.md
#	android/core/data/build.gradle.kts
#	android/gradle/libs.versions.toml
2026-08-17 15:16:38 +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 483155ece3 feat(android): SSE engine + reconnect state machine (B-M1c)
:core:data app.hapi.data.sse — the /api/events transport per
docs/api/client-contract/sse.md (reference web/src/hooks/useSSE.ts):

- SseConnection: SseTransport seam (Connected/Event/Failure flow) with the
  okhttp-sse implementation on a dedicated client (readTimeout=0, no cache,
  own dispatcher), buildEventsUrl, Last-Event-ID header on resume, and an
  acceptEncodingIdentity fallback flag for the gzip escape hatch.
- ReconnectPolicy: normative constants + pure backoff schedule (immediate
  first retry, 1s..30s exponential, 300s ceiling after 8 attempts, 0..500ms
  injected jitter).
- SseEngine: per-key (global / session:<id>) connection loops — handshake
  gate on connection-changed{status:connected} with ok/gap resume verdict,
  per-key cursors advanced only after the downstream hand-off
  (at-least-once), 10s connect deadline, 90s staleness watchdog ticking 10s,
  one silent 401 re-auth per cycle costing no backoff attempt, background
  retry deferral + 45s foreground stale check; all timing via injected
  delay/clock for virtual-time tests.
- SyncEventRouter: 13-type union fan-out to the SyncTargets seam (M2 wires
  stores), handshake gap -> requestFullResync, Unknown ignored.

Tests (32): virtual-time engine suite (fake transport + turbine), policy
schedule with seeded jitter, router mapping, and MockWebServer integration
through the real okhttp transport — including proof that gzip SSE frames
surface incrementally (flush-per-event body withheld behind a throttle).
Gzip also verified against a live local hub (Content-Encoding: gzip,
handshake decoded instantly, heartbeat +30.0s mid-stream).
2026-08-17 15:14:55 +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 83bd25aa3b feat(android): scaffold Compose app + core modules + CI (B-M0) 2026-08-17 11:22:35 +08:00