An Android client for DeepSeek Harness that talks to a DSH backend directly, over the harness's own protocol.
A hobby project, written by an AI. The code here was produced by a coding agent working with one person, not by a maintained team. It works for what it was built for, but expect rough edges, uneven readability, and no guarantee of upkeep or support. Issues and pull requests may go unanswered.
A DSH instance your phone can reach. This app is a client, not a server: it needs a running DSH whose HTTP API is reachable from the phone over the LAN, together with a plugin that exposes that API for LAN use.
dsh-lan-access— required. DSH's HTTP API is bound to loopback by default; this plugin serves it on the LAN and is what makes the backend reachable from a phone at all. Install it in your DSH instance before using this app. The app talks to the endpoint that plugin exposes, so its routing and port are part of the setup.dsh-mobile— not required, deliberately. That plugin serves a web client whose bundle is re-downloaded on every reconnect; this app speaks the protocol instead and never fetches a client bundle.
Worth reading before you point this at anything.
- The connection is plain HTTP and WebSocket, with no authentication. DSH's LAN API is not authenticated and this app adds nothing on top: anyone on the same network who knows the address can read your sessions and send prompts as you. That is the trade the underlying plugin makes. Use it on a network you trust -- a home LAN, a private VPN -- and not on a shared or public one.
- The app stores the endpoint address on the device, in its private preferences. It stores no credentials because there are none to store.
- It requests only
INTERNETandACCESS_NETWORK_STATE. No storage, no location, no contacts. - Cleartext traffic is enabled in the manifest because the protocol is cleartext; without it the app could not reach the endpoint at all.
Enter host:port (for example 192.168.1.20:3080) in Settings and connect. The
app speaks plain HTTP and WebSocket on your LAN, so use it only on a network you
trust.
- Sessions grouped by workspace, newest first, with the Host's archive set respected and archived sessions one toggle away. Each row shows its state: blue while a turn runs, orange when it wants an answer, green when a turn finished that you have not looked at yet.
- Live transcript: assistant replies as Markdown (headings, lists, tables, quotes, links, fenced code with syntax highlighting), tool calls as cards that fold in their results, background-job notices as cards, and a follow-the-newest behaviour that only follows while you are at the bottom.
- The plan and the output:
todo/writerenders as a checklist, and files a turn presents open in a preview — text inline, images full screen with pinch-to-zoom. - Control: send prompts, cancel a running turn, switch model and reasoning
effort, run
/compact, answer approvals and questions the Host asks, and create sessions either in a chosen workspace or in the default directory. - Resilience: reconnects on its own, and a dropped connection does not take the app down with it.
Both were taken against a throwaway demo session ("watering a houseplant"), and the session list is redacted: what you see is the app's rendering, not anyone's work.
| sessions | conversation |
|---|---|
![]() |
![]() |
Android 10 (API 29) or newer; built against API 36.
Take the APK from Releases. It is signed with a release key; the debug APK CI uploads as an artifact is signed differently, and the two cannot update each other in place.
Open the project in Android Studio, or build from the command line with the Gradle
wrapper (./gradlew assembleDebug).
tools/ holds a second, Gradle-free build path (tools/build.sh for debug,
tools/build-release.sh for a minified release, tools/run-jvm-tests.sh for the
tests). It exists because the machine this was developed on cannot run Gradle at
all, and it drives aapt2, kotlinc, d8/R8 and apksigner directly. You do not
need it if Gradle works for you — the notes in tools/README.md explain what it
does and what it caught.
CI (.github/workflows/android.yml) runs the tests with Gradle and builds both
APKs; tagging v* publishes the signed release.
The letters DSH, set as a monogram: white ground, near-black letters, nothing else. An earlier attempt drew a rounded phone outline with a terminal prompt inside it — three ideas (frame, notch, glyph) in a space that fits one — and at 48px it read as a smudge. A monogram survives the size because the letters are the shape.
| legacy | adaptive, circular mask | adaptive, rounded-square mask |
|---|---|---|
Drawn from primitives by tools/make-icon.py — no SVG, no downloaded asset, no
vendor artwork — and committed under app/src/main/res/mipmap-*, so it is covered by
this project's MIT licence along with the rest of the code.
It is deliberately not DeepSeek's logo, wordmark or mascot, and it uses none of their artwork: a third-party client wearing the official mark invites exactly the confusion about who made it that this project wants to avoid. "DSH" here is an abbreviation of the API it speaks, not a claim to the brand. If the vendor objects to the monogram anyway, open an issue and it will be replaced.
Regenerate app/src/main/res/mipmap-* with python3 tools/make-icon.py, or these
previews with python3 tools/make-icon.py --export. --preview prints both shapes as
ASCII (dark meaning ink), which is how the mark was checked without an image viewer —
and how the rejected outline was caught.
| path | what |
|---|---|
app/src/main/java/.../Protocol.kt |
wire types and decoders |
app/src/main/java/.../DshClient.kt |
OkHttp transport: unary calls, mux streams, reconnect loop |
app/src/main/java/.../AppState.kt |
state holder, event → transcript reducer, actions |
app/src/main/java/.../MainActivity.kt |
Compose UI |
app/src/main/java/.../SimpleMarkdown.kt |
hand-written Markdown subset |
app/src/main/java/.../CodeHighlight.kt |
hand-written syntax highlighting |
docs/event-coverage.md |
every session event type, and how each is rendered |
docs/design-notes.md |
why the app works the way it does, one note per change |
tools/README.md |
the Gradle-free build, the local test runner, protocol probing |
Four things about this harness are easy to get wrong, and each cost a bug here:
- Event shapes come from the wire, never from a guess.
docs/event-coverage.mdrecords where each one was observed, and the decoder tests pin them with captured payloads. - A
user/messageis an envelope, not a person. Onlysource.kind == "user"is human;plugin,agent-message,subagent-settled,agent-instructionsandskill-catalogare all machine-produced, and rendering them as user text puts words in the reader's mouth. - The Host owns workspace grouping and the archive set; the client renders what
workspace/followreports, and archiving is one-way — there is no unarchive API. session/createtakesworkspaceIdorcwd, never both, and only the workspace route makes the new session a member of that group.
MIT covers everything in this repository, including the launcher icon, which is drawn here rather than taken from anyone. It is an unofficial client: not affiliated with, endorsed by, or supported by DeepSeek. See Icon.

