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