Bundle
dsh-approval-explain
Explain what a pending dsh approval will actually do, with one LLM call: an Explain button beside Allow/Reject plus the Host /explain command behind it
- Source
- Martlet-Tech
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-approval-explain
[中文](README.zh.md) | English
Adds an **Explain** button to [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) approval cards: one click spends one LLM call telling you what the thing you are about to approve *actually does*.

```
┌─ ● Waiting for approval ────────────────────────────┐
│ │
│ escalate sandbox to danger-full-access: user … │
│ Set-Content -LiteralPath 'D:\Downloads\notes.md' │
│ …(a very long script body) │
│ │
│ [ 💡Explain ] [ Reject ] [ Allow ] │
│ │
│ What: creates (or overwrites) notes.md under … │
│ Touches: writes D:\Downloads\notes.md; deletes … │
│ Safety: caution — overwrites a same-named file … │
└────────────────────────────────────────────────────┘
```
## Why you need it
The approval card has a "detail" area that shows what you are approving. It is filled by an extractor that **only recognizes a `command` field**:
```js
// packages/client/ui-chat/src/client/chat/ApprovalCommand.tsx
return typeof args.command === 'string' ? args.command : undefined
```
So only tools whose argument happens to be named `command` — `bash`, `pwsh` — can put anything in that area. For tools like `write` / `edit`, whose arguments are `file_path` + `content`, **that area is always empty**.
The result: a card saying "escalate sandbox to danger-full-access" while you cannot see what you are actually approving.
This plugin does two things:
1. **Never leaves it blank** — it takes over the detail area and supports all three argument shapes: `command` / `file_path` / `path`;
2. **Lets the model read it for you** — the Explain button makes one LLM call and returns a fixed three-line verdict: What / Touches / Safety.
The Explain button reuses the native button geometry (`outline`, 36px capsule) so it sits flush with Reject / Allow once, tinted with the DeepSeek family colors (`--dsw-static-deepseek-50` / `-500`) and prefixed with a 💡 icon.
## Install
Requires **pnpm** (`dsh plugin` is a forwarding shell for pnpm). Enable it if you have not:
```sh
corepack enable pnpm # Node ships corepack; recommended
# or
npm i -g pnpm
```
Then:
```sh
dsh plugin --profile web add github:Martlet-Tech/dsh-approval-explain
```
**Restart** dsh / DShell after installing — a profile reads its bundle list only at startup.
### If it does not take effect
`dsh plugin add` puts the package into the profile's `node_modules` and, because this package declares `dsh.bundle`, appends it to `dsh.profile.bundles`. If that append does not happen (a pnpm version difference, for example), add the line by hand:
```jsonc
// $DSH_HOME/profiles/web/package.json
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-approval-explain" // ← add this line
]
}
}
```
Verify with `dsh --profile web --dump-config`: the output should contain a `# == dsh-approval-explain` layer.
### Why no build authorization is needed
This package **ships its build output directly** (`lib/` is committed), so it runs no `prepare` script and never triggers the pnpm ≥10 `allowBuilds` prompt for git dependencies.
### Uninstall
```sh
dsh plugin --profile web remove dsh-approval-explain
```
The plugin is **additive**: it takes over the approval card by priority and **disables** none of dsh's Loader rows. Uninstalling it, or having it fail to load, restores dsh's original behavior completely.
`/explain <content>` also works directly in the composer, without the button.
## Requirements
- dsh `0.1.5-rc.1` or newer
- A configured model provider (it reuses your current session's provider/model; no extra configuration)
- Web GUI only (`dsh web`)
## Finding other plugins
dsh **has no plugin store** — no official marketplace, no remote registry, and the GUI cannot browse or install. Discovery happens through GitHub topics:
- [`github.com/topics/dsh-plugin`](https://github.com/topics/dsh-plugin) — the only channel the official README recommends
- [`awesome-dsh-plugin`](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) — a community-curated list
Installs always go through `dsh plugin --profile <name> add <spec>`, where `<spec>` may be an npm package name, `github:user/repo`, a tarball, or a local path.
## How it works
The whole path uses mechanisms dsh already has; it adds no remote endpoint:
```
Browser "Explain" button
└─ ctx.remote.commands.execute(sessionId, '/explain <content>', [])
└─ host-side /explain command handler ← runs on the Host, can reach ctx.llm
└─ ctx.llm.stream({ provider, model, messages, system })
└─ text deltas accumulate → { kind: 'success', text }
└─ the result returns synchronously and expands in place
```
Two design decisions are worth explaining:
**Why a "human command" instead of a custom remote API.** `ctx.llm` is a host-side service with no `@Remote`, so the browser cannot reach it. A custom `@Remote` endpoint would need Typert code generation, which only scans `packages/` inside the dsh repository. `ctx.remote.commands.execute()` is already mounted and its command handlers run host-side — the only ready-made channel that needs no changes to dsh. `/compact` is the existing precedent for this path.
**Why the whole file has zero imports.** The plugin loads as an out-of-tree package through the profile's junction; Node walks up from the junction's real path looking for `node_modules`, and there is no `@deepseek-ai/*` there. So the host half gets its service via `ctx.get('llm')` and builds messages literally against the `Message` shape, importing no package at all.
## Known limitations
- **Every click leaves a trace**: the command system records `command/run` + `command/done` lines in the session log. That is auditability, not zero footprint.
- **Explaining costs tokens**: a small request capped at roughly 700 output tokens. dsh has no approval gate on LLM calls, so this step has no second confirmation.
- **It takes over the whole approval card**: the "Reject / Allow once" row is rendered by dsh itself and exposes no slot, so putting a button on that row means taking over the card. If dsh changes the card structure in the future, this plugin will need to follow.
- **It is coupled to `StreamChunk` field names**: `text-delta` / `block-end` / `finish` are the current structure; zero dependencies means tracking the version.
- For now it only **explains**. It does not change the approval outcome and does not cache verdicts.
## License
MIT
Install
dsh plugin --profile web add github:Martlet-Tech/dsh-approval-explain
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-approval-explain from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.