diff --git a/ADAPTER.md b/ADAPTER.md index 37d994f61..e098a2d56 100644 --- a/ADAPTER.md +++ b/ADAPTER.md @@ -17,6 +17,8 @@ | UI 层(`screens/`、`components/`、`hooks/`、`ink/`) | 不 import `src/backends/`;从 `src/dsh-adapter/` 只取类型,运行期经 facade(`src/dsh-adapter/types.ts` 的类型 re-export、`channel.ts`/`plugin.ts` 提供的服务)接触上游。存量值 import 登记在 `scripts/adapter-boundary.allowlist.json`,只减不增 | | `native.dsh` | 只允许 `src/dsh-adapter/` 内访问 | | `native.codex` | 只允许 `src/backends/codex/` 内访问 | +| `'@deepseek-ai/…'` 字符串字面量 | 只在 `src/dsh-adapter/` 内出现(运行期拼出来交给 `createRequire` / `import()` 的说明符也算) | +| 宿主专属模块(`host-contract.ts` 的 `HOST_MODULES`,cordis 除外,含 `@deepseek-ai/dsh` 本身) | 任何地方不得值 import;只由 `src/dsh-adapter/host-dsh.ts` 按宿主 realpath 动态加载,说明符只写在 `host-contract.ts`;`host-dsh.ts` 对 `@deepseek-ai/*` 只能 `import type`;`host-contract.ts` 只给 `host-dsh.ts` 与 `contract.ts` import | ### 后端 manifest @@ -94,6 +96,30 @@ manifest 一致,过期即红)。 - 白名单包:blessed list(harness 包按完整版本号校验,框架包 cordis/schemastery 按 major 校验) - 启动时:检测到 drift 打 warning;CI 上 `pnpm run verify:contract` 直接失败 +## 独立入口的宿主契约 + +本包入口(`src/dsh-adapter/host-entry.ts`)在进程内加载 PATH 上 `dsh` 的安装 +(docs/standalone-host-design.md),契约单一来源是 +`src/dsh-adapter/host-contract.ts`: + +- **依赖**:`host-dsh.ts` 取类型的 `dsh-app-boot`、`dsh-cmdline`、`dsh-home-paths`、 + `dsh-http-proxy`、`dsh-launch-environment`(`HOST_TYPE_PACKAGES`)是 optional peer + dev、 + 进 blessed 清单。宿主 CLI `@deepseek-ai/dsh` **不是**依赖(它的依赖树是整个 CLI): + `host-dsh.ts` 本地声明入口读取的 `profile-boot` 三个导出。运行期**不**从本包解析 + 它们:入口按宿主 realpath 加载宿主自己的副本(profile 由宿主 dsh 安装、cordis 必须单实例), + 所以它们也在运行期可缺的集合里(缺席不算 drift)。新包范围只写已核对的 `0.2.0-rc.2` + (`dsh-home-paths` 沿用家族宽范围)。`cordis-plugin-loader` 只当 `unknown` 用,不声明 peer。 +- **能力探测**:`HOST_MODULES` 列出 8 个宿主模块与入口读取的每个导出。`loadHostDsh` 逐项检查, + 缺模块 / 缺导出即抛出点名原因,入口回退(DSH 内核交给 `dsh --profile`,Claude 内核无宿主解析) + 并把原因写 stderr 与界面通知。`host-dsh.ts` 用 `Pick` 取类型 + (`profile-boot` 除外,本地声明),契约写了 pinned 宿主不存在的导出时编译失败。 +- **复刻面**:`HOST_REPLICAS` 列出入口复刻的上游符号,`host-replica.snapshot.json` 记录在 + `HOST_REPLICA_VERSION` 上声明各符号的 lib 文件的整文件 sha256(信号偏粗:该文件任何改动都要求复核); + 刻意的偏差逐条记在 `HOST_DEVIATIONS`(同文件),审查复刻改动时以它为准。 +- **门禁**:`verify:contract` 额外跑 `scripts/verify-host-contract.ts`(覆盖面见其头注释):假宿主回退 + 在 CI 上总跑;生产探测与复刻指纹只在有已装宿主(PATH 上的 `dsh` 或 `DSH_TUI_CONTRACT_DSH`)时跑。 + 移动版本线时在装有该版本 `dsh` 的机器上复核、把变化搬进 `host-dsh.ts`,再用 `--snapshot` 重写快照。 + ## Patch Surface `cordis.patch.yml` 里对官方行的干预已快照到 `patch-surface.snapshot.json`: @@ -128,6 +154,8 @@ web-app patch 按 include 语义合成一遍,直接拦截 loader entry id 复用 - dev 树由 `pnpm-workspace.yaml` 的 overrides 钉在 `0.2.0-rc.2`, CI `alpha-compat` lane 还对同版上游 tag 的固定 SHA 做源码类型与 patch 合成校验。 旧 SQLite 迁移工具的依赖闭包单独锁在 `vendor/sqlite-island`。 +- 主验证线移动时 `verify:contract` 会要求复核独立入口的复刻面(见「独立入口的宿主契约」), + `HOST_REPLICA_VERSION` 与主验证线一起改。 - `contract.ts` 是唯一真源:主验证线原地替换、不累积;`package.json` 的 peer/dev 范围、CI 钉住的上游 SHA、校验脚本里的版本常量都只是它的镜像, 必须同一次改齐(位置见 [docs/contributing.md](docs/contributing.md) 跨文件清单)。 diff --git a/AGENTS.md b/AGENTS.md index 638748adc..9c003a33d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,13 @@ # AGENTS.md -dsh-TUI 是 DeepSeek Harness 的终端界面插件:零核心改动、纯插件挂载的交互式 TUI(`@deepseek-harness-tui/dsh-tui`)。Agent、会话、模型、工具、持久化与策略域由 DeepSeek Harness 拥有,本包只消费它们。改动前先读 [docs/contributing.md](docs/contributing.md)(本仓库共享开发契约的权威文本)与 [ADAPTER.md](ADAPTER.md)(上游边界与契约);整体结构见 [docs/architecture.md](docs/architecture.md)。 +dsh-TUI 是 DeepSeek Harness 的终端界面应用(`@deepseek-harness-tui/dsh-tui`),零核心改动、只消费 DSH 的公开导出。DSH 是首要适配目标与首方后端;本包正从「DSH 的插件」演进为「拥有自身入口与组装根的终端应用」,届时 DSH 与 claude / codex 一样作为后端按需在进程内加载。Agent、会话、模型、工具、持久化与策略域仍然由 DeepSeek Harness 拥有,本包只消费它们。改动前先读 [docs/contributing.md](docs/contributing.md)(本仓库共享开发契约的权威文本)与 [ADAPTER.md](ADAPTER.md)(上游边界与契约);整体结构见 [docs/architecture.md](docs/architecture.md);独立宿主的方案与非目标见 [docs/standalone-host-design.md](docs/standalone-host-design.md)。 ## 仓库布局 ``` src/index.ts 公共 Cordis 插件入口、配置 Schema、对运行时实现的惰性移交 src/dsh-adapter/plugin.ts 运行时实现:TTY 校验、服务注册、Agent 创建/恢复、React 树挂载与收尾 +src/dsh-adapter/host-entry.ts 本包入口:非 DSH 内核不组合完整 DSH profile(改组合轻量 profile:本包的行 + profile 声明的第三方插件)、在裸 Cordis 根上挂运行时;路由判定在 src/hostEntryRoute.ts(docs/standalone-host-design.md) src/dsh-adapter/channel.ts Channel 入口:后端中立核心(channel/core/)+ 仅 DSH 会话挂载的扩展(channel/extensions.ts) src/agent/ 后端中立的会话领域:AgentEvent、AgentSession、类型化能力(无 I/O、无厂商依赖) src/channel/ 共享投影器(AgentEvent → 视图状态)与审批/问卷等中立 store @@ -22,7 +23,7 @@ src/ink/ Ink 系渲染器与终端实现——敏感基础设施, src/native-ts/ 渲染器使用的 Yoga 布局引擎 src/terminal-utils/ 终端格式化与呈现辅助 src/dsh-adapter/ 唯一允许 import 官方 @deepseek-ai/* 的位置;themes.ts 提供 tuiThemes 插件接缝 -src/*Prefs.ts 等 ~/.dsh-tui 下的持久化用户偏好与会话元数据 +src/*Prefs.ts 等 ~/.dsh-tui 下的持久化用户偏好与会话元数据;src/tuiSettingsFile.ts 是 /settings 的 dsh-tui 分区(settings.json) .agents/skills/ 仅供仓库维护者使用的项目技能,不随 npm 包分发 presets/ 随包分发的 preset(liangshen) bin/dsh-tui.js dsh-tui 直达命令入口 @@ -57,6 +58,7 @@ pnpm smoke # 通用无头屏幕组装冒烟 - 厂商包按目录隔离:`@deepseek-ai/*` 只在 `src/dsh-adapter/`,`@anthropic-ai/*` 只在 `src/backends/claude/`;后端中立层 `src/agent/`、`src/channel/` 不 import 厂商包、`src/dsh-adapter/` 与 `src/backends/`;UI 层不 import `src/backends/`,从 `src/dsh-adapter/` 只取类型(存量值 import 的 allowlist 只减不增)。完整规则表见 [ADAPTER.md](ADAPTER.md);`pnpm run verify:boundary` 扫描全部源码,越界即失败。 - 校验版本线、peer 范围与 blessed 包清单在 `src/dsh-adapter/contract.ts`;本地检测到 drift 打警告,CI 上 `verify:contract` 直接失败。 +- 本包入口按宿主 realpath 加载已装 `dsh` 的模块:清单、复刻面与偏差的唯一来源是 `src/dsh-adapter/host-contract.ts`,加载只在 `host-dsh.ts`(对 `@deepseek-ai/*` 只 `import type`);`verify:contract` 跑假宿主回退,有已装宿主时再跑能力探测与复刻指纹(`host-replica.snapshot.json`);宿主 CLI 不是依赖,版本线移动时在装有该版本 `dsh` 的机器上复核。见 [ADAPTER.md](ADAPTER.md)「独立入口的宿主契约」。 - 运行时或发布类型引用的 `@deepseek-ai/*` 框架包必须同时是 peer 与 dev 依赖(`verify:manifest-deps` 门禁);仅测试/脚本使用的框架包只进 dev 依赖。 - `cordis.patch.yml` 对官方行的干预已快照到 `patch-surface.snapshot.json`,改动需保持同步(`verify:patch-surface` 门禁)。 diff --git a/README.md b/README.md index 98e4469de..d0641209d 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,12 @@ # dsh-TUI -> An interactive terminal UI plugin for DeepSeek Harness. It ships a -> pixel-whale header, live work status, streaming thinking, double-Esc time -> rewind, a context progress bar, and a TPS gauge. It mounts as a pure plugin, -> with no core changes. Install to enable; uninstall leaves no patches behind. +> An interactive terminal UI for DeepSeek Harness. It ships a pixel-whale +> header, live work status, streaming thinking, double-Esc time rewind, a +> context progress bar, and a TPS gauge. Zero core changes: it consumes only +> DSH's public exports, and is moving from a mounted plugin to an app with its +> own entry and composition root, where DSH loads in process as the first-party +> backend. Install to enable; uninstall leaves no patches behind. ## Highlights @@ -104,6 +106,10 @@ IDs. It requires matching profile dependencies with `@deepseek-ai/schemastery` 3.18.3 or newer; an incompatible schema stops TUI startup with repair guidance instead of showing an uneditable settings page. Older hosts keep their legacy settings scope. +The `dsh-tui` section of `/settings` is saved in `~/.dsh-tui/settings.json`, shared by +both kernels; the first launch imports it once from the profile's `cordis.patch.yml` +(see [configuration](docs/configuration.en.md#tui-configuration)). + ```sh # Install the CLI and this plugin globally (ships the dsh-tui command) npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui @@ -215,6 +221,11 @@ including clicking outside it on the launchpad, cancels a pending installation. - **Not available**: DSH-only commands such as `/tree`, `/preset`, `/provider`, `/workspace`, `/agentview` and `/bg`. One process runs one backend; `/kernel` switches by restarting into a new session. +- **Startup**: the screen appears before the session opens, with only a light + profile composed (this package plus the profile's third-party plugins); see + [Claude backend](docs/claude-backend.en.md#known-limitations). The DSH kernel + starts from the same entry and loads DSH in the same process behind its first + screen; `DSH_TUI_HOST_ENTRY=0` restores the old launch path for every kernel. Details and known limitations: [Claude backend](docs/claude-backend.en.md). diff --git a/README_ZH.md b/README_ZH.md index 82c14bb01..ad58412a1 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -18,8 +18,8 @@ # dsh-TUI -> 面向 DeepSeek Harness 的交互式终端界面插件:像素鲸鱼顶栏、实时工作状态、流式思考展示、双击 Esc 时间回溯、上下文进度条与 TPS 仪表。 -> 零核心改动,纯插件挂载。安装即启用,卸载不留核心补丁。 +> 面向 DeepSeek Harness 的交互式终端界面:像素鲸鱼顶栏、实时工作状态、流式思考展示、双击 Esc 时间回溯、上下文进度条与 TPS 仪表。 +> 零核心改动,只消费 DSH 的公开导出;正从「挂载型插件」演进为拥有自身入口与组装根的终端应用,届时 DSH 作为首方后端在进程内加载。安装即启用,卸载不留核心补丁。 ## 功能亮点 @@ -93,6 +93,9 @@ profile 依赖须配套,包含 `@deepseek-ai/schemastery` 3.18.3 或更新版 Schema 不兼容时,TUI 在启动阶段报错并提示修复安装,不再显示不可编辑的设置页。 旧 host 继续使用原有设置 scope。 +`/settings` 的 `dsh-tui` 分区保存在 `~/.dsh-tui/settings.json`,两个内核共用;第一次启动时 +从 profile 的 `cordis.patch.yml` 一次性导入(见[配置参考](docs/configuration.md#tui-配置))。 + ```sh # 安装(全局,自带 dsh-tui 命令) npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui @@ -182,6 +185,9 @@ Enter,按引导一键安装——dsh-TUI 自己定位 profile 目录并装锁 双击 `Esc` 回退、子代理、后台任务、图片、`/btw`,以及 Claude 上报的美元费用。 - **不可用**:DSH 专属命令,如 `/tree`、`/preset`、`/provider`、`/workspace`、 `/agentview`、`/bg`。一个进程只跑一个后端,`/kernel` 切换时会重启并开新会话。 +- **启动**:界面先于会话出现,只组合轻量 profile(本包与 profile 声明的第三方插件), + 见 [Claude 后端](docs/claude-backend.md#已知限制)。DSH 内核从同一入口启动,界面先出现, + DSH 在同一进程里加载;`DSH_TUI_HOST_ENTRY=0` 让所有内核恢复原来的启动方式。 详细说明与已知限制:[Claude 后端](docs/claude-backend.md)。 diff --git a/bin/dsh-tui.js b/bin/dsh-tui.js index 456febbaa..2e5a65676 100755 --- a/bin/dsh-tui.js +++ b/bin/dsh-tui.js @@ -28,7 +28,7 @@ * `DSH_TUI_LANG` 显式指定时从其值,否则默认中文(同 src/i18n.ts 的缺省)。 */ import { spawn, spawnSync } from 'node:child_process' -import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs' +import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs' import { homedir } from 'node:os' import { dirname, isAbsolute, join, resolve } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' @@ -118,6 +118,26 @@ const shellOpt = isWin ? { shell: true } : {} const cmd = (command, args) => isWin ? [`${command} ${shellQuote(args).join(' ')}`, []] : [command, args] +// dsh CLI 预检(`dsh --version`),异步起、不占关键路径:本包入口不需要 dsh CLI, +// 只有 `dsh --profile` 出口(`requireDsh()`)与入口失败后的提示判定才消费结果。 +// **不能** unref:`requireDsh()` await 它时若这是唯一活跃句柄,Node 会把顶层 await +// 判成永不结算、以 exit 13 终止启动器。 +let dshProbe +const probeDsh = () => { + dshProbe ??= new Promise(resolve => { + const child = spawn(...cmd('dsh', ['--version']), { stdio: 'ignore', ...shellOpt }) + child.on('error', () => resolve(false)) + child.on('exit', code => resolve(code === 0)) + }) + return dshProbe +} + +const requireDsh = async () => { + if (await probeDsh()) return + console.error(msg('noDsh')) + process.exit(1) +} + // 内联 semver(解析 + 严格大于):启动器可能在依赖不完整的环境里被执行 // (迁移、半损坏安装、测试沙箱),零外部依赖是自保底线。覆盖 semver 的 // 核心-先行版比较规则:先行版标识符逐段比(数字段按数值、小于字母段), @@ -787,6 +807,34 @@ const startDshSession = (dshArgs, profile = PROFILE, env = process.env) => }) }) +// entry 子进程的 Node 编译缓存:纯 env 注入,替身进程继承。用户显式设过 +// `NODE_COMPILE_CACHE`(含空串)不覆盖。 +// 路径沿用 `join(homedir(), '.dsh-tui', …)` 写法,**不要**抽成共用的模块级常量: +// verify-safe-mode.mjs 把 readLastRunRecord 至「TTY 判定」注释之间的源码切进只注入 +// 少数全局的 vm 沙箱,区间内引用的外部常量会是 `undefined`,套件静默转红。 +// (别把那两处切片标记原文抄进注释:会让脚本的 indexOf 提前命中。) +const withCompileCache = env => { + if (env.NODE_COMPILE_CACHE !== undefined) return env + const cacheDir = join(homedir(), '.dsh-tui', 'compile-cache') + try { + mkdirSync(cacheDir, { recursive: true, mode: 0o700 }) + } catch { + return env + } + return { ...env, NODE_COMPILE_CACHE: cacheDir } +} + +// 本包自己的入口:`node <入口> <应用参数>`。结果模型与 startDshSession 相同。 +const startEntrySession = (entry, appArgs, env = process.env) => + new Promise(resolve => { + const child = spawn(process.execPath, [entry, ...appArgs], { stdio: 'inherit', env: withCompileCache(env) }) + child.on('error', err => resolve({ kind: 'error', error: err })) + child.on('exit', (code, signal) => { + if (signal) resolve({ kind: 'signal', signal }) + else resolve({ kind: 'exit', code: code ?? 0 }) + }) + }) + // 救援子进程的环境:显式构造,而不是把宿主 process.env 原样交给它。救援的 // 语义是「干净冷启动」,而启动器自己写进 process.env 的会话控制变量会把刚 // 崩掉的主 profile 的会话 id / 工作区目标带进救援——救援 profile 里并不存在 @@ -1449,14 +1497,8 @@ if (!runningInsideProfile && ownVersion !== undefined && process.env.DSH_TUI_NO_ forwardExit(child) } else { // ─── profile 副本(或源码运行):完整启动逻辑 ───────────────────────────── - // dsh CLI 预检(缺失时给安装指引,先于一切 profile 逻辑)。 - { - const probe = spawnSync(...cmd('dsh', ['--version']), { stdio: 'pipe', ...shellOpt }) - if (probe.error || probe.status !== 0) { - console.error(msg('noDsh')) - process.exit(1) - } - } + // dsh CLI 预检:异步起,结果在要起 dsh 的出口才消费(见 probeDsh)。 + void probeDsh() let installedVersion try { @@ -1611,7 +1653,28 @@ if (!runningInsideProfile && ownVersion !== undefined && process.env.DSH_TUI_NO_ // DSH consumes its own --; only the app tail belongs behind it. Preserve // the app-level separator too, and replay this same argv on a safe retry. const firstArgs = [...hostArgs, ...(args.length > 0 ? ['--', ...args] : [])] + + // 内核分流(docs/standalone-host-design.md):默认所有内核都走本包入口,由入口 + // 判定内核(src/hostEntryRoute.ts)。DSH_TUI_HOST_ENTRY=0 让所有内核回到 + // `dsh --profile`;dsh 自己的 hostArgs(--version、--dump-config* 等)始终交给 dsh。 + const hostEntry = join(ownDir, 'lib', 'types', 'dsh-adapter', 'host-entry.js') + const hostEntryEnabled = process.env.DSH_TUI_HOST_ENTRY !== '0' && existsSync(hostEntry) // 必须在首次 spawn 之前:本次启动的 TUI 写的记录都晚于这个时刻。 noteLaunchChain() - settleFirstResult(await startDshSession(firstArgs), firstArgs) + if (hostEntryEnabled && hostArgs.length === 0) { + // 替身进程经它重起(src/update.ts restartArgv)。只在入口路线设置:带 hostArgs + // (如 --patch)的 `dsh --profile` 启动重起时须原样重放整条 dsh argv。 + process.env.DSH_TUI_HOST_ENTRY_PATH = hostEntry + process.env.DSH_TUI_PROFILE ??= PROFILE + // The in-process DSH kernel reads the bundled guide skills like `dsh` does. + const result = await startEntrySession(hostEntry, args, withGuideSkillDir(process.env)) + // 没有 dsh CLI:安全模式与排查提示都指向不存在的 `dsh --profile`(DSH 内核下 + // 入口已打印 noDsh),原样退出。 + if (result.kind === 'exit' && result.code !== 0 && !(await probeDsh())) process.exit(result.code) + settleFirstResult(result, firstArgs) + } else { + // `dsh --profile` 出口:hostArgs,或入口被关闭的非默认路径。 + await requireDsh() + settleFirstResult(await startDshSession(firstArgs), firstArgs) + } } diff --git a/docs/README.md b/docs/README.md index b1d0895f5..6dd535935 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,6 +33,7 @@ The root README lists what ships; the details live here. Chinese files have no s | --- | --- | --- | --- | | 架构与限制 / Architecture & limitations | [architecture.md](architecture.md) | [architecture.en.md](architecture.en.md) | 运行链路、性能、安全边界与已知限制。 | | 会话挂载运行时 / Session mount runtime | [session-mount-runtime.md](session-mount-runtime.md) | [session-mount-runtime.en.md](session-mount-runtime.en.md) | 多 TUI 占用规则与本机账本。 | +| 独立宿主设计 / Standalone host design | [standalone-host-design.md](standalone-host-design.md) | — | 本包自持入口与组装根、DSH 进程内加载的方案与决策。 | | 多后端架构 / Agent backends | [agent-backend-design.md](agent-backend-design.md) | — | 后端中立层、DSH/Claude/Codex 的实现与接入新后端。 | | Codex 后端方案 / Codex backend design | [codex-backend-design.md](codex-backend-design.md) | — | Codex(app-server)原生后端的技术方案与分期实施手册。 | | Codex 后端交接 / Codex backend handoff | [codex-backend-handoff.md](codex-backend-handoff.md) | — | 当前进度、剩余工作、新机器准备与实测配置(接手者先读)。 | diff --git a/docs/architecture.en.md b/docs/architecture.en.md index 3f85b2013..b999fbbf1 100644 --- a/docs/architecture.en.md +++ b/docs/architecture.en.md @@ -2,6 +2,8 @@ [Documentation index](README.md) · [简体中文](architecture.md) +This document describes the **current** implementation: dsh-TUI runs as a plugin mounted in a DSH profile (the path below reflects that). For the move to an app that owns its entry and composition root, with DSH loaded in process as the first-party backend, see [Standalone host design](standalone-host-design.md). + ## Runtime path ```text diff --git a/docs/architecture.md b/docs/architecture.md index 44608db5a..834ea67e7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,6 +2,8 @@ [文档索引](README.md) · [English](architecture.en.md) +本文描述**当前实现**的形态:dsh-TUI 作为 DSH profile 里的插件挂载运行(下面的链路以此为准)。向「本包自持入口与组装根、DSH 在进程内作为首方后端加载」的演进见 [独立宿主设计](standalone-host-design.md)。 + ## 运行链路 ```text diff --git a/docs/claude-backend.en.md b/docs/claude-backend.en.md index 59d98db36..db08c671a 100644 --- a/docs/claude-backend.en.md +++ b/docs/claude-backend.en.md @@ -202,6 +202,19 @@ their prompts carried. ## Known limitations - One dsh-TUI process runs one backend; switching restarts through `/kernel`. +- On Claude, `dsh-tui` starts the TUI from the package's own entry and + composes only a light profile (this package plus the third-party plugins the + profile declares, without DSH's agent core): the screen appears first and sending waits until + the session has opened. Until then Enter keeps the draft and says the kernel + is still starting, and a command-line prompt is sent once the session opens; + a failed open says why (the landing page closes to show it) and `/new` + retries. Third-party + DSH/Cordis plugins (themes, panels, decision hooks) load and join once the + screen is up; a plugin that needs DSH's own services (agents, tools, the LLM + layer) does not activate on this kernel. `DSH_TUI_HOST_ENTRY=0` goes back to + starting through `dsh --profile dsh-tui`. (The DSH kernel starts from the + package's entry by default too, but composes the whole DSH profile into that + process.) - In a Claude session the side panel's workspace panel reports it is unsupported and the trajectory panel stays empty. - No `/add-dir`; the refusal dialog offers only *Retry* and *Cancel*; an MCP diff --git a/docs/claude-backend.md b/docs/claude-backend.md index 7195e6b04..ebddd9054 100644 --- a/docs/claude-backend.md +++ b/docs/claude-backend.md @@ -158,6 +158,14 @@ Claude 设置 > `default`。Claude 设置里的 `defaultMode: bypassPermissions` ## 已知限制 - 一个 dsh-TUI 进程只用一个后端,切换要经 `/kernel` 重启。 +- Claude 内核下 `dsh-tui` 从本包自己的入口启动,只组合轻量 profile(本包与 profile + 声明的第三方插件,不含 DSH 的 agent 核心):界面先出现, + 会话打开后才能发送:在此之前按 Enter 草稿留在原处并提示还在启动,命令行带的首句在 + 会话打开后发出;打开失败会说明原因(落地页自动收起),`/new` 重试。第三方 + DSH/Cordis 插件(主题、面板、决策拦截)照常加载,在界面出现后加入;依赖 DSH 自身 + 服务(agent、工具、LLM 层)的插件在这个内核下不会激活。`DSH_TUI_HOST_ENTRY=0` 回到经 + `dsh --profile dsh-tui` 启动。(DSH 内核默认也从本包入口启动,但会在同一进程里组合 + 完整的 DSH profile。) - 侧栏的工作区面板在 Claude 会话下提示不支持,轨迹面板为空。 - 不支持 `/add-dir`;拒答对话框只有「重试」和「取消」;需要浏览器操作的 MCP 请求不会 自动打开浏览器。 diff --git a/docs/configuration.en.md b/docs/configuration.en.md index 206712bb8..dd3f0767e 100644 --- a/docs/configuration.en.md +++ b/docs/configuration.en.md @@ -28,10 +28,18 @@ only for a genuinely new service. ## TUI configuration -On DSH 0.1.7, `/settings` writes plugin Config fields to the active profile's -`cordis.patch.yml`. Older hosts still use `~/.dsh/settings.yaml`; that file is -not the new settings entry point. Language and layout preferences update live; -fullscreen and image previews require `/restart`. +The `dsh-tui` section of `/settings` lives in the TUI's own +`~/.dsh-tui/settings.json`, shared by the DSH and Claude kernels. When that file +does not exist yet, the first launch imports the editable fields of the `dsh-tui` +row's `config` from the active profile's `cordis.patch.yml` once (read only; the +profile is not changed and `!!js` expressions are not imported), and never again. +After the import, those fields left in the profile patch only act as a static +deployment layer under the DSH kernel (a fallback while the user layer is unset); +remove them from the patch so both kernels see the same defaults. Other plugins' +sections still go through DSH's settings service (written to the active profile's +`cordis.patch.yml`; older hosts use `~/.dsh/settings.yaml`). +Language and layout preferences update live; fullscreen and image previews +require `/restart`. A complete common override looks like this: @@ -311,6 +319,7 @@ for the complete field reference. | `DSH_TUI_RESUME_BACKEND` | The backend `DSH_TUI_RESUME_SESSION` was read from; set only when the launcher **derived** the target (a bare `--resume` reading that backend's last session, or the safe-mode retry following the last-run record). When the boot lands on another backend — an uninstalled id falling back to `dsh`, or the remembered kernel when `DSH_TUI_BACKEND` is absent — that target **refuses with a non-zero exit**: never carried across, never a silent cold start, never a new session (the safe-mode retry is the one exception, degrading to a warning plus a cold start). An id the user passed explicitly carries no mark and is handed over as it is | | `DSH_TUI_RESUME_RETRY` | One-shot marker: this boot is safe mode's "retry normal startup". A retry derives its target from the last-run record, whose backend may no longer be registered, so a launch carrying this marker degrades a revoked resume target to a warning plus a cold start instead of failing. The boot deletes it from `process.env` as soon as it reads it | | `DSH_TUI_BACKEND` | Session backend (built-in `dsh` / `claude` / `codex`, or an installed plugin backend), normally set by `dsh-tui --backend`; an uninstalled or misspelled id starts on `dsh` with a warning (with a resume request in play it refuses instead, see above) | +| `DSH_TUI_HOST_ENTRY` | Set to `0` to start both kernels through `dsh --profile` (by default both start from the package's own entry). When the entry cannot use the installed dsh (the `dsh` on `PATH` is not `@deepseek-ai/dsh`, its launcher script cannot be followed, or a module or export it needs is missing), a DSH launch falls back to `dsh --profile` by itself and says why in the terminal and on screen | | `DSH_TUI_CLAUDE_PERMISSION_MODE` | Start permission mode of the Claude backend (`default`/`acceptEdits`/`plan`/`dontAsk`/`bypassPermissions`); wins over the mode `/permission` remembered | | `DSH_TUI_WORKSPACE_TARGET` | Workspace path or URI resolved at startup, normally set by `dsh-tui ` | | `DSH_TUI_SESSION_ROOT` | Override the JSONL session root; profile default `$DSH_HOME/sessions`, bare `cordis.yml` default `~/.dsh-tui/sessions` | diff --git a/docs/configuration.md b/docs/configuration.md index 68df94a9d..b456613f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,8 +27,13 @@ Profile 启动按顺序叠加: ## TUI 配置 -DSH 0.1.7 的 `/settings` 写入当前 profile 的 `cordis.patch.yml`,字段属于插件 -Config;旧版仍使用 `~/.dsh/settings.yaml`。不要把旧文件路径当成新版设置入口。 +`/settings` 的 `dsh-tui` 分区存放在 TUI 自己的 `~/.dsh-tui/settings.json`,DSH 与 +Claude 两个内核共用这一份。第一次启动时,若该文件不存在,会从当前 profile 的 +`cordis.patch.yml` 里 `dsh-tui` 行的 `config` 一次性导入可编辑字段(只读,不改 profile; +`!!js` 表达式不导入),之后不再重复导入。导入后 profile 补丁里残留的这些字段只在 DSH +内核下作为静态部署层生效(用户层未设置时兜底),建议从补丁里删掉,避免两个内核看到 +不同的默认值。其他插件的分区仍由 DSH 的设置服务读写(写入当前 profile 的 +`cordis.patch.yml`;旧版是 `~/.dsh/settings.yaml`)。 语言、布局等偏好实时更新;全屏和图片预览需 `/restart`。 下面是完整的常用覆盖示例: @@ -276,6 +281,7 @@ Profile 模式不再使用旧的 `DSH_TUI_COMPACT_RATIO`、`DSH_TUI_COMPACT_RETA | `DSH_TUI_RESUME_BACKEND` | `DSH_TUI_RESUME_SESSION` 的来源后端,只在启动器**派生**恢复目标时(裸 `--resume` 读该后端的上次会话、安全模式重试按最后运行记录)设置;boot 若落在别的后端(未装的 id 回落 `dsh`、没给 `DSH_TUI_BACKEND` 时跟随记住的内核),该目标一律**明确报错并非零退出**——不跨后端恢复、不静默冷启动、不新建会话(安全模式重试是唯一例外,降级为告警 + 冷启动)。用户显式给出的 id 不带此标记、原样透传 | | `DSH_TUI_RESUME_RETRY` | 一次性标记:本次启动是安全模式的「重试正常启动」。重试的目标派生自最后运行记录,记录里的内核可能已经不在注册表里,所以带此标记的那次启动在恢复目标被撤销时降级为告警 + 冷启动,而不是报错退出。boot 读到即从 `process.env` 删除 | | `DSH_TUI_BACKEND` | 会话后端(内置 `dsh` / `claude` / `codex`,或已装的插件后端),通常由 `dsh-tui --backend` 设置;未安装或写错的 id 按 `dsh` 启动并告警(若同时带了 `--resume` 请求则改为报错退出,见上) | +| `DSH_TUI_HOST_ENTRY` | 设为 `0` 时两个内核都经 `dsh --profile` 启动(默认两个内核都从本包入口启动)。入口用不了已安装的 dsh(`PATH` 上的 `dsh` 不是 `@deepseek-ai/dsh`、启动脚本跟不进去、所需模块或导出缺失)时,DSH 启动自动回退到 `dsh --profile`,并在终端与界面里说明原因 | | `DSH_TUI_CLAUDE_PERMISSION_MODE` | Claude 后端的起始权限模式(`default`/`acceptEdits`/`plan`/`dontAsk`/`bypassPermissions`),优先于 `/permission` 记住的选择 | | `DSH_TUI_WORKSPACE_TARGET` | 启动时解析的工作区路径或 URI,通常由 `dsh-tui <目标>` 设置 | | `DSH_TUI_SESSION_ROOT` | 覆盖 JSONL 会话根目录;profile 默认 `$DSH_HOME/sessions`,裸 `cordis.yml` 默认 `~/.dsh-tui/sessions` | diff --git a/docs/contributing.en.md b/docs/contributing.en.md index df3ccc65d..16746d4b1 100644 --- a/docs/contributing.en.md +++ b/docs/contributing.en.md @@ -271,6 +271,11 @@ seam. 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. + - The host packages the standalone entry takes types from (`HOST_TYPE_PACKAGES` in + `src/dsh-adapter/host-contract.ts`) are optional peer + dev too, for types only; the + host CLI `@deepseek-ai/dsh` is no dependency. At + run time `host-dsh.ts` loads the installed host's copies by realpath, never this + package's (ADAPTER.md "独立入口的宿主契约"). - Non-host packages such as `dsh-working-activity` stay runtime dependencies. - Historical exception, now resolved: `dsh-working-activity@0.2.4` and earlier pulled a real copy of `@deepseek-ai/schemastery` (plus cosmokit) into the @@ -454,6 +459,7 @@ change, also run the closest focused script: | Standalone Markdown nodes (tables, mermaid diagrams) and streaming block spacing | `pnpm verify:table-layout`, `pnpm verify:mermaid-diagram`, `node --import tsx/esm scripts/verify-streaming-markdown-spacing.tsx` | | Cross-process session mount ledger (failure behavior, strict reads, lock recovery, reservations) | `pnpm verify:session-mounts` | | Unsent-draft handoff across screens (snapshot, cursor, image bindings, ownership) | `pnpm verify:composer-draft-handoff`; end-to-end screen switching also `node scripts/verify-session-browser.mjs` | +| Launcher `bin/dsh-tui.js` (argv parsing and delegation/bootstrap, safe mode, the `/update` launcher migration, the entry child's env) | `node scripts/verify-launcher.mjs` + `node scripts/verify-safe-mode.mjs` + `node scripts/verify-update.mjs` + `node scripts/verify-update-recovery.mjs` | Most focused scripts invoked with plain `node` import `lib/types/`; run `pnpm build` first. Scripts that import TypeScript sources declare the @@ -682,7 +688,7 @@ guide owns detailed contracts such as the toolchain and verification matrix. | Adding or changing a backend | A new `src/backends//` (`manifest.ts` + implementation), plus the regenerated `src/dsh-adapter/backends.generated.ts` that `pnpm compile` writes (a checked-in generated file; `verify-backend-registry` fails when it is stale): the build-time index comes from `scripts/gen-backend-index.mjs` — do **not** hand-edit `src/kernelPrefs.ts` or `src/dsh-adapter/backends.ts` (the directory and identity assertions look entries up by id, so a new directory needs no regression sync). The boundary gate derives its vendor-package and `native.` rules from the manifest but compares them verbatim against the `EXPECTED_*` snapshot in `scripts/verify-adapter-boundary.ts`: a backend declaring a non-empty `vendorPackages` or a `nativeKey` must update that snapshot together with `ADAPTER.md` (the gate's failure says so), while declaring neither needs no change. `install` is a declarative recipe instead (`{ executor, specifier, version }`), with the host-side executor table in `src/dsh-adapter/install/` (one value today, `pnpm-profile-add`): the registry derives "can this entry be installed" by looking the name up, the wizard installs the specifier *you* declared, and naming an executor this host does not implement simply means "no install surface" (never a throw). A backend with nothing to add — the ones driving a system CLI — declares nothing. Names come from the manifest (a plugin uses `kind:'literal'`, never the i18n catalog); module-level/process-wide pools declare `unloadExport`, session-scoped resources stay with `session.dispose()`; the `id`, `label` and `install` boundaries are in the "backend manifest" section of `ADAPTER.md`. Register a new backend's focused regressions in `scripts/run-ci-group.mjs`; the registry gate is `scripts/verify-backend-registry.ts`; user-visible values (`--backend`, the config row) go into both READMEs and `docs/configuration{,.en}.md` | | Claude Agent SDK version | The exact version in both the optional peer and dev entries of `package.json`, `pnpm-lock.yaml`, `src/backends/claude/contract.ts` (`VALIDATED_SDK_VERSION`/`VALIDATED_CLI_VERSIONS`), the install command in `docs/claude-backend{,.en}.md`; `verify:claude-contract` checks they agree | | Codex protocol/validated version | Regenerate through `scripts/codex-protocol-sync.mjs`, update `src/backends/codex/contract.ts`, method tables/fixtures/redaction/live-replay regressions and bilingual Codex guides; do not add a Codex SDK npm dependency or claim the minimum validates every experimental API | -| Upstream validated-line bump | `src/dsh-adapter/contract.ts`, `src/dsh-adapter/oauth/`, both peer and dev ranges in `package.json`, `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](../ADAPTER.md) | +| Upstream validated-line bump | `src/dsh-adapter/contract.ts`, `src/dsh-adapter/host-contract.ts` (`HOST_REPLICA_VERSION`; after reviewing the replicas, `scripts/verify-host-contract.ts --snapshot` rewrites `host-replica.snapshot.json`), `src/dsh-adapter/oauth/`, both peer and dev ranges in `package.json`, `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](../ADAPTER.md) | ## Git And Release Safety diff --git a/docs/contributing.md b/docs/contributing.md index afdf52a5b..1b0a4a267 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -204,6 +204,9 @@ Cordis config - 新增此类引用时两组声明都要加、范围保持一致(verify:manifest-deps 门禁会校验)。 - 仅测试/脚本使用的框架包(如 dsh-settings、dsh-tools、dsh-session-persistence-*) 只需 dev 依赖,不要为它们声明 peer。 + - 独立入口取类型的宿主包(`src/dsh-adapter/host-contract.ts` 的 `HOST_TYPE_PACKAGES`) + 同样是 optional peer + dev,只为类型;宿主 CLI `@deepseek-ai/dsh` 不是依赖。运行期由 `host-dsh.ts` 按宿主 realpath 加载宿主副本,绝不从本包解析(见 ADAPTER.md + 「独立入口的宿主契约」)。 - `dsh-working-activity` 等非宿主包仍是 runtime dependency。 - 历史例外已消除:`dsh-working-activity@0.2.4` 及更早版本会经其 runtime dependency 把 `@deepseek-ai/schemastery`(连带 cosmokit)的真实拷贝带进 @@ -343,6 +346,7 @@ CI 回归都要跑。窄改动还要跑最近的聚焦脚本: | Markdown 独立节点(表格、mermaid 图、公式块)、LaTeX 公式与流式分块间距 | `pnpm verify:table-layout`、`pnpm verify:mermaid-diagram`、`pnpm verify:latex-math`、`node --import tsx/esm scripts/verify-streaming-markdown-spacing.tsx` | | 跨进程会话占用账本(失败行为、严格读、锁回收、预约) | `pnpm verify:session-mounts` | | 未发送草稿的跨屏交接(快照、光标、图片绑定、归属) | `pnpm verify:composer-draft-handoff`;端到端换屏另见 `node scripts/verify-session-browser.mjs` | +| 启动器 `bin/dsh-tui.js`(argv 解析与委托/自举、安全模式、`/update` 的启动器迁移、entry 子进程的 env) | `node scripts/verify-launcher.mjs` + `node scripts/verify-safe-mode.mjs` + `node scripts/verify-update.mjs` + `node scripts/verify-update-recovery.mjs` | 多数用普通 `node` 调用的脚本 import `lib/types/`——先跑 `pnpm build`。import TypeScript 源的脚本在头部声明 `node --import tsx/esm