@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
package/lib/index.js ADDED
@@ -0,0 +1,302 @@
1
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import Schema from "@deepseek-ai/schemastery";
3
+ import { readFileSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawn } from "node:child_process";
7
+ //#region src/config.ts
8
+ /** Defaults shared by the Host schema and the client-side fallback. */
9
+ const DEFAULT_CONVERSATION_CONFIG = {
10
+ mode: "teleprompter",
11
+ preset: "balanced",
12
+ revealCharsPerSec: 80,
13
+ scrollSpeedPxPerSec: 48,
14
+ maxScrollSpeedPxPerSec: 1e3
15
+ };
16
+ /**
17
+ * Window global the Host writes into the served index HTML. The browser boot
18
+ * graph carries no per-entry config, so this inline script is the only
19
+ * Host-to-client configuration channel for a composed web plugin.
20
+ */
21
+ const CONVERSATION_BOOT_GLOBAL = "__DSH_CONVERSATION_UI_CONFIG__";
22
+ //#endregion
23
+ //#region src/boot-config.ts
24
+ /**
25
+ * Host-rendered configuration bootstrap for the browser half: each index
26
+ * response embeds the schema-validated plugin config as a window global the
27
+ * client entry reads at apply time. Same pattern as ui-theme's boot theme.
28
+ */
29
+ /** Build the inline script assigning the validated config to the boot global. */
30
+ function bootConfigScript(config) {
31
+ return `<script>window[${JSON.stringify(CONVERSATION_BOOT_GLOBAL)}]=${JSON.stringify(config)}<\/script>`;
32
+ }
33
+ /**
34
+ * Insert the config bootstrap immediately after the opening body tag, before
35
+ * any plugin bundle runs. Body-less fragments receive it at the end, where
36
+ * the HTML parser has already synthesized a body.
37
+ * @param html - Raw application index HTML.
38
+ * @param config - Schema-validated plugin configuration.
39
+ * @returns HTML containing the config bootstrap.
40
+ */
41
+ function injectConversationConfig(html, config) {
42
+ const script = bootConfigScript(config);
43
+ const body = /<body(?:\s[^>]*)?>/i.exec(html);
44
+ if (body === null) return `${html}${script}`;
45
+ const at = body.index + body[0].length;
46
+ return `${html.slice(0, at)}${script}${html.slice(at)}`;
47
+ }
48
+ //#endregion
49
+ //#region src/package-meta.ts
50
+ /** Host-only package metadata. The package manifest remains the source of truth. */
51
+ function packageManifestPath() {
52
+ try {
53
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
54
+ } catch {}
55
+ return join(process.cwd(), "package.json");
56
+ }
57
+ const manifest = JSON.parse(readFileSync(packageManifestPath(), "utf8"));
58
+ function readRequiredString(field) {
59
+ const value = manifest[field];
60
+ if (typeof value !== "string" || value.length === 0) throw new Error(`dsh-conversation-ui: package.json must contain a non-empty ${field}`);
61
+ return value;
62
+ }
63
+ /** npm package name, read from this plugin's manifest. */
64
+ const CONVERSATION_PACKAGE_NAME = readRequiredString("name");
65
+ /** Version of the code currently loaded by the Host, read from its manifest. */
66
+ const CONVERSATION_PACKAGE_VERSION = readRequiredString("version");
67
+ //#endregion
68
+ //#region src/profile-installation.ts
69
+ /** Host-only profile source inspection and fixed npm update runner. */
70
+ function record(value) {
71
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
72
+ }
73
+ function profileDirectory(baseUrl) {
74
+ if (baseUrl === void 0) return void 0;
75
+ try {
76
+ const url = new URL(baseUrl);
77
+ return url.protocol === "file:" ? fileURLToPath(url) : void 0;
78
+ } catch {
79
+ return;
80
+ }
81
+ }
82
+ function hasBundle(manifest, packageName) {
83
+ const bundles = record(record(manifest.dsh)?.profile)?.bundles;
84
+ return Array.isArray(bundles) && bundles.includes(packageName);
85
+ }
86
+ function isLocalSpecifier(specifier) {
87
+ return specifier.startsWith("link:") || specifier.startsWith("file:") || specifier.startsWith(".") || isAbsolute(specifier) || /^[A-Za-z]:[\\/]/.test(specifier);
88
+ }
89
+ function isRegistrySpecifier(specifier) {
90
+ return specifier.length > 0 && !specifier.includes(":") && !/[\\/]/.test(specifier);
91
+ }
92
+ function isNpmSpecifier(specifier, packageName) {
93
+ if (!specifier.startsWith("npm:")) return isRegistrySpecifier(specifier);
94
+ const aliased = specifier.slice(4);
95
+ if (aliased === packageName) return true;
96
+ const prefix = `${packageName}@`;
97
+ return aliased.startsWith(prefix) && isRegistrySpecifier(aliased.slice(prefix.length));
98
+ }
99
+ /**
100
+ * Read only the profile manifest anchored by the current Cordis config tree.
101
+ * A malformed or unrelated tree never receives an update affordance.
102
+ */
103
+ function inspectProfileInstallation(baseUrl, packageName) {
104
+ const profileDir = profileDirectory(baseUrl);
105
+ if (profileDir === void 0) return { kind: "unmanaged" };
106
+ let manifest;
107
+ try {
108
+ manifest = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8"));
109
+ } catch {
110
+ return { kind: "unmanaged" };
111
+ }
112
+ const specifier = record(manifest.dependencies)?.[packageName];
113
+ if (typeof specifier !== "string" || !hasBundle(manifest, packageName)) return { kind: "unmanaged" };
114
+ if (isLocalSpecifier(specifier)) return { kind: "development" };
115
+ if (!isNpmSpecifier(specifier, packageName)) return { kind: "unmanaged" };
116
+ return {
117
+ kind: "npm",
118
+ profileDir,
119
+ profileName: basename(profileDir)
120
+ };
121
+ }
122
+ /** Run the same fixed package update operation that a profile user would invoke. */
123
+ function updateNpmProfilePackage(profileDir, packageName) {
124
+ const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
125
+ return new Promise((resolve, reject) => {
126
+ const child = spawn(command, ["update", packageName], {
127
+ cwd: profileDir,
128
+ stdio: "ignore",
129
+ shell: false,
130
+ windowsHide: true
131
+ });
132
+ child.once("error", reject);
133
+ child.once("exit", (code, signal) => {
134
+ if (code === 0) resolve();
135
+ else reject(/* @__PURE__ */ new Error(`dsh-conversation-ui: pnpm update failed (${signal ?? String(code)})`));
136
+ });
137
+ });
138
+ }
139
+ //#endregion
140
+ //#region src/settings-api.ts
141
+ /** Shared wire vocabulary for the plugin-owned settings RPC channel. */
142
+ /** Dedicated, loopback-only RPC channel registered by the Host half. */
143
+ const CONVERSATION_SETTINGS_RPC_CHANNEL = "/conversation-ui";
144
+ /** Endpoints accepted by {@link CONVERSATION_SETTINGS_RPC_CHANNEL}. */
145
+ const CONVERSATION_SETTINGS_RPC = {
146
+ read: "settings.read",
147
+ write: "settings.write",
148
+ upgrade: "plugin.upgrade"
149
+ };
150
+ //#endregion
151
+ //#region src/settings.ts
152
+ /**
153
+ * User-owned settings for the conversation-ui plugin, exposed to the Host
154
+ * settings service and edited from the Web Settings "plugin configuration"
155
+ * page. This is the runtime-editable complement to {@link ConversationConfig}: that
156
+ * contract is composed at load and bridged once through the boot global, while
157
+ * these preferences live in the durable user-settings document and take effect
158
+ * live.
159
+ */
160
+ /** Settings namespace registered by the Host and served through the plugin RPC. */
161
+ const CONVERSATION_SETTINGS_NS = "conversation-ui";
162
+ /** Defaults shared by the Host schema and the client-side fallback. */
163
+ const DEFAULT_CONVERSATION_SETTINGS = { thinkAutoExpand: true };
164
+ //#endregion
165
+ //#region src/plugin.ts
166
+ /** Display name shown by the Host loader while the plugin is mounted. */
167
+ const name = "dsh-conversation-ui";
168
+ const Config = Schema.object({
169
+ mode: Schema.union(["typewriter", "teleprompter"]).default(DEFAULT_CONVERSATION_CONFIG.mode),
170
+ preset: Schema.union([
171
+ "realtime",
172
+ "balanced",
173
+ "silky"
174
+ ]).default(DEFAULT_CONVERSATION_CONFIG.preset),
175
+ revealCharsPerSec: Schema.number().min(5).max(200).default(DEFAULT_CONVERSATION_CONFIG.revealCharsPerSec),
176
+ scrollSpeedPxPerSec: Schema.number().min(1).max(200).default(DEFAULT_CONVERSATION_CONFIG.scrollSpeedPxPerSec),
177
+ maxScrollSpeedPxPerSec: Schema.number().min(1).max(2e3).default(DEFAULT_CONVERSATION_CONFIG.maxScrollSpeedPxPerSec)
178
+ });
179
+ /**
180
+ * Schema of the user-owned settings section. The Host keeps it in the durable
181
+ * settings provider while the browser edits it through the plugin RPC below.
182
+ */
183
+ const ConversationSettingsSchema = Schema.object({ thinkAutoExpand: Schema.boolean().default(DEFAULT_CONVERSATION_SETTINGS.thinkAutoExpand) });
184
+ /**
185
+ * Host half: log the resolved configuration and bridge it to the browser
186
+ * half. The web boot graph carries no per-entry config, so the validated
187
+ * value is injected into every served index response as a boot global the
188
+ * client entry reads at apply time.
189
+ * @param ctx - Host context carrying the web server service when composed.
190
+ * @param config - Schema-validated configuration with defaults filled.
191
+ */
192
+ function apply(ctx, config) {
193
+ console.log(`[dsh-conversation-ui] plugin loaded! mode=${config.mode} preset=${config.preset} seed=${config.revealCharsPerSec}cps scroll=${config.scrollSpeedPxPerSec}px/s maxScroll=${config.maxScrollSpeedPxPerSec}px/s`);
194
+ ctx.inject(["webServer"], (httpCtx) => {
195
+ httpCtx.effect(() => httpCtx.webServer.tapIndex((html) => injectConversationConfig(html, config)), "dsh-conversation-ui: boot config bridge");
196
+ });
197
+ ctx.inject(["settings"], (settingsCtx) => {
198
+ const scope = settingsCtx.settings.register(settingsNamespace(CONVERSATION_SETTINGS_NS), ConversationSettingsSchema, { applies: "live" });
199
+ settingsCtx.inject(["connection"], (connectionCtx) => {
200
+ let upgrade;
201
+ const view = () => {
202
+ const installation = inspectProfileInstallation(connectionCtx.baseUrl, CONVERSATION_PACKAGE_NAME);
203
+ return {
204
+ version: CONVERSATION_PACKAGE_VERSION,
205
+ installation: installation.kind,
206
+ writable: connectionCtx.settings.writable,
207
+ thinkAutoExpand: scope.get().thinkAutoExpand,
208
+ canUpgrade: installation.kind === "npm"
209
+ };
210
+ };
211
+ const handle = async (endpoint, payload) => {
212
+ if (endpoint === CONVERSATION_SETTINGS_RPC.read) return {
213
+ ok: true,
214
+ value: view()
215
+ };
216
+ if (endpoint === CONVERSATION_SETTINGS_RPC.write) {
217
+ if (typeof payload !== "object" || payload === null || typeof payload.thinkAutoExpand !== "boolean") return {
218
+ ok: false,
219
+ error: {
220
+ code: "settings-rejected",
221
+ message: "thinkAutoExpand must be a boolean",
222
+ details: { ns: CONVERSATION_SETTINGS_NS }
223
+ }
224
+ };
225
+ if (!connectionCtx.settings.writable) return {
226
+ ok: false,
227
+ error: {
228
+ code: "settings-rejected",
229
+ message: "conversation-ui settings are read-only",
230
+ details: { ns: CONVERSATION_SETTINGS_NS }
231
+ }
232
+ };
233
+ try {
234
+ await scope.update({ thinkAutoExpand: payload.thinkAutoExpand });
235
+ } catch {
236
+ return {
237
+ ok: false,
238
+ error: {
239
+ code: "settings-rejected",
240
+ message: "conversation-ui settings update failed",
241
+ details: { ns: CONVERSATION_SETTINGS_NS }
242
+ }
243
+ };
244
+ }
245
+ return {
246
+ ok: true,
247
+ value: view()
248
+ };
249
+ }
250
+ if (endpoint === CONVERSATION_SETTINGS_RPC.upgrade) {
251
+ const installation = inspectProfileInstallation(connectionCtx.baseUrl, CONVERSATION_PACKAGE_NAME);
252
+ if (installation.kind !== "npm") return {
253
+ ok: false,
254
+ error: {
255
+ code: "internal",
256
+ message: "conversation-ui is not an npm profile dependency",
257
+ details: {}
258
+ }
259
+ };
260
+ if (upgrade !== void 0) return {
261
+ ok: false,
262
+ error: {
263
+ code: "internal",
264
+ message: "conversation-ui update is already running",
265
+ details: {}
266
+ }
267
+ };
268
+ upgrade = updateNpmProfilePackage(installation.profileDir, CONVERSATION_PACKAGE_NAME);
269
+ try {
270
+ await upgrade;
271
+ } catch {
272
+ return {
273
+ ok: false,
274
+ error: {
275
+ code: "internal",
276
+ message: "conversation-ui update failed",
277
+ details: {}
278
+ }
279
+ };
280
+ } finally {
281
+ upgrade = void 0;
282
+ }
283
+ return {
284
+ ok: true,
285
+ value: { restartRequired: true }
286
+ };
287
+ }
288
+ return {
289
+ ok: false,
290
+ error: {
291
+ code: "internal",
292
+ message: `unknown conversation-ui endpoint ${JSON.stringify(endpoint)}`,
293
+ details: {}
294
+ }
295
+ };
296
+ };
297
+ connectionCtx.effect(() => connectionCtx.connection.rpc.handle(CONVERSATION_SETTINGS_RPC_CHANNEL, handle, { authority: "loopback" }), "dsh-conversation-ui: settings RPC");
298
+ });
299
+ });
300
+ }
301
+ //#endregion
302
+ export { Config, apply, name };
package/package.json ADDED
@@ -0,0 +1,182 @@
1
+ {
2
+ "name": "@jcy2387/dsh-conversation-ui",
3
+ "version": "0.1.0-rc.12",
4
+ "description": "DeepSeek Harness (dsh) plugin: Codex-style progress, tool activity, and final answers.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": "^22.19.0 || >=24.0.0"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "registry": "https://registry.npmjs.org/"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/DamonBao/dsh-codex-suite.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/DamonBao/dsh-codex-suite/issues"
20
+ },
21
+ "homepage": "https://github.com/DamonBao/dsh-codex-suite#readme",
22
+ "keywords": [
23
+ "dsh-plugin",
24
+ "deepseek-harness",
25
+ "deepseek",
26
+ "dsh",
27
+ "cordis",
28
+ "streaming",
29
+ "conversation-ui",
30
+ "typewriter",
31
+ "typewriter-effect",
32
+ "markdown",
33
+ "chat-ui",
34
+ "web-ui",
35
+ "plugin",
36
+ "react"
37
+ ],
38
+ "main": "lib/index.js",
39
+ "exports": {
40
+ ".": {
41
+ "types": "./src/index.ts",
42
+ "default": "./lib/index.js"
43
+ },
44
+ "./client": {
45
+ "types": "./src/client/index.ts",
46
+ "default": "./lib/client.js"
47
+ },
48
+ "./src/*": "./src/*",
49
+ "./package.json": "./package.json"
50
+ },
51
+ "files": [
52
+ "lib/**/*.js",
53
+ "lib/**/*.js.map",
54
+ "src/**/*.ts",
55
+ "src/**/*.tsx",
56
+ "src/**/*.css",
57
+ "cordis.patch.yml",
58
+ "conversation-ui-off.yml",
59
+ "README.md",
60
+ "README.en.md",
61
+ "LICENSE"
62
+ ],
63
+ "dsh": {
64
+ "bundle": {
65
+ "patch": "./cordis.patch.yml"
66
+ },
67
+ "client": {
68
+ "platform": "web",
69
+ "inject": [
70
+ "@deepseek-ai/dsh-client-runtime",
71
+ "@deepseek-ai/dsh-client-ui-conversation",
72
+ "@deepseek-ai/dsh-client-locale",
73
+ "@deepseek-ai/dsh-client-connection",
74
+ "@deepseek-ai/dsh-client-ui-settings",
75
+ "@deepseek-ai/dsh-client-ui-settings-plugins"
76
+ ]
77
+ }
78
+ },
79
+ "dependencies": {
80
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
81
+ "react-icons": "5.5.0"
82
+ },
83
+ "peerDependencies": {
84
+ "@deepseek-ai/cordis": "^4.0.1",
85
+ "@deepseek-ai/dsh-client-connection": "*",
86
+ "@deepseek-ai/dsh-client-locale": "*",
87
+ "@deepseek-ai/dsh-client-runtime": "*",
88
+ "@deepseek-ai/dsh-client-ui-attachment": "*",
89
+ "@deepseek-ai/dsh-client-ui-conversation": "*",
90
+ "@deepseek-ai/dsh-client-ui-primitives": "*",
91
+ "@deepseek-ai/dsh-client-ui-settings": "*",
92
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "*",
93
+ "@deepseek-ai/dsh-client-ui-slots": "*",
94
+ "@deepseek-ai/dsh-host-webserver": "*",
95
+ "@deepseek-ai/schemastery": "*",
96
+ "react": "^18.2.0",
97
+ "react-dom": "^18.2.0"
98
+ },
99
+ "peerDependenciesMeta": {
100
+ "@deepseek-ai/dsh-client-locale": {
101
+ "optional": true
102
+ },
103
+ "@deepseek-ai/dsh-client-runtime": {
104
+ "optional": true
105
+ },
106
+ "@deepseek-ai/dsh-client-ui-attachment": {
107
+ "optional": true
108
+ },
109
+ "@deepseek-ai/dsh-client-connection": {
110
+ "optional": true
111
+ },
112
+ "@deepseek-ai/dsh-client-ui-conversation": {
113
+ "optional": true
114
+ },
115
+ "@deepseek-ai/dsh-client-ui-primitives": {
116
+ "optional": true
117
+ },
118
+ "@deepseek-ai/dsh-client-ui-settings": {
119
+ "optional": true
120
+ },
121
+ "@deepseek-ai/dsh-client-ui-settings-plugins": {
122
+ "optional": true
123
+ },
124
+ "@deepseek-ai/dsh-client-ui-slots": {
125
+ "optional": true
126
+ },
127
+ "@deepseek-ai/dsh-host-webserver": {
128
+ "optional": true
129
+ }
130
+ },
131
+ "devDependencies": {
132
+ "@deepseek-ai/cordis": "4.0.1",
133
+ "@deepseek-ai/dsh-client-connection": "0.1.0-rc.8",
134
+ "@deepseek-ai/dsh-client-locale": "0.1.0-rc.8",
135
+ "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.8",
136
+ "@deepseek-ai/dsh-client-ui-attachment": "0.1.0-rc.8",
137
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.8",
138
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.8",
139
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.8",
140
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.0-rc.8",
141
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.8",
142
+ "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8",
143
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
144
+ "@deepseek-ai/schemastery": "3.18.1",
145
+ "@shikijs/langs": "^4.4.3",
146
+ "@testing-library/react": "^16.3.0",
147
+ "@types/node": "^22.20.0",
148
+ "@types/react": "~18.3.1",
149
+ "@types/react-dom": "~18.3.0",
150
+ "anser": "^2.3.5",
151
+ "clsx": "^2.1.1",
152
+ "immer": "^10.1.1",
153
+ "jsdom": "^26.1.0",
154
+ "katex": "^0.16.47",
155
+ "lightningcss": "^1.30.1",
156
+ "mdast-util-from-markdown": "^2.0.3",
157
+ "mdast-util-gfm": "^3.1.0",
158
+ "mdast-util-math": "^3.0.0",
159
+ "micromark-core-commonmark": "^2.0.3",
160
+ "micromark-extension-gfm": "^3.0.0",
161
+ "micromark-extension-math": "^3.1.0",
162
+ "micromark-factory-space": "^2.0.1",
163
+ "micromark-util-character": "^2.1.1",
164
+ "micromark-util-classify-character": "^2.0.1",
165
+ "micromark-util-sanitize-uri": "^2.0.1",
166
+ "micromark-util-symbol": "^2.0.1",
167
+ "micromark-util-types": "^2.0.2",
168
+ "react": "^18.3.1",
169
+ "react-dom": "^18.3.1",
170
+ "shiki": "^4.4.3",
171
+ "tsdown": "^0.22.2",
172
+ "typescript": "^5.9.2",
173
+ "vitest": "^4.1.8",
174
+ "zod": "^4.4.3",
175
+ "zustand": "~4.4.7"
176
+ },
177
+ "scripts": {
178
+ "build": "tsdown",
179
+ "test": "vitest run",
180
+ "typecheck": "tsc -p tsconfig.json --pretty false"
181
+ }
182
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Host-rendered configuration bootstrap for the browser half: each index
3
+ * response embeds the schema-validated plugin config as a window global the
4
+ * client entry reads at apply time. Same pattern as ui-theme's boot theme.
5
+ */
6
+
7
+ import { CONVERSATION_BOOT_GLOBAL, type ConversationConfig } from './config.ts'
8
+
9
+ /** Build the inline script assigning the validated config to the boot global. */
10
+ function bootConfigScript(config: ConversationConfig): string {
11
+ return `<script>window[${JSON.stringify(CONVERSATION_BOOT_GLOBAL)}]=${JSON.stringify(config)}</script>`
12
+ }
13
+
14
+ /**
15
+ * Insert the config bootstrap immediately after the opening body tag, before
16
+ * any plugin bundle runs. Body-less fragments receive it at the end, where
17
+ * the HTML parser has already synthesized a body.
18
+ * @param html - Raw application index HTML.
19
+ * @param config - Schema-validated plugin configuration.
20
+ * @returns HTML containing the config bootstrap.
21
+ */
22
+ export function injectConversationConfig(html: string, config: ConversationConfig): string {
23
+ const script = bootConfigScript(config)
24
+ const body = /<body(?:\s[^>]*)?>/i.exec(html)
25
+ if (body === null) return `${html}${script}`
26
+ const at = body.index + body[0].length
27
+ return `${html.slice(0, at)}${script}${html.slice(at)}`
28
+ }
@@ -0,0 +1,89 @@
1
+ // AnimatedDisclosure: the harness DisclosureRow chrome (24px row, trailing
2
+ // chevron, whole-row click/Enter/Space toggle) with one deliberate
3
+ // difference - the expanded body stays mounted while collapsed and animates
4
+ // between 0fr and 1fr grid tracks, so expand/collapse glides instead of
5
+ // snapping. The harness primitive unmounts closed children (`{open &&
6
+ // children}`) and a plugin cannot change that in place, so this fork mirrors
7
+ // its DOM shape and `data-disclosure-row` contract: host styling keyed on the
8
+ // row attribute and the Think chrome keep working unchanged.
9
+
10
+ import { type KeyboardEvent, type ReactNode } from 'react'
11
+ import {
12
+ IconChevronDownOutline14,
13
+ IconChevronRightOutline14,
14
+ } from '@deepseek-ai/dsh-client-ui-primitives'
15
+ import css from './TypewriterAssistantNodeView.module.css'
16
+
17
+ /** Class-name join for optional overlay classes over the chrome defaults. */
18
+ function cx(...parts: Array<string | undefined>): string {
19
+ return parts.filter(part => part !== undefined && part !== '').join(' ')
20
+ }
21
+
22
+ export interface AnimatedDisclosureProps {
23
+ icon: ReactNode
24
+ title: string
25
+ open: boolean
26
+ onToggle: () => void
27
+ collapsedContent?: ReactNode
28
+ children?: ReactNode
29
+ rowClassName?: string | undefined
30
+ leadingClassName?: string | undefined
31
+ titleClassName?: string | undefined
32
+ chevronClassName?: string | undefined
33
+ }
34
+
35
+ /**
36
+ * Render one disclosure header whose expanded body is height-animated.
37
+ * @param props - Visual content, controlled open state, and the toggle
38
+ * callback fired by row click and Enter/Space.
39
+ * @returns the animated disclosure row.
40
+ */
41
+ export function AnimatedDisclosure({
42
+ icon,
43
+ title,
44
+ open,
45
+ onToggle,
46
+ collapsedContent,
47
+ children,
48
+ rowClassName,
49
+ leadingClassName,
50
+ titleClassName,
51
+ chevronClassName,
52
+ }: AnimatedDisclosureProps) {
53
+ const toggleFromKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
54
+ if (event.key !== 'Enter' && event.key !== ' ') return
55
+ event.preventDefault()
56
+ onToggle()
57
+ }
58
+ return (
59
+ <div className={css.disclosureRoot} data-open={open || undefined}>
60
+ <div
61
+ className={cx(css.disclosureRow, rowClassName)}
62
+ data-disclosure-row
63
+ data-expandable=""
64
+ role="button"
65
+ tabIndex={0}
66
+ aria-expanded={open}
67
+ onClick={onToggle}
68
+ onKeyDown={toggleFromKeyboard}
69
+ >
70
+ <span className={cx(css.disclosureLeading, leadingClassName)} data-disclosure-icon>
71
+ {icon}
72
+ </span>
73
+ <span className={cx(css.disclosureTitle, titleClassName)}>{title}</span>
74
+ {!open && collapsedContent}
75
+ <span
76
+ className={cx(css.disclosureTrailing, chevronClassName)}
77
+ data-disclosure-chevron
78
+ data-state={open ? 'expanded' : 'collapsed'}
79
+ aria-hidden
80
+ >
81
+ {open ? <IconChevronDownOutline14 size={14} /> : <IconChevronRightOutline14 size={14} />}
82
+ </span>
83
+ </div>
84
+ <div className={css.disclosureContent} data-disclosure-content data-collapsed={open ? undefined : ''}>
85
+ {children}
86
+ </div>
87
+ </div>
88
+ )
89
+ }