Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bd62b42
sync: upstream dsh-v0.1.2-alpha.4 (initial merge)
whitelonng Sep 1, 2026
5cc1164
chore(sync): adapt product integration to alpha.4 (phase 1)
whitelonng Sep 1, 2026
ce6d6f9
chore(sync): alpha.4 adaptation phase 2 — host face green, client fac…
whitelonng Sep 1, 2026
0fd30ce
chore(sync): finish alpha.4 adaptation — both faces green, catalogs r…
whitelonng Sep 1, 2026
7d734a0
chore(sync): apply lint autofixes to adapted test files
whitelonng Sep 1, 2026
ca6573d
fix(sync): align inherited workflows with fork policy
whitelonng Sep 1, 2026
f0ff99d
fix(sync): migrate product tests to the alpha.4 client stack
whitelonng Sep 2, 2026
8119e84
fix(sync): drop duplicate storage rows from the web-app insert and ad…
whitelonng Sep 2, 2026
a42985d
fix(sync): close coverage gaps and clear the remaining lint errors
whitelonng Sep 2, 2026
b19b4a4
fix(sync): re-pin fork lint fingerprints and realign trigger arbitrat…
whitelonng Sep 2, 2026
a631232
fix(sync): restore English locale strings drained by the merge
whitelonng Sep 2, 2026
a09d9b0
fix(sync): refresh fork web goldens after locale restoration
whitelonng Sep 2, 2026
4ca3462
fix(sync): repair documentation gates after upstream merge
whitelonng Sep 2, 2026
37a36af
fix(sync): align workspace constraints and keep fork-only locale empties
whitelonng Sep 2, 2026
583c715
fix(sync): drop empty invariant companions and prune their wiring
whitelonng Sep 2, 2026
90e36b3
chore(sync): re-record translation pairing after invariant README edits
whitelonng Sep 2, 2026
782b913
chore(sync): regenerate module graph after invariant wiring changes
whitelonng Sep 2, 2026
1d8ac32
fix(sync): restore desktop runtime closure gate and its preset peers
whitelonng Sep 2, 2026
1bcb16f
fix(sync): widen the inspector console broadcast wait on CI
whitelonng Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
2026-07-19-gui-layering-and-rpc-protocol.md: b27d8d024612d890819bfca9b43c0c81464dfdd3
2026-07-19-gui-layering-and-rpc-protocol.zh.md: 3cf4ba6421c7332c1f8cebb61656a1546f3ad45f
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Agent Note: GUI layering and the RPC protocol — host/client layering by capability provider, the four-quadrant message model, and the fetch carrier

Status: implemented
Archived: 2026-08-27

English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)

> Division of labor: this document = the layering model + the channel-independent RPC protocol; the protocol's Web implementation combines HTTP uplink with the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md), the browser object layer is in the [web client architecture note](2026-07-19-gui-web-client-architecture.md), and DSHCode delivery is in the [Electron desktop shell note](2026-08-13-electron-desktop-loopback-shell.md).
> Division of labor: this document = the layering model + the channel-independent RPC protocol; the protocol's Web implementation combines HTTP uplink with the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md), while the browser object layer is in the [web client architecture note](2026-07-19-gui-web-client-architecture.md).

## Problem

Expand All @@ -15,7 +16,7 @@ We need a UI integration layer. Beyond the existing ACP/stdio baseline, more pro

That demands a stable layered responsibility model in the engineering codebase, so future clients plug in cleanly.

At the same time the physical channels differ per consumer (browser HTTP/WebSocket, in-process fetch/SSE, and a possible future IPC carrier), so we also need a channel-independent message model and a single contract source of truth — "adding a method" and "swapping a carrier" must not entangle each other, and every message on the wire must be type-validatable, observable, and reconcilable.
At the same time the physical channels differ per consumer (browser HTTP/WebSocket, in-process fetch/SSE, IPC later), so we also need a channel-independent message model and a single contract source of truth — "adding a method" and "swapping a carrier" must not entangle each other, and every message on the wire must be type-validatable, observable, and reconcilable.

## Decision

Expand All @@ -26,20 +27,20 @@ Directories layer as follows:
- `packages/host/*`: packages provide host-side capability only (representing the Node.js engineering core built on the existing harness plugin system), and additionally
- the unified backend protocol (fetch, HTTP, streaming interfaces…) — definitions and support, see the "Message protocol" sections below
- `packages/client/*`: packages provide client-side capability only; every package stays single-sided. Three kinds live here (the axes are owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md)):
- **Pure libraries** (`ui-slots`, `web-react`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the first three are seeded into the module table.
- **Pure libraries** (`ui-slots`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the two client libraries are seeded into the module table.
- **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dsh.client` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
- `apps/web` (`dsh-web-frontend`) is the vite application: a thin `main.ts` over the shell API exported by `dsh-client-web`.
- `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-web-frontend` dist; `dsh --profile headless` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
- `apps/desktop` (`@dshcode/desktop`) boots the same Web profile in the Electron main process and displays it through the existing loopback HTTP/WebSocket carrier; an IPC carrier remains an unimplemented alternative.
- A future Electron application reuses the same web client packages over an IPC fetch carrier.

```
apps/* (applications: apps/web = vite, apps/cli = bin, apps/desktop = Electron)
apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
│ consume
packages/host/* packages/client/*
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
apiproxy front layer: protocol pure libs: ui-slots / ui-primitives
runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
webserver Web HTTP carriage client half = src/client/)
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
Expand All @@ -64,22 +65,22 @@ On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Nod
|---|---|---|---|
| Front layer | `dsh-host-apiproxy` | TS/zod definitions (api/) + the fetch abstraction (fetch/: handler + client base class) | Keep it simple — every consumer needs it; importable from Node and browser alike; protocol content in the "Message protocol" sections below; clients must not bypass api through ctx |
| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dsh.client packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
| Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Browser carriage, including the DSHCode BrowserWindow; zero workspace dependencies (the registry arrives by structural injection) |
| Client libraries | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | Slot registry core / ctx↔React glue / pure React atoms | Zero cordis runtime dependency in components; seeded into the loader module table by the shell |
| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | Browser-side cordis plugin tree (wire consumer, core services, theme, i18n, layout, sidebar, conversation, trajectory) — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); the consumption face goes exclusively through ApiProxy |
| Application | `@deepseek-ai/dsh` (apps/cli) + `dsh-web-frontend` (apps/web) + `@dshcode/desktop` (apps/desktop) | Coarse bin dispatch, the Vite browser entry, and the Electron delivery shell over the shared Web profile | Applications do not load each other's launch surfaces; workspace and packaging knowledge stays in the owning app |
| Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it |
| Client libraries | `dsh-client-ui-slots` / `dsh-client-ui-primitives` | Slot contracts / pure React atoms | Seeded into the loader module table by the shell |
| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-ui-renderer` / feature UI packages | Browser-side Cordis plugin tree: wire consumer, core services, theme, React rendering, and feature composition — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); cross-plugin value cooperation uses services and slots |
| Application | `@deepseek-ai/dsh` (apps/cli) + `dsh-web-frontend` (apps/web, the vite application) | Coarse bin dispatch + one assembly module per application (web.ts / headless.ts); the vite app is a thin main over the `dsh-client-web` shell surface | Applications use dynamic imports so they never load each other; workspace knowledge like dist location stays in the app |

#### Naming rule

Packages under `packages/host/*` and `packages/client/*` **must carry the directory-group prefix in the package name**: host/runtime → `dsh-host-runtime`, client/runtime → `dsh-client-runtime`. The directory name does not repeat the group prefix (host/ already expresses it). The package-name tail therefore ≠ the directory name, so the `dsh-*` wildcard in tsconfig.base.json (which resolves by directory name) misses them — **each package in these two groups needs an explicit paths entry**, including separate entries for the client packages' `/client` subpaths so source-level resolution matches the exports map.

#### How to integrate a new application (operational checklist)

1. **Pick a fetch impersonation**: browser same-origin HTTP / in-process `host.handler.fetch` injection / your own transport-aspect subclass (e.g. a future Electron IPC carrier, see the "Subclass table" below).
1. **Pick a fetch impersonation**: browser same-origin HTTP / in-process `host.handler.fetch` injection / your own transport-aspect subclass (e.g. future Electron IPC, see the "Subclass table" below).
2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the application's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports.

The shipped applications preserve the division: the Web profile mounts Host, carrier, and browser composition; DSHCode embeds that same profile behind an ephemeral loopback port; and `dsh --profile headless` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh --profile headless` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.

## Message protocol

Expand Down Expand Up @@ -209,7 +210,7 @@ The same domain tree as `ApiProxy`, but unary methods **take the business payloa

### The instance-level envelope observation aspect

All four quadrant full forms pass through `onEnvelope`; the base implementation is an **instance-owned microtask-batched buffer** (frame storms must not disturb consumers per frame; module-level state would leak across instances/tests, hence instance-owned). Observers subscribe via `subscribeEnvelopes(listener)` (receiving whole batches as `readonly RpcMessage[]`, returning an unsubscribe function); a listener throw is isolated (observation must never bite the carrier). With no subscribers the buffering costs nothing. No shipped consumer subscribes today — the aspect is the designated seat for wire diagnostics (the retired RPC debug panel was its first consumer, and a future one plugs in without touching the carrier).
All four quadrant full forms pass through `onEnvelope`; the base implementation is an **instance-owned microtask-batched buffer** (frame storms must not disturb consumers per frame; module-level state would leak across instances/tests, hence instance-owned). Observers subscribe via `subscribeEnvelopes(listener)` (receiving whole batches as `readonly RpcMessage[]`, returning an unsubscribe function); a listener throw is isolated (observation must never bite the carrier). With no subscribers the buffering costs nothing. No shipped consumer subscribes — the aspect is the designated seat for wire diagnostics (the retired RPC debug panel was its first consumer, and a future one plugs in without touching the carrier).

### The subclass table (transport carriage)

Expand All @@ -218,7 +219,7 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh --profile headless` drives core directly |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser client; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) |
| IPC bridge subclass (hypothetical example — no IPC implementation exists) | an Electron shell | IPC serialization round trip | would swap only doFetch; contract and base class unchanged |
| IPC bridge subclass (hypothetical example — no such shell exists) | an Electron shell | IPC serialization round trip | would swap only doFetch; contract and base class unchanged |

## How to extend (operational checklists)

Expand All @@ -240,7 +241,7 @@ Every client consumes one contract: adding a unary method is a five-step mechani

| Rejected | One-line reason |
|---|---|
| Packaging capabilities by product (a web family, an electron family) | Products share host/client capabilities rather than an application implementation; DSHCode adds one owning application package but no duplicate capability packages |
| Packaging by product (a web family, an electron family) | Products share host/client capabilities rather than an application implementation; capability-provider layering means a new application needs zero new packages |
| A package per mixture (e.g. a standalone headless package) | A mixture has exactly one consumer (its own app); packaging it is ownerless abstraction, while assembly in the app is readable and disposable |
| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Clients require wire validation, observability, and multi-client consistency. Direct headless is a local entry point with no client boundary and uses the public Agent/Session seams rather than a client command plane |
| webserver depending on runtime (saving the handler injection) | Structural-typing injection keeps webserver reusable by sidecars/tests with zero workspace deps; a package dependency would drag assembly knowledge into the carrier layer |
Expand Down
Loading
Loading