682 lines
34 KiB
Markdown
682 lines
34 KiB
Markdown
# DSH Desktop (reference host)
|
||
|
||
A minimal Electron desktop that talks to the DeepSeek Harness (DSH) runtime
|
||
over its internal JSON-RPC 2.0 protocol. It is the reference host for the
|
||
protocol: a place to see the whole capability surface working end to end
|
||
before we build the "real" apps against it. Not a shipped product — a demo
|
||
kept outside the official repo so the shell can move fast and the protocol
|
||
can stay strict.
|
||
|
||

|
||
|
||
[](docs/demo-shots/showcase-2026-07-18/demo-walkthrough.mp4)
|
||
|
||
*[Demo walkthrough](docs/demo-shots/showcase-2026-07-18/demo-walkthrough.mp4) — 3.6 MB mp4, click poster to play.*
|
||
|
||
## Quick start
|
||
|
||
Two `pnpm install` steps: once at the repo root (installs `tsx`, which
|
||
this shell uses to run the runtime's `.ts` bins), then once inside
|
||
`examples/desktop/` for the shell's own deps.
|
||
|
||
```sh
|
||
# from the repo root of your deepseek-harness clone
|
||
pnpm install
|
||
cd examples/desktop
|
||
pnpm install
|
||
pnpm start
|
||
```
|
||
|
||
First launch shows a two-step onboarding overlay (role · approval mode)
|
||
that writes a starter `~/.dsh-desktop/` and boots the **stdio-deepseek**
|
||
profile. From that point on, `pnpm start` picks up where you left off —
|
||
sessions and overlays live under `~/.dsh-desktop/`. (See below for
|
||
what happens when you launch without `DEEPSEEK_API_KEY` set — you get
|
||
a one-click switch card, not a red wall.)
|
||
|
||
Prereqs: node 22.22+ / pnpm 11.7+, plus the DSH runtime SDK on disk.
|
||
The shell auto-detects the SDK in two shapes:
|
||
|
||
- **In-repo (default when you clone deepseek-harness):** the shell
|
||
sits at `examples/desktop/` and finds the runtime by walking up to
|
||
the repo root. Nothing to configure.
|
||
- **Sibling checkout:** if you've cloned this shell into its own
|
||
directory next to a `deepseek-harness-dev/` checkout (the original
|
||
dev-workflow layout), the shell falls back to that sibling. Prefers
|
||
the `.worktrees/integration/` worktree when materialized — that's
|
||
where `daemon-demo` lives until it lands on master.
|
||
|
||
Set `DSH_DEV_ROOT=…` to force any custom layout.
|
||
|
||
**The default profile is `stdio-deepseek` — a real DeepSeek model,
|
||
real tool calls, real approvals.** That's on purpose (flipped
|
||
2026-07-18 per boss call): new downloaders should see the actual
|
||
model on the first prompt, not an echo bot. It needs
|
||
`DEEPSEEK_API_KEY`; the two supported places to put it are:
|
||
|
||
- A `.env` file at the DSH runtime root (repo root when you cloned
|
||
deepseek-harness; the sibling `deepseek-harness-dev/` checkout in
|
||
the dev-workflow layout), one line, format `DEEPSEEK_API_KEY=sk-...`
|
||
— the runtime picks this up on spawn.
|
||
- Or exported in your shell before `pnpm start`
|
||
(`export DEEPSEEK_API_KEY=sk-...`).
|
||
|
||
If no key is present when you launch, the shell doesn't wall you off
|
||
with a red banner — it renders a runtime card titled
|
||
**"DEEPSEEK_API_KEY needed for real-model profile"** with hint text
|
||
that spells out both options ("(1) set DEEPSEEK_API_KEY in .env or
|
||
your shell (see README Quick Start), or (2) try the keyless echo demo
|
||
to explore the UI first") and a **Switch to keyless demo
|
||
(stdio-echo)** button that one-click drops you into the echo profile
|
||
so you can walk every tab without a key. Any profile you switch to
|
||
persists to `~/.dsh-desktop/config.json` and comes back on next boot.
|
||
|
||
The model dropdown filters itself to models the current profile can
|
||
actually reach, so if you switch to an echo profile you'll only see
|
||
`mock-echo`, and switching back to `stdio-deepseek` restores
|
||
`deepseek-v4-flash` / `deepseek-v4-pro`. See the **Profiles** table
|
||
below for the full matrix; the "What's real vs demo" section right
|
||
after that spells out which surfaces are live wire vs. fixture on
|
||
each profile.
|
||
|
||
## Pages & Features
|
||
|
||
The sidebar nav groups the 14 pages into three bands: **observation**
|
||
(what the runtime is doing right now), **iteration** (loop-in tooling
|
||
you point at the runtime), and **runtime & rest** (the plumbing you
|
||
configure and the places you send the results). Pages marked
|
||
**(demo)** carry a `demo` chip on the nav item; the "What's real vs
|
||
demo" section below spells out exactly which surfaces are fixture and
|
||
which gap ticket tracks the missing wire.
|
||
|
||
### Observation
|
||
|
||
#### Chat
|
||
|
||
The default pane. A conversation is a DSH **session**: the sidebar lists
|
||
all your sessions with a pulsing live-dot on whichever one has a turn in
|
||
flight, and the transcript replays cleanly when you switch between them.
|
||
Streaming assistant text, reasoning blocks, and tool calls all render
|
||
inline as they arrive; every tool call is a collapsible `<details>` block
|
||
keyed by `callId`, reasoning is a first-class bubble (not hidden behind
|
||
a toggle), and `context/message` / `steering/message` events surface as
|
||
📎 cards so you can see what the runtime added to the model's context
|
||
and why. A `Cancel` button appears mid-turn and cuts the stream via
|
||
`session/cancel`.
|
||
|
||
The wire accepts only one in-flight prompt per session, so pressing Enter
|
||
while a turn is running **queues** your message instead of erroring: a strip
|
||
above the composer shows one chip per queued message with a `queued N`
|
||
counter, and each chip lets you edit the text inline, delete it, or bump it to
|
||
the front (**send next**). When the turn ends, the head of the queue is sent
|
||
automatically — one message per turn completion — so a burst of follow-ups
|
||
plays out in order without you babysitting each turn. The queue is
|
||
per-session (switching sessions shows that session's queue), survives a
|
||
cancelled turn, and is cleared with a notice if the runtime restarts or you
|
||
switch profiles.
|
||
|
||

|
||
|
||
#### Session Tree
|
||
|
||
Sessions are not a flat list — DSH records lineage in the session header
|
||
(`parentSession`, `seedLength`) and emits `subagent.started` /
|
||
`subagent.finished` notifications when a child agent runs. The **Tree**
|
||
tab folds the flat `session/list` into a forest so you can see the
|
||
whole fork ancestry at a glance. Orphans (children whose parent is
|
||
missing from the list) surface with an `(orphan)` badge rather than
|
||
being silently promoted to roots. Inside chat, every assistant bubble
|
||
carries a hover-revealed **fork from here** button that mints a child
|
||
session at that exact seq and marks it in the tree with a `⑂ forks
|
||
from here (N)` card.
|
||
|
||
#### Context
|
||
|
||
A per-turn ledger of everything the runtime injected, compacted, or
|
||
recalled on the current session. Each event row shows the source knob
|
||
(inject / recall / compact), the payload preview, and the seq it
|
||
attached to, so you can walk backwards from any assistant reply to the
|
||
exact context slice that produced it. Per-knob chips call out which
|
||
gap-ticket (G<n>) writeback is still pending — the page is real wire
|
||
for events that already ship (`context/message`, `session/compact`
|
||
outcomes) and honestly labels the rest.
|
||
|
||
#### Tracing
|
||
|
||
The project-wide runs table — every session across every profile in a
|
||
single eight-column aggregate: **Name / Most Recent Run / Trace Count /
|
||
Error Rate / P50 / P99 / Total Tokens / Total Cost**. Clicking a row
|
||
opens a tri-view drawer (Tree / Timeline / Graph — see Feature
|
||
highlights) that recursively unfolds a single session's event tree,
|
||
LLM/tool timing spans, and callgraph. Meant for the "which of my 200
|
||
sessions actually cost me tokens this afternoon" question, backed by
|
||
the same `session/list` projection the Chat sidebar reads.
|
||
|
||
### Iteration
|
||
|
||
#### Playground **(demo)**
|
||
|
||
An isolated scratch runtime that lives right next to the Plugins pane.
|
||
Enter via the **Playground** button; a fresh daemon boots against a
|
||
throwaway leaf so you can try a plugin lineup without disturbing your
|
||
main session. `Discard` throws the whole thing away; `Apply` promotes
|
||
the tried lineup back into your user overlay. The compare drawer picks
|
||
a live session, copies its first user message into the playground
|
||
input, and paints the live session's events alongside the playground
|
||
stream — same prompt, two overlays, side by side. The scratch boot is
|
||
real; the standalone Playground page hasn't fully landed, so the nav
|
||
button currently shims to Plugins.
|
||
|
||
#### Hub **(demo)**
|
||
|
||
The plugin discovery surface — the "shop" side of the same list the
|
||
Plugins page edits. Plugin listings themselves are real wire
|
||
(`daemon/plugins/list`), but the surrounding catalogue metadata
|
||
(categories, descriptions, ratings, install counts) is fixture pending
|
||
G1 / G11 / G12. A page-footer SDK legend spells out which chip on a
|
||
card is coming from where.
|
||
|
||
#### Bench
|
||
|
||
The benchmark harness surface — pick a benchmark, pick a lineup, watch
|
||
the runner tick through cases and stream the pass/fail column into
|
||
your session tree. The benchmark list and detail views are fixtures
|
||
until the wire methods land; the page header carries a legend chip
|
||
`demo · G18/G19/G20 pending` so you can tell fixture rows from live
|
||
ones at a glance.
|
||
|
||
#### Rubrics **(demo)**
|
||
|
||
The RL-annotation surface: pick a session, walk its turns, attach a
|
||
rubric score + a written justification to each assistant reply.
|
||
"Create from scratch" opens a rubric editor; "Import from…" pulls a
|
||
rubric spec from a plugin. Drafts saved live in memory until the G1
|
||
seam lands — the page-header legend `demo · G1 seam pending` calls
|
||
that out.
|
||
|
||
### Runtime & rest
|
||
|
||
#### Plugins
|
||
|
||
DSH is a cordis leaf — a flat list of `{id, name, config?}` entries —
|
||
and the Plugins tab is where you see and shape that list. Three
|
||
sub-areas: **Installed** (base entries and user-overlay entries side by
|
||
side; toggle any row, or **Add plugin…** to append a new one — changes
|
||
stage into your user overlay and ship on the next **Apply + restart**),
|
||
**Browse** (a curated marketplace read from `config/plugin-index.json`;
|
||
cards carry icon, package name, permission badges (net / fs /
|
||
subprocess), and a one-click **Install** that writes an overlay patch),
|
||
and **Vibe a plugin** (opens a chat under a leaf that mounts
|
||
`@deepseek-ai/dsh-tool-cordis` so the model gets `cordis_inspect`,
|
||
`cordis_mount`, `cordis_unmount` and can literally write plugin code
|
||
that extends its own runtime in a `node:vm` sandbox — two clicks from
|
||
"I need a plugin that does X" to a running plugin that does X).
|
||
|
||
#### Runtimes
|
||
|
||
Local runtime profiles and their isolated daemons in one place — one
|
||
row per profile with a status dot, PID, socket path, and the compose
|
||
recipe (which cordis leaves it's built from). Real wire on the plugin
|
||
list (`daemon/plugins/list`); the compose recipe view is fixture-tier
|
||
with a visible `composed locally · gap G8` chip. Meant for the "which
|
||
daemon am I actually talking to right now" question when you have
|
||
several profiles alive.
|
||
|
||
#### Missions
|
||
|
||
Single-screen view of everything happening across every session at
|
||
once. Built for the "agent has fanned out" case — plan execution,
|
||
code-review sweeps, research bundles — where the flat chat view stops
|
||
being enough. Three subviews over the same in-memory aggregate:
|
||
**Tree** (one info-dense row per session with status dot, title,
|
||
last-activity, event / tool-call / todo counts, and the last-event
|
||
summary), **Topology** (hand-rolled SVG layered layout of the
|
||
parent→child DAG; running nodes get a pulsing halo, edges into or out
|
||
of a running session carry a dashed flow animation), and **Board**
|
||
(kanban over todos aggregated across every session that emitted a
|
||
`todo/write`, three columns, each card badges back to its originating
|
||
session). Every card click jumps back to Chat for that session. The
|
||
header button `mock: mission demo` injects a synthetic 3-level 8-node
|
||
scenario so every subview is demoable without a live daemon; the
|
||
default empty-state preview shows three ghost cards labelled `preview`
|
||
until a real `todo/write` replaces them.
|
||
|
||
#### Growth
|
||
|
||
An auditable self-evolution log. Every plugin install, toggle, vibe
|
||
session, overlay apply, and onboarding choice appends a line to
|
||
`~/.dsh-desktop/growth-log.jsonl`; the page turns that into a heatmap
|
||
+ timeline + identity card. Deliberately "facts, not persona copy" —
|
||
every timeline node is clickable, and either drops you into the actual
|
||
session that produced it or shows the raw log row. Each node also has
|
||
an **Ask DSH about this** pill that opens a chat scoped to that entry.
|
||
|
||
#### PRs
|
||
|
||
A PR list that lives one click away from the chat. `gh pr list` runs
|
||
in the current profile's `cwd` (default: the DSH dev clone), so the
|
||
list is the real PRs against `deepseek-harness/deepseek-harness`.
|
||
Rows carry state / title / `head → base` / diff totals / relative
|
||
time. Clicking a row opens the PR in your default browser; the
|
||
trailing **Ask DSH** button mints a fresh session, seeds it with a
|
||
scoped review prompt, and switches you to Chat. Two clicks from PR
|
||
list to a running review chat. Falls back to a small demo dataset
|
||
when `gh` isn't installed or you're signed out, with a `demo data`
|
||
badge and a banner explaining the fix.
|
||
|
||
#### Settings
|
||
|
||
Profile switch, model dropdown, `DEEPSEEK_API_KEY` presence check, and
|
||
the local pricing table (pricing lives in the shell, not the daemon,
|
||
so tokens-to-cost math is auditable client-side). Also where you
|
||
reset onboarding, override `DSH_DEV_ROOT` / `DSH_ARTIFACT_DIR`, and
|
||
inspect which plugins the current profile composes. Every knob here
|
||
is real wire.
|
||
|
||
### Off-nav affordances
|
||
|
||
#### Quick chat
|
||
|
||
A floating composer that opens above whichever tab you're on. Trigger
|
||
with **⌘⇧Space** (or **Ctrl+Shift+Space** on Linux/Windows), or click
|
||
**Quick chat** in the chat pane header. The card shows the five most
|
||
recent sessions with click-to-jump, plus a fresh composer. Submitting
|
||
mints a new session, switches to Chat, and sends the prompt. Meant for
|
||
the "quick thought, don't lose my place" case.
|
||
|
||
#### Devtools drawer
|
||
|
||
The user-visible embodiment of DSH's *model-visible ⟺ logged* contract:
|
||
every `session.event` the runtime shipped over the wire lands here.
|
||
Dev-facing families (`hook/*`, `request/header*`, `approval/*`,
|
||
`permission/*`, `bash/sandbox-mode`, `step/*`, `tool/code-dispatch`, …)
|
||
that would otherwise spam chat live here instead. Open with the ⚙
|
||
button in the chat header, or press ⌥D. Preset filters (All /
|
||
Approvals / Hooks / Requests), auto-populated type chips, and a
|
||
case-insensitive search box that runs against event type, session id,
|
||
or pretty-printed payload. Auto-scroll follows the tail; a 500-entry
|
||
ring buffer accumulates in the background so opening the drawer later
|
||
still shows the recent tail.
|
||
|
||
## Feature highlights
|
||
|
||
The affordances that cut across pages — the recurring ways the shell
|
||
teaches you to read what the runtime is doing. Every one of these is a
|
||
place where DSH departs from a "chat app that also lists tools" and
|
||
becomes a visualiser of the runtime's actual state.
|
||
|
||
- **Trace tri-view (Tree / Timeline / Graph).** One session, three
|
||
reprojections of the same event stream. Tree is the recursive event
|
||
hierarchy (turn → tool call → sub-events). Timeline is a Gantt-style
|
||
span chart with LLM latency and tool latency on separate lanes.
|
||
Graph is a callgraph over `parentSession` + subagent edges. Wired
|
||
from the Tracing table and from any assistant bubble's ⋯ menu.
|
||
- **Recursive collapsible Fields tree.** Every event payload — no
|
||
matter how deep — renders as a folder-tree of `field: value` rows
|
||
you can twist open one level at a time. No "click to expand JSON in
|
||
a modal"; the whole payload is the UI. The same widget backs
|
||
`session.event` inspection, tool-call args/results, and the `{ }`
|
||
drawer.
|
||
- **Reasoning as a first-class bubble.** Reasoning deltas stream into
|
||
their own bubble with a distinct visual weight, not folded behind a
|
||
"show thinking" toggle. The runtime emits them; the shell renders
|
||
them; if the model has none, the bubble simply doesn't appear.
|
||
- **Diff and terminal tool cards.** Tool calls that carry a
|
||
filesystem-diff payload render a unified-diff card with per-hunk
|
||
syntax highlighting; tool calls that carry a shell/bash payload
|
||
render a terminal card with a scrollback pane. Both fall back to the
|
||
generic collapsible-details view if the `meta` discriminant is
|
||
missing.
|
||
- **Cordis self-modification card.** The three self-referential tools
|
||
(`cordis_mount` / `cordis_unmount` / `cordis_inspect`) — the model
|
||
editing its own live runtime — get a purpose-built card instead of
|
||
raw text. Mount shows the `{id, name, state}` of the new entry plus a
|
||
`+entry` delta (and the awaited services when a plugin is pending);
|
||
unmount shows the removed id with a `-entry` delta; inspect renders
|
||
the returned runtime inventory (services / plugins / tools / dynamic /
|
||
api / events) through the shared collapsible Fields tree. All three
|
||
are parsed from the tool's own plain-text result (they carry a
|
||
`generic` render intent), so the card never fabricates a shape the
|
||
runtime didn't emit — an unrecognised text falls back to raw.
|
||
- **Nested code-dispatch tree.** When Code Mode (`run_code`) fans out to
|
||
sub-tool calls, each sub-call appears as an expandable row inside the
|
||
parent's result box: collapsed it keeps the one-line
|
||
`└─ ✓ name summary` shape; expanded it reveals that sub-call's
|
||
arguments and result, and carries its own `{ }` inspector badge
|
||
anchored to the sub-call event.
|
||
- **Fork & edit-rerun.** Every assistant bubble has a hover-revealed
|
||
**fork from here** button; use it to mint a child session seeded at
|
||
that exact seq, edit the user turn, and let a different reply
|
||
stream in. The child is anchored back in the session tree with a
|
||
`⑂ forks from here (N)` card so the branching is legible.
|
||
- **`{ }` Inspector — zero loss.** Every inspectable element in the
|
||
chat stream — user + assistant bubbles, reasoning blocks, tool calls
|
||
and results, compaction cards, context 📎 injections, subagent
|
||
cards — carries an unobtrusive `{ }` badge that opens one unified
|
||
right-side Inspector with three tabs: **Pretty** (a readable,
|
||
type-specific view of that element), **Raw** (the verbatim
|
||
`session.event` from the session log, with a seq/type/time header +
|
||
copy), and **JSON** (the same event through the recursive collapsible
|
||
Fields tree). The pretty renderer is a projection; the Raw/JSON tabs
|
||
are the source of truth, always one click away.
|
||
- **Compaction visualisation.** When the runtime emits a
|
||
`session/compact` outcome, the compacted range renders as a
|
||
collapsed banner inline in chat with the summary + token delta
|
||
side by side, and the Context page ledger anchors a per-turn marker
|
||
at the same seq. You can always see where the model's memory got
|
||
trimmed and by how much.
|
||
- **MCP surface.** MCP tools are first-class citizens in the plugin
|
||
list, the tool-call renderer, and the plugin catalog — same widget
|
||
chrome, same permission badges, same Vibe hooks. An MCP tool is
|
||
just another plugin the model sees.
|
||
- **Demo-chip convention.** Every fixture-backed surface labels
|
||
itself: a `demo` chip on the nav item for whole-page fixtures, a
|
||
`demo` corner badge on individual mock-minted cards, a
|
||
`mock · <method> not on wire yet` chip on card headers where the
|
||
UX is real but the method is proposed, and a page-header legend on
|
||
mixed pages that names the gap ticket. Fixture vs real is always
|
||
legible.
|
||
- **Real-model default profile.** `stdio-deepseek` (real DeepSeek,
|
||
real tool calls, real approvals) is the default on first launch
|
||
(flipped 2026-07-18) so new downloaders see the actual model on
|
||
the first prompt, not an echo bot. Keyless demoing is one click
|
||
away via the **Switch to keyless demo (stdio-echo)** card that
|
||
renders in place of a red-wall error when `DEEPSEEK_API_KEY` is
|
||
missing.
|
||
|
||
## Profiles
|
||
|
||
The profile dropdown in the sidebar swaps the whole runtime. Three you'll
|
||
actually use, plus two vibe variants:
|
||
|
||
| Profile | What it's for | Needs |
|
||
| --- | --- | --- |
|
||
| **stdio-echo** | Keyless demo, works on master. Direct-spawns the `jsonrpc-demo` bin (present on `deepseek-harness` master) over stdio. Runtime state (plugin list, sandbox mode, compact) is unavailable because there's no daemon to ask, but conversation, tool cards, forks, and devtools all work. This is the one-click fallback offered by the missing-key card. | none |
|
||
| **stdio-deepseek** | Real DeepSeek adapter, real tool calls, real approvals. **Default on first launch** (2026-07-18) — new downloaders should see a real reply, not the echo bot. Use this for actual work, and to demo any live-tool behaviour. | `DEEPSEEK_API_KEY` in the repo's `.env` or your shell |
|
||
| **daemon-echo** | Full-feature demo, keyless. Boots the real daemon over a Unix socket; the model is a mock that echoes your input back. Every renderer capability (daemon plugin list, sandbox toggles, live compact) is demoable. **Not yet available on master** — the `daemon-demo` bin lives in the `.worktrees/integration` worktree of the dev clone until it lands upstream; wait for that or use `DSH_DEV_ROOT` to point at a checkout that has it. | `daemon-demo` bin (integration worktree only) |
|
||
| **daemon-vibe-echo** | daemon-echo + the vibe leaf loaded. The Vibe entry point is gated (mock-echo can't actually compose plugins) but the leaf is there for UI walkthroughs. **Not yet available on master** — same reason as daemon-echo. | `daemon-demo` bin (integration worktree only) |
|
||
| **stdio-vibe-deepseek** | Real Vibe. `cordis_mount` etc. run under the DeepSeek adapter and the model can extend its own runtime. | `DEEPSEEK_API_KEY` |
|
||
|
||
The model dropdown right below the profile dropdown filters itself to
|
||
models the current profile can actually reach: `mock-echo` on the echo
|
||
profiles, `deepseek-v4-flash` / `deepseek-v4-pro` (plus their `[1m]`
|
||
long-context variants) on `stdio-deepseek`. Switching profile also
|
||
resets the model to the profile's default, so you can't accidentally
|
||
end up on a model your current runtime has no adapter for.
|
||
|
||
## What's real vs demo
|
||
|
||
Every surface in the shell is either driven by a live JSON-RPC method
|
||
or filled with an on-disk fixture so the whole capability is walkable
|
||
without a live daemon. Both are useful for different reasons — real
|
||
surfaces show you what a plugin author can actually hook into today,
|
||
fixtures show you the *shape* the runtime will emit once the wire
|
||
lands. The two are visually distinguishable so you can tell at a
|
||
glance:
|
||
|
||
- **`demo` chip on the nav item** — the whole page is fixture-only.
|
||
- **`demo` corner badge on a card** — the card was minted by a
|
||
`mock-*` debug button or a fixture seed, not by a real wire event.
|
||
- **`mock · <method> not on wire yet` chip** on a card header — the
|
||
card family is real UX; the method is proposed but not yet on the
|
||
shipped protocol.
|
||
- **page-header legend chip** (e.g. `wire · file-tier · G<n>` on Hub /
|
||
Runtimes / Bench) — the page mixes real wire and fixture; the chip
|
||
spells out which is which and which gap-ticket (G<n>) tracks the
|
||
missing wire.
|
||
|
||
The current split, on the default `stdio-deepseek` profile (the same
|
||
9-real / 5-fixture set applies to `daemon-echo` — the model
|
||
underneath swaps, the surfaces don't):
|
||
|
||
**Real wire (9 surfaces):**
|
||
|
||
- Chat — `session/prompt` + `session/cancel` + streaming
|
||
`session.event` (assistant chunks, reasoning deltas, tool calls,
|
||
context injections all reach real events).
|
||
- Session Tree — real `session/list` folded into a forest via
|
||
`parentSession` / `seedLength` in each `SessionListEntry.header`.
|
||
- Context page — real `session.event` ledger (inject / recall /
|
||
compact events all wire); per-knob chips call out which G<n>
|
||
writeback is still pending.
|
||
- Tracing page — `session/list` projection (eight-column table over
|
||
real sessions).
|
||
- Plugins → Installed / Browse — `daemon/plugins/list` and
|
||
`plugins:add` are real wire; the curated Browse index is a fixture
|
||
clearly labelled `Curated demo index`.
|
||
- Runtimes — `daemon/plugins/list` is real; the compose recipe view
|
||
is fixture-tier with a `composed locally · gap G8` chip.
|
||
- PRs — real `gh pr list` in the profile's `cwd`; the fallback demo
|
||
dataset is only used when `gh` is missing / signed out, and shows a
|
||
visible `demo data` badge.
|
||
- Devtools — every `session.event` the runtime shipped shows up here,
|
||
live.
|
||
- Settings — profile switch, `DEEPSEEK_API_KEY` presence check, and
|
||
the local pricing table are all real (pricing lives in the shell,
|
||
not the daemon).
|
||
|
||
**Fixture-only (5 surfaces):**
|
||
|
||
- **Playground** — `demo` chip on the nav item. The scratch runtime
|
||
boot is real, but the entry point currently shims to the Plugins
|
||
tab; the standalone Playground page hasn't landed.
|
||
- **Hub** — `demo` chip on the nav item + page-footer SDK legend.
|
||
Plugin listings are real wire; the surrounding catalogue metadata
|
||
(categories, descriptions, ratings) is fixture pending G1 / G11 /
|
||
G12.
|
||
- **Bench** — `demo` chip on the nav item + page-header legend
|
||
(`demo · G18/G19/G20 pending`). The benchmark list and detail views
|
||
are fixtures until the wire methods land.
|
||
- **Rubrics** — `demo` chip on the nav item + page-header legend
|
||
(`demo · G1 seam pending`). Drafts saved from "Create from scratch"
|
||
live in memory only.
|
||
- **Missions empty-state preview** — the three-card ghost kanban
|
||
("Draft the release notes", "Refactor the compact seam", "Land the
|
||
artifact preview PR") is a `preview` fixture; real todos replace it
|
||
as soon as any session writes a `todo/write`.
|
||
|
||
The full audit — every card, every wire method, every gap ticket
|
||
(G1..G21) — lives in `docs/review-demo-labels.md`.
|
||
|
||
## Known limitations
|
||
|
||
The demo is deliberately narrow in a few places — either because a wire
|
||
method is still landing, or because the runtime capability doesn't exist
|
||
in the target profile.
|
||
|
||
- **stdio profiles don't have runtime-state features.** No plugin
|
||
list, no live sandbox-mode badge, no `session/compact` button — those
|
||
read/write daemon state that only the daemon profiles expose.
|
||
- **`session/compact` is proposed, not shipped.** The **Compact now**
|
||
button works if the daemon has a `compact-basic` (or equivalent) leaf
|
||
mounted; on MethodNotFound the button greys out with an explanatory
|
||
tooltip. No mock compaction is fabricated.
|
||
- **`session/fork` is still landing.** The **fork from here** button
|
||
and the tree smoke scenario fall back to a synthetic `session/new`
|
||
and badge the child `(mock)` so callers know it's an empty child, not
|
||
seeded.
|
||
- **Old sessions can't be resumed after `kill -9`.** The daemon's SDK
|
||
server owns a per-connection sessionId map. Respawn works and new
|
||
sessions work; resuming a session that predates the crash does not.
|
||
- **Plugin toggle is full-daemon-respawn.** The **Apply + restart**
|
||
button tears the daemon down and brings it back up. Per-plugin
|
||
dispose + re-mount is on the runtime side's list (`daemon/plugins/toggle`,
|
||
`daemon/plugins/reload`).
|
||
- **Mission Control has no persistence.** A page reload wipes the
|
||
aggregate and reseeds from the next `session/list` refresh — the
|
||
view is a live overlay, not a store.
|
||
- **Artifact preview: inline peek in-stream, full view in the browser.**
|
||
Each artifact card can expand a low-key inline preview — Markdown
|
||
rendered read-only by a tiny dependency-free renderer (`md-mini.js`,
|
||
text-node-only, so any raw HTML inside the doc stays literal), and
|
||
`.html` framed in a `sandbox="allow-scripts"` iframe pointed at the
|
||
existing `127.0.0.1` server. There is still **no embedded webview for
|
||
full pages**: the inline frame is a fixed-height peek, and "Open in
|
||
browser" remains the path for the real, full-size artifact. Previews
|
||
are collapsed by default and lazy (content builds on first expand);
|
||
when the artifact server isn't up the `.html` expand falls back to the
|
||
open-in-browser action instead of a broken frame.
|
||
- **Growth reads jsonl + `session/list`.** A follow-up will migrate to
|
||
the `session/list` + `session/events` aggregation so events that
|
||
never touch the overlay (pure chat activity) also show up.
|
||
- **Widget cards are demoable via the header mocks.** No live tool on
|
||
any shipped profile emits `card: 'widget'` yet; the three
|
||
`mock: widget · …` buttons inject a synthetic `tool/call` +
|
||
`tool/result` pair so the pipeline is exercised end to end.
|
||
|
||
---
|
||
|
||
## For developers
|
||
|
||
Everything below this line is implementation notes. If you're just using
|
||
the demo, you're done above.
|
||
|
||
### Architecture
|
||
|
||
Topology mirrors ChatGPT.app: an Electron main process spawns the DSH
|
||
**daemon** (long-lived host process) and connects a Unix-domain socket
|
||
for JSON-RPC frames; the renderer only renders. A `stdio-*` fallback
|
||
is kept for airgapped smoke tests and for demoing without a daemon
|
||
build.
|
||
|
||
```
|
||
+-----------------+ IPC +------------------+ unix-socket JSON-RPC v2 +-----------------+
|
||
| renderer | <--------------> | Electron main | <---------------------------> | DSH daemon |
|
||
| (vanilla JS) | preload API | (RuntimeSuper + | newline-delimited frames | (daemon-demo) |
|
||
+-----------------+ | DaemonSuper — | +-----------------+
|
||
| respawn/probe) |
|
||
+------------------+
|
||
Fallback (stdio-echo / stdio-deepseek):
|
||
direct-spawn jsonrpc-demo bin, stdio frames
|
||
```
|
||
|
||
### Structure
|
||
|
||
```
|
||
src/
|
||
main/
|
||
jsonrpc-client.js pure JSON-RPC 2.0 framing (unit-tested)
|
||
transport.js StdioTransport + SocketTransport
|
||
runtime.js supervisor: transport + client + crash re-spawn + initialize
|
||
daemon.js probe+spawn+respawn for the daemon profile
|
||
profiles.js which cordis.yml + model to launch
|
||
gh-prs.js gh CLI wrapper (pure)
|
||
artifact-server.js 127.0.0.1 static server + SSE live-reload
|
||
main.js BrowserWindow, IPC surface
|
||
preload/preload.js contextIsolation bridge
|
||
renderer/
|
||
index.html two-pane layout
|
||
style.css vanilla, no framework
|
||
renderer.js session-event → DOM dispatch
|
||
(~30 IIFE modules — mission, plugins, growth, playground, quick-chat, devtools, …)
|
||
test/
|
||
*.test.js 439 unit tests (node --test, no Electron)
|
||
smoke-runtime.js headless smoke scenarios (stdio / daemon / kill / tree)
|
||
```
|
||
|
||
### Protocol surface used
|
||
|
||
From `packages/ui/jsonrpc/src/protocol.ts` (v2):
|
||
|
||
- `initialize({ cwd, model, protocolVersion: 2, capabilities: {interruptions:true} })`
|
||
→ `{ serverInfo, protocolVersion, capabilities }`
|
||
- `session/new({ sessionId })` → `{ sessionId }`
|
||
- `session/prompt({ sessionId, contentBlocks })` → `{ accepted: true }`
|
||
- `session/cancel({ sessionId, reason })` → `{ cancelled }`
|
||
- `session/list({})` → `{ sessions: [{ sessionId, header, live, persisted }] }`
|
||
- `session/events({ sessionId })` → metadata list;
|
||
`session/events({ sessionId, seq, before, after })` → full-event window
|
||
- `shutdown` → `{}`
|
||
|
||
Server→client requests:
|
||
- `session/interrupt({ sessionId, interruptId, payload | spec })` →
|
||
`{ outcome: 'accepted', payload } | { outcome: 'rejected' } | { outcome: 'cancelled' }`
|
||
|
||
Notifications: `session.event`, `session.finished`, `subagent.started`,
|
||
`subagent.finished`.
|
||
|
||
Daemon-only: `daemon/ping` → `{ name, version, pid, startedAt }`.
|
||
|
||
Proposed (not yet in shipped protocol; graceful fallback in the shell):
|
||
`session/fork`, `session/compact`, `daemon/plugins/list`,
|
||
`daemon/plugins/toggle`, `daemon/plugins/reload`,
|
||
`daemon/persona/get`.
|
||
|
||
### Tests
|
||
|
||
```sh
|
||
pnpm test # 439 unit tests (node --test, no Electron)
|
||
node test/smoke-runtime.js stdio # stdio-echo end-to-end
|
||
node test/smoke-runtime.js daemon # daemon-echo + session/list + prompt
|
||
node test/smoke-runtime.js kill # SIGKILL daemon → auto-respawn + reconnect
|
||
node test/smoke-runtime.js tree # session tree + fork lineage (mock fallback)
|
||
node test/smoke-runtime.js all # everything (~90s)
|
||
```
|
||
|
||
### Manual verification recipes
|
||
|
||
**Kill-recovery.** In another shell, `pkill -9 -f daemon-demo/src/bin.ts`.
|
||
Status bar flips `running → crashed → respawning → running` on schedule
|
||
`[0,300,1000,2500,5000]ms`. Send a new prompt — new session works;
|
||
old sessions can't be resumed (see Known Limitations).
|
||
|
||
**Approval card.** Header debug button `mock-approval` renders an inline
|
||
card wired to the real interrupt-resolution path. In a coding-agent
|
||
profile with a real bash tool call, a live `session/interrupt` request
|
||
from the daemon hits the same handler and gets an `accepted` /
|
||
`rejected` / `cancelled` response over the JSON-RPC id.
|
||
|
||
**Plugins toggle.** `pnpm start`, click **Plugins**, uncheck `bash`,
|
||
click **Apply + restart**, wait for status dot green, reopen the tab —
|
||
`bash` shows dim / unchecked / still source=`base`.
|
||
|
||
**Onboarding reset.** `rm -rf ~/.dsh-desktop && pnpm start` — overlay
|
||
covers the app, pick role + approval mode; overlay dismisses and
|
||
runtime restarts once. Or use sidebar → **Reset onboarding**.
|
||
|
||
**Vibe.** Ensure your DSH runtime `.env` has `DEEPSEEK_API_KEY` (see
|
||
Quick Start for `.env` locations). Sidebar profile → `stdio-vibe-deepseek`.
|
||
Plugins → **Vibe a plugin**. A new session opens; ask it to mount a
|
||
plugin and watch a `cordis_mount` tool block appear.
|
||
|
||
### Wire needs owed by the protocol team
|
||
|
||
- `session/fork` (see Known Limitations).
|
||
- `session/compact` (see Known Limitations).
|
||
- `daemon/plugins/list` / `daemon/plugins/toggle` /
|
||
`daemon/plugins/reload` (per-plugin dispose + re-mount).
|
||
- `daemon/persona/get` + role templates on the daemon side; today the
|
||
role choice from onboarding lives in `~/.dsh-desktop/config.json` on
|
||
the shell.
|
||
- `SessionListEntry.header.seedLength` populated end to end so fork
|
||
markers can anchor at the exact seq rather than the current tail.
|
||
|
||
### Adaptive layout, widget channel, artifact preview
|
||
|
||
Three cross-cutting affordances documented in detail in the design docs
|
||
under `docs/`:
|
||
|
||
- **Adaptive layout** — four heuristic buckets (chat / code-review /
|
||
artifact / monitor) auto-toggle a body class and a handful of CSS
|
||
variables based on the recent event mix. Manual lock available; see
|
||
`src/renderer/layout-heuristics.js` and `layout-controller.js`.
|
||
- **Widget channel** — inline interactive cards that ride on the
|
||
existing `tool/result` event's `meta` field. See
|
||
`docs/widget-channel-design.md` for the wire shape, plus
|
||
`src/renderer/widgets.js` and the three header mocks.
|
||
- **Artifact preview** — `127.0.0.1` static server + SSE live-reload
|
||
that watches `~/Library/Application Support/dsh-desktop-demo/.artifacts/`
|
||
(override with `DSH_ARTIFACT_DIR=…`). Cards carry an inline peek —
|
||
`.md` rendered read-only by `src/renderer/md-mini.js`, `.html` framed
|
||
in a `sandbox="allow-scripts"` iframe — while the full view still opens
|
||
in your default browser (no embedded webview for full pages).
|
||
Tool-driven and debug-mock paths both exercised. See
|
||
`src/main/artifact-server.js` and `src/renderer/md-mini.js`.
|
||
|
||
### Design docs
|
||
|
||
Deeper background lives under `docs/`: `arch-review-report.md`,
|
||
`capability-ui-coverage.md`, `product-flow-review.md`,
|
||
`product-ia-design.md`, `qa-walkthrough-report.md`,
|
||
`ui-refs-distilled.md`, `widget-channel-design.md`.
|