KhalilYamber/dsh-prism

dsh-prism

DSH 界面两档模式插件:简化/原生一键切换,工具卡片白话化,降低上手门槛 / Two-level UI mode plugin for DeepSeek Harness

Uncategorizedcordisdeepseek-harnessdsh

dsh-prism

简体中文 | English

⚠️ Development stopped — this repository is archived (2026-09-27)

What this plugin set out to do, the DSH UI now does itself: the current interface ships folded tool-call groups, concise tool rows, and readable tool output natively. Both problems the plugin existed to solve are gone.

Development stops here. Final version v1.14.1 (2026-09-24); source, development records and acceptance material are kept in this repository as an archive. Never published to npm.

Not recommended as a finished plugin for new users; come back here only when you need something the official UI still lacks, such as an explicitly toggleable tier.

A three-tier UI plugin for the DeepSeek Harness web interface: toggle between Native, Tidy, and Plain with one click. Tidy keeps the product's own tool-row language, Plain rewrites it as plain speech, and both group tool calls into collapsed summaries. Beginners get plain-speak, power users get the full product — the same interface, read three ways.

Why this exists

After the DeepSeek Harness release, community criticism converged on one point: the learning curve.

  • 界面新闻: the "everything is a plugin" design leans heavily on configuration (YAML + plugins + effect components + services). "Powerful for advanced users, but for people who just want a working agent quickly, the barrier to entry is high"
  • 极客公园: DSH is "not friendly to non-programmers", more like a framework than a finished product — a developer's preview
  • Community developers: "Who would actually use this? Why would I plug and unplug things for no reason?"

DSH's bare-bones design is deliberate. But people who want a quick start and people who want the full feature set should not be forced into the same UI. dsh-prism answers that tension with three tiers:

ModeFor whomWhat the UI looks like
PlainPeople who want to get going without learning the jargonTool calls are grouped and collapsed: the chain of calls before the final reply folds into a one-line summary; expanded, every row speaks plain language ("Reading a file") with emoji icons
TidyPeople who want a tidier flow without changing how they read the productSame grouping and collapsing; expanded, every row uses the product's own row language: monochrome icon + category title (Bash / Read / Edit / Tool call) + "·" + argument summary, e.g. Bash · Run syntax check, Tool call · get_goal · {} — rendered with the host's built-in official ui-primitives components
NativePower users who need complete informationZero plugin takeover — the product renders exactly as shipped, pixel for pixel

Native is the default. Switching takes effect immediately, and your choice is remembered: refreshing the page or restarting DSH keeps the tier you picked. Don't want it? Remove the plugin and the UI is back to factory state with nothing left behind.

Features

  • Sidebar entry: a round button at the sidebar foot, above Settings (the official sidebar.footer.action slot, sharing the row with WSL / memory entries); it shows the current tier initial (N / T / P) and opens the menu to switch between Native / Tidy / Plain, which also carries a "Hide complex tools" toggle (Plain tier only)
  • Tidy tier = collapsed groups × native rows:
    • Shares grouping and the collapsed summary line with Plain; only the expanded rows differ: they render through the host's built-in official @deepseek-ai/dsh-client-ui-primitives (DisclosureRow, StateDot, official icon components), with titles and summaries derived by the product's own toolRowModel rules — visually the same as a shipped tool row
    • Row anatomy: state marker (the official StateDot) + monochrome icon + category title + "·" + argument summary; generic tools carry the tool name the way the product does (Tool call · get_goal · {})
    • Two-level state: a tool that truly failed (isError) shows red; a command that exited non-zero (a grep with no match, a truncated head pipe — routine cases) shows amber, captioned "command returned non-zero"
    • Expanded content follows the tool family into official semantic blocks: the command family renders the official TerminalBlock (command header + output + copy button), the read family the official ReadBlock (with line numbers); everything else keeps the delivery document (MarkdownText). When the official primitive is unavailable it falls back in place — nothing is dropped
    • The fold toggle uses the official FoldToggle; this host version does not ship it yet, so the plugin's own fallback ("expand ▾ / collapse ▾") is used, and the official one engages automatically once the host upgrades
    • Complex tools are not folded (information parity with Native); redaction and detail rendering keep the same floor
  • Plain mode = collapsed tool groups × delivery documents:
    • Segmented summary lines: the tool calls between two pieces of prose fold into one line each; collapsed it shows "N tools · M thoughts" (M counts that segment's reasoning blocks from assistant output; when there are no thoughts only the tool count shows), with that segment's status at the end (✓ done / ● running / ✕ had errors / ⚠ mixed counts)
    • While a turn runs, the summary line says what is happening: item N · that tool's plain wording · elapsed time; once settled it returns to "N tools · M thoughts"
    • The expanded panel is segmented by step: while the turn runs the panel opens by itself and only the current step's thinking is expanded; when the turn settles it folds back to the single line (a manual click always wins). A segment head tells the truth: "Thinking done" when that step reasoned, "Step done" when it only called tools; the head controls that segment's thinking text only (collapsed by default)
    • Tool rows sit on the same level as the segment heads and stay there (a work log), grouped by a thin rule down the left of each segment; a row opens onto its native content: the full arguments (commands are not truncated) plus the full result, both redacted first
    • The answer step is rendered by the plugin itself (the product's renderer for the same key is shadowed, and returning null means a blank cell — there is no "hand back" mechanism): one collapsed "Thinking" row, then the reply text, so the answer keeps its reason
    • Seats that step aside take no space: folded seats are collapsed with display (tool/retry seats, or a settled assistant seat that renders nothing), released per turn on a tier switch or unload — display rather than hidden, because the product unconditionally strips a same-named hidden attribute on its side
    • Hide complex tools: 21 advanced tools (goals / plans, subagent orchestration, background jobs, plugin system) collapse into plain summary rows by default; click "expand" to reveal and open the detail; the menu toggle turns the folding off at any time
    • Grouping rule: a turn is cut into spans at every assistant step that carries prose (the tool calls between two pieces of prose form one span each), each span collapsing into its own line right below that prose; inside a span the nodes are still segmented by step. A step that only reasoned is not a boundary (its thinking text stays inside the panel segment); the answer step is always a boundary and nothing after it folds. A running turn keeps accumulating new calls; replay after refresh regroups by the same rule; every tool appears exactly once
  • The clicked block is pinned while a disclosure opens: opening a panel changes the flow height, and the host's scroll owner (ui-chat's ChatView.tsx: a ResizeObserver on the flow column that snaps the viewport to the new floor while the reader is pinned to the bottom) would otherwise pull the clicked row out from under the pointer. The plugin records the clicked block's on-screen position at click time and holds it, so the clicked row stays still while its content grows downward. The correction writes only the scroll offset, disarms 320ms after the layout settles (1.2s at the latest), and yields at once to any wheel / touch / scroll-key gesture; mid-transcript reading never moved, and the pin adds nothing there
  • Bilingual UI: every string follows the DSH interface language (Simplified Chinese / English); switching language in Settings takes effect instantly without a refresh — tool copy, argument summaries, menus, and group status all ship in both languages
  • Data redaction (one shared floor for both collapsed tiers):
    • Sensitive argument names such as token / secret / password / api_key / authorization are never read — the Tidy tier filters them too; when a payload holds only sensitive keys the summary stays empty instead of falling back to raw JSON
    • Common secret shapes in result text and summaries (sk-xxx, Bearer xxx, ?token=xxx, key=xxx) are replaced with placeholders
    • The Plain tier additionally reduces paths to the file name (file_path and similar render as basename); the Tidy tier shows paths by the product rules, to stay close to the shipped rows
    • The detail panel shows only the redacted result — raw arguments never surface
  • Native tier = product as shipped: in Native mode the plugin registers no tool-row renderer at all and hands rendering back to the product (including generic cards); the collapsed-group nodes register only in Tidy and Plain (taking over tool-call / assistant-step / model-retry with a lower priority), and tier switches register / unregister dynamically and take effect instantly
  • Diagnosability: one-line health check in the tier menu (version + the three registrations + the three spacing counters); the console keeps the last 6 partition runs and the last 5 turn snapshots under __PRISM_DIAG__; suspicious shapes are auto-logged (__PRISM_DIAG_LOG__() to read, __PRISM_DIAG_LOG_CLEAR__() to clear), so a problem that heals itself still leaves its scene behind
  • Copy rules: the 33-tool rule table supplies plain-language copy (e.g. pwsh → "running a command on the computer"); tools outside the table get an argument-name-derived summary. In the Plain tier every row inside a group renders by these rules; the Tidy tier uses the product's own category titles and summary rules (Bash · …, Tool call · name · …) and keeps the plugin filter only on the redaction floor

Design principles

  • Presentation only: changes purely the UI rendering; model input and output are untouched — the agent's work is unaffected
  • Zero takeover in Native: no cards are registered in Native mode; the product UI returns completely; in Plain mode tool calls fold into groups whose rows render by the plain-language rules, but the official tool cards themselves are never modified and remain original in Native mode
  • Tier memory: the tier you pick is stored in the browser (dsh.prism.mode), so a refresh or a DSH restart keeps it; other UI state (group open/close, menu, hide-complex toggle) stays in memory
  • Theme-following: only official --dsw-alias-* design variables are used; adapts to both light and dark themes
  • Fail loud: registration and render errors always console.error('[prism] …') and land in the diagnostics surface (window.__PRISM_DIAG__) — never swallowed
  • Never swallow, never invent: missing data degrades to a plain rendering instead of a guess or a text-parsed reconstruction; folding never makes content disappear

Installation

DeepSeek Harness is required first (Node.js 22.19+ or 24+).

The plugin is distributed through GitHub; clone this repository and install it by local path:

npx -y @deepseek-ai/dsh plugin --profile web add <path-to-this-repo>

You can also download published artifacts from Releases. After the Web UI starts, the tier entry appears at the sidebar foot, above Settings.

Usage

  1. After startup, click the tier entry at the sidebar foot, above Settings (it shows the current mode)
  2. Choose Plain: tool calls in a task fold into groups — a one-line summary when collapsed; click to open the panel with a plain-language row per tool and its delivery-document detail
  3. The menu can toggle "Hide complex tools" (Plain mode only)
  4. Choose Native: the full native interface returns
  5. The tier is remembered: a refresh or a DSH restart keeps the current tier; switch back to Native from the menu at any time

FAQ

Is my tier remembered? How do I clear it? Yes. Your tier is stored in browser local storage (key dsh.prism.mode), so a refresh or a DSH restart keeps it. To go back to "Native, no trace", just switch to Native; to remove the memory as well, run localStorage.removeItem('dsh.prism.mode') in the browser console and refresh. A different browser (or cleared browser data) starts from the default Native tier.

Why do some tool cards look unchanged? Tools like read, write, and web_search already have polished official native cards; the plugin registers no replacement cards for them (Native mode is completely original). In Plain mode they are folded into the group alongside other tools and shown as unified plain-language rows — the official cards themselves are never altered.

Why do some status dots show red and others amber? Red means the tool really failed; amber means the command exited non-zero (a grep with no match or a truncated head pipe counts — routine stuff), captioned "command returned non-zero". A run of piped commands no longer paints the whole fold red.

Does Plain mode affect how the agent works? No. The plugin only changes the display; the model receives and produces exactly the same input and output as in Native mode.

What do tables / code blocks look like in the detail panel? Result text is redacted first, then handed to the official MarkdownText renderer (tables, code blocks, headings, lists, inline code, and links included); when the official primitives are unavailable it falls back to monospace plain text and never throws.

Roadmap

  • Keep tracking DSH official interface evolution and stay compatible with new releases (once the host ships them, previously missing primitives such as the official FoldToggle engage automatically)
  • Expand the tool rule table and official-block coverage: the Search / Web / Diff blocks await structured metadata from the product side
  • Keep polishing rendering stability in long sessions and the fold experience
  • Add adaptation notes and FAQ entries based on community feedback

Ideas or tools that don't fit well? Open an issue and discuss.

Changelog

v1.14.1 (2026-09-24)

  • Fixed: the status lights misfiring across the board (reported the same day: "the red/green/amber lights for tool success and failure — I see red and amber almost all the time now"). Root cause: the v1.14.0 official-vocabulary change added a second exitCodeOf in the result section that shadowed the data-layer one; the newcomer returns undefined when no exit code is found, while the check reads code !== null — undefined !== null is always true, so every unmarked text (i.e. every normally successful tool) was judged "exited non-zero": nearly every row showed ✕ and the segment-level success count stayed at 0 (the ✓ n · ! m counter branch was unreachable — which is also the answer to the previous release's open question about that branch never firing)
  • Row-level grading caught up: a non-zero exit now shows amber ⚠ ("command returned non-zero") at row level too, matching the segment level; only a real tool failure (isError) is red — the v1.14.0 intent ("no more rows of red dots") now holds in both places (plain-language cards and the Tidy tier's official StateDot warning state)
  • Anchored to prevent false positives: exit-code parsing now matches the product's parseExitStatus exactly (leading newline + end-of-string anchor) — a [exit code: N] string appearing inside a file or a search result no longer counts
  • Verification: harness 161/161 (default and --real-primitives); the new assertions cover row-level three states, the counter branch being reachable, the end anchor, and failures still going red; the reverse run against the buggy build turns exactly 3 sentinels red (row-level measured [0,0,2] — the two-red-crosses picture reproduced); the other gates pass 16/16, 26/26, 17/17, 13/13, 20/20; live check PASS with 1.14.1 served and zero errors

v1.14.0 (2026-09-24)

  • The fold cards adopt the official vocabulary (requested 2026-09-24: "the front end of the fold cards isn't natural enough — can it be polished with the official ui-primitives?"). The investigation found the Tidy tier's rows were already the official DisclosureRow, while three parts stayed hand-drawn: the row-end state dot, the fold toggle, and what the expanded row contains. This release replaces them:
    • State dot → the official StateDot (its running state is the same chasing animation as the one in the TerminalBlock header, so rows and cards never disagree)
    • Fold toggle → the official FoldToggle (button + aria-expanded; the caption moves from a made-up "expand" to the product's "N more rows")
    • Tool detail → the command family renders through the official TerminalBlock (command + output + exit code + one-click copy + its own height fold), the read family through the official ReadBlock (a file block with line numbers)
  • Fall back in place when unavailable: a missing official primitive, or a tool family not yet wired, keeps the original delivery document (MarkdownText) — no content is dropped; unwired families must not impersonate an official block
  • No duplication, no loss: the official block carries its own command header / file name, so command / file_path are pulled from the outer argument area (on the live app the command used to show up twice); the remaining arguments (cwd / timeout / offset …) stay as before
  • Two-level state (requested 2026-09-24 after a screenful of red dots): "tool failed" and "command exited non-zero" used to both count as err, so a run of grep / head pipes (non-zero exits) painted every fold row red. Now only isError (a real tool failure) is red; a non-zero exit code goes amber (reusing the existing mixed state, captioned "command returned non-zero")
  • Why only two families: the Search / Web / Diff official blocks consume structured metadata the product's tool plugins put into their results (path / offset / lines / totalLines and the like); the plugin layer cannot reach it, and parsing the text instead would invent content that looks right and is wrong (red line: never swallow a block, never invent one)
  • Verification: the harness gained 3 assertions (command family either/or, read family either/or, unwired families do not impersonate), 154/154 green; a differential run against v1.13.0 turns the result-area assertions red. One device gap fixed: PRISM_REAL_PRIMITIVES=1 does not survive WSL → Windows node.exe, so a --real-primitives flag was added
  • Same-release closing verification: the two-level state difference is precise: running the pre-grading build turns exactly 1 of 156 assertions red (its sentinel); the full set is 156/156 (both default and --real-primitives modes, measured)

v1.13.0 (2026-09-24)

  • Automatic incident logging (reported 2026-09-23: "in long sessions the Tidy tier occasionally shows the native look", and "switching to Native and back to Tidy, plus a refresh, brings it back"). A fault of this kind heals itself, and catching it by hand is unreliable ("there is no broken page left to find"), so a very light pen now hangs on the render path: only when a suspicious shape appears and its fingerprint changes does it write one record to local storage (key dsh.prism.diag.log, capped at 20, oldest half dropped on overflow)
  • Two suspicious criteria, both meaning "rows that should have folded did not": span=false (the node fell into no span → the tool cell draws a standalone native row, which in the Tidy tier looks exactly like Native) and claimOK=false (the claim failed → nobody drew that span's fold line). The product folding its own turn (foldable) and nodes outside the window (inWin=false) are normal shapes and are not recorded — logging them would only drown the signal
  • What is written: structure only (whether a span formed, whether it was claimed, where, and that turn's DOM counts of tool seats / prism fold lines / collapsed seats); no node keys, no prose; the fingerprint leads with the session, so same-numbered turns from two sessions never swallow each other
  • How to read it: __PRISM_DIAG_LOG__() in the console returns the JSON, __PRISM_DIAG_LOG_CLEAR__() clears it; the dev/prism-claim-dump.js probe prints it alongside
  • Verification: the harness gained 6 assertions (one record when no span forms / no repeat write for the same shape / nothing recorded for normal shapes / a changed shape within a turn replaces rather than stacks / corrupted storage is wiped on the spot / logging still works after the wipe), 152/152 green; the reverse run against --client=dev/client-1.12.0-backup.js (the build without the logger) turns 5 of them red without breaking the run — the assertions bite. Live check: delivered version 1.13.0, tier and all three registrations healthy, the read control in place, the storage key absent while nothing is wrong; plus an end-to-end pass (inject a sample → read it back → clear)
  • Three issues fixed in the closing review (all introduced by this release): the fingerprint no longer contains the tool count (it used to write to disk once per arriving tool in a long turn); within one turn and one kind only the newest record is kept (a single turn cannot overflow the 20-slot cap); corrupted or over-quota storage is cleaned once and re-armed (the logger can no longer fail silently forever). One assertion was also fixed: on the old build "comparing empty arrays by length" happened to be equal and then read undefined, crashing the whole run — now it turns red as it should, without the crash

v1.12.0 (2026-09-22)

  • Tier memory: the chosen tier is stored in browser local storage (dsh.prism.mode, the same route the host uses for its own UI preferences such as dsh.conversation.contentWidth). The read happens before the store is created, so renderers register from the remembered tier at startup. Unreadable, malformed, or blocked storage counts as "never stored" and falls back to Native; a storage failure logs one console.warn and never touches the errors counter
  • Only the tier is remembered: group open/close, menu, and the hide-complex toggle stay in memory
  • Verification: new device dev/prism-persist-check.mjs (real React + real Chrome + real reload) 20/20; reverse verification: dropping the write → 8 red, reading a constant → 5 red. Regression: harness 145/145, seat contract 16/16, data red team 26/26, browser red team 17/17, pin check 13/13, self-check probe 12/12