DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Composer Markdown — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-composer-markdown

Composer Markdown

DSH web 插件:对话输入框 Markdown 编辑增强——无序/有序列表自动续行、列表层级缩进阶梯(Tab 降级;Shift+Tab 或在前缀后按 Backspace 升级;整棵子树联动;降级限制为父级下一级;顶层升级后恢复为普通文本)、行内代码样式(保留字面反引号)、代码块围栏自动闭合,以及围栏内 Enter 保护。仅增强编辑状态下的视觉效果与按键行为;发送的文本保持字面 Markdown,逐字节保真。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-composer-markdown@0.1.3
README兼容性版本

说明

DSH web 插件:为对话输入框提供 Markdown 编辑辅助功能——列表自动续行、列表层级缩进阶梯(Tab 降级;Shift+Tab 或在前缀后按 Backspace 升级;整棵子树联动;降级限制为父级下一级;顶层升级后恢复为普通文本)、行内代码样式(保留字面反引号)、代码块围栏自动闭合,以及围栏内 Enter 保护。仅增强编辑状态下的视觉效果;提交的文本保持字面 Markdown。

兼容性与来源证明

Composer Markdown 以 dsh-composer-markdown 发布,当前版本为 0.1.3。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
npm
Registry 更新时间
2026/9/15

版本

0.1.3stable
2026/9/15
0.1.2stable
2026/9/11
0.1.1stable
2026/9/7

相关插件

正在加载相关插件…

最新版
0.1.3
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
510.6 kB
文件数
23
Surface
web
许可证
MIT
发布源
npm
GitHub
★ 0
周下载
0
最近提交
2026/9/15
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 ui-customization 分类下经过校验的插件。

Web App@deepseek-ai/dsh-web-appdsh 浏览器界面捆绑包:位于 dsh-base 之上的 Web 补丁层,加上运行时粘合插件(提供前端 dist、Web 界面提示符、bash 运行时变量和 URL 行)Experimental Agent Team Web Profile@deepseek-ai/dsh-experimental-agent-team-web-profile用于 Agent Teams Remote 和 UI 插件的实验性 Web 配置层Client Ui Task Board@linxin666/dsh-client-ui-task-board面向 DSH Web GUI 的主机权威任务面板,支持实际会话执行、主机 cron 调度以及可选的跨平台空闲睡眠保护;以挂载方式提供,无需修改 DSH 源代码。Client Ui Git Graph@linxin666/dsh-client-ui-git-graph外部 dsh Web GUI 插件:空会话 Git 分支选择器和 Git 图,包含实际的主机端 Git 操作与防护,作为 dsh 配置文件包

README

dsh-composer-markdown

中文 · npm · GitHub

In one sentence: a pure client-side plugin for DSH (DeepSeek Harness) Web that adds Markdown editing aids to the conversation composer — list auto-continuation and renumbering, inline-code styling (backticks hidden once rendered), and code-fence auto-closing with atomic block interactions. Every editing gesture rides on Shift+Enter; Enter keeps DSH's native "submit" semantics and is never intercepted. Apart from the ordered-list renumbering, everything is edit-state visuals and key gestures only — the submitted text always stays literal Markdown, byte-for-byte faithful.

  • Platform: web (the DSH Web GUI, dsh plugin --profile web)
  • Shape: a pure client plugin — the host half is a no-op carrier, zero runtime dependencies, toggleable any time in "Settings → Plugins" (设置 → 插件)
  • Tests: 475 assertions passing (a zero-dependency runner: artifact freshness + pure-logic unit tests + bundle-shape assertions + an optional host-contract smoke)
  • License: MIT

Contents

  • Purpose and use cases
  • Feature overview
  • Installation
  • Usage
  • Architecture
  • Compatibility
  • Known limits
  • Development notes and conventions

Purpose and use cases

The DSH composer is a Lexical plain-text editor that natively offers only "Enter submits / Shift+Enter soft-breaks" — writing Markdown means typing every literal character by hand. This plugin brings the composer's Markdown editing experience up to the level of mature chat products:

  • Write structured content in the input box: lists, steps, code snippets, code blocks — without leaving the keyboard for a toolbar;
  • Make literal Markdown readable: • bullet markers, monospace numbering, the inline-code background, and paired ``` fences rendering as a real code block — what you see approaches how the sent message renders;
  • Keep the plain-text contract: every enhancement is an edit-state visual layer; what gets sent is exactly the literal Markdown you typed, byte-for-byte — the model side, the message renderer, and the clipboard projection are untouched;
  • Change no native habit: Enter still submits; the / and @ menus, @xxx reference chips, image pastes, and IME input all behave exactly as before.

Not for: rich-text (non-Markdown) editing, or rewriting/beautifying Markdown at send time — this plugin deliberately performs no send-state transformation.

Feature overview

  you type                   edit-state rendering                sent text
─────────────────────────────────────────────────────────────────────
  - apple                  • apple  (bullet marker)             - apple
      ⇧↵ (Shift+Enter)     - ␣      (next line auto-prefixed)    …
  1. step one              1. step one (monospace digits)       1. step one
      ⇧↵                   2. ␣     (auto-incremented, ...)      …
  `code`                   code     (mono+bg, backticks hidden) `code`
  ```ts ⇧↵                ┌─────────────────┐                  ```
                          │ ts badge        │                  (blank)
                          │ ␣ (caret here)  │                  ```
                          └─────────────────┘

By domain:

DomainCapabilities
Listsbullet/ordered continuation (any visual line and any nesting depth, including soft-broken and pasted lines), split-at-caret continuation, empty-item exit, the per-group renumbering invariant (delete closes gaps / breaks restart / merges accumulate / nesting-aware), mid-insert shift-down, the level ladder (Tab sinks, Shift+Tab or Backspace-after-the-atom lifts — whole subtree rides along; a top-level lift unlists back to plain text), atomic atom delete (indent included), atomic arrow hops over atoms (indent included), no resting inside an atom, differentiated marker rendering (• dot / monospace digits)
Inline codepaired-backtick inner text styled as inline code, backticks hidden the instant the pair closes (zero-advance invisibility, real font metrics kept), revealed while a selection genuinely covers them (select-what-you-see)
Code blocks``` / ```ts + Shift+Enter skeleton closing, the type-```-at-a-content-line-head flow that preserves the tail, commit-only paired rendering (whole-block code look + floating language badge + zero-height marker lines), one-keystroke unwrap at block boundaries (markers removed, body kept), ↑/↓ skipping the marker lines, Shift+Enter inside a fence inserts a paragraph, fences open at the head of any line (soft-line promotion — a reference chip's embedded newline blocks only its own boundary, the rest still promote, v0.1.3), orphan close-marker cleanup

Installation

Prerequisites

  • DSH 0.1.2-alpha.x (the composer is a Lexical plain-text editor + @lexical/plain-text, lexical 0.49);
  • Node.js ^22.19.0 || >=24.0.0 — the same window DSH itself requires; the package's engines field mirrors it;
  • a modern desktop browser (Chrome / Edge / Firefox / Safari).

Installing the plugin

The package declares a dsh.bundle.patch (cordis.patch.yml: one insert row mounting the host half's no-op entry, which makes the plugin appear in "Settings → Plugins").

From a local checkout (the usual development route):

dsh plugin --profile web add link:/absolute/path/to/dsh-composer-markdown

From the npm registry:

dsh plugin --profile web add dsh-composer-markdown

From GitHub (the built client.js is committed, so the plugin loads even when pnpm skips the prepare build script; pin a commit with #<sha> so a later push cannot change what you run):

dsh plugin --profile web add github:chendefine/dsh-composer-markdown

pnpm ≥ 10 blocks a git dependency's prepare script until allowed. If you want the install to rebuild client.js from src/client/, add the key pnpm prints (e.g. dsh-composer-markdown: true) under allowBuilds in the profile's pnpm-workspace.yaml and re-run the add — treat it as permission to run the package's code on your machine at install time.

Or through the DSH plugin marketplace (设置 → DSH插件市场) — tag the repo with the dsh-plugin topic and it is indexed automatically.

After installing, restart the dsh web service and refresh the browser page. With a link: install, edit the sources under src/client/, run node scripts/build.mjs to regenerate client.js — in dev mode HMR hot-reloads the regenerated artifact (no refresh needed; refresh once if HMR is not running).

Toggle and uninstall

  • Toggle: Settings → Plugins (设置 → 插件) → composer-markdown; disabling restores all native behavior (effective after a refresh);
  • Uninstall: dsh plugin --profile web remove dsh-composer-markdown.

Disable/uninstall auto-cleans: the document listeners, the injected style tag, and every code-style format bit in the editor (the draft text content is unchanged).

Usage

Every editing gesture rides on Shift+Enter; Enter = native submit, never intercepted; a Shift+Enter that matches no scenario = the native soft break. Tab / Shift+Tab are claimed only when the caret's line is a list item (the level ladder) — everywhere else they stay native (focus traversal and the like). Plain Backspace / Delete / arrow keys are claimed only when they hit exactly the boundaries below — everything else keeps the native per-character behavior.

Lists

OperationTriggerBehavior
Bullet/ordered continuationType content after a line-head - / * / N. (any indent depth), press Shift+EnterThe new line below is auto-seeded with the {indent}- / {n+1}. prefix (nesting level preserved), caret right after it; works on any visual line — paragraph heads, soft-broken lines, and multi-line-pasted lines alike
Split at caretShift+Enter with the caret mid-content of a list itemThe line is cut at the caret: everything after it moves down and becomes the new item's content, spliced after the new prefix (byte-faithful); a caret at the line start / inside the marker / at the line end keeps the append-below behavior
Mid-insert shift-downShift+Enter inserting a new item mid-groupThe new line takes n+1 and every member below in the same group shifts +1 (1. 2. 3. inserting between 1 and 2 → 1. 2. 3. 4.); the group stays continuous and duplicate-free; insert and shift share one undo step
Renumbering (the invariant)After any change (delete/insert a line, empty-item exit, paste, undo/redo)Every ordered group (consecutive ordered lines, one indent, outside fences) is always numbered 1. … n. from its first member: a deleted line closes the gap below; a group split by a plain/blank/bullet line restarts its tail at 1.; deleting the splitting line merges the two runs into one continuous count; deeper indents merely suspend the outer group (a nested sublist never interrupts the outer run — its count resumes when the outer indent returns), nested groups normalize independently. Deliberate trade: a group cannot hold a non-1 start or a manual gap — hand-typed digits snap back (continuity IS the contract)
Empty-item exitShift+Enter on an item that is only a prefixThe prefix is removed and the empty paragraph kept (an empty line on soft lines) — press twice to cleanly leave the list; the caret stays on the emptied line (v0.1.2) wherever the item sat — trailing, mid-paragraph between items, or the block's first line — never rolled onto the previous item's end nor clamped onto the next line's head
Level ladder — sinkTab with the collapsed caret anywhere on a list item's line (mid-content included)

Inline code

  • When a line holds a paired `non-empty` whose content stays on one line and whose inner-edge characters are non-blank (full-width space / NBSP included): the inner text gets the inline-code look (code font, light background, rounded corners — reusing the DSH theme tokens);
  • The paired backticks hide the instant the pair closes (invisible, zero width, still kept byte-for-byte in the draft and the sent text); they stay hidden while the caret travels inside the pair or brushes either side;
  • Only a selection genuinely covering one of the backtick characters (Shift+arrows, select-all) temporarily reveals the pair (select-what-you-see); it hides again once the selection collapses; a cross-paragraph selection (Ctrl+A) projects per paragraph — no misses at paragraph boundaries;
  • Non-matching pairs are fully inert (their backticks stay visible and never affect later pairing); ``-style multi-backtick delimiters are not recognized (naive left-to-right pairing).

Code blocks

OperationTriggerBehavior
Skeleton closingThe caret's line is exactly ``` (optionally with a language id ```ts) and the caret sits inside/after the marker run, Shift+EnterInserts the three-line skeleton ``` / empty / ```, caret at the head of the empty line; works when typed at the head of any line (soft-broken and pasted lines included); a caret before the marker run (bare ``` line, offset 0) is plain text and keeps the native soft break
Content-preserving flowType ``` at the head of a line that already has content (caret still right after it), Shift+EnterSplits after the ```: the marker line keeps ```, an empty line + closing skeleton is inserted, and everything after ``` on the original line moves below the closing marker (outside the block, byte-faithful); the language-id flow is unaffected (```ts at line end still yields the badged skeleton)
Paired renderingA paired ```…``` region in the draftThe whole region renders as one code block: code background + code font inside (the same theme tokens and geometry as the message-side CodeBlock); the first marker line is zero-height, the language id shows as a floating badge at the block's top-right; the ``` markers are fully imperceptible (invisible, never host the caret); an unclosed ``` is plain text, always — typing ``` mid-draft never swallows the content below into a block
One-keystroke unwrapBackspace / Delete at a block boundary (← at the first body line's head / the paragraph after the block; → at the paragraph before the block / the last body line's end)One keystroke removes the pair of ``` marker lines: the block un-renders, the body survives verbatim as plain paragraphs, the caret stays put — its own undo step
Leaving the block↓ at the last body line's end (an empty paragraph grows below when the block ends the draft)The caret drops to the new paragraph below the block to continue typing plain text; ↑/↓ skip the marker lines at block boundaries; the marker-line eviction lands a caret (v0.1.3), so ↑/↓ stay armed from a body edge the caret was homed onto — the box top/bottom is never a keyboard trap

Coexistence with native behavior

  • Enter: keeps DSH's native "submit" and is never intercepted (on list lines and inside fences alike);
  • Ctrl/Cmd+Enter: keeps the native "accelerated submit";
  • Shift+Enter (no scenario matched): the native soft break (<br>);
  • Tab / Shift+Tab (not a list-item line, inside a fence, a range selection, or a modifier chord): fully native (browser focus traversal etc.), never intercepted; likewise yielded while a trigger menu is open; held repeats on a list-item line re-plan like any press (they never fall through to focus traversal);
  • While a / or @ trigger menu is open: the Enter family is fully yielded;
  • IME (e.g. Chinese input): keys during a composition trigger nothing (three-signal guard: isComposing / keyCode 229 / a 10 ms window after compositionend — the same guard the DSH input machine itself uses);
  • Slash-command claims, @ reference chips, image drag-drop/paste, busy/locked states, and Q&A/approval cards are unaffected;
  • Coexisting with other composer plugins: capture-phase first-come-first-served — a key already preventDefault-ed is yielded to; keys this plugin consumes get stopImmediatePropagation.

Architecture

The two halves

A DSH plugin has a host (node) half and a browser half; this plugin is the extreme pure-client shape:

┌─ host half (node) ────────────────────────────────────────────┐
│ index.js          a no-op carrier: an importable entry row for │
│                   the bundle, so the plugin shows up in        │
│                   "Settings → Plugins" and is toggleable there │
└────────────────────────────────────────────────────────────────┘
┌─ browser half (web) ──────────────────────────────────────────┐
│ client.js         the single-file load artifact (the DSH       │
│                   loader only accepts single-file bundles),    │
│                   generated from the 15 src/client/ modules by │
│                   scripts/build.mjs and committed; registers   │
│                   via window.__ModuleLoader__.load             │
│                   ({id, factory}), with an in-bundle           │
│                   CommonJS-style module registry               │
└────────────────────────────────────────────────────────────────┘

Source layering (src/client/, one-way dependencies top-down)

constants.js     constants + line-grammar regexes + CSS class names
                 (the stylesheet composes its selectors from them — no drift)
editor.js        the host-contract seam: the ONLY module touching the
                 host's underscore internals
                 (__lexicalEditor/_nodes/_nodeMap/_selection/_compositionKey)
grammar.js       the line-grammar substrate: visual lines, fence intervals
                 + committed coverage, the shared caret-line resolution,
                 the selection-covers reveal rule — derived once per read
doc.js           the document read: block/leaf flat geometry + caret/selection
                 mapping (committed/live flavors) + the husk-block query
fence-plan.js    pure fence projections and decisions (line/block roles,
                 soft-line promotion, fence pairs, orphan cleanup,
                 atomic keys, caret homing)
code-plan.js     the pure inline-code kernels + per-block span/delimiter
                 projections
enter-plan.js    pure Shift+Enter arbitration (one decision tree: fences
                 first, then lists)
list-plan.js     pure list planning (the state-driven renumber walk, marker
                 glyphs, atomic delete/hop/homing, join/detach, shift-down,
                 re-anchor)
analysis.js      one analyzeDraft(texts): the model + every domain projection
edits.js         the edit algebra: the ONLY home of every live-node mutation
                 (flat-range erase / soft-line promotion / paragraph split /
                 digit rewrite / format shapes)
style-sheet.js   the CSS text (composed from the class constants) + the
                 style-tag lifecycle
present.js       the presentation layer: block marks (fence classes / badge)
                 + the ONE glyph-mark engine ({at, class, reveal} table)
                 + stripDom
gestures.js      the key surface: guards + the gesture policy table
                 (prepare/apply/history steps) + the one claim path
restyle.js       the convergent engine: the ordered STAGES table (normalize →
                 DOM marks → repairs → caret home → code/marker shapes),
                 one analysis per pass, at most one structural commit
                 + cross-pass memory + loop guard; instance state, never
                 module globals
index.js         the composition root: lifecycle + __internals assembly

Five cross-cutting designs

Features are not individually plumbed pipelines — they are rows over one set of orthogonal abstractions:

  1. One line grammar: visual-line splitting (each paragraph split on \n; a soft break's \n and a pasted \n are the same thing), fence pairing with committed coverage, and caret-line resolution are derived once in grammar.js; lists, inline code, fences, and Enter arbitration all consume the same model — they cannot drift apart by construction.
  2. One draft analysis: analyzeDraft(texts) assembles the fence roles, soft-line promotion, inline-code spans/delimiters, list marker glyphs, and the renumber invariant into a single object — every projection sees the same lines and the same fence coverage.
  3. One edit algebra: plan (pure data, unit-tested) and apply (live-node operations, edits.js only) are strictly separated; neither the gesture policies nor the convergence engine touch nodes directly.
  4. One presentation engine: edit-state visuals come in exactly two shapes — block marks (fence paragraph classes / badge) and glyph marks (a single-char leaf + class + reveal rule); hidden backticks and list-marker styling share one engine and one reveal predicate — adding a glyph kind is a table row, not a pass.
  5. One table-driven dual driver: the keyboard side is a policy table (one prepare/apply/history per key family), the convergence side is a stage table (one pure plan function per stage); new behavior is a row in a table, not a new pipeline.

Key technical details

The keyboard path (why the document capture phase): DSH's Enter command handler swallows every non-Shift Enter at CRITICAL priority (submit), leaving no seam at the command layer; Lexical dispatches Enter from the root element's own keydown listener. The plugin therefore listens to keydown at the document capture phase — guaranteed to run before Lexical — and only on a matching edit scenario calls preventDefault() + stopImmediatePropagation() to keep out the native soft break, then performs the insertion via editor.update(). Plain Enter never enters this path.

The hidden-glyph trick (zero-advance invisibility): Lexical renders every text node as its own element. The styling write first splits the text around each backtick/marker character (splitText) and flags the single-char leaf unmergeable (Lexical's normalization would merge same-format adjacent leaves back; only this detail bit keeps the isolation); a follow-up DOM scan then classes those leaves via editor.getElementByKey(): the code font + letter-spacing: -1ch cancels the character's advance exactly, and color: transparent hides the ink — real font metrics are kept. display: none is unusable (no box — the browser cannot anchor a caret beside it), and so is font-size: 0 (the caret takes its height from the text node it anchors in — you would get an invisible zero-height caret). The bullet dot renders through an ::after pseudo-element (placed after the zero-advance dash, so a line-head caret renders before the whole • unit).

Commit-only fence activation: only a closed fence renders; an unclosed interval covers only its own marker line in the coverage rule — typing ``` mid-line neither renders the content below into a code block nor turns off list continuation / inline-code pairing on those (plain-text) lines.

Soft-line promotion (fences at any line head): the fence grammar speaks paragraphs, while a soft break is an in-paragraph <br> and a multi-line paste splices a literal \n into a text node. Restyle stage 1 promotes the fence-adjacent soft boundaries to real paragraph breaks (text-projection identical; the draft/send text stays unchanged byte-for-byte), so ``` typed at the head of any line can commit into a block.

The convergence engine: restyle is driven by the editor's update listener — each pass reads the document once, runs the stage table, and commits at most one structural update; the commit re-fires the listener and the next pass finds no diff and stops (writes happen only on difference — idempotent convergence). Self-triggered writes are rate-limited (16 per rolling second) as a loop guard; an in-flight IME composition is yielded to outright. Format/style writes carry the history-merge tag so they don't pollute the undo stack; structural gestures (insert+shift, join/detach+renumber) are discrete undo steps — one Ctrl+Z reverts the whole gesture.

Host contract and safe degradation: lexical is not a module-table shared module, and the plugin must not bundle a second lexical (two copies would split module state and node classes). Every node operation goes through the host editor instance: editor._nodes for the real node classes, editorState._nodeMap/_selection for reads and positioning, and the host nodes' own methods (insertAfter/append/select/splitText/spliceText/setFormat) throughout. These internal access points, like __lexicalEditor, are de-facto contracts concentrated in editor.js; when an upgrade breaks one, the plugin degrades safely to a no-op (one console.warn).

Send fidelity: except for the ordered-list renumbering (which itself rewrites the digits to their literal continuous values — what you see is what gets sent), no line touches text content — the send text = the composer's clipboard projection = per-text-node getTextContent(); format bits and DOM classes never affect serialization.

Compatibility

  • DSH 0.1.2-alpha.x (composer = a Lexical plain-text editor + @lexical/plain-text, lexical 0.49);
  • modern desktop browsers (Chrome / Edge / Firefox / Safari);
  • coexisting with other composer plugins: capture-phase first-come-first-served (see "Coexistence with native behavior").

Known limits

  • List-marker styling and backtick hiding are edit-state visual layers: the characters remain in the draft/copy/send text; a covering selection restores the raw glyphs and widths. The bullet dot's width does not exactly match the original dash (the dot uses the code-font ::after) — in extremely narrow columns the dot may be slightly wider than the original - ; an acceptable typographic difference.
  • Vertical homing has a brief transient: after ↑/↓ lands inside a marker, the eviction happens in the following restyle scan (microtask-scale); in extreme cases one frame of the interior position may be visible; ←/→ arbitration at key time has no such transient.
  • A code block's language id cannot be edited directly (the marker line is unreachable): changing the language = one Backspace unwrap (body preserved), then retype the fence.
  • An unclosed ``` never renders (pasted/draft-restored ones included); Shift+Enter on the marker line (or typing the closing ```) commits it into a block.
  • / ````x`` ````-style multi-backtick delimiters are not recognized (naive pairing).
  • Ordered-list normalization rewrites every non-continuous numbering as it stands: manual gaps and non-1 starts snap back; deeper indents merely suspend the outer group (blank lines, fences, and plain/bullet lines at-or-shallower than the group still break it); groups with more than 9-digit numbers (≥ 1 billion lines) are out of scope.
  • The level ladder's subtree boundary is "strictly deeper indent": a blank line, a fence region, or a line at-or-shallower than the item ends the subtree (the same boundary semantics the renumber grouping uses); deep-indented lines after a blank are not the item's children and do not ride the shift.
  • The sink cap (v2.9): the parent = the nearest item line above (plain text lines are skipped, not parents; blanks and fence regions end the search); an item may sit at most one level below it (parent indent + 2). An item at the cap — and a list's first item — gets a claimed-but-inert no-op on Tab: no boundless descent, and focus never jumps away mid-list-editing. To go one level deeper, sink the parent (or a sibling above) first. Lifting has no cap (level 0 is its natural floor).
  • Unlisting (a top-level lift) splits an ordered group: the plain line left behind makes the same-indent members below restart at 1. per the invariant — a direct consequence of the numbering contract, not a bug. Since v2.8 this gesture replaces the old ordered-item Backspace join-into-the-line-above.
  • The atom's "unselectable, undeletable" indent is a steady-state keyboard guarantee: a collapsed caret cannot enter the atom interior (arrow hops, stray-caret homing, whole-atom Delete at the line head); drag/Ctrl+A range selections can still cover the atom characters (same select-what-you-see rule that keeps manual digit editing reachable), and native deletion then applies.

Development notes and conventions

Build and test

node scripts/build.mjs                       # regenerate client.js from src/client/
node scripts/build.mjs --check               # only verify client.js is in sync with the sources
node tests/run-tests.mjs                     # artifact freshness + pure-logic unit tests + bundle-shape assertions
DSH_CHECKOUT=/path/to/dsh node tests/run-tests.mjs   # + the host-contract smoke (grep assertions)

npm run build / npm test are equivalent shortcuts. The manual acceptance checklist lives in tests/e2e-recipe.md (Chinese; drivable with playwright-cli — keyboard events must be real keydowns). Design and evidence document: dsh-composer-markdown-view-tech.md at the repository root (Chinese; requirements, evidence index, rejected alternatives).

Source conventions

  • Never edit client.js directly: it is the artifact generated by scripts/build.mjs, committed to the repo, and its freshness is asserted by the tests; edit src/client/, rebuild, and commit the regenerated artifact. The DSH loader loads each plugin's ./client export as a single <script src> (no multi-file plugin support) — the one and only reason a build step exists.
  • Module convention: every src/client/*.js file is a CommonJS-style module body (receiving module, exports, require; require('./xxx') resolves through the in-bundle registry); the build concatenates them verbatim in MODULES order — no transpilation, no re-indentation. Zero runtime dependencies — no external packages.
  • Never import/bundle lexical: lexical is not a module-table seed; a second copy would split module state and node classes. All node work goes through the host editor instance (editor.js is the only contract seam); when adding host-internal access points, put them in editor.js and keep the "broken contract → warn once → no-op" degradation.
  • Extension is a table row, not a pipeline: a new key gesture → one {keys, guard, prepare, apply, history} entry in the gestures.js policy table; a new convergence stage → one pure plan function in restyle.js's STAGES; a new glyph style → one {at, class, reveal} row in present.js's glyph table. No new pipelines.
  • plan/apply separation: planners must be pure functions of the current draft (no cross-pass memory — the fence-pair memory of the orphan cleanup excepted), exposed for unit testing through __internals to the zero-browser runner; live-node mutations live only in edits.js.
  • Convergence discipline: write only on difference; at most one structural commit per pass; style/caret writes carry history-merge, structural gestures use discrete undo steps; respect the MAX_WRITES_PER_SECOND loop guard and the IME yield.
  • Styling reuses DSH theme tokens (--ds-font-family-code, --dsw-alias-markdown-inline-code, --dsw-alias-markdown-code-block*); selectors are composed from the class-name constants in constants.js — no hardcoded colors, no CSS/class-name drift.

License

MIT (see LICENSE).

The item and every deeper line below it (nested sublists, deeper-continuation text — the whole subtree, recursively) each gain 2 spaces of indent; same-level siblings and everything after them are untouched; the caret rides its in-line position; the whole subtree moves in one keystroke, one undo step; the reshuffled ordered groups normalize on the invariant right after (a 2. nested under a parent renumbers to that nested run's 1.). Sinking is capped (v2.9): an item may sit at most ONE level below its parent — the nearest item line above (plain lines skipped, blanks/fences ending the search). An item already one level below its parent, and a list's FIRST item (nothing above to nest under), get a claimed-but-inert no-op on Tab (focus never jumps away mid-list-editing) — the ladder cannot descend without bound
Level ladder — lift / unlistSame positions, Shift+Tab — or Backspace with the collapsed caret right after the list atom (at the content head)A nested item (indent ≥ 1 space) → the item and its whole subtree each lose min(2, indent) spaces (odd hand-typed indents floor at 0); a top-level item → unlisted: the whole atom dies, the content stays as a plain-text line (the caret parks on that same line, v0.1.2), the subtree still rises one level. The plain line an unlist leaves behind splits an ordered group — the same-indent members below restart at 1. per the invariant. Holding Tab sinks once and then holds at the cap; holding Shift+Tab climbs one rung per repeat — focus never escapes an item line on either key
Atomic atom deleteDelete with the collapsed caret right at a list item's line head (before the whole atom)The indent + marker atom goes in one stroke (␣␣1. — all of it), the line becomes top-level plain text, its own undo step; the indent can never be eaten character by character
Atomic arrow hop over atomsPlain ← / → with the collapsed caret at any list-atom boundary→ at the line head jumps clear over the whole ␣␣1. / ␣␣- (indent included) to the content's first character; ← right after the atom jumps back to the line head; a caret already inside the atom (including inside the indent spaces — a click, a vertical move) exits to the far edge — the keyboard never walks the atom interior. Shift+arrows stay native (a selection can still cover exactly the digit characters for manual renumbering)
Differentiated marker renderingAny visual line headed by - / * / N. (outside fences, empty prefixes included)Ordered digits switch to the code font; the bullet dash/star renders as • (dot + space as one marker unit); the marker bytes are kept verbatim in the draft/copy/send text; a selection genuinely covering a marker character reveals the raw character
No resting inside an atomA collapsed caret landing inside a list atom (indent spaces included) via any path (↑/↓, mouse click)The caret is homed to the nearest atom edge (ties snap to the line head); the edges themselves (line head / content head) are legal rest points; selections are unaffected
text-anchored
New paragraph inside a fenceShift+Enter inside a fence (the closing line included)Inserts a new paragraph (not a soft break), split at the caret; plain Enter inside a fence still submits
  • Inline-code content that happens to be a /name / @name text-reference token in the lexicon may have its styling overridden by the chip coloring (text content unaffected).
  • A reference chip (@xxx) containing line breaks cannot be soft-line-promoted (chips are atomic nodes); fence judgement on such lines degrades to the whole paragraph.
  • A fully reveal-free inline-code scheme needs chip nodes (tech-doc route B, left for future evolution).