dsh-better-display 0.2.1 → 0.3.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 +11 -0
- package/README.en.md +4 -2
- package/README.md +4 -2
- package/compat/harness-rc2.json +59 -0
- package/docs/official-rendering-bridge.md +65 -0
- package/lib/client.js +858 -456
- package/lib/client.js.map +1 -1
- package/lib/types/client/Blocks.d.ts +2 -1
- package/lib/types/client/Blocks.d.ts.map +1 -1
- package/lib/types/client/Blocks.js +2 -2
- package/lib/types/client/Blocks.js.map +1 -1
- package/lib/types/client/OfficialContent.d.ts +25 -0
- package/lib/types/client/OfficialContent.d.ts.map +1 -0
- package/lib/types/client/OfficialContent.js +42 -0
- package/lib/types/client/OfficialContent.js.map +1 -0
- package/lib/types/client/Reader.d.ts.map +1 -1
- package/lib/types/client/Reader.js +38 -6
- package/lib/types/client/Reader.js.map +1 -1
- package/lib/types/client/ToolActivity.d.ts.map +1 -1
- package/lib/types/client/ToolActivity.js +21 -17
- package/lib/types/client/ToolActivity.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +23 -33
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/markdown/MarkdownText.d.ts +3 -1
- package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -1
- package/lib/types/client/markdown/MarkdownText.js +14 -7
- package/lib/types/client/markdown/MarkdownText.js.map +1 -1
- package/lib/types/client/markdown/render.d.ts +2 -0
- package/lib/types/client/markdown/render.d.ts.map +1 -1
- package/lib/types/client/markdown/render.js +22 -6
- package/lib/types/client/markdown/render.js.map +1 -1
- package/lib/types/client/motion.d.ts.map +1 -1
- package/lib/types/client/motion.js +22 -13
- package/lib/types/client/motion.js.map +1 -1
- package/lib/types/client/native/ContextInjectionRow.d.ts +12 -3
- package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -1
- package/lib/types/client/native/ContextInjectionRow.js +6 -3
- package/lib/types/client/native/ContextInjectionRow.js.map +1 -1
- package/lib/types/client/official-slots.d.ts +71 -0
- package/lib/types/client/official-slots.d.ts.map +1 -0
- package/lib/types/client/official-slots.js +183 -0
- package/lib/types/client/official-slots.js.map +1 -0
- package/lib/types/client/platform-media.d.ts +6 -0
- package/lib/types/client/platform-media.d.ts.map +1 -0
- package/lib/types/client/platform-media.js +13 -0
- package/lib/types/client/platform-media.js.map +1 -0
- package/lib/types/client/settings.js.map +1 -1
- package/lib/types/client/store.d.ts +1 -0
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/client/store.js +1 -0
- package/lib/types/client/store.js.map +1 -1
- package/lib/types/client/tool-activity.d.ts +1 -1
- package/lib/types/client/tool-activity.d.ts.map +1 -1
- package/lib/types/client/tool-activity.js +9 -2
- package/lib/types/client/tool-activity.js.map +1 -1
- package/lib/types/client/types.d.ts +18 -8
- package/lib/types/client/types.d.ts.map +1 -1
- package/lib/types/client/word-motion.d.ts.map +1 -1
- package/lib/types/client/word-motion.js +2 -1
- package/lib/types/client/word-motion.js.map +1 -1
- package/package.json +22 -16
- package/scripts/check-harness-compat.mjs +71 -0
- package/scripts/link-harness-dependencies.mjs +1 -0
- package/scripts/verify-auto-fold.mjs +114 -0
- package/scripts/verify-official-bridge.mjs +119 -0
- package/src/client/Blocks.tsx +2 -1
- package/src/client/OfficialContent.tsx +57 -0
- package/src/client/Reader.module.css +23 -8
- package/src/client/Reader.tsx +37 -9
- package/src/client/ToolActivity.tsx +22 -27
- package/src/client/index.tsx +27 -31
- package/src/client/markdown/MarkdownText.tsx +14 -6
- package/src/client/markdown/render.tsx +23 -7
- package/src/client/motion.tsx +18 -11
- package/src/client/native/ContextInjectionRow.tsx +19 -7
- package/src/client/official-slots.tsx +211 -0
- package/src/client/platform-media.ts +11 -0
- package/src/client/settings.tsx +1 -1
- package/src/client/store.ts +2 -0
- package/src/client/tool-activity.ts +9 -2
- package/src/client/types.ts +14 -8
- package/src/client/word-motion.tsx +2 -1
- package/tests/browser-auto-fold.tsx +64 -0
- package/tests/browser-official-bridge.tsx +144 -0
- package/tests/official-slots.test.ts +112 -0
- package/tests/platform-media.test.ts +17 -0
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { memo, useMemo, useRef } from 'react'
|
|
16
16
|
import type { ReactNode } from 'react'
|
|
17
|
+
import type { MarkdownPathImages } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
17
18
|
import { IncrementalMarkdownParser } from './incremental.js'
|
|
18
19
|
import { parseGfm, parseGfmWithMath } from './parse.js'
|
|
19
20
|
import {
|
|
@@ -31,6 +32,7 @@ function renderSettled(
|
|
|
31
32
|
text: string,
|
|
32
33
|
codeLabels: MarkdownCodeLabels | undefined,
|
|
33
34
|
fileMentions: MarkdownFileMentions | undefined,
|
|
35
|
+
pathImages: MarkdownPathImages | undefined,
|
|
34
36
|
renderText: MarkdownRenderContext['renderText'],
|
|
35
37
|
renderAtom: MarkdownRenderContext['renderAtom'],
|
|
36
38
|
): ReactNode[] {
|
|
@@ -43,6 +45,7 @@ function renderSettled(
|
|
|
43
45
|
streaming: false,
|
|
44
46
|
codeLabels,
|
|
45
47
|
fileMentions,
|
|
48
|
+
pathImages,
|
|
46
49
|
targets,
|
|
47
50
|
footnoteOrder: [],
|
|
48
51
|
footnoteCounts: new Map(),
|
|
@@ -73,7 +76,7 @@ class StreamingRenderer {
|
|
|
73
76
|
private lastRendered: ReactNode[] = []
|
|
74
77
|
|
|
75
78
|
/** @param codeLabels - Fence copy labels baked into cached elements; the owner replaces the renderer when they change. */
|
|
76
|
-
constructor(private readonly codeLabels: MarkdownCodeLabels | undefined, private readonly renderText: MarkdownRenderContext['renderText'], private readonly renderAtom: MarkdownRenderContext['renderAtom']) {}
|
|
79
|
+
constructor(private readonly codeLabels: MarkdownCodeLabels | undefined, private readonly renderText: MarkdownRenderContext['renderText'], private readonly renderAtom: MarkdownRenderContext['renderAtom'], private readonly pathImages: MarkdownPathImages | undefined) {}
|
|
77
80
|
|
|
78
81
|
/**
|
|
79
82
|
* Render the current accumulated text. Idempotent per text value, so React
|
|
@@ -109,6 +112,7 @@ class StreamingRenderer {
|
|
|
109
112
|
streaming: true,
|
|
110
113
|
codeLabels: this.codeLabels,
|
|
111
114
|
fileMentions: undefined,
|
|
115
|
+
pathImages: this.pathImages,
|
|
112
116
|
targets: frameTargets,
|
|
113
117
|
footnoteOrder: this.frozenFootnoteOrder,
|
|
114
118
|
footnoteCounts: this.frozenFootnoteCounts,
|
|
@@ -129,6 +133,7 @@ class StreamingRenderer {
|
|
|
129
133
|
streaming: true,
|
|
130
134
|
codeLabels: this.codeLabels,
|
|
131
135
|
fileMentions: undefined,
|
|
136
|
+
pathImages: this.pathImages,
|
|
132
137
|
targets: frameTargets,
|
|
133
138
|
footnoteOrder: [...this.frozenFootnoteOrder],
|
|
134
139
|
footnoteCounts: new Map(this.frozenFootnoteCounts),
|
|
@@ -162,26 +167,29 @@ class StreamingRenderer {
|
|
|
162
167
|
* relative links, and unsafe protocols are disabled, while absolute HTTP(S)
|
|
163
168
|
* images render directly.
|
|
164
169
|
*/
|
|
165
|
-
export const MarkdownText = memo(function MarkdownText({ text, streaming = false, codeLabels, fileMentions, renderText, renderAtom }: {
|
|
170
|
+
export const MarkdownText = memo(function MarkdownText({ text, streaming = false, codeLabels, fileMentions, pathImages, renderText, renderAtom }: {
|
|
166
171
|
text: string
|
|
167
172
|
streaming?: boolean
|
|
168
173
|
codeLabels?: MarkdownCodeLabels | undefined
|
|
169
174
|
fileMentions?: MarkdownFileMentions | undefined
|
|
175
|
+
pathImages?: MarkdownPathImages | undefined
|
|
170
176
|
renderText?: MarkdownRenderContext['renderText']
|
|
171
177
|
renderAtom?: MarkdownRenderContext['renderAtom']
|
|
172
178
|
}) {
|
|
173
179
|
const streamRef = useRef<StreamingRenderer | null>(null)
|
|
180
|
+
const streamImagesRef = useRef(pathImages)
|
|
174
181
|
const streamLabelsRef = useRef<MarkdownCodeLabels | undefined>(codeLabels)
|
|
175
182
|
const children = useMemo(() => {
|
|
176
183
|
if (!streaming) {
|
|
177
184
|
streamRef.current = null
|
|
178
|
-
return renderSettled(text, codeLabels, fileMentions, renderText, renderAtom)
|
|
185
|
+
return renderSettled(text, codeLabels, fileMentions, pathImages, renderText, renderAtom)
|
|
179
186
|
}
|
|
180
|
-
if (streamRef.current === null || streamLabelsRef.current !== codeLabels) {
|
|
181
|
-
streamRef.current = new StreamingRenderer(codeLabels, renderText, renderAtom)
|
|
187
|
+
if (streamRef.current === null || streamLabelsRef.current !== codeLabels || streamImagesRef.current !== pathImages) {
|
|
188
|
+
streamRef.current = new StreamingRenderer(codeLabels, renderText, renderAtom, pathImages)
|
|
182
189
|
streamLabelsRef.current = codeLabels
|
|
190
|
+
streamImagesRef.current = pathImages
|
|
183
191
|
}
|
|
184
192
|
return streamRef.current.render(text)
|
|
185
|
-
}, [text, streaming, codeLabels, fileMentions, renderText, renderAtom])
|
|
193
|
+
}, [text, streaming, codeLabels, fileMentions, pathImages, renderText, renderAtom])
|
|
186
194
|
return <div className={css.markdown}>{children}</div>
|
|
187
195
|
})
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* may add node types this renderer has no mapping for.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { Fragment, createElement } from 'react'
|
|
20
|
+
import { Fragment, createElement, useState } from 'react'
|
|
21
21
|
import type { Key, ReactNode } from 'react'
|
|
22
22
|
import clsx from 'clsx'
|
|
23
23
|
import type * as Md from 'mdast'
|
|
@@ -27,6 +27,7 @@ import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
|
27
27
|
import { renderTexToReact } from './katex.js'
|
|
28
28
|
import { McpAppCodeBlock, StreamingMcpAppPlaceholder } from '../McpAppFrame.js'
|
|
29
29
|
import { isMcpAppCodeBlock, extractMcpAppTitle, extractMcpAppHeight } from '../mcp-app.js'
|
|
30
|
+
import type { MarkdownPathImages } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
30
31
|
import type { PositionedBlock } from './incremental.js'
|
|
31
32
|
import css from './MarkdownText.module.css'
|
|
32
33
|
|
|
@@ -136,6 +137,7 @@ export interface MarkdownRenderContext {
|
|
|
136
137
|
readonly inBlockquote?: boolean
|
|
137
138
|
/** Inline-code file mentions; absent wherever no opener vocabulary exists. */
|
|
138
139
|
readonly fileMentions: MarkdownFileMentions | undefined
|
|
140
|
+
readonly pathImages?: MarkdownPathImages
|
|
139
141
|
/** Inside an anchor's children: interactive mentions must not nest there. */
|
|
140
142
|
readonly inLink?: boolean
|
|
141
143
|
/** Reference targets visible to this pass. */
|
|
@@ -301,7 +303,7 @@ function renderNode(node: Md.RootContent, key: Key, context: MarkdownRenderConte
|
|
|
301
303
|
case 'linkReference':
|
|
302
304
|
return renderLinkReference(node, key, context)
|
|
303
305
|
case 'image':
|
|
304
|
-
return renderImage(node.url, node.alt ?? '', key)
|
|
306
|
+
return renderImage(node.url, node.alt ?? '', key, context)
|
|
305
307
|
case 'imageReference':
|
|
306
308
|
return renderImageReference(node, key, context)
|
|
307
309
|
case 'footnoteReference':
|
|
@@ -515,17 +517,31 @@ function inlineCodeHttpUrl(value: string): string | undefined {
|
|
|
515
517
|
}
|
|
516
518
|
}
|
|
517
519
|
|
|
518
|
-
function renderImage(url: string, alt: string, key: Key): ReactNode {
|
|
519
|
-
|
|
520
|
+
function renderImage(url: string, alt: string, key: Key, context: MarkdownRenderContext): ReactNode {
|
|
521
|
+
// RC2 semantics, including the revalidation of a trusted vocabulary result.
|
|
522
|
+
const remote = remoteImageUrl(sanitizeUrl(normalizeUri(url)))
|
|
523
|
+
const rewritten = remote === undefined ? context.pathImages?.resolve(url) : undefined
|
|
524
|
+
let imageSrc = remote
|
|
525
|
+
if (rewritten !== undefined) {
|
|
526
|
+
try {
|
|
527
|
+
if (['http:', 'https:', 'blob:', 'data:'].includes(new URL(rewritten).protocol)) imageSrc = rewritten
|
|
528
|
+
} catch { /* A vocabulary result must be an absolute URL. */ }
|
|
529
|
+
}
|
|
520
530
|
if (imageSrc === undefined) {
|
|
521
531
|
return <span key={key} className={css.imageAlt}>{alt}</span>
|
|
522
532
|
}
|
|
533
|
+
return <MarkdownImage key={`${key}:${imageSrc}`} src={imageSrc} alt={alt} destination={url} />
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function MarkdownImage({ src, alt, destination }: { src: string; alt: string; destination: string }): ReactNode {
|
|
537
|
+
const [failed, setFailed] = useState(false)
|
|
538
|
+
if (failed) return <span className={css.imageAlt}>{alt || destination}</span>
|
|
523
539
|
return (
|
|
524
540
|
<img
|
|
525
|
-
key={key}
|
|
526
541
|
className={css.image}
|
|
527
|
-
src={
|
|
542
|
+
src={src}
|
|
528
543
|
alt={alt}
|
|
544
|
+
onError={() => { setFailed(true) }}
|
|
529
545
|
loading="lazy"
|
|
530
546
|
decoding="async"
|
|
531
547
|
referrerPolicy="no-referrer"
|
|
@@ -563,7 +579,7 @@ function renderImageReference(
|
|
|
563
579
|
): ReactNode {
|
|
564
580
|
const definition = context.targets.definitions.get(node.identifier.toUpperCase())
|
|
565
581
|
if (definition === undefined) return `![${node.alt ?? ''}${referenceSuffix(node)}`
|
|
566
|
-
return renderImage(definition.url, node.alt ?? '', key)
|
|
582
|
+
return renderImage(definition.url, node.alt ?? '', key, context)
|
|
567
583
|
}
|
|
568
584
|
|
|
569
585
|
function renderFootnoteReference(
|
package/src/client/motion.tsx
CHANGED
|
@@ -126,9 +126,6 @@ export function ProcessFragment({ open, motion, onRead, returnFocusTo, nodeKey,
|
|
|
126
126
|
setPresent(open);
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
|
-
if (from > target && motion) {
|
|
130
|
-
document.body.dataset.readerFolding = 'true';
|
|
131
|
-
}
|
|
132
129
|
const animation = element.animate(
|
|
133
130
|
from > target
|
|
134
131
|
? [
|
|
@@ -149,7 +146,6 @@ export function ProcessFragment({ open, motion, onRead, returnFocusTo, nodeKey,
|
|
|
149
146
|
if (settled) return;
|
|
150
147
|
settled = true;
|
|
151
148
|
if (running.current === animation) running.current = null;
|
|
152
|
-
delete document.body.dataset.readerFolding;
|
|
153
149
|
animation.cancel();
|
|
154
150
|
setPresent(open);
|
|
155
151
|
};
|
|
@@ -164,7 +160,6 @@ export function ProcessFragment({ open, motion, onRead, returnFocusTo, nodeKey,
|
|
|
164
160
|
return () => {
|
|
165
161
|
window.clearTimeout(deadline);
|
|
166
162
|
running.current?.cancel();
|
|
167
|
-
delete document.body.dataset.readerFolding;
|
|
168
163
|
};
|
|
169
164
|
}, [open, motion, returnFocusTo]);
|
|
170
165
|
if (!open && !present) return null;
|
|
@@ -299,16 +294,28 @@ export function useReadingScroll(root: RefObject<HTMLElement>, motion: boolean):
|
|
|
299
294
|
if (followFrame !== 0 || layoutDepth > 0) return;
|
|
300
295
|
// Our easing frames must not be mistaken for a user leaving the bottom.
|
|
301
296
|
if (lastWrittenTop !== null && Math.abs(scroll.scrollTop - lastWrittenTop) < 1) return;
|
|
297
|
+
// Bind the follow intent to the position observed HERE. Every scroll that
|
|
298
|
+
// reaches this point is the viewport moving under someone else's hand: a
|
|
299
|
+
// wheel, a drag, a key, or the browser clamping the offset after a shrink.
|
|
300
|
+
//
|
|
301
|
+
// The previous form inferred detaching from a strictly upward delta against
|
|
302
|
+
// lastWrittenTop, which left two holes. That baseline is nulled by
|
|
303
|
+
// cancelFollow/onWheel/onTouch/onKey, and the guard above already returned
|
|
304
|
+
// for any scroll whose delta is under 1px — so a reader parked mid-transcript
|
|
305
|
+
// by a scrollbar drag stayed following === true AND pinned === true. The next
|
|
306
|
+
// content growth then took the re-attach branch and yanked the viewport to
|
|
307
|
+
// the bottom, discarding what they were reading. Reading the position
|
|
308
|
+
// directly needs no baseline: a fold's clamp lands at the very bottom and
|
|
309
|
+
// still keeps following, while any other offset detaches.
|
|
310
|
+
const bottom = atBottom();
|
|
311
|
+
pinned.current = bottom;
|
|
312
|
+
if (!bottom) { cancelAnimationFrame(followFrame); followFrame = 0; scheduleCapture(); }
|
|
302
313
|
if (!following.current) {
|
|
303
314
|
// Only an explicit return to the bottom resumes following.
|
|
304
|
-
if (
|
|
315
|
+
if (bottom) { following.current = true; setDetached(false); }
|
|
305
316
|
return;
|
|
306
317
|
}
|
|
307
|
-
|
|
308
|
-
// upward move by the reader detaches the tail.
|
|
309
|
-
if (lastWrittenTop !== null && scroll.scrollTop < lastWrittenTop - 1) {
|
|
310
|
-
following.current = false; setDetached(true); pinned.current = false; cancelAnimationFrame(followFrame); followFrame = 0; scheduleCapture();
|
|
311
|
-
}
|
|
318
|
+
if (!bottom) { following.current = false; setDetached(true); }
|
|
312
319
|
};
|
|
313
320
|
const onWheel = (event: WheelEvent) => {
|
|
314
321
|
cancelAnimationFrame(followFrame); followFrame = 0; lastWrittenTop = null;
|
|
@@ -10,8 +10,17 @@ import css from './ContextInjectionRow.module.css'
|
|
|
10
10
|
export interface ContextInjectionRowProps {
|
|
11
11
|
content: ContextMessageNode['content']
|
|
12
12
|
source: ContextMessageNode['source']
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Role and producer name projected from the durable source.
|
|
15
|
+
*
|
|
16
|
+
* Host generations disagree on the field name: newer builds project
|
|
17
|
+
* `provenance`, the 0.1.6-alpha generations project `producer`. Both are
|
|
18
|
+
* optional here and resolved below, so the row renders on either host instead
|
|
19
|
+
* of throwing on `undefined.role` and degrading its whole block boundary.
|
|
20
|
+
*/
|
|
21
|
+
provenance?: ContextMessageNode['provenance'] | null
|
|
22
|
+
/** Alpha-generation spelling of {@link provenance}. */
|
|
23
|
+
producer?: ContextMessageNode['provenance'] | null
|
|
15
24
|
/** Producer-declared information form; null renders the opaque body. */
|
|
16
25
|
form: ContextMessageNode['form']
|
|
17
26
|
/** The owning view's locale seat, passed down as a plain prop. */
|
|
@@ -29,8 +38,11 @@ export interface ContextInjectionRowProps {
|
|
|
29
38
|
* @param props - Durable content, its projected producer role/name and form, and the locale seat.
|
|
30
39
|
* @returns A collapsed context row with a bounded, form-specific body.
|
|
31
40
|
*/
|
|
32
|
-
export function ContextInjectionRow({ content, source, provenance, form, t }: ContextInjectionRowProps) {
|
|
41
|
+
export function ContextInjectionRow({ content, source, provenance, producer, form, t }: ContextInjectionRowProps) {
|
|
33
42
|
const [open, setOpen] = useState(false)
|
|
43
|
+
// Two host generations, one component. A row whose projection is absent
|
|
44
|
+
// entirely still renders: the marker falls back to a plain injection.
|
|
45
|
+
const view = provenance ?? producer ?? { role: 'inject' as const, label: null }
|
|
34
46
|
// Resolved rather than declared: a form whose fields are unreadable renders
|
|
35
47
|
// the opaque body, and the marker must say what the row actually shows.
|
|
36
48
|
const { rendered, summary, body } = contextBody(form, { content, source, t })
|
|
@@ -38,18 +50,18 @@ export function ContextInjectionRow({ content, source, provenance, form, t }: Co
|
|
|
38
50
|
return (
|
|
39
51
|
<DisclosureRow
|
|
40
52
|
className={css.root}
|
|
41
|
-
icon={
|
|
53
|
+
icon={view.role === 'recall'
|
|
42
54
|
? <span data-context-recall-icon><ReferenceIcon kind="session" /></span>
|
|
43
55
|
: <IconBrowseOutline16 size={14} />}
|
|
44
56
|
chevronClassName={css.chevron}
|
|
45
|
-
title={t(
|
|
46
|
-
collapsedContent={
|
|
57
|
+
title={t(view.role === 'recall' ? 'message.contextRecall' : 'message.contextInjection')}
|
|
58
|
+
collapsedContent={view.label === null ? undefined : (
|
|
47
59
|
/* ToolRow's separator shape: an aria-hidden dot, so the accessible name
|
|
48
60
|
stays the two readable parts and the two disclosure rows expose one
|
|
49
61
|
name shape. A source that names no producer drops the dot with it. */
|
|
50
62
|
<>
|
|
51
63
|
<span className={css.sep} aria-hidden />
|
|
52
|
-
<span className={css.source} data-context-source>{
|
|
64
|
+
<span className={css.source} data-context-source>{view.label}</span>
|
|
53
65
|
{summary !== null && (
|
|
54
66
|
<>
|
|
55
67
|
<span className={css.sep} aria-hidden />
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createElement, memo, useMemo, type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { SlotEntryDef, SlotMap, SlotSpec, StoredEntry } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RC2 composition adapter. Only public registry operations are used. We lend
|
|
8
|
+
* registrations a Reader-owned seat, not a second implementation of their UI.
|
|
9
|
+
* The platform still binds injection, hooks, locale, stores and boundaries.
|
|
10
|
+
* Original entries, declarations and components are never changed.
|
|
11
|
+
*
|
|
12
|
+
* Child seats have unique names because RC2 declarations have one owner. The
|
|
13
|
+
* sole component wrapper translates those names before delegating to the
|
|
14
|
+
* framework-provided renderSlot. It does not implement a slot renderer.
|
|
15
|
+
*/
|
|
16
|
+
export const OFFICIAL_SLOTS = {
|
|
17
|
+
actions: 'conversation.chat.assistant-actions',
|
|
18
|
+
tools: 'tool.call.toolview',
|
|
19
|
+
tail: 'conversation.chat.turnTail',
|
|
20
|
+
nodes: 'conversation.chat.node',
|
|
21
|
+
images: 'conversation.message.images',
|
|
22
|
+
} as const;
|
|
23
|
+
export type OfficialFamily = keyof typeof OFFICIAL_SLOTS;
|
|
24
|
+
export const OFFICIAL_SEATS = {
|
|
25
|
+
actions: 'dsh-better-display.official.actions/conversation.chat.assistant-actions',
|
|
26
|
+
tools: 'dsh-better-display.official.tools/tool.call.toolview',
|
|
27
|
+
tail: 'dsh-better-display.official.tail/conversation.chat.turnTail',
|
|
28
|
+
nodes: 'dsh-better-display.official.nodes/conversation.chat.node',
|
|
29
|
+
images: 'dsh-better-display.official.images/conversation.message.images',
|
|
30
|
+
} as const;
|
|
31
|
+
export type OfficialSeat = typeof OFFICIAL_SEATS[OfficialFamily];
|
|
32
|
+
export const officialSeat = (family: OfficialFamily, source: string = OFFICIAL_SLOTS[family]) =>
|
|
33
|
+
`dsh-better-display.official.${family}/${source}`;
|
|
34
|
+
|
|
35
|
+
type Spec = SlotSpec<SlotEntryDef>;
|
|
36
|
+
type Render = (key: string, owner: object, options?: object) => ReactNode;
|
|
37
|
+
type RenderProps = { renderSlot: Render; renderSlotChain?: Render } & Record<string, unknown>;
|
|
38
|
+
type Registration = StoredEntry['options'] & Omit<StoredEntry, 'options' | 'component'> & { name: string };
|
|
39
|
+
|
|
40
|
+
/** The documented, type-erased registry inspection/registration boundary. */
|
|
41
|
+
export interface CompositionRegistry {
|
|
42
|
+
spec(key: string): Spec | undefined;
|
|
43
|
+
entriesOfSlot(key: string): readonly StoredEntry[];
|
|
44
|
+
subscribe(key: string, listener: () => void): () => void;
|
|
45
|
+
inject(key: string, effect: () => () => void): () => void;
|
|
46
|
+
register(options: Registration, component: unknown): () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
50
|
+
interface SlotMap {
|
|
51
|
+
'dsh-better-display.official.actions/conversation.chat.assistant-actions': SlotMap['conversation.chat.assistant-actions'];
|
|
52
|
+
'dsh-better-display.official.tools/tool.call.toolview': SlotMap['tool.call.toolview'];
|
|
53
|
+
'dsh-better-display.official.tail/conversation.chat.turnTail': SlotMap['conversation.chat.turnTail'];
|
|
54
|
+
'dsh-better-display.official.nodes/conversation.chat.node': SlotMap['conversation.chat.node'];
|
|
55
|
+
'dsh-better-display.official.images/conversation.message.images': SlotMap['conversation.message.images'];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Existing Reader presentations own these rows. All other node kinds, including
|
|
60
|
+
// future registrations, reach the official renderer through the fallback seat.
|
|
61
|
+
const READER_NODES = new Set(['user', 'steering', 'assistant-step', 'tool-call', 'turn-tail', 'turn-process']);
|
|
62
|
+
const EXPECTED: Record<OfficialFamily, Spec> = {
|
|
63
|
+
actions: { kind: 'list', scope: 'session' },
|
|
64
|
+
tools: { kind: 'keyed', scope: 'session' },
|
|
65
|
+
tail: { kind: 'chain', scope: 'session' },
|
|
66
|
+
nodes: { kind: 'keyed', scope: 'session' },
|
|
67
|
+
images: { kind: 'single', scope: 'session' },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reader already presents produced paths with its copy/reveal actions. RC2's
|
|
72
|
+
* official tail additionally owns explicit `present` artifacts. Leave those
|
|
73
|
+
* cards, actions and state intact, and omit only the duplicate produced chips.
|
|
74
|
+
* This changes a presentation prop, never the source match or session data.
|
|
75
|
+
* Unrecognized shapes pass through intact for safe forward degradation.
|
|
76
|
+
*/
|
|
77
|
+
export function readerTailMatch(value: unknown, displayedPaths?: readonly string[]): unknown {
|
|
78
|
+
if (!value || typeof value !== 'object') return value;
|
|
79
|
+
const match = value as Record<string, unknown>;
|
|
80
|
+
const displayed = new Set(displayedPaths ?? []);
|
|
81
|
+
return displayed.size > 0 && Array.isArray(match.produced) && Array.isArray(match.presented)
|
|
82
|
+
? { ...match, produced: match.produced.filter(path => !displayed.has(path)) }
|
|
83
|
+
: value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function tailPresentation(component: unknown) {
|
|
87
|
+
const Original = component as ComponentType<Record<string, unknown>>;
|
|
88
|
+
return memo(function ReaderTailPresentation(props: Record<string, unknown>) {
|
|
89
|
+
const matched = useMemo(() => readerTailMatch(props.matched, props.readerProducedPaths as readonly string[] | undefined), [props.matched, props.readerProducedPaths]);
|
|
90
|
+
return createElement(Original, { ...props, matched });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function officialChildren(slots: Pick<CompositionRegistry, 'spec'>): { [K in OfficialSeat]: SlotSpec<SlotMap[K]> } {
|
|
95
|
+
return Object.fromEntries(Object.entries(OFFICIAL_SLOTS).map(([name, source]) => {
|
|
96
|
+
const family = name as OfficialFamily;
|
|
97
|
+
const spec = slots.spec(source);
|
|
98
|
+
if (spec && (spec.kind !== EXPECTED[family].kind || spec.scope !== EXPECTED[family].scope)) {
|
|
99
|
+
throw new Error(`Official slot contract changed: ${source} (${spec.kind}/${spec.scope})`);
|
|
100
|
+
}
|
|
101
|
+
// In particular, retain conversation.chat.node's contextual turn-data hook.
|
|
102
|
+
return [officialSeat(family), spec ?? EXPECTED[family]];
|
|
103
|
+
})) as { [K in OfficialSeat]: SlotSpec<SlotMap[K]> };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function translatedComponent(entry: StoredEntry, names: ReadonlyMap<string, string>) {
|
|
107
|
+
if (names.size === 0) return entry.component;
|
|
108
|
+
const Original = entry.component as ComponentType<Record<string, unknown>>;
|
|
109
|
+
return memo(function OfficialSlotChildren(props: RenderProps) {
|
|
110
|
+
const renders = useMemo(() => {
|
|
111
|
+
const translate = (render: Render | undefined): Render => (key, owner, options) => {
|
|
112
|
+
const seat = names.get(key);
|
|
113
|
+
if (!seat || !render) throw new Error(`Undeclared official child slot: ${key}`);
|
|
114
|
+
return render(seat, owner, options);
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
renderSlot: translate(props.renderSlot),
|
|
118
|
+
...(props.renderSlotChain ? { renderSlotChain: translate(props.renderSlotChain) } : {}),
|
|
119
|
+
};
|
|
120
|
+
}, [props.renderSlot, props.renderSlotChain]);
|
|
121
|
+
return createElement(Original, { ...props, ...renders });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Mirror one elected contribution set incrementally. Unrelated additions do not
|
|
127
|
+
* remount existing cards or recreate their subscriptions. A source unload/HMR
|
|
128
|
+
* disposes the corresponding subtree and closures before its replacement.
|
|
129
|
+
*/
|
|
130
|
+
export function mirrorOfficialSlot(
|
|
131
|
+
slots: CompositionRegistry,
|
|
132
|
+
source: string,
|
|
133
|
+
target: string,
|
|
134
|
+
namespace: string,
|
|
135
|
+
accept: (entry: StoredEntry) => boolean = () => true,
|
|
136
|
+
): () => void {
|
|
137
|
+
const mounted = new Map<StoredEntry, () => void>();
|
|
138
|
+
let stopped = false;
|
|
139
|
+
const reconcile = () => {
|
|
140
|
+
if (stopped) return;
|
|
141
|
+
const entries = slots.entriesOfSlot(source).filter(accept);
|
|
142
|
+
const wanted = new Set(entries);
|
|
143
|
+
for (const [entry, dispose] of mounted) {
|
|
144
|
+
if (wanted.has(entry)) continue;
|
|
145
|
+
dispose();
|
|
146
|
+
mounted.delete(entry);
|
|
147
|
+
}
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (mounted.has(entry)) continue;
|
|
150
|
+
const names = new Map(Object.keys(entry.children ?? {}).map(key => [key, `${namespace}/${key}`]));
|
|
151
|
+
const children = Object.fromEntries([...names].map(([key, seat]) => [seat, entry.children![key]]));
|
|
152
|
+
const disposers: (() => void)[] = [];
|
|
153
|
+
const dispose = () => { for (const stop of disposers.splice(0).reverse()) stop(); };
|
|
154
|
+
try {
|
|
155
|
+
const { component: _component, options, children: _children, ...metadata } = entry;
|
|
156
|
+
const presentation = source === OFFICIAL_SLOTS.tail && entry.locale === 'deliverables'
|
|
157
|
+
? { ...entry, component: tailPresentation(entry.component) }
|
|
158
|
+
: entry;
|
|
159
|
+
disposers.push(slots.register({
|
|
160
|
+
...options, ...metadata, name: target,
|
|
161
|
+
...(names.size ? { children } : {}),
|
|
162
|
+
registrant: `dsh-better-display → ${entry.registrant ?? source}`,
|
|
163
|
+
}, translatedComponent(presentation, names)));
|
|
164
|
+
for (const [key, seat] of names) {
|
|
165
|
+
disposers.push(mirrorOfficialSlot(slots, key, seat, namespace));
|
|
166
|
+
}
|
|
167
|
+
mounted.set(entry, dispose);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
dispose();
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const unsubscribe = slots.subscribe(source, reconcile);
|
|
175
|
+
const dispose = () => {
|
|
176
|
+
stopped = true;
|
|
177
|
+
unsubscribe();
|
|
178
|
+
for (const stop of [...mounted.values()].reverse()) stop();
|
|
179
|
+
mounted.clear();
|
|
180
|
+
};
|
|
181
|
+
try { reconcile(); } catch (error) { dispose(); throw error; }
|
|
182
|
+
return dispose;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function installOfficialSlots(ctx: Context): () => void {
|
|
186
|
+
// The SlotMap overloads are for statically declared keys. Aliases are a
|
|
187
|
+
// runtime composition, using exactly the public erased metadata contract.
|
|
188
|
+
const slots = ctx.slots as unknown as CompositionRegistry;
|
|
189
|
+
const disposers: (() => void)[] = [];
|
|
190
|
+
try {
|
|
191
|
+
for (const [name, source] of Object.entries(OFFICIAL_SLOTS)) {
|
|
192
|
+
const family = name as OfficialFamily;
|
|
193
|
+
disposers.push(slots.inject(source, () => {
|
|
194
|
+
const spec = slots.spec(source)!;
|
|
195
|
+
const target = officialSeat(family);
|
|
196
|
+
const declared = slots.spec(target);
|
|
197
|
+
if (spec.kind !== declared?.kind || spec.scope !== declared.scope) {
|
|
198
|
+
throw new Error(`Official slot contract changed: ${source}`);
|
|
199
|
+
}
|
|
200
|
+
return mirrorOfficialSlot(slots, source, target, `dsh-better-display.official.${family}`,
|
|
201
|
+
family === 'nodes' ? entry => !READER_NODES.has(entry.options.key ?? '') : undefined);
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
for (const stop of disposers.reverse()) stop();
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
return () => { for (const stop of disposers.splice(0).reverse()) stop(); };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type OfficialSlotKey = typeof OFFICIAL_SLOTS[OfficialFamily] & keyof SlotMap;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** RC2 Chat's same-origin local media contract. Authorization stays Host-side. */
|
|
2
|
+
export function localPathMediaUrl(protocol: string, origin: string, value: string): string | undefined {
|
|
3
|
+
if (protocol !== 'http:' && protocol !== 'https:') return undefined;
|
|
4
|
+
if (!value.startsWith('/') || value.startsWith('//')) return undefined;
|
|
5
|
+
return `${origin}/api/file?path=${encodeURIComponent(value)}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const readerPathImages = {
|
|
9
|
+
resolve: (value: string) => typeof window === 'undefined' ? undefined
|
|
10
|
+
: localPathMediaUrl(window.location.protocol, window.location.origin, value),
|
|
11
|
+
};
|
package/src/client/settings.tsx
CHANGED
|
@@ -38,7 +38,7 @@ function createSkillProbe(ctx: Context): SkillStatusProbe {
|
|
|
38
38
|
return await res.json() as HostSkillStatus;
|
|
39
39
|
},
|
|
40
40
|
listRemoteSkills: async () => {
|
|
41
|
-
const remote = ctx.remote as { skills?: RemoteSkillsFace } | undefined;
|
|
41
|
+
const remote = ctx.remote as unknown as { skills?: RemoteSkillsFace } | undefined;
|
|
42
42
|
const skills = remote?.skills
|
|
43
43
|
?? (ctx.get?.('remote.skills') as RemoteSkillsFace | undefined)
|
|
44
44
|
?? ((ctx.get?.('remote') as { skills?: RemoteSkillsFace } | undefined)?.skills);
|
package/src/client/store.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface ReaderState {
|
|
|
27
27
|
}
|
|
28
28
|
type ReaderActions = {
|
|
29
29
|
setExpanded: (draft: ReaderState, key: string, value: boolean) => void;
|
|
30
|
+
resetExpanded: (draft: ReaderState) => void;
|
|
30
31
|
setMotion: (draft: ReaderState, value: boolean) => void;
|
|
31
32
|
setAutoFold: (draft: ReaderState, value: boolean) => void;
|
|
32
33
|
setDeliverableOpenMode: (draft: ReaderState, value: DeliverableOpenMode) => void;
|
|
@@ -54,6 +55,7 @@ export function createReaderStore(): EngineStoreHandle<ReaderState, ReaderAction
|
|
|
54
55
|
persist: 'dsh.reader.v1',
|
|
55
56
|
actions: {
|
|
56
57
|
setExpanded: (draft, key: string, value: boolean) => { draft.expanded[key] = value; },
|
|
58
|
+
resetExpanded: draft => { draft.expanded = {}; },
|
|
57
59
|
setMotion: (draft, value: boolean) => { draft.motion = value; },
|
|
58
60
|
setAutoFold: (draft, value: boolean) => {
|
|
59
61
|
applyFoldIntensity(draft, value
|
|
@@ -5,7 +5,7 @@ import type { ReaderGroup } from './projection.js';
|
|
|
5
5
|
|
|
6
6
|
export type ToolDraft = Extract<AssistantBlock, { kind: 'tool-call' }>;
|
|
7
7
|
export type ToolPhase = 'preparing' | 'running' | 'returned' | 'succeeded' | 'failed' | 'interrupted';
|
|
8
|
-
export type ToolCategory = 'write' | 'read' | 'terminal' | 'search' | 'web' | 'other';
|
|
8
|
+
export type ToolCategory = 'write' | 'read' | 'terminal' | 'search' | 'web' | 'code' | 'other';
|
|
9
9
|
export interface ToolActivityEntry {
|
|
10
10
|
kind: 'tool'; key: string; callId: string; step: number; order: number;
|
|
11
11
|
draft?: ToolDraft; block?: ToolCallBlock;
|
|
@@ -275,12 +275,19 @@ export function activitySummary(entry: Pick<ToolActivityEntry, 'block' | 'draft'
|
|
|
275
275
|
: /^(read|read_file)$/.test(name) ? 'read'
|
|
276
276
|
: /^(bash|shell|terminal|terminal_send|exec_command|pwsh)$/.test(name) ? 'terminal'
|
|
277
277
|
: /^(grep|glob|find|search)$/.test(name) ? 'search'
|
|
278
|
-
: /^(web_search|web_fetch|web_open)$/.test(name) ? 'web'
|
|
278
|
+
: /^(web_search|web_fetch|web_open)$/.test(name) ? 'web'
|
|
279
|
+
// Code interpreters: no file, no shell, no query — the call is a program.
|
|
280
|
+
// Without this branch `run_code` fell through to `other`, which has no
|
|
281
|
+
// renderer of its own, so its result text was emitted as ordinary prose
|
|
282
|
+
// and stayed outside every fold.
|
|
283
|
+
: /^(run_code|execute_code|code_interpreter|python|node|eval|repl)$/.test(name) ? 'code'
|
|
284
|
+
: 'other';
|
|
279
285
|
const title = category === 'write' ? `${name === 'write' ? '写入' : '修改'}${file ? ` ${file}` : name === 'apply_patch' ? '代码补丁' : '文件'}`
|
|
280
286
|
: category === 'read' ? `读取${file ? ` ${file}` : '文件'}`
|
|
281
287
|
: category === 'terminal' ? description || '运行命令'
|
|
282
288
|
: category === 'search' ? name === 'glob' ? '查找文件' : '搜索内容'
|
|
283
289
|
: category === 'web' ? name === 'web_search' ? '搜索网页' : '读取网页'
|
|
290
|
+
: category === 'code' ? (description || '运行代码')
|
|
284
291
|
: name;
|
|
285
292
|
return { name, raw, args, category, title, target: target ?? command ?? stringValue(args, 'query', 'pattern', 'url'), command,
|
|
286
293
|
cwd: stringValue(args, 'workdir', 'cwd'), content: stringValue(args, 'content', 'new_string', 'newText', 'file_text') };
|
package/src/client/types.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type { ComponentType } from 'react';
|
|
2
1
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
3
|
-
import type { AssistantBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
2
|
+
import type { AssistantBlock, MessageImageLoader } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
4
3
|
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
5
4
|
import type { PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
|
|
6
5
|
import type {} from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
7
6
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
8
7
|
import type {} from '@deepseek-ai/dsh-client-ui-session/client';
|
|
9
8
|
import type { createReaderStore } from './store.js';
|
|
9
|
+
import type { OfficialSeat } from './official-slots.js';
|
|
10
|
+
import type { OpenFileOptions, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
11
|
+
import type {} from '@deepseek-ai/dsh-client-ui-tool/client';
|
|
10
12
|
|
|
11
13
|
export interface ReaderBlockOwner {
|
|
12
14
|
block: AssistantBlock;
|
|
@@ -31,7 +33,11 @@ export interface ReaderInjected {
|
|
|
31
33
|
*/
|
|
32
34
|
fillComposer: (text: string) => boolean;
|
|
33
35
|
/** Open a workspace file or directory; mode comes from the Better Display setting. */
|
|
34
|
-
openFile: (path: string) => Promise<void> | void;
|
|
36
|
+
openFile: (path: string, options?: OpenFileOptions) => Promise<void> | void;
|
|
37
|
+
officialImageLoader: MessageImageLoader;
|
|
38
|
+
officialFileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined;
|
|
39
|
+
officialPreviewFile: (path: string) => void;
|
|
40
|
+
officialHost: { getSnapshot: () => { home?: string }; subscribe: (listener: () => void) => () => void };
|
|
35
41
|
/** Root-scoped Better Display prefs (`dsh.reader.v1`), shared with Settings. */
|
|
36
42
|
openPrefs?: {
|
|
37
43
|
getSnapshot: () => {
|
|
@@ -55,16 +61,16 @@ export interface ReaderInjected {
|
|
|
55
61
|
forkAt?: (seq: number) => void;
|
|
56
62
|
/** Load session history through a target sequence number. */
|
|
57
63
|
loadThrough?: (seq: unknown) => Promise<void>;
|
|
58
|
-
/** Resolve a custom tool view registered in the `tool.call.toolview` slot (e.g. diff cards). */
|
|
59
|
-
getToolView?: (toolName: string) => ComponentType<any> | null;
|
|
60
64
|
}
|
|
61
65
|
export type ReaderProps = PropsRuntime<'conversation.view'>
|
|
62
66
|
& PropsLocale<'chat'>
|
|
63
|
-
& PropsRenderSlots<'dsh-better-display.block'>
|
|
67
|
+
& PropsRenderSlots<'dsh-better-display.block' | OfficialSeat>
|
|
64
68
|
& PropsStore<ReturnType<typeof createReaderStore>>
|
|
65
69
|
& ReaderInjected;
|
|
66
|
-
export type BlockRenderProps = Pick<ReaderProps, 'renderSlotChain' | 'loadImage' | 'fillComposer'
|
|
67
|
-
|
|
70
|
+
export type BlockRenderProps = Pick<ReaderProps, 'renderSlotChain' | 'loadImage' | 'fillComposer'> & {
|
|
71
|
+
official?: Pick<ReaderProps, 'renderSlot' | 'renderSlotChain' | 'officialImageLoader' | 'officialFileMentions' | 'officialPreviewFile' | 'officialHost' | 'openView'>;
|
|
72
|
+
cwd?: string;
|
|
73
|
+
openFile?: (path: string, options?: OpenFileOptions) => Promise<void> | void;
|
|
68
74
|
revealFile?: (path: string) => Promise<void> | void;
|
|
69
75
|
forkAt?: (seq: number) => void;
|
|
70
76
|
/** Durable closing-message seq of this turn (turn-tail closing), used as the fork anchor. */
|
|
@@ -2,6 +2,7 @@ import { createContext, useContext, useLayoutEffect, useMemo, useRef } from 'rea
|
|
|
2
2
|
import type { ReactNode, RefObject } from 'react';
|
|
3
3
|
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import { MarkdownText } from './markdown/MarkdownText.js';
|
|
5
|
+
import { readerPathImages } from './platform-media.js';
|
|
5
6
|
import { WORD_MOTION, WordTimeline } from './word-timeline.js';
|
|
6
7
|
import css from './Reader.module.css';
|
|
7
8
|
|
|
@@ -86,6 +87,6 @@ export function MotionMarkdown({ text, streaming, enabled, revision, fileMention
|
|
|
86
87
|
return current.hasLiveText ? <MotionAtom born={current.bornAt(offset)} generation={current.generation} offset={offset}>{children}</MotionAtom> : children;
|
|
87
88
|
}, []);
|
|
88
89
|
return <WordScope.Provider value={scope}>
|
|
89
|
-
<MarkdownText text={text} streaming={streaming} codeLabels={CODE_LABELS} fileMentions={fileMentions} renderText={renderText} renderAtom={renderAtom} />
|
|
90
|
+
<MarkdownText text={text} streaming={streaming} codeLabels={CODE_LABELS} fileMentions={fileMentions} pathImages={readerPathImages} renderText={renderText} renderAtom={renderAtom} />
|
|
90
91
|
</WordScope.Provider>;
|
|
91
92
|
}
|