dsh-output-styles 0.3.2 → 0.4.1

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.
@@ -0,0 +1,134 @@
1
+ # output.render.* — the dsh-output-styles renderer protocol
2
+
3
+ > 中文版见 [renderer-protocol.zh.md](renderer-protocol.zh.md)。Style switching stays fully
4
+ > compatible; this document covers the presentation layer added in 0.4.0.
5
+
6
+ The renderer protocol turns output presentation into an extension point: any
7
+ plugin can register a **renderer** — a pure function that maps raw
8
+ model-visible text to display text — and the harness-facing surfaces of this
9
+ plugin (`/export`, the `ctx.outputRenderers.renderText` service) apply them
10
+ through one auditable pipeline.
11
+
12
+ ## Renderer contract
13
+
14
+ ```ts
15
+ interface OutputRenderer {
16
+ id: string // kebab-case, unique in the registry
17
+ name: string // human-readable
18
+ description: string // one sentence
19
+ match: RendererMatch[] // [] = matches everything
20
+ priority: number // higher wins; ties break by registration order
21
+ presenter: (text: string, context: RenderContext) => string // PURE — no DOM, no I/O
22
+ }
23
+
24
+ interface RendererMatch {
25
+ tool?: string | string[] // tool names; '*' = any; omitted = any
26
+ contentType?: 'text' | 'markdown' | 'html' | ContentType[]
27
+ }
28
+
29
+ interface RenderContext {
30
+ tool: string // '' for assistant/user prose
31
+ contentType: ContentType
32
+ sessionId?: string
33
+ meta?: Record<string, string>
34
+ }
35
+ ```
36
+
37
+ Rules the registry enforces (fail-loud):
38
+
39
+ - Invalid renderers (bad id grammar, missing fields, non-function presenter,
40
+ non-finite priority) throw at registration and never enter the registry.
41
+ - Duplicate ids throw; `register()` returns a disposer that removes exactly
42
+ that registration — reversibility is the caller's `ctx.effect` job.
43
+
44
+ ## Render pipeline
45
+
46
+ ```text
47
+ renderText(text, context)
48
+ → output.render/before waterfall (listeners transform {text, context}, MUST next())
49
+ → rule table (first match by priority)
50
+ ├─ rule hit → the named renderer applies (explicit; no other renderer runs)
51
+ └─ no hit → every matching renderer applies in priority order (composition)
52
+ → { original, rendered, rendererId?, changed }
53
+ ```
54
+
55
+ - The waterfall listener contract is the ordinary Cordis waterfall semantics:
56
+ a listener that returns without calling `next()` short-circuits the whole
57
+ pipeline — do that only on purpose.
58
+ - A rule naming an unregistered renderer fails loudly at render time (the
59
+ registry can gain and lose renderers at runtime; silent fallback would
60
+ hide drift).
61
+
62
+ ## Built-in renderers
63
+
64
+ | id | Behavior |
65
+ | --- | --- |
66
+ | `concise` | Collapses whitespace runs and blank-line stacks, caps the presented text at a budget with a `[truncated]` marker. |
67
+ | `step-by-step` | Renumbers list items (dashes, bullets, or digits) consistently from 1; leaves prose untouched. |
68
+
69
+ Their ids mirror the two headline style names, so a rule like
70
+ `{ match: { tool: 'bash' }, style: 'concise' }` reads naturally.
71
+
72
+ ## Per-session / per-tool rules
73
+
74
+ ```yaml
75
+ # cordis.yml, under the dsh-output-styles row
76
+ config:
77
+ rules:
78
+ - match: { tool: bash }
79
+ style: concise
80
+ - match: { tool: read, contentType: text }
81
+ style: step-by-step
82
+ priority: 5
83
+ - match: { session: "session-id-here" }
84
+ style: step-by-step
85
+ ```
86
+
87
+ Matching is exact (no globs except `'*'` for any tool); `match.session`
88
+ scopes a rule to one session. Rules can also be edited in the settings UI
89
+ (`output-style-rules` namespace), where the same shape is validated at
90
+ write time.
91
+
92
+ ## Auditability
93
+
94
+ Presentation never destroys the source:
95
+
96
+ - every result object carries `original` beside `rendered`;
97
+ - the original text of an exported conversation is the session log itself —
98
+ `/export` projects it through the official `deriveEventMessage` surface
99
+ rule, the same rule the harness uses to build model requests;
100
+ - the render application is deterministic (same rules + same renderers in the
101
+ same order), so the rendered output and its source reconstruct together.
102
+
103
+ ## Worked third-party example
104
+
105
+ ```ts
106
+ // my-plugin/renderers.ts
107
+ export const tableCompactor = {
108
+ id: 'sql-table',
109
+ name: 'SQL table compactor',
110
+ description: 'Truncates oversized SQL result sets to the head plus a row count.',
111
+ match: [{ tool: 'sql', contentType: 'text' }],
112
+ priority: 20,
113
+ presenter: (text: string): string => {
114
+ const rows = text.split('\n')
115
+ if (rows.length <= 50) return text
116
+ return [...rows.slice(0, 50), `… ${rows.length - 50} more rows`].join('\n')
117
+ },
118
+ }
119
+
120
+ // my-plugin/index.ts
121
+ export function apply(ctx: Context): void {
122
+ const renderers = ctx.get('outputRenderers') // optional: dsh-output-styles may be absent
123
+ if (renderers !== undefined) {
124
+ ctx.effect(() => renderers.register(tableCompactor))
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Consuming the pipeline
130
+
131
+ ```ts
132
+ const result = await ctx.outputRenderers.renderText(rawText, { tool: 'sql', contentType: 'text' })
133
+ // { original, rendered, rendererId, changed } — log both halves wherever you surface it.
134
+ ```
@@ -0,0 +1,125 @@
1
+ # output.render.* —— dsh-output-styles 渲染器协议
2
+
3
+ > English version: [renderer-protocol.md](renderer-protocol.md)。`/style` 切换行为完全兼容;
4
+ > 本文档只覆盖 0.4.0 新增的呈现层。
5
+
6
+ 渲染器协议把输出呈现变成扩展点:任何插件都可以注册一个**渲染器**——把原始模型可见文本
7
+ 映射为展示文本的纯函数——本插件面向外部的表面(`/export`、`ctx.outputRenderers.renderText`
8
+ 服务)统一经一条可审计的流水线应用它们。
9
+
10
+ ## 渲染器契约
11
+
12
+ ```ts
13
+ interface OutputRenderer {
14
+ id: string // kebab-case,注册表内唯一
15
+ name: string // 人类可读名称
16
+ description: string // 一句话说明
17
+ match: RendererMatch[] // [] = 匹配一切
18
+ priority: number // 数值越大越优先;平局按注册顺序
19
+ presenter: (text: string, context: RenderContext) => string // 纯函数——无 DOM、无 I/O
20
+ }
21
+
22
+ interface RendererMatch {
23
+ tool?: string | string[] // 工具名;'*' = 任意;缺省 = 任意
24
+ contentType?: 'text' | 'markdown' | 'html' | ContentType[]
25
+ }
26
+
27
+ interface RenderContext {
28
+ tool: string // assistant/user 散文为 ''
29
+ contentType: ContentType
30
+ sessionId?: string
31
+ meta?: Record<string, string>
32
+ }
33
+ ```
34
+
35
+ 注册表强制执行的规则(失败大声):
36
+
37
+ - 非法渲染器(id 语法错、缺字段、presenter 非函数、priority 非有限数)在注册时抛错,
38
+ 绝不进入注册表。
39
+ - id 重复抛错;`register()` 返回 disposer,精确移除本次注册——可逆性由调用方的
40
+ `ctx.effect` 负责。
41
+
42
+ ## 渲染流水线
43
+
44
+ ```text
45
+ renderText(text, context)
46
+ → output.render/before waterfall (监听器转换 {text, context},必须 next())
47
+ → 规则表(按优先级取第一条命中)
48
+ ├─ 规则命中 → 只应用该规则指定的渲染器(显式,其余渲染器不参与)
49
+ └─ 未命中 → 按优先级依次应用所有匹配的渲染器(组合)
50
+ → { original, rendered, rendererId?, changed }
51
+ ```
52
+
53
+ - waterfall 监听器契约就是普通 Cordis waterfall 语义:不调用 `next()` 就返回会短路整条
54
+ 流水线——只在有意为之的时候这样做。
55
+ - 规则指向未注册渲染器时在渲染期响亮失败(注册表在运行期可增删渲染器;静默回退会掩盖漂移)。
56
+
57
+ ## 内置渲染器
58
+
59
+ | id | 行为 |
60
+ | --- | --- |
61
+ | `concise` | 折叠空白串与空行堆,在预算处截断并加 `[truncated]` 标记。 |
62
+ | `step-by-step` | 把列表项(短横线、圆点或数字)统一从 1 重新编号;散文保持原样。 |
63
+
64
+ 两个 id 与两大招牌风格同名,因此规则
65
+ `{ match: { tool: 'bash' }, style: 'concise' }` 读起来很自然。
66
+
67
+ ## 按会话 / 按工具规则
68
+
69
+ ```yaml
70
+ # cordis.yml,dsh-output-styles 行的 config 下
71
+ config:
72
+ rules:
73
+ - match: { tool: bash }
74
+ style: concise
75
+ - match: { tool: read, contentType: text }
76
+ style: step-by-step
77
+ priority: 5
78
+ - match: { session: "session-id-here" }
79
+ style: step-by-step
80
+ ```
81
+
82
+ 匹配是精确匹配(除 `'*'` 表示任意工具外没有通配符);`match.session` 把规则限定在一个会话。
83
+ 规则也可以在设置页编辑(`output-style-rules` 命名空间),同一形状在写入时校验。
84
+
85
+ ## 可审计性
86
+
87
+ 呈现绝不销毁来源:
88
+
89
+ - 每个结果对象在 `rendered` 旁边携带 `original`;
90
+ - 导出会话的原始文本就是会话日志本身——`/export` 经官方 `deriveEventMessage` surface
91
+ 规则投影它,与 harness 构建模型请求用的是同一条规则;
92
+ - 渲染应用是确定性的(同样的规则 + 同样顺序的渲染器),因此渲染输出与其来源总是一起重建。
93
+
94
+ ## 第三方示例(完整)
95
+
96
+ ```ts
97
+ // my-plugin/renderers.ts
98
+ export const tableCompactor = {
99
+ id: 'sql-table',
100
+ name: 'SQL table compactor',
101
+ description: 'Truncates oversized SQL result sets to the head plus a row count.',
102
+ match: [{ tool: 'sql', contentType: 'text' }],
103
+ priority: 20,
104
+ presenter: (text: string): string => {
105
+ const rows = text.split('\n')
106
+ if (rows.length <= 50) return text
107
+ return [...rows.slice(0, 50), `… ${rows.length - 50} more rows`].join('\n')
108
+ },
109
+ }
110
+
111
+ // my-plugin/index.ts
112
+ export function apply(ctx: Context): void {
113
+ const renderers = ctx.get('outputRenderers') // 可选依赖:dsh-output-styles 可能未挂载
114
+ if (renderers !== undefined) {
115
+ ctx.effect(() => renderers.register(tableCompactor))
116
+ }
117
+ }
118
+ ```
119
+
120
+ ## 消费流水线
121
+
122
+ ```ts
123
+ const result = await ctx.outputRenderers.renderText(rawText, { tool: 'sql', contentType: 'text' })
124
+ // { original, rendered, rendererId, changed } —— 在任何展示它的地方把两半都记下来。
125
+ ```