dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Adds clean, flexible folding to keep content compact, organized, and easy to navigate.
Fold consecutive tool calls inside one assistant turn in the DeepSeek Harness (DSH) Web GUI into a single line with a live running-tool label, a running call count, and expand/collapse. Long user input is folded to 3 lines with its own toggle.
Implementation is pure Slot / React. No DOM patching, no MutationObserver,
no display:none, no querySelector games — the official product renderers
are reused for every expanded tool card (and the user bubble is rebuilt from
official primitives).
tool-call nodes that are consecutive in the chat flow
(snapshot.chat.order) and belong to the same turn form one group.
Assistant-step nodes whose blocks contain ONLY reasoning (Think rows) are
TRANSPARENT: they neither split chains, but fold WITH the group (hidden
while collapsed, re-shown between the calls when expanded). The inline
notice rows — context injection (context), automatic compaction
(compaction), manual compaction, commands and workflow runs — fold in
with the adjacent work the same way (neighboring fold blocks merge into one
bar and count toward the group's block count). Diagnostics the user must
always see — model-retry (已重试模型请求), turn-error (本轮运行失败)
and turn-max-tokens (达到输出上限) — are NOT folded and NOT merged: they
render the official cell view unconditionally and act as run boundaries.
Think rows with NO adjacent tools fold into their OWN bar (think-only
group). The reasoning part of a text-bearing node is folded away too —
only text stays visible. Only real assistant TEXT (and user/steering
messages, commands, compaction, …) ends a run — verified against a real 288-call session: with the old rule
every per-step Think row split the chain into 150 groups; with
transparency the same stream folds into 85 groups split exclusively by
text. (In the DSH data model every tool-producing step streams a reasoning
block, so without this rule every call would isolate into its own group.)Running <tool>), the accumulated call
count (completed + running), and a chevron. When the running call settles,
the label switches to the next running call, or disappears entirely once
every call has settled — including error/cancelled/interrupted calls
(any tool-result form, isError or not).tool.call.toolview dispatch — the
exact same slot path the product's ToolCallTree uses — so bash/read/grep/
web/… cards, status, parameters, output, error, subcalls and nested calls
all keep their native look. New calls arriving while expanded are appended
live; the user's expanded state never resets (it lives in React state of
the group leader seat, keyed by the stable first-call node key).N blocks folded
— N is the number of folded blocks (tool calls + Think rows folded into
the group; subcalls inside a block are not counted). While a call runs,
the left side shows Running <tool>.[Think] · <latest line>, a working tool call
renders its real row ([icon] Bash · <command description> for a terminal
call, Read · <path>, Search · <query>, ask_user_question · <question>, … — the product's toolRowModel replicated verbatim). While
a call executes the working call is the latest; once it settles and the
model reasons again, the Think row takes over; when the conversation is
idle the left side is empty. The live block shows ONLY while the group is
COLLAPSED — expanded, the details are right below, so the bar's left goes
empty. The bar whose own group hosts the active node additionally shows
the product's row sweep.compaction), context injection (context), manual compaction
(manual-compaction), user commands such as /permission (command),
unknown surfaces (unknown) and workflow runs (workflow-run) are folded
like any other work block — merged with the adjacent tool/think run, or each
behind its own 1 blocks folded bar (expandable, so they stay reachable).
ONLY the three diagnostics — model-retry, turn-error,
turn-max-tokens — never fold: they render the official cell view
unconditionally and always stay visible. Plain text (user and steering
messages, assistant text, summaries and the summary's copy/chrome row) also
stays visible.Expand toggle below the bubble (shown only when
the text really overflows, measured via ResizeObserver). The clamp lives
on a PADDING-FREE inner box (max-height: 72px = exactly 3 × 24px line
height), so every browser renders exactly 3 lines with the bubble's
bottom gap intact — browsers whose legacy line-clamp behavior would show
a partial 4th line flush against the bubble bottom get clipped to 3 lines
(verified in headless Chromium). The bubble is a faithful replica of the
product's UserStyleBubble built from official primitives (the
official projectUserText — /name/@name/session ref chips with the
exact per-release gating of the host bubble, clickable @file / skill
chips on the 0.1.6 alphas via the seat openFile / openSkill actions —,
JsonBlock extras, the
official attachment row: slot-backed ImageGallery calls, per-image
compact calls plus generic-file cards (FileTypeIcon/fileExtension/
fileSizeText, the exact 0.1.5/0.1.6 composition the product's own file
card uses), the product time +
copy actions with the official writeClipboard) — replication, not
delegation, because
Chromium's line-clamp does not clamp content inside a nested flex
container (the official row is display:flex; verified empirically in
headless Chromium). Short messages render untouched (clamp is a no-op,
toggle hidden).loadOlder) — no button click needed; the product's Load older button
remains as a manual fallback. While the user KEEPS resting at the top and
hasMore stays true, pages continue loading one after another until the
history is exhausted or the user scrolls away (each completed load re-arms
the pump with the refreshed snapshot). The scroll host is resolved through
the product's own scrollerOf contract
([data-conversation-scroll]), the action is the session scope's official
conversation.loadOlder(), and guards (near-top threshold, hasMore,
loadingOlder, in-flight pump) prevent duplicate or mid-scroll loads.
This is the one behavioral DOM read in the plugin (a passive scroll
listener); nothing is patched or restyled.Supports the three current DSH channel releases:
| npm tag | supported release | peer range coverage |
|---|---|---|
latest | 0.1.5-rc.3 | exact version |
next | 0.1.7-rc.2 | exact version |
alpha | 0.1.7-alpha.2 | exact version |
The four shell-owned peer packages use the same explicit three-version set; versions outside this release matrix are intentionally not declared supported.
The two-leg range is deliberate: npm semver excludes a prerelease from a
range whose legs only carry a different major.minor.patch tuple, so the
0.1.6 alpha needs its own leg (and the first leg keeps the 0.1.5 channel
releases, including the previous 0.1.5-alpha.2). All four shell-owned peer
packages (dsh-client-ui-slots, dsh-client-ui-primitives,
dsh-client-ui-attachment, dsh-attachment) accept the same range, so the
plugin resolves against whatever channel the host ships. The newest alpha,
0.1.6-alpha.2, adds a reusable Factory subsystem to SlotCore; the
register / releaseEntry child-declaration contract the overlay depends on
is unchanged — test/overlay.test.mjs exercises the real package in both its
published and minified forms. Older releases are
intentionally out of scope — the version-compatibility layers for them have
been removed (no useSession-carried chat adapter, no namespace probe, no
status/final fallbacks, no plugin-owned turn-level big fold — that fold is
product-owned). The train shares ONE chat-node seat kit: useChat returns
the chat target (chat.legacy.turnEnds is the turn closure), useSession
only the window flags, and the host registers the chat-cell dictionaries
under chat. The remaining channel details — the loadImage owner kit, the
0.1.5 user-bubble update (official projectUserText signature, file
content blocks with generic-file cards, the FileTypeIcon / fileExtension
file-card primitives), and the 0.1.6-alpha.1 additions (the optional
projectUserText references actions that make @file / skill /name
chips clickable, and the tool.call.toolview owner's loadImage currency
forwarded by the tool-group renderer, both carried unchanged by
0.1.6-alpha.2) — are sealed in
src/client/snapshot-face.ts (snapshot normalization),
src/client/registry.ts (compositeT namespace fallback),
src/client/UserNodeWrapper.tsx (user-text + attachment kits) and
src/client/ToolCallGroupView.tsx (tool owner currency); the runtime
overlay validates the live SlotCore shape and fails closed (plugin stays
inert) if the relevant internals change.
dsh-fold is a pure browser-side plugin: it reads only the chat
snapshot the shell hands to every seat (via the useChat/useSession
selector hooks), never the session event log. It therefore declares no
runtime dependency on the DSH core (@deepseek-ai/dsh-session,
dsh-agent, dsh-llm-*, …) — an install can never drag a second,
version-skewed DSH runtime next to the host's. The only runtime imports are
the shell-owned client packages, declared as peer dependencies and resolved
to the host's own instances at boot (dsh-client-ui-slots,
dsh-client-ui-primitives, dsh-client-ui-attachment, dsh-attachment,
react).
ChatView (conversation.view)
└─ ChatNodeSeat × N one seat per business node
└─ renderSlot("conversation.chat.node", {node}, {entryKey: kind})
├─ cell "tool-call":
│ product: ToolCallTree (priority 0)
│ ours: ToolCallGroupView (priority -100 ← lowest wins)
│ ├─ collapsed bar (running label · count · chevron)
│ └─ expanded: interleaved items
│ ├─ think rows: InlineThink (ReasoningRow replica)
│ └─ tool calls: ToolCallBranch × N
│ └─ renderSlot("tool.call.toolview", …)
│ ← official per-tool cards (bash, read, …)
└─ cell "assistant-step":
product: AssistantNodeView (priority 0)
ours: AssistantNodeWrapper (priority -100)
├─ reasoning-only node inside a tool run → null
│ (the group owns its Think rows)
└─ everything else → delegates to the official
AssistantNodeView from the live registry
└─ cell "user":
product: UserMessageNodeView (priority 0)
ours: UserNodeWrapper (priority -100)
└─ product bubble replica from official
primitives + 3-line clamp + Expand/Collapse
└─ cells "compaction" / "context" / "manual-compaction" / "command"
/ "unknown" / "workflow-run":
product: CompactionItem / ContextInjectionRow / … (priority 0)
ours: NoticeNodeWrapper (priority -100)
└─ merges with the adjacent run or renders a
one-line folded bar; expands to the official
view (command keeps its commandview slot)
└─ cells "model-retry" / "turn-error" / "turn-max-tokens":
ours: NoticeNodeWrapper (priority -100)
└─ NEVER folds: delegates straight to the
official cell view, always visible
conversation.chat.node, cells tool-call,
assistant-step, user, compaction, context, manual-compaction
and command, shadowed by priority (the slot core documents "register at
a different priority to shadow it (lowest renders)"). The group is
computed in React from the conversation snapshot (useSession →
snapshot.chat.order + snapshot.chat.nodes), never from the DOM.null (zero-height flowItem), so there is exactly one
row per group.children for an already-declared slot, and without that
children table the shadowing entry receives no renderSlot binding for
tool.call.toolview. src/client/slots-core-overlay.ts installs a
reversible overlay on SlotCore.prototype.register /
.releaseEntry (same module instance the shell uses — ui-slots is a
shell-own static module) that treats an identical child spec as a
shared co-declaration. The overlay is a THIN WRAPPER around the live
methods (no method-text dependency), so it works against both the
published package and the minified shipped web bundle; it restores the
pristine methods on unload and never disturbs the product's
declarations. This is the exact change of docs/core-patch.md (a
source-level patch), delivered at runtime so the plugin works on
unmodified DSH installs. Without this overlay the pure slot API
cannot express "shadow a keyed renderer AND delegate to its child
slot" — that is the single architecture limitation this plugin papered
over, with the core change kept strictly separated.dsh plugin --profile web add github:Yancey2023/dsh-fold
# restart the web app:
dsh --profile web
The CLI clones the repo, resolves the bundle patch, and appends dsh-fold
to the profile bundle stack. To update to the latest version:
dsh plugin --profile web update dsh-fold
# restart:
dsh --profile web
pnpm install
pnpm build
# option A — official plugin CLI:
dsh plugin --profile web add /absolute/path/to/dsh-fold
# option B — helper script (equivalent):
pnpm run install:dsh # uses DSH_PROFILE (default: web)
# then restart the web app:
dsh --profile web
dsh plugin … add sees the package's dsh.bundle.patch declaration and
appends dsh-fold to the profile bundle stack; the client loader
picks up the dsh.client.platform: "web" manifest and serves
exports["./client"] to the page. The host row is a minimal anchor only.
dsh plugin --profile web remove dsh-fold
# or: pnpm run uninstall:dsh
# restart the web app — the official tool-call UI renders again immediately.
There is no residue: the slot entry, the overlay, the locale dictionaries and the stylesheet are all removed with the plugin (each registration is owned by the plugin fiber / ctx.effect).
pnpm install # esbuild, typescript, @types/react, react, ui-slots (devDeps)
pnpm build # tsc --noEmit + esbuild bundles (lib/)
pnpm test # 14 suites: group · tool-row · auto-load · overlay (real ui-slots) · bundle smoke · component · assistant · user · notice · integration · session-api (real dsh-session) · runtime-hygiene
| Case | Result |
|---|---|
| 1 single tool running | Running bash 1 ▸ |
| 2 read✓ grep✓ bash running | Running bash 3 ▸ |
| 3 all settled | 3 ▸ — left side empty |
| 4 last failed | 2 ▸ — left side empty (error = ended) |
| 5 expand | bar + official cards, in order |
| 6 expand then new call | stays expanded, count grows, member appended |
| 7 assistant text | text untouched; groups never absorb it |
| 8 two chains split by text | two independent bars |
| 9 streaming | live count/running label via snapshot re-render (no polling) |
| 10 uninstall | official ToolCallTree wins the cell again automatically |
| 11 long user input | bubble clamps to exactly 3 lines, Expand toggle appears |
| 12 expand user input | full text, Collapse toggle, copy/time actions intact |
| 13 short user input | untouched, no toggle |
| 14 user message w/ images/refs | official gallery + /name @name chips preserved |
| 15 running bash in a group | bar shows the real block row: Bash · <description/command> |
| 16 streaming think (no tool yet) | bar shows Think · <latest line> |
| 17 all settled | bar left side empty |
| 18 compaction/context/command | folded (1 blocks folded) or merged with the adjacent run; model-retry/turn-error/turn-max-tokens never fold |
| 19 long user input, legacy clamp | still exactly 3 lines + bottom gap (padding-free clamp box) |
| 20 scroll to top with older history | auto-loads the next page (no click), once per scroll-to-top |
| 21 mid-scroll / no history / loading | no auto-load |
ToolCall
wrapper (call-row div + subcall recursion) because that wrapper is not
exported; the actual cards are the official tool.call.toolview entries.
The generic fallback for tools without a registered toolview is a
compact theme-aware card (name/args/output/error) instead of the internal
GenericToolCard.-webkit-line-clamp cannot clamp through the
official row's nested flex layout (verified in headless Chromium), so the
clamp has to live on our own element. The replica keeps the product's
bubble, ref chips, images, time and copy actions; if a future DSH changes
the user bubble's visuals, the replica CSS must follow.toolRowModel + the running Think row) — same titles/summaries as the
product rows, but re-rendered by the plugin; a future DSH changing the row
model's titles or summary keys must be mirrored in tool-row.ts.model-retry, turn-error, turn-max-tokens) are NEVER
folded — they render the official cell views unconditionally and always
stay visible; the other notices (compaction, context, …) fold into
bars but remain expandable, so failures stay reachable in one click.entry.children); if a future version breaks
them, the wrapper fails closed (plugin inert, official UI unaffected).src/client/group.ts pure grouping over the snapshot (unit-tested)
src/client/tool-row.ts running-tool row model (product toolRowModel replica)
src/client/auto-load.ts scroll-to-top auto-load-older (sessions scope)
src/client/AutoLoadHost.tsx seat ref anchor wiring seats into the auto-loader
src/client/snapshot-face.ts chat-target snapshot adapter (consumer of the seat kit)
src/client/ToolCallGroupView.tsx group row (live block content) + official members
src/client/AssistantNodeWrapper.tsx assistant-step shadow (official delegation)
src/client/UserNodeWrapper.tsx user bubble replica + 3-line clamp + toggle
src/client/NoticeNodeWrapper.tsx notice-cell shadow (diagnostics never fold)
src/client/registry.ts shared live-registry access for delegation
src/client/translate.ts shared fold translate slot
src/client/slots-core-overlay.ts reversible SlotCore overlay (docs/core-patch.md)
src/client/styles.ts theme-variable CSS
src/client/vendor.d.ts minimal ambient types for the DSH page packages
src/client/index.ts plugin entry (registration)
src/host/index.ts minimal host anchor
cordis.patch.yml bundle patch layer (host row)
build.mjs tsc + esbuild
scripts/install-dsh.cjs install/uninstall helper
scripts/verify-pack.mjs release gate: pnpm pack + artifact scan
test/ group · tool-row · auto-load · overlay · bundle-smoke · render suites · session-api (real dsh-session) · runtime-hygiene
docs/core-patch.md the one core change, as a source patch
MIT