@jcy2387/dsh-conversation-ui 0.1.0-rc.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +123 -0
  3. package/README.md +123 -0
  4. package/conversation-ui-off.yml +3 -0
  5. package/cordis.patch.yml +10 -0
  6. package/lib/client.js +3672 -0
  7. package/lib/client.js.map +1 -0
  8. package/lib/index.js +302 -0
  9. package/package.json +182 -0
  10. package/src/boot-config.ts +28 -0
  11. package/src/client/AnimatedDisclosure.tsx +89 -0
  12. package/src/client/ConversationCard.module.css +253 -0
  13. package/src/client/ConversationCard.tsx +122 -0
  14. package/src/client/DeliverablesCard.module.css +187 -0
  15. package/src/client/DeliverablesCard.tsx +142 -0
  16. package/src/client/FollowHost.tsx +25 -0
  17. package/src/client/ToolActivityGroup.tsx +341 -0
  18. package/src/client/TurnPreludeUserNodeView.tsx +258 -0
  19. package/src/client/TurnProcessMember.tsx +92 -0
  20. package/src/client/TypewriterAssistantNodeView.module.css +1260 -0
  21. package/src/client/TypewriterAssistantNodeView.tsx +390 -0
  22. package/src/client/TypewriterToolNodeView.tsx +916 -0
  23. package/src/client/conversation-ui-card-controller.ts +162 -0
  24. package/src/client/conversation-ui-settings-api.ts +65 -0
  25. package/src/client/deliverables.ts +263 -0
  26. package/src/client/index.ts +242 -0
  27. package/src/client/locales.ts +74 -0
  28. package/src/client/teleprompterGlide.ts +590 -0
  29. package/src/client/turnElapsed.ts +27 -0
  30. package/src/client/turnProcessFold.ts +224 -0
  31. package/src/client/useConversationContent.ts +471 -0
  32. package/src/client/useFpsGuard.ts +84 -0
  33. package/src/config.ts +44 -0
  34. package/src/css-modules.d.ts +9 -0
  35. package/src/index.ts +2 -0
  36. package/src/package-meta.ts +39 -0
  37. package/src/plugin.ts +155 -0
  38. package/src/profile-installation.ts +121 -0
  39. package/src/settings-api.ts +33 -0
  40. package/src/settings.ts +30 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Performance guard for the streaming reveal.
