dsh-output-styles 0.4.2 → 0.5.0

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to this project are documented in this file. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.5.0] - 2026-08-23
8
+
9
+ ### Added
10
+
11
+ - **`/export --save <path>`**: the export command now writes the rendered
12
+ document to a workspace path, gated by the user-approval service and the fs
13
+ service. `/export [md|markdown|html] [--renderer=<id>] [--save <path>]` — the
14
+ no-argument behavior is unchanged (the document is returned as output text);
15
+ `--save` writes only after `ctx.get('approval')` grants `allowed-once`
16
+ (fail-closed when the service is absent, rejects, cancels, or throws), uses
17
+ `ctx.get('fs')` for the actual write (fail-loud with a structured error when
18
+ absent), and passes the document through `sanitizeText` before writing.
19
+ `md` is accepted as a Markdown alias. `@deepseek-ai/dsh-fs` and
20
+ `@deepseek-ai/dsh-user-approval` are declared as optional peer dependencies.
21
+
22
+ ### Changed
23
+
24
+ - **Standards alignment**: `package.json` declares `packageManager` (`pnpm@11.7.0`,
25
+ matching CI and the lockfile), and the `cordis.patch.yml` reference comment
26
+ now lists the two renderer-protocol config keys (`rules`, `enableExport`)
27
+ added in 0.4.0. No runtime behavior changes.
28
+ - Five READMEs: `/export --save` reference, the `fs:write` workshop permission,
29
+ and the test count refreshed to 127.
30
+
31
+ ## [0.4.3] - 2026-08-22
32
+
33
+ ### Changed
34
+
35
+ - **rc2 compatibility**: every `@deepseek-ai/dsh-*` dependency moves to
36
+ `0.1.1-rc.2` (devDependencies and the runtime storage dependencies pinned);
37
+ the `@deepseek-ai/dsh-session-projection` peer range raises to
38
+ `>=0.1.1-rc.2 <0.2.0` because the projection unit now uses the rc2-only
39
+ `stateSchema` + `wire` definition. The five README compatibility rows, the
40
+ `dshWorkshop` compatibility claim, the compat workflow's rc pins, and the
41
+ workspace `minimumReleaseAgeExclude` follow.
42
+
43
+ ### Fixed
44
+
45
+ - **rc2 session-projection definition**: the `style` projection registers a
46
+ `stateSchema` (persisted fold-state validator) and a client `wire`
47
+ (`viewSchema` + `view`) instead of the rc8 top-level `schema`/`view`, and
48
+ declares its `SessionProjectionStateMap` merge so the `register` overload
49
+ resolves the client-visible unit.
50
+
7
51
  ## [0.4.2] - 2026-08-21
8
52
 
9
53
  ### Changed
package/README.es.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Surface | Status |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 || >=24.0.0` |
29
29
  | Platforms | Todas (host + cliente web) |
30
30
  | Model | Cualquiera (inyección en el prompt del sistema) |
@@ -40,7 +40,7 @@
40
40
  - **Paridad Claude Code** — `keep-coding-instructions`, `force-for-plugin` (alias `force`), compatibilidad JSON `outputStyles`, directorios `stylesDir` por capas, recarga en caliente y fallback del proyecto sobre la costura de settings de DSH.
41
41
  - **Registro de renderers (`output.render.*`)** — `ctx.outputRenderers` permite a cualquier plugin registrar un presenter puro, aplicado a través de la cascada `output.render/before`; renderers integrados `concise` y `step-by-step`.
42
42
  - **Reglas por sesión/por herramienta** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` nombran el renderer para solicitudes coincidentes; editables mediante la sección de settings `output-style-rules`.
43
- - **`/export`** — renderiza la sesión actual a Markdown o HTML saneado a través de la tubería de render; cada render conserva el texto original junto al renderizado.
43
+ - **`/export`** — renderiza la sesión actual a Markdown o HTML saneado a través de la tubería de render; `--save <path>` escribe el documento saneado en esa ruta de workspace tras la aprobación del usuario. Cada render conserva el texto original junto al renderizado.
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ Todos los parámetros son campos Schemastery `Config` (modificables desde cordis
111
111
  | `includeBuiltins` | `true` | Incluir los `styles/` del paquete como capa de menor prioridad |
112
112
  | `watchStyles` | `true` | Recargar la librería cuando un archivo de estilo cambia en disco |
113
113
  | `rules` | `[]` | Reglas de render por sesión/herramienta: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | Registrar el comando `/export` (exportación de sesión Markdown/HTML, consciente del renderer) |
114
+ | `enableExport` | `true` | Registrar el comando `/export` (exportación de sesión Markdown/HTML, consciente del renderer; `--save` escribe con aprobación) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | Lista estilos, cambia o restaura el valor por defecto del proyecto |
121
- | `/export` | command | Renderiza la sesión actual a Markdown o HTML saneado |
121
+ | `/export` | command | Renderiza la sesión actual a Markdown o HTML saneado; `--save` escribe con aprobación |
122
122
  | `output_style` | storage domain | Elección de estilo por sesión, indexada por sessionId |
123
123
  | `systemPrompt.section()` | contribution | Inyecta el cuerpo del estilo actual en cada ensamblado |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + la cascada `output.render/before` |
@@ -135,8 +135,10 @@ Todos los parámetros son campos Schemastery `Config` (modificables desde cordis
135
135
  | `/style off` | Restaura el valor por defecto del proyecto (default de settings, luego `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | Renderiza la sesión actual a Markdown a través de la pipeline de render |
138
+ | `/export md` | Renderiza a Markdown (`md` es la forma abreviada de `markdown`) |
138
139
  | `/export html` | Renderiza a HTML saneado |
139
140
  | `/export --renderer=concise` | Renderiza forzando un renderer (reglas omitidas) |
141
+ | `/export md --save report.md` | Renderiza y luego escribe el documento saneado en `report.md` tras la aprobación |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ Filtrado contra el ecosistema DSH antes del desarrollo (instantánea 2026-08): n
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**: el manifiesto de workshop declara `fs:read`, `fs:watch`, `storage:read`, `storage:write` y `settings:read`.
187
+ - **Permissions**: el manifiesto de workshop declara `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write` y `settings:read`.
186
188
  - **Data**: la elección de estilo vive en el dominio de almacenamiento `output_style` (indexada por sessionId); no se persiste otro estado, sin solicitudes de red.
187
189
  - **Session log**: el nombre del estilo viene de `command/run`, el texto exacto inyectado de `request/header`; el marcador de procedencia `{ kind: 'plugin', plugin: 'dsh-output-styles' }` viaja en el registro del dominio.
188
190
 
@@ -191,6 +193,7 @@ Filtrado contra el ecosistema DSH antes del desarrollo (instantánea 2026-08): n
191
193
  - **Solo servicios públicos.** Contribuye `systemPrompt`, comandos, almacenamiento y settings; sin cambios en engine / agent-loop / apiproxy / UI oficial.
192
194
  - **Visible para el modelo ⟺ registrado.** Todo lo que el modelo ve es reconstruible desde el registro de sesión — sin un nuevo tipo de evento de sesión, sin cambios en el agent-loop.
193
195
  - **Original siempre conservado.** Cada render (y `/export`) conserva el texto original junto al renderizado; para la exportación HTML se usa HTML saneado.
196
+ - **Escrituras en disco controladas.** `/export --save` escribe solo después de que el servicio de aprobación lo conceda, y el contenido escrito pasa primero por la función pura `sanitizeText`; sin un servicio de aprobación o fs no escribe nada (fail-closed).
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ Filtrado contra el ecosistema DSH antes del desarrollo (instantánea 2026-08): n
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # ambos proyectos tsc
206
- pnpm test # vitest — 107 tests
209
+ pnpm test # vitest — 127 tests
207
210
  pnpm run verify # typecheck + tests + self-contained (la puerta de prepublishOnly)
208
211
  pnpm run build # artefactos lib/ (bundles host + client)
209
212
  pnpm pack # tarball para dsh plugin add
package/README.hi.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Surface | Status |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 || >=24.0.0` |
29
29
  | Platforms | सभी (host + वेब क्लाइंट) |
30
30
  | Model | कोई भी (सिस्टम-प्रॉम्प्ट इंजेक्शन) |
@@ -40,7 +40,7 @@
40
40
  - **Claude Code समानता** — `keep-coding-instructions`, `force-for-plugin` (उपनाम `force`), `outputStyles` JSON संगतता, स्तरित `stylesDir` निर्देशिकाएँ, हॉट रीलोड और DSH settings सीम पर परियोजना-डिफ़ॉल्ट फ़ॉलबैक।
41
41
  - **रेंडरर रजिस्ट्री (`output.render.*`)** — `ctx.outputRenderers` किसी भी प्लगइन को एक शुद्ध presenter पंजीकृत करने देता है, जो `output.render/before` वॉटरफ़ॉल से लागू होता है; अंतर्निहित रेंडरर `concise` और `step-by-step`।
42
42
  - **प्रति-सत्र/प्रति-टूल नियम** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` मिलान वाले अनुरोधों के लिए रेंडरर नामित करते हैं; `output-style-rules` settings अनुभाग से संपादन-योग्य।
43
- - **`/export`** — रेंडर पाइपलाइन से वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करता है; हर रेंडर मूल पाठ को रेंडर किए गए के साथ रखता है।
43
+ - **`/export`** — रेंडर पाइपलाइन से वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करता है; `--save <path>` उपयोगकर्ता की स्वीकृति के बाद सैनिटाइज़्ड दस्तावेज़ को उस workspace पथ पर लिखता है। हर रेंडर मूल पाठ को रेंडर किए गए के साथ रखता है।
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ flowchart LR
111
111
  | `includeBuiltins` | `true` | पैकेज के अंतर्निहित `styles/` को निम्नतम-प्राथमिकता परत के रूप में शामिल करें |
112
112
  | `watchStyles` | `true` | डिस्क पर शैली फ़ाइल बदलने पर पुस्तकालय फिर से लोड करें |
113
113
  | `rules` | `[]` | प्रति-सत्र/प्रति-टूल रेंडर नियम: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | `/export` कमांड पंजीकृत करें (Markdown/HTML सत्र निर्यात, रेंडरर-जागरूक) |
114
+ | `enableExport` | `true` | `/export` कमांड पंजीकृत करें (Markdown/HTML सत्र निर्यात, रेंडरर-जागरूक; `--save` स्वीकृति से लिखता है) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | शैलियाँ सूचीबद्ध करें, बदलें या परियोजना डिफ़ॉल्ट बहाल करें |
121
- | `/export` | command | वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करें |
121
+ | `/export` | command | वर्तमान सत्र को Markdown या सैनिटाइज़्ड HTML में प्रस्तुत करें; `--save` स्वीकृति से लिखता है |
122
122
  | `output_style` | storage domain | sessionId से अनुक्रमित सत्र-स्कोप्ड शैली चयन |
123
123
  | `systemPrompt.section()` | contribution | हर संयोजन पर वर्तमान शैली का मुख्य भाग इंजेक्ट करता है |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + `output.render/before` वॉटरफ़ॉल |
@@ -135,8 +135,10 @@ flowchart LR
135
135
  | `/style off` | परियोजना डिफ़ॉल्ट बहाल करें (settings डिफ़ॉल्ट, फिर `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | रेंडर पाइपलाइन से वर्तमान सत्र को Markdown में प्रस्तुत करें |
138
+ | `/export md` | Markdown में प्रस्तुत करें (`md`, `markdown` का संक्षिप्त रूप है) |
138
139
  | `/export html` | सैनिटाइज़्ड HTML में प्रस्तुत करें |
139
140
  | `/export --renderer=concise` | एक रेंडरर बाध्य करके प्रस्तुत करें (नियम छोड़े गए) |
141
+ | `/export md --save report.md` | प्रस्तुत करें, फिर स्वीकृति के बाद सैनिटाइज़्ड दस्तावेज़ को `report.md` में लिखें |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ flowchart LR
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**: workshop मैनिफ़ेस्ट `fs:read`, `fs:watch`, `storage:read`, `storage:write` और `settings:read` घोषित करता है।
187
+ - **Permissions**: workshop मैनिफ़ेस्ट `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write` और `settings:read` घोषित करता है।
186
188
  - **Data**: शैली चयन `output_style` स्टोरेज डोमेन में रहता है (sessionId से अनुक्रमित); कोई अन्य स्थिति स्थायी नहीं, कोई नेटवर्क अनुरोध नहीं।
187
189
  - **Session log**: शैली नाम `command/run` से आता है, सटीक इंजेक्ट किया गया पाठ `request/header` से; स्रोत मार्कर `{ kind: 'plugin', plugin: 'dsh-output-styles' }` डोमेन रिकॉर्ड में चलता है।
188
190
 
@@ -191,6 +193,7 @@ flowchart LR
191
193
  - **केवल सार्वजनिक सेवाएँ।** `systemPrompt`, कमांड, स्टोरेज और settings योगदान करता है; engine / agent-loop / apiproxy / आधिकारिक UI में कोई बदलाव नहीं।
192
194
  - **मॉडल-दृश्य ⟺ लॉग किया गया।** मॉडल जो देखता है वह सब सत्र लॉग से पुनर्निर्माण-योग्य है — कोई नया सत्र घटना प्रकार नहीं, कोई agent-loop बदलाव नहीं।
193
195
  - **मूल हमेशा रखा गया।** हर रेंडर (और `/export`) मूल पाठ को रेंडर किए गए के साथ रखता है; HTML निर्यात के लिए सैनिटाइज़्ड HTML उपयोग होता है।
196
+ - **डिस्क लेखन गेटेड।** `/export --save` केवल स्वीकृति सेवा की अनुमति के बाद लिखता है, और लिखा गया कंटेंट पहले `sanitizeText` शुद्ध फ़ंक्शन से गुज़रता है; स्वीकृति या fs सेवा के बिना कुछ भी नहीं लिखा जाता (fail-closed)।
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ flowchart LR
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # दोनों tsc परियोजनाएँ
206
- pnpm test # vitest — 107 tests
209
+ pnpm test # vitest — 127 tests
207
210
  pnpm run verify # typecheck + tests + self-contained (prepublishOnly द्वार)
208
211
  pnpm run build # lib/ कलाकृतियाँ (host + client बंडल)
209
212
  pnpm pack # dsh plugin add के लिए tarball
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <div align="center">
2
2
 
3
3
  # 🎨 dsh-output-styles
4
+ [![Gitee](https://img.shields.io/badge/Gitee-mirror-c71d23?logo=gitee)](https://gitee.com/perrylink/dsh-output-styles)
4
5
 
5
6
  **Claude Code `outputStyles` for DeepSeek Harness** — switch the model's output style at runtime, per session, durably.
6
7
 
@@ -24,7 +25,7 @@
24
25
 
25
26
  | Surface | Status |
26
27
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
28
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
29
  | Node | `^22.19.0 || >=24.0.0` |
29
30
  | Platforms | All (host + web client) |
30
31
  | Model | Any (system-prompt injection) |
@@ -40,7 +41,7 @@
40
41
  - **Claude Code parity** — `keep-coding-instructions`, `force-for-plugin` (`force` alias), `outputStyles` JSON compatibility, layered `stylesDir` directories, hot reload, and project-default fallback over the DSH settings seam.
41
42
  - **Renderer registry (`output.render.*`)** — `ctx.outputRenderers` lets any plugin register a pure presenter, applied through the `output.render/before` waterfall; built-in renderers `concise` and `step-by-step`.
42
43
  - **Per-session/per-tool rules** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` name the renderer for matching requests; editable through the `output-style-rules` settings section.
43
- - **`/export`** — render the current session to Markdown or sanitized HTML through the render pipeline; every render keeps the original text beside the rendered one.
44
+ - **`/export`** — render the current session to Markdown or sanitized HTML through the render pipeline; `--save <path>` writes the sanitized document to that workspace path after user approval. Every render keeps the original text beside the rendered one.
44
45
 
45
46
  ## Quick start
46
47
 
@@ -111,14 +112,14 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). Inval
111
112
  | `includeBuiltins` | `true` | Include the package's bundled `styles/` as the lowest-priority layer |
112
113
  | `watchStyles` | `true` | Reload the library when a style file changes on disk |
113
114
  | `rules` | `[]` | Per-session/per-tool render rules: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | Register the `/export` command (Markdown/HTML session export, renderer-aware) |
115
+ | `enableExport` | `true` | Register the `/export` command (Markdown/HTML session export, renderer-aware; `--save` writes with approval) |
115
116
 
116
117
  ## Tools & surfaces
117
118
 
118
119
  | Surface | Kind | Notes |
119
120
  |---|---|---|
120
121
  | `/style` | command | List styles, switch, or restore the project default |
121
- | `/export` | command | Render the current session to Markdown or sanitized HTML |
122
+ | `/export` | command | Render the current session to Markdown or sanitized HTML; `--save` writes with approval |
122
123
  | `output_style` | storage domain | Session-scoped style choice, keyed by sessionId |
123
124
  | `systemPrompt.section()` | contribution | Injects the current style body at every assembly |
124
125
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + the `output.render/before` waterfall |
@@ -135,8 +136,10 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). Inval
135
136
  | `/style off` | Restore the project default (settings default, then `defaultStyle`) |
136
137
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
138
  | `/export` | Render the current session to Markdown through the renderer pipeline |
139
+ | `/export md` | Render to Markdown (`md` is the shorthand for `markdown`) |
138
140
  | `/export html` | Render to sanitized HTML |
139
141
  | `/export --renderer=concise` | Render with one renderer forced (rules bypassed) |
142
+ | `/export md --save report.md` | Render, then write the sanitized document to `report.md` after approval |
140
143
 
141
144
  ## Style library
142
145
 
@@ -182,7 +185,7 @@ Screened against the DSH ecosystem before development (2026-08 snapshot): no `st
182
185
 
183
186
  ## Permissions & data
184
187
 
185
- - **Permissions**: declares `fs:read`, `fs:watch`, `storage:read`, `storage:write`, and `settings:read` in its workshop manifest.
188
+ - **Permissions**: declares `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write`, and `settings:read` in its workshop manifest.
186
189
  - **Data**: the style choice lives in the `output_style` storage domain (keyed by sessionId); no other state is persisted, no network requests.
187
190
  - **Session log**: the style name comes from `command/run`, the exact injected text from `request/header`; the provenance marker `{ kind: 'plugin', plugin: 'dsh-output-styles' }` rides in the domain record.
188
191
 
@@ -191,6 +194,7 @@ Screened against the DSH ecosystem before development (2026-08 snapshot): no `st
191
194
  - **Public services only.** Contributes `systemPrompt`, commands, storage, and settings; no engine / agent-loop / apiproxy / official-UI changes.
192
195
  - **Model-visible ⟺ logged.** Everything the model sees is reconstructable from the session log — no new session event type, no agent-loop changes.
193
196
  - **Original always kept.** Every render (and `/export`) keeps the original text beside the rendered one; sanitized HTML is used for HTML export.
197
+ - **Disk writes gated.** `/export --save` writes only after the approval service grants it, and the written content passes through the `sanitizeText` pure function first; without an approval or fs service it writes nothing (fail-closed).
194
198
 
195
199
  ## Known limitations
196
200
 
@@ -203,7 +207,7 @@ Screened against the DSH ecosystem before development (2026-08 snapshot): no `st
203
207
  ```sh
204
208
  pnpm install
205
209
  pnpm run typecheck # both tsc projects
206
- pnpm test # vitest — 107 tests
210
+ pnpm test # vitest — 127 tests
207
211
  pnpm run verify # typecheck + tests + self-contained (the prepublishOnly gate)
208
212
  pnpm run build # lib/ artifacts (host + client bundles)
209
213
  pnpm pack # tarball for dsh plugin add
package/README.pt.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Surface | Status |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 || >=24.0.0` |
29
29
  | Platforms | Todas (host + cliente web) |
30
30
  | Model | Qualquer (injeção no prompt do sistema) |
@@ -40,7 +40,7 @@ O `dsh-output-styles` é o equivalente do `outputStyles` do Claude Code para o D
40
40
  - **Paridade Claude Code** — `keep-coding-instructions`, `force-for-plugin` (alias `force`), compatibilidade JSON `outputStyles`, diretórios `stylesDir` em camadas, recarga a quente e fallback do projeto sobre a costura de settings do DSH.
41
41
  - **Registro de renderers (`output.render.*`)** — `ctx.outputRenderers` permite a qualquer plugin registrar um presenter puro, aplicado pela cascata `output.render/before`; renderers integrados `concise` e `step-by-step`.
42
42
  - **Regras por sessão/por ferramenta** — `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` nomeiam o renderer para solicitações coincidentes; editáveis pela seção de settings `output-style-rules`.
43
- - **`/export`** — renderiza a sessão atual para Markdown ou HTML saneado pela pipeline de render; cada render mantém o texto original ao lado do renderizado.
43
+ - **`/export`** — renderiza a sessão atual para Markdown ou HTML saneado pela pipeline de render; `--save <path>` escreve o documento saneado nessa rota de workspace após aprovação do usuário. Cada render mantém o texto original ao lado do renderizado.
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ Todos os parâmetros são campos Schemastery `Config` (alteráveis pelo cordis.y
111
111
  | `includeBuiltins` | `true` | Incluir os `styles/` do pacote como camada de menor prioridade |
112
112
  | `watchStyles` | `true` | Recarregar a biblioteca quando um arquivo de estilo muda em disco |
113
113
  | `rules` | `[]` | Regras de render por sessão/ferramenta: `[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | Registrar o comando `/export` (exportação de sessão Markdown/HTML, ciente do renderer) |
114
+ | `enableExport` | `true` | Registrar o comando `/export` (exportação de sessão Markdown/HTML, ciente do renderer; `--save` escreve com aprovação) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | Lista estilos, alterna ou restaura o padrão do projeto |
121
- | `/export` | command | Renderiza a sessão atual para Markdown ou HTML saneado |
121
+ | `/export` | command | Renderiza a sessão atual para Markdown ou HTML saneado; `--save` escreve com aprovação |
122
122
  | `output_style` | storage domain | Escolha de estilo por sessão, indexada por sessionId |
123
123
  | `systemPrompt.section()` | contribution | Injeta o corpo do estilo atual a cada montagem |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + a cascata `output.render/before` |
@@ -135,8 +135,10 @@ Todos os parâmetros são campos Schemastery `Config` (alteráveis pelo cordis.y
135
135
  | `/style off` | Restaura o padrão do projeto (default de settings, depois `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | Renderiza a sessão atual para Markdown pela pipeline de render |
138
+ | `/export md` | Renderiza para Markdown (`md` é a forma abreviada de `markdown`) |
138
139
  | `/export html` | Renderiza para HTML saneado |
139
140
  | `/export --renderer=concise` | Renderiza forçando um renderer (regras ignoradas) |
141
+ | `/export md --save report.md` | Renderiza e então escreve o documento saneado em `report.md` após aprovação |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ Filtrado contra o ecossistema DSH antes do desenvolvimento (instantânea 2026-08
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**: o manifesto de workshop declara `fs:read`, `fs:watch`, `storage:read`, `storage:write` e `settings:read`.
187
+ - **Permissions**: o manifesto de workshop declara `fs:read`, `fs:write`, `fs:watch`, `storage:read`, `storage:write` e `settings:read`.
186
188
  - **Data**: a escolha de estilo vive no domínio de armazenamento `output_style` (indexada por sessionId); nenhum outro estado é persistido, sem solicitações de rede.
187
189
  - **Session log**: o nome do estilo vem de `command/run`, o texto exato injetado de `request/header`; o marcador de procedência `{ kind: 'plugin', plugin: 'dsh-output-styles' }` viaja no registro do domínio.
188
190
 
@@ -191,6 +193,7 @@ Filtrado contra o ecossistema DSH antes do desenvolvimento (instantânea 2026-08
191
193
  - **Somente serviços públicos.** Contribui `systemPrompt`, comandos, armazenamento e settings; sem alterações em engine / agent-loop / apiproxy / UI oficial.
192
194
  - **Visível para o modelo ⟺ registrado.** Tudo o que o modelo vê é reconstruível a partir do log de sessão — sem novo tipo de evento de sessão, sem alterações no agent-loop.
193
195
  - **Original sempre conservado.** Cada render (e `/export`) mantém o texto original ao lado do renderizado; a exportação HTML usa HTML saneado.
196
+ - **Escritas em disco controladas.** `/export --save` escreve somente após o serviço de aprovação conceder, e o conteúdo escrito passa primeiro pela função pura `sanitizeText`; sem um serviço de aprovação ou fs, nada é escrito (fail-closed).
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ Filtrado contra o ecossistema DSH antes do desenvolvimento (instantânea 2026-08
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # ambos os projetos tsc
206
- pnpm test # vitest — 107 tests
209
+ pnpm test # vitest — 127 tests
207
210
  pnpm run verify # typecheck + tests + self-contained (a porta de prepublishOnly)
208
211
  pnpm run build # artefatos lib/ (bundles host + client)
209
212
  pnpm pack # tarball para dsh plugin add
package/README.zh.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  | Surface | Status |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8` |
27
+ | Harness | DeepSeek Harness `0.1.1-rc.2` |
28
28
  | Node | `^22.19.0 || >=24.0.0` |
29
29
  | Platforms | 全部(host + Web 客户端) |
30
30
  | Model | 任意(系统提示注入) |
@@ -40,7 +40,7 @@
40
40
  - **Claude Code 对齐** —— `keep-coding-instructions`、`force-for-plugin`(别名 `force`)、`outputStyles` JSON 兼容、分层 `stylesDir` 目录、热重载,以及通过 DSH settings 接缝的项目默认回退。
41
41
  - **渲染器注册表(`output.render.*`)** —— `ctx.outputRenderers` 允许任意插件注册纯 presenter,经 `output.render/before` waterfall 应用;内置渲染器 `concise` 与 `step-by-step`。
42
42
  - **按会话/按工具规则** —— `rules: [{ match: { tool: 'bash' }, style: 'concise' }]` 为匹配请求指定渲染器;可通过 `output-style-rules` 设置区编辑。
43
- - **`/export`** —— 经渲染管线把当前会话导出为 Markdown 或净化 HTML;每次渲染都保留原文与渲染结果并列。
43
+ - **`/export`** —— 经渲染管线把当前会话导出为 Markdown 或净化 HTML;`--save <path>` 经用户审批后把净化文档写入该工作区路径。每次渲染都保留原文与渲染结果并列。
44
44
 
45
45
  ## Quick start
46
46
 
@@ -111,14 +111,14 @@ flowchart LR
111
111
  | `includeBuiltins` | `true` | 将包内置 `styles/` 作为最低优先级层 |
112
112
  | `watchStyles` | `true` | 风格文件在磁盘上变化时重载库 |
113
113
  | `rules` | `[]` | 按会话/按工具渲染规则:`[{ match: { tool?, contentType?, session? }, style, priority? }]` |
114
- | `enableExport` | `true` | 注册 `/export` 命令(Markdown/HTML 会话导出,感知渲染器) |
114
+ | `enableExport` | `true` | 注册 `/export` 命令(Markdown/HTML 会话导出,感知渲染器;`--save` 经审批写入) |
115
115
 
116
116
  ## Tools & surfaces
117
117
 
118
118
  | Surface | Kind | Notes |
119
119
  |---|---|---|
120
120
  | `/style` | command | 列出风格、切换或恢复项目默认 |
121
- | `/export` | command | 把当前会话渲染为 Markdown 或净化 HTML |
121
+ | `/export` | command | 把当前会话渲染为 Markdown 或净化 HTML;`--save` 经审批写入 |
122
122
  | `output_style` | storage domain | 按 sessionId 隔离的会话级风格选择 |
123
123
  | `systemPrompt.section()` | contribution | 在每次组装时注入当前风格正文 |
124
124
  | `output.render.*` | renderer registry | `ctx.outputRenderers` + `output.render/before` waterfall |
@@ -135,8 +135,10 @@ flowchart LR
135
135
  | `/style off` | 恢复项目默认(settings 默认,其次 `defaultStyle`) |
136
136
  | `/style nope` | `error: unknown output style "nope" (available: …)` |
137
137
  | `/export` | 经渲染管线把当前会话渲染为 Markdown |
138
+ | `/export md` | 渲染为 Markdown(`md` 是 `markdown` 的简写) |
138
139
  | `/export html` | 渲染为净化 HTML |
139
140
  | `/export --renderer=concise` | 强制指定一个渲染器渲染(跳过规则) |
141
+ | `/export md --save report.md` | 渲染后经审批把净化文档写入 `report.md` |
140
142
 
141
143
  ## Style library
142
144
 
@@ -182,7 +184,7 @@ flowchart LR
182
184
 
183
185
  ## Permissions & data
184
186
 
185
- - **Permissions**:workshop 清单声明 `fs:read`、`fs:watch`、`storage:read`、`storage:write` 与 `settings:read`。
187
+ - **Permissions**:workshop 清单声明 `fs:read`、`fs:write`、`fs:watch`、`storage:read`、`storage:write` 与 `settings:read`。
186
188
  - **Data**:风格选择存于 `output_style` 存储域(按 sessionId 隔离);不持久化其他状态,无网络请求。
187
189
  - **Session log**:风格名来自 `command/run`,精确注入文本来自 `request/header`;来源标记 `{ kind: 'plugin', plugin: 'dsh-output-styles' }` 随域记录携带。
188
190
 
@@ -191,6 +193,7 @@ flowchart LR
191
193
  - **仅公开服务。** 贡献 `systemPrompt`、命令、存储与 settings;不改 engine / agent-loop / apiproxy / 官方 UI。
192
194
  - **模型可见 ⟺ 已记录。** 模型所见的一切都能从会话日志重建 —— 无新增会话事件类型、无 agent-loop 改动。
193
195
  - **始终保留原文。** 每次渲染(含 `/export`)都保留原文与渲染结果并列;HTML 导出使用净化 HTML。
196
+ - **写盘有门禁。** `/export --save` 仅在审批服务放行后写入,且写入内容先经 `sanitizeText` 纯函数净化;缺少审批或 fs 服务时一律不写入(fail-closed)。
194
197
 
195
198
  ## Known limitations
196
199
 
@@ -203,7 +206,7 @@ flowchart LR
203
206
  ```sh
204
207
  pnpm install
205
208
  pnpm run typecheck # 两个 tsc 项目
206
- pnpm test # vitest —— 107 个测试
209
+ pnpm test # vitest —— 127 个测试
207
210
  pnpm run verify # typecheck + tests + self-contained(prepublishOnly 门禁)
208
211
  pnpm run build # lib/ 产物(host + client 包)
209
212
  pnpm pack # 供 dsh plugin add 的 tarball
package/cordis.patch.yml CHANGED
@@ -31,6 +31,11 @@
31
31
  # truncationMarker: "\n\n[style truncated]"
32
32
  # includeBuiltins: true
33
33
  # watchStyles: true
34
+ # rules: [] # per-session/per-tool render rules
35
+ # # [{ match: { tool?, contentType?, session? }, style, priority? }]
36
+ # enableExport: true # register the /export command
37
+ # # (md|html; --save <path> writes the
38
+ # # sanitized document after approval)
34
39
 
35
40
  # Optional invariant companion (envelope-level checks). Only for profiles
36
41
  # that DISABLE the main row above: the main plugin already registers
@@ -132,3 +132,13 @@ export function apply(ctx: Context): void {
132
132
  const result = await ctx.outputRenderers.renderText(rawText, { tool: 'sql', contentType: 'text' })
133
133
  // { original, rendered, rendererId, changed } — log both halves wherever you surface it.
134
134
  ```
135
+
136
+ ## Export to disk
137
+
138
+ `/export` returns the rendered document as command output text. `/export
139
+ [md|markdown|html] [--renderer=<id>] --save <path>` additionally writes the
140
+ document to a workspace path: the document passes through the `sanitizeText`
141
+ pure function first, then the write is gated by the approval service
142
+ (`ctx.get('approval')`, fail-closed when absent) and performed by the fs
143
+ service (`ctx.get('fs')`, fail-loud when absent). The render pipeline itself is
144
+ unchanged — the same presenters and rule table apply before either output.
@@ -123,3 +123,11 @@ export function apply(ctx: Context): void {
123
123
  const result = await ctx.outputRenderers.renderText(rawText, { tool: 'sql', contentType: 'text' })
124
124
  // { original, rendered, rendererId, changed } —— 在任何展示它的地方把两半都记下来。
125
125
  ```
126
+
127
+ ## 导出到磁盘
128
+
129
+ `/export` 把渲染后的文档作为命令输出文本返回。`/export [md|markdown|html]
130
+ [--renderer=<id>] --save <path>` 另外把文档写入工作区路径:文档先经 `sanitizeText`
131
+ 纯函数净化,随后写入由审批服务(`ctx.get('approval')`,缺失则 fail-closed)把关、由
132
+ fs 服务(`ctx.get('fs')`,缺失则大声失败)执行。渲染流水线本身不变——两种输出之前都应用
133
+ 同样的 presenter 与规则表。
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as styleSelectionSchema, c as loadStyleLibrary, d as STYLE_COMMAND, f as applyStyleEvent, g as STYLE_SOURCE, h as OUTPUT_STYLE_DOMAIN, i as installInvariant, l as truncateStyle, m as OFF, o as STYLE_NAME_RE, p as parseStyleInput, s as isValidStyleName, t as PACKAGE_NAME, u as EMPTY_STYLE_STATE, v as styleSelectionViewSchema } from "./invariant-CEWlfnrw.js";
1
+ import { _ as styleFoldStateSchema, c as loadStyleLibrary, d as STYLE_COMMAND, f as applyStyleEvent, g as STYLE_SOURCE, h as OUTPUT_STYLE_DOMAIN, i as installInvariant, l as truncateStyle, m as OFF, o as STYLE_NAME_RE, p as parseStyleInput, s as isValidStyleName, t as PACKAGE_NAME, u as EMPTY_STYLE_STATE, v as styleSelectionSchema, y as styleSelectionViewSchema } from "./invariant-ExQ574yb.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -732,10 +732,13 @@ async function apply(ctx, config) {
732
732
  ctx.inject(["sessionProjections"], (projectionCtx) => {
733
733
  projectionCtx.sessionProjections.register({
734
734
  key: "style",
735
- schema: styleSelectionViewSchema,
735
+ stateSchema: styleFoldStateSchema,
736
736
  init: () => EMPTY_STYLE_STATE,
737
737
  apply: applyStyleEvent,
738
- view: (state) => viewStyleSelection(runtime, state),
738
+ wire: {
739
+ viewSchema: styleSelectionViewSchema,
740
+ view: (state) => viewStyleSelection(runtime, state)
741
+ },
739
742
  stateVersion: 2
740
743
  });
741
744
  });
@@ -791,12 +794,12 @@ async function apply(ctx, config) {
791
794
  commandCtx.commands.register({
792
795
  name: "export",
793
796
  description: "Export this session as Markdown or HTML (renderer-aware)",
794
- input: { hint: "[markdown|html] [--renderer=<id>]" },
795
- handler: async ({ agent, rawInput }) => {
797
+ input: { hint: "[markdown|html] [--renderer=<id>] [--save <path>]" },
798
+ handler: async ({ agent, rawInput, signal }) => {
796
799
  const input = parseExportInput(rawInput);
797
800
  if (input.kind === "error") return {
798
801
  kind: "error",
799
- text: "usage: /export [markdown|html] [--renderer=<id>]"
802
+ text: "usage: /export [markdown|html] [--renderer=<id>] [--save <path>]"
800
803
  };
801
804
  const lines = conversationLines(agent.session.events);
802
805
  const rules = input.renderer === void 0 ? [...effectiveRules] : [{
@@ -804,28 +807,57 @@ async function apply(ctx, config) {
804
807
  style: input.renderer,
805
808
  priority: 0
806
809
  }];
810
+ const document = renderExport(renderers, lines, input.format, rules);
811
+ if (input.save === void 0) return {
812
+ kind: "success",
813
+ text: document.text
814
+ };
815
+ const saved = await saveExportFile(ctx.get("fs"), ctx.get("approval"), agent, input.save, sanitizeText(document.text), signal);
816
+ if (saved.kind === "error") return {
817
+ kind: "error",
818
+ text: saved.text
819
+ };
807
820
  return {
808
821
  kind: "success",
809
- text: renderExport(renderers, lines, input.format, rules).text
822
+ text: `saved ${input.format} export to ${saved.path}`
810
823
  };
811
824
  }
812
825
  });
813
826
  });
814
827
  }
815
- /** Parse `/export [markdown|html] [--renderer=<id>]` from the raw command input. */
828
+ /** Parse `/export [markdown|html] [--renderer=<id>] [--save <path>]` from the raw command input. */
816
829
  function parseExportInput(rawInput) {
817
830
  const raw = String(rawInput ?? "").trim();
818
831
  const parts = raw === "" ? [] : raw.split(/\s+/);
819
832
  let format = "markdown";
820
833
  let renderer;
821
- for (const part of parts) {
822
- if (part === "markdown" || part === "html") {
823
- format = part;
834
+ let save;
835
+ for (let index = 0; index < parts.length; index += 1) {
836
+ const part = parts[index];
837
+ if (part === void 0) continue;
838
+ if (part === "markdown" || part === "md") {
839
+ format = "markdown";
840
+ continue;
841
+ }
842
+ if (part === "html") {
843
+ format = "html";
844
+ continue;
845
+ }
846
+ const rendererMatch = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part);
847
+ if (rendererMatch !== null) {
848
+ renderer = rendererMatch[1];
824
849
  continue;
825
850
  }
826
- const match = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part);
827
- if (match !== null) {
828
- renderer = match[1];
851
+ const saveInline = /^--save=(.+)$/.exec(part);
852
+ if (saveInline !== null) {
853
+ save = saveInline[1];
854
+ continue;
855
+ }
856
+ if (part === "--save") {
857
+ const next = parts[index + 1];
858
+ if (next === void 0 || next === "") return { kind: "error" };
859
+ save = next;
860
+ index += 1;
829
861
  continue;
830
862
  }
831
863
  return { kind: "error" };
@@ -833,7 +865,69 @@ function parseExportInput(rawInput) {
833
865
  return {
834
866
  kind: "ok",
835
867
  format,
836
- ...renderer === void 0 ? {} : { renderer }
868
+ ...renderer === void 0 ? {} : { renderer },
869
+ ...save === void 0 ? {} : { save }
870
+ };
871
+ }
872
+ /**
873
+ * Write one rendered export document to a workspace path after user approval.
874
+ * Fail-closed: a missing approval service, a rejected/cancelled/unavailable
875
+ * decision, or a throwing approval channel all deny the write; a missing fs
876
+ * service fails loudly with a structured error. The caller already sanitized
877
+ * `content` before this write.
878
+ * @param fs - the fs service (`ctx.get('fs')`), or undefined when none is composed.
879
+ * @param approval - the approval service (`ctx.get('approval')`), or undefined when none is composed.
880
+ * @param agent - the agent whose session the export belongs to (routes the approval).
881
+ * @param path - the workspace path to write.
882
+ * @param content - the sanitized document text.
883
+ * @param signal - the command's abort signal, forwarded to approval and resolve.
884
+ * @returns the written path, or a structured failure.
885
+ */
886
+ async function saveExportFile(fs, approval, agent, path, content, signal) {
887
+ if (approval === void 0) return {
888
+ kind: "error",
889
+ code: "approval-unavailable",
890
+ text: "dsh-output-styles: /export --save requires an approval service (compose @deepseek-ai/dsh-user-approval); nothing was written"
891
+ };
892
+ let outcome;
893
+ try {
894
+ outcome = await approval.request({
895
+ agent,
896
+ toolName: "export",
897
+ reason: `write the exported document to ${path}`,
898
+ ...signal === void 0 ? {} : { signal }
899
+ });
900
+ } catch {
901
+ outcome = "unavailable";
902
+ }
903
+ switch (outcome) {
904
+ case "allowed-once": break;
905
+ case "rejected": return {
906
+ kind: "error",
907
+ code: "approval-denied",
908
+ text: "dsh-output-styles: /export --save was rejected; nothing was written"
909
+ };
910
+ case "cancelled": return {
911
+ kind: "error",
912
+ code: "approval-cancelled",
913
+ text: "dsh-output-styles: /export --save was cancelled; nothing was written"
914
+ };
915
+ default: return {
916
+ kind: "error",
917
+ code: "approval-unavailable",
918
+ text: "dsh-output-styles: /export --save approval is unavailable; nothing was written"
919
+ };
920
+ }
921
+ if (fs === void 0) return {
922
+ kind: "error",
923
+ code: "fs-unavailable",
924
+ text: "dsh-output-styles: /export --save requires an fs service (compose @deepseek-ai/dsh-fs); nothing was written"
925
+ };
926
+ const target = await fs.resolve(path, signal === void 0 ? void 0 : { signal });
927
+ await fs.writeText(target, content);
928
+ return {
929
+ kind: "written",
930
+ path
837
931
  };
838
932
  }
839
933
  //#endregion
@@ -857,4 +951,4 @@ const name = "dsh-output-styles";
857
951
  */
858
952
  const inject = ["systemPrompt", "storageDomain"];
859
953
  //#endregion
860
- export { Config, DEFAULT_STYLES_DIR, EMPTY_STYLE_STATE, OFF, OUTPUT_STYLE_DOMAIN, OutputStyleRuntime, PACKAGE_NAME, STYLE_COMMAND, STYLE_NAME_RE, STYLE_SECTION_NAME, STYLE_SOURCE, apply, applyStyleEvent, inject, installInvariant, isValidStyleName, loadStyleLibrary, name, parseStyleInput, resolveConfig, styleSelectionSchema, styleSelectionViewSchema, truncateStyle };
954
+ export { Config, DEFAULT_STYLES_DIR, EMPTY_STYLE_STATE, OFF, OUTPUT_STYLE_DOMAIN, OutputStyleRuntime, PACKAGE_NAME, STYLE_COMMAND, STYLE_NAME_RE, STYLE_SECTION_NAME, STYLE_SOURCE, apply, applyStyleEvent, inject, installInvariant, isValidStyleName, loadStyleLibrary, name, parseStyleInput, resolveConfig, styleFoldStateSchema, styleSelectionSchema, styleSelectionViewSchema, truncateStyle };
@@ -53,6 +53,19 @@ const styleSelectionViewSchema = z.object({
53
53
  })),
54
54
  currentValue: z.string().min(1).nullable()
55
55
  });
56
+ /**
57
+ * Validates the `style` projection's persisted fold state before it seeds a
58
+ * fold (the `stateSchema` of the registered unit). The state is plain JSON:
59
+ * the settled selection (`current`, null when off) plus the in-flight switch
60
+ * parked by `command/run` and resolved by its paired `command/done`.
61
+ */
62
+ const styleFoldStateSchema = z.object({
63
+ current: z.string().min(1).nullable(),
64
+ pending: z.object({
65
+ commandId: z.string().min(1),
66
+ target: z.union([z.object({ name: z.string().min(1) }), z.object({ off: z.literal(true) })])
67
+ }).nullable()
68
+ });
56
69
  //#endregion
57
70
  //#region lib/types/style-command.js
58
71
  /**
@@ -495,4 +508,4 @@ function getInvariantRegistry(ctx) {
495
508
  */
496
509
  const apply = (ctx) => Promise.resolve(getInvariantRegistry(ctx).register(PACKAGE_NAME, installInvariant(COMPANION_FACTS)));
497
510
  //#endregion
498
- export { styleSelectionSchema as _, name as a, loadStyleLibrary as c, STYLE_COMMAND as d, applyStyleEvent as f, STYLE_SOURCE as g, OUTPUT_STYLE_DOMAIN as h, installInvariant as i, truncateStyle as l, OFF as m, apply as n, STYLE_NAME_RE as o, parseStyleInput as p, inject as r, isValidStyleName as s, PACKAGE_NAME as t, EMPTY_STYLE_STATE as u, styleSelectionViewSchema as v };
511
+ export { styleFoldStateSchema as _, name as a, loadStyleLibrary as c, STYLE_COMMAND as d, applyStyleEvent as f, STYLE_SOURCE as g, OUTPUT_STYLE_DOMAIN as h, installInvariant as i, truncateStyle as l, OFF as m, apply as n, STYLE_NAME_RE as o, parseStyleInput as p, inject as r, isValidStyleName as s, PACKAGE_NAME as t, EMPTY_STYLE_STATE as u, styleSelectionSchema as v, styleSelectionViewSchema as y };
package/lib/invariant.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as name, i as installInvariant, n as apply, r as inject, t as PACKAGE_NAME } from "./invariant-CEWlfnrw.js";
1
+ import { a as name, i as installInvariant, n as apply, r as inject, t as PACKAGE_NAME } from "./invariant-ExQ574yb.js";
2
2
  export { PACKAGE_NAME, apply, inject, installInvariant, name };
@@ -23,7 +23,7 @@ export { applyStyleEvent, EMPTY_STYLE_STATE, parseStyleInput, STYLE_COMMAND, } f
23
23
  export type { StyleFoldState, StyleInput } from './style-command.js';
24
24
  export { isValidStyleName, loadStyleLibrary, STYLE_NAME_RE, truncateStyle, } from './style-library.js';
25
25
  export type { OutputStyle } from './style-library.js';
26
- export { OFF, OUTPUT_STYLE_DOMAIN, STYLE_SOURCE, styleSelectionSchema, styleSelectionViewSchema, } from './types.js';
26
+ export { OFF, OUTPUT_STYLE_DOMAIN, STYLE_SOURCE, styleFoldStateSchema, styleSelectionSchema, styleSelectionViewSchema, } from './types.js';
27
27
  export type { StyleOption, StyleSelection, StyleSelectionView } from './types.js';
28
28
  export { installInvariant, PACKAGE_NAME } from './invariant.js';
29
29
  export type { InvariantFacts, InvariantInstaller, InvariantRegistry } from './invariant.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,UAAoC,CAAA;AAEvD,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAChG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EACL,GAAG,EACH,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACjF,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC/D,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAG3F,mBAAmB,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,UAAoC,CAAA;AAEvD,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAChG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EACL,GAAG,EACH,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACjF,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC/D,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAG3F,mBAAmB,YAAY,CAAA"}
@@ -146,10 +146,56 @@ type ExportInput = {
146
146
  kind: 'ok';
147
147
  format: 'markdown' | 'html';
148
148
  renderer?: string;
149
+ save?: string;
149
150
  } | {
150
151
  kind: 'error';
151
152
  };
152
- /** Parse `/export [markdown|html] [--renderer=<id>]` from the raw command input. */
153
+ /** Parse `/export [markdown|html] [--renderer=<id>] [--save <path>]` from the raw command input. */
153
154
  export declare function parseExportInput(rawInput: unknown): ExportInput;
155
+ /** Approval outcome vocabulary, structural (mirrors the approval seam without importing it). */
156
+ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
157
+ /** Structural slice of the DSH filesystem service the save path uses. */
158
+ export interface ExportFileSystem {
159
+ resolve(path: string, opts?: {
160
+ cwd?: string;
161
+ signal?: AbortSignal;
162
+ }): Promise<unknown>;
163
+ writeText(target: unknown, content: string): Promise<unknown>;
164
+ }
165
+ /** Structural slice of the DSH approval service the save path uses. */
166
+ export interface ExportApproval {
167
+ request(request: {
168
+ agent: unknown;
169
+ toolName: string;
170
+ reason: string;
171
+ signal?: AbortSignal;
172
+ }): Promise<ApprovalOutcome>;
173
+ }
174
+ /** Error codes of the `/export --save` path; each is a stable machine-readable label. */
175
+ export type ExportSaveErrorCode = 'fs-unavailable' | 'approval-unavailable' | 'approval-denied' | 'approval-cancelled';
176
+ /** Result of a `/export --save` attempt: the written path or a structured failure. */
177
+ export type ExportSaveResult = {
178
+ readonly kind: 'written';
179
+ readonly path: string;
180
+ } | {
181
+ readonly kind: 'error';
182
+ readonly code: ExportSaveErrorCode;
183
+ readonly text: string;
184
+ };
185
+ /**
186
+ * Write one rendered export document to a workspace path after user approval.
187
+ * Fail-closed: a missing approval service, a rejected/cancelled/unavailable
188
+ * decision, or a throwing approval channel all deny the write; a missing fs
189
+ * service fails loudly with a structured error. The caller already sanitized
190
+ * `content` before this write.
191
+ * @param fs - the fs service (`ctx.get('fs')`), or undefined when none is composed.
192
+ * @param approval - the approval service (`ctx.get('approval')`), or undefined when none is composed.
193
+ * @param agent - the agent whose session the export belongs to (routes the approval).
194
+ * @param path - the workspace path to write.
195
+ * @param content - the sanitized document text.
196
+ * @param signal - the command's abort signal, forwarded to approval and resolve.
197
+ * @returns the written path, or a structured failure.
198
+ */
199
+ export declare function saveExportFile(fs: ExportFileSystem | undefined, approval: ExportApproval | undefined, agent: unknown, path: string, content: string, signal?: AbortSignal): Promise<ExportSaveResult>;
154
200
  export {};
155
201
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@
10
10
  import type { SessionId } from '@deepseek-ai/dsh-session';
11
11
  import { z as zod } from 'zod';
12
12
  import type { OutputRenderer, RenderContext, RenderedText } from './renderers.js';
13
+ import type { StyleFoldState } from './style-command.js';
13
14
  /** The reserved switch target that removes a session's selection. */
14
15
  export declare const OFF = "off";
15
16
  /**
@@ -71,10 +72,30 @@ export declare const styleSelectionViewSchema: zod.ZodObject<{
71
72
  }, zod.core.$strip>>;
72
73
  currentValue: zod.ZodNullable<zod.ZodString>;
73
74
  }, zod.core.$strip>;
75
+ /**
76
+ * Validates the `style` projection's persisted fold state before it seeds a
77
+ * fold (the `stateSchema` of the registered unit). The state is plain JSON:
78
+ * the settled selection (`current`, null when off) plus the in-flight switch
79
+ * parked by `command/run` and resolved by its paired `command/done`.
80
+ */
81
+ export declare const styleFoldStateSchema: zod.ZodObject<{
82
+ current: zod.ZodNullable<zod.ZodString>;
83
+ pending: zod.ZodNullable<zod.ZodObject<{
84
+ commandId: zod.ZodString;
85
+ target: zod.ZodUnion<readonly [zod.ZodObject<{
86
+ name: zod.ZodString;
87
+ }, zod.core.$strip>, zod.ZodObject<{
88
+ off: zod.ZodLiteral<true>;
89
+ }, zod.core.$strip>]>;
90
+ }, zod.core.$strip>>;
91
+ }, zod.core.$strip>;
74
92
  declare module '@deepseek-ai/dsh-session-projection/types' {
75
93
  interface SessionProjectionMap {
76
94
  style: StyleSelectionView;
77
95
  }
96
+ interface SessionProjectionStateMap {
97
+ style: StyleFoldState;
98
+ }
78
99
  }
79
100
  /** The `ctx.outputRenderers` service: the output.render.* renderer registry. */
80
101
  export interface OutputRenderersService {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,EAAE,CAAC,IAAI,GAAG,EAAE,MAAM,KAAK,CAAA;AAE9B,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAEjF,qEAAqE;AACrE,eAAO,MAAM,GAAG,QAAQ,CAAA;AAExB;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;CAA2D,CAAA;AAEpF,gDAAgD;AAChD,eAAO,MAAM,oBAAoB;;;;;;mBAQ/B,CAAA;AAEF,kDAAkD;AAClD,MAAM,WAAW,cAAe,SAAQ,GAAG,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC;CAAG;AAEjF;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;CAM9B,CAAA;AAEF,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAA;IACnB,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,iFAAiF;AACjF,eAAO,MAAM,wBAAwB;;;;;;;;mBAQnC,CAAA;AAEF,OAAO,QAAQ,2CAA2C,CAAC;IACzD,UAAU,oBAAoB;QAC5B,KAAK,EAAE,kBAAkB,CAAA;KAC1B;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAA;IAC9C,wFAAwF;IACxF,IAAI,IAAI,cAAc,EAAE,CAAA;IACxB,4DAA4D;IAC5D,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,EAAE,CAAA;IACjD,gGAAgG;IAChG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CACxE;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,2EAA2E;QAC3E,eAAe,EAAE,sBAAsB,CAAA;KACxC;IACD,UAAU,MAAM;QACd;;;;WAIG;QACH,sBAAsB,CAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,aAAa,CAAA;SAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,aAAa,CAAA;SAAE,KAAK,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;KACrL;CACF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,EAAE,CAAC,IAAI,GAAG,EAAE,MAAM,KAAK,CAAA;AAE9B,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,qEAAqE;AACrE,eAAO,MAAM,GAAG,QAAQ,CAAA;AAExB;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;CAA2D,CAAA;AAEpF,gDAAgD;AAChD,eAAO,MAAM,oBAAoB;;;;;;mBAQ/B,CAAA;AAEF,kDAAkD;AAClD,MAAM,WAAW,cAAe,SAAQ,GAAG,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC;CAAG;AAEjF;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;CAM9B,CAAA;AAEF,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAA;IACnB,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,iFAAiF;AACjF,eAAO,MAAM,wBAAwB;;;;;;;;mBAQnC,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;mBAS/B,CAAA;AAEF,OAAO,QAAQ,2CAA2C,CAAC;IACzD,UAAU,oBAAoB;QAC5B,KAAK,EAAE,kBAAkB,CAAA;KAC1B;IACD,UAAU,yBAAyB;QACjC,KAAK,EAAE,cAAc,CAAA;KACtB;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAA;IAC9C,wFAAwF;IACxF,IAAI,IAAI,cAAc,EAAE,CAAA;IACxB,4DAA4D;IAC5D,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,EAAE,CAAA;IACjD,gGAAgG;IAChG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CACxE;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,2EAA2E;QAC3E,eAAe,EAAE,sBAAsB,CAAA;KACxC;IACD,UAAU,MAAM;QACd;;;;WAIG;QACH,sBAAsB,CAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,aAAa,CAAA;SAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,aAAa,CAAA;SAAE,KAAK,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;KACrL;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-output-styles",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Claude Code outputStyles-equivalent runtime output-style switching for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -17,6 +17,7 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
+ "packageManager": "pnpm@11.7.0",
20
21
  "engines": {
21
22
  "node": "^22.19.0 || >=24.0.0"
22
23
  },
@@ -99,6 +100,7 @@
99
100
  },
100
101
  "permissions": [
101
102
  "fs:read",
103
+ "fs:write",
102
104
  "fs:watch",
103
105
  "storage:read",
104
106
  "storage:write",
@@ -106,7 +108,7 @@
106
108
  ],
107
109
  "compatibility": {
108
110
  "dshVersions": [
109
- "0.1.0-rc.8"
111
+ "0.1.1-rc.2"
110
112
  ]
111
113
  },
112
114
  "capability": {
@@ -123,21 +125,29 @@
123
125
  }
124
126
  },
125
127
  "dependencies": {
126
- "@deepseek-ai/dsh-storage": "0.1.0-rc.8",
127
- "@deepseek-ai/dsh-storage-domain": "0.1.0-rc.8",
128
- "@deepseek-ai/dsh-storage-json": "0.1.0-rc.8",
128
+ "@deepseek-ai/dsh-storage": "0.1.1-rc.2",
129
+ "@deepseek-ai/dsh-storage-domain": "0.1.1-rc.2",
130
+ "@deepseek-ai/dsh-storage-json": "0.1.1-rc.2",
129
131
  "yaml": "^2.8.0",
130
132
  "zod": "^4.4.3"
131
133
  },
132
134
  "peerDependencies": {
133
135
  "@deepseek-ai/cordis": "^4.0.1",
136
+ "@deepseek-ai/dsh-fs": ">=0.1.0-rc.8 <0.2.0",
134
137
  "@deepseek-ai/dsh-session": ">=0.1.0-rc.8 <0.2.0",
135
- "@deepseek-ai/dsh-session-projection": ">=0.1.0-rc.8 <0.2.0",
138
+ "@deepseek-ai/dsh-session-projection": ">=0.1.1-rc.2 <0.2.0",
136
139
  "@deepseek-ai/dsh-settings": ">=0.1.0-rc.8 <0.2.0",
137
140
  "@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.8 <0.2.0",
141
+ "@deepseek-ai/dsh-user-approval": ">=0.1.0-rc.8 <0.2.0",
138
142
  "@deepseek-ai/schemastery": "^3.18.1"
139
143
  },
140
144
  "peerDependenciesMeta": {
145
+ "@deepseek-ai/dsh-fs": {
146
+ "optional": true
147
+ },
148
+ "@deepseek-ai/dsh-user-approval": {
149
+ "optional": true
150
+ },
141
151
  "@deepseek-ai/dsh-settings": {
142
152
  "optional": true
143
153
  },
@@ -161,29 +171,29 @@
161
171
  "@deepseek-ai/cordis": "4.0.1",
162
172
  "@deepseek-ai/cordis-plugin-include": "1.0.6",
163
173
  "@deepseek-ai/cordis-plugin-loader": "1.0.2",
164
- "@deepseek-ai/dsh-agent": "0.1.0-rc.8",
165
- "@deepseek-ai/dsh-api-remotes": "0.1.0-rc.8",
166
- "@deepseek-ai/dsh-attachment": "0.1.0-rc.8",
167
- "@deepseek-ai/dsh-brand": "0.1.0-rc.8",
168
- "@deepseek-ai/dsh-client-locale": "0.1.0-rc.8",
169
- "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.8",
170
- "@deepseek-ai/dsh-client-ui-commands": "0.1.0-rc.8",
171
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.0-rc.8",
172
- "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.8",
173
- "@deepseek-ai/dsh-commands": "0.1.0-rc.8",
174
- "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
175
- "@deepseek-ai/dsh-scope": "0.1.0-rc.8",
176
- "@deepseek-ai/dsh-session": "0.1.0-rc.8",
177
- "@deepseek-ai/dsh-session-persistence": "0.1.0-rc.8",
178
- "@deepseek-ai/dsh-session-persistence-jsonl": "0.1.0-rc.8",
179
- "@deepseek-ai/dsh-session-projection": "0.1.0-rc.8",
180
- "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
181
- "@deepseek-ai/dsh-storage": "0.1.0-rc.8",
182
- "@deepseek-ai/dsh-storage-domain": "0.1.0-rc.8",
183
- "@deepseek-ai/dsh-storage-json": "0.1.0-rc.8",
184
- "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.8",
185
- "@deepseek-ai/dsh-timeout": "0.1.0-rc.8",
186
- "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.8",
174
+ "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
175
+ "@deepseek-ai/dsh-api-remotes": "0.1.1-rc.2",
176
+ "@deepseek-ai/dsh-attachment": "0.1.1-rc.2",
177
+ "@deepseek-ai/dsh-brand": "0.1.1-rc.2",
178
+ "@deepseek-ai/dsh-client-locale": "0.1.1-rc.2",
179
+ "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
180
+ "@deepseek-ai/dsh-client-ui-commands": "0.1.1-rc.2",
181
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.1-rc.2",
182
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
183
+ "@deepseek-ai/dsh-commands": "0.1.1-rc.2",
184
+ "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
185
+ "@deepseek-ai/dsh-scope": "0.1.1-rc.2",
186
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
187
+ "@deepseek-ai/dsh-session-persistence": "0.1.1-rc.2",
188
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.1.1-rc.2",
189
+ "@deepseek-ai/dsh-session-projection": "0.1.1-rc.2",
190
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
191
+ "@deepseek-ai/dsh-storage": "0.1.1-rc.2",
192
+ "@deepseek-ai/dsh-storage-domain": "0.1.1-rc.2",
193
+ "@deepseek-ai/dsh-storage-json": "0.1.1-rc.2",
194
+ "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
195
+ "@deepseek-ai/dsh-timeout": "0.1.1-rc.2",
196
+ "@deepseek-ai/dsh-typert-protocol": "0.1.1-rc.2",
187
197
  "@deepseek-ai/schemastery": "3.18.1",
188
198
  "@types/node": "^22.20.0",
189
199
  "@vitest/coverage-v8": "4.1.10",
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  OFF,
41
41
  OUTPUT_STYLE_DOMAIN,
42
42
  STYLE_SOURCE,
43
+ styleFoldStateSchema,
43
44
  styleSelectionSchema,
44
45
  styleSelectionViewSchema,
45
46
  } from './types.ts'
package/src/runtime.ts CHANGED
@@ -23,9 +23,9 @@ import { resolveConfig, type Config } from './config.ts'
23
23
  import { installInvariant, PACKAGE_NAME, type InvariantFacts, type InvariantRegistry } from './invariant.ts'
24
24
  import { loadStyleLibrary, truncateStyle, type OutputStyle } from './style-library.ts'
25
25
  import { applyStyleEvent, EMPTY_STYLE_STATE, parseStyleInput, STYLE_COMMAND, type StyleFoldState } from './style-command.ts'
26
- import { OUTPUT_STYLE_DOMAIN, STYLE_SOURCE, styleSelectionViewSchema, type StyleSelection, type StyleSelectionView } from './types.ts'
26
+ import { OUTPUT_STYLE_DOMAIN, STYLE_SOURCE, styleFoldStateSchema, styleSelectionViewSchema, type StyleSelection, type StyleSelectionView } from './types.ts'
27
27
  import { BUILTIN_RENDERERS, RendererRegistry, type OutputRenderer, type RenderContext, type RenderedText, type StyleRule } from './renderers.ts'
28
- import { conversationLines, renderExport } from './export.ts'
28
+ import { conversationLines, renderExport, sanitizeText } from './export.ts'
29
29
 
30
30
  /** Bundled style-library directory (package `styles/`), the lowest-priority `stylesDir` entry. */
31
31
  export const DEFAULT_STYLES_DIR = fileURLToPath(new URL('../styles/', import.meta.url))
@@ -418,10 +418,13 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
418
418
  ctx.inject(['sessionProjections'], (projectionCtx) => {
419
419
  projectionCtx.sessionProjections.register<'style', StyleFoldState>({
420
420
  key: 'style',
421
- schema: styleSelectionViewSchema,
421
+ stateSchema: styleFoldStateSchema,
422
422
  init: () => EMPTY_STYLE_STATE,
423
423
  apply: applyStyleEvent,
424
- view: state => viewStyleSelection(runtime, state),
424
+ wire: {
425
+ viewSchema: styleSelectionViewSchema,
426
+ view: state => viewStyleSelection(runtime, state),
427
+ },
425
428
  stateVersion: 2,
426
429
  })
427
430
  })
@@ -504,23 +507,41 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
504
507
  // Markdown or sanitized HTML through the renderer pipeline. The document
505
508
  // itself is the visible artifact; the original lines are the session log
506
509
  // the export was projected from — rendered and original stay reconstructable.
510
+ // `--save <path>` additionally writes the sanitized document to that
511
+ // workspace path, gated by the approval service and the fs service (both
512
+ // optional; a missing approval service denies the write, a missing fs
513
+ // service fails loudly).
507
514
  if (resolved.enableExport) {
508
515
  ctx.inject(['commands'], (commandCtx) => {
509
516
  commandCtx.commands.register({
510
517
  name: 'export',
511
518
  description: 'Export this session as Markdown or HTML (renderer-aware)',
512
- input: { hint: '[markdown|html] [--renderer=<id>]' },
513
- handler: async ({ agent, rawInput }) => {
519
+ input: { hint: '[markdown|html] [--renderer=<id>] [--save <path>]' },
520
+ handler: async ({ agent, rawInput, signal }) => {
514
521
  const input = parseExportInput(rawInput)
515
522
  if (input.kind === 'error') {
516
- return { kind: 'error', text: 'usage: /export [markdown|html] [--renderer=<id>]' }
523
+ return { kind: 'error', text: 'usage: /export [markdown|html] [--renderer=<id>] [--save <path>]' }
517
524
  }
518
525
  const lines = conversationLines(agent.session.events)
519
526
  const rules: StyleRule[] = input.renderer === undefined
520
527
  ? [...effectiveRules]
521
528
  : [{ match: {}, style: input.renderer, priority: 0 }]
522
529
  const document = renderExport(renderers, lines, input.format, rules)
523
- return { kind: 'success', text: document.text }
530
+ if (input.save === undefined) {
531
+ return { kind: 'success', text: document.text }
532
+ }
533
+ // The disk path sanitizes the rendered document before writing and
534
+ // writes only after the approval service grants it.
535
+ const saved = await saveExportFile(
536
+ ctx.get('fs') as ExportFileSystem | undefined,
537
+ ctx.get('approval') as ExportApproval | undefined,
538
+ agent,
539
+ input.save,
540
+ sanitizeText(document.text),
541
+ signal,
542
+ )
543
+ if (saved.kind === 'error') return { kind: 'error', text: saved.text }
544
+ return { kind: 'success', text: `saved ${input.format} export to ${saved.path}` }
524
545
  },
525
546
  })
526
547
  })
@@ -528,25 +549,151 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
528
549
  }
529
550
 
530
551
  /** Parsed `/export` invocation. */
531
- type ExportInput = { kind: 'ok'; format: 'markdown' | 'html'; renderer?: string } | { kind: 'error' }
532
-
533
- /** Parse `/export [markdown|html] [--renderer=<id>]` from the raw command input. */
552
+ type ExportInput = {
553
+ kind: 'ok'
554
+ format: 'markdown' | 'html'
555
+ renderer?: string
556
+ save?: string
557
+ } | { kind: 'error' }
558
+
559
+ /** Parse `/export [markdown|html] [--renderer=<id>] [--save <path>]` from the raw command input. */
534
560
  export function parseExportInput(rawInput: unknown): ExportInput {
535
561
  const raw = String(rawInput ?? '').trim()
536
562
  const parts = raw === '' ? [] : raw.split(/\s+/)
537
563
  let format: 'markdown' | 'html' = 'markdown'
538
564
  let renderer: string | undefined
539
- for (const part of parts) {
540
- if (part === 'markdown' || part === 'html') {
541
- format = part
565
+ let save: string | undefined
566
+ for (let index = 0; index < parts.length; index += 1) {
567
+ const part = parts[index]
568
+ if (part === undefined) continue
569
+ if (part === 'markdown' || part === 'md') {
570
+ format = 'markdown'
571
+ continue
572
+ }
573
+ if (part === 'html') {
574
+ format = 'html'
575
+ continue
576
+ }
577
+ const rendererMatch = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part)
578
+ if (rendererMatch !== null) {
579
+ renderer = rendererMatch[1]
580
+ continue
581
+ }
582
+ const saveInline = /^--save=(.+)$/.exec(part)
583
+ if (saveInline !== null) {
584
+ save = saveInline[1]
542
585
  continue
543
586
  }
544
- const match = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part)
545
- if (match !== null) {
546
- renderer = match[1]
587
+ if (part === '--save') {
588
+ const next = parts[index + 1]
589
+ if (next === undefined || next === '') return { kind: 'error' }
590
+ save = next
591
+ index += 1
547
592
  continue
548
593
  }
549
594
  return { kind: 'error' }
550
595
  }
551
- return { kind: 'ok', format, ...renderer === undefined ? {} : { renderer } }
596
+ return {
597
+ kind: 'ok',
598
+ format,
599
+ ...renderer === undefined ? {} : { renderer },
600
+ ...save === undefined ? {} : { save },
601
+ }
602
+ }
603
+
604
+ /** Approval outcome vocabulary, structural (mirrors the approval seam without importing it). */
605
+ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
606
+
607
+ /** Structural slice of the DSH filesystem service the save path uses. */
608
+ export interface ExportFileSystem {
609
+ resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<unknown>
610
+ writeText(target: unknown, content: string): Promise<unknown>
611
+ }
612
+
613
+ /** Structural slice of the DSH approval service the save path uses. */
614
+ export interface ExportApproval {
615
+ request(request: { agent: unknown; toolName: string; reason: string; signal?: AbortSignal }): Promise<ApprovalOutcome>
616
+ }
617
+
618
+ /** Error codes of the `/export --save` path; each is a stable machine-readable label. */
619
+ export type ExportSaveErrorCode = 'fs-unavailable' | 'approval-unavailable' | 'approval-denied' | 'approval-cancelled'
620
+
621
+ /** Result of a `/export --save` attempt: the written path or a structured failure. */
622
+ export type ExportSaveResult =
623
+ | { readonly kind: 'written'; readonly path: string }
624
+ | { readonly kind: 'error'; readonly code: ExportSaveErrorCode; readonly text: string }
625
+
626
+ /**
627
+ * Write one rendered export document to a workspace path after user approval.
628
+ * Fail-closed: a missing approval service, a rejected/cancelled/unavailable
629
+ * decision, or a throwing approval channel all deny the write; a missing fs
630
+ * service fails loudly with a structured error. The caller already sanitized
631
+ * `content` before this write.
632
+ * @param fs - the fs service (`ctx.get('fs')`), or undefined when none is composed.
633
+ * @param approval - the approval service (`ctx.get('approval')`), or undefined when none is composed.
634
+ * @param agent - the agent whose session the export belongs to (routes the approval).
635
+ * @param path - the workspace path to write.
636
+ * @param content - the sanitized document text.
637
+ * @param signal - the command's abort signal, forwarded to approval and resolve.
638
+ * @returns the written path, or a structured failure.
639
+ */
640
+ export async function saveExportFile(
641
+ fs: ExportFileSystem | undefined,
642
+ approval: ExportApproval | undefined,
643
+ agent: unknown,
644
+ path: string,
645
+ content: string,
646
+ signal?: AbortSignal,
647
+ ): Promise<ExportSaveResult> {
648
+ if (approval === undefined) {
649
+ return {
650
+ kind: 'error',
651
+ code: 'approval-unavailable',
652
+ text: 'dsh-output-styles: /export --save requires an approval service (compose @deepseek-ai/dsh-user-approval); nothing was written',
653
+ }
654
+ }
655
+ let outcome: ApprovalOutcome
656
+ try {
657
+ outcome = await approval.request({
658
+ agent,
659
+ toolName: 'export',
660
+ reason: `write the exported document to ${path}`,
661
+ ...signal === undefined ? {} : { signal },
662
+ })
663
+ } catch {
664
+ // An approval channel that cannot answer is an unanswerable ask: fail closed.
665
+ outcome = 'unavailable'
666
+ }
667
+ switch (outcome) {
668
+ case 'allowed-once': break
669
+ case 'rejected':
670
+ return {
671
+ kind: 'error',
672
+ code: 'approval-denied',
673
+ text: 'dsh-output-styles: /export --save was rejected; nothing was written',
674
+ }
675
+ case 'cancelled':
676
+ return {
677
+ kind: 'error',
678
+ code: 'approval-cancelled',
679
+ text: 'dsh-output-styles: /export --save was cancelled; nothing was written',
680
+ }
681
+ default:
682
+ // 'unavailable' plus any out-of-vocabulary answer fail closed (never write).
683
+ return {
684
+ kind: 'error',
685
+ code: 'approval-unavailable',
686
+ text: 'dsh-output-styles: /export --save approval is unavailable; nothing was written',
687
+ }
688
+ }
689
+ if (fs === undefined) {
690
+ return {
691
+ kind: 'error',
692
+ code: 'fs-unavailable',
693
+ text: 'dsh-output-styles: /export --save requires an fs service (compose @deepseek-ai/dsh-fs); nothing was written',
694
+ }
695
+ }
696
+ const target = await fs.resolve(path, signal === undefined ? undefined : { signal })
697
+ await fs.writeText(target, content)
698
+ return { kind: 'written', path }
552
699
  }
package/src/types.ts CHANGED
@@ -12,6 +12,7 @@ import type { SessionId } from '@deepseek-ai/dsh-session'
12
12
  import { z as zod } from 'zod'
13
13
  import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
14
14
  import type { OutputRenderer, RenderContext, RenderedText } from './renderers.ts'
15
+ import type { StyleFoldState } from './style-command.ts'
15
16
 
16
17
  /** The reserved switch target that removes a session's selection. */
17
18
  export const OFF = 'off'
@@ -80,10 +81,30 @@ export const styleSelectionViewSchema = zod.object({
80
81
  currentValue: zod.string().min(1).nullable(),
81
82
  })
82
83
 
84
+ /**
85
+ * Validates the `style` projection's persisted fold state before it seeds a
86
+ * fold (the `stateSchema` of the registered unit). The state is plain JSON:
87
+ * the settled selection (`current`, null when off) plus the in-flight switch
88
+ * parked by `command/run` and resolved by its paired `command/done`.
89
+ */
90
+ export const styleFoldStateSchema = zod.object({
91
+ current: zod.string().min(1).nullable(),
92
+ pending: zod.object({
93
+ commandId: zod.string().min(1),
94
+ target: zod.union([
95
+ zod.object({ name: zod.string().min(1) }),
96
+ zod.object({ off: zod.literal(true) }),
97
+ ]),
98
+ }).nullable(),
99
+ })
100
+
83
101
  declare module '@deepseek-ai/dsh-session-projection/types' {
84
102
  interface SessionProjectionMap {
85
103
  style: StyleSelectionView
86
104
  }
105
+ interface SessionProjectionStateMap {
106
+ style: StyleFoldState
107
+ }
87
108
  }
88
109
 
89
110
  /** The `ctx.outputRenderers` service: the output.render.* renderer registry. */