dsh-oc-tui 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@
17
17
  - [5.2 斜杠命令](#52-斜杠命令)
18
18
  - [5.3 交互式提示:授权与提问](#53-交互式提示授权与提问)
19
19
  - [5.4 思考强度](#54-思考强度)
20
- - [5.5 上下文仪表与遥测](#55-上下文仪表与遥测)
20
+ - [5.5 会话统计与上下文仪表](#55-会话统计与上下文仪表)
21
21
  - [5.6 设置菜单](#56-设置菜单)
22
22
  - [5.7 程序内更新](#57-程序内更新)
23
23
  - [6. 工作原理](#6-工作原理)
@@ -36,8 +36,9 @@
36
36
  | **工具活动** | 工具卡片带一行摘要(`read src/app.ts`、`run npm test`),运行中显示流动 spinner,结果按 Markdown 渲染。 |
37
37
  | **交互式提问** | 模型可以暂停并向你提问——选项列表、多选、自由文本、可滚动的计划评审,全部在终端内完成。 |
38
38
  | **内联授权** | `approval/request` 询问用 `y` / `n` 直接回答,无需离开界面。 |
39
- | **遥测页脚** | 会话 token、平均首 token 时间(TTFT)、解码吞吐、KV 缓存命中率,均由持久事件折叠得出。 |
40
- | **上下文仪表** | 实时上下文占用(`ctx ▓▓░░ 32K/128K 25%`),点击可展开构成明细。 |
39
+ | **遥测页脚** | 会话统计条:轮次/步数、LLM 与工具耗时、平均首 token 时间(TTFT)、解码吞吐、KV 缓存命中率与输入/输出 token,均由持久事件折叠得出。 |
40
+ | **统计窗口** | 点击统计条或上下文仪表条,或输入 `/stats`,打开会话统计与 token 用量明细窗口。 |
41
+ | **上下文仪表** | 实时上下文占用(`ctx ▓▓░░ 32K/128K 25%`),明细窗口内含按系统提示词/工具/消息拆分的构成占比。 |
41
42
  | **思考强度** | `Tab` 循环切换当前模型真实支持的推理等级;`Ctrl+E` 打开滑块。选择按请求应用并持久化。 |
42
43
  | **共享设置** | 与 WebUI 相同的 Host 设置命名空间——通用、会话、各 provider 模型配置、凭据——持久化到 `$DSH_HOME/settings.yaml`。 |
43
44
  | **程序内更新** | 在 TUI 内检测并切换 `@deepseek-ai/dsh` 与 `dsh-oc-tui` 版本,Windows 上采用延迟安装避免静默损坏。 |
@@ -199,14 +200,17 @@ dsh-oc-tui --version # 启动器版本
199
200
  | `Up` / `Down` | 在多行输入中上下移动光标;位于第一行/最后一行时改为浏览输入历史 |
200
201
  | `Left` / `Right` | 在输入框中左右移动光标 |
201
202
  | `PgUp` / `PgDn` | 滚动转录区 |
202
- | `Esc` | 关闭上下文仪表盘 / 关闭强度滑块 / 关闭帮助 / 取消授权询问 |
203
+ | `Esc` | 关闭会话统计窗口 / 关闭强度滑块 / 关闭帮助 / 取消授权询问 / 中断正在运行的轮次 / 清空正在输入的提示词 |
204
+ | `Esc Esc` | 空闲且输入框为空时打开 rewind 选择器 |
203
205
  | `y` / `n` | 回答界面内的授权询问 |
204
206
 
205
- **鼠标**:滚轮滚动页面——会话页滚动转录区,设置窗口打开时滚动设置窗口。在转录区按住左键拖动可选中文字,随后按右键把所选文字复制到剪贴板。
207
+ **鼠标**:滚轮滚动页面——会话页滚动转录区,设置窗口打开时滚动设置窗口。在转录区按住左键拖动可选中文字,随后按右键把所选文字复制到剪贴板。会话页的统计条与上下文仪表条都是点击目标(见 [5.5](#55-会话统计与上下文仪表))。
206
208
 
207
209
  ### 5.2 斜杠命令
208
210
 
209
- 内建命令:`/help` `/settings` `/new` `/resume <id>` `/model <id>` `/provider <route>` `/clear` `/cancel` `/quit`(`/exit` 等效)。
211
+ 内建命令:`/help` `/settings` `/new` `/resume <id>` `/model <id>` `/provider <route>` `/rewind` `/stats` `/clear` `/cancel` `/quit`(`/exit` 等效)。
212
+
213
+ **Rewind(回退)**:`Esc Esc`(或 `/rewind`)列出当前会话的提示词。选择"恢复对话"会以所选提示词之前的事件**分叉出一个新会话**,父会话完整留在磁盘上——与 dsh 自身的 `session/fork` 行为一致;选择器默认停在最近一条真实提示词上,因此连按两次 `Enter` 即回退最后一轮。`/rewind <n|last> [conversation|code|both]` 可跳过选择器直接执行。分叉出的新会话从**空 inbox** 开始:切点落在某轮 turn 之前,也会切掉那一轮对 inbox 的认领,因此父会话里排队过的输入(**包括你这次要丢掉的那条提示词**)不会被再次投递,它们仍留在父会话日志里;若有此类输入,结果行会显示 `dropped N inherited pending input`。**恢复文件**是尽力而为且有护栏的:必须在 git 工作区中(否则提示 `files not restored (not a git worktree)` 且不改动任何文件);已跟踪文件用 `HEAD` 覆盖且不触碰索引;只有当日志中该路径的首次写入发生在回退点之后时,才会删除未跟踪文件。所有被覆盖或删除的内容都会先复制到 `$DSH_HOME/rewind-backups/<sessionId>/<时间戳>/`,结果行会给出该目录。由于日志不保存文件内容,已跟踪文件回到的是最近一次提交,而非回退点当时的状态。
210
214
 
211
215
  dsh 的人类命令(`/compact`、`/goal`、`/plan` 等)会转发给 `ctx.commands`,不经过模型轮次执行。**这些命令需要有活动会话**:在标题屏上敲会得到 `/<name>: start a session first` 提示,而不是被静默丢弃。因此想用 `/plan` 进入计划模式,请先随便发一条消息建立会话。
212
216
 
@@ -242,11 +246,23 @@ dsh 的人类命令(`/compact`、`/goal`、`/plan` 等)会转发给 `ctx.com
242
246
 
243
247
  选择通过 `agent/request` waterfall 应用到该会话的请求,并保存到 `agent-default-model.reasoningEffort`。
244
248
 
245
- ### 5.5 上下文仪表与遥测
249
+ ### 5.5 会话统计与上下文仪表
250
+
251
+ 输入框上方一行是**会话统计条**,对应 Web 界面输入框下方的统计行——用 `│` 分隔、按同样的顺序列出同一批数字:
246
252
 
247
- 状态栏带实时上下文占用条,数据来自 token-meter 的 `contextPressure` 投影(与 Web 界面输入框右侧的环形仪表同源):当前上下文长度 / 模型上下文窗口,占用升高时转入警示色与错误色。点击占用条会打开明细面板(再次点击或 `Esc` 关闭),显示占用读数与按启发式拆分的构成占比——系统提示词、工具、对话消息——对应 WebUI 的 ContextMeter 弹窗。当 profile 缺少 token-meter 投影时,占用条自动隐藏,不影响其它功能。
253
+ ```
254
+ ▤ 1 turn · 2 steps│LLM 1.3s · tools 1.2s│TTFT avg 400ms · 20.0 tok/s│cache 55%│in 110 · out 30
255
+ ```
248
256
 
249
- 页脚报告会话 token、平均首 token 时间、解码吞吐与缓存命中率,均由持久的 step/chunk/message 事件折叠得出。
257
+ - `turns` / `steps` 统计**已结束的步**(`step/end`):失败、取消、触顶的步同样计入;`LLM` 是 `step/start` → 组装完成的回复,`tools` 是配对的 `tool/call` → `tool/result`。
258
+ - `TTFT avg` 是每步首 token 延迟的平均值,`tok/s` 是解码吞吐(首 token → 组装完成之间的输出 token)。
259
+ - `cache` 是提示词侧缓存命中率(缓存读取 ÷ 全部计费输入),`in` / `out` 是本次会话累计的计费输入与输出 token。
260
+ - 极窄终端会**整组**丢弃放不下的尾部数字并标出 `│…`,不会把某个数字截成两半;完整数字始终在明细窗口里。
261
+ - 会话还没有任何已结束的步、也没有任何 token 计费时,统计条整行隐藏,把该行还给转录区。
262
+
263
+ 统计条整行是点击目标:点击它(或点击状态栏右端的上下文仪表条 `ctx ▓▓░░ 32K/128K 25%`,或输入 `/stats`)打开**会话统计窗口**,再次点击、点击别处或按 `Esc` 关闭。窗口把同一个统计条拆成明细行(`usage` / `duration` / `speed` / `tokens` / `cache`),并附上上下文占用读数与按启发式拆分的构成占比——系统提示词、工具、对话消息——对应 WebUI 的 ContextMeter 弹窗。
264
+
265
+ 数据来源与 Web 界面一致,优先级为**投影优先、本地折叠兜底**:token-meter 的 `tokenUsage`、`contextPressure`、`contextBreakdown` 投影由 `dsh-base` 挂载;`sessionStats` 投影只有 Web 应用层 bundle 才挂载,因此 TUI 自己按同样的规则折叠持久日志里的 `step` / `chunk` / `message` / `tool` 事件。profile 缺少某个投影时,对应数字自动退回本地折叠而不影响其它功能;两者都不存在时(例如尚无任何事件)该行/该组自动隐藏。
250
266
 
251
267
  ### 5.6 设置菜单
252
268
 
@@ -264,6 +280,8 @@ dsh 的人类命令(`/compact`、`/goal`、`/plan` 等)会转发给 `ctx.com
264
280
 
265
281
  WebUI 专属选项(`ui-theme` 外观、`locale` 语言)不在 TUI 中显示,因为它们在终端里没有效果。
266
282
 
283
+ **第三方插件的设置页分区(目前尚不支持)**:`@dsh-std` 生态里有 `ui.dsh/v1alpha1` 的 `ContributionHost` / `UiContribution` 协议,本意是让第三方插件往宿主的设置面追加只读分区(`host-rendered` 模式,即宿主自己渲染对方给的数据)。TUI **目前不托管**这类贡献,所以设置页只显示上面列出的内置分区:本插件的 `@dsh-std` facet 无法注册贡献宿主——adapter 对 facet 提交的实现有强制校验(必须有 `handle` 函数,字段名必须是 `protocol`),而协议里的 `UiContributionProvider` 两条都不满足,提交它会让 adapter 在挂载时回滚整个 profile;真正的注册入口是 adapter 的实例方法 `registerUiContributionProvider`,facet 拿不到 adapter 实例。需要宿主自己执行第三方 JS 的 `local-module` 模式则永久不在计划内(TUI 无沙箱,且协议说明没有同一 page realm 的 TUI 不需要实现它)。另需注意:本插件的 facet 在当前上游下**一条协议 support 都不暂存**——Community v0.15 清单无法声明 supports,而 lifecycle 要求先声明才能暂存——所以它不只是不托管贡献宿主,Presentation 与 CommandRuntime 同样处于休眠状态。详见 [dsh-std 接入说明](dsh-std-接入说明.md) 开头的「阻断性发现」。
284
+
267
285
  ### 5.7 程序内更新
268
286
 
269
287
  `Ctrl+P → Update` 页面负责检测并切换 dsh 与 TUI 自身的版本,所有检查与安装都通过 `npm` / `dsh plugin`(即 pnpm)执行,因此会尊重你配置的 registry 与镜像。状态行**只以稳定版为目标**:
@@ -289,8 +307,8 @@ macOS/Linux 没有 DLL 锁,但检测到其它 dsh 进程运行时也会拒绝
289
307
 
290
308
  - 插件是用 `tui` profile 加载的 Cordis 函数插件。`lib/startup.js` 解析本应用的命令行参数并提供 `tuiStartup` 服务;`lib/index.js` 拥有 UI 主循环。
291
309
  - `lib/term.js` 是零依赖终端引擎:raw 模式、备用屏幕、差分单元缓冲、按键解码(真彩 ANSI、CJK 宽度感知)。它把隐藏的终端光标停在输入光标处,使系统 IME 的候选窗锚定在输入框内;同时理解 SGR 与旧式 X10 两种鼠标编码,滚轮/点击字节不会漏进输入文本。
292
- - `lib/ui.js` 是响应式视图模型与渲染器(DeepSeek 蓝白主题、会话栏、转录区、多行输入框、命令建议、遥测页脚)。转录行按块缓存,每帧只实体化可见窗口,流式绘制合并,活动块按短节流重绘——因此渲染成本有界,输出速度不随历史增长而下降。思考内容折叠以保持转录可读,运行中的工具与思考块用流动 spinner 提示。
293
- - `lib/metrics.js` 把持久的 step/chunk/message 事件折叠为 token、TTFT、吞吐与缓存命中指标。
310
+ - `lib/ui.js` 是响应式视图模型与渲染器(DeepSeek 蓝白主题、会话栏、转录区、多行输入框、命令建议、会话统计条、状态栏)。转录行按块缓存,每帧只实体化可见窗口,流式绘制合并,活动块按短节流重绘——因此渲染成本有界,输出速度不随历史增长而下降。思考内容折叠以保持转录可读,运行中的工具与思考块用流动 spinner 提示。
311
+ - `lib/metrics.js` 把持久的 step/chunk/message/tool 事件折叠为整场会话的轮次/步数、耗时、TTFT、吞吐、缓存命中与计费 token(与 Web 界面的 `sessionStats` / `tokenUsage` 投影同规则),并在 profile 提供投影时以投影值为准。
294
312
  - `lib/interrupt.js` 管理 stdin 与 `SIGINT` 共用的清空/取消/二次退出状态机。
295
313
  - `lib/markdown.js` 把模型输出(标题、列表、引用、代码、行内样式)渲染为带样式的行。
296
314
  - `lib/updates.js` 隔离 Update 页面的全部 npm/pnpm 交互——registry 查询、无依赖 semver 比较、dsh 安装探测、异步安装——一律走 `child_process.spawn`,从不使用 `spawnSync`。
@@ -355,9 +373,9 @@ dsh --profile tui --dump-config # 先初始化 base profile
355
373
  # $DSH_HOME/profiles/tui/cordis.patch.yml
356
374
  - insert:
357
375
  - id: tui-startup
358
- name: 'file:///D:/Projects/DeepSeekHarnessPlugins/dsh-oc-tui/lib/startup.js'
376
+ name: 'file:///path/to/dsh-oc-tui/lib/startup.js'
359
377
  - id: tui-app
360
- name: 'file:///D:/Projects/DeepSeekHarnessPlugins/dsh-oc-tui/lib/index.js'
378
+ name: 'file:///path/to/dsh-oc-tui/lib/index.js'
361
379
  config:
362
380
  sidebar: true
363
381
  showReasoning: true
@@ -367,12 +385,12 @@ dsh --profile tui --dump-config # 先初始化 base profile
367
385
 
368
386
  ## 9. 已知限制
369
387
 
370
- - 零依赖终端引擎尚未暴露 IME 组字与 bracketed-paste 图片附件。
388
+ - 零依赖终端引擎尚未暴露 IME 组字;图片附件已支持:bracketed paste 的原始图片字节、`data:image/...;base64,...` 数据 URL、本地图片路径或图片 URL 都会变成 `[Image N]` 附件,而粘贴一段无法识别的文本时会向终端请求剪贴板(OSC 52)。
371
389
  - 插件不支持热重载:profile 的 HMR 根是 profile 目录,运行中的 TUI 保持它启动时的那份副本。
372
390
  - `dsh tui` 作为裸子命令需要 shell 别名——原版启动器只硬编码了 `web` 与 `plugin`。
373
391
  - dsh 的人类斜杠命令需要活动会话;标题屏上会提示先建立会话。
374
392
  - `Esc` 让出问题不会取消工具调用,而是委托;没有其它应答者时该工具调用会失败。WebUI composer 支持的**逐题跳过**尚未实现。
375
- - `--resume`、设置 → 管理会话、上下文仪表依赖 `@deepseek-ai/dsh-base` 挂载的服务(`sessionQuery`、`sessionProjections`);手工搭建的 profile 需自行提供。
393
+ - `--resume`、设置 → 管理会话、会话统计条与上下文仪表依赖 `@deepseek-ai/dsh-base` 挂载的服务(`sessionQuery`、`sessionProjections`);手工搭建的 profile 需自行提供。`sessionStats` 投影只由 Web 应用层 bundle 挂载,缺少时 TUI 自行从会话日志折叠同样的数字。
376
394
  - Windows 上的延迟 dsh 安装只等待**调度它的那个 TUI**,不是机器上所有 dsh 进程;执行前请关掉其它 TUI 窗口(以及 `dsh web`)。
377
395
 
378
396
  ## 10. 目录结构
@@ -382,17 +400,22 @@ lib/index.js 插件入口:agents、事件、输入、命令、授权、
382
400
  lib/startup.js 命令行参数提供者(tuiStartup 服务)
383
401
  lib/term.js 终端引擎(raw 模式、屏幕、按键解码)
384
402
  lib/ui.js 响应式视图模型 + 渲染器(含问答弹窗)
385
- lib/metrics.js 持久事件遥测统计
403
+ lib/metrics.js 整场会话统计 + token 用量折叠(Web 统计条 / tokenUsage 投影同规则)
386
404
  lib/interrupt.js Ctrl+C 生命周期状态机
387
405
  lib/web-settings.js WebUI 设置投影
388
406
  lib/updates.js 程序内更新(npm registry + 安装)
389
407
  lib/markdown.js Markdown -> 带样式文本行
390
408
  lib/util.js 文本/显示工具
409
+ lib/bridge.js 活体 TUI 注册表(@dsh-std facet 的转发目标)
410
+ lib/facet.js @dsh-std facet 入口(互操作外壳,不启动 TUI)
411
+ lib/std/ @dsh-std 协议适配(adapt、presentation、commands、command-list)
391
412
  bin/dsh-oc-tui.js 便捷启动器
392
413
  install.sh 一键安装脚本(Linux/macOS)
393
414
  install.ps1 一键安装脚本(Windows)
415
+ dsh-plugin.json @dsh-std 组件清单(仅用于发现与预检)
394
416
  cordis.patch.yml bundle 补丁层(TUI 行、预设名册、提问工具)
395
417
  docs/用户手册.md 本手册
418
+ docs/dsh-std-接入说明.md @dsh-std 接入范围与约束
396
419
  tests/smoke.test.mjs 独立冒烟测试
397
420
  ```
398
421
 
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/Yan-Zero/dsh-std/main/packages/manifest/schema/dsh-plugin-0.15.schema.json",
3
+ "manifestVersion": "0.15",
4
+ "id": "io.github.rayafriandion.dsh-oc-tui",
5
+ "name": "dsh-oc-tui",
6
+ "version": "0.1.4",
7
+ "license": "LGPL-3.0-or-later",
8
+ "source": { "repository": "https://github.com/rayafriandion/dsh-oc-tui" },
9
+ "facets": { "host": { "entry": "lib/facet.js", "apiVersion": "v1alpha1" } },
10
+ "requires": {
11
+ "contracts": [
12
+ { "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "optional": true }
13
+ ]
14
+ },
15
+ "permissions": [
16
+ { "name": "storage.local.read", "scope": "io.github.rayafriandion.dsh-oc-tui",
17
+ "reason": "Read TUI settings from the host settings store." },
18
+ { "name": "storage.local.write", "scope": "io.github.rayafriandion.dsh-oc-tui",
19
+ "reason": "Persist TUI settings chosen in the Settings pages." }
20
+ ],
21
+ "contributes": {
22
+ "x-dev.dsh-std.extensions": [
23
+ { "id": "io.github.rayafriandion.dsh-oc-tui.settings",
24
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "settings",
25
+ "spec": { "title": "Open the TUI settings pages",
26
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
27
+ { "id": "io.github.rayafriandion.dsh-oc-tui.help",
28
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "help",
29
+ "spec": { "title": "Show the TUI key and command reference",
30
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
31
+ { "id": "io.github.rayafriandion.dsh-oc-tui.stats",
32
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "stats",
33
+ "spec": { "title": "Show session token and cache statistics",
34
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
35
+ { "id": "io.github.rayafriandion.dsh-oc-tui.new",
36
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "new",
37
+ "spec": { "title": "Start a new session",
38
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
39
+ { "id": "io.github.rayafriandion.dsh-oc-tui.resume",
40
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "resume",
41
+ "spec": { "title": "Resume a persisted session",
42
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
43
+ { "id": "io.github.rayafriandion.dsh-oc-tui.clear",
44
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "clear",
45
+ "spec": { "title": "Clear the transcript",
46
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
47
+ { "id": "io.github.rayafriandion.dsh-oc-tui.cancel",
48
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "cancel",
49
+ "spec": { "title": "Cancel the running turn",
50
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
51
+ { "id": "io.github.rayafriandion.dsh-oc-tui.rewind",
52
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "rewind",
53
+ "spec": { "title": "Rewind the session to an earlier point",
54
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } },
55
+ { "id": "io.github.rayafriandion.dsh-oc-tui.quit",
56
+ "apiVersion": "commands.dsh/v1alpha1", "kind": "Command", "name": "quit",
57
+ "spec": { "title": "Leave the TUI", "aliases": ["exit"],
58
+ "placements": [{ "apiVersion": "tui.dsh/v1alpha1", "kind": "CommandLine" }] } }
59
+ ]
60
+ },
61
+ "overrides": [
62
+ { "target": "@deepseek-ai/dsh-base", "kind": "patch",
63
+ "description": "cordis.patch.yml inserts the tui-startup, tui-app, agent-presets and tool-ask-user rows." }
64
+ ]
65
+ }
package/lib/bridge.js ADDED
@@ -0,0 +1,33 @@
1
+ // The live-TUI registry: the one place that answers "is a TUI instance running,
2
+ // and how do I reach it".
3
+ //
4
+ // The @dsh-std facet (lib/facet.js) does not own the TUI's lifecycle — the
5
+ // cordis bundle rows in cordis.patch.yml do. The facet only publishes protocol
6
+ // implementations whose handlers forward here. Because the adapter's mount
7
+ // order relative to the bundle rows is not guaranteed, handlers must look the
8
+ // handle up at call time (late binding) rather than capture it at activation.
9
+ //
10
+ // No dependencies: lib/index.js imports this, so it must stay free of anything
11
+ // that could fail to resolve in a lean profile.
12
+
13
+ let active = null
14
+
15
+ // Register the running TUI. Returns an idempotent release function that only
16
+ // clears the registry if this registration is still the current one — a newer
17
+ // registration must survive an older one's teardown.
18
+ export function registerLiveTui(handle) {
19
+ active = handle
20
+ let released = false
21
+ return () => {
22
+ if (released) return
23
+ released = true
24
+ if (active === handle) active = null
25
+ }
26
+ }
27
+
28
+ // The current TUI handle, or null when no TUI is running. Callers must handle
29
+ // null: it is the normal state when the facet is mounted in a profile whose
30
+ // bundle rows did not load.
31
+ export function liveTui() {
32
+ return active
33
+ }
package/lib/facet.js ADDED
@@ -0,0 +1,129 @@
1
+ // The @dsh-std facet entry declared by dsh-plugin.json.
2
+ //
3
+ // HARD CONSTRAINT: this module must never start the TUI. The TUI's lifecycle
4
+ // belongs to the cordis bundle rows in cordis.patch.yml; the adapter mounts
5
+ // this facet in ADDITION to those rows, and the TUI seizes the terminal
6
+ // (raw mode, alt screen, mouse tracking), so a second instance would fight the
7
+ // first rather than merely duplicate it.
8
+ //
9
+ // The facet is therefore an interop surface: it publishes protocol
10
+ // implementations whose handlers forward through lib/bridge.js to whichever TUI
11
+ // instance is live, and reports `degraded` when none is.
12
+ //
13
+ // The optional peers that can throw are @dsh-std/presentation and
14
+ // @dsh-std/command, imported dynamically by activateProtocols. A throw from
15
+ // this module makes the adapter's mountProfileComponents roll back EVERY
16
+ // component it had already mounted in the profile, so those imports are
17
+ // guarded and a missing peer degrades, never throws.
18
+
19
+ const DEGRADED_MESSAGE =
20
+ 'dsh-oc-tui is activated by its cordis bundle rows (cordis.patch.yml); '
21
+ + 'no live TUI instance is registered, so no protocol support is published.'
22
+
23
+ const NO_DECLARED_SUPPORTS_MESSAGE =
24
+ 'a Community v0.15 manifest cannot declare protocol supports, and the lifecycle '
25
+ + 'requires a declared support before a facet may stage one; no protocol support '
26
+ + 'is published. See the plan\'s blocking-findings section.'
27
+
28
+ // Whether the last activate() staged nothing because the facet declares no
29
+ // supports. Assigned on every activate path so a later activation with declared
30
+ // supports clears it.
31
+ let stagedNothing = false
32
+
33
+ export default {
34
+ async activate(context) {
35
+ // A facet may only stage protocols that its OWN projection declares as
36
+ // supports: LifecycleCoordinator.stageProtocol throws
37
+ // `facet attempted to implement undeclared protocol ...` for anything else,
38
+ // and a throw here makes the adapter's mountProfileComponents roll back
39
+ // every component in the profile.
40
+ //
41
+ // A Community v0.15 manifest cannot declare supports at all — the schema
42
+ // rejects both `requires.supports` and a top-level `supports`, and
43
+ // projectManifest emits only `protocols.requires` — and nothing in
44
+ // adapter-dsh writes protocols.supports onto a facet either. So for this
45
+ // plugin the declared list is empty, and staging anything would throw. We
46
+ // therefore stage nothing and let snapshot() report why. If upstream ever
47
+ // lets a v0.15 component declare supports, this same code stages normally.
48
+ //
49
+ // No presence probe for @dsh-std/sdk: nothing in lib/ consumes it, and the
50
+ // only optional peer that can actually throw is @dsh-std/presentation,
51
+ // which activateProtocols guards itself. A probe here would silently
52
+ // suppress the registration whenever sdk alone is absent, even though
53
+ // nothing needs it.
54
+ if (declaredSupports(context).length === 0) {
55
+ stagedNothing = true
56
+ return
57
+ }
58
+ stagedNothing = false
59
+ const dispose = await activateProtocols(context)
60
+ context.scope.add(dispose)
61
+ },
62
+
63
+ async deactivate() {
64
+ // Everything is registered through context.scope, which the lifecycle
65
+ // coordinator disposes on deactivation. Nothing to do here.
66
+ },
67
+
68
+ async snapshot() {
69
+ const { liveTui } = await import('./bridge.js')
70
+ if (!liveTui()) return { state: 'degraded', message: DEGRADED_MESSAGE }
71
+ if (stagedNothing) return { state: 'degraded', message: NO_DECLARED_SUPPORTS_MESSAGE }
72
+ return { state: 'active' }
73
+ },
74
+ }
75
+
76
+ // The supports this facet's own projection declares, or [] when it declares
77
+ // none. Matched on participantId first because it is unique per activation,
78
+ // falling back to the facet name.
79
+ function declaredSupports(context) {
80
+ const selected = context?.plan?.selected ?? []
81
+ const mine = selected.find((row) => row?.participantId === context?.identity?.participantId)
82
+ ?? selected.find((row) => row?.identity?.facet === context?.identity?.facet)
83
+ return mine?.facet?.protocols?.supports ?? []
84
+ }
85
+
86
+ // Each protocol lands in its own task; the list grows as they do.
87
+ async function activateProtocols(context) {
88
+ const disposers = []
89
+ const disposeAll = () => {
90
+ for (const dispose of disposers.reverse()) {
91
+ try { dispose() } catch { /* teardown must not mask the original failure */ }
92
+ }
93
+ }
94
+
95
+ // Imported dynamically so a missing @dsh-std/presentation degrades this facet
96
+ // instead of throwing: a throw here makes the adapter's
97
+ // mountProfileComponents roll back every component it had already mounted.
98
+ let createPresentationImplementations
99
+ try {
100
+ ({ createPresentationImplementations } = await import('./std/presentation.js'))
101
+ } catch {
102
+ return disposeAll
103
+ }
104
+ // The adapter validates each staged implementation: it must expose `handle`,
105
+ // its participantId must equal this facet's activation participant id, and
106
+ // its protocol must equal the support it is staged with
107
+ // (packages/adapter-dsh/src/index.ts:1774).
108
+ for (const implementation of createPresentationImplementations(context.identity.participantId)) {
109
+ disposers.push(context.protocols.implement(implementation.protocol, implementation))
110
+ }
111
+
112
+ // lib/std/commands.js statically imports @dsh-std/command, which npm does not
113
+ // install (optional peer). A failed import degrades the command surface only:
114
+ // presentation stays published, and the disposer returned below still tears
115
+ // those registrations down.
116
+ let createCommandRuntimeImplementation
117
+ try {
118
+ ({ createCommandRuntimeImplementation } = await import('./std/commands.js'))
119
+ } catch {
120
+ return disposeAll
121
+ }
122
+ // The adapter validates the staged implementation and requires its
123
+ // participantId to equal this facet's activation participant id
124
+ // (packages/adapter-dsh/src/index.ts:1774).
125
+ const commandRuntime = createCommandRuntimeImplementation(context.identity.participantId)
126
+ disposers.push(context.protocols.implement(commandRuntime.protocol, commandRuntime))
127
+
128
+ return disposeAll
129
+ }