dsh-output-styles 0.4.3 → 0.6.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 +30 -0
- package/README.es.md +9 -5
- package/README.hi.md +9 -5
- package/README.md +10 -5
- package/README.pt.md +9 -5
- package/README.zh.md +9 -5
- package/cordis.patch.yml +5 -0
- package/docs/COEXISTENCE.md +62 -0
- package/docs/renderer-protocol.md +10 -0
- package/docs/renderer-protocol.zh.md +8 -0
- package/lib/index.js +186 -38
- package/lib/types/coexist.d.ts +49 -0
- package/lib/types/coexist.d.ts.map +1 -0
- package/lib/types/config.d.ts +9 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +47 -1
- package/lib/types/runtime.d.ts.map +1 -1
- package/package.json +11 -1
- package/src/coexist.ts +68 -0
- package/src/config.ts +11 -0
- package/src/index.ts +2 -0
- package/src/runtime.ts +198 -43
package/src/runtime.ts
CHANGED
|
@@ -20,12 +20,13 @@ import type { Domain, DomainFacility, KvTable } from '@deepseek-ai/dsh-storage-d
|
|
|
20
20
|
import z from '@deepseek-ai/schemastery'
|
|
21
21
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
22
22
|
import { resolveConfig, type Config } from './config.ts'
|
|
23
|
+
import { detectCoreOutputStyles } from './coexist.ts'
|
|
23
24
|
import { installInvariant, PACKAGE_NAME, type InvariantFacts, type InvariantRegistry } from './invariant.ts'
|
|
24
25
|
import { loadStyleLibrary, truncateStyle, type OutputStyle } from './style-library.ts'
|
|
25
26
|
import { applyStyleEvent, EMPTY_STYLE_STATE, parseStyleInput, STYLE_COMMAND, type StyleFoldState } from './style-command.ts'
|
|
26
27
|
import { OUTPUT_STYLE_DOMAIN, STYLE_SOURCE, styleFoldStateSchema, styleSelectionViewSchema, type StyleSelection, type StyleSelectionView } from './types.ts'
|
|
27
28
|
import { BUILTIN_RENDERERS, RendererRegistry, type OutputRenderer, type RenderContext, type RenderedText, type StyleRule } from './renderers.ts'
|
|
28
|
-
import { conversationLines, renderExport } from './export.ts'
|
|
29
|
+
import { conversationLines, renderExport, sanitizeText } from './export.ts'
|
|
29
30
|
|
|
30
31
|
/** Bundled style-library directory (package `styles/`), the lowest-priority `stylesDir` entry. */
|
|
31
32
|
export const DEFAULT_STYLES_DIR = fileURLToPath(new URL('../styles/', import.meta.url))
|
|
@@ -265,6 +266,14 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
265
266
|
)
|
|
266
267
|
}
|
|
267
268
|
const resolved = resolveConfig(config, DEFAULT_STYLES_DIR)
|
|
269
|
+
// Coexistence with a core outputStyles capability: when it is composed (and
|
|
270
|
+
// respectCoreOutputStyles is true), prompt injection belongs to the core —
|
|
271
|
+
// this plugin keeps hot-switch / rules / export and skips the two injections
|
|
272
|
+
// below to avoid double-injecting a style directive.
|
|
273
|
+
const coreActive = resolved.respectCoreOutputStyles && detectCoreOutputStyles(ctx)
|
|
274
|
+
if (coreActive) {
|
|
275
|
+
ctx.logger.info('dsh-output-styles: core outputStyles detected — degrading to hot-switch + rules + export (no prompt injection)')
|
|
276
|
+
}
|
|
268
277
|
const styles = loadStyleLibrary(
|
|
269
278
|
resolved.stylesDirs,
|
|
270
279
|
{ compatJson: resolved.compatJson },
|
|
@@ -348,36 +357,38 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
348
357
|
},
|
|
349
358
|
)
|
|
350
359
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
if (!coreActive) {
|
|
361
|
+
ctx.systemPrompt.section({
|
|
362
|
+
name: STYLE_SECTION_NAME,
|
|
363
|
+
order: resolved.sectionOrder,
|
|
364
|
+
text: (context: AssembleContext) => {
|
|
365
|
+
const agent = context.agent
|
|
366
|
+
if (agent === undefined) return ''
|
|
367
|
+
const style = runtime.effectiveStyle(agent.session.id)
|
|
368
|
+
// A keep-coding-instructions: false style owns the whole prompt: the
|
|
369
|
+
// assembly waterfall below rebuilds the section list, so this section
|
|
370
|
+
// stays silent instead of double-injecting.
|
|
371
|
+
if (style === undefined || !style.keepCodingInstructions) return ''
|
|
372
|
+
return runtime.promptText(agent.session.id)
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// Claude Code keep-coding-instructions: false — the active style replaces
|
|
377
|
+
// the whole system prompt. The waterfall runs after downstream
|
|
378
|
+
// contributions resolve (tools, contexts, and variables still assemble),
|
|
379
|
+
// then swaps in a single style section.
|
|
380
|
+
ctx.on('system-prompt/assemble', async (_assembly: PromptAssembly, context: AssembleContext, next) => {
|
|
381
|
+
const out = await next()
|
|
355
382
|
const agent = context.agent
|
|
356
|
-
if (agent === undefined) return
|
|
383
|
+
if (agent === undefined) return out
|
|
357
384
|
const style = runtime.effectiveStyle(agent.session.id)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Claude Code keep-coding-instructions: false — the active style replaces
|
|
367
|
-
// the whole system prompt. The waterfall runs after downstream
|
|
368
|
-
// contributions resolve (tools, contexts, and variables still assemble),
|
|
369
|
-
// then swaps in a single style section.
|
|
370
|
-
ctx.on('system-prompt/assemble', async (_assembly: PromptAssembly, context: AssembleContext, next) => {
|
|
371
|
-
const out = await next()
|
|
372
|
-
const agent = context.agent
|
|
373
|
-
if (agent === undefined) return out
|
|
374
|
-
const style = runtime.effectiveStyle(agent.session.id)
|
|
375
|
-
if (style === undefined || style.keepCodingInstructions) return out
|
|
376
|
-
return {
|
|
377
|
-
...out,
|
|
378
|
-
sections: [{ name: STYLE_SECTION_NAME, text: runtime.promptText(agent.session.id) }],
|
|
379
|
-
}
|
|
380
|
-
})
|
|
385
|
+
if (style === undefined || style.keepCodingInstructions) return out
|
|
386
|
+
return {
|
|
387
|
+
...out,
|
|
388
|
+
sections: [{ name: STYLE_SECTION_NAME, text: runtime.promptText(agent.session.id) }],
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
}
|
|
381
392
|
|
|
382
393
|
// The /style command: the one write path a web client uses. The child
|
|
383
394
|
// activates only when a command registry is composed (headless assemblies
|
|
@@ -507,23 +518,41 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
507
518
|
// Markdown or sanitized HTML through the renderer pipeline. The document
|
|
508
519
|
// itself is the visible artifact; the original lines are the session log
|
|
509
520
|
// the export was projected from — rendered and original stay reconstructable.
|
|
521
|
+
// `--save <path>` additionally writes the sanitized document to that
|
|
522
|
+
// workspace path, gated by the approval service and the fs service (both
|
|
523
|
+
// optional; a missing approval service denies the write, a missing fs
|
|
524
|
+
// service fails loudly).
|
|
510
525
|
if (resolved.enableExport) {
|
|
511
526
|
ctx.inject(['commands'], (commandCtx) => {
|
|
512
527
|
commandCtx.commands.register({
|
|
513
528
|
name: 'export',
|
|
514
529
|
description: 'Export this session as Markdown or HTML (renderer-aware)',
|
|
515
|
-
input: { hint: '[markdown|html] [--renderer=<id>]' },
|
|
516
|
-
handler: async ({ agent, rawInput }) => {
|
|
530
|
+
input: { hint: '[markdown|html] [--renderer=<id>] [--save <path>]' },
|
|
531
|
+
handler: async ({ agent, rawInput, signal }) => {
|
|
517
532
|
const input = parseExportInput(rawInput)
|
|
518
533
|
if (input.kind === 'error') {
|
|
519
|
-
return { kind: 'error', text: 'usage: /export [markdown|html] [--renderer=<id>]' }
|
|
534
|
+
return { kind: 'error', text: 'usage: /export [markdown|html] [--renderer=<id>] [--save <path>]' }
|
|
520
535
|
}
|
|
521
536
|
const lines = conversationLines(agent.session.events)
|
|
522
537
|
const rules: StyleRule[] = input.renderer === undefined
|
|
523
538
|
? [...effectiveRules]
|
|
524
539
|
: [{ match: {}, style: input.renderer, priority: 0 }]
|
|
525
540
|
const document = renderExport(renderers, lines, input.format, rules)
|
|
526
|
-
|
|
541
|
+
if (input.save === undefined) {
|
|
542
|
+
return { kind: 'success', text: document.text }
|
|
543
|
+
}
|
|
544
|
+
// The disk path sanitizes the rendered document before writing and
|
|
545
|
+
// writes only after the approval service grants it.
|
|
546
|
+
const saved = await saveExportFile(
|
|
547
|
+
ctx.get('fs') as ExportFileSystem | undefined,
|
|
548
|
+
ctx.get('approval') as ExportApproval | undefined,
|
|
549
|
+
agent,
|
|
550
|
+
input.save,
|
|
551
|
+
sanitizeText(document.text),
|
|
552
|
+
signal,
|
|
553
|
+
)
|
|
554
|
+
if (saved.kind === 'error') return { kind: 'error', text: saved.text }
|
|
555
|
+
return { kind: 'success', text: `saved ${input.format} export to ${saved.path}` }
|
|
527
556
|
},
|
|
528
557
|
})
|
|
529
558
|
})
|
|
@@ -531,25 +560,151 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
531
560
|
}
|
|
532
561
|
|
|
533
562
|
/** Parsed `/export` invocation. */
|
|
534
|
-
type ExportInput = {
|
|
535
|
-
|
|
536
|
-
|
|
563
|
+
type ExportInput = {
|
|
564
|
+
kind: 'ok'
|
|
565
|
+
format: 'markdown' | 'html'
|
|
566
|
+
renderer?: string
|
|
567
|
+
save?: string
|
|
568
|
+
} | { kind: 'error' }
|
|
569
|
+
|
|
570
|
+
/** Parse `/export [markdown|html] [--renderer=<id>] [--save <path>]` from the raw command input. */
|
|
537
571
|
export function parseExportInput(rawInput: unknown): ExportInput {
|
|
538
572
|
const raw = String(rawInput ?? '').trim()
|
|
539
573
|
const parts = raw === '' ? [] : raw.split(/\s+/)
|
|
540
574
|
let format: 'markdown' | 'html' = 'markdown'
|
|
541
575
|
let renderer: string | undefined
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
576
|
+
let save: string | undefined
|
|
577
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
578
|
+
const part = parts[index]
|
|
579
|
+
if (part === undefined) continue
|
|
580
|
+
if (part === 'markdown' || part === 'md') {
|
|
581
|
+
format = 'markdown'
|
|
582
|
+
continue
|
|
583
|
+
}
|
|
584
|
+
if (part === 'html') {
|
|
585
|
+
format = 'html'
|
|
545
586
|
continue
|
|
546
587
|
}
|
|
547
|
-
const
|
|
548
|
-
if (
|
|
549
|
-
renderer =
|
|
588
|
+
const rendererMatch = /^--renderer=([a-z0-9][a-z0-9-]*)$/.exec(part)
|
|
589
|
+
if (rendererMatch !== null) {
|
|
590
|
+
renderer = rendererMatch[1]
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
const saveInline = /^--save=(.+)$/.exec(part)
|
|
594
|
+
if (saveInline !== null) {
|
|
595
|
+
save = saveInline[1]
|
|
596
|
+
continue
|
|
597
|
+
}
|
|
598
|
+
if (part === '--save') {
|
|
599
|
+
const next = parts[index + 1]
|
|
600
|
+
if (next === undefined || next === '') return { kind: 'error' }
|
|
601
|
+
save = next
|
|
602
|
+
index += 1
|
|
550
603
|
continue
|
|
551
604
|
}
|
|
552
605
|
return { kind: 'error' }
|
|
553
606
|
}
|
|
554
|
-
return {
|
|
607
|
+
return {
|
|
608
|
+
kind: 'ok',
|
|
609
|
+
format,
|
|
610
|
+
...renderer === undefined ? {} : { renderer },
|
|
611
|
+
...save === undefined ? {} : { save },
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Approval outcome vocabulary, structural (mirrors the approval seam without importing it). */
|
|
616
|
+
type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
|
617
|
+
|
|
618
|
+
/** Structural slice of the DSH filesystem service the save path uses. */
|
|
619
|
+
export interface ExportFileSystem {
|
|
620
|
+
resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<unknown>
|
|
621
|
+
writeText(target: unknown, content: string): Promise<unknown>
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Structural slice of the DSH approval service the save path uses. */
|
|
625
|
+
export interface ExportApproval {
|
|
626
|
+
request(request: { agent: unknown; toolName: string; reason: string; signal?: AbortSignal }): Promise<ApprovalOutcome>
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Error codes of the `/export --save` path; each is a stable machine-readable label. */
|
|
630
|
+
export type ExportSaveErrorCode = 'fs-unavailable' | 'approval-unavailable' | 'approval-denied' | 'approval-cancelled'
|
|
631
|
+
|
|
632
|
+
/** Result of a `/export --save` attempt: the written path or a structured failure. */
|
|
633
|
+
export type ExportSaveResult =
|
|
634
|
+
| { readonly kind: 'written'; readonly path: string }
|
|
635
|
+
| { readonly kind: 'error'; readonly code: ExportSaveErrorCode; readonly text: string }
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Write one rendered export document to a workspace path after user approval.
|
|
639
|
+
* Fail-closed: a missing approval service, a rejected/cancelled/unavailable
|
|
640
|
+
* decision, or a throwing approval channel all deny the write; a missing fs
|
|
641
|
+
* service fails loudly with a structured error. The caller already sanitized
|
|
642
|
+
* `content` before this write.
|
|
643
|
+
* @param fs - the fs service (`ctx.get('fs')`), or undefined when none is composed.
|
|
644
|
+
* @param approval - the approval service (`ctx.get('approval')`), or undefined when none is composed.
|
|
645
|
+
* @param agent - the agent whose session the export belongs to (routes the approval).
|
|
646
|
+
* @param path - the workspace path to write.
|
|
647
|
+
* @param content - the sanitized document text.
|
|
648
|
+
* @param signal - the command's abort signal, forwarded to approval and resolve.
|
|
649
|
+
* @returns the written path, or a structured failure.
|
|
650
|
+
*/
|
|
651
|
+
export async function saveExportFile(
|
|
652
|
+
fs: ExportFileSystem | undefined,
|
|
653
|
+
approval: ExportApproval | undefined,
|
|
654
|
+
agent: unknown,
|
|
655
|
+
path: string,
|
|
656
|
+
content: string,
|
|
657
|
+
signal?: AbortSignal,
|
|
658
|
+
): Promise<ExportSaveResult> {
|
|
659
|
+
if (approval === undefined) {
|
|
660
|
+
return {
|
|
661
|
+
kind: 'error',
|
|
662
|
+
code: 'approval-unavailable',
|
|
663
|
+
text: 'dsh-output-styles: /export --save requires an approval service (compose @deepseek-ai/dsh-user-approval); nothing was written',
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
let outcome: ApprovalOutcome
|
|
667
|
+
try {
|
|
668
|
+
outcome = await approval.request({
|
|
669
|
+
agent,
|
|
670
|
+
toolName: 'export',
|
|
671
|
+
reason: `write the exported document to ${path}`,
|
|
672
|
+
...signal === undefined ? {} : { signal },
|
|
673
|
+
})
|
|
674
|
+
} catch {
|
|
675
|
+
// An approval channel that cannot answer is an unanswerable ask: fail closed.
|
|
676
|
+
outcome = 'unavailable'
|
|
677
|
+
}
|
|
678
|
+
switch (outcome) {
|
|
679
|
+
case 'allowed-once': break
|
|
680
|
+
case 'rejected':
|
|
681
|
+
return {
|
|
682
|
+
kind: 'error',
|
|
683
|
+
code: 'approval-denied',
|
|
684
|
+
text: 'dsh-output-styles: /export --save was rejected; nothing was written',
|
|
685
|
+
}
|
|
686
|
+
case 'cancelled':
|
|
687
|
+
return {
|
|
688
|
+
kind: 'error',
|
|
689
|
+
code: 'approval-cancelled',
|
|
690
|
+
text: 'dsh-output-styles: /export --save was cancelled; nothing was written',
|
|
691
|
+
}
|
|
692
|
+
default:
|
|
693
|
+
// 'unavailable' plus any out-of-vocabulary answer fail closed (never write).
|
|
694
|
+
return {
|
|
695
|
+
kind: 'error',
|
|
696
|
+
code: 'approval-unavailable',
|
|
697
|
+
text: 'dsh-output-styles: /export --save approval is unavailable; nothing was written',
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (fs === undefined) {
|
|
701
|
+
return {
|
|
702
|
+
kind: 'error',
|
|
703
|
+
code: 'fs-unavailable',
|
|
704
|
+
text: 'dsh-output-styles: /export --save requires an fs service (compose @deepseek-ai/dsh-fs); nothing was written',
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const target = await fs.resolve(path, signal === undefined ? undefined : { signal })
|
|
708
|
+
await fs.writeText(target, content)
|
|
709
|
+
return { kind: 'written', path }
|
|
555
710
|
}
|