Thanks for considering contributing to dsh-TUI! This guide is the shared
development contract for humans and coding agents working on @deepseek-harness-tui/dsh-tui.
- Report bugs through the bug issue form: version, terminal environment, and a minimal reproduction.
- Request features in Discussions Ideas.
Issues do not accept feature requests. Accepted proposals get a tracking issue,
and its assignee owns the implementation. Do not start writing code before the
proposal is accepted — OAuth,
/cost, notifications, a plugin API and a remote runtime were each written in full and then closed. If a maintainer has not responded within 14 days, you may open a PR directly; it gets theunreviewed-proposallabel and is treated as unreviewed. - Open a pull request against
main. Keep changes focused: one logical change per PR, with a Chinese or bilingual title and a description that covers motivation, what changed, and how it was verified. A pull request that changes code must link an issue: add aCloses #<issue>line to the description, or link it through the Development sidebar. Theissue-linkCI group checks this and fails without a link. Docs-only changes are exempt (same routing as the build and regression groups); for a maintainer release, revert, or CI hotfix that genuinely has no issue to link, apply theno-issue-neededlabel. - Run the verification matrix below before requesting a review; CI runs the same commands.
- New features should include or extend a focused regression script.
It applies only to pull requests opened on or after 2026-08-24. Pull requests already open before that date follow the previous rules and need no Discussion or tracking issue.
This file applies to the entire repository. It is the shared development
contract for humans and coding agents working on @deepseek-harness-tui/dsh-tui.
@deepseek-harness-tui/dsh-tui is a single-package, ESM-only TypeScript project. It provides a
React terminal UI front door for DeepSeek Harness through Cordis. The package
owns the TUI, its local command surface, and a ported Ink/Yoga renderer.
DeepSeek Harness owns the agent, session, model, tool, skill, persistence,
and policy domains that the TUI consumes.
Before making a broad change, read package.json, the relevant README section,
and every source file being edited. Prefer the repository's existing service
boundaries and helpers over introducing parallel abstractions.
src/index.ts: public Cordis plugin entry point, configuration schema, and lazy handoff to the runtime plugin.src/dsh-adapter/plugin.ts: TTY validation, service registration, agent creation/resume, React tree mounting, and terminal/process teardown.src/dsh-adapter/questions-answerer.tsandpreset-resolution.ts: isolate upstream prerelease dispatch for user questions and agent presets so version branches do not spread into bootstrap or channel actions. Note: the questionnaire "provider seat" guard (DUPLICATE_PROVIDER probe + private symbol check, #586) only applies to the legacy rcregisterProviderpath. On the 0.1.2 line'suser-questions/requestwaterfall, Cordis first scope-filters requests carrying an agent; agentless/authrequests are dispatched without a scope carrier. Under the answerer convention, the first eligible listener that returns instead of delegating withnext()claims the request. Cordis waterfall is around middleware, however: an outer listener can callnext()and then observe, replace, or reject the downstream result, while{ prepend: true }inserts a listener at the front. Upstream offers no supported way to discover or reserve a verifiably exclusive claimant, so the legacy seat guard and its warning cannot be reproduced locally.src/dsh-adapter/channel.ts: event-to-view projection and the non-React action surface. It translates DSH session events into transcript rows and implements submit, steering, rewind, resume, model/preset switching, local reports, and related state transitions.src/screens/Chat.tsx: top-level interaction coordinator. It owns modal precedence, global keyboard handling, scroll/search/selection state, slash command dispatch, and composition of the chat screen.src/screens/StatusLine.tsxandsrc/screens/StatusMetrics.ts: terminal status presentation and metric derivation.src/components/: feature components.components/design-system/contains theme-aware primitives;components/messages/contains transcript rows;components/questions/contains theask_user_questionUI.src/ui.ts: preferred facade for the local renderer, themedBox/Text, hooks, and public TUI primitives.src/ink/: ported, low-level Ink renderer and terminal implementation. Treat it as sensitive infrastructure: keep changes focused and accompany them with renderer-specific regression coverage.src/native-ts/yoga-layout/: ported layout engine used by the renderer.src/cc/: terminal formatting and presentation helpers adapted for the Claude Code-style UI.src/*Prefs.ts,src/customTheme.ts, andsrc/sessionHistory.ts: persisted user preferences and local session metadata under~/.dsh-tui..agents/skills/*/SKILL.md: project skills for repository maintainers, discovered by the DSH filesystem provider and excluded from the npm package.cordis.patch.yml: package bundle overlay used by profile installation. Ordering, row IDs, disabled host rows, and insert/override semantics matter.cordis.yml: full bare-composition example for direct Cordis/DSH startup.scripts/: headless regressions, reproduction harnesses, probes, and diagnostics. Read each script's header before running it.lib/: ignored JavaScript, declarations, and declaration maps generated fromsrc/and shipped to npm../invariantuses the compiledlib/types/dsh-adapter/invariant.jsentry as well.README.mdandREADME_EN.md: Chinese and English user documentation. Keep behavior, configuration, shortcuts, and limitations synchronized between them.
The central runtime path is:
Cordis config
-> src/index.ts
-> src/dsh-adapter/plugin.ts
-> DSH agent/session services
-> src/dsh-adapter/channel.ts (session events -> Channel snapshot)
-> src/screens/Chat.tsx
-> src/components/*
-> src/ui.ts
-> src/ink/* + Yoga layout
-> terminal ANSI output
Keep ownership in the layer where it belongs:
- Agent/session/tool facts come from DSH services and durable session events.
- Projection and TUI actions belong in
channel.ts, not in presentation components. - Interaction modes and key precedence belong in
Chat.tsxor the focused modal/input component. - Reusable visual behavior belongs in
components/and theme-aware primitives. - Terminal protocol, layout, hit-testing, selection, and frame-diff behavior
belong in
ink/.
Do not reimplement a DSH domain service in the TUI merely to make a screen easier to build. Adapt the service through the channel or an existing registry seam.
-
Supported Node versions are
^22.19 || >=24; CI uses Node 24. -
CI and publishing use pnpm 11. Use pnpm as the development package manager. The
packageManagerfield in the rootpackage.jsonis the single source of truth for the pnpm version; both CI and corepack read it from there. -
Install a clean checkout with:
git clone --recurse-submodules https://github.com/ccch1mneyyy/dsh-TUI.git cd dsh-TUI pnpm install --frozen-lockfileIn an existing checkout, run
git submodule update --init --recursivefirst.vendor/dsh-stdanddsh-authare workspace /link:dependencies, so the install always fails while those submodules are empty. -
pnpm-lock.yamlis the single lockfile. npm consumers do not read a dependency's lockfile, sopackage-lock.jsonhas been removed (follow-up of #173). -
When intentionally changing dependencies, update
pnpm-lock.yamlwithpnpm add, inspect the full lockfile diff, and avoid unrelated upgrades. -
Every
@deepseek-ai/*framework package this package references at runtime or from its published types (mirroringUPSTREAM_BLESSED_PACKAGES, including@deepseek-ai/schemastery) is both a peer and a dev dependency: framework packages are host-provided and resolve at runtime to the host's own instance through the$DSH_HOME/profiles/node_modulesfallback tree (see #198 — declaring them as runtime dependencies lands real copies inside the profile and splits module identity from the host). The dev declarations exist only so the package can type-check locally. Add new references of this kind to both sections at matching ranges (the verify:manifest-deps gate enforces it). Framework packages used only by tests/scripts (e.g. dsh-settings, dsh-tools, dsh-session-persistence-*) stay dev-only — do NOT declare peers for them. Non-host packages such asdsh-working-activitystay runtime dependencies. Historical exception, now resolved:dsh-working-activity@0.2.4and earlier pulled a real copy of@deepseek-ai/schemastery(plus cosmokit) into the profile via its runtime dependency, shadowing the fallback tree; 0.2.5 peer-ified it (working-activity#2), so profiles no longer carry any framework copies. Keep the dependency range at^0.2.6or above (0.2.6 also fixes the web-side WorkingLine absent-field guard on unpatched hosts, working-activity#5). -
Do not expose, persist, or print credentials. Interactive startup reads
DEEPSEEK_API_KEY; diagnostics may report whether it is set but must not reveal the complete value.
The normal build and type-check gate is:
pnpm buildThis removes the complete lib/ directory, runs tsc -p tsconfig.json to emit
src/ into lib/types/, and then checks the adapter boundary, upstream
contract, and patch surface. The prepare lifecycle serves source-checkout
bootstrapping only (it fails fast when the vendored submodules are absent —
see scripts/prepare-guard.mjs); Git URL dependency installs have been triply
blocked since vendoring (#308: workspace deps / submodules / pnpm ≥11's
prepare allowlist) and are unsupported — install the registry package. Local
and CI workflows use explicit commands instead of depending on whether pnpm
implicitly runs the root lifecycle.
Rules for generated output:
- Edit
src/, neverlib/, to implement behavior. - After any source change, run
pnpm build, but do not commit generated files fromlib/. - Clean compilation removes the complete
lib/first, so renamed or deleted source modules cannot leave stale output behind. - Run
pnpm verify:packageto ensure everymain,types,bin, andexportstarget is present in the npm tarball and to smoke-import the main and invariant entries. - Documentation-only, workflow-only, and YAML-only changes do not require a rebuild unless they also alter TypeScript inputs.
- Git URL installation with
--ignore-scriptsskipsprepareand is therefore unsupported. Registry packages already contain compiled output and do not depend on lifecycle scripts running on the consumer's machine.
scripts/build.sh is an alternate builder for a local DeepSeek Harness source
checkout. It locates a DSH checkout and rewires dependencies to that checkout.
It is not the default build command for this standalone repository.
There is no root test or lint script. Do not claim that either ran. The
TypeScript build is the universal static gate, followed by focused executable
regressions.
CI runs these commands after installation:
pnpm compile # generate a clean runtime
test -f lib/types/index.js
pnpm verify:build # build gates without recompiling
pnpm verify:package # npm tarball and entry smoke test
node --import tsx/esm scripts/repro-askpanel.tsx
node --import tsx/esm scripts/verify-askpanel-layout.tsx
node --import tsx/esm scripts/repro-toolcards.tsxRun all three CI regressions for changes to shared rendering, Chat, prompt or
question layout, tool cards, theme primitives, or the Ink core. For a narrow
change, also run the closest focused script:
| Change area | Focused verification |
|---|---|
| General headless screen composition | pnpm smoke |
| Channel submit/steer/pending behavior | node scripts/verify-submit.mjs |
| Prompt queue behavior | node scripts/verify-queue.mjs |
| Goal/todo projection and rendering | node scripts/verify-channel-goal-todo.mjs and node scripts/verify-goal-todo.mjs |
| Compaction and folded transcript rows | node scripts/verify-compact.mjs |
| Compaction × session-switch lifecycle (cancel before the fork snapshot, persistence-classified toast) | node --import tsx/esm scripts/verify-compact-switch.tsx |
| Theme loading, persistence, and runtime plugin seam | node --import tsx/esm scripts/verify-themes.mjs, node --import tsx/esm scripts/verify-runtime-themes.ts |
| Scrolling/sticky-bottom behavior | node scripts/verify-scroll.mjs, node scripts/verify-resticky.mjs, and the matching repro-* harness |
Long plan-review body (exit_plan_mode windowing + wheel) |
node --import tsx/esm scripts/verify-plan-review-scroll.tsx |
| Fullscreen copy-on-select | node scripts/verify-copy-on-select.mjs |
| Component-level mouse drag protocol (target capture, bubbling, click/selection compatibility, interrupted-session cleanup) | node --import tsx/esm scripts/verify-drag-protocol.tsx |
| Mouse pointer event pipeline (wheel coords/modifier bits, click/hover dispatch, out-of-bounds clamping, pointer-state reset) | node --import tsx/esm scripts/verify-pointer-events.ts |
| Hover event performance (complete interest boundaries, no-interest rect fast path, frame/multi-root invalidation) | node --import tsx/esm scripts/verify-hover-coalesce.tsx |
| Prompt-input mouse selection editing (drag/Shift+click/double-click word select, delete/replace, layered Esc, Ctrl+C copy, CJK wide cells, fold-side clamping) | node --import tsx/esm scripts/verify-input-selection.tsx |
Most focused scripts invoked with plain node import lib/types/; run
pnpm build first. Scripts that import TypeScript sources declare the
node --import tsx/esm <script> form in their header. Do not infer the input
layer from the file extension: verify-themes.mjs, for example, imports
src/ through tsx.
Some scripts are forensic or interactive tools, not bounded tests. In
particular, heap/leak scripts, PTY probes, replay capture, performance probes,
and scripts/run.ts can require a specific OS, terminal, native dependency,
DSH checkout, or long-running process. Read the header and prerequisites; do
not run every file in scripts/ as a blanket suite.
For terminal-visible changes, headless assertions are necessary but not always sufficient. When the environment is available, manually exercise the affected flow in both inline and fullscreen modes and at a narrow terminal width. Check startup, resize, scrolling, input, cancellation, and clean exit. Windows ConPTY, tmux, OSC clipboard behavior, and synchronized output have distinct paths, so use the matching probe when changing one of them.
pnpm tui invokes scripts/run.ts, which assumes the package lives inside a
DeepSeek Harness monorepo layout with apps/cli and packages/*. It is not a
portable standalone smoke command. For an end-user integration check, install
the plugin into a DSH profile and run dsh --profile dsh-tui in a real TTY with
the required credentials.
- The package is ESM. Relative imports in TypeScript use
.jsspecifiers, for exampleimport { Chat } from './screens/Chat.js'. Preserve this rule. - In repository-authored TypeScript, follow the prevailing style: two-space indentation, single quotes, no semicolons, and trailing commas in multiline constructs. The ported Ink files may retain their upstream tabs or quoting; do not mass-format them.
- Prefer
import typefor type-only dependencies. - Do not introduce
anymerely becausetsconfig.jsonrelaxesnoImplicitAny. Those relaxations exist to compile the ported Ink core and must not become the quality bar for new application code. Useunknownand narrow it, or define a small structural interface at an external seam. - Preserve readonly data where the surrounding API uses it. Keep state mutations inside the channel/store implementation rather than mutating values from components.
- Keep exported APIs documented with concise JSDoc. Explain contracts and non-obvious invariants, not line-by-line mechanics.
- Avoid one-use abstractions and unrelated refactors. Inline a trivial helper when it has one call site and does not clarify a real invariant.
- Preserve initialization ordering around environment-sensitive imports.
FORCE_COLOR,NODE_ENV, and terminal capability flags are often read at module evaluation time; moving an import above their setup can change behavior without a type error.
- Keep
src/index.tsas the small public plugin contract andsrc/dsh-adapter/plugin.tsas the runtime implementation. Preserve the lazy handoff unless the task intentionally changes the plugin-loading contract. - Register resources through Cordis and clean them up through
ctx.effector the existing single exit funnel. A render failure must remain loud and non-zero; normal exit must restore terminal state before process exit. cordis.patch.ymlis layered overdsh-base. Do not duplicate a service row that the base already mounts. Distinguish an ID override from aninsert, and preserve ordering when one service depends on another.- A profile override replaces an entire
configblock. When documentation shows an override, include every key that must survive the replacement. - When adding or renaming a plugin option, update the
Configinterface and Schema insrc/index.ts, its consumption in runtime code, the applicable rows incordis.patch.ymlandcordis.yml, and both READMEs.
- The durable DSH session event log is the transcript source of truth. Rows are replayed/projected from events; do not insert optimistic assistant or tool facts that can diverge from persistence.
- Preserve event ordering, sequence anchors, and call-ID matching. Rewind, resume, folding, tool result association, and exports depend on them.
- Every observable channel mutation must use the appropriate synchronous or
frame-coalesced emitter so
versionadvances and subscribers are notified. - Keep long-session memory bounded. Do not remove transcript folding, replay coalescing, virtualization, or cache limits without a measured replacement.
- Agent changes such as resume, rewind, model switch, and preset switch must reset all session-scoped projections together. Audit rows, goals, todos, titles, pending messages, metrics, and loaded context for stale state.
- Resolve agent/model/tool/preset capabilities through the mounted DSH services and registries. Do not guess external API shapes; inspect the installed package types when changing an integration.
- Keyboard precedence is behavior, not incidental control flow. A focused questionnaire or modal consumes its keys before global handlers; mouse text selection consumes Escape before rewind/clear behavior; the prompt owns text editing only when no overlay is active.
- Do not hardcode a new shortcut in one component and stop there. Update the relevant help UI and both README shortcut tables, and add or extend a regression for conflicts with existing modes.
- Local slash commands are declared in
src/commands.tsand dispatched inChat.tsx; registry commands are merged at runtime. When adding a command, update declaration, dispatch, help/documentation, the i18n description (cmd-desc-<name>insrc/i18n.ts, zh only — en falls back to the declaration), and any related skill mapping together. - Skill commands stay out of LOCAL_COMMANDS: user-invocable skills discovered by DSH are merged from the registry as dispatch commands. Names must be parseable kebab-case and must not collide with a local command.
- Keep
ask_user_questionserialized throughQuestionStore; concurrent questions are intentionally presented FIFO and summarized after completion.
- Prefer themed primitives and hooks exported by
src/ui.ts. Reach intosrc/ink/only for behavior that the facade intentionally does not expose. - Terminal width is display-cell width, not JavaScript string length. Account for ANSI escapes, combining characters, emoji, and East Asian wide glyphs; use the repository's width, slicing, wrapping, and ANSI helpers.
- Keep frame output buffered and normal runs quiet. Do not add
console.logor stdout diagnostics while the TUI is active. Use an opt-in stderr/debug path such asDSH_TUI_DEBUG, or the existingDSH_TUI_RENDER_LOGframe capture. - Preserve raw-mode, cursor, alternate-screen, synchronized-output, mouse, focus, and terminal-query cleanup on success, error, interrupt, and teardown.
- Avoid render-time unbounded collections or per-token/per-frame allocations. Streaming sessions are long lived, and this repository has explicit regressions for prior OOM and scroll-performance failures.
- Layout changes must not allow transcript content to displace the input and status line. Exercise resize storms, long unbroken content, streaming rows, scrolled-up state, and sticky-bottom restoration when those paths change.
- Keep platform detection narrow. Windows Terminal/ConPTY, WSL, tmux, VS Code, and terminals with or without truecolor/DEC 2026 support follow different protocol paths.
- Follow the existing precedence for configurable preferences: explicit deployment config or environment override, then persisted user choice, then detected/default value. Document any change to that order.
- Persist user data beneath the existing
~/.dsh-tuilocations. Validate and safely parse external JSON; malformed optional state should warn or fall back rather than crash the TUI. - Treat theme names, plugin descriptors, and file contents as untrusted input. Preserve path containment checks, plugin ID constraints, activation cleanup, and all-or-nothing validation of malformed theme files.
- Keep theme additions complete across the
Themecontract and every built-in palette. Runtime themes must use thetuiThemesseam; plugins must not rewrite~/.dsh-tui/themes/or bypass the managed extension service. Use semantic theme keys in components instead of isolated literal colors.
| If you change | Keep these in sync |
|---|---|
| Plugin config or environment behavior | src/index.ts, runtime consumer, cordis.patch.yml, cordis.yml, README.md, README_EN.md |
| Slash commands or shortcuts | src/commands.ts, src/screens/Chat.tsx, help/input components, both READMEs, relevant skill mapping/tests |
| Theme contract, plugin seam, or persisted theme behavior | src/theme.ts, src/themeCatalog.ts, src/dsh-adapter/themes.ts, all palettes, theme provider/picker, custom-theme parser, theme verification, both READMEs, plugin docs |
| Session/channel behavior | src/dsh-adapter/channel.ts, affected UI projections, compiled output, focused channel/replay regression |
| Renderer/layout behavior | src/ink/ or Yoga source, compiled output, CI regressions, focused scroll/resize/PTY probe |
| Skill discovery or presentation | DSH adapter, slash-command merge, /skills, and focused regressions; maintainer-only skills live in .agents/skills/ and must stay out of npm |
| User-facing documented behavior | Chinese and English READMEs, plus config comments/help text where applicable |
| Package version or dependency | package.json, pnpm-lock.yaml, generated/published artifacts as applicable; do not churn the legacy npm lock incidentally |
| Upstream validated-line bump | src/dsh-adapter/contract.ts, both peer and dev ranges in package.json, bundled dsh-auth/package.json and dsh-auth/pnpm-lock.yaml, pnpm-workspace.yaml, the upstream SHA in the alpha-compat job of .github/workflows/ci.yml, the version constants in scripts/verify-{alpha-source,patch-surface,web-coexistence,upstream-contract}, patch-surface.snapshot.json, ADAPTER.md, docs/user-guide.md; steps in the upgrade section of ADAPTER.md |
- The worktree may contain another person's changes. Inspect
git statusand relevant diffs before editing, preserve unrelated changes, and never discard work you did not create. - Do not run destructive cleanup commands such as
git reset --hard,git checkout ., orgit clean -fd. Do not usegit stashto hide another session's work. - Stage explicit paths only; never use
git add .orgit add -Ain a shared worktree. - Do not commit, tag, push, publish, or create a release unless the user asks.
- Publishing is tag-driven.
.github/workflows/publish.ymlrequires av*tag whose version exactly matchespackage.json, then builds, runs focused regressions, and publishes to npm. Treat version changes and tags as release operations, not routine cleanup. - Release notes credit contributors. Create GitHub Releases with
gh release create vX.Y.Z --notes-file notes.md --generate-notes: the hand-written summary comes first, and GitHub appends What's Changed (PR title + author + link), New Contributors, and the Full Changelog;.github/release.ymlexcludes bots from the generated list. In the hand-written summary, entries from external contributors end with(#PR by @user); the maintainer's own entries are unmarked. Write bare#123and@user— GitHub renders them as links. - Before handing off a code change, inspect
git diff --check, the source diff, the generated diff, andgit status. Report exactly which verification ran and any platform or credential-dependent checks that could not run.