Commit Graph
23 Commits
Author SHA1 Message Date
weishu b9d5f803b2 fix(android): device-feedback round 1
- decode fs.stat-derived epoch fields leniently (fractional mtimeMs from
  real hubs broke machines/files decode and pinned the offline banner)
- offline banner: only a failed sessions fetch counts; machines/baseline
  failures are advisory; live SSE emission clears it and seeds the unread
  baseline (all-rows-unread gray dot cascade)
- session row: single weighted title (short names no longer truncated at
  half width), unread dot moved beside the timestamp
- composer restyle: borderless input pill with inline mic, hand-drawn
  vector glyphs replacing emoji icons, 42dp round actions, park-draft
  relocated to the session overflow menu
2026-08-18 16:27:32 +08:00
weishu 0f3bf5ca1b merge: B-M4d Android scratchlist
# Conflicts:
#	android/app/src/main/kotlin/app/hapi/companion/Navigation.kt
#	android/app/src/main/kotlin/app/hapi/companion/feature/chat/ChatScreen.kt
#	android/app/src/main/kotlin/app/hapi/companion/feature/chat/composer/ChatComposer.kt
#	android/core/data/src/main/kotlin/app/hapi/data/api/HapiApi.kt
2026-08-18 10:50:57 +08:00
weishu 33d186f444 feat(android): scratchlist (B-M4d)
Per-session notes/drafts workbench, the native twin of the web
ScratchlistPanel + use-hub-scratchlist (tiann/hapi#893):

- wire/ScratchlistApi.kt: entry/attachment/limits DTOs mirroring
  shared/src schemas, caps (200 entries / 10k chars), typed error codes
  (scratchlist_at_cap, _attachment_too_large, _attachment_in_use, ...).
- HapiApi: entries CRUD (POST idempotent-on-entryId), limits GET,
  base64-JSON attachment upload, raw-bytes fetch over the cached image
  client, attachment delete.
- SessionStore now surfaces scratchlistUpdatedAt patches as a
  scratchlistInvalidations SharedFlow (bare refetch trigger, sse.md).
- ScratchlistStore: per-session entries StateFlow, refresh on open +
  on SSE signal (observed sessions only, 16 ms coalesced), optimistic
  create/update/delete with surgical rollback, friendly atCap state
  (local pre-check + hub 409), uploads-in-flight progress, cached
  limits with offline defaults. ScratchlistAttachmentGuard holds the
  pure pre-upload budget verdicts (fits / downscale-to-target / reject).
- feature/scratchlist: chat/{id}/scratchlist route, entry cards (text
  preview, relative age, authed Coil thumbnails), edit sheet (text +
  attachment strip with photo picker, downscale-to-JPEG import, remove,
  delete), FAB new note, full-screen viewer (generated-image pattern).
- Chat seams: top-bar notepad badge (entry count), composer overflow
  "Park draft to scratchlist" (clears only after the hub accepts), and
  per-entry "To composer" that inserts into the live ChatViewModel of
  the chat entry below the route.

Tests: store CRUD optimistic/rollback + cap 409 + invalidation-signal
refetch + upload progress/413 + attachment delete 409 (MockWebServer),
guard verdicts, ChatViewModel park/insert/badge seams (fake store).
2026-08-18 10:47:02 +08:00
weishu 75f830002b merge: B-M4e Android usage/storage dashboards + settings
# Conflicts:
#	android/app/src/main/kotlin/app/hapi/companion/Navigation.kt
2026-08-18 10:45:42 +08:00
weishu 769e0ff390 feat(android): usage/storage dashboards + settings scaffold (B-M4e)
- wire: UsageApi.kt (UsageSummaryResponse/buckets/totals, SqliteStorageUsageResponse)
- api: GET /api/usage/summary?range&timeZone + GET /api/storage/sqlite on HapiApi
- feature/settings: settings home (theme system/light/dark/OLED + Material You
  toggle, language persist-only until M5, About with app/protocol/hub health),
  usage dashboard (range control, 8 stat tiles, Canvas daily bars with tap
  tooltip, byAgent/byModel bar lists), storage dashboard (Canvas donut + rows)
- owner gating: JwtPeek ns == 'default' hides Usage/Storage entries (web
  SettingsNav parity); screens map 403 to an owner-only explanation
- theme plumbing: ThemePrefs/LanguagePrefs on hapi_prefs DataStore, exposed via
  AppGraph, applied at MainActivity setContent through HapiTheme(oled/dynamic)
- tests: usage/storage wire decode, HapiApi query+403 shapes, formatting/
  hit-rate/calendar-fill/slice math, theme+language round-trips, ns gating,
  usage/storage/settings viewmodels
2026-08-18 10:43:40 +08:00
weishu 210f9b3ff7 feat(android): files/git browser + file viewer (B-M4c)
:core:protocol git/: GitStatusParser (porcelain v2 --branch: branch headers
incl. detached/initial, 1/2/u records, untracked/ignored) + NumstatParser
(counts, binary, brace + plain rename normalization) + buildGitStatusFiles
merge — behavior cross-checked against web/src/lib/gitParsers.ts by running
the same inputs through the TS implementation, quirks preserved.

Endpoints: HapiApi gains git-status, git-diff-numstat(?staged),
git-diff-file(path, staged?), file read (base64), files search (?query&limit),
directory (?path); wire types in new wire/FilesApi.kt.

feature/files: FilesScreen (chat/{id}/files) with Changes (branch header,
staged/unstaged sections, status letters + ±counts), Browse (lazy directory
tree, dirs-first sort, hidden-file toggle), Search (300ms debounced) tabs;
FileViewerScreen (chat/{id}/file?path&staged&mode&line) with diff mode
(UnifiedDiffParser → DiffView, staged/unstaged toggle) ⇄ full mode (CodeBlock
with 400-line highlight cap, markdown Source/Preview via the shared Markdown
renderer, image bitmaps), copy path, middle-ellipsis path. Chat wiring: file
citations open the viewer in full mode (cited line shown as a hint chip — no
per-line highlight inside the single-Text CodeBlock), top-bar folder icon
opens the files browser.

Tests: parser fixtures (renames, binary, detached, conflicts, untracked
dirs) + VM tests with fakes (numstat merge, degraded numstat banner, lazy
directory loading/cache, hidden filter, search debounce, diff auto-fallback,
staged reload, base64/image/binary decode).
2026-08-18 10:40:34 +08:00
weishu c164af0738 feat(android): dictation, slash commands, session ops (B-M3ce)
- Voice dictation: DictationController (seam over recorder + api, JVM-tested),
  MediaRecorder m4a/AAC recorder, provider discovery via
  GET /api/voice/transcription/providers (new HapiApi method + wire types),
  mic button + recording chip + RECORD_AUDIO flow in the composer,
  transcript appended with space separator (web appendTranscript twin).
- Slash commands: '/' at start-of-input opens a dropdown merging
  metadata.slashCommands names with GET /slash-commands (RPC wins dedupe,
  exact>prefix>contains filter); tap inserts '/name '. Skills '$' deferred.
- Session ops: SessionStore gains rename (optimistic + roll-forward),
  delete (optimistic + 409 restore), reopen (marks returned id active);
  list long-press sheet + chat top-bar overflow wire Rename/Reopen/Delete;
  chat reopen reuses the supersede path (window seed + draft move +
  ChatEvent.SessionSuperseded); 422 formatted via formatReopenError;
  inactive-session affordance bar above the composer.
- Additive wire/API: TranscriptionProvidersResponse/TranscriptionProviderInfo,
  HapiApi.getTranscriptionProviders, ChatSessionApi.getSlashCommands,
  SessionListStore rename/delete/reopen.
- Manifest: RECORD_AUDIO + microphone uses-feature required=false. README blurb.
- Tests: DictationController (happy/no-provider/errors/cancel), slash
  merge/filter/trigger, store rename/delete/reopen (MockWebServer),
  VM reopen-supersede/delete/rename/slash suggestions; full gate green
  (protocol 227, data 148, app 96 tests; assembleDebug OK).
2026-08-18 10:16:05 +08:00
weishu 3ce85936c7 fix(android): dedupe CodexModels wire types from parallel B-M3ab/B-M3d merges 2026-08-17 21:04:04 +08:00
weishu ca138c912c merge: B-M3ab Android composer + permission actions + session config 2026-08-17 21:02:33 +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 e640aa42fd feat(android): new session flow (B-M3d)
Machine -> directory -> agent/options -> spawn, web NewSession parity:

- feature/newsession: NewSessionScreen + NewSessionViewModel (plain class
  over fakeable seams), NewSessionForm/Logic (exact SpawnSessionRequest
  mapping), NewSessionGateway, DataStore-backed prefs (last machine,
  per-machine recent paths LRU cap 8, form draft so backing out keeps input)
- directory: server-side autocomplete (list-directory on the parent path,
  debounced 250ms with per-parent cache), recent-path chips, paths/exists
  probe with the web's missing-dir affordances (worktree = error, simple =
  two-tap create-and-make-directory)
- options per flavor: claude static models+effort; codex machine catalog
  (hidden on rpc_target_missing) + reasoning effort + collaboration mode +
  fast tier gate; grok/codex-family native permission modes; claude/agy/
  cursor YOLO toggle with native-mode hint; pi managed note; copilot agent
  mode; worktree name validation; startingMode omitted (remote default)
- wire/api: CodexModelsResponse + GET /machines/:id/codex-models (flagged
  addition), MockWebServer coverage
- nav: newSession route (optional machineId), FAB on the session list,
  spawn success navigate-replaces to chat/{id}
- tests: spawn-body exactness (4 configs), debounce/parent derivation/
  listing cache, recent LRU, worktree validation, two-tap missing dir,
  draft restore + codex catalog reconcile (21 new app tests green)
2026-08-17 20:40:36 +08:00
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