-
-
Notifications
You must be signed in to change notification settings - Fork 4
For Developers
An orientation page. The authoritative documents are in the repository:
docs/ARCHITECTURE.md,
docs/PROTOCOL.md,
CONTRIBUTING.md.
| Android Studio | Koala or newer |
| JDK | 17+ |
| Android SDK | 35 (compileSdk / targetSdk 35, minSdk 26) |
| Gradle | 8.11.1 (wrapper) · AGP 8.7.3 · Kotlin 2.0.21 |
| Stack | Jetpack Compose · Hilt · OkHttp · kotlinx.serialization · DataStore · WorkManager |
Dependencies are pinned in gradle/libs.versions.toml. Do not set org.gradle.java.home in
gradle.properties — a machine path there breaks CI.
core/ pure JVM, no Android imports
wire/ envelopes, the lenient WireJson codec, OkHttp transport,
downlink-only WebSockets, the typed API client, the
connection loop (readiness handshake + backoff)
wire/dto/ kotlinx.serialization ports of the harness schemas
session/ EventFold: raw session events -> ConversationSnapshot
notify/ CompletionClassifier: which events deserve a notification
app/ Android UI, connection/discovery, foreground service,
notifications, i18n
mock-harness/ Ktor stand-in for the harness /api server, used by tests
tools/capture/ Node recorder: real harness traffic -> conformance fixtures
tools/brand/ SVG -> VectorDrawable converter for the launcher icon
harness/ the LAN patch and setup guide for the harness itself
Two shapes worth internalising before changing anything:
-
The wire layer never crashes on unknown data. Unknown keys are ignored; unknown event, frame and
tool-card types fall through to
Unknown*passthroughs. HTTP status is carrier-only — business failures arrive asok: falsewith a typed error code. - Session projections are derived, not fetched. Permissions, stats, token usage, context pressure and breakdown, image limits and plan state are read off the projection frames already in the snapshot, so they stay in lockstep with the transcript and cost no round trips. An absent key means the harness composes no such service, so the UI hides the control.
One process-scoped store owns all harness state and exposes it as StateFlows; screens observe, and
user actions go back through the store to the API client. RPC errors never throw — they land on the
store's error channel.
./gradlew :app:assembleDebug # debug APK
./gradlew :app:assembleRelease # release APK (signed when keystore env is set)The shipped version comes from the git tag: the release workflow exports DSH_VERSION_NAME from the
tag name and versionCode is derived from it. A local build falls back to the literal in
app/build.gradle.kts.
Release signing activates only when the keystore file exists, so a build without it produces an unsigned APK rather than failing:
| Env var | |
|---|---|
DSH_KEYSTORE |
path to the keystore |
DSH_KEYSTORE_PASSWORD · DSH_KEY_ALIAS · DSH_KEY_PASSWORD
|
credentials |
-
dsh webon your computer (default port 3080). - Device or emulator over USB:
adb reverse tcp:3080 tcp:3080, then connect the app to127.0.0.1:3080. - Over Wi-Fi: apply the LAN patch — see LAN Mode.
Connecting over adb reverse makes the app a loopback client, which is the only way to exercise
the surfaces the harness pins to loopback.
mock-harness is a scriptable Ktor implementation of the /api protocol: the two WebSockets, the
unary and gateway POST paths, session export, and a replica of the trust fence that 403s a
non-trusted Host before dispatch. It deliberately does not depend on core's DTOs, so the mock
and the client cannot co-drift.
tools/capture records real harness traffic into fixtures (Node 18+):
npm install --prefix tools/capture
DSH_URL=http://192.168.1.20:3080 DSH_SECONDS=10 node tools/capture/capture.mjsRun it from the repository root so capture-output/ lands where .gitignore expects it.
./gradlew :core:test :mock-harness:test :app:testDebugUnitTest # what CI runs
./gradlew :app:lintDebugPlain JUnit4 with kotlinx-coroutines-test; no Robolectric and no instrumented tests. That is why
decision logic is deliberately extracted into free functions — slash adjudication, search derivation,
node visibility, tool-row summaries, subnet checks, connect diagnosis, subagent tree building,
history paging, landing-session policy, version comparison — each of which is unit-tested directly.
MissingTranslation and ImpliedQuantity are promoted to lint errors: that is what actually
enforces the eleven-language claim, so adding a string means adding it everywhere.
- Kotlin official code style; one file per screen or component where sensible.
- The wire layer parses leniently — unknown keys, events and tool cards must fall back, never crash.
- UI strings live in
values*/strings.xmlonly. Never hardcode text. - Pull requests need CI green: unit tests, lint, assemble.
| Workflow | Trigger | Does |
|---|---|---|
ci.yml |
push to main, all PRs |
JDK 17 + Android SDK → unit tests → :app:lintDebug → :app:assembleDebug → uploads the debug APK as an artifact |
release.yml |
tags matching v*
|
Restores the keystore from secrets → DSH_VERSION_NAME from the tag → release unit tests → :app:assembleRelease → SHA256SUMS.txt → publishes a GitHub release |
Release secrets: RELEASE_KEYSTORE (base64), RELEASE_KEYSTORE_PASSWORD, RELEASE_KEY_ALIAS,
RELEASE_KEY_PASSWORD. Tagging v0.3.2 ships 0.3.2; see Release Notes.
Everything about envelopes, the two downlink streams, the gateway paths, projections, the handshake and
the trust fence is in
docs/PROTOCOL.md.
The authoritative shapes live in the harness repository itself — this app documents only the subset it
implements.
DSH Mobile — an unofficial, community-built Android remote for the
DeepSeek Harness. The harness and its brand belong
to their respective owners.
MIT ·
repository ·
canonical technical docs in
docs/.