dsh-subagent-model
Manifest valid★ 1Model visibility for DeepSeek Harness: a header chip naming the model and reasoning effort each Session runs on, a subagent dropdown marking which child carries its own route, and sidebar hover cards showing what each conversation did.
dsh-subagent-model
See which model a DeepSeek Harness session is really running on. A chip in the header names the model and reasoning effort of whichever conversation is open — including a subagent's — a dropdown marks which child carries its own route instead of inheriting its parent's, and sidebar hover cards say what each conversation actually did.
Install
From the GUI, paste this into Add plugin:
dsh-subagent-model
Or from a terminal:
dsh plugin add dsh-subagent-model
No build step is involved and there are no dependencies at all. Every entry
point ships as committed source: the Host reads src/client.js and serves it to
the page verbatim, so pnpm never has to run anything and you are never asked to
allowlist a build. Restart the profile afterwards and the chip appears.
The only dependency-shaped field is a peer range on the runtime itself —
@deepseek-ai/dsh-app-boot: >=0.2.0-rc.2 <0.3.0 — and that is the field DSH
actually evaluates: a plugin whose dsh peer range does not satisfy the running
runtime is skipped at boot, with a warning naming the mismatch. Accept it on
an unexpected runtime with dsh plugin allow-version, or wait for a release
that fits the range.
Installing by other routes
| Route | Command |
|---|---|
| npm, the default | dsh plugin add dsh-subagent-model |
| Pinned to a version | dsh plugin add dsh-subagent-model@1.0.0 |
| Pinned to a commit | dsh plugin add github:mirelconstantin/dsh-subagent-model#<sha> |
| Tarball | dsh plugin add ./dsh-subagent-model-1.0.0.tgz |
| Local checkout | dsh plugin add /absolute/path/to/dsh-subagent-model |
What it shows
| seat | what it shows | where |
|---|---|---|
| header chip | model: <id> (effort) for the active Session | beside the subagent pill in the conversation header |
| subagent tree | the shipped subagent dropdown, with a route marker per row | the subagent-catalog header cell |
| hover-card rows | model, effort, work done, active time, tokens, cache, context window, task, kind | inside every Session's hover card in the sidebar |
| per-turn row | which model each turn actually ran on, and what it switched from | the action row under a completed turn |
The chip's tooltip also names the reasoning efforts a model offers and which one is its default, read from the live model catalog. A model the catalog does not list adds nothing: the catalog is advisory and routing accepts ids it does not advertise, so its absence is never treated as a fault.
The effort appears only when one is set — it is optional on the same route record the model comes from, and a session with none configured stays compact.
Presentation only. The model, its tools, and its session log are untouched: no tool is registered, no service is published, and nothing the model can observe changes.
The problem it solves
DeepSeek Harness routes a session by model, and a subagent may run a different one than its parent — but nothing on the page said so. Two things follow:
A subagent's model was invisible until you opened it. Session projections
reach the browser in two places, and they hold different sessions.
projectionsBySession only carries a session whose projection store is
resident in the client, and stores are created lazily
(dsh-api-session-controller/lib/index.js:2525) — so a subagent the user never
opened was simply absent. The list rows carry the same values under
byId[id].projectionValues (:2942, :3521, :3549), synthesized from the
parent's subagentCatalog (:3518-3542), which is what covers a child that is
listed but never loaded. Reading both sources is the whole fix.
An inherited route was unrecoverable. With model selection off — the
subagent default (dsh-tool-subagent/README.md:47) — every child inherits its
parent's route (:67), and that route is nowhere in the child's own log. The
plugin walks parentId up the lineage until an ancestor records an explicit
one, so an opened subagent names a real model marked (inherited) rather than
nothing.
Where the value comes from
The modelSelection Session projection, which the harness already registers and
already ships to the browser:
dsh-api-session-controller/lib/types/model-selection-projection.js
stateSchema : { lastUsed, pending } (routes carry optional reasoningEffort)
wire view : { lastUsed, next } // next = pending ?? lastUsed
registered : lib/index.js:2114 (installModelSelectionProjection)
lastUsed is folded from each request's own request/header config, so it is
the route the session actually used, not the one it was configured with.
next is the pending selection while one is queued and lastUsed otherwise, so
reading next answers "which model is this session on" in both states. That is
the same read the shipped Agent Team row performs
(dsh-experimental-client-ui-agent-team/lib/client.js:89).
Subagents, honestly
Model selection is off by default, so a subagent reading Inherited from parent
is the expected case, not a bug — nothing was overridden. What differs by
construction:
- A continuable subagent with its own route snapshots
agentProvider/agentModel/agentReasoningEffortin its descriptor (dsh-subagent/lib/types/descriptor.js:38-45), so its explicit model is recovered and shown in full. - A one-shot subagent with its own route deliberately omits the agent route
(
:37,:118-126) because cold resume does not need it. An explicitly selected one-shot model is therefore not durable, and the plugin saysInherited from parentrather than guessing. That is a limit of the durable format, not of this plugin.
To pin every subagent's model, set modelSelectionSettings: true on the
subagent tool and pass a route per call; list_subagent_models then becomes
available for discovery.
Four seats, and why the tree replaces rather than adds
The chip, the hover rows and the per-turn row ADD a cell each, so nothing
shipped is displaced. The tree cannot: the shipped CatalogRows renders no
per-row slot
(dsh-client-ui-subagent/lib/client.js:164-338), so a marker beside a subagent
requires owning the subagent-catalog cell itself. It therefore claims that id
at a written priority: -9998, which is what makes a list slot keep it
instead of the shipped occupant (dsh-client-ui-slots/lib/index.js:221,
:278-293) while a duplicate id is rejected only at the same priority
(:183-184).
That number is -9998 and not -9999 on purpose: the sibling
dsh-subagent-tree claims the same cell at -9999, and co-installed the two would
collide at one priority and throw, blanking the header.
That takeover is the one decision worth knowing about: removing the
SHADOWED_CELL_ID registration in src/client.js restores the shipped
dropdown and loses the markers, and nothing else depends on it.
| slot | declared by | this plugin |
|---|---|---|
conversation.session.header.actions | dsh-client-ui-conversation/lib/index.js:18268-18271 | id 'subagent-model' (chip), and id 'subagent-catalog' at priority -9998 (tree) |
sidebar.session.row.hover | dsh-client-ui-workspace/lib/index.js:4322-4325 | id 'subagent-model' |
conversation.chat.turnTail | dsh-cordis-client-runner/lib/client.js:2564-2566 | id 'dsh-subagent-model-turn-route' (per-turn row) |
conversation.chat.node | dsh-client-ui-chat/lib/client.js (keyed by node kind) | the same kind, visibility: hidden — the store the tail row reads |
What each seat tells you
The chip names the effective route of whichever conversation is open, root or
subagent, and marks it (switching) while a queued selection has not been
consumed, (inherited) when it came from a parent. Its tooltip adds the model
breakdown across descendants — 3× deepseek-chat, 2× deepseek-reasoner — and,
when a generation actually splits that breakdown, the same breakdown cut by
generation. That is the integration with the sibling dsh-subagent-tree: both
facts come from one walk over subagentCatalog, so they read in one place.
The tree puts a marker next to a subagent whose route is its own, and a quieter one when it merely inherits. With selection off every child inherits, so marking each one would mark all of them and say nothing — a marker therefore appears only for a subagent that differs, and its tooltip names the parent's model for comparison.
The hover card answers "what was this conversation". Each row is read from a projection the client already holds and appears only when its value is present, so a zero never reads as "did nothing": model and effort, turns and steps, active time, generated tokens, the delegating agent's own task label, and the mode that decides whether the session can be continued.
Per-turn model history
request/header is the event recording each request's route. Nothing in
SessionProjectionMap ships it, so the chip reads the modelSelection
projection — but the event journal itself reaches the browser unfiltered, and
request/header is in it (dsh-api-session-controller/lib/client.js:166, the
stream at :398). The harness's own chat view already reads it that way
(dsh-client-ui-chat/lib/client.js:9389-9394).
So each turn's real route is recovered client-side, with no Host work: a
request/header conversation Definition folds the events into a per-turn map,
and a conversation.chat.turnTail entry reads it. Because the opening journal
frame is a snapshot, a session opened before the plugin mounted still gets its
history rather than only events from that moment on.
reason decides where a turn's route is anchored. initial, resume and
series all anchor; a bare change is a mid-series re-route and does not,
unless it carries startsSeries. An absent reasoningEffort is not "off" — a
adapterDefaults.reasoningEffort means the adapter supplies it, and the row
says so rather than implying no effort.
The older Route change <from> → <to>, not yet applied row is the disagreement
between next and lastUsed, and still reports a queued switch that no
request has consumed yet.
Honesty about missing data
Four states are kept apart, because collapsing them would print a lie:
| state | chip | hover card |
|---|---|---|
| loaded, model known | model: deepseek-chat (high) | Model deepseek-chat (high) |
| loaded, no request yet | (nothing) | Model No model yet |
| listed in a catalog, never opened | (nothing) | Model <model>, or Inherited from parent |
| neither store knows it | (nothing) | (nothing) |
Files
| path | role |
|---|---|
cordis.patch.yml | exactly one Loader row — the anchor |
src/index.js | Host anchor; publishes nothing |
src/model.js | the pure logic, as plain ESM for the unit suite |
src/client.js | the browser half; registers all seats |
src/client.model.js | the same pure logic as a browser chunk the half loads |
src/client.route-history.js | the request/header fold and the per-turn route map |
locale/{en,ro}.json | dictionaries for namespace dsh-subagent-model |
scripts/unit.mjs | node --test suite for src/model.js and locale parity |
scripts/check-plugin.mjs | manifest, patch, exports, browser-half contract |
No build step, no bundler, no transpiler and no installed dependency — the one declared peer is the runtime this plugin was verified against, and DSH resolves it from the installation.
Why the pure logic lives in a second chunk
A classic script cannot import, but it does not have to: the require the
factory is handed carries an async method that fetches a package-local chunk
(dsh-client-modules/lib/client.js:707-713), and CLIENT_CHUNK = /^client\.[A-Za-z0-9][A-Za-z0-9._-]*\.js$/ (:169, served at :915-932) is the
pattern that file has to match. Three shipped packages already ship a second
chunk this way — dsh-client-ui-sidebar-terminal and
dsh-client-ui-sidebar-documentpreview — so this is a supported shape, not a
workaround.
src/model.js is a plain ESM module, which no browser script can reach, so
src/client.model.js holds the same functions as a self-registering chunk and
src/client.js loads it once. The duplication was previously the whole cost of
shipping both; it is now confined to one chunk whose exports are checked against
src/model.js.
Every seat renders nothing until that chunk resolves, which is what keeps a failed fetch from taking the conversation header down. The trade is that the chip appears a tick later than a sync import would allow; nothing else on the page waits on it.
The browser half
Two loading rules are load-bearing, both measured on a live page and not
assumed. See src/client.js for the full notes.
inject stays empty. Naming slots makes fiber.await() park the row until
the renderer provides it, while the renderer waits on the UI packages this row
sits beside. The two wait on each other and the whole boot fails with
web boot: 1 entry did not activate …, taking the entire GUI down. With an empty
gate apply runs before slots exists, so whenService polls with
setTimeout. Do not substitute queueMicrotask: a microtask poll re-queues
itself ahead of the macrotasks every package activation depends on, starves the
boot, and reproduces the same failure.
ro is added to the language catalog. en and zh are shipped catalog
languages; ro is not, and the fallback chain walks the catalog rather than the
registered dictionaries — so a ro dictionary alone is unreachable until
locale.addLanguage adds the language. Both locale/en.json and
locale/ro.json ship the same key set, and scripts/check-plugin.mjs fails the
build if they drift apart.
Verify
node --test scripts/unit.mjs # 84 tests: model logic, cycles, depth grouping, locale parity
node scripts/check-plugin.mjs # 31 checks: manifest, dsh peer range, patch, exports, browser contract
Licence
Apache-2.0. See LICENSE.
Comments
Loading…
From the same category
by ranxianglei
基本稳定可用 100K tokens is enough. Universal context-compression proxy for ALL AI coding agents,10w上下文足矣
★ 929
↓ 465.9k/wk
MIT
TypeScript
Oct 12, 2026
dsh plugin --profile web add billion-contextby Han-1413141
DeepSeek Harness session cost meter plugin: session/daily cost, budget, history, OpenCode Go quota, official & custom-provider balance, Codex-like token heatmap, peak/off-peak pricing with pre-switch
★ 391
↓ 24.3k/wk
MIT
JavaScript
Oct 12, 2026
dsh plugin --profile web add dsh-cost-meterby Nwflower
Import 14+ external agent chat histories (Claude Code, Codex, ChatGPT, Cursor, Gemini, Reasonix, opencode, ZCode, Grok Build, OpenClaw, Pi, Hermes, Kimi CLI, DSH) into DeepSeek Harness as resumable se
★ 226
MIT
JavaScript
Oct 7, 2026
dsh plugin --profile web add dsh-chat-importby Totoro-qaq
DeepSeek Harness plugin for previewable cross-preset session migration. Fixed-schema handoffs preserve state, source-model intent, and unresolved images; the original session stays untouched.
★ 165
MIT
JavaScript
Oct 11, 2026
dsh plugin --profile web add dsh-plugin-bridgeby Anionex
deepseek harness对话和代码状态回退插件 | DSH — rewind conversation and workspace state, powered by a persistent Change Ledger
★ 132
BSD-3-Clause
JavaScript
Oct 1, 2026
dsh plugin --profile web add @anionex/dsh-turn-rewindby SiriLee
DSH 插件:真正便捷无感的同窗口内对话回退,从不新建分支;自带轻量工作区备份,可一并还原文件(完整 Claude Code /rewind 语义)。 · DSH plugin: genuinely effortless in-window conversation rewind — never forking a new session; ships a lightweight workspac
★ 125
↓ 5.5k/wk
MIT
TypeScript
Oct 9, 2026
dsh plugin --profile web add dsh-rewind-plugin