3
+ *
4
+ * Feeds an EMA-smoothed frame-rate monitor from a rAF loop while streaming
5
+ * and tracks whether the reply is on-screen. The returned `shouldHoldBack`
6
+ * predicate is true only while the frame rate is below the threshold AND the
7
+ * reply is offscreen — exactly the spec's "skip offscreen DOM updates when
8
+ * FPS < 30" rule. The smoother consumes the predicate as its commit veto.
9
+ */
10
+
11
+ import { useCallback, useEffect, useRef } from 'react'
12
+
13
+ const FPS_THRESHOLD = 30
14
+ const FPS_ALPHA = 0.12
15
+ const RECOVER_FRAMES = 6
16
+ const MAX_FRAME_MS = 100
17
+
18
+ interface MutableFps {
19
+ emaMs: number
20
+ lastMs: number
21
+ healthyRun: number
22
+ degraded: boolean
23
+ }
24
+
25
+ export function useFpsGuard(active: boolean): {
26
+ ref: (element: HTMLElement | null) => void
27
+ shouldHoldBack: () => boolean
28
+ } {
29
+ const fpsRef = useRef<MutableFps>({ emaMs: 0, lastMs: 0, healthyRun: 0, degraded: false })
30
+ const visibleRef = useRef(true)
31
+ const elementRef = useRef<HTMLElement | null>(null)
32
+
33
+ useEffect(() => {
34
+ if (!active) return
35
+ let rafId = 0
36
+ const frame = (now: number) => {
37
+ rafId = requestAnimationFrame(frame)
38
+ const fps = fpsRef.current
39
+ if (fps.lastMs === 0) {
40
+ fps.lastMs = now
41
+ return
42
+ }
43
+ const delta = Math.min(MAX_FRAME_MS, Math.max(1, now - fps.lastMs))
44
+ fps.lastMs = now
45
+ fps.emaMs = fps.emaMs === 0 ? delta : fps.emaMs + FPS_ALPHA * (delta - fps.emaMs)
46
+ const currentFps = 1000 / fps.emaMs
47
+ if (currentFps < FPS_THRESHOLD) {
48
+ fps.healthyRun = 0
49
+ fps.degraded = true
50
+ } else if (fps.degraded) {
51
+ fps.healthyRun += 1
52
+ if (fps.healthyRun >= RECOVER_FRAMES) fps.degraded = false
53
+ }
54
+ }
55
+ rafId = requestAnimationFrame(frame)
56
+ return () => {
57
+ cancelAnimationFrame(rafId)
58
+ fpsRef.current = { emaMs: 0, lastMs: 0, healthyRun: 0, degraded: false }
59
+ }
60
+ }, [active])
61
+
62
+ const ref = useCallback((element: HTMLElement | null) => {
63
+ elementRef.current = element
64
+ }, [])
65
+
66
+ useEffect(() => {
67
+ const element = elementRef.current
68
+ if (element === null || typeof IntersectionObserver === 'undefined') return
69
+ const observer = new IntersectionObserver(
70
+ (entries) => {
71
+ for (const entry of entries) visibleRef.current = entry.isIntersecting
72
+ },
73
+ { rootMargin: '120px 0px' },
74
+ )
75
+ observer.observe(element)
76
+ return () => observer.disconnect()
77
+ })
78
+
79
+ const shouldHoldBack = useCallback(() => {
80
+ return active && fpsRef.current.degraded && !visibleRef.current
81
+ }, [active])
82
+
83
+ return { ref, shouldHoldBack }
84
+ }
package/src/config.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared plugin configuration contract. The Host half defines the
3
+ * Schemastery schema over this shape (defaults live here so both halves stay
4
+ * symmetric); the client half falls back to the same defaults when the Host
5
+ * boot-config bridge is absent (client-only composition).
6
+ */
7
+
8
+ import type { ConversationSmoothingPreset } from './client/useConversationContent.ts'
9
+
10
+ /** Streaming render direction of the assistant node view. */
11
+ export type ConversationMode = 'typewriter' | 'teleprompter'
12
+
13
+ /** Plugin configuration validated by the Host schema and bridged to the browser half. */
14
+ export interface ConversationConfig {
15
+ /** Render direction: horizontal per-character reveal or uniform upward glide. */
16
+ readonly mode: ConversationMode
17
+ /** Smoothing preset for the reveal cadence. */
18
+ readonly preset: ConversationSmoothingPreset
19
+ /** Fixed grapheme reveal rate used by `typewriter` mode. */
20
+ readonly revealCharsPerSec: number
21
+ /** Minimum visual follow speed while the transcript is pinned. */
22
+ readonly scrollSpeedPxPerSec: number
23
+ /**
24
+ * Follow velocity ceiling in CSS pixels per second, so a huge first lag
25
+ * does not teleport. Ordinary wraps stay well below this.
26
+ */
27
+ readonly maxScrollSpeedPxPerSec: number
28
+ }
29
+
30
+ /** Defaults shared by the Host schema and the client-side fallback. */
31
+ export const DEFAULT_CONVERSATION_CONFIG: ConversationConfig = {
32
+ mode: 'teleprompter',
33
+ preset: 'balanced',
34
+ revealCharsPerSec: 80,
35
+ scrollSpeedPxPerSec: 48,
36
+ maxScrollSpeedPxPerSec: 1000,
37
+ }
38
+
39
+ /**
40
+ * Window global the Host writes into the served index HTML. The browser boot
41
+ * graph carries no per-entry config, so this inline script is the only
42
+ * Host-to-client configuration channel for a composed web plugin.
43
+ */
44
+ export const CONVERSATION_BOOT_GLOBAL = '__DSH_CONVERSATION_UI_CONFIG__'
@@ -0,0 +1,9 @@
1
+ declare module '*.module.css' {
2
+ const classes: Record<string, string>
3
+ export default classes
4
+ }
5
+
6
+ declare module '*.css' {
7
+ const stylesheet: string
8
+ export default stylesheet
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Host entry. The browser implementation is exported from `./client`. */
2
+ export { apply, name, Config } from './plugin.ts'
@@ -0,0 +1,39 @@
1
+ /** Host-only package metadata. The package manifest remains the source of truth. */
2
+
3
+ import { readFileSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ interface PackageManifest {
8
+ name?: unknown
9
+ version?: unknown
10
+ }
11
+
12
+ function packageManifestPath(): string {
13
+ // Production loads this Node entry from its installed file. Vite's test
14
+ // transform instead supplies an http URL, where the project manifest is the
15
+ // closest equivalent source of truth.
16
+ try {
17
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
18
+ } catch {
19
+ // Fall through to the project root when a browser-oriented test transform
20
+ // has replaced import.meta.url with a non-file URL.
21
+ }
22
+ return join(process.cwd(), 'package.json')
23
+ }
24
+
25
+ const manifest = JSON.parse(readFileSync(packageManifestPath(), 'utf8')) as PackageManifest
26
+
27
+ function readRequiredString(field: keyof PackageManifest): string {
28
+ const value = manifest[field]
29
+ if (typeof value !== 'string' || value.length === 0) {
30
+ throw new Error(`dsh-conversation-ui: package.json must contain a non-empty ${field}`)
31
+ }
32
+ return value
33
+ }
34
+
35
+ /** npm package name, read from this plugin's manifest. */
36
+ export const CONVERSATION_PACKAGE_NAME = readRequiredString('name')
37
+
38
+ /** Version of the code currently loaded by the Host, read from its manifest. */
39
+ export const CONVERSATION_PACKAGE_VERSION = readRequiredString('version')
package/src/plugin.ts ADDED
@@ -0,0 +1,155 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type {} from '@deepseek-ai/dsh-host-webserver'
3
+ import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
4
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
5
+ import Schema from '@deepseek-ai/schemastery'
6
+ import { DEFAULT_CONVERSATION_CONFIG, type ConversationConfig } from './config.ts'
7
+ import { injectConversationConfig } from './boot-config.ts'
8
+ import { CONVERSATION_PACKAGE_NAME, CONVERSATION_PACKAGE_VERSION } from './package-meta.ts'
9
+ import { inspectProfileInstallation, updateNpmProfilePackage } from './profile-installation.ts'
10
+ import { CONVERSATION_SETTINGS_RPC, CONVERSATION_SETTINGS_RPC_CHANNEL, type ConversationSettingsView } from './settings-api.ts'
11
+ import { DEFAULT_CONVERSATION_SETTINGS, CONVERSATION_SETTINGS_NS, type ConversationSettings } from './settings.ts'
12
+
13
+ /** Display name shown by the Host loader while the plugin is mounted. */
14
+ export const name = 'dsh-conversation-ui'
15
+
16
+ /**
17
+ * Plugin configuration accepted from the overlay's `config` section. Cordis
18
+ * validates the value against this schema at load and fills omitted fields
19
+ * from the shared defaults, so an invalid value fails the load loudly.
20
+ */
21
+ export interface Config extends ConversationConfig {}
22
+
23
+ export const Config: Schema<Config> = Schema.object({
24
+ mode: Schema.union(['typewriter', 'teleprompter'] as const).default(DEFAULT_CONVERSATION_CONFIG.mode),
25
+ preset: Schema.union(['realtime', 'balanced', 'silky'] as const).default(DEFAULT_CONVERSATION_CONFIG.preset),
26
+ revealCharsPerSec: Schema.number()
27
+ .min(5)
28
+ .max(200)
29
+ .default(DEFAULT_CONVERSATION_CONFIG.revealCharsPerSec),
30
+ scrollSpeedPxPerSec: Schema.number()
31
+ .min(1)
32
+ .max(200)
33
+ .default(DEFAULT_CONVERSATION_CONFIG.scrollSpeedPxPerSec),
34
+ maxScrollSpeedPxPerSec: Schema.number()
35
+ .min(1)
36
+ .max(2000)
37
+ .default(DEFAULT_CONVERSATION_CONFIG.maxScrollSpeedPxPerSec),
38
+ })
39
+
40
+ /**
41
+ * Schema of the user-owned settings section. The Host keeps it in the durable
42
+ * settings provider while the browser edits it through the plugin RPC below.
43
+ */
44
+ export const ConversationSettingsSchema: Schema<ConversationSettings> = Schema.object({
45
+ thinkAutoExpand: Schema.boolean().default(DEFAULT_CONVERSATION_SETTINGS.thinkAutoExpand),
46
+ })
47
+
48
+ /**
49
+ * Host half: log the resolved configuration and bridge it to the browser
50
+ * half. The web boot graph carries no per-entry config, so the validated
51
+ * value is injected into every served index response as a boot global the
52
+ * client entry reads at apply time.
53
+ * @param ctx - Host context carrying the web server service when composed.
54
+ * @param config - Schema-validated configuration with defaults filled.
55
+ */
56
+ export function apply(ctx: Context, config: Config): void {
57
+ console.log(
58
+ `[dsh-conversation-ui] plugin loaded! mode=${config.mode} preset=${config.preset} `
59
+ + `seed=${config.revealCharsPerSec}cps scroll=${config.scrollSpeedPxPerSec}px/s `
60
+ + `maxScroll=${config.maxScrollSpeedPxPerSec}px/s`,
61
+ )
62
+ ctx.inject(['webServer'], (httpCtx) => {
63
+ httpCtx.effect(
64
+ () => httpCtx.webServer.tapIndex(html => injectConversationConfig(html, config)),
65
+ 'dsh-conversation-ui: boot config bridge',
66
+ )
67
+ })
68
+ // The core settings RPC deliberately filters third-party namespaces. Keep
69
+ // the durable provider as the authority, but expose this one schema through
70
+ // the plugin's own loopback-only connection channel instead.
71
+ ctx.inject(['settings'], (settingsCtx) => {
72
+ const scope = settingsCtx.settings.register(
73
+ settingsNamespace(CONVERSATION_SETTINGS_NS),
74
+ ConversationSettingsSchema,
75
+ { applies: 'live' },
76
+ )
77
+ settingsCtx.inject(['connection'], (connectionCtx) => {
78
+ let upgrade: Promise<void> | undefined
79
+
80
+ const view = (): ConversationSettingsView => {
81
+ const installation = inspectProfileInstallation(connectionCtx.baseUrl, CONVERSATION_PACKAGE_NAME)
82
+ return {
83
+ version: CONVERSATION_PACKAGE_VERSION,
84
+ installation: installation.kind,
85
+ writable: connectionCtx.settings.writable,
86
+ thinkAutoExpand: scope.get().thinkAutoExpand,
87
+ canUpgrade: installation.kind === 'npm',
88
+ }
89
+ }
90
+
91
+ const handle: ConnectionRpcHandler = async (endpoint, payload) => {
92
+ if (endpoint === CONVERSATION_SETTINGS_RPC.read) return { ok: true, value: view() }
93
+ if (endpoint === CONVERSATION_SETTINGS_RPC.write) {
94
+ if (typeof payload !== 'object' || payload === null
95
+ || typeof (payload as { thinkAutoExpand?: unknown }).thinkAutoExpand !== 'boolean') {
96
+ return {
97
+ ok: false,
98
+ error: {
99
+ code: 'settings-rejected',
100
+ message: 'thinkAutoExpand must be a boolean',
101
+ details: { ns: CONVERSATION_SETTINGS_NS },
102
+ },
103
+ }
104
+ }
105
+ if (!connectionCtx.settings.writable) {
106
+ return {
107
+ ok: false,
108
+ error: {
109
+ code: 'settings-rejected',
110
+ message: 'conversation-ui settings are read-only',
111
+ details: { ns: CONVERSATION_SETTINGS_NS },
112
+ },
113
+ }
114
+ }
115
+ try {
116
+ await scope.update({ thinkAutoExpand: (payload as { thinkAutoExpand: boolean }).thinkAutoExpand })
117
+ } catch {
118
+ return {
119
+ ok: false,
120
+ error: {
121
+ code: 'settings-rejected',
122
+ message: 'conversation-ui settings update failed',
123
+ details: { ns: CONVERSATION_SETTINGS_NS },
124
+ },
125
+ }
126
+ }
127
+ return { ok: true, value: view() }
128
+ }
129
+ if (endpoint === CONVERSATION_SETTINGS_RPC.upgrade) {
130
+ const installation = inspectProfileInstallation(connectionCtx.baseUrl, CONVERSATION_PACKAGE_NAME)
131
+ if (installation.kind !== 'npm') {
132
+ return { ok: false, error: { code: 'internal', message: 'conversation-ui is not an npm profile dependency', details: {} } }
133
+ }
134
+ if (upgrade !== undefined) {
135
+ return { ok: false, error: { code: 'internal', message: 'conversation-ui update is already running', details: {} } }
136
+ }
137
+ upgrade = updateNpmProfilePackage(installation.profileDir, CONVERSATION_PACKAGE_NAME)
138
+ try {
139
+ await upgrade
140
+ } catch {
141
+ return { ok: false, error: { code: 'internal', message: 'conversation-ui update failed', details: {} } }
142
+ } finally {
143
+ upgrade = undefined
144
+ }
145
+ return { ok: true, value: { restartRequired: true } }
146
+ }
147
+ return { ok: false, error: { code: 'internal', message: `unknown conversation-ui endpoint ${JSON.stringify(endpoint)}`, details: {} } }
148
+ }
149
+ connectionCtx.effect(
150
+ () => connectionCtx.connection.rpc.handle(CONVERSATION_SETTINGS_RPC_CHANNEL, handle, { authority: 'loopback' }),
151
+ 'dsh-conversation-ui: settings RPC',
152
+ )
153
+ })
154
+ })
155
+ }
@@ -0,0 +1,121 @@
1
+ /** Host-only profile source inspection and fixed npm update runner. */
2
+
3
+ import { spawn } from 'node:child_process'
4
+ import { readFileSync } from 'node:fs'
5
+ import { basename, isAbsolute, join } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ /** A profile installation that can be updated through a fixed pnpm invocation. */
9
+ export interface NpmProfileInstallation {
10
+ kind: 'npm'
11
+ profileDir: string
12
+ profileName: string
13
+ }
14
+
15
+ /** A local source is deliberately not eligible for one-click npm updates. */
16
+ export interface DevelopmentProfileInstallation {
17
+ kind: 'development'
18
+ }
19
+
20
+ /** The active context is not a recognized profile dependency. */
21
+ export interface UnmanagedProfileInstallation {
22
+ kind: 'unmanaged'
23
+ }
24
+
25
+ /** Installation source visible to the plugin's Host RPC. */
26
+ export type ProfileInstallation = NpmProfileInstallation | DevelopmentProfileInstallation | UnmanagedProfileInstallation
27
+
28
+ interface ProfileManifest {
29
+ dependencies?: unknown
30
+ dsh?: unknown
31
+ }
32
+
33
+ function record(value: unknown): Record<string, unknown> | undefined {
34
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
35
+ ? value as Record<string, unknown>
36
+ : undefined
37
+ }
38
+
39
+ function profileDirectory(baseUrl: string | undefined): string | undefined {
40
+ if (baseUrl === undefined) return undefined
41
+ try {
42
+ const url = new URL(baseUrl)
43
+ return url.protocol === 'file:' ? fileURLToPath(url) : undefined
44
+ } catch {
45
+ return undefined
46
+ }
47
+ }
48
+
49
+ function hasBundle(manifest: ProfileManifest, packageName: string): boolean {
50
+ const dsh = record(manifest.dsh)
51
+ const profile = record(dsh?.profile)
52
+ const bundles = profile?.bundles
53
+ return Array.isArray(bundles) && bundles.includes(packageName)
54
+ }
55
+
56
+ function isLocalSpecifier(specifier: string): boolean {
57
+ return specifier.startsWith('link:')
58
+ || specifier.startsWith('file:')
59
+ || specifier.startsWith('.')
60
+ || isAbsolute(specifier)
61
+ || /^[A-Za-z]:[\\/]/.test(specifier)
62
+ }
63
+
64
+ function isRegistrySpecifier(specifier: string): boolean {
65
+ // A registry range/tag contains neither a scheme nor a path. Treat every
66
+ // package-manager extension (catalog:, patch:, portal:, workspace:, etc.)
67
+ // as unconfirmed rather than guessing that an update may replace it.
68
+ return specifier.length > 0 && !specifier.includes(':') && !/[\\/]/.test(specifier)
69
+ }
70
+
71
+ function isNpmSpecifier(specifier: string, packageName: string): boolean {
72
+ if (!specifier.startsWith('npm:')) return isRegistrySpecifier(specifier)
73
+ const aliased = specifier.slice('npm:'.length)
74
+ if (aliased === packageName) return true
75
+ const prefix = `${packageName}@`
76
+ return aliased.startsWith(prefix) && isRegistrySpecifier(aliased.slice(prefix.length))
77
+ }
78
+
79
+ /**
80
+ * Read only the profile manifest anchored by the current Cordis config tree.
81
+ * A malformed or unrelated tree never receives an update affordance.
82
+ */
83
+ export function inspectProfileInstallation(
84
+ baseUrl: string | undefined,
85
+ packageName: string,
86
+ ): ProfileInstallation {
87
+ const profileDir = profileDirectory(baseUrl)
88
+ if (profileDir === undefined) return { kind: 'unmanaged' }
89
+ let manifest: ProfileManifest
90
+ try {
91
+ manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as ProfileManifest
92
+ } catch {
93
+ return { kind: 'unmanaged' }
94
+ }
95
+ const dependencies = record(manifest.dependencies)
96
+ const specifier = dependencies?.[packageName]
97
+ if (typeof specifier !== 'string' || !hasBundle(manifest, packageName)) {
98
+ return { kind: 'unmanaged' }
99
+ }
100
+ if (isLocalSpecifier(specifier)) return { kind: 'development' }
101
+ if (!isNpmSpecifier(specifier, packageName)) return { kind: 'unmanaged' }
102
+ return { kind: 'npm', profileDir, profileName: basename(profileDir) }
103
+ }
104
+
105
+ /** Run the same fixed package update operation that a profile user would invoke. */
106
+ export function updateNpmProfilePackage(profileDir: string, packageName: string): Promise<void> {
107
+ const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
108
+ return new Promise((resolve, reject) => {
109
+ const child = spawn(command, ['update', packageName], {
110
+ cwd: profileDir,
111
+ stdio: 'ignore',
112
+ shell: false,
113
+ windowsHide: true,
114
+ })
115
+ child.once('error', reject)
116
+ child.once('exit', (code, signal) => {
117
+ if (code === 0) resolve()
118
+ else reject(new Error(`dsh-conversation-ui: pnpm update failed (${signal ?? String(code)})`))
119
+ })
120
+ })
121
+ }
@@ -0,0 +1,33 @@
1
+ /** Shared wire vocabulary for the plugin-owned settings RPC channel. */
2
+
3
+ /** Dedicated, loopback-only RPC channel registered by the Host half. */
4
+ export const CONVERSATION_SETTINGS_RPC_CHANNEL = '/conversation-ui'
5
+
6
+ /** Endpoints accepted by {@link CONVERSATION_SETTINGS_RPC_CHANNEL}. */
7
+ export const CONVERSATION_SETTINGS_RPC = {
8
+ read: 'settings.read',
9
+ write: 'settings.write',
10
+ upgrade: 'plugin.upgrade',
11
+ } as const
12
+
13
+ /** How the active profile supplied this plugin. */
14
+ export type ConversationInstallationKind = 'npm' | 'development' | 'unmanaged'
15
+
16
+ /** Redacted, plugin-owned configuration state returned to the browser. */
17
+ export interface ConversationSettingsView {
18
+ /** Version of the package currently running in the Host process. */
19
+ version: string
20
+ /** Source class of this package in the active profile. */
21
+ installation: ConversationInstallationKind
22
+ /** Whether the Host settings provider permits a write. */
23
+ writable: boolean
24
+ /** Current resolved preference. */
25
+ thinkAutoExpand: boolean
26
+ /** Whether a fixed npm update command is safe to offer. */
27
+ canUpgrade: boolean
28
+ }
29
+
30
+ /** Successful package update acknowledgement; loading the new code needs a restart. */
31
+ export interface ConversationUpgradeView {
32
+ restartRequired: true
33
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * User-owned settings for the conversation-ui plugin, exposed to the Host
3
+ * settings service and edited from the Web Settings "plugin configuration"
4
+ * page. This is the runtime-editable complement to {@link ConversationConfig}: that
5
+ * contract is composed at load and bridged once through the boot global, while
6
+ * these preferences live in the durable user-settings document and take effect
7
+ * live.
8
+ */
9
+
10
+ /** Settings namespace registered by the Host and served through the plugin RPC. */
11
+ export const CONVERSATION_SETTINGS_NS = 'conversation-ui'
12
+
13
+ /**
14
+ * Preferences a user may set. Deliberately separate from {@link ConversationConfig}
15
+ * because the two change at different times: composition-time values go
16
+ * through the boot global, a live UI edit goes through the protected plugin RPC.
17
+ */
18
+ export interface ConversationSettings {
19
+ /**
20
+ * Whether a reasoning ("Think") block auto-expands while it is the
21
+ * streaming tail. Off keeps the block collapsed — the user can still open
22
+ * it by hand — and stops the running state from re-owning the disclosure.
23
+ */
24
+ thinkAutoExpand: boolean
25
+ }
26
+
27
+ /** Defaults shared by the Host schema and the client-side fallback. */
28
+ export const DEFAULT_CONVERSATION_SETTINGS: ConversationSettings = {
29
+ thinkAutoExpand: true,
30
+ }