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.
- package/LICENSE +201 -0
- package/README.es.md +197 -0
- package/README.ja.md +197 -0
- package/README.ko.md +197 -0
- package/README.md +197 -0
- package/README.zh.md +197 -0
- package/cordis.patch.yml +40 -0
- package/docs/VERIFICATION.zh.md +93 -0
- package/lib/client.js +83 -0
- package/lib/index.js +415 -0
- package/lib/invariant-LV6hQX5s.js +442 -0
- package/lib/invariant.js +2 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +13 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/config.d.ts +73 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/index.d.ts +31 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +59 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/runtime.d.ts +144 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/style-command.d.ts +68 -0
- package/lib/types/style-command.d.ts.map +1 -0
- package/lib/types/style-library.d.ts +78 -0
- package/lib/types/style-library.d.ts.map +1 -0
- package/lib/types/types.d.ts +78 -0
- package/lib/types/types.d.ts.map +1 -0
- package/package.json +138 -0
- package/src/client/index.ts +104 -0
- package/src/client/locales.ts +14 -0
- package/src/config.ts +110 -0
- package/src/index.ts +51 -0
- package/src/invariant.ts +144 -0
- package/src/runtime.ts +439 -0
- package/src/style-command.ts +89 -0
- package/src/style-library.ts +348 -0
- package/src/types.ts +86 -0
- package/styles/concise.md +17 -0
- package/styles/explanatory.md +14 -0
- package/styles/formal.md +14 -0
- package/styles/step-by-step.md +16 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of `dsh-output-styles`: a popup picker decorating the HOST
|
|
3
|
+
* `/style` command. The picker reads the `style` session projection
|
|
4
|
+
* (`{ options, currentValue }`), which the host plugin keeps fresh, and
|
|
5
|
+
* submits the completed `/style <name>` / `/style off` line back through the
|
|
6
|
+
* command Remote — so every switch keeps the host's durable command
|
|
7
|
+
* lifecycle (`command/run`/`command/done`) and the projection stays the
|
|
8
|
+
* single displayed fact.
|
|
9
|
+
* @module dsh-output-styles/client
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ClientContext, ISessions } from '@deepseek-ai/dsh-client-runtime/client'
|
|
13
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
14
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
15
|
+
import type { CommandUiContract, SelectOption } from '@deepseek-ai/dsh-client-ui-commands/client'
|
|
16
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
17
|
+
import type { StyleSelectionView } from '../types.ts'
|
|
18
|
+
import { en, zh, type StyleKey } from './locales.ts'
|
|
19
|
+
|
|
20
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
21
|
+
interface LocaleNamespaceMap {
|
|
22
|
+
/** The style picker's copy. */
|
|
23
|
+
style: StyleKey
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Client plugin name; keep stable after publishing. */
|
|
28
|
+
export const name = 'dsh-output-styles-client'
|
|
29
|
+
|
|
30
|
+
/** Required client services: the command surface, the sessions face, the command Remote, and locale. */
|
|
31
|
+
export const inject = ['commandUi', 'locale', 'remote', 'sessions']
|
|
32
|
+
|
|
33
|
+
/** Dictionary namespace owned by this plugin. */
|
|
34
|
+
const NS = 'style'
|
|
35
|
+
|
|
36
|
+
/** The picker row that restores the project default; stable, never a style name. */
|
|
37
|
+
const OFF_ID = 'off'
|
|
38
|
+
|
|
39
|
+
/** Flatten the projection into picker rows: the off row first, then one row per style. */
|
|
40
|
+
function optionsOf(view: StyleSelectionView, t: TranslateNS<typeof NS>): SelectOption[] {
|
|
41
|
+
const rows: SelectOption[] = [{
|
|
42
|
+
id: OFF_ID,
|
|
43
|
+
label: t('option.off'),
|
|
44
|
+
detail: t('option.offDetail'),
|
|
45
|
+
active: view.currentValue === null,
|
|
46
|
+
}]
|
|
47
|
+
for (const option of view.options) {
|
|
48
|
+
rows.push({
|
|
49
|
+
id: option.value,
|
|
50
|
+
label: option.name,
|
|
51
|
+
detail: option.whenToUse !== undefined
|
|
52
|
+
? `${option.description} · ${option.whenToUse}`
|
|
53
|
+
: option.description,
|
|
54
|
+
active: view.currentValue === option.value,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
return rows
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Client plugin body: register the `style` dictionaries and decorate the
|
|
62
|
+
* host `/style` command's bare invocation with the projection-backed picker.
|
|
63
|
+
* @param ctx - client root context.
|
|
64
|
+
*/
|
|
65
|
+
export function apply(ctx: ClientContext): void {
|
|
66
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-output-styles: style dictionaries')
|
|
67
|
+
const t = ctx.locale.bind(NS)
|
|
68
|
+
|
|
69
|
+
ctx.inject(['commandUi', 'remote', 'sessions'], (scope: ClientContext) => {
|
|
70
|
+
const commandUi = scope.get('commandUi') as CommandUiContract
|
|
71
|
+
// The host graph (`dsh-session`) and the client graph (`dsh-client-runtime`)
|
|
72
|
+
// both declare a `sessions` service on Context; this package compiles
|
|
73
|
+
// against both faces, so the runtime value is the client service named
|
|
74
|
+
// 'sessions' and the host type is what the merged declaration resolves.
|
|
75
|
+
const sessions = scope.get('sessions') as unknown as ISessions
|
|
76
|
+
const remote = scope.remote
|
|
77
|
+
|
|
78
|
+
scope.effect(() => commandUi.decorate({
|
|
79
|
+
name: 'style',
|
|
80
|
+
available: () => true,
|
|
81
|
+
ui: {
|
|
82
|
+
kind: 'popupSelect',
|
|
83
|
+
options: async (session) => {
|
|
84
|
+
const binding = sessions.binding(session.sessionId)
|
|
85
|
+
const view = binding?.session.projections.faceOf('style').getSnapshot() as StyleSelectionView | undefined
|
|
86
|
+
if (view === undefined) return []
|
|
87
|
+
return optionsOf(view, t)
|
|
88
|
+
},
|
|
89
|
+
onSelect: async (option, session) => {
|
|
90
|
+
const line = option.id === OFF_ID ? '/style off' : `/style ${option.id}`
|
|
91
|
+
const result = await remote.commands.execute(session.sessionId, line)
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
throw new Error(`command.execute failed: ${result.error.code}: ${result.error.message}`)
|
|
94
|
+
}
|
|
95
|
+
if (result.value === undefined) {
|
|
96
|
+
throw new Error(`unknown or malformed command: ${line}`)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}), 'dsh-output-styles: /style picker')
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
2
|
+
export const zh = {
|
|
3
|
+
'option.off': '关闭(默认)',
|
|
4
|
+
'option.offDetail': '恢复项目默认输出风格',
|
|
5
|
+
} satisfies Record<string, string>
|
|
6
|
+
|
|
7
|
+
/** The style picker namespace key union. */
|
|
8
|
+
export type StyleKey = keyof typeof zh
|
|
9
|
+
|
|
10
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
11
|
+
export const en = {
|
|
12
|
+
'option.off': 'Off (default)',
|
|
13
|
+
'option.offDetail': 'Restore the project default output style',
|
|
14
|
+
} satisfies Record<StyleKey, string>
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializable configuration, schema, and direct-call defaults.
|
|
3
|
+
*
|
|
4
|
+
* Every tunable lives here: a deployment changes behavior through
|
|
5
|
+
* `cordis.yml`, never by editing source. The schema is validated by the
|
|
6
|
+
* harness Loader while the plugin loads; invalid configuration fails the
|
|
7
|
+
* load with an actionable error.
|
|
8
|
+
* @module dsh-output-styles/config
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import z from '@deepseek-ai/schemastery'
|
|
12
|
+
import { resolve } from 'node:path'
|
|
13
|
+
|
|
14
|
+
/** Plugin configuration supplied by the profile composition. */
|
|
15
|
+
export interface Config {
|
|
16
|
+
/**
|
|
17
|
+
* Directories holding the style library (`*.md`, and with {@link Config.compatJson}
|
|
18
|
+
* also `*.json`). Each entry resolves against the process working directory.
|
|
19
|
+
* Later directories override earlier ones on a same-named style; the bundled
|
|
20
|
+
* `styles/` directory participates as the lowest-priority entry unless
|
|
21
|
+
* {@link Config.includeBuiltins} is false. An empty list means the bundled
|
|
22
|
+
* library only (or none, with `includeBuiltins: false`). A bare string is
|
|
23
|
+
* accepted as a single-directory list.
|
|
24
|
+
*/
|
|
25
|
+
stylesDir?: string | string[]
|
|
26
|
+
/** Style-body budget in characters; longer bodies are truncated at the budget with a marker. */
|
|
27
|
+
maxStyleChars?: number
|
|
28
|
+
/**
|
|
29
|
+
* Style injected into sessions that never selected one (and no project
|
|
30
|
+
* settings default exists). The empty string (default) means new sessions
|
|
31
|
+
* get no style — the session's own selection, made through `/style`, is
|
|
32
|
+
* always what wins for a session that has one.
|
|
33
|
+
*/
|
|
34
|
+
defaultStyle?: string
|
|
35
|
+
/** Load Claude Code `outputStyles` JSON entries (`{ name, description, prompt }`) beside Markdown styles. */
|
|
36
|
+
compatJson?: boolean
|
|
37
|
+
/** Order of the injected system-prompt section (90: after the persona, before tool guidance at 100–199). */
|
|
38
|
+
sectionOrder?: number
|
|
39
|
+
/** Marker appended at the truncation point when a style body exceeds {@link Config.maxStyleChars}. */
|
|
40
|
+
truncationMarker?: string
|
|
41
|
+
/** Include the package's bundled `styles/` directory in the library. */
|
|
42
|
+
includeBuiltins?: boolean
|
|
43
|
+
/** Reload the library when a style file changes on disk (default true). */
|
|
44
|
+
watchStyles?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Configuration after defaults have been resolved. */
|
|
48
|
+
export interface ResolvedConfig {
|
|
49
|
+
/** Absolute style-library directories, lowest priority first (bundled styles first when included). */
|
|
50
|
+
stylesDirs: string[]
|
|
51
|
+
/** Style-body budget in characters; at least 1. */
|
|
52
|
+
maxStyleChars: number
|
|
53
|
+
/** Style injected into sessions that never selected one; `''` means none. */
|
|
54
|
+
defaultStyle: string
|
|
55
|
+
/** Whether Claude Code `outputStyles` JSON entries are loaded. */
|
|
56
|
+
compatJson: boolean
|
|
57
|
+
/** Order of the injected system-prompt section; a finite number. */
|
|
58
|
+
sectionOrder: number
|
|
59
|
+
/** Marker appended at the truncation point. */
|
|
60
|
+
truncationMarker: string
|
|
61
|
+
/** Whether the bundled `styles/` directory participates. */
|
|
62
|
+
includeBuiltins: boolean
|
|
63
|
+
/** Whether the library reloads on style-file changes. */
|
|
64
|
+
watchStyles: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Loader-visible configuration schema and defaults. */
|
|
68
|
+
export const Config: z<Config> = z.object({
|
|
69
|
+
stylesDir: z.union([z.string(), z.array(z.string())]).default([]),
|
|
70
|
+
maxStyleChars: z.number().min(1).default(4000),
|
|
71
|
+
defaultStyle: z.string().default(''),
|
|
72
|
+
compatJson: z.boolean().default(true),
|
|
73
|
+
sectionOrder: z.number().default(90),
|
|
74
|
+
truncationMarker: z.string().default('\n\n[style truncated]'),
|
|
75
|
+
includeBuiltins: z.boolean().default(true),
|
|
76
|
+
watchStyles: z.boolean().default(true),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the same defaults for direct callers that bypass the Cordis Loader,
|
|
81
|
+
* and fail loud on values the schema cannot express (a non-finite section
|
|
82
|
+
* order).
|
|
83
|
+
* @param config - Partial serialized configuration.
|
|
84
|
+
* @param defaultStylesDir - Absolute bundled directory used when built-ins are included.
|
|
85
|
+
* @returns Configuration with every default applied.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveConfig(config: Config, defaultStylesDir: string): ResolvedConfig {
|
|
88
|
+
const maxStyleChars = config.maxStyleChars ?? 4000
|
|
89
|
+
if (!Number.isFinite(maxStyleChars) || maxStyleChars < 1) {
|
|
90
|
+
throw new Error(`dsh-output-styles: maxStyleChars must be a finite number ≥ 1, got ${String(config.maxStyleChars)}`)
|
|
91
|
+
}
|
|
92
|
+
const sectionOrder = config.sectionOrder ?? 90
|
|
93
|
+
if (!Number.isFinite(sectionOrder)) {
|
|
94
|
+
throw new Error(`dsh-output-styles: sectionOrder must be a finite number, got ${String(config.sectionOrder)}`)
|
|
95
|
+
}
|
|
96
|
+
const includeBuiltins = config.includeBuiltins ?? true
|
|
97
|
+
const rawDirs = Array.isArray(config.stylesDir) ? config.stylesDir : config.stylesDir === undefined || config.stylesDir === '' ? [] : [config.stylesDir]
|
|
98
|
+
const customDirs = rawDirs.map(dir => resolve(dir))
|
|
99
|
+
const stylesDirs = includeBuiltins ? [defaultStylesDir, ...customDirs] : customDirs
|
|
100
|
+
return {
|
|
101
|
+
stylesDirs,
|
|
102
|
+
maxStyleChars,
|
|
103
|
+
defaultStyle: config.defaultStyle ?? '',
|
|
104
|
+
compatJson: config.compatJson ?? true,
|
|
105
|
+
sectionOrder,
|
|
106
|
+
truncationMarker: config.truncationMarker ?? '\n\n[style truncated]',
|
|
107
|
+
includeBuiltins,
|
|
108
|
+
watchStyles: config.watchStyles ?? true,
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-output-styles`: Claude Code `outputStyles`-equivalent runtime output
|
|
3
|
+
* styles for DeepSeek Harness. The plugin registers a model-visible system
|
|
4
|
+
* prompt section that injects the current session's style body, a `/style`
|
|
5
|
+
* slash command that switches it, per-session persistence over the
|
|
6
|
+
* `output_style` storage domain, and the `style` session projection.
|
|
7
|
+
* @module dsh-output-styles
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Cordis plugin name; keep this stable after publishing. */
|
|
11
|
+
export const name = 'dsh-output-styles'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Services that must exist before the plugin applies: the prompt-assembly
|
|
15
|
+
* registry for the injected section, and the storage domain facility for
|
|
16
|
+
* per-session persistence. A composition without a routed kv backend keeps
|
|
17
|
+
* the plugin pending until the storage rows appear (Cordis dependency
|
|
18
|
+
* semantics), instead of racing a parallel mount.
|
|
19
|
+
*/
|
|
20
|
+
export const inject = ['systemPrompt', 'storageDomain']
|
|
21
|
+
|
|
22
|
+
export { Config, resolveConfig } from './config.ts'
|
|
23
|
+
export type { ResolvedConfig } from './config.ts'
|
|
24
|
+
export { apply, DEFAULT_STYLES_DIR, OutputStyleRuntime, STYLE_SECTION_NAME } from './runtime.ts'
|
|
25
|
+
export {
|
|
26
|
+
applyStyleEvent,
|
|
27
|
+
EMPTY_STYLE_STATE,
|
|
28
|
+
parseStyleInput,
|
|
29
|
+
STYLE_COMMAND,
|
|
30
|
+
} from './style-command.ts'
|
|
31
|
+
export type { StyleFoldState, StyleInput } from './style-command.ts'
|
|
32
|
+
export {
|
|
33
|
+
isValidStyleName,
|
|
34
|
+
loadStyleLibrary,
|
|
35
|
+
STYLE_NAME_RE,
|
|
36
|
+
truncateStyle,
|
|
37
|
+
} from './style-library.ts'
|
|
38
|
+
export type { OutputStyle } from './style-library.ts'
|
|
39
|
+
export {
|
|
40
|
+
OFF,
|
|
41
|
+
OUTPUT_STYLE_DOMAIN,
|
|
42
|
+
STYLE_SOURCE,
|
|
43
|
+
styleSelectionSchema,
|
|
44
|
+
styleSelectionViewSchema,
|
|
45
|
+
} from './types.ts'
|
|
46
|
+
export type { StyleOption, StyleSelection, StyleSelectionView } from './types.ts'
|
|
47
|
+
export { installInvariant, PACKAGE_NAME } from './invariant.ts'
|
|
48
|
+
export type { InvariantFacts, InvariantInstaller, InvariantRegistry } from './invariant.ts'
|
|
49
|
+
// Type-only re-export: keeps the `style` SessionProjectionMap merge edge in
|
|
50
|
+
// the emitted index.d.ts, so consumers receive the projection key's type.
|
|
51
|
+
export type * from './types.ts'
|
package/src/invariant.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-output-styles`.
|
|
3
|
+
*
|
|
4
|
+
* Two post-commit diagnostic checks over the events this plugin's write path
|
|
5
|
+
* produces (both target events are committed before dispatch, so a violation
|
|
6
|
+
* is reported — via the host registry's `fail` — rather than vetoed):
|
|
7
|
+
*
|
|
8
|
+
* 1. Every `output_style`/`selection` durable write carries this plugin's own
|
|
9
|
+
* source marker and a legal style name; when the library is known,
|
|
10
|
+
* the name must be a library member.
|
|
11
|
+
* 2. A successful `/style <name>` command has a matching selection record on
|
|
12
|
+
* its session by the time its `command/done` settles, and `/style off`
|
|
13
|
+
* leaves no record. The standalone companion (no library/domain handle)
|
|
14
|
+
* skips this check.
|
|
15
|
+
*
|
|
16
|
+
* The main plugin registers a facts-bearing installer from its own context;
|
|
17
|
+
* the `./invariant` export is the standalone companion usable through a
|
|
18
|
+
* separate profile row.
|
|
19
|
+
* @module dsh-output-styles/invariant
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
23
|
+
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
24
|
+
import type {} from '@deepseek-ai/dsh-commands'
|
|
25
|
+
import type { DomainChanged } from '@deepseek-ai/dsh-storage-domain'
|
|
26
|
+
import { parseStyleInput, STYLE_COMMAND } from './style-command.ts'
|
|
27
|
+
import { isValidStyleName } from './style-library.ts'
|
|
28
|
+
import { OFF, STYLE_SOURCE } from './types.ts'
|
|
29
|
+
|
|
30
|
+
/** Full npm package name owning the reported failures. */
|
|
31
|
+
export const PACKAGE_NAME = 'dsh-output-styles'
|
|
32
|
+
|
|
33
|
+
/** A package-attributed invariant failure reported by the host registry. */
|
|
34
|
+
export type InvariantFailure = (message: string) => never
|
|
35
|
+
|
|
36
|
+
/** Facts the installer needs beyond the event stream. */
|
|
37
|
+
export interface InvariantFacts {
|
|
38
|
+
/** Library style names, or undefined when the companion has no library handle. */
|
|
39
|
+
knownStyles(): ReadonlySet<string> | undefined
|
|
40
|
+
/** One session's durable selection record; absent disables the pairing check. */
|
|
41
|
+
selectionFor?(sessionId: SessionId): { style: string } | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Installer callback accepted by the host's invariant registry. */
|
|
45
|
+
export type InvariantInstaller = (ctx: Context, fail: InvariantFailure) => void | Promise<void>
|
|
46
|
+
|
|
47
|
+
/** Minimal runtime contract used by the companion without a source checkout. */
|
|
48
|
+
export interface InvariantRegistry {
|
|
49
|
+
register(packageName: string, installer: InvariantInstaller): () => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Cordis companion plugin name. */
|
|
53
|
+
export const name = 'dsh-output-styles-invariant'
|
|
54
|
+
/** Service required before the companion can reserve package ownership. */
|
|
55
|
+
export const inject = ['invariants']
|
|
56
|
+
|
|
57
|
+
/** Facts for the standalone companion: envelope checks only, no library/domain handle. */
|
|
58
|
+
const COMPANION_FACTS: InvariantFacts = {
|
|
59
|
+
knownStyles: () => undefined,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One `/style` command run awaiting its `command/done`. */
|
|
63
|
+
interface PendingSwitch {
|
|
64
|
+
session: Session
|
|
65
|
+
expected: { name: string } | { off: true }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the installer over a facts source. The standalone companion and the
|
|
70
|
+
* main plugin share this body; only the facts differ.
|
|
71
|
+
* @param facts - library and domain access for the checks.
|
|
72
|
+
* @returns the installer the host registry activates in its child context.
|
|
73
|
+
*/
|
|
74
|
+
export function installInvariant(facts: InvariantFacts): InvariantInstaller {
|
|
75
|
+
return (ctx, fail) => {
|
|
76
|
+
const pending = new Map<string, PendingSwitch>()
|
|
77
|
+
|
|
78
|
+
ctx.on('domain/changed', (change: DomainChanged) => {
|
|
79
|
+
if (change.domain !== 'output_style' || change.table !== 'selection' || change.operation !== 'put') return
|
|
80
|
+
const value = change.value as { style?: unknown; source?: unknown }
|
|
81
|
+
if (typeof value.style !== 'string' || !isValidStyleName(value.style)) {
|
|
82
|
+
fail(`selection record names invalid style ${JSON.stringify(value.style)}`)
|
|
83
|
+
}
|
|
84
|
+
if (JSON.stringify(value.source) !== JSON.stringify(STYLE_SOURCE)) {
|
|
85
|
+
fail(`selection record source is ${JSON.stringify(value.source)}; expected this plugin's own marker`)
|
|
86
|
+
}
|
|
87
|
+
const known = facts.knownStyles()
|
|
88
|
+
if (known !== undefined && typeof value.style === 'string' && !known.has(value.style)) {
|
|
89
|
+
fail(`selection record names style "${value.style}" that is not in the style library`)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
ctx.on('session/event', (session: Session, event: SessionEvent) => {
|
|
94
|
+
if (event.type === 'command/run'
|
|
95
|
+
&& event.data.name === STYLE_COMMAND
|
|
96
|
+
&& event.data.args !== undefined) {
|
|
97
|
+
const input = parseStyleInput(event.data.args)
|
|
98
|
+
if (input.kind === 'off') {
|
|
99
|
+
pending.set(String(event.data.commandId), { session, expected: { off: true } })
|
|
100
|
+
} else if (input.kind === 'switch') {
|
|
101
|
+
pending.set(String(event.data.commandId), { session, expected: { name: input.name } })
|
|
102
|
+
}
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (event.type !== 'command/done') return
|
|
106
|
+
const entry = pending.get(String(event.data.commandId))
|
|
107
|
+
if (entry === undefined) return
|
|
108
|
+
pending.delete(String(event.data.commandId))
|
|
109
|
+
if (event.data.kind !== 'success' || facts.selectionFor === undefined) return
|
|
110
|
+
const record = facts.selectionFor(entry.session.id)
|
|
111
|
+
if ('off' in entry.expected) {
|
|
112
|
+
if (record !== undefined) {
|
|
113
|
+
fail(`/style ${OFF} settled on session "${entry.session.id}" but its selection record still exists`)
|
|
114
|
+
}
|
|
115
|
+
} else if (record === undefined || record.style !== entry.expected.name) {
|
|
116
|
+
fail(`/style ${entry.expected.name} settled on session "${entry.session.id}" without a matching selection record`)
|
|
117
|
+
}
|
|
118
|
+
}, { global: true })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the host registry through Cordis's named service lookup. Keeping
|
|
124
|
+
* this narrow local contract lets the companion build without host source
|
|
125
|
+
* files; a composed DSH profile still supplies the real `invariants` service.
|
|
126
|
+
* @param ctx - Cordis context carrying the host service.
|
|
127
|
+
* @returns the host invariant registry.
|
|
128
|
+
* @throws {Error} when the companion is loaded without its host service.
|
|
129
|
+
*/
|
|
130
|
+
function getInvariantRegistry(ctx: Context): InvariantRegistry {
|
|
131
|
+
const registry = ctx.get('invariants') as InvariantRegistry | undefined
|
|
132
|
+
if (registry === undefined) {
|
|
133
|
+
throw new Error(`invariant companion requires the "invariants" service for ${PACKAGE_NAME}`)
|
|
134
|
+
}
|
|
135
|
+
return registry
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register the standalone companion with envelope-only facts.
|
|
140
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
141
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
142
|
+
*/
|
|
143
|
+
export const apply = (ctx: Context): Promise<() => void> =>
|
|
144
|
+
Promise.resolve(getInvariantRegistry(ctx).register(PACKAGE_NAME, installInvariant(COMPANION_FACTS)))
|