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.
@@ -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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"}
@@ -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":"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"}
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;AAItI,kGAAkG;AAClG,eAAO,MAAM,kBAAkB,QAAwD,CAAA;AAEvF,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,2BAA2B,CAAA;AAQ1D;;;;;;GAMG;AACH,qBAAa,kBAAkB;IAqB3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IApBzB,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,WAAW,CAAyB;IAE5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;IAC9D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,cAAc,CAAc;IAEpC,2DAA2D;IAC3D,IAAI,MAAM,IAAI,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,CAE7C;IAED;;;;OAIG;gBAEgB,MAAM,EAAE,MAAM,CAAC,OAAO,mBAAmB,CAAC,EAC3D,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,EACxC,OAAO,EAAE;QACP,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;QAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;KAClC;IAWH,qDAAqD;IACrD,IAAI,KAAK,IAAI,SAAS,MAAM,EAAE,CAE7B;IAED,4EAA4E;IAC5E,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;IAED;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI;IAKtD;;;;OAIG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,MAAM,GAAG,IAAI;IAI1C;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI1C;;;;OAIG;IACH,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,cAAc,GAAG,SAAS;IAI9D;;;;;;;OAOG;IACH,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS;IAO7D;;;;OAIG;IACH,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM;IAIzC;;;;;;OAMG;IACH,UAAU,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM;IAOxC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM;IAUtC;;;;;OAKG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAItC;;;;;;OAMG;IACG,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3D;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAmBD;;;;;;;;;GASG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4QvE;AAED,mCAAmC;AACnC,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErG,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG,WAAW,CAkB/D"}
@@ -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
@@ -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;AAG9B,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"}
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.2",
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": "harness-profile",
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, '&amp;')
121
+ .replace(/</g, '&lt;')
122
+ .replace(/>/g, '&gt;')
123
+ .replace(/"/g, '&quot;')
124
+ .replace(/'/g, '&#39;')
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
+ }