@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,242 @@
1
+ import { createElement, useSyncExternalStore, type ComponentType } from 'react'
2
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
3
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
4
+ // Type-only: the connection Context merge and the plugins section's SlotMap
5
+ // entry ('settings.plugin.item').
6
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
7
+ import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
8
+ import type { ChatNodeViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
9
+ import { TypewriterAssistantNodeView } from './TypewriterAssistantNodeView.tsx'
10
+ import { wrapFollowNodeView, type FollowWrapProps } from './TypewriterToolNodeView.tsx'
11
+ import { wrapTurnPreludeNodeView } from './TurnPreludeUserNodeView.tsx'
12
+ import { ConversationCard } from './ConversationCard.tsx'
13
+ import { ConversationCardController } from './conversation-ui-card-controller.ts'
14
+ import { DeliverablesCard } from './DeliverablesCard.tsx'
15
+ import { deliverablesDefinition, selectDeliverables } from './deliverables.ts'
16
+ import { createConversationSettingsApi } from './conversation-ui-settings-api.ts'
17
+ import { NS as SETTINGS_NS, en, zh } from './locales.ts'
18
+ import { DEFAULT_CONVERSATION_CONFIG, CONVERSATION_BOOT_GLOBAL, type ConversationConfig } from '../config.ts'
19
+ import { DEFAULT_CONVERSATION_SETTINGS } from '../settings.ts'
20
+
21
+ /**
22
+ * Cordis services required by the browser half. Only `slots` is load-bearing
23
+ * for the stream itself; locale and Connection power the configuration card
24
+ * and are wired through `ctx.inject` below so a deployment without them still
25
+ * streams with defaults.
26
+ */
27
+ export const inject = ['slots']
28
+
29
+ type AssistantProps = ChatNodeViewProps<'assistant-step'>
30
+
31
+ const CONVERSATION_MODES: readonly string[] = ['typewriter', 'teleprompter']
32
+ const CONVERSATION_PRESETS: readonly string[] = ['realtime', 'balanced', 'silky']
33
+
34
+ /** Steering/command rows stay untouched; `assistant-step` is replaced. */
35
+ const SKIP_WRAP = new Set(['assistant-step', 'steering', 'command-input'])
36
+
37
+ /**
38
+ * Read the Host-bridged boot config. The inline script is produced by this
39
+ * plugin's Host half from a schema-validated value, so only the structural
40
+ * guarantees that could break between the two halves are re-checked: the
41
+ * global is absent when the client runs without its Host entry (defaults
42
+ * apply), and any present-but-malformed value fails loudly instead of
43
+ * rendering a half-configured view.
44
+ * @returns The resolved configuration for the assistant node view.
45
+ */
46
+ function readBootConfig(): ConversationConfig {
47
+ const raw = (globalThis as Record<string, unknown>)[CONVERSATION_BOOT_GLOBAL]
48
+ if (raw === undefined) {
49
+ console.info('[dsh-conversation-ui] no host config bridge; using defaults')
50
+ return DEFAULT_CONVERSATION_CONFIG
51
+ }
52
+ if (
53
+ typeof raw !== 'object' || raw === null
54
+ || !CONVERSATION_MODES.includes((raw as ConversationConfig).mode)
55
+ || !CONVERSATION_PRESETS.includes((raw as ConversationConfig).preset)
56
+ || typeof (raw as ConversationConfig).revealCharsPerSec !== 'number'
57
+ || typeof (raw as ConversationConfig).scrollSpeedPxPerSec !== 'number'
58
+ || typeof (raw as ConversationConfig).maxScrollSpeedPxPerSec !== 'number'
59
+ ) {
60
+ throw new Error(`[dsh-conversation-ui] malformed ${CONVERSATION_BOOT_GLOBAL} boot global: ${JSON.stringify(raw)}`)
61
+ }
62
+ return raw as ConversationConfig
63
+ }
64
+
65
+ /**
66
+ * Wrap every keyed Chat row except `assistant-step` in place. A second
67
+ * register with the same `children` table throws because the child slot is
68
+ * already declared, and only the winning entry receives `renderSlot`;
69
+ * swapping `entry.component` keeps the original children, locale, and inject
70
+ * seats. `assistant-step` is replaced below so text and Think use the
71
+ * Codex-style Assistant presentation.
72
+ * @param ctx - Browser context carrying the slot registry.
73
+ * @returns Restorer that puts the original components back.
74
+ */
75
+ function wrapGrowingChatRows(ctx: ClientContext, config: ConversationConfig): () => void {
76
+ const restores: Array<() => void> = []
77
+ const wrapped = new WeakSet<object>()
78
+
79
+ const wrapAll = (): void => {
80
+ for (const entry of ctx.slots.entries('conversation.chat.node')) {
81
+ const key = entry.options.key
82
+ if (key === undefined || SKIP_WRAP.has(key)) continue
83
+ const current = entry.component
84
+ if (
85
+ (typeof current !== 'function' && (typeof current !== 'object' || current === null))
86
+ || wrapped.has(current)
87
+ ) continue
88
+ const inner = current as ComponentType<FollowWrapProps>
89
+ const next = key === 'user'
90
+ ? wrapTurnPreludeNodeView(inner)
91
+ : wrapFollowNodeView(inner, {
92
+ minSpeedPxPerSec: config.scrollSpeedPxPerSec,
93
+ maxSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
94
+ })
95
+ wrapped.add(next)
96
+ entry.component = next
97
+ restores.push(() => {
98
+ if (entry.component === next) entry.component = inner
99
+ })
100
+ }
101
+ }
102
+
103
+ wrapAll()
104
+ const off = ctx.on('slots/changed', (key: string) => {
105
+ if (key === 'conversation.chat.node') wrapAll()
106
+ })
107
+ return () => {
108
+ off()
109
+ for (const restore of restores) restore()
110
+ }
111
+ }
112
+
113
+ /**
114
+ * A live preference cell read by `useSyncExternalStore`. It starts on the
115
+ * shared default and, once the plugin-owned settings controller is attached,
116
+ * tracks the resolved `thinkAutoExpand` value. A composition (or test) that
117
+ * ships no settings surface keeps the cell on the default.
118
+ */
119
+ class PreferenceCell {
120
+ private readonly listeners = new Set<() => void>()
121
+ private card: ConversationCardController | undefined
122
+ private value = DEFAULT_CONVERSATION_SETTINGS.thinkAutoExpand
123
+
124
+ /** Re-point the cell at the plugin-owned settings controller. */
125
+ attach(card: ConversationCardController): () => void {
126
+ this.card = card
127
+ this.refresh()
128
+ const unsubscribe = card.subscribe(() => { this.refresh() })
129
+ return () => {
130
+ unsubscribe()
131
+ if (this.card !== card) return
132
+ this.card = undefined
133
+ this.refresh()
134
+ }
135
+ }
136
+
137
+ private read(): boolean {
138
+ return this.card?.getSnapshot().thinkAutoExpand ?? DEFAULT_CONVERSATION_SETTINGS.thinkAutoExpand
139
+ }
140
+
141
+ private refresh(): void {
142
+ const next = this.read()
143
+ if (next === this.value) return
144
+ this.value = next
145
+ for (const listener of this.listeners) listener()
146
+ }
147
+
148
+ readonly getSnapshot = (): boolean => this.value
149
+
150
+ readonly subscribe = (listener: () => void): () => void => {
151
+ this.listeners.add(listener)
152
+ return () => { this.listeners.delete(listener) }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Register the Codex-style renderer after the conversation package declares the
158
+ * keyed Chat node seat. A lower priority shadows the built-in assistant row;
159
+ * every other growing row is wrapped in place so Tool cards, retries, and
160
+ * workflow runs share conversation follow. The Host-bridged configuration
161
+ * selects immediate or typewriter reveal, smoothing preset, and glide speed; the
162
+ * plugin-owned settings RPC supplies the live auto-expand preference when the
163
+ * settings surface is composed.
164
+ * @param ctx - Browser context carrying the shared slot registry.
165
+ */
166
+ export function apply(ctx: ClientContext): void {
167
+ const config = readBootConfig()
168
+ const preference = new PreferenceCell()
169
+
170
+ // The native deliverables package already publishes successful file paths,
171
+ // so the Codex card must register independently of our optional richer event
172
+ // accumulator. Keeping the slot inside the service injection left Web with
173
+ // only the built-in "产物" chips when that callback was not activated.
174
+ ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
175
+ name: 'conversation.chat.turnTail',
176
+ priority: -100,
177
+ select: selectDeliverables,
178
+ registrant: 'dsh-conversation-ui',
179
+ }, DeliverablesCard))
180
+ ctx.inject(['conversationEvents'], (deliverablesCtx) => {
181
+ const events = deliverablesCtx.get('conversationEvents') as {
182
+ register: (definition: typeof deliverablesDefinition) => () => void
183
+ }
184
+ return events.register(deliverablesDefinition)
185
+ })
186
+
187
+ // The card talks to the plugin-owned loopback RPC, so the core settings
188
+ // namespace allowlist cannot make it disappear. The stream still applies
189
+ // with defaults when the optional Settings UI or Connection is absent.
190
+ ctx.inject(['slots', 'locale', 'connection'], (settingsCtx) => {
191
+ const card = new ConversationCardController(
192
+ // The shared Context augmentation also carries the Host-side Connection
193
+ // shape. This browser entry runs after the client provider installs its
194
+ // handle, so narrow through unknown to its client contract here.
195
+ createConversationSettingsApi(settingsCtx.get('connection') as unknown as ConnectionHandle),
196
+ )
197
+ const detachPreference = preference.attach(card)
198
+ card.start()
199
+ settingsCtx.effect(() => settingsCtx.locale.register(SETTINGS_NS, { zh, en }), 'dsh-conversation-ui: settings dictionaries')
200
+ settingsCtx.slots.inject('settings.plugin.item', () => settingsCtx.slots.register({
201
+ name: 'settings.plugin.item',
202
+ key: 'conversation-ui',
203
+ locale: SETTINGS_NS,
204
+ inject: () => card.inject(),
205
+ }, ConversationCard))
206
+ return () => {
207
+ card.stop()
208
+ detachPreference()
209
+ }
210
+ })
211
+
212
+ const configured = function ConversationConfiguredView(props: AssistantProps) {
213
+ const thinkAutoExpand = useSyncExternalStore(
214
+ preference.subscribe,
215
+ preference.getSnapshot,
216
+ preference.getSnapshot,
217
+ )
218
+ return createElement(TypewriterAssistantNodeView, {
219
+ ...props,
220
+ mode: config.mode,
221
+ preset: config.preset,
222
+ revealCharsPerSec: config.revealCharsPerSec,
223
+ scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
224
+ maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
225
+ thinkAutoExpand,
226
+ })
227
+ }
228
+ ctx.slots.inject('conversation.chat.node', () => {
229
+ const unwrap = wrapGrowingChatRows(ctx, config)
230
+ const unshadow = ctx.slots.register({
231
+ name: 'conversation.chat.node',
232
+ key: 'assistant-step',
233
+ priority: -100,
234
+ locale: 'conversation',
235
+ registrant: 'dsh-conversation-ui',
236
+ }, configured)
237
+ return () => {
238
+ unshadow()
239
+ unwrap()
240
+ }
241
+ })
242
+ }
@@ -0,0 +1,74 @@
1
+ /** Locale bundles for the conversation-ui plugin configuration card. */
2
+
3
+ /** Dictionary namespace owned by this plugin's settings card. */
4
+ export const NS = 'settings.conversationUi'
5
+
6
+ /** Locale keys the card renders. */
7
+ export type ConversationLocaleKey =
8
+ | 'title' | 'description'
9
+ | 'thinkAutoExpand' | 'thinkAutoExpandHint'
10
+ | 'readOnly' | 'loading' | 'unavailable' | 'retry'
11
+ | 'version' | 'developmentVersion'
12
+ | 'updates' | 'updateHint' | 'developmentBuild' | 'updateUnavailable'
13
+ | 'update' | 'updating' | 'restartRequired' | 'updateFailed'
14
+ | 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed'
15
+
16
+ /** English copy. */
17
+ export const en: Record<ConversationLocaleKey, string> = {
18
+ title: 'Conversation UI',
19
+ description: 'Codex-style progress, tool activity, and final answers.',
20
+ thinkAutoExpand: 'Auto-expand thinking',
21
+ thinkAutoExpandHint: 'Open the thinking block while it streams. Turn off to keep it collapsed.',
22
+ readOnly: 'This deployment stores settings read-only.',
23
+ loading: 'Loading plugin settings…',
24
+ unavailable: 'Plugin settings are unavailable in this connection.',
25
+ retry: 'Retry',
26
+ version: 'Version {version}',
27
+ developmentVersion: 'Development version {version}',
28
+ updates: 'Updates',
29
+ updateHint: 'Install the newest npm version, then restart Harness.',
30
+ developmentBuild: 'Linked source; updates are managed in the checkout.',
31
+ updateUnavailable: 'Updates are available only for an npm profile installation.',
32
+ update: 'Update',
33
+ updating: 'Updating…',
34
+ restartRequired: 'Updated. Restart Harness to load the new version.',
35
+ updateFailed: 'The package update failed; your current version is unchanged.',
36
+ save: 'Save',
37
+ saving: 'Saving…',
38
+ discard: 'Discard',
39
+ unsaved: 'Unsaved',
40
+ saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
41
+ }
42
+
43
+ /** Simplified Chinese copy. */
44
+ export const zh: Record<ConversationLocaleKey, string> = {
45
+ title: '对话界面增强',
46
+ description: '按 Codex 风格展示过程答复、工具活动和最终答案。',
47
+ thinkAutoExpand: '自动展开思考',
48
+ thinkAutoExpandHint: '思考块在流式时自动展开;关闭后保持折叠,可手动展开。',
49
+ readOnly: '本部署的设置为只读。',
50
+ loading: '正在加载插件设置…',
51
+ unavailable: '当前连接无法访问插件设置。',
52
+ retry: '重试',
53
+ version: '版本 {version}',
54
+ developmentVersion: '开发版本 {version}',
55
+ updates: '更新',
56
+ updateHint: '安装最新 npm 版本后重启 Harness。',
57
+ developmentBuild: '当前为本地链接版本,请在源码目录管理更新。',
58
+ updateUnavailable: '只有 profile 使用 npm 包时才能更新。',
59
+ update: '更新',
60
+ updating: '更新中…',
61
+ restartRequired: '已更新;重启 Harness 后加载新版本。',
62
+ updateFailed: '包更新失败,当前版本未改变。',
63
+ save: '保存',
64
+ saving: '保存中…',
65
+ discard: '放弃修改',
66
+ unsaved: '未保存',
67
+ saveFailed: '本部署没有接受这些值,已保留供你修改。',
68
+ }
69
+
70
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
71
+ interface LocaleNamespaceMap {
72
+ 'settings.conversationUi': ConversationLocaleKey
73
+ }
74
+ }