dsh-output-styles 0.2.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.
Files changed (44) hide show
  1. package/LICENSE +201 -0
  2. package/README.es.md +197 -0
  3. package/README.ja.md +197 -0
  4. package/README.ko.md +197 -0
  5. package/README.md +197 -0
  6. package/README.zh.md +197 -0
  7. package/cordis.patch.yml +40 -0
  8. package/docs/VERIFICATION.zh.md +93 -0
  9. package/lib/client.js +83 -0
  10. package/lib/index.js +415 -0
  11. package/lib/invariant-LV6hQX5s.js +442 -0
  12. package/lib/invariant.js +2 -0
  13. package/lib/types/client/index.d.ts +30 -0
  14. package/lib/types/client/index.d.ts.map +1 -0
  15. package/lib/types/client/locales.d.ts +13 -0
  16. package/lib/types/client/locales.d.ts.map +1 -0
  17. package/lib/types/config.d.ts +73 -0
  18. package/lib/types/config.d.ts.map +1 -0
  19. package/lib/types/index.d.ts +31 -0
  20. package/lib/types/index.d.ts.map +1 -0
  21. package/lib/types/invariant.d.ts +59 -0
  22. package/lib/types/invariant.d.ts.map +1 -0
  23. package/lib/types/runtime.d.ts +144 -0
  24. package/lib/types/runtime.d.ts.map +1 -0
  25. package/lib/types/style-command.d.ts +68 -0
  26. package/lib/types/style-command.d.ts.map +1 -0
  27. package/lib/types/style-library.d.ts +78 -0
  28. package/lib/types/style-library.d.ts.map +1 -0
  29. package/lib/types/types.d.ts +78 -0
  30. package/lib/types/types.d.ts.map +1 -0
  31. package/package.json +138 -0
  32. package/src/client/index.ts +104 -0
  33. package/src/client/locales.ts +14 -0
  34. package/src/config.ts +110 -0
  35. package/src/index.ts +51 -0
  36. package/src/invariant.ts +144 -0
  37. package/src/runtime.ts +439 -0
  38. package/src/style-command.ts +89 -0
  39. package/src/style-library.ts +348 -0
  40. package/src/types.ts +86 -0
  41. package/styles/concise.md +17 -0
  42. package/styles/explanatory.md +14 -0
  43. package/styles/formal.md +14 -0
  44. package/styles/step-by-step.md +16 -0
@@ -0,0 +1,93 @@
1
+ # 验证记录(dsh-output-styles)
2
+
3
+ > 本文档记录交付验证的实测命令与输出。运行环境:Windows + Node 22 + pnpm 11;宿主为已安装的 `@deepseek-ai/dsh` CLI `0.1.0-rc.6`(`dsh --version` 输出 `0.1.0-rc.6`)。版本:0.2.0。
4
+
5
+ ## 1. 单元与集成测试
6
+
7
+ ```text
8
+ $ pnpm test
9
+ ✓ tests/config.spec.ts (9 tests)
10
+ ✓ tests/style-command.spec.ts (10 tests)
11
+ ✓ tests/invariant.spec.ts (9 tests)
12
+ ✓ tests/style-library.spec.ts (24 tests)
13
+ ✓ tests/commands-projections.spec.ts (7 tests)
14
+ ✓ tests/runtime.spec.ts (23 tests)
15
+ ✓ tests/client.spec.ts (6 tests)
16
+ Test Files 7 passed (7)
17
+ Tests 87 passed (87)
18
+ ```
19
+
20
+ 覆盖:风格解析(md/json 单对象与数组集合、坏文件/坏条目跳过、重名/保留字/双 force 抛错、`name` 缺省继承文件名、多词名、`keep-coding-instructions`/`force` 字段)、`/style` 分派(无参带描述列表/整段文本切换/off/未知名)、码点安全截断与预算、多目录分层(后者覆盖前者、`includeBuiltins`)、fs.watch 热加载(新文件生效、破坏 defaultStyle 时保留旧库)、settings 项目默认(回落、会话选择优先、非法名拒写)、`keep-coding-instructions: false` 整段替换系统提示、force 覆盖、会话隔离、HMR 配置热更新、`style` 投影按 command/done 成功折叠(checkpoint v2 往返、失败命令不改变状态)、不变量检查、Web 客户端选择器(装饰注册、投影选项、整行提交、Remote 失败浮出)。
21
+
22
+ 集成用例用 npm 发布的 `@deepseek-ai/dsh-*@0.1.0-rc.6` 真实宿主/客户端服务组装(sessions / systemPrompt / commands / storage / storage-json / storage-domain / sessionProjections / settings;客户端 commandUi / sessions / remote / locale 为结构型测试替身)。
23
+
24
+ `pnpm run typecheck`(两个 tsc 工程)、`pnpm run build`(宿主 + 客户端两个 bundle)、`pnpm run verify:self-contained`(44 个文本文件)均通过。
25
+
26
+ ## 2. 打包与 bundle 补丁层装载(0.2.0 新能力)
27
+
28
+ ```text
29
+ $ pnpm pack
30
+ Tarball Contents(节选): package.json, LICENSE, lib/index.js, lib/invariant.js, lib/invariant-*.js,
31
+ lib/client.js, lib/types/**/*.d.ts, src/**, styles/{concise,explanatory,formal,step-by-step}.md,
32
+ cordis.patch.yml, README.md, README.zh.md, README.ja.md, README.ko.md, README.es.md, docs/
33
+
34
+ $ env:DSH_HOME = <临时目录>
35
+ $ dsh plugin --profile scratch add ./dsh-output-styles-0.2.0.tgz
36
+ Packages: +9 ... Done in 3.1s ← 不再出现 "declares no dsh.bundle" 警告
37
+
38
+ $ dsh --profile scratch --dump-config
39
+ # == dsh-output-styles
40
+ - id: storage
41
+ name: '@deepseek-ai/dsh-storage'
42
+ - id: storage-json
43
+ name: '@deepseek-ai/dsh-storage-json'
44
+ - id: storage-domain
45
+ name: '@deepseek-ai/dsh-storage-domain'
46
+ - id: output-styles
47
+ name: dsh-output-styles
48
+ ```
49
+
50
+ 包清单 `dsh.bundle.patch = ./cordis.patch.yml` 生效:一条 `plugin add` 即把 storage 三行 + 插件行作为补丁层组合进 profile(按 id 插入替换同 id 行,对 web profile 幂等)。0.1.0 时代的「纯依赖安装、需手写行」摩擦消除。
51
+
52
+ ## 3. `--dump-config` 行生效
53
+
54
+ 上一步的输出即证明:四行全部出现在生效组合中,schema 默认 config 正常展开(`stylesDir: []`、`includeBuiltins: true`、`watchStyles: true`),启动无 FAILED。
55
+
56
+ ## 4. headless 实测(模型可见注入与回复风格变化)
57
+
58
+ 0.1.0 交付时的真实 API 实测(deepseek-v4-pro)记录保留如下;0.2.0 的模型可见注入路径(`systemPrompt.section` + `system-prompt/assemble` waterfall)改动已由第 1 节的组装级集成测试覆盖,`keep-coding-instructions: false` 的整段替换断言即经真实 rc.6 `SystemPrompt.assemble` 输出验证。真实 API 复测需 `DEEPSEEK_API_KEY`,本机无 key 时按既有策略手动执行:
59
+
60
+ ```text
61
+ $ dsh --profile styles-verify "请只用一句话介绍你自己,不要客套。"
62
+ (无风格)我是 DeepSeek Harness 中的 AI 编码代理,运行于 deepseek-v4-pro 模型之上,负责在插件化代理框架内执行代码编写、文件操作、命令运行与多步骤任务编排。
63
+
64
+ $ (cordis.patch.yml 中 output-styles 行改为 defaultStyle: concise,HMR 生效)
65
+ $ dsh --profile styles-verify "请只用一句话介绍你自己,不要客套。"
66
+ (concise)我是运行在 DeepSeek Harness 插件化平台上、基于 deepseek-v4-pro 模型的 AI 编码代理,能直接读写文件、执行命令并检索代码来完成工程任务。
67
+ ```
68
+
69
+ 会话日志取证(经宿主真实持久化包 `dsh-session-persistence-jsonl` 解码读回,`scripts/verify-session-log.mjs`,0.2.0 起按任意风格名匹配 `# Output style: <name>` 并报告宿主身份是否共存):
70
+
71
+ ```text
72
+ == session session-3d490e6d-…(concise 运行,170 events)==
73
+ request/header logged before dispatch: true
74
+ style heading in logged system prompt: true
75
+ active style name in logged prompt: concise
76
+ style body in logged system prompt: true
77
+ harness identity alongside style: true
78
+ ```
79
+
80
+ 即:模型可见的风格正文在派发前完整写入 `request/header`(模型可见 ⟺ 已记录),有风格会话与无风格会话可区分;风格名由 `command/run`(`/style` 命令生命周期)与 `output_style` 域记录(含 `{ kind: 'plugin', plugin: 'dsh-output-styles' }` 来源标记)重建。`keep-coding-instructions: false` 的会话则 `harness identity alongside style: false`。
81
+
82
+ ## 5. Web UI 入口
83
+
84
+ - **命令入口**:宿主 `/style` 命令经真实 rc.6 命令注册表注册(集成测试断言 `ctx.commands.list(agent)` 含 `{ name: 'style', input: { hint: '<style | off>' } }`)——这正是 Web UI 经 BFF 读取的同一注册表。
85
+ - **选择器(0.2.0 新能力)**:`dsh-output-styles/client` 客户端行用 `commandUi.decorate` 把 `/style` 裸调用装饰成投影驱动的 popupSelect(`off` 行 + 每风格一行、当前行高亮、中英双语);客户端入口测试断言选项构建、`/style <整段名字>`/`/style off` 整行提交与 Remote 失败浮出。实测装载:向 profile 添加 `- id: output-styles-client / name: 'dsh-output-styles/client'` 行后经 Loader 解析,随 web bundle 的客户端运行时激活。
86
+ - **会话投影**:`style`(`{ options, currentValue, options[].whenToUse }`)供 Web UI 读取,经 `sessionProjections.snapshot`/`checkpoint` 往返验证(stateVersion 2)。
87
+ - **项目默认**:settings 命名空间 `output-style`(`{ style }`)注册在 settings seam 上,无 settings 服务时待激活、不影响主功能(集成测试覆盖两种组合)。
88
+
89
+ ## 6. 已知边界
90
+
91
+ - 真实 API 复测(第 4 节)需要 `DEEPSEEK_API_KEY`;无 key 时组装级集成测试是模型可见路径的回归保障,真实 API 手动复测步骤不变。
92
+ - 风格不作用于子代理会话(与 Claude Code 语义一致,README「与 Claude Code 的差异」表明确记录)。
93
+ - settings 提供方未配置时项目默认回落 `defaultStyle`;settings 值在风格热加载后变为悬空名时静默降级为无风格(与悬空会话选择同策略)。
package/lib/client.js ADDED
@@ -0,0 +1,83 @@
1
+ //#region src/client/locales.ts
2
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3
+ const zh = {
4
+ "option.off": "关闭(默认)",
5
+ "option.offDetail": "恢复项目默认输出风格"
6
+ };
7
+ /** English dictionary, checked complete against the zh key set. */
8
+ const en = {
9
+ "option.off": "Off (default)",
10
+ "option.offDetail": "Restore the project default output style"
11
+ };
12
+ //#endregion
13
+ //#region src/client/index.ts
14
+ /** Client plugin name; keep stable after publishing. */
15
+ const name = "dsh-output-styles-client";
16
+ /** Required client services: the command surface, the sessions face, the command Remote, and locale. */
17
+ const inject = [
18
+ "commandUi",
19
+ "locale",
20
+ "remote",
21
+ "sessions"
22
+ ];
23
+ /** Dictionary namespace owned by this plugin. */
24
+ const NS = "style";
25
+ /** The picker row that restores the project default; stable, never a style name. */
26
+ const OFF_ID = "off";
27
+ /** Flatten the projection into picker rows: the off row first, then one row per style. */
28
+ function optionsOf(view, t) {
29
+ const rows = [{
30
+ id: OFF_ID,
31
+ label: t("option.off"),
32
+ detail: t("option.offDetail"),
33
+ active: view.currentValue === null
34
+ }];
35
+ for (const option of view.options) rows.push({
36
+ id: option.value,
37
+ label: option.name,
38
+ detail: option.whenToUse !== void 0 ? `${option.description} · ${option.whenToUse}` : option.description,
39
+ active: view.currentValue === option.value
40
+ });
41
+ return rows;
42
+ }
43
+ /**
44
+ * Client plugin body: register the `style` dictionaries and decorate the
45
+ * host `/style` command's bare invocation with the projection-backed picker.
46
+ * @param ctx - client root context.
47
+ */
48
+ function apply(ctx) {
49
+ ctx.effect(() => ctx.locale.register(NS, {
50
+ zh,
51
+ en
52
+ }), "dsh-output-styles: style dictionaries");
53
+ const t = ctx.locale.bind(NS);
54
+ ctx.inject([
55
+ "commandUi",
56
+ "remote",
57
+ "sessions"
58
+ ], (scope) => {
59
+ const commandUi = scope.get("commandUi");
60
+ const sessions = scope.get("sessions");
61
+ const remote = scope.remote;
62
+ scope.effect(() => commandUi.decorate({
63
+ name: "style",
64
+ available: () => true,
65
+ ui: {
66
+ kind: "popupSelect",
67
+ options: async (session) => {
68
+ const view = sessions.binding(session.sessionId)?.session.projections.faceOf("style").getSnapshot();
69
+ if (view === void 0) return [];
70
+ return optionsOf(view, t);
71
+ },
72
+ onSelect: async (option, session) => {
73
+ const line = option.id === OFF_ID ? "/style off" : `/style ${option.id}`;
74
+ const result = await remote.commands.execute(session.sessionId, line);
75
+ if (!result.ok) throw new Error(`command.execute failed: ${result.error.code}: ${result.error.message}`);
76
+ if (result.value === void 0) throw new Error(`unknown or malformed command: ${line}`);
77
+ }
78
+ }
79
+ }), "dsh-output-styles: /style picker");
80
+ });
81
+ }
82
+ //#endregion
83
+ export { apply, inject, name };
package/lib/index.js ADDED
@@ -0,0 +1,415 @@
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-LV6hQX5s.js";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { watch } from "node:fs";
6
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
7
+ //#region src/config.ts
8
+ /**
9
+ * Serializable configuration, schema, and direct-call defaults.
10
+ *
11
+ * Every tunable lives here: a deployment changes behavior through
12
+ * `cordis.yml`, never by editing source. The schema is validated by the
13
+ * harness Loader while the plugin loads; invalid configuration fails the
14
+ * load with an actionable error.
15
+ * @module dsh-output-styles/config
16
+ */
17
+ /** Loader-visible configuration schema and defaults. */
18
+ const Config = z.object({
19
+ stylesDir: z.union([z.string(), z.array(z.string())]).default([]),
20
+ maxStyleChars: z.number().min(1).default(4e3),
21
+ defaultStyle: z.string().default(""),
22
+ compatJson: z.boolean().default(true),
23
+ sectionOrder: z.number().default(90),
24
+ truncationMarker: z.string().default("\n\n[style truncated]"),
25
+ includeBuiltins: z.boolean().default(true),
26
+ watchStyles: z.boolean().default(true)
27
+ });
28
+ /**
29
+ * Resolve the same defaults for direct callers that bypass the Cordis Loader,
30
+ * and fail loud on values the schema cannot express (a non-finite section
31
+ * order).
32
+ * @param config - Partial serialized configuration.
33
+ * @param defaultStylesDir - Absolute bundled directory used when built-ins are included.
34
+ * @returns Configuration with every default applied.
35
+ */
36
+ function resolveConfig(config, defaultStylesDir) {
37
+ const maxStyleChars = config.maxStyleChars ?? 4e3;
38
+ if (!Number.isFinite(maxStyleChars) || maxStyleChars < 1) throw new Error(`dsh-output-styles: maxStyleChars must be a finite number ≥ 1, got ${String(config.maxStyleChars)}`);
39
+ const sectionOrder = config.sectionOrder ?? 90;
40
+ if (!Number.isFinite(sectionOrder)) throw new Error(`dsh-output-styles: sectionOrder must be a finite number, got ${String(config.sectionOrder)}`);
41
+ const includeBuiltins = config.includeBuiltins ?? true;
42
+ const customDirs = (Array.isArray(config.stylesDir) ? config.stylesDir : config.stylesDir === void 0 || config.stylesDir === "" ? [] : [config.stylesDir]).map((dir) => resolve(dir));
43
+ return {
44
+ stylesDirs: includeBuiltins ? [defaultStylesDir, ...customDirs] : customDirs,
45
+ maxStyleChars,
46
+ defaultStyle: config.defaultStyle ?? "",
47
+ compatJson: config.compatJson ?? true,
48
+ sectionOrder,
49
+ truncationMarker: config.truncationMarker ?? "\n\n[style truncated]",
50
+ includeBuiltins,
51
+ watchStyles: config.watchStyles ?? true
52
+ };
53
+ }
54
+ //#endregion
55
+ //#region src/runtime.ts
56
+ /**
57
+ * Runtime boundary and Cordis activation: style resolution over the durable
58
+ * selection domain, the model-visible system-prompt section, the `/style`
59
+ * command, the `style` session projection, and the invariant registration.
60
+ *
61
+ * Every registration is an effect — Cordis undoes all of them on unload, so
62
+ * configuration hot-reload replaces the whole plugin without residue.
63
+ * @module dsh-output-styles/runtime
64
+ */
65
+ /** Bundled style-library directory (package `styles/`), the lowest-priority `stylesDir` entry. */
66
+ const DEFAULT_STYLES_DIR = fileURLToPath(new URL("../styles/", import.meta.url));
67
+ /** Prompt-section name; a fixed registry key a scoped composition could shadow. */
68
+ const STYLE_SECTION_NAME = "output-style:selection";
69
+ /** Settings namespace owning the project-level default (`outputStyle`). */
70
+ const SETTINGS_NS = settingsNamespace("output-style");
71
+ /** Coalescing delay for style-file change events; an internal implementation constant, not a deployment knob. */
72
+ const WATCH_DEBOUNCE_MS = 250;
73
+ /**
74
+ * Resolved style behavior for one session. A forced style (frontmatter
75
+ * `force: true`) wins over everything; otherwise the session's own durable
76
+ * selection wins, sessions that never selected one fall back to the project
77
+ * default (settings `outputStyle`, then the configured default style), and
78
+ * `''` means no style at all.
79
+ */
80
+ var OutputStyleRuntime = class {
81
+ domain;
82
+ library;
83
+ forcedStyle;
84
+ selection;
85
+ defaultStyle;
86
+ maxStyleChars;
87
+ truncationMarker;
88
+ projectDefault;
89
+ /** Style library in deterministic directory/file order. */
90
+ get styles() {
91
+ return this.library;
92
+ }
93
+ /**
94
+ * @param domain - the opened `output_style` domain; the caller owns `close()`.
95
+ * @param styles - the loaded style library (at most one `force` style).
96
+ * @param options - resolved style budget and default.
97
+ */
98
+ constructor(domain, styles, options) {
99
+ this.domain = domain;
100
+ this.library = styles;
101
+ this.selection = domain.table("selection");
102
+ this.defaultStyle = options.defaultStyle;
103
+ this.maxStyleChars = options.maxStyleChars;
104
+ this.truncationMarker = options.truncationMarker;
105
+ this.forcedStyle = [...styles.values()].find((style) => style.force);
106
+ this.projectDefault = () => this.defaultStyle;
107
+ }
108
+ /** Every switchable style name, in library order. */
109
+ get names() {
110
+ return [...this.library.keys()];
111
+ }
112
+ /** The forced style's name, or undefined when the library declares none. */
113
+ get forcedName() {
114
+ return this.forcedStyle?.name;
115
+ }
116
+ /**
117
+ * Atomically swap the style library (style-file hot reload). The forced
118
+ * style is recomputed; the caller already validated the new library.
119
+ * @param styles - the replacement library.
120
+ */
121
+ reload(styles) {
122
+ this.library = styles;
123
+ this.forcedStyle = [...styles.values()].find((style) => style.force);
124
+ }
125
+ /**
126
+ * Point the project-default resolution at a live source (the settings
127
+ * scope while one is attached, the composition entry otherwise).
128
+ * @param get - thunk returning the project default style name (`''` = none).
129
+ */
130
+ setProjectDefault(get) {
131
+ this.projectDefault = get;
132
+ }
133
+ /**
134
+ * Resolve one style by name.
135
+ * @param name - style name.
136
+ * @returns the style, or undefined when the library has none.
137
+ */
138
+ get(name) {
139
+ return this.library.get(name);
140
+ }
141
+ /**
142
+ * The session's durable selection record.
143
+ * @param sessionId - the session the selection belongs to.
144
+ * @returns the record, or undefined when the session never selected one.
145
+ */
146
+ selectionFor(sessionId) {
147
+ return this.selection.get(sessionId);
148
+ }
149
+ /**
150
+ * The style in force for a session: a forced library style, else the
151
+ * session's own selection, else the project default, else none. A stale
152
+ * selection or project default (its style left the library) also degrades
153
+ * to none.
154
+ * @param sessionId - the session the style is resolved for.
155
+ * @returns the effective style, or undefined when no style applies.
156
+ */
157
+ effectiveStyle(sessionId) {
158
+ if (this.forcedStyle !== void 0) return this.forcedStyle;
159
+ const record = this.selectionFor(sessionId);
160
+ const name = record !== void 0 ? record.style : this.projectDefault();
161
+ return name === "" ? void 0 : this.library.get(name);
162
+ }
163
+ /**
164
+ * The effective style name, or `''` when no style applies.
165
+ * @param sessionId - the session the style is resolved for.
166
+ * @returns the style name, or `''`.
167
+ */
168
+ currentName(sessionId) {
169
+ return this.effectiveStyle(sessionId)?.name ?? "";
170
+ }
171
+ /**
172
+ * The model-visible style directive for a session: a header naming the
173
+ * style plus its body under the configured budget. The exact text is what
174
+ * the harness logs in `request/header` before dispatch.
175
+ * @param sessionId - the session the directive is built for.
176
+ * @returns the directive, or `''` when no style applies.
177
+ */
178
+ promptText(sessionId) {
179
+ const style = this.effectiveStyle(sessionId);
180
+ if (style === void 0) return "";
181
+ const body = truncateStyle(style.body, this.maxStyleChars, this.truncationMarker);
182
+ return `# Output style: ${style.name}\n\nUse the following output style for every response in this conversation:\n\n${body}`;
183
+ }
184
+ /**
185
+ * The `/style` no-argument listing: the current selection followed by one
186
+ * line per style (`name — description`, with `whenToUse` appended when set).
187
+ * @param sessionId - the session the listing describes.
188
+ * @returns the multi-line command result text.
189
+ */
190
+ listLine(sessionId) {
191
+ const current = this.currentName(sessionId);
192
+ const lines = [current === "" ? "output style off" : `current output style: ${current}`];
193
+ for (const style of this.styles.values()) {
194
+ const whenToUse = style.whenToUse === void 0 ? "" : ` (${style.whenToUse})`;
195
+ lines.push(`${style.name} — ${style.description}${whenToUse}`);
196
+ }
197
+ return lines.join("\n");
198
+ }
199
+ /**
200
+ * The unknown-name error line, shared by the command handler and the direct
201
+ * {@link OutputStyleRuntime.select} write path.
202
+ * @param name - the rejected switch target.
203
+ * @returns the error text listing every switchable name.
204
+ */
205
+ unknownStyleLine(name) {
206
+ return `unknown output style "${name}" (available: ${this.names.join(", ")})`;
207
+ }
208
+ /**
209
+ * Durably select a style for a session. The write resolves only after the
210
+ * backend acknowledged it, so a settled command implies a stored record.
211
+ * @param session - the session the selection belongs to.
212
+ * @param name - library style name; unknown names throw.
213
+ * @returns resolution after durability.
214
+ */
215
+ async select(session, name) {
216
+ if (this.styles.get(name) === void 0) throw new Error(`dsh-output-styles: ${this.unknownStyleLine(name)}`);
217
+ await this.selection.put(session.id, {
218
+ style: name,
219
+ source: STYLE_SOURCE
220
+ });
221
+ }
222
+ /**
223
+ * Remove a session's selection, restoring the configured default.
224
+ * @param session - the session whose selection is removed.
225
+ * @returns resolution after durability.
226
+ */
227
+ async turnOff(session) {
228
+ await this.selection.delete(session.id);
229
+ }
230
+ /**
231
+ * Close the opened domain (the plugin fiber's async disposer).
232
+ * @returns resolution after the backend unit is released.
233
+ */
234
+ async close() {
235
+ await this.domain.close();
236
+ }
237
+ };
238
+ /** Build the `style` projection's wire value for one folded state. */
239
+ function viewStyleSelection(runtime, state) {
240
+ const currentValue = state.current !== null && runtime.styles.has(state.current) ? state.current : null;
241
+ return {
242
+ options: [...runtime.styles.entries()].map(([value, style]) => ({
243
+ value,
244
+ name: style.name,
245
+ description: style.description,
246
+ ...style.whenToUse === void 0 ? {} : { whenToUse: style.whenToUse }
247
+ })),
248
+ currentValue
249
+ };
250
+ }
251
+ /**
252
+ * Apply the plugin to its Cordis context.
253
+ *
254
+ * Declares `storageDomain` via `inject` (ready before `apply`; a composition
255
+ * without a routed kv backend keeps the plugin pending). Bad style files are
256
+ * skipped with warnings; a duplicate or reserved style name, an unreadable
257
+ * style directory, or a `defaultStyle` naming no style fails the load.
258
+ * @param ctx - scoped plugin context; registrations must be owned by its effects.
259
+ * @param config - configuration resolved by Cordis from the exported schema.
260
+ */
261
+ async function apply(ctx, config) {
262
+ const storageDomain = ctx.get("storageDomain");
263
+ if (storageDomain === void 0) throw new Error("dsh-output-styles: the \"storageDomain\" service declared by inject is unavailable — mount the storage facility (see README) or declare the inject");
264
+ const resolved = resolveConfig(config, DEFAULT_STYLES_DIR);
265
+ const styles = loadStyleLibrary(resolved.stylesDirs, { compatJson: resolved.compatJson }, (message) => {
266
+ ctx.logger.warn(`dsh-output-styles: ${message}`);
267
+ });
268
+ if (resolved.defaultStyle !== "" && !styles.has(resolved.defaultStyle)) throw new Error(`dsh-output-styles: defaultStyle "${resolved.defaultStyle}" names no style in ${resolved.stylesDirs.join(", ")} (available: ${[...styles.keys()].join(", ") || "none"})`);
269
+ const domain = await storageDomain.open(OUTPUT_STYLE_DOMAIN);
270
+ ctx.effect(() => () => domain.close());
271
+ const runtime = new OutputStyleRuntime(domain, styles, resolved);
272
+ if (resolved.watchStyles) {
273
+ let timer;
274
+ const reload = () => {
275
+ try {
276
+ const next = loadStyleLibrary(resolved.stylesDirs, { compatJson: resolved.compatJson }, (message) => {
277
+ ctx.logger.warn(`dsh-output-styles: ${message}`);
278
+ });
279
+ if (resolved.defaultStyle !== "" && !next.has(resolved.defaultStyle)) {
280
+ ctx.logger.warn(`dsh-output-styles: style file change removed defaultStyle "${resolved.defaultStyle}"; keeping the previous library`);
281
+ return;
282
+ }
283
+ runtime.reload(next);
284
+ } catch (error) {
285
+ ctx.logger.warn(`dsh-output-styles: style file change not applied: ${error instanceof Error ? error.message : String(error)}`);
286
+ }
287
+ };
288
+ const schedule = () => {
289
+ if (timer !== void 0) return;
290
+ timer = setTimeout(() => {
291
+ timer = void 0;
292
+ reload();
293
+ }, WATCH_DEBOUNCE_MS);
294
+ };
295
+ ctx.effect(() => {
296
+ const watchers = [];
297
+ for (const dir of resolved.stylesDirs) try {
298
+ watchers.push(watch(dir, { persistent: false }, () => {
299
+ schedule();
300
+ }));
301
+ } catch (error) {
302
+ ctx.logger.warn(`dsh-output-styles: cannot watch style directory ${dir}: ${error instanceof Error ? error.message : String(error)}`);
303
+ }
304
+ return () => {
305
+ if (timer !== void 0) clearTimeout(timer);
306
+ for (const watcher of watchers) watcher.close();
307
+ };
308
+ });
309
+ }
310
+ installSettingsSection(ctx, SETTINGS_NS, z.object({ style: z.string().default("") }), { style: resolved.defaultStyle }, {
311
+ setSource: (current) => {
312
+ runtime.setProjectDefault(() => current().style);
313
+ },
314
+ onChange: () => {},
315
+ validate: (value) => {
316
+ if (value.style !== "" && !runtime.styles.has(value.style)) throw new Error(`dsh-output-styles: settings outputStyle "${value.style}" names no style (available: ${[...runtime.styles.keys()].join(", ") || "none"})`);
317
+ }
318
+ });
319
+ ctx.systemPrompt.section({
320
+ name: STYLE_SECTION_NAME,
321
+ order: resolved.sectionOrder,
322
+ text: (context) => {
323
+ const agent = context.agent;
324
+ if (agent === void 0) return "";
325
+ const style = runtime.effectiveStyle(agent.session.id);
326
+ if (style === void 0 || !style.keepCodingInstructions) return "";
327
+ return runtime.promptText(agent.session.id);
328
+ }
329
+ });
330
+ ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
331
+ const out = await next();
332
+ const agent = context.agent;
333
+ if (agent === void 0) return out;
334
+ const style = runtime.effectiveStyle(agent.session.id);
335
+ if (style === void 0 || style.keepCodingInstructions) return out;
336
+ return {
337
+ ...out,
338
+ sections: [{
339
+ name: STYLE_SECTION_NAME,
340
+ text: runtime.promptText(agent.session.id)
341
+ }]
342
+ };
343
+ });
344
+ ctx.inject(["commands"], (commandCtx) => {
345
+ commandCtx.commands.register({
346
+ name: STYLE_COMMAND,
347
+ description: "Switch the model output style for this session",
348
+ input: { hint: "<style | off>" },
349
+ handler: async ({ agent, rawInput }) => {
350
+ const input = parseStyleInput(rawInput);
351
+ if (input.kind === "none") return {
352
+ kind: "success",
353
+ text: runtime.listLine(agent.session.id)
354
+ };
355
+ if (input.kind === "off") {
356
+ await runtime.turnOff(agent.session);
357
+ const forced = runtime.forcedName;
358
+ return {
359
+ kind: "success",
360
+ text: forced === void 0 ? "output style off" : `output style off (style "${forced}" remains in force)`
361
+ };
362
+ }
363
+ if (runtime.get(input.name) === void 0) return {
364
+ kind: "error",
365
+ text: runtime.unknownStyleLine(input.name)
366
+ };
367
+ await runtime.select(agent.session, input.name);
368
+ return {
369
+ kind: "success",
370
+ text: `switched to ${input.name}`
371
+ };
372
+ }
373
+ });
374
+ });
375
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
376
+ projectionCtx.sessionProjections.register({
377
+ key: "style",
378
+ schema: styleSelectionViewSchema,
379
+ init: () => EMPTY_STYLE_STATE,
380
+ apply: applyStyleEvent,
381
+ view: (state) => viewStyleSelection(runtime, state),
382
+ stateVersion: 2
383
+ });
384
+ });
385
+ ctx.inject(["invariants"], (invariantCtx) => {
386
+ const registry = invariantCtx.get("invariants");
387
+ if (registry === void 0) return;
388
+ registry.register(PACKAGE_NAME, installInvariant({
389
+ knownStyles: () => new Set(runtime.names),
390
+ selectionFor: (sessionId) => runtime.selectionFor(sessionId)
391
+ }));
392
+ });
393
+ }
394
+ //#endregion
395
+ //#region src/index.ts
396
+ /**
397
+ * `dsh-output-styles`: Claude Code `outputStyles`-equivalent runtime output
398
+ * styles for DeepSeek Harness. The plugin registers a model-visible system
399
+ * prompt section that injects the current session's style body, a `/style`
400
+ * slash command that switches it, per-session persistence over the
401
+ * `output_style` storage domain, and the `style` session projection.
402
+ * @module dsh-output-styles
403
+ */
404
+ /** Cordis plugin name; keep this stable after publishing. */
405
+ const name = "dsh-output-styles";
406
+ /**
407
+ * Services that must exist before the plugin applies: the prompt-assembly
408
+ * registry for the injected section, and the storage domain facility for
409
+ * per-session persistence. A composition without a routed kv backend keeps
410
+ * the plugin pending until the storage rows appear (Cordis dependency
411
+ * semantics), instead of racing a parallel mount.
412
+ */
413
+ const inject = ["systemPrompt", "storageDomain"];
414
+ //#endregion
415
+ 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 };