dsh-output-styles 0.3.2 → 0.4.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 +15 -0
- package/README.es.md +15 -0
- package/README.ja.md +15 -0
- package/README.ko.md +15 -0
- package/README.md +63 -1
- package/README.zh.md +37 -0
- package/docs/renderer-protocol.md +134 -0
- package/docs/renderer-protocol.zh.md +125 -0
- package/lib/index.js +434 -3
- package/lib/types/config.d.ts +32 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/export.d.ts +76 -0
- package/lib/types/export.d.ts.map +1 -0
- package/lib/types/renderers.d.ts +119 -0
- package/lib/types/renderers.d.ts.map +1 -0
- package/lib/types/runtime.d.ts +11 -0
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/types.d.ts +32 -0
- package/lib/types/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +49 -0
- package/src/export.ts +199 -0
- package/src/renderers.ts +264 -0
- package/src/runtime.ts +110 -0
- package/src/types.ts +28 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `output.render.*` protocol: a presenter registry that turns raw
|
|
3
|
+
* model-visible text into display text. A renderer is a pure function —
|
|
4
|
+
* `presenter(text, meta)` maps args to presentation data and never touches
|
|
5
|
+
* the DOM — matched by tool name and content type, ordered by priority.
|
|
6
|
+
* Every rendered result carries its original text alongside, so any consumer
|
|
7
|
+
* (this plugin's `/export`, third-party panels) can log both and keep the
|
|
8
|
+
* "model-visible ⟺ reconstructable" invariant.
|
|
9
|
+
*
|
|
10
|
+
* This module is dependency-free (no DOM, no node: imports, no DSH imports)
|
|
11
|
+
* so the protocol vocabulary itself is portable and unit-testable anywhere.
|
|
12
|
+
* @module dsh-output-styles/renderers
|
|
13
|
+
*/
|
|
14
|
+
/** Content-type vocabulary a renderer can match on. */
|
|
15
|
+
export type ContentType = 'text' | 'markdown' | 'html';
|
|
16
|
+
/** Match rule deciding whether a renderer applies to one render request. */
|
|
17
|
+
export interface RendererMatch {
|
|
18
|
+
/** Tool names the renderer applies to ('*' = every tool); omitted = match all. */
|
|
19
|
+
readonly tool?: string | readonly string[];
|
|
20
|
+
/** Content types the renderer applies to; omitted = match all. */
|
|
21
|
+
readonly contentType?: ContentType | readonly ContentType[];
|
|
22
|
+
}
|
|
23
|
+
/** Facts a render request carries (structural; the DOM never reaches this layer). */
|
|
24
|
+
export interface RenderContext {
|
|
25
|
+
/** The tool that produced the text, or '' for assistant/user prose. */
|
|
26
|
+
readonly tool: string;
|
|
27
|
+
/** The declared content type of the text ('text' when unknown). */
|
|
28
|
+
readonly contentType: ContentType;
|
|
29
|
+
/** The session the text belongs to (per-session rules match on it). */
|
|
30
|
+
readonly sessionId?: string;
|
|
31
|
+
/** Session-scoped extras a presenter may read (workspace title etc.). */
|
|
32
|
+
readonly meta?: Readonly<Record<string, string>>;
|
|
33
|
+
}
|
|
34
|
+
/** The renderer contract a third-party plugin registers. */
|
|
35
|
+
export interface OutputRenderer {
|
|
36
|
+
/** Unique renderer id (kebab-case); the rule field and `/export --renderer` name it. */
|
|
37
|
+
readonly id: string;
|
|
38
|
+
/** Human-readable name. */
|
|
39
|
+
readonly name: string;
|
|
40
|
+
/** One sentence on what the presenter does. */
|
|
41
|
+
readonly description: string;
|
|
42
|
+
/** Applicability rules; an empty array matches everything. */
|
|
43
|
+
readonly match: readonly RendererMatch[];
|
|
44
|
+
/** Higher priority wins; ties break by registration order (earlier first). */
|
|
45
|
+
readonly priority: number;
|
|
46
|
+
/** Pure presentation function: args in, display data out. */
|
|
47
|
+
readonly presenter: (text: string, context: RenderContext) => string;
|
|
48
|
+
}
|
|
49
|
+
/** The auditable render result: original and rendered travel together. */
|
|
50
|
+
export interface RenderedText {
|
|
51
|
+
/** The original model-visible text (never mutated). */
|
|
52
|
+
readonly original: string;
|
|
53
|
+
/** The presented text (equal to the original when nothing matched). */
|
|
54
|
+
readonly rendered: string;
|
|
55
|
+
/** The renderer that applied, or undefined when no renderer matched. */
|
|
56
|
+
readonly rendererId?: string;
|
|
57
|
+
/** Whether the presentation changed the text. */
|
|
58
|
+
readonly changed: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** One per-session/per-tool style rule from the configuration. */
|
|
61
|
+
export interface StyleRule {
|
|
62
|
+
/** Match facts; an empty object matches everything. */
|
|
63
|
+
readonly match: {
|
|
64
|
+
/** Tool name or '*' (omitted = any tool). */
|
|
65
|
+
readonly tool?: string;
|
|
66
|
+
/** Content type (omitted = any). */
|
|
67
|
+
readonly contentType?: ContentType;
|
|
68
|
+
/** Exact session id (omitted = any session) — the per-session axis. */
|
|
69
|
+
readonly session?: string;
|
|
70
|
+
};
|
|
71
|
+
/** Renderer id to apply (built-ins mirror the style names: concise, step-by-step). */
|
|
72
|
+
readonly style: string;
|
|
73
|
+
/** Higher priority wins; ties break by rule order (earlier first). */
|
|
74
|
+
readonly priority: number;
|
|
75
|
+
}
|
|
76
|
+
/** Registry handle returned by register(). */
|
|
77
|
+
export type RendererDisposer = () => void;
|
|
78
|
+
/**
|
|
79
|
+
* Validates a renderer before registration: id grammar, name, description,
|
|
80
|
+
* match shape, priority, presenter function. Throws on the first violation
|
|
81
|
+
* (fail-loud — a bad renderer never sits in the registry).
|
|
82
|
+
* @param renderer - candidate renderer.
|
|
83
|
+
*/
|
|
84
|
+
export declare function validateRenderer(renderer: OutputRenderer): void;
|
|
85
|
+
/**
|
|
86
|
+
* The renderer registry: reversible registration, ordered resolution, and
|
|
87
|
+
* rule-driven rendering. Registration is a caller-owned effect (the runtime
|
|
88
|
+
* hands register()'s disposer to ctx.effect); the registry itself is pure
|
|
89
|
+
* state with no timers, listeners, or I/O.
|
|
90
|
+
*/
|
|
91
|
+
export declare class RendererRegistry {
|
|
92
|
+
private readonly renderers;
|
|
93
|
+
private order;
|
|
94
|
+
/** Register a renderer; the disposer removes exactly this registration. */
|
|
95
|
+
register(renderer: OutputRenderer): RendererDisposer;
|
|
96
|
+
/** Every registered renderer, deterministic order (priority desc, registration asc). */
|
|
97
|
+
list(): OutputRenderer[];
|
|
98
|
+
/** Resolve the renderers that match a request, highest priority first. */
|
|
99
|
+
resolve(context: RenderContext): OutputRenderer[];
|
|
100
|
+
/**
|
|
101
|
+
* Render text through the rule table, then the matching renderer pipeline:
|
|
102
|
+
* the first matching rule names a renderer (applied alone, it is explicit),
|
|
103
|
+
* otherwise every matching renderer applies in priority order (each sees the
|
|
104
|
+
* previous renderer's output — composition, not competition).
|
|
105
|
+
* @param text - the raw model-visible text.
|
|
106
|
+
* @param context - tool / content-type / session facts.
|
|
107
|
+
* @param rules - configured style rules, highest priority first.
|
|
108
|
+
* @returns the auditable result (original always preserved).
|
|
109
|
+
*/
|
|
110
|
+
render(text: string, context: RenderContext, rules?: readonly StyleRule[]): RenderedText;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The built-in style renderers, mirroring the two headline styles: `concise`
|
|
114
|
+
* compacts whitespace under a line/char budget, `step-by-step` numbers list
|
|
115
|
+
* items consistently. Their ids double as `/style`-compatible names in the
|
|
116
|
+
* rule table.
|
|
117
|
+
*/
|
|
118
|
+
export declare const BUILTIN_RENDERERS: readonly OutputRenderer[];
|
|
119
|
+
//# sourceMappingURL=renderers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderers.d.ts","sourceRoot":"","sources":["../../src/renderers.ts"],"names":[],"mappings":"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"}
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -141,4 +141,15 @@ export declare class OutputStyleRuntime {
|
|
|
141
141
|
* @param config - configuration resolved by Cordis from the exported schema.
|
|
142
142
|
*/
|
|
143
143
|
export declare function apply(ctx: Context, config: Config): Promise<void>;
|
|
144
|
+
/** Parsed `/export` invocation. */
|
|
145
|
+
type ExportInput = {
|
|
146
|
+
kind: 'ok';
|
|
147
|
+
format: 'markdown' | 'html';
|
|
148
|
+
renderer?: string;
|
|
149
|
+
} | {
|
|
150
|
+
kind: 'error';
|
|
151
|
+
};
|
|
152
|
+
/** Parse `/export [markdown|html] [--renderer=<id>]` from the raw command input. */
|
|
153
|
+
export declare function parseExportInput(rawInput: unknown): ExportInput;
|
|
154
|
+
export {};
|
|
144
155
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAKlE,OAAO,KAAK,EAAE,MAAM,EAA2B,MAAM,iCAAiC,CAAA;AAGtF,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AAExD,OAAO,EAAmC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEtF,OAAO,EAAE,mBAAmB,EAA0C,KAAK,cAAc,EAA2B,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"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"}
|
package/lib/types/types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
11
11
|
import { z as zod } from 'zod';
|
|
12
|
+
import type { OutputRenderer, RenderContext, RenderedText } from './renderers.ts';
|
|
12
13
|
/** The reserved switch target that removes a session's selection. */
|
|
13
14
|
export declare const OFF = "off";
|
|
14
15
|
/**
|
|
@@ -75,4 +76,35 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
|
75
76
|
style: StyleSelectionView;
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
/** The `ctx.outputRenderers` service: the output.render.* renderer registry. */
|
|
80
|
+
export interface OutputRenderersService {
|
|
81
|
+
/** Register a renderer (returns the disposer; caller owns it via ctx.effect). */
|
|
82
|
+
register(renderer: OutputRenderer): () => void;
|
|
83
|
+
/** Every registered renderer, deterministic order (priority desc, registration asc). */
|
|
84
|
+
list(): OutputRenderer[];
|
|
85
|
+
/** Renderers matching a request, highest priority first. */
|
|
86
|
+
resolve(context: RenderContext): OutputRenderer[];
|
|
87
|
+
/** Render text through the `output.render/before` waterfall, then rules + matched renderers. */
|
|
88
|
+
renderText(text: string, context: RenderContext): Promise<RenderedText>;
|
|
89
|
+
}
|
|
90
|
+
declare module '@deepseek-ai/cordis' {
|
|
91
|
+
interface Context {
|
|
92
|
+
/** The output.render.* renderer registry provided by dsh-output-styles. */
|
|
93
|
+
outputRenderers: OutputRenderersService;
|
|
94
|
+
}
|
|
95
|
+
interface Events {
|
|
96
|
+
/**
|
|
97
|
+
* Pre-render waterfall: listeners receive `{ text, context }` and MUST
|
|
98
|
+
* call `next()` with their transformed request (or the unchanged one);
|
|
99
|
+
* returning without `next()` short-circuits the render pipeline.
|
|
100
|
+
*/
|
|
101
|
+
'output.render/before'(request: {
|
|
102
|
+
text: string;
|
|
103
|
+
context: RenderContext;
|
|
104
|
+
}, next: (request: {
|
|
105
|
+
text: string;
|
|
106
|
+
context: RenderContext;
|
|
107
|
+
}) => Promise<RenderedText>): Promise<RenderedText>;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
78
110
|
//# sourceMappingURL=types.d.ts.map
|
package/lib/types/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-output-styles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Claude Code outputStyles-equivalent runtime output-style switching for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
},
|
|
100
100
|
"install": {
|
|
101
101
|
"mode": "transactional",
|
|
102
|
-
"adapter": "
|
|
102
|
+
"adapter": "profile-bundle",
|
|
103
103
|
"failurePolicy": "generation-rollback",
|
|
104
104
|
"touchesCurrentBeforeActivation": false
|
|
105
105
|
},
|
package/src/config.ts
CHANGED
|
@@ -11,6 +11,23 @@
|
|
|
11
11
|
import z from '@deepseek-ai/schemastery'
|
|
12
12
|
import { resolve } from 'node:path'
|
|
13
13
|
|
|
14
|
+
/** One per-session/per-tool style rule: match facts + the renderer to apply. */
|
|
15
|
+
export interface StyleRuleConfig {
|
|
16
|
+
/** Match facts; an empty object matches every render request. */
|
|
17
|
+
match: {
|
|
18
|
+
/** Tool name or '*' (omitted = any tool). */
|
|
19
|
+
tool?: string
|
|
20
|
+
/** Content type (omitted = any). */
|
|
21
|
+
contentType?: 'text' | 'markdown' | 'html'
|
|
22
|
+
/** Exact session id (omitted = any session) — the per-session axis. */
|
|
23
|
+
session?: string
|
|
24
|
+
}
|
|
25
|
+
/** Renderer id to apply; built-in ids mirror the style names (concise, step-by-step). */
|
|
26
|
+
style: string
|
|
27
|
+
/** Higher priority wins; ties break by rule order (earlier first). */
|
|
28
|
+
priority?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/** Plugin configuration supplied by the profile composition. */
|
|
15
32
|
export interface Config {
|
|
16
33
|
/**
|
|
@@ -42,6 +59,10 @@ export interface Config {
|
|
|
42
59
|
includeBuiltins?: boolean
|
|
43
60
|
/** Reload the library when a style file changes on disk (default true). */
|
|
44
61
|
watchStyles?: boolean
|
|
62
|
+
/** Per-session/per-tool render rules (renderer registry); applied by `/export` and the render service. */
|
|
63
|
+
rules?: StyleRuleConfig[]
|
|
64
|
+
/** Register the `/export` command (Markdown/HTML session-export, renderer-aware). */
|
|
65
|
+
enableExport?: boolean
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
/** Configuration after defaults have been resolved. */
|
|
@@ -62,6 +83,10 @@ export interface ResolvedConfig {
|
|
|
62
83
|
includeBuiltins: boolean
|
|
63
84
|
/** Whether the library reloads on style-file changes. */
|
|
64
85
|
watchStyles: boolean
|
|
86
|
+
/** Per-session/per-tool render rules with resolved priorities. */
|
|
87
|
+
rules: Array<{ match: { tool?: string; contentType?: 'text' | 'markdown' | 'html'; session?: string }; style: string; priority: number }>
|
|
88
|
+
/** Whether the `/export` command registers. */
|
|
89
|
+
enableExport: boolean
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
/** Loader-visible configuration schema and defaults. */
|
|
@@ -74,6 +99,16 @@ export const Config: z<Config> = z.object({
|
|
|
74
99
|
truncationMarker: z.string().default('\n\n[style truncated]'),
|
|
75
100
|
includeBuiltins: z.boolean().default(true),
|
|
76
101
|
watchStyles: z.boolean().default(true),
|
|
102
|
+
rules: z.array(z.object({
|
|
103
|
+
match: z.object({
|
|
104
|
+
tool: z.string().required(false),
|
|
105
|
+
contentType: z.union([z.const('text'), z.const('markdown'), z.const('html')]).required(false),
|
|
106
|
+
session: z.string().required(false),
|
|
107
|
+
}).required(false),
|
|
108
|
+
style: z.string().min(1),
|
|
109
|
+
priority: z.number().required(false),
|
|
110
|
+
})).default([]),
|
|
111
|
+
enableExport: z.boolean().default(true),
|
|
77
112
|
})
|
|
78
113
|
|
|
79
114
|
/**
|
|
@@ -97,6 +132,18 @@ export function resolveConfig(config: Config, defaultStylesDir: string): Resolve
|
|
|
97
132
|
const rawDirs = Array.isArray(config.stylesDir) ? config.stylesDir : config.stylesDir === undefined || config.stylesDir === '' ? [] : [config.stylesDir]
|
|
98
133
|
const customDirs = rawDirs.map(dir => resolve(dir))
|
|
99
134
|
const stylesDirs = includeBuiltins ? [defaultStylesDir, ...customDirs] : customDirs
|
|
135
|
+
for (const rule of config.rules ?? []) {
|
|
136
|
+
const match = rule.match ?? {}
|
|
137
|
+
if (match.tool !== undefined && match.tool !== '*' && /[^a-zA-Z0-9_-]/.test(match.tool)) {
|
|
138
|
+
throw new Error(`dsh-output-styles: rule tool ${JSON.stringify(match.tool)} must be a tool name or '*'`)
|
|
139
|
+
}
|
|
140
|
+
if (rule.style === '' || /[^a-z0-9-]/.test(rule.style)) {
|
|
141
|
+
throw new Error(`dsh-output-styles: rule style ${JSON.stringify(rule.style)} must be a kebab-case renderer id`)
|
|
142
|
+
}
|
|
143
|
+
if (rule.priority !== undefined && !Number.isFinite(rule.priority)) {
|
|
144
|
+
throw new Error(`dsh-output-styles: rule priority must be a finite number, got ${String(rule.priority)}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
100
147
|
return {
|
|
101
148
|
stylesDirs,
|
|
102
149
|
maxStyleChars,
|
|
@@ -106,5 +153,7 @@ export function resolveConfig(config: Config, defaultStylesDir: string): Resolve
|
|
|
106
153
|
truncationMarker: config.truncationMarker ?? '\n\n[style truncated]',
|
|
107
154
|
includeBuiltins,
|
|
108
155
|
watchStyles: config.watchStyles ?? true,
|
|
156
|
+
rules: (config.rules ?? []).map(rule => ({ ...rule, priority: rule.priority ?? 0 })),
|
|
157
|
+
enableExport: config.enableExport ?? true,
|
|
109
158
|
}
|
|
110
159
|
}
|
package/src/export.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session export: a pure projection of the current session's message surface
|
|
3
|
+
* into Markdown or sanitized HTML, with renderer application on top. The
|
|
4
|
+
* extraction reads only the public `session.events` log through the official
|
|
5
|
+
* `deriveEventMessage` projection — the same rule the harness uses to build
|
|
6
|
+
* model requests — so the exported document is reconstructable from the log.
|
|
7
|
+
* Every render application preserves the original text beside the rendered
|
|
8
|
+
* one, keeping the auditable pair intact inside the export document.
|
|
9
|
+
*
|
|
10
|
+
* Host-side module (imports the dsh-session surface projection); the pure
|
|
11
|
+
* Markdown/HTML/sanitize functions stay free of both DOM and I/O.
|
|
12
|
+
* @module dsh-output-styles/export
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { deriveEventMessage, foldSurface } from '@deepseek-ai/dsh-session/surface'
|
|
16
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
17
|
+
import type { RendererRegistry, RenderedText } from './renderers.ts'
|
|
18
|
+
|
|
19
|
+
/** One exported conversation line. */
|
|
20
|
+
export interface ExportLine {
|
|
21
|
+
/** Speaker role: 'user', 'assistant', or 'tool'. */
|
|
22
|
+
readonly role: 'user' | 'assistant' | 'tool'
|
|
23
|
+
/** The original message text (or tool-call/tool-result rendering). */
|
|
24
|
+
readonly text: string
|
|
25
|
+
/** Tool name for tool lines, otherwise undefined. */
|
|
26
|
+
readonly tool?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A completed export document (renderer application included). */
|
|
30
|
+
export interface ExportDocument {
|
|
31
|
+
readonly plugin: 'dsh-output-styles'
|
|
32
|
+
readonly schema: 'output-export-v1'
|
|
33
|
+
readonly format: 'markdown' | 'html'
|
|
34
|
+
readonly exportedAt: string
|
|
35
|
+
/** The rendered document text (what the user copies/downloads). */
|
|
36
|
+
readonly text: string
|
|
37
|
+
readonly lines: readonly ExportLine[]
|
|
38
|
+
readonly rendered: readonly RenderedText[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract the message surface of a session log as plain lines. Tool calls and
|
|
43
|
+
* tool results render as labeled lines so the transcript stays readable; the
|
|
44
|
+
* content is never summarized — this is a projection, not an interpretation.
|
|
45
|
+
* @param events - the session log, in seq order.
|
|
46
|
+
* @returns the conversation lines in surface order.
|
|
47
|
+
*/
|
|
48
|
+
export function conversationLines(events: readonly SessionEvent[]): ExportLine[] {
|
|
49
|
+
const { nodes } = foldSurface(events)
|
|
50
|
+
const lines: ExportLine[] = []
|
|
51
|
+
for (const seq of nodes) {
|
|
52
|
+
const event = events.find(item => item.seq === seq)
|
|
53
|
+
if (event === undefined) continue
|
|
54
|
+
const message = deriveEventMessage(event)
|
|
55
|
+
if (message === null) continue
|
|
56
|
+
const described = describeMessage(message)
|
|
57
|
+
if (described.text === '') continue
|
|
58
|
+
lines.push(described)
|
|
59
|
+
}
|
|
60
|
+
return lines
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Project one derived message into an export line: string content becomes a
|
|
65
|
+
* user/assistant line; a content carrying only tool_use parts becomes a tool
|
|
66
|
+
* call line; only tool_result parts become a tool result line. Mixed content
|
|
67
|
+
* keeps the text parts and the message role.
|
|
68
|
+
*/
|
|
69
|
+
function describeMessage(message: { readonly content?: unknown; readonly role?: string }): ExportLine {
|
|
70
|
+
const content = message.content
|
|
71
|
+
const baseRole: 'user' | 'assistant' = message.role === 'assistant' ? 'assistant' : 'user'
|
|
72
|
+
if (typeof content === 'string') {
|
|
73
|
+
return { role: baseRole, text: content.trim() }
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(content)) {
|
|
76
|
+
const textParts: string[] = []
|
|
77
|
+
const toolUses: string[] = []
|
|
78
|
+
const toolResults: string[] = []
|
|
79
|
+
for (const part of content) {
|
|
80
|
+
if (part === null || typeof part !== 'object') continue
|
|
81
|
+
const record = part as Record<string, unknown>
|
|
82
|
+
if (record['type'] === 'text' && typeof record['text'] === 'string') {
|
|
83
|
+
textParts.push(record['text'])
|
|
84
|
+
} else if (record['type'] === 'tool_use') {
|
|
85
|
+
toolUses.push(String(record['name'] ?? 'tool'))
|
|
86
|
+
} else if (record['type'] === 'tool_result') {
|
|
87
|
+
const raw = record['content']
|
|
88
|
+
const body = typeof raw === 'string'
|
|
89
|
+
? raw
|
|
90
|
+
: Array.isArray(raw)
|
|
91
|
+
? raw.map(item => typeof item === 'object' && item !== null && typeof (item as Record<string, unknown>)['text'] === 'string' ? (item as Record<string, unknown>)['text'] : '').join(' ')
|
|
92
|
+
: ''
|
|
93
|
+
toolResults.push(body.trim())
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (textParts.length === 0 && toolUses.length > 0) {
|
|
97
|
+
const first = toolUses[0]
|
|
98
|
+
return first === undefined
|
|
99
|
+
? { role: 'tool', text: '[tool call]' }
|
|
100
|
+
: { role: 'tool', tool: first, text: toolUses.map(name => `[tool call: ${name}]`).join('\n') }
|
|
101
|
+
}
|
|
102
|
+
if (textParts.length === 0 && toolResults.length > 0) {
|
|
103
|
+
return { role: 'tool', tool: 'tool-result', text: toolResults.join('\n') }
|
|
104
|
+
}
|
|
105
|
+
return { role: baseRole, text: textParts.join('\n').trim() }
|
|
106
|
+
}
|
|
107
|
+
return { role: baseRole, text: '' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sanitize one text for HTML embedding: escape the five markup-significant
|
|
112
|
+
* characters and strip control characters except newline/tab. Pure function,
|
|
113
|
+
* covered by extreme-case tests (tags, null bytes, huge inputs).
|
|
114
|
+
* @param text - the raw text.
|
|
115
|
+
* @returns the HTML-safe text.
|
|
116
|
+
*/
|
|
117
|
+
export function sanitizeText(text: string): string {
|
|
118
|
+
// eslint-disable-next-line no-control-regex
|
|
119
|
+
return text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
|
|
120
|
+
.replace(/&/g, '&')
|
|
121
|
+
.replace(/</g, '<')
|
|
122
|
+
.replace(/>/g, '>')
|
|
123
|
+
.replace(/"/g, '"')
|
|
124
|
+
.replace(/'/g, ''')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Render one export line as a Markdown block: headings by role and fenced
|
|
129
|
+
* blocks for tool lines so multi-line content never breaks the transcript.
|
|
130
|
+
* @param line - the export line.
|
|
131
|
+
* @returns the Markdown text.
|
|
132
|
+
*/
|
|
133
|
+
export function lineToMarkdown(line: ExportLine): string {
|
|
134
|
+
if (line.tool !== undefined) {
|
|
135
|
+
return `### \`${line.tool}\`\n\n\`\`\`text\n${line.text}\n\`\`\``
|
|
136
|
+
}
|
|
137
|
+
const heading = line.role === 'user' ? '## User' : '## Assistant'
|
|
138
|
+
return `${heading}\n\n${line.text}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Build the complete Markdown document for a set of lines. */
|
|
142
|
+
export function toMarkdown(lines: readonly ExportLine[]): string {
|
|
143
|
+
const body = lines.map(lineToMarkdown).join('\n\n')
|
|
144
|
+
return `# Session export\n\n${body === '' ? '_No messages yet._' : body}\n`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Build the complete sanitized HTML document for a set of lines. */
|
|
148
|
+
export function toHtml(lines: readonly ExportLine[]): string {
|
|
149
|
+
const body = lines.map((line) => {
|
|
150
|
+
const safe = sanitizeText(line.text).replace(/\n/g, '<br>')
|
|
151
|
+
if (line.tool !== undefined) {
|
|
152
|
+
return `<h3><code>${sanitizeText(line.tool)}</code></h3>\n<pre>${safe}</pre>`
|
|
153
|
+
}
|
|
154
|
+
const heading = line.role === 'user' ? 'User' : 'Assistant'
|
|
155
|
+
return `<h2>${heading}</h2>\n<p>${safe}</p>`
|
|
156
|
+
}).join('\n')
|
|
157
|
+
return '<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><title>Session export</title></head>\n<body>\n'
|
|
158
|
+
+ `<h1>Session export</h1>\n${body === '' ? '<p><em>No messages yet.</em></p>' : body}\n</body>\n</html>\n`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Apply the renderer pipeline to every line of a conversation (per-tool and
|
|
163
|
+
* per-content-type rules decide which renderer touches which line) and build
|
|
164
|
+
* the export document. Both the rendered document and the original lines
|
|
165
|
+
* stay inside the returned document, so the render application is auditable.
|
|
166
|
+
* @param registry - the renderer registry.
|
|
167
|
+
* @param lines - the extracted conversation lines.
|
|
168
|
+
* @param format - export format.
|
|
169
|
+
* @param rules - configured style rules.
|
|
170
|
+
* @param now - exportedAt timestamp (injected for deterministic tests).
|
|
171
|
+
* @returns the complete export document.
|
|
172
|
+
*/
|
|
173
|
+
export function renderExport(
|
|
174
|
+
registry: RendererRegistry,
|
|
175
|
+
lines: readonly ExportLine[],
|
|
176
|
+
format: 'markdown' | 'html',
|
|
177
|
+
rules: readonly import('./renderers.ts').StyleRule[],
|
|
178
|
+
now: Date = new Date(),
|
|
179
|
+
): ExportDocument {
|
|
180
|
+
const rendered: RenderedText[] = []
|
|
181
|
+
const presented: ExportLine[] = lines.map((line) => {
|
|
182
|
+
const result = registry.render(line.text, {
|
|
183
|
+
tool: line.tool ?? '',
|
|
184
|
+
contentType: 'markdown',
|
|
185
|
+
}, rules)
|
|
186
|
+
rendered.push(result)
|
|
187
|
+
return { ...line, text: result.rendered }
|
|
188
|
+
})
|
|
189
|
+
const document = format === 'markdown' ? toMarkdown(presented) : toHtml(presented)
|
|
190
|
+
return {
|
|
191
|
+
plugin: 'dsh-output-styles',
|
|
192
|
+
schema: 'output-export-v1',
|
|
193
|
+
format,
|
|
194
|
+
exportedAt: now.toISOString(),
|
|
195
|
+
text: document,
|
|
196
|
+
lines: presented,
|
|
197
|
+
rendered,
|
|
198
|
+
}
|
|
199
|
+
}
|