dsh-better-display 0.2.1 → 0.3.2
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 +24 -0
- package/README.en.md +5 -3
- package/README.md +5 -3
- package/compat/harness-rc2.json +97 -0
- package/docs/official-rendering-bridge.md +65 -0
- package/lib/client.js +6476 -5977
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +4 -2
- 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 +53 -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 +42 -11
- 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 +23 -19
- 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 +7 -4
- package/lib/types/client/native/ContextInjectionRow.js.map +1 -1
- package/lib/types/client/native/ReferenceIcon.js +3 -3
- package/lib/types/client/native/ReferenceIcon.js.map +1 -1
- package/lib/types/client/native/tool-call-model.js +1 -1
- package/lib/types/client/native/tool-call-model.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/primitive-labels.d.ts.map +1 -1
- package/lib/types/client/primitive-labels.js +3 -2
- package/lib/types/client/primitive-labels.js.map +1 -1
- 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 +11 -4
- 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 +23 -17
- 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 +121 -0
- package/src/client/Blocks.tsx +2 -1
- package/src/client/OfficialContent.tsx +69 -0
- package/src/client/Reader.module.css +35 -8
- package/src/client/Reader.tsx +44 -17
- package/src/client/ToolActivity.tsx +24 -29
- 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 +21 -9
- package/src/client/native/ReferenceIcon.tsx +3 -3
- package/src/client/native/tool-call-model.ts +1 -1
- package/src/client/official-slots.tsx +211 -0
- package/src/client/platform-media.ts +11 -0
- package/src/client/primitive-labels.ts +3 -2
- package/src/client/settings.tsx +1 -1
- package/src/client/store.ts +2 -0
- package/src/client/tool-activity.ts +11 -4
- 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 +152 -0
- package/tests/official-slots.test.ts +112 -0
- package/tests/platform-media.test.ts +17 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import type { ContextMessageNode } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
3
3
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
4
|
-
import { DisclosureRow,
|
|
4
|
+
import { DisclosureRow, IconBrowseOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
5
5
|
import { ReferenceIcon } from './ReferenceIcon.js'
|
|
6
6
|
import { contextBody } from './ContextBody.js'
|
|
7
7
|
import css from './ContextInjectionRow.module.css'
|
|
@@ -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['producer'] | null
|
|
22
|
+
/** Alpha-generation spelling of {@link provenance}. */
|
|
23
|
+
producer?: ContextMessageNode['producer'] | 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
|
+
: <IconBrowseOutlineRegular 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 />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
IconBrowseOutlineRegular, IconFolderCloseRegular,
|
|
4
4
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
5
5
|
|
|
6
6
|
/** Reference domains with distinct composer and transcript glyphs. */
|
|
@@ -29,7 +29,7 @@ export function ReferenceIcon({ kind, size = 16, className }: ReferenceIconProps
|
|
|
29
29
|
/>
|
|
30
30
|
</svg>
|
|
31
31
|
)
|
|
32
|
-
case 'file': return <
|
|
33
|
-
case 'folder': return <
|
|
32
|
+
case 'file': return <IconBrowseOutlineRegular size={size} className={className} />
|
|
33
|
+
case 'folder': return <IconFolderCloseRegular size={size} className={className} />
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -217,7 +217,7 @@ function deriveBody(variant: ToolRowVariant, argsRaw: string): string | null {
|
|
|
217
217
|
export function toolRowModel(toolName: string, block: ToolCallBlock, cwd?: string, home?: string): ToolRowModel {
|
|
218
218
|
const variant = classifyTool(toolName)
|
|
219
219
|
const done = 'kind' in block
|
|
220
|
-
const argsRaw = (done ? block.call?.argsRaw : block.argsRaw) ?? ''
|
|
220
|
+
const argsRaw = (done ? block.call?.argsRaw : ('argsRaw' in block ? block.argsRaw : '')) ?? ''
|
|
221
221
|
const state: ToolRowState = !done ? 'running'
|
|
222
222
|
: block.error?.code === 'interrupted' ? 'stopped'
|
|
223
223
|
: block.isError ? 'error' : 'ok'
|
|
@@ -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: 'list', 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
|
+
};
|
|
@@ -14,22 +14,23 @@ export const markdownLabels: MarkdownLabels = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export const readBlockLabels: ReadBlockLabels = {
|
|
17
|
+
codeLabel: '代码', wrapLabel: '自动换行', unwrapLabel: '不换行',
|
|
17
18
|
window: (shown, total) => `显示 ${shown} / ${total} 行`,
|
|
18
19
|
copy: '复制', copied: '已复制', collapseAria: '收起文件内容',
|
|
19
20
|
expandAria: hidden => `展开其余 ${hidden} 行`, collapse: '收起', expand: hidden => `展开其余 ${hidden} 行`,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export const terminalBlockLabels: TerminalBlockLabels = {
|
|
23
|
-
signal: signal => `信号 ${signal}`, exitCode: code => `退出码 ${code}`,
|
|
24
|
+
signal: signal => `信号 ${signal}`, exitCode: code => `退出码 ${code}`, noExitCode: '无退出码',
|
|
24
25
|
running: '执行中', failed: '失败', done: '已完成', copy: '复制', copied: '已复制',
|
|
25
26
|
noOutput: '没有输出', collapseAria: '收起命令输出', collapse: '收起',
|
|
26
27
|
expandAria: hidden => `展开其余 ${hidden} 行`, expand: hidden => `展开其余 ${hidden} 行`,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
export const diffBlockLabels: DiffBlockLabels = {
|
|
31
|
+
codeLabel: '代码', wrapLabel: '自动换行', unwrapLabel: '不换行',
|
|
30
32
|
copy: '复制', copied: '已复制', collapseAria: '收起差异', collapse: '收起',
|
|
31
33
|
expandAria: hidden => `展开其余 ${hidden} 行`, expand: hidden => `展开其余 ${hidden} 行`,
|
|
32
|
-
files: count => `${count} 个文件`,
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
export const searchBlockLabels: SearchBlockLabels = {
|
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;
|
|
@@ -95,13 +95,13 @@ export function toolIdentity(entry: Pick<ToolActivityEntry, 'block' | 'draft'>)
|
|
|
95
95
|
const block = entry.block;
|
|
96
96
|
return {
|
|
97
97
|
name: block ? 'kind' in block ? block.call?.name ?? entry.draft?.name ?? '工具调用' : block.name : entry.draft?.name ?? '工具调用',
|
|
98
|
-
raw: block ? 'kind' in block ? block.call?.argsRaw ?? entry.draft?.argsRaw ?? '' : block.argsRaw : entry.draft?.argsRaw ?? '',
|
|
98
|
+
raw: block ? 'kind' in block ? block.call?.argsRaw ?? entry.draft?.argsRaw ?? '' : ('argsRaw' in block ? block.argsRaw : '') : entry.draft?.argsRaw ?? '',
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** Name and raw arguments of any call block, whether it has landed or is pending. */
|
|
103
103
|
function childIdentity(block: ToolCallBlock): { name?: string; raw: string } {
|
|
104
|
-
if (!block || !('kind' in block)) return { name: block?.name, raw: block
|
|
104
|
+
if (!block || !('kind' in block)) return { name: block?.name, raw: block && 'argsRaw' in block ? block.argsRaw : '' };
|
|
105
105
|
return { name: block.call?.name, raw: block.call?.argsRaw ?? '' };
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useSyncExternalStore } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import { Reader } from '../src/client/Reader.js';
|
|
5
|
+
import { createReaderStore } from '../src/client/store.js';
|
|
6
|
+
import type { ReaderProps } from '../src/client/types.js';
|
|
7
|
+
|
|
8
|
+
// Real Reader and real root/session stores; only conversation data is synthetic.
|
|
9
|
+
const store = createReaderStore();
|
|
10
|
+
const prefs = store.create();
|
|
11
|
+
const session = store.create('auto-fold-fixture');
|
|
12
|
+
const nodes = new Map();
|
|
13
|
+
const turns = new Map();
|
|
14
|
+
for (const [number, closed] of [[1, true], [2, false]] as const) {
|
|
15
|
+
const turn = {
|
|
16
|
+
turn: number, status: closed ? 'closed' : 'open',
|
|
17
|
+
start: { seq: number * 10, time: 1 },
|
|
18
|
+
end: closed ? { time: 1001, data: { reason: { kind: 'completed' } } } : undefined,
|
|
19
|
+
data: new Map(closed ? [['turn-tail', { closing: { step: 1 } }]] : []),
|
|
20
|
+
steps: [] as any[],
|
|
21
|
+
};
|
|
22
|
+
for (let step = 0; step < 2; step++) {
|
|
23
|
+
const key = `turn-${number}-step-${step}`;
|
|
24
|
+
const data = { turn: number, step, status: closed || step === 0 ? 'settled' : 'running', time: 1,
|
|
25
|
+
blocks: [
|
|
26
|
+
{ kind: 'reasoning', text: closed ? `已完成思考 ${step}` : `进行中思考 ${step}` },
|
|
27
|
+
{ kind: 'text', text: closed && step === 1 ? '最终回答始终保留' : `过程说明 ${number}-${step}` },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
const location = { step, start: { seq: number * 10 + step }, data: new Map([['assistant-step', data]]) };
|
|
31
|
+
turn.steps.push(location);
|
|
32
|
+
nodes.set(key, { key, kind: 'assistant-step', visibility: 'visible', anchorSeq: number * 10 + step,
|
|
33
|
+
location: { kind: 'step', turn, step: location }, data });
|
|
34
|
+
}
|
|
35
|
+
turns.set(number, turn);
|
|
36
|
+
}
|
|
37
|
+
const chat = { order: [...nodes.keys()], nodes, timeline: { turns } } as unknown as ChatSnapshot;
|
|
38
|
+
const sessionSnapshot = { running: true, openState: 'ready', pendingSubmissions: [], hasMore: false, loadingOlder: false };
|
|
39
|
+
const sessionsSnapshot = { byId: { 'auto-fold-fixture': { cwd: '/fixture' } } };
|
|
40
|
+
const pending = new Map();
|
|
41
|
+
const host = { home: '/fixture' };
|
|
42
|
+
const useReaderStore: ReaderProps['useStore'] = selector => selector(useSyncExternalStore(session.subscribe, session.getSnapshot));
|
|
43
|
+
const props = {
|
|
44
|
+
sessionId: 'auto-fold-fixture', useChat: selector => selector(chat),
|
|
45
|
+
useSession: selector => selector(sessionSnapshot), useSessions: selector => selector(sessionsSnapshot),
|
|
46
|
+
useSessionStatus: selector => selector(new Map([['auto-fold-fixture', { running: true, pendingInteraction: pending.get('auto-fold-fixture'), completionUnread: false }]])),
|
|
47
|
+
useStore: useReaderStore, actions: session.actions, openPrefs: prefs,
|
|
48
|
+
t: key => key, renderSlot: (_name, _owner, options) => options?.fallback ?? null,
|
|
49
|
+
renderSlotChain: (_name, _owner, options) => options?.fallback ?? null,
|
|
50
|
+
loadImage: async () => ({ data: new Uint8Array(), mediaType: 'image/png' }),
|
|
51
|
+
officialImageLoader: Object.assign(async () => null, { peek: () => null }),
|
|
52
|
+
officialFileMentions: () => undefined, officialPreviewFile: () => {},
|
|
53
|
+
officialHost: { getSnapshot: () => host, subscribe: () => () => {} },
|
|
54
|
+
fillComposer: () => true, openFile: () => {}, loadOlder: async () => {}, openView: () => {},
|
|
55
|
+
} as ReaderProps;
|
|
56
|
+
|
|
57
|
+
Object.assign(window, { autoFoldFixture: {
|
|
58
|
+
// Exercise Settings' root preference path without recreating the Reader.
|
|
59
|
+
preference: (value: boolean) => prefs.actions.setAutoFold(value),
|
|
60
|
+
state: () => session.getSnapshot(),
|
|
61
|
+
} });
|
|
62
|
+
createRoot(document.getElementById('app')!).render(
|
|
63
|
+
<div data-conversation-scroll style={{ height: '100vh', overflow: 'auto' }}><Reader {...props} /></div>,
|
|
64
|
+
);
|