Skip to content
Merged
2 changes: 2 additions & 0 deletions .mstar/knowledge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
|----------|--------|-------------|--------|
| `developer-experience/dsh-standalone-plugin-dev.md` | standalone bundle bring-up (peer-stubs → link farm → registry peers) | Standalone dsh plugin bundle development against private @deepseek-ai packages: registry peer resolution via autoInstallPeers (superseding the link farm, peer-stubs, and gitignored shim overlay), prepare-based git-URL installs, plural inject names, install smoke | active |
| `developer-experience/pnpm11-workspace-config-and-windows-link-farm.md` | PR #11 (fix/install: Windows + pnpm 11) | pnpm 11 ignores non-auth .npmrc settings (move autoInstallPeers/nodeLinker/allowBuilds to pnpm-workspace.yaml), peer ranges must match prerelease tags (^0.0.1 vs 0.0.1-rc.1), Windows-safe link farm (junction/file per target, USERPROFILE fallback, separator normalization) | superseded (registry peers) |
| `developer-experience/dsh-tui-verification-and-host-boot-repair.md` | iteration:iter-20260816-dsh-advisor-n8 QA | dsh profile boot repair (node-addon-require-builtin platform binding missing → all boots fail with cordis-plugin-loader ERR_MODULE_NOT_FOUND while --dump-config still works; repair = global dsh reinstall + relink binding) + reliable interactive TUI verification (deterministic evidence + direct PTY probe + documented human spot-check residual; subagent PTY loops are unreliable) | active |
| `architecture-patterns/omp-advisor-dsh-port.md` | core MVP port | omp advisor → dsh mechanism map (cursor/delta/guard/delivery/failure) + MVP decisions + accepted gaps | active |
| `architecture-patterns/dsh-plugin-client-half.md` | client half + settings section work | dsh web client half for a standalone plugin: dsh.client declaration (nested under dsh, post-20da39e), closure-factory CJS bundle contract (frozen externals/purity/automatic JSX), CSS-modules inline injection + style-tag lifecycle + bundle hygiene, settings.section slot registration (legacy — the advisor's configuration surface is now the settings.plugin.item card, see dsh-plugin-config-card-surface.md), settings namespace wiring | active |
| `architecture-patterns/dsh-plugin-config-card-surface.md` | iteration:iter-20260811-dsh-advisor-n6/guides/plugin-config-migration.md | dsh web "插件配置" page card surface: the settings.plugin.item card slot (declared by the ui-plugin-config settings.section id 'plugins'), generator + yield registration with locale / business-only inject faces, PropsRuntime + PropsLocale + InjectFace contract, type-only peer dependency, load-on-mount invariant, settings-scope vs GatewayService data-channel routes, CSS-fragment build discipline — the advisor's current configuration surface (supersedes the settings.section recipe) | active |
| `architecture-patterns/dsh-auxiliary-model-start-profile.md` | dsh-advisor-minimal-start | dsh auxiliary model calls (advisor KD-6): minimal closed-whitelist GenerateOptions (zero tools, literal-pinned caps) + capability-gated thinking-off with failure-retry caching discipline (never cache a failed capability lookup; throw and deadline abort are one failure class) | active |
| `architecture-patterns/dsh-tui-plugin-client-surface.md` | iteration:iter-20260816-dsh-advisor-n8 | dsh-TUI plugin client surface with zero dsh-TUI changes: bundle composition into the dsh-tui profile, DSH command-registry auto-merge into the TUI / menu, the tuiCommandTrees plugin-facing seam (structural TuiCommandTreeProvider types — no @deepseek-harness-tui peer), no-settings-page constraint (namespace + profile patch + global settings.yaml), and the session-less config-readback parity rule (readback resolves the composed config like the web gateway, never the per-session override) | active |
| `architecture-patterns/dsh-settings-exposure-boundary.md` | settings exposure work (patch retirement + gateway channel) | dsh host settings exposure boundary — NO registration-level opt-in exists upstream (verified at pristine 20da39e); the working fix is the official GatewayService RPC channel (`/api/<ns>/<method>`, typertGateway claims, in-process settings.update un-gated) — web section reads/writes through it, bypassing the allowlist; no host patch; circular-verification trap documented; **SRC `@Remote` claims fail for locally-linked plugins under a dlx host (module-private marker table, physically separate peers) — explicit `ctx.typert.register(contribution)` is the module-identity-proof path (2026-08-13)** | active |
| `workflow-patterns/dsh-host-dispatch-concurrency.md` | core MVP port | dsh same-step tool-call scheduling: subagent calls are exclusive (serial) — isConcurrencySafe fail-closed | active |
| `workflow-patterns/dsh-upstream-bump-adaptation.md` | upstream bump + patch retirement | Surviving a dsh snapshot upgrade as a plugin bundle: probe discriminators (present/absent), dshClient → dsh.client migration (no fallback, negative-verdict cache), restart + runtime verification sequence; host-patch mechanism retired; host tree must stay pristine (staging worktree edits are the same class as the retired patch) | active |
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ dsh plugins that make **auxiliary** model calls (advisor reviewer, future subage

Two invariants, both regression-pinned at the single builder seam:

1. **Minimal request shape — closed whitelist.** Every auxiliary `ctx.llm.stream` call builds `GenerateOptions` in ONE function (`buildOptions`) whose key set is exactly `['maxTokens','messages','model','provider','reasoningEffort','signal','system']` (minus `reasoningEffort` when the model does not advertise it). No `tools` (the wire field is omitted by both dsh adapters when unset — verified deepseek `serialize.ts`, pi-ai `context.ts`; no stock `llm/stream` middleware injects tools). Tests assert `Object.keys(options).sort()` equality with a **hardcoded literal** (not derived from the code under test) plus literal `5120` for the frozen token cap — so any new key or value drift breaks loudly. Pin the plugin control surface (`GenerateOptions`), never adapter wire JSON.
1. **Minimal request shape — closed whitelist.** Every auxiliary `ctx.llm.stream` call builds `GenerateOptions` in ONE function (`buildOptions`) whose key set is exactly `['maxTokens','messages','model','provider','reasoningEffort','signal','system']` (minus `reasoningEffort` when the model does not advertise it). No `tools` (the wire field is omitted by both dsh adapters when unset — verified deepseek `serialize.ts`, pi-ai `context.ts`; no stock `llm/stream` middleware injects tools). Tests assert `Object.keys(options).sort()` equality with a **hardcoded literal** (not derived from the code under test) plus literal `768` for the frozen token cap — so any new key or value drift breaks loudly. Pin the plugin control surface (`GenerateOptions`), never adapter wire JSON.

2. **Thinking-off is capability-gated, never unconditional.** Sending `reasoningEffort` to a model without reasoning metadata throws `UNSUPPORTED_REASONING_EFFORT` from `LlmRuntime.resolveCallFor` and silently kills the auxiliary caller for non-deepseek models. Resolve capabilities via `ctx.llm.resolveModelInfo(provider, model, signal)` and send `'off'` **only** when `reasoning.efforts` advertises it (DeepSeek wire: `'off' → thinking: {type:'disabled'}` — host behavior, not plugin config).

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
module: dsh plugin TUI client surface (dsh-tui)
date: 2026-08-16
problem_type: architecture_pattern
category: architecture-patterns
severity: medium
title: Adding a plugin client surface to the dsh-TUI terminal front door (tuiCommandTrees + session-less config readback)
description: Verified pattern for surfacing a standalone dsh plugin inside the dsh-TUI terminal front end with zero dsh-TUI changes — bundle composition into the dsh-tui profile, DSH command-registry auto-merge into the TUI / menu, the plugin-facing tuiCommandTrees seam (register a TuiCommandTreeProvider for localized descriptions + subcommand completion), the no-settings-page constraint (settings = settings namespace + profile patch + global $DSH_HOME/settings.yaml), and the session-less config-readback parity rule (a readback command must resolve the composed config exactly like the web gateway — never the per-session override).
last_updated: 2026-08-16
tags:
- dsh
- plugin
- dsh-tui
- client
---

# Adding a plugin client surface to the dsh-TUI terminal front door

## Context

dsh-TUI (`@deepseek-harness-tui/dsh-tui`, profile `dsh-tui`, launcher `bin/dsh-tui.js` self-bootstraps `dsh --profile dsh-tui add @deepseek-harness-tui/dsh-tui@<ver>` then spawns `dsh --profile dsh-tui`) is a terminal-only Ink/TUI front door over dsh-base. It renders NO web client bundles, has NO settings page, NO typert gateway, and NO generic plugin settings UI. Verified against source @ 557a27a (2026-08-16).

The dsh-advisor plugin (a per-session reviewer) needed a first-class TUI surface: commands discoverable in the `/` menu + a settings readback, without modifying the dsh-TUI repo and without adding a dependency on it.

## Guidance

### 1. Bundle composition needs zero dsh-TUI changes

`dsh plugin --profile dsh-tui add <pkg>` reads the package's `dsh.bundle.patch` (package.json `dsh` → `cordis.patch.yml`), appends its `- insert:` rows as a composition layer (dsh-base → bundles → bundle patches → user patch layer `~/.dsh/profiles/dsh-tui/cordis.patch.yml`). The advisor's `- insert: id: advisor` row lands in the profile with no host edits. `dsh --profile dsh-tui --dump-config` shows the row (composition-only, works even when the full plugin-tree boot is broken).

### 2. Commands auto-surface in the TUI `/` menu

The TUI merges the DSH command registry into its `/` menu (`refreshCommandList` in `src/dsh-adapter/channel.ts`: `commandService.list(target)` → merged rows; dispatch via `commandService.execute`). A plugin's registry commands (`ctx.inject(['commands'], ...)`) appear automatically. The row's `tag` comes from `CommandDefinition.input.hint` — keep it in sync when adding subcommands.

### 3. The plugin-facing TUI seam is `tuiCommandTrees`

`ctx.tuiCommandTrees` (cordis Service, row `dsh-tui-command-trees` — shipped in the dsh-tui bundle) lets plugins register:

```ts
interface TuiCommandTreeProvider {
root: string // '^[a-z][a-z0-9_-]*$'; duplicate root throws
descriptions?: LocalizedDescriptions // Readonly<Partial<Record<'zh'|'en', string>>>
children(canonicalPath: readonly string[]): readonly CommandCompletionNode[] // root at index 0
}
interface CommandCompletionNode {
name: string; aliases?: readonly string[]; description: string
descriptions?: LocalizedDescriptions; tag?: string; descriptionKey?: string
}
```

`descriptions(root)` overrides the root row's description; `children` drives `/` overlay completion (leaves return `[]` — the TUI asks at depth 2). **Do NOT add `@deepseek-harness-tui/dsh-tui` as a dependency** — the shapes are small and structural; replicate them locally (zero new peers). The cordis Context lacks the `tuiCommandTrees` augmentation outside dsh-TUI — use a structural cast with a conditional `ctx.inject(['tuiCommandTrees'], ...)` (absent service → clean no-op, same pattern as settings/typert/commands children). Register behind any single-instance claim (the advisor's `claimReviewer()`) and defensively catch duplicate-root ('already registered' → debug log + no-op disposer) — the multi-fiber composition that affects sibling optional registrations applies here too.

### 4. The TUI has no settings page — settings surface = namespace + readback + docs

No plugin settings UI seam exists (filed upstream: ccch1mneyyy/dsh-TUI#165). The working surface is:
- the plugin's settings namespace (registers via the dsh settings service; reads the same live composed config),
- operator edit paths: profile patch layer (`~/.dsh/profiles/dsh-tui/cordis.patch.yml`) + the GLOBAL `$DSH_HOME/settings.yaml` (shared across ALL profiles — the web Settings card writes the same user layer),
- a read-only readback command (`/advisor config`) rendering the composed config + edit hints.

### 5. Session-less config readback parity (correctness rule)

A settings-readback command MUST read the composed config exactly like the web gateway (`/api/advisor/get`): resolve the bridge source through the shared resolver (`resolveAdvisorConfig`), with **no session context**. Never route the readback through the per-session effective config (`effectiveConfig`/`safeEffective`) — those bake the `/advisor off` session toggle into `enabled`, so a user who turns the advisor off for the session would see the readback misreport the persisted settings (web-vs-TUI divergence). Runtime state (on/off, pending, last activity) stays in the status command; config state stays in the config command. Containment: when the resolver throws on a rejected settings user layer, seed the readback's scalar latches from the RAW source (`raw?.immuneTurns ?? 3`, ...) — mirroring the gateway's S1 fallback — so both front ends report the same values.

## Why This Matters

- A terminal front door and a web front door share one config SSOT; readback parity prevents "the TUI says 3, the web card says 5" confusion for the same settings.yaml.
- The `tuiCommandTrees` seam is the entire plugin-facing UI surface of dsh-TUI today — knowing it means future TUI work (e.g. the post-#165 write surface) starts from the right contract instead of re-deriving it from the host source.
- Zero new peers keeps the plugin's dependency contract intact (mount-only, public-registry peers only).

## When to Apply

- Adding or maintaining ANY plugin surface in a dsh-TUI profile (commands, completion, settings readback).
- The upstream settings-seam work (ccch1mneyyy/dsh-TUI#165): when dsh-TUI gains a settings UI, the advisor's TUI write surface should reuse the same composed-config resolver + namespace, adding a write path on top of this readback.

## Examples

- dsh-advisor iter-20260816-n8: `src/tui.ts` (structural TuiCommandTreeProvider for `/advisor` with zh/en descriptions + on|off|status|config completion), `src/commands.ts` + `src/index.ts` (`/advisor config` — `AdvisorComposedConfig` built from `safeResolved()`, session-less; `safeFallback` seeds scalars from the raw source; `input.hint` `'[on|off|status|config]'`), README dsh-tui profile section, `dsh --profile dsh-tui` live QA (dump-config + PTY boot + `/advisor status|config` rendering).
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ omp's advisor attaches an independent reviewer model to a session: after each pr
| turn-end hook (`setOnTurnEnd`) | session/event listener; stepped turn/end detection (`findLastMessageTurnEnd` semantics); filter reason kinds completed / max-tokens / error |
| cursor + delivered-prefix fingerprints | per-session cursor + message fingerprints; prefix rewrite (compact events, surface replace ops) → reset + full replay of the post-rewrite transcript; seed-to-length on mid-session enable |
| delta renderer (role labels, own-message exclusion) | markdown with `**user:**` / `**agent:**` labels; advisor's own injected messages excluded via a custom merge-extensible source kind (`advisor`) — the self-review guard; bounded delta window (default 60 messages, 0 = unbounded) with a truncation marker |
| advise tool + canned `Recorded.` | MVP: JSON-framed note + severity reply (first balanced `{...}`; empty note dropped; missing severity → nit; no parse retry; output cap `ADVISOR_MAX_TOKENS = 5120` since n4, 2026-08-11 — KD-2's original 256 was superseded 20× and frozen as KD-6) — no tool loop |
| advise tool + canned `Recorded.` | MVP: JSON-framed note + severity reply (first balanced `{...}`; empty note dropped; missing severity → nit; no parse retry; output cap `ADVISOR_MAX_TOKENS = 768` since 2026-08-16 — KD-2's 256 → 5120 supersession chain settled at 768 with thinking-off default, frozen as KD-6) — no tool loop |
| emission guard | normalize (NFKC → lowercase → non-alnum runs → single space), content-free phrase suppression, exact-text dedupe (FIFO, 4096), one-note-per-update, severity escalation (nit→concern→blocker allowed, equal/lower suppressed) |
| delivery routing | nit → inject (non-waking, next pre-step); concern/blocker → steer (waking); immuneTurns cooldown (default 3) after a delivered interrupt downgrades later interrupting notes to inject; messages carry `[advisor:{severity}]` text + the advisor source kind |
| backlog/catch-up/failure | async drain per session; retry once + backoff → drop; 3 drops → flush backlog; quota → paused (batch retained, no auto-resume); permanent errors → halted; **call-level deadline** (dsh-timeout, default 60 s) so a hung stream cannot wedge the drain; never parks the primary |
Expand Down
Loading
Loading