# @deepseek-ai/dsh-client-ui-cordis

English | [中文](README.zh.md)

Cordis dynamic-plugin surfaces, browser half: a frame-wide panel that operates every definition the host holds, and a read-only `cordis_define` card that records what a session defined.

**The panel is global on purpose.** A model-driven `cordis_run` blocks host-side on a `cordis/request-run` round trip whose answer is a person pressing approve, and it can name a definition belonging to a session nobody is looking at — an approval reachable only inside that session's transcript would be unreachable exactly when it blocks the model. So the answering surface is a `shell.overlay` entry (a generic frame-wide floating seat this package contributed to `ui-layout`, not a React root of its own): a badge counting what runs plus what waits, opening a list of every definition with its run controls. The list is never filtered by session for the same reason; the selected session's rows are grouped first and everyone else's stay listed below. Rows come from the host's global `inventory` call, re-read rather than patched whenever an announcement changes what exists, because the announcements carry no labels. An open request whose definition that read does not cover still gets a row, rendered from the run activity's own session, label, reason and request identity: `cordis_define` broadcasts nothing, so a package defined after the last read is unknown to the page while its request already blocks the model, and a badge counting an answer the list cannot show would strand it. Such a request also triggers one registry read, which brings the row its run controls. Grouping reads one resolved field per row — the live run's session when there is one, the registry row's otherwise — rather than a different store per phase; both activity arms carry it, so answering a request cannot make its row leave the group it was answered in. Within a group the rows blocking a model come first.

**The card is a record.** It shows the name and purpose the model wrote, the source it wrote, and whether that definition is running — no switch, no approval, and a pointer to the panel. Its material comes only from the frozen call/result slice (labels from `argsRaw`, the host-minted id from the result's `meta.id`), so replay renders the same card, and one reading is the card's own to own: a successful `cordis_undefine` in this session's log is durable and terminal, and outranks the wire, where a retract announcement looks identical to a plain stop.

**A row reads two independent facts.** What the host runs comes from the inventory; what THIS page has loaded comes from the runner's live set. They diverge on every reload: the host keeps running everything while a fresh page holds nothing. A row the host runs and this page holds offers the global stop; a row the host runs and this page does NOT hold offers the load back first and that stop second — separate controls, because loading the browser half here and stopping the definition for every page are different acts. A host-only definition is exempt from all of it: with no browser half to load, "this page does not hold it" is simply what it always looks like, so its row reads plainly running and offers the stop alone. Idle, it offers a run labelled as just that — nothing is loaded here, so the control does not say it is — and the run request carries `hasClientHalf` to the runner, which is what makes it bring up the host half and fetch no browser half. Collapsing the two facts into the host's `running` alone left a reloaded page with nothing but the global stop, so the documented reload recovery (pull the inventory, load the definition back into this page) had no control to go through.

**A crash after a successful load still belongs on the row.** A browser half can load, answer `cordis_run` with ok, and only then throw when React renders it — the teaching error for a mistake like reaching for `setInterval` reaches the browser console, which neither the model nor the person watching the panel reads. So the row carries the runner's last render failure for this page inline, in the same slot as a load failure: one is "it never loaded", the other "it loaded and then threw", and a row can honestly show both. The line names the seat that crashed and answers the question that follows from it — whether anything of theirs is still on screen: a shadowing seat retires the crashed entry and the shipped UI comes back, while a chain entry keeps its place and its UI may simply be incomplete.

Neither surface keeps run state in component state — settling a define call moves its card in the chat flow, which remounts it. Facts live in observables owned by whoever can close them: the browser-side runner owns open requests, orchestration outcomes (it resolves them, including when another page answers first), this page's live set and its render failures — it is the only party watching `slots.onEntryError` and the only one that can map a crashed entry back to the package that registered it — and this package owns the inventory it read and the announcements it folded.

The `/client` export surface is the plugin body (`apply`/`inject`) plus the injected face, run-state, port and event payload types.

## Model Experience

Indirectly, through the run and stop verbs these surfaces drive — the browser-side runner's orchestration for a run, and `dynamicCordisRunner.stop` for a stop, the same host verbs the model's `cordis_run` and `cordis_stop` tools reach — so whatever a running definition then contributes is the runner's effect while nothing model-visible originates in this package, which renders logged call/result slices and a host inventory read, adds no prompt content, writes no session event, and deliberately leaves no session-log trace of a person approving, declining, running or stopping anything.

#### KV Cache effect

None: no prompt input originates here, and answering a run request neither extends nor rewrites the history tail.

## Known Limitations and Deferred Work

- **An open panel does not see registry changes that announce nothing** — `cordis_define`, and an undefine of a definition that was not running, change the registry without a dispatch announcement, so a panel left open across one of them keeps its rows until it is closed and opened again (opening re-reads). A run request is the exception, because it blocks the model: it both renders its own row and triggers a read. Acting on a row the host no longer holds is still honest: the call answers `definition-missing` and the row goes terminal. Polling while open was considered and rejected as the wrong price for it.
- **A request-only row is answerable but not operable** — it offers approve/decline only, because the run/stop switch needs the registry row the read has yet to deliver. Grouping and copy are unaffected (the ask carries the session, label and reason).
- **A row disappears for the width of one read if its orchestration outruns it** — the activity's orchestrating arm carries the session but no label, deliberately: a user-initiated run has no ask to take one from, and naming the row after its id would be worse than briefly omitting it. So an approved request whose registry read has not landed leaves no row until it does. In practice the read is triggered when the request arrives, so it has almost always landed by the time anyone answers.
- **A render failure is this page's own reading, and it arrives too late for the run receipt** — the panel shows the last crash the runner saw HERE, so a package that renders fine in this tab shows nothing even while it crashes in another. It also cannot appear in `cordis_run`'s answer: rendering happens after the run settles, so the model learns about it by asking (`cordis_inspect what:"temporary"`) rather than from the call it already made.
- **A second page's load failure is invisible to the others** — the host settles a dispatch on the first load report and records later ones without acting, so a page whose browser half failed after another page acknowledged keeps reading as running. That page sees the reason on its own row (the runner reports it); the others cannot.
- **Any page may answer any request** — approvals are frame-wide by design, so a person in one browser tab can approve a run the model asked for while another tab is in front of the defining session. First answer wins and the rest converge; narrowing who may answer is deferred.
- **A card whose call head left the event window loses its labels** — the card derives name and purpose from the call arguments, so a session long enough to truncate them leaves it naming its call id. The panel is unaffected: the host inventory carries the labels.
- **Window truncation degrades the unloaded reading** — the card calls a definition unloaded from a successful `cordis_undefine` in this session's log; a session long enough to push that result out of the window shows the definition as merely not running.
