dsh-new-session-cards
中文 | English
A standalone plugin that turns the new-session screen of DSH (DeepSeek Harness) Web GUI into a four-step card wizard — so you can never silently start a task with the default task mode or model.
Zero modification to DSH itself: ships as an independent bundle — dsh plugin add to install, remove to uninstall.
---
Why DSH is fun
The whole DSH product is a tree of Cordis plugins — there is no core to "operate on"; everything can be replaced from the outside:
- Composer takeover chain: the new-session input area is a selector-routed chain. Any plugin can take it over — turning it into a full-screen UI (that is exactly what this plugin does) — and then step aside so the original input bar comes back automatically.
- Slot system: sidebar, settings pages, session headers, message rows… all expose injectable slots. Plugins snap onto them like building blocks.
- Theme tokens: the
--dsw-alias-*semantic palette — plugin styles use zero literal colors and adapt to light/dark automatically. - Bundle distribution: two manifests (
dsh.bundle+dsh.client), installable from a local directory, a tarball, or directly from GitHub. - Plugin is code: a client plugin is plain JS/TSX bundled into a browser bundle, loaded under the exact same contract as the built-in plugins.
You do not need to fork DSH to deeply reshape its UI. This plugin is a complete worked example: take over → configure → step aside.
What this plugin does
The original new-session screen (workspace chip, agent-preset chip, small model selector in the corner of the input card) all fall back to deployment defaults silently, so users easily skip configuration and send. This plugin replaces the new-session screen with:
1. Step 1 — Task mode: agent-preset card grid (single-select); the default preset carries a «默认/Default» badge 2. Step 2 — Model: model card grid (single-select, provider tag); the default model carries a «默认/Default» badge 3. Step 3 — Other settings: workspace (full width) + access mode / plan mode (side by side) 4. Step 4 — Message: after «确认配置并开始/Confirm and start», an in-wizard message composer — input box + Send + the "/" command menu, running on the exact same input machine and command pipeline as the standard input bar. Sending the first message lets the wizard step aside naturally and enter the conversation.
Confirming itself never sends a message. No configuration is ever used without your knowledge.
You can return to the original hero at any time: the «使用标准输入框/Use the standard input bar» link inside the wizard, or Settings → General → New-session configuration wizard toggle (preference stored in browser localStorage).
Install
# Prerequisite: DSH CLI installed with an initialized profile (e.g. web)
dsh plugin --profile web add github:supergameboy/dsh-new-session-cardsA GitHub install fetches sources, and pnpm ≥10 requires explicitly allowing build scripts (only allow sources you trust):
# <profile dir>/pnpm-workspace.yaml
allowBuilds:
dsh-new-session-cards: trueThen re-run add (the prepare script builds lib/ automatically). Local directory or tarball installs need no build allowance:
dsh plugin --profile web add ./dsh-new-session-cards # local dir (run pnpm build first)
dsh plugin --profile web add ./dsh-new-session-cards-0.1.0.tgzVerify and enable:
dsh --profile web --dump-config # should show a "# == dsh-new-session-cards" layer
dsh --profile web # open a new session screen to see the wizard> Already installed and changed the code? The profile links the checkout, so a browser refresh is enough — the server reads the latest bundle file on every request; no service restart needed.
Uninstall:
dsh plugin --profile web remove dsh-new-session-cardsBuild from source
pnpm install
pnpm build # tsdown: lib/index.js (node half) + lib/client.js (browser bundle)
pnpm typecheck # requires the DSH source tree at ../deepseek-harness (see pitfall #1)---
Pitfalls (for DSH plugin authors)
Real pitfalls hit while developing this plugin, recorded as "pitfall → symptom → fix". Read this first if you want to get productive quickly with DSH client plugins.
1. The @deepseek-ai/* dependency graph on npm is incomplete
- Pitfall: the rc versions of
@deepseek-ai/*on the npm registry have missing transitive dependencies (e.g.dsh-user-interactionis unpublished) —pnpm install404s. - Fix: declare no
@deepseek-aidependency inpackage.json. Runtime needs none — the client bundle's externals (react,dsh-client-runtime/client, and the other platform modules) resolve from the shell's module table and everything else is inlined; the build needs none either (import typeis erased). Type-checking resolves them through tsconfigpathspointing at the DSH source tree's build output (../deepseek-harness/packages/*/*/lib/types).
2. --dsw-alias-brand-primary is ink, not brand blue
- Pitfall:
brand-primaryresolves toneutral-bluish-1000(near-black ink). Using it as a button background withlabel-primary(also ink) as text → ink on ink — button text completely invisible. - Fix: use
--dsw-alias-state-business-primary(the DeepSeek brand blue) for emphasis/selection — the InputBar source itself comments: "Business blue, not brand-primary: that token resolves to ink in this sheet"; for primary buttons use--dsw-alias-button-primary-fillbackground +--dsw-alias-label-primary-foreground(white) text — better yet, just reuse theButtoncomponent fromui-primitives.
3. Data loading is not automatic
- Pitfall: an injected
loadcallback is only defined — nothing ever calls it, so cards stay on skeletons forever (no text) and the Next button stays disabled (it depends onstatus === 'ready') — the user is stuck in the wizard. - Fix: call
load()on mount:useEffect(() => { load() }, [load])(copy the shippedModelSelectpattern).
4. Under a composer takeover, the fallback input bar is hidden, not unmounted
- Pitfall:
conversation.composer.barkeeps its DOM and is only CSS-hidden during a takeover (the textarea is always "in" the DOM). Everything anchored to it hides with it: theconversation.input.overlayslot (command menu MenuView, popupSelect shell) anchors inside the hidden InputBar and is not portaled. - Fix: render your own "/" command menu in the takeover UI — but reuse the pipeline:
sessions.scope(sessionId)gets the session scope,inputTriggers.sessionOf(actx)gets the per-session controller (menu store / track / arbitrate / pick), and the menu UI renders from the menu store.
5. Chain re-routing needs a "change" to trigger
- Pitfall: chain selectors are pure (owner props only). After confirmation the state changed but the owner props did not → no re-route → the wizard never leaves.
- Fix: make ConversationRoot re-render. Two working triggers: (a)
conversation.blocks.set(sessionId, undefined)(the root subscribes to block changes); (b) after sending the first message,composerPhaseflips from blank to active (what this plugin finally uses: Step4 send naturally steps aside, no manual flip needed).
6. Locale namespaces need declaration merging
- Pitfall:
ctx.locale.register(NS, ...)is typedN extends keyof LocaleNamespaceMap— a custom NS fails to compile. - Fix: in the plugin,
declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { myNs: MyKeyUnion } }. In components usePropsLocale<typeof NS>(NS is a value — needstypeof).
7. RpcResult discriminant narrowing dies inside immer callbacks
- Pitfall:
response.result.oknarrowing fails insidestore.update((draft) => { ... response.result.error.message })(TS does not propagate narrowing into the mutator callback) → "Property 'error' does not exist". - Fix: extract to a local first:
const message = response.result.error.message, usemessagein the callback.
8. A client-only plugin cannot register a host settings namespace
- Pitfall: settings persistence goes through
settingsScope.bind(), but namespaces are registered host-side (needs host plugin code); a client-only plugin's namespace is alwaysunavailable. - Fix: store the preference in browser localStorage (e.g.
dsh-new-session-cards:useWizard), wrapped in acreateSnapshotStorereactive mirror; the settings row (settings.general.item) reads/writes it — the UI is identical to the built-in settings rows.
9. Build-configuration nitpicks (tsdown / tsc)
- CSS Modules virtual ids must resolve absolute paths from the importer (
resolve(dirname(importer), source)), otherwiseENOENT. - The node half needs
fixedExtension: false, otherwise it emitsindex.mjswhilepackage.jsonexports point at.js. - tsconfig
pathswildcards allow only **one*per pattern** (packages/*/*/lib/typeswon't work — expand per group). - Type-checking against the DSH source tree needs:
allowImportingTsExtensions,lib: ["ES2023"](source usesfindLast),types: ["node"], and a CSS Modules declaration file (src/css-modules.d.ts). - Installing
@deepseek-ai/cordisfrom npm (4.x exists) is not required: cordis types are resolved fromvendor/cordisvia paths as well.
10. Distribution-chain pitfalls
- GitHub installs are source installs: you need a
preparescript (runs after pnpm ≥10's allowBuilds) that builds self-contained — no monorepo sibling context may be assumed. - The
dsh.clientmanifest'sexports["./client"]must point at a real bundle file (clientModulesvalidates at boot; a miss fails the plugin). - Profile bundle order = configuration layer order: when several plugins are installed, later layers override same-id rows from earlier ones.
---
How it works (30 seconds)
flowchart LR
A[New-session hero] -->|conversation.composer takeover chain| B[Four-step wizard]
B -->|Confirm| C[Step 4 Message]
C -->|Send first message| D[Conversation]
D -->|New session| A- Takeover: registers a
conversation.composerchain entry; the selector matches only "blank session + preference on + not confirmed"; sending flips blank → active and the chain steps aside automatically - Data: presets via
agentPresets.list/selectRPC; models via themodelDirectoriesservice; workspace/sessions via the standard stores; the command menu reuses theinputTriggerspipeline - Styling: all
--dsw-alias-*tokens, CSS Modules, WCAG 2.1 AA,prefers-reduced-motionfallbacks, zh/en copy - Preference: localStorage (
useWizardtoggle), one row in Settings → General
Design docs
[docs/design/](docs/design/) holds the full fractal UI design documents (L0 decision chain → page → component library → style spec, two rounds of independent validation passed).
License
MIT