dsh-better-display 0.3.8 → 0.3.9
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/DESIGN.md +43 -3
- package/README.en.md +9 -1
- package/README.md +9 -1
- package/docs/dependency-update-policy.md +9 -0
- package/docs/extension-points.md +172 -0
- package/docs/official-rendering-bridge.md +48 -19
- package/docs/openapi.yaml +147 -0
- package/docs/runbooks/blank-reader.md +39 -0
- package/lib/client.js +1417 -490
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +378 -144
- package/lib/types/client/AssistantNode.d.ts +20 -0
- package/lib/types/client/AssistantNode.d.ts.map +1 -0
- package/lib/types/client/AssistantNode.js +46 -0
- package/lib/types/client/AssistantNode.js.map +1 -0
- package/lib/types/client/Blocks.d.ts.map +1 -1
- package/lib/types/client/Blocks.js +33 -10
- package/lib/types/client/Blocks.js.map +1 -1
- package/lib/types/client/ChoreographedFlow.d.ts.map +1 -1
- package/lib/types/client/ChoreographedFlow.js +2 -0
- package/lib/types/client/ChoreographedFlow.js.map +1 -1
- package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -1
- package/lib/types/client/ClosedProcessSummary.js +3 -1
- package/lib/types/client/ClosedProcessSummary.js.map +1 -1
- package/lib/types/client/McpAppFrame.d.ts.map +1 -1
- package/lib/types/client/McpAppFrame.js +5 -11
- package/lib/types/client/McpAppFrame.js.map +1 -1
- package/lib/types/client/OfficialContent.d.ts +17 -3
- package/lib/types/client/OfficialContent.d.ts.map +1 -1
- package/lib/types/client/OfficialContent.js +33 -7
- package/lib/types/client/OfficialContent.js.map +1 -1
- package/lib/types/client/Reader.d.ts.map +1 -1
- package/lib/types/client/Reader.js +43 -71
- package/lib/types/client/Reader.js.map +1 -1
- package/lib/types/client/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/SettingsSection.js +0 -5
- package/lib/types/client/SettingsSection.js.map +1 -1
- package/lib/types/client/StickyLane.d.ts +12 -0
- package/lib/types/client/StickyLane.d.ts.map +1 -1
- package/lib/types/client/StickyLane.js +78 -2
- package/lib/types/client/StickyLane.js.map +1 -1
- package/lib/types/client/ToolActivity.js.map +1 -1
- package/lib/types/client/index.d.ts +3 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +6 -0
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/live-turn.d.ts +17 -5
- package/lib/types/client/live-turn.d.ts.map +1 -1
- package/lib/types/client/live-turn.js +106 -76
- package/lib/types/client/live-turn.js.map +1 -1
- package/lib/types/client/motion.d.ts +26 -2
- package/lib/types/client/motion.d.ts.map +1 -1
- package/lib/types/client/motion.js +136 -10
- package/lib/types/client/motion.js.map +1 -1
- package/lib/types/client/official-slots.d.ts +9 -0
- package/lib/types/client/official-slots.d.ts.map +1 -1
- package/lib/types/client/official-slots.js +22 -7
- package/lib/types/client/official-slots.js.map +1 -1
- package/lib/types/client/projection.d.ts +15 -2
- package/lib/types/client/projection.d.ts.map +1 -1
- package/lib/types/client/projection.js +16 -4
- package/lib/types/client/projection.js.map +1 -1
- package/lib/types/client/rewind-cut.d.ts +131 -0
- package/lib/types/client/rewind-cut.d.ts.map +1 -0
- package/lib/types/client/rewind-cut.js +321 -0
- package/lib/types/client/rewind-cut.js.map +1 -0
- package/lib/types/client/text-transform.d.ts +93 -0
- package/lib/types/client/text-transform.d.ts.map +1 -0
- package/lib/types/client/text-transform.js +183 -0
- package/lib/types/client/text-transform.js.map +1 -0
- package/lib/types/client/timeline.d.ts.map +1 -1
- package/lib/types/client/timeline.js.map +1 -1
- package/lib/types/client/tool-activity.d.ts +9 -2
- package/lib/types/client/tool-activity.d.ts.map +1 -1
- package/lib/types/client/tool-activity.js +42 -2
- package/lib/types/client/tool-activity.js.map +1 -1
- package/lib/types/client/turn-slots.d.ts +29 -0
- package/lib/types/client/turn-slots.d.ts.map +1 -0
- package/lib/types/client/turn-slots.js +62 -0
- package/lib/types/client/turn-slots.js.map +1 -0
- package/lib/types/client/types.d.ts +18 -0
- package/lib/types/client/types.d.ts.map +1 -1
- package/lib/types/dsh-better-display.d.ts +4 -1
- package/lib/types/dsh-better-display.d.ts.map +1 -1
- package/lib/types/dsh-better-display.js +56 -7
- package/lib/types/dsh-better-display.js.map +1 -1
- package/lib/types/flags.d.ts +7 -0
- package/lib/types/flags.d.ts.map +1 -0
- package/lib/types/flags.js +37 -0
- package/lib/types/flags.js.map +1 -0
- package/lib/types/log.d.ts +17 -0
- package/lib/types/log.d.ts.map +1 -0
- package/lib/types/log.js +122 -0
- package/lib/types/log.js.map +1 -0
- package/lib/types/metrics.d.ts +18 -0
- package/lib/types/metrics.d.ts.map +1 -0
- package/lib/types/metrics.js +35 -0
- package/lib/types/metrics.js.map +1 -0
- package/package.json +36 -5
- package/scripts/changelog.mjs +20 -0
- package/scripts/check-agents-md.mjs +67 -0
- package/scripts/check-bundle-size.mjs +38 -0
- package/scripts/check-dead-flags.mjs +55 -0
- package/scripts/check-todo-debt.mjs +69 -0
- package/scripts/security-audit-report.mjs +92 -0
- package/scripts/verify-commentary-visible.mjs +627 -0
- package/src/client/AssistantNode.tsx +72 -0
- package/src/client/Blocks.tsx +49 -10
- package/src/client/ChoreographedFlow.tsx +3 -1
- package/src/client/ClosedProcessSummary.tsx +3 -1
- package/src/client/McpAppFrame.tsx +5 -13
- package/src/client/OfficialContent.tsx +47 -9
- package/src/client/Reader.module.css +68 -18
- package/src/client/Reader.tsx +36 -76
- package/src/client/SettingsSection.tsx +1 -7
- package/src/client/StickyLane.tsx +81 -2
- package/src/client/ToolActivity.tsx +1 -1
- package/src/client/index.tsx +8 -1
- package/src/client/live-turn.ts +100 -69
- package/src/client/motion.tsx +135 -7
- package/src/client/official-slots.tsx +23 -7
- package/src/client/projection.ts +16 -4
- package/src/client/rewind-cut.ts +340 -0
- package/src/client/text-transform.ts +265 -0
- package/src/client/timeline.ts +0 -2
- package/src/client/tool-activity.ts +49 -2
- package/src/client/turn-slots.ts +80 -0
- package/src/client/types.ts +19 -0
- package/src/dsh-better-display.ts +58 -7
- package/src/flags.ts +40 -0
- package/src/log.ts +137 -0
- package/src/metrics.ts +50 -0
- package/tests/browser-commentary.tsx +191 -0
- package/tests/chat-flow-hook.test.ts +1 -1
- package/tests/cloud-gate.test.ts +1 -1
- package/tests/context-tool-update.test.ts +1 -1
- package/tests/conversation-scroll.test.ts +1 -1
- package/tests/deliverables.test.ts +1 -1
- package/tests/entry.test.ts +1 -1
- package/tests/fixtures/reader-lane-owners.tsx +46 -0
- package/tests/fixtures/reader-official-users.tsx +64 -0
- package/tests/fixtures/reader-theme-overlap.html +80 -0
- package/tests/fixtures/reader-user-bubble.tsx +222 -0
- package/tests/flags.test.ts +66 -0
- package/tests/fold-choreography.test.ts +32 -8
- package/tests/fold-intensity.test.ts +11 -6
- package/tests/footer-precedence.test.ts +1 -1
- package/tests/history-word-regression.test.ts +1 -1
- package/tests/live-turn.test.ts +125 -54
- package/tests/log.test.ts +99 -0
- package/tests/mcp-apps.test.ts +1 -1
- package/tests/message-chrome.test.ts +1 -1
- package/tests/metrics.test.ts +52 -0
- package/tests/official-slots.test.ts +41 -2
- package/tests/open-file.test.ts +1 -1
- package/tests/pending-submission.test.ts +12 -1
- package/tests/pinned-selection.test.ts +16 -0
- package/tests/platform-media.test.ts +1 -1
- package/tests/projection.test.ts +66 -3
- package/tests/reader-glass-css.test.ts +1 -1
- package/tests/reader-official-users.test.ts +11 -1
- package/tests/reader-sticky-surface.test.ts +377 -0
- package/tests/reader-theme-lanes.test.ts +54 -0
- package/tests/reader-theme-overlap.test.ts +151 -0
- package/tests/reader-user-bubble.test.ts +137 -0
- package/tests/reasoning-follow.test.ts +1 -1
- package/tests/retiring-content.test.ts +98 -6
- package/tests/reveal.test.ts +3 -3
- package/tests/rewind-cut.test.ts +1090 -0
- package/tests/settings-copy.test.ts +1 -1
- package/tests/skill-status.test.ts +1 -1
- package/tests/stock-install.test.ts +1 -1
- package/tests/stream-buffer.test.ts +1 -1
- package/tests/styles.test.ts +30 -3
- package/tests/terminal-input-preview.test.ts +1 -1
- package/tests/text-transform.test.ts +272 -0
- package/tests/timeline.test.ts +1 -1
- package/tests/tool-clock.test.ts +1 -1
- package/tests/tool-diff.test.ts +1 -1
- package/tests/waiting-clock.test.ts +1 -1
- package/tests/word-timeline.test.ts +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Fragment, memo } from 'react';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
import type { ChatConversationViewNode, ChatNode, ChatNodeKind } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import { Blocks, CopyAnswer } from './Blocks.js';
|
|
5
|
+
import { ReasoningCard } from './ReasoningCard.js';
|
|
6
|
+
import { OfficialActions, OfficialAssistantBody } from './OfficialContent.js';
|
|
7
|
+
import { Disclosure, ProcessFragment } from './motion.js';
|
|
8
|
+
import { FoldSummaryText } from './LiveFold.js';
|
|
9
|
+
import { assistantSegments, forkAnchorSeq, hasVisibleBody, isEarlierNarration } from './projection.js';
|
|
10
|
+
import type { TurnBoundary } from './projection.js';
|
|
11
|
+
import type { BlockRenderProps, ReaderProps } from './types.js';
|
|
12
|
+
import css from './Reader.module.css';
|
|
13
|
+
|
|
14
|
+
function isNode<K extends ChatNodeKind>(node: ChatConversationViewNode, kind: K): node is ChatNode<K> {
|
|
15
|
+
return node.kind === kind;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat' | 'sessionId'> & {
|
|
19
|
+
nodeKey: string; boundary: TurnBoundary; pinned?: boolean; processOpen?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const AssistantNode = memo(function AssistantNode({ useChat, nodeKey, boundary, processOpen = false, pinned = false, folded = false, partStart, seamSummary, seamAfter = false, motion, onRead, returnFocusTo, ...render }: SeatProps & {
|
|
23
|
+
motion: boolean; onRead: () => void; returnFocusTo: RefObject<HTMLButtonElement>; partStart?: number; folded?: boolean;
|
|
24
|
+
seamSummary?: string; seamAfter?: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
const node = useChat(snapshot => snapshot.nodes.get(nodeKey));
|
|
27
|
+
if (!node || node.visibility === 'hidden' || !isNode(node, 'assistant-step')) return null;
|
|
28
|
+
const data = node.data;
|
|
29
|
+
const parts = assistantSegments(data.blocks);
|
|
30
|
+
const earlier = isEarlierNarration(data, boundary);
|
|
31
|
+
const hasToolCalls = data.blocks.some(block => block.kind === 'tool-call');
|
|
32
|
+
const isProcessStep = earlier || folded || hasToolCalls || (boundary.latestStep > 0 && data.step < boundary.latestStep);
|
|
33
|
+
const body = data.blocks.filter(block => block.kind !== 'reasoning' && block.kind !== 'tool-call');
|
|
34
|
+
const visible = partStart === undefined ? parts : parts.filter(part => part.start === partStart);
|
|
35
|
+
const bodyProps = { streaming: data.status === 'running', holdFormatting: pinned, startedAt: data.time, interrupted: data.status === 'interrupted', liveText: true as const };
|
|
36
|
+
return <>{visible.map(part => {
|
|
37
|
+
const index = parts.findIndex(item => item.start === part.start);
|
|
38
|
+
const last = index === parts.length - 1;
|
|
39
|
+
if (part.kind === 'reasoning') return <ProcessFragment key={part.start} open={processOpen} motion={motion} onRead={onRead} returnFocusTo={returnFocusTo} nodeKey={nodeKey} framed>
|
|
40
|
+
<ReasoningCard step={data.step} active={processOpen && boundary.status === 'open' && data.step === boundary.latestStep} motion={motion} selected={pinned} onRead={onRead}>
|
|
41
|
+
<Blocks {...render} blocks={part.blocks} streaming={data.status === 'running' && last && data.blocks.at(-1)?.kind === 'reasoning'}
|
|
42
|
+
holdFormatting={pinned} startedAt={data.time} interrupted={data.status === 'interrupted'} liveText />
|
|
43
|
+
</ReasoningCard>
|
|
44
|
+
</ProcessFragment>;
|
|
45
|
+
if (!hasVisibleBody(part.blocks)) return null;
|
|
46
|
+
// Narration stays body prose and never enters a disclosure. The seam in
|
|
47
|
+
// the gap summarises folded process between two body blocks. A foreign
|
|
48
|
+
// assistant-step renderer receives these original blocks; Reader markdown,
|
|
49
|
+
// including the text transform, is only the fallback.
|
|
50
|
+
const isFinalAnswer = !isProcessStep;
|
|
51
|
+
return <Fragment key={part.start}>
|
|
52
|
+
{seamSummary && <div className={css.processSeamRow} data-reader-process-seam data-reader-process-seam-summary={seamSummary}>
|
|
53
|
+
<Disclosure open={false} onChange={() => onRead()} ariaLabel="被折叠的过程"
|
|
54
|
+
label={<FoldSummaryText summary={seamSummary} motion={false} />} />
|
|
55
|
+
</div>}
|
|
56
|
+
<article className={css.answer} data-reader-body data-reader-anchor data-reader-key={nodeKey} data-reader-source-start={part.start}
|
|
57
|
+
data-reader-seam-after={seamAfter || undefined} data-answer-status={data.status} data-answer-phase={isFinalAnswer ? 'body' : 'process'}>
|
|
58
|
+
<OfficialAssistantBody {...render} node={node} blocks={part.blocks} {...bodyProps} />
|
|
59
|
+
{isFinalAnswer && last && data.status === 'interrupted' && <span className={css.stopped}>已停止</span>}
|
|
60
|
+
{isFinalAnswer && last && data.status !== 'running' && boundary.status === 'closed' && (
|
|
61
|
+
<CopyAnswer blocks={body} onFork={(() => {
|
|
62
|
+
// The fork anchor must be the durable closing message seq (same as
|
|
63
|
+
// the official turn-tail branch). AssistantChatData carries no seq
|
|
64
|
+
// of its own; passing it would fork the whole session instead.
|
|
65
|
+
const anchor = forkAnchorSeq([data.finalNode, { seq: render.forkSeq }]);
|
|
66
|
+
return render.forkAt && anchor !== undefined ? () => render.forkAt!(anchor) : undefined;
|
|
67
|
+
})()} metrics={render.metrics} extraActions={<OfficialActions official={render.official} messageId={data.finalNode?.messageId} />} />
|
|
68
|
+
)}
|
|
69
|
+
</article>
|
|
70
|
+
</Fragment>;
|
|
71
|
+
})}</>;
|
|
72
|
+
});
|
package/src/client/Blocks.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, Fragment, memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
1
|
+
import { Component, Fragment, memo, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
4
4
|
import type { AssistantBlock, UserMessageNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
@@ -9,6 +9,8 @@ import { markdownLabels, truncatedJsonLabel } from './primitive-labels.js';
|
|
|
9
9
|
import type { BlockRenderProps, ReaderBlockOwner } from './types.js';
|
|
10
10
|
import { useStreamingText } from './streaming.js';
|
|
11
11
|
import { MotionMarkdown, MotionPlainText } from './word-motion.js';
|
|
12
|
+
import { activeTextTransform, displayStreaming, originalAnswerText, readerBlockOwner, useDisplayText } from './text-transform.js';
|
|
13
|
+
import type { TextTransformRegistry } from './text-transform.js';
|
|
12
14
|
import css from './Reader.module.css';
|
|
13
15
|
|
|
14
16
|
const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
|
|
@@ -82,7 +84,16 @@ export function contentBlocks(content: UserMessageNode['content']): AssistantBlo
|
|
|
82
84
|
|
|
83
85
|
type TextPresentation = { startedAt?: number; interrupted?: boolean; liveText?: boolean };
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
type MarkdownStreamProps = {
|
|
88
|
+
text: string;
|
|
89
|
+
streaming: boolean;
|
|
90
|
+
holdFormatting: boolean;
|
|
91
|
+
kind?: 'body' | 'reasoning';
|
|
92
|
+
fileMentions?: BlockRenderProps['fileMentions'];
|
|
93
|
+
transformed?: boolean;
|
|
94
|
+
} & TextPresentation;
|
|
95
|
+
|
|
96
|
+
function MarkdownStream({ text, streaming, holdFormatting, startedAt, interrupted = false, liveText = false, kind = 'body', fileMentions, transformed = false }: MarkdownStreamProps) {
|
|
86
97
|
const root = useRef<HTMLDivElement>(null);
|
|
87
98
|
const presentation = useStreamingText(text, streaming, { startedAt, interrupted: interrupted || !liveText, selected: holdFormatting });
|
|
88
99
|
// Native Markdown changes block keys for its full final parse. Keep the last
|
|
@@ -92,24 +103,50 @@ function ReadingMarkdown({ text, streaming, holdFormatting, startedAt, interrupt
|
|
|
92
103
|
const effectiveMode = holdFormatting ? committedMode.current : presentation.formatStreaming;
|
|
93
104
|
useLayoutEffect(() => { committedMode.current = effectiveMode; }, [effectiveMode]);
|
|
94
105
|
return <div ref={root} className={css.readingText} data-reader-text data-reader-text-kind={kind} data-received-length={text.length} data-shown-length={presentation.text.length}
|
|
95
|
-
data-presentation-pending={presentation.pending || undefined} data-motion-style="opacity-blur" data-ud-motion="reader-text-arrival" data-ud-motion-type="reveal" data-ud-motion-no-flash="true"
|
|
106
|
+
data-presentation-pending={presentation.pending || undefined} data-motion-style="opacity-blur" data-ud-motion="reader-text-arrival" data-ud-motion-type="reveal" data-ud-motion-no-flash="true"
|
|
107
|
+
data-text-transform={transformed ? (streaming ? 'live' : 'final') : undefined}>
|
|
96
108
|
<MotionMarkdown text={presentation.text} streaming={effectiveMode} enabled={liveText && presentation.reveal && effectiveMode} revision={presentation.revision} fileMentions={fileMentions} />
|
|
97
109
|
</div>;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
function
|
|
112
|
+
function TransformedMarkdown({ registry, blockKey, source, text, streaming, ...props }: MarkdownStreamProps & { registry: TextTransformRegistry; blockKey: string; source: ReaderBlockOwner['source'] }) {
|
|
113
|
+
const shown = useDisplayText(registry, { text, streaming, blockKey, kind: 'body', source });
|
|
114
|
+
return <MarkdownStream {...props} text={shown.text} streaming={displayStreaming(streaming, shown, true)} transformed />;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ReadingMarkdown({ blockKey, source, ...props }: MarkdownStreamProps & { blockKey: string; source: ReaderBlockOwner['source'] }) {
|
|
118
|
+
const registry = activeTextTransform();
|
|
119
|
+
if (registry?.has('body')) return <TransformedMarkdown registry={registry} blockKey={blockKey} source={source} {...props} />;
|
|
120
|
+
return <MarkdownStream {...props} />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type ReasoningStreamProps = { text: string; streaming: boolean; holdFormatting: boolean; transformed?: boolean } & TextPresentation;
|
|
124
|
+
|
|
125
|
+
function ReasoningStream({ text, streaming, holdFormatting, startedAt, interrupted = false, liveText = false, transformed = false }: ReasoningStreamProps) {
|
|
101
126
|
const presentation = useStreamingText(text, streaming, { startedAt, interrupted: interrupted || !liveText, selected: holdFormatting });
|
|
102
127
|
return <div className={css.readingText} data-reader-text data-reader-text-kind="reasoning" data-received-length={text.length} data-shown-length={presentation.text.length}
|
|
103
|
-
data-presentation-pending={presentation.pending || undefined} data-motion-style="opacity-blur" data-ud-motion="reader-text-arrival" data-ud-motion-type="reveal" data-ud-motion-no-flash="true"
|
|
128
|
+
data-presentation-pending={presentation.pending || undefined} data-motion-style="opacity-blur" data-ud-motion="reader-text-arrival" data-ud-motion-type="reveal" data-ud-motion-no-flash="true"
|
|
129
|
+
data-text-transform={transformed ? (streaming ? 'live' : 'final') : undefined}>
|
|
104
130
|
<MotionPlainText text={presentation.text} enabled={liveText && presentation.reveal} revision={presentation.revision} />
|
|
105
131
|
</div>;
|
|
106
132
|
}
|
|
107
133
|
|
|
108
|
-
function
|
|
134
|
+
function TransformedReasoning({ registry, blockKey, source, text, streaming, ...props }: ReasoningStreamProps & { registry: TextTransformRegistry; blockKey: string; source: ReaderBlockOwner['source'] }) {
|
|
135
|
+
const shown = useDisplayText(registry, { text, streaming, blockKey, kind: 'reasoning', source });
|
|
136
|
+
return <ReasoningStream {...props} text={shown.text} streaming={displayStreaming(streaming, shown, true)} transformed />;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ReadingReasoning({ blockKey, source, ...props }: ReasoningStreamProps & { blockKey: string; source: ReaderBlockOwner['source'] }) {
|
|
140
|
+
const registry = activeTextTransform();
|
|
141
|
+
if (registry?.has('reasoning')) return <TransformedReasoning registry={registry} blockKey={blockKey} source={source} {...props} />;
|
|
142
|
+
return <ReasoningStream {...props} />;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fallback(block: AssistantBlock, streaming: boolean, source: ReaderBlockOwner['source'], loadImage: BlockRenderProps['loadImage'], fillComposer: BlockRenderProps['fillComposer'], holdFormatting: boolean, presentation: TextPresentation, blockKey: string, fileMentions?: BlockRenderProps['fileMentions']): ReactNode {
|
|
109
146
|
switch (block.kind) {
|
|
110
|
-
case 'text': return source === 'user' ? <MarkdownText text={block.text} labels={markdownLabels} /> : <ReadingMarkdown text={block.text} streaming={streaming} holdFormatting={holdFormatting} {...presentation} fileMentions={fileMentions} />;
|
|
147
|
+
case 'text': return source === 'user' ? <MarkdownText text={block.text} labels={markdownLabels} /> : <ReadingMarkdown text={block.text} streaming={streaming} holdFormatting={holdFormatting} {...presentation} fileMentions={fileMentions} blockKey={blockKey} source={source} />;
|
|
111
148
|
case 'image': return <ImageBlock attachment={block.attachment} loadImage={loadImage} />;
|
|
112
|
-
case 'reasoning': return <ReadingReasoning text={block.text} streaming={streaming} holdFormatting={holdFormatting} {...presentation} />;
|
|
149
|
+
case 'reasoning': return <ReadingReasoning text={block.text} streaming={streaming} holdFormatting={holdFormatting} {...presentation} blockKey={blockKey} source={source} />;
|
|
113
150
|
case 'tool-call': return <JsonBlock label={`工具参数 · ${block.name}`} payload={block.argsRaw} truncatedLabel={truncatedJsonLabel} />;
|
|
114
151
|
case 'other': {
|
|
115
152
|
const raw = block.block;
|
|
@@ -130,10 +167,11 @@ function fallback(block: AssistantBlock, streaming: boolean, source: ReaderBlock
|
|
|
130
167
|
export const Blocks = memo(function Blocks({ blocks, streaming = false, source = 'assistant', holdFormatting = false, startedAt, interrupted, liveText, renderSlotChain, loadImage, fillComposer, fileMentions }: BlockRenderProps & TextPresentation & {
|
|
131
168
|
blocks: readonly AssistantBlock[]; streaming?: boolean; source?: ReaderBlockOwner['source']; holdFormatting?: boolean;
|
|
132
169
|
}) {
|
|
170
|
+
const reactId = useId();
|
|
133
171
|
return <ComposerFillContext.Provider value={fillComposer}>
|
|
134
172
|
<div className={css.blocks} data-streaming={streaming || undefined}>
|
|
135
173
|
{blocks.map((block, index) => <BlockBoundary key={block.kind === 'image' ? `image:${block.attachment.attachmentId}:${index}` : `${index}:${block.kind}`}>
|
|
136
|
-
<Fragment>{renderSlotChain('dsh-better-display.block',
|
|
174
|
+
<Fragment>{renderSlotChain('dsh-better-display.block', readerBlockOwner(block, streaming, source, { startedAt, interrupted, liveText }, holdFormatting), { fallback: fallback(block, streaming, source, loadImage, fillComposer, holdFormatting, { startedAt, interrupted, liveText }, `${reactId}:${index}:${block.kind}`, fileMentions) })}</Fragment>
|
|
137
175
|
</BlockBoundary>)}
|
|
138
176
|
</div>
|
|
139
177
|
</ComposerFillContext.Provider>;
|
|
@@ -164,7 +202,8 @@ function MessageClock({ time }: { time: number }) {
|
|
|
164
202
|
|
|
165
203
|
export function CopyAnswer({ blocks, onFork, metrics, extraActions }: { blocks: readonly AssistantBlock[]; onFork?: () => void; metrics?: BlockRenderProps['metrics']; extraActions?: ReactNode }) {
|
|
166
204
|
const { receipt, copy } = useCopyReceipt();
|
|
167
|
-
|
|
205
|
+
// Session text. A display transformer does not write these blocks.
|
|
206
|
+
const text = originalAnswerText(blocks);
|
|
168
207
|
const endedAt = metrics?.endedAt;
|
|
169
208
|
const hasMetrics = metrics !== undefined && (metrics.usage !== undefined || metrics.runMs !== undefined || endedAt !== undefined);
|
|
170
209
|
if (!text.trim() && !onFork && !hasMetrics) return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
-
import type { ReactNode
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
3
|
import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
4
|
import type { ReaderProps } from './types.js';
|
|
5
5
|
import type { LiveStep, LiveTurnItem } from './live-turn.js';
|
|
@@ -10,6 +10,7 @@ import { FoldSummaryText } from './LiveFold.js';
|
|
|
10
10
|
import { DiffStat } from './DiffPanel.js';
|
|
11
11
|
import { StreamMotionContext } from './streaming.js';
|
|
12
12
|
import css from './Reader.module.css';
|
|
13
|
+
import { useSummaryLane } from './StickyLane.js';
|
|
13
14
|
|
|
14
15
|
const FlowSnapshot = createContext<ChatSnapshot | null>(null);
|
|
15
16
|
/** Public projection only. Descendants cannot leak live data through a frozen frame. */
|
|
@@ -44,6 +45,7 @@ function FlowCell({ hidden, instant, motion, rowKey, children, summary = false }
|
|
|
44
45
|
hidden: boolean; instant: boolean; motion: boolean; rowKey: string; children: ReactNode; summary?: boolean;
|
|
45
46
|
}) {
|
|
46
47
|
const ref = useRef<HTMLDivElement>(null);
|
|
48
|
+
useSummaryLane(ref, summary);
|
|
47
49
|
const [present, setPresent] = useState(!hidden);
|
|
48
50
|
const previous = useRef(hidden);
|
|
49
51
|
if (!hidden && !present) setPresent(true);
|
|
@@ -4,6 +4,7 @@ import { Disclosure } from './motion.js';
|
|
|
4
4
|
import { FoldSummaryText } from './LiveFold.js';
|
|
5
5
|
import { foldSummary } from './live-turn.js';
|
|
6
6
|
import { DiffStat } from './DiffPanel.js';
|
|
7
|
+
import { usePublishedHeight } from './StickyLane.js';
|
|
7
8
|
import type { LiveStep } from './live-turn.js';
|
|
8
9
|
|
|
9
10
|
/** Keep process counts discoverable after the live-fold presentation retires. */
|
|
@@ -11,9 +12,10 @@ export function ClosedProcessSummary({ steps, open, onChange, controls }: {
|
|
|
11
12
|
steps: readonly LiveStep[]; open: boolean; onChange: (value: boolean) => void; controls: string;
|
|
12
13
|
}) {
|
|
13
14
|
const button = useRef<HTMLButtonElement>(null);
|
|
15
|
+
const lane = usePublishedHeight('--reader-summary-height');
|
|
14
16
|
if (!steps.length) return null;
|
|
15
17
|
const summary = foldSummary(steps);
|
|
16
|
-
return <div className={css.closedProcessSummary} data-reader-closed-summary={summary}>
|
|
18
|
+
return <div ref={lane} className={css.closedProcessSummary} data-reader-closed-summary={summary}>
|
|
17
19
|
<div className={css.summaryRow}>
|
|
18
20
|
<Disclosure open={open} onChange={onChange} controls={controls} buttonRef={button} ariaLabel="本轮过程详情"
|
|
19
21
|
label={<FoldSummaryText summary={summary} motion={false} />} />
|
|
@@ -44,7 +44,6 @@ export const McpAppFrame = memo(function McpAppFrame({
|
|
|
44
44
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
45
45
|
const lastParamsRef = useRef<Record<string, unknown>>({});
|
|
46
46
|
const [height, setHeight] = useState(() => Math.max(60, Math.min(2400, initialHeight)));
|
|
47
|
-
const [ready, setReady] = useState(false);
|
|
48
47
|
const [receipt, setReceipt] = useState<string | null>(null);
|
|
49
48
|
const [lastPrompt, setLastPrompt] = useState<string | null>(null);
|
|
50
49
|
const [reloadNonce, setReloadNonce] = useState(0);
|
|
@@ -81,7 +80,7 @@ export const McpAppFrame = memo(function McpAppFrame({
|
|
|
81
80
|
|
|
82
81
|
const handleUserSubmit = useCallback((params: Record<string, unknown>) => {
|
|
83
82
|
lastParamsRef.current = params;
|
|
84
|
-
let summary
|
|
83
|
+
let summary: string;
|
|
85
84
|
if (typeof params.choice === 'string') {
|
|
86
85
|
const desc = typeof params.desc === 'string' ? ` (${params.desc})` : '';
|
|
87
86
|
summary = `选择: ${params.choice}${desc}`;
|
|
@@ -127,17 +126,13 @@ export const McpAppFrame = memo(function McpAppFrame({
|
|
|
127
126
|
},
|
|
128
127
|
};
|
|
129
128
|
iframe.contentWindow?.postMessage(response, '*');
|
|
130
|
-
setReady(true);
|
|
131
129
|
return;
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
//
|
|
135
|
-
if (data.method === 'ui/ready' || data.method === 'ui/notifications/initialized')
|
|
136
|
-
setReady(true);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
132
|
+
// ui/ready is a notification. The frame has no ready flag to flip.
|
|
133
|
+
if (data.method === 'ui/ready' || data.method === 'ui/notifications/initialized') return;
|
|
139
134
|
|
|
140
|
-
//
|
|
135
|
+
// ui/resize
|
|
141
136
|
if (data.method === 'ui/resize' && data.params?.height) {
|
|
142
137
|
const h = Number(data.params.height);
|
|
143
138
|
if (Number.isFinite(h) && h > 0) {
|
|
@@ -146,7 +141,7 @@ export const McpAppFrame = memo(function McpAppFrame({
|
|
|
146
141
|
return;
|
|
147
142
|
}
|
|
148
143
|
|
|
149
|
-
//
|
|
144
|
+
// ui/submit or ui/update-model-context
|
|
150
145
|
if (data.method === 'ui/submit' || data.method === 'ui/update-model-context') {
|
|
151
146
|
const params = (data.params as Record<string, unknown>) || {};
|
|
152
147
|
handleUserSubmit(params);
|
|
@@ -209,13 +204,10 @@ export const McpAppFrame = memo(function McpAppFrame({
|
|
|
209
204
|
},
|
|
210
205
|
},
|
|
211
206
|
}, '*');
|
|
212
|
-
|
|
213
|
-
setReady(true);
|
|
214
207
|
}, []);
|
|
215
208
|
|
|
216
209
|
const handleReload = useCallback(() => {
|
|
217
210
|
setReloadNonce(n => n + 1);
|
|
218
|
-
setReady(false);
|
|
219
211
|
setReceipt(null);
|
|
220
212
|
}, []);
|
|
221
213
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useCallback, useState, useSyncExternalStore, type ReactNode } from 'react';
|
|
2
|
-
import type { ChatConversationViewNode, ChatNodeOwnerProps, TurnTailOwnerProps, UseDisclosure } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
2
|
+
import type { AssistantActionOwnerProps, ChatConversationViewNode, ChatNode, ChatNodeOwnerProps, TurnTailOwnerProps, UseDisclosure } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
3
3
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
4
|
-
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
5
|
-
import type { AssistantActionOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import type { AssistantBlock, ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
6
5
|
type MessageId = AssistantActionOwnerProps['messageId'];
|
|
7
6
|
import { OFFICIAL_SEATS } from './official-slots.js';
|
|
8
7
|
import type { BlockRenderProps } from './types.js';
|
|
9
8
|
import { chatSeatProps } from './chat-seat.js';
|
|
9
|
+
import { Blocks } from './Blocks.js';
|
|
10
10
|
|
|
11
11
|
export function OfficialActions({ official, messageId }: Pick<BlockRenderProps, 'official'> & { messageId?: MessageId }) {
|
|
12
12
|
if (!official || !messageId) return null;
|
|
@@ -38,11 +38,8 @@ export function OfficialTool({ official, block, toolName, cwd, openFile, fallbac
|
|
|
38
38
|
</div>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const [disclosureReset] = useState(() => createSnapshotStore(0));
|
|
44
|
-
if (!official) return fallback;
|
|
45
|
-
const owner: ChatNodeOwnerProps = {
|
|
41
|
+
function chatNodeOwner(render: BlockRenderProps, official: NonNullable<BlockRenderProps['official']>): ChatNodeOwnerProps {
|
|
42
|
+
return {
|
|
46
43
|
cwd: render.cwd,
|
|
47
44
|
openFile: render.openFile ?? (() => {}),
|
|
48
45
|
openSkill: () => {},
|
|
@@ -54,14 +51,55 @@ export function OfficialNode({ node, fallback, ...render }: BlockRenderProps & {
|
|
|
54
51
|
}),
|
|
55
52
|
fileMentions: official.officialFileMentions,
|
|
56
53
|
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function OfficialNode({ node, fallback, ...render }: BlockRenderProps & { node: ChatConversationViewNode; fallback: ReactNode }) {
|
|
57
|
+
const official = render.official;
|
|
58
|
+
const [disclosureReset] = useState(() => createSnapshotStore(0));
|
|
59
|
+
if (!official) return fallback;
|
|
57
60
|
const turn = node.location.kind === 'turn' || node.location.kind === 'step' ? node.location.turn : undefined;
|
|
58
61
|
// The runtime node domain is open; the public SlotMap enumerates the known kinds.
|
|
59
62
|
const renderNode = official.renderSlot as unknown as (key: string, owner: object, options: object) => ReactNode;
|
|
60
63
|
return <div data-reader-official-node={node.kind} {...(node.kind === 'user' || node.kind === 'steering' ? chatSeatProps(node.kind, node.key) : {})}>
|
|
61
|
-
{renderNode(OFFICIAL_SEATS.nodes, { ...
|
|
64
|
+
{renderNode(OFFICIAL_SEATS.nodes, { ...chatNodeOwner(render, official), node }, { entryKey: node.kind, hookContext: { turnData: turn?.data, disclosureReset }, fallback })}
|
|
62
65
|
</div>;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
type AssistantBodyProps = BlockRenderProps & {
|
|
69
|
+
node: ChatNode<'assistant-step'>;
|
|
70
|
+
blocks: readonly AssistantBlock[];
|
|
71
|
+
streaming?: boolean;
|
|
72
|
+
holdFormatting?: boolean;
|
|
73
|
+
startedAt?: number;
|
|
74
|
+
interrupted?: boolean;
|
|
75
|
+
liveText?: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Body of one assistant segment. A mirrored assistant-step replacement draws
|
|
80
|
+
* it; Reader markdown is the fallback when no replacement is registered.
|
|
81
|
+
* The caller keeps reasoning blocks on the reading card.
|
|
82
|
+
*/
|
|
83
|
+
export function OfficialAssistantBody({
|
|
84
|
+
node, blocks, streaming = false, holdFormatting = false, startedAt, interrupted = false, liveText = false, ...render
|
|
85
|
+
}: AssistantBodyProps) {
|
|
86
|
+
const official = render.official;
|
|
87
|
+
const [disclosureReset] = useState(() => createSnapshotStore(0));
|
|
88
|
+
const fallback = <Blocks {...render} blocks={blocks} streaming={streaming} holdFormatting={holdFormatting} startedAt={startedAt} interrupted={interrupted} liveText={liveText} />;
|
|
89
|
+
if (!official) return fallback;
|
|
90
|
+
const turn = node.location.kind === 'turn' || node.location.kind === 'step' ? node.location.turn : undefined;
|
|
91
|
+
const sameBlocks = blocks.length === node.data.blocks.length && blocks.every((block, index) => block === node.data.blocks[index]);
|
|
92
|
+
const view = sameBlocks ? node : { ...node, data: { ...node.data, blocks } };
|
|
93
|
+
const renderNode = official.renderSlot as unknown as (key: string, owner: object, options: object) => ReactNode;
|
|
94
|
+
// groupPart matches the host split: response skips reasoning. The block
|
|
95
|
+
// slice is what a replacement that ignores groupPart actually receives.
|
|
96
|
+
return renderNode(OFFICIAL_SEATS.nodes, { ...chatNodeOwner(render, official), node: view, groupPart: 'response' }, {
|
|
97
|
+
entryKey: 'assistant-step',
|
|
98
|
+
hookContext: { turnData: turn?.data, disclosureReset },
|
|
99
|
+
fallback,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
65
103
|
export function OfficialTail({ official, owner, produced }: Pick<BlockRenderProps, 'official'> & { owner?: TurnTailOwnerProps; produced: readonly string[] }) {
|
|
66
104
|
const tailOwner = owner && official ? { ...owner, openFile: official.officialPreviewFile, readerProducedPaths: produced } : undefined;
|
|
67
105
|
return official && tailOwner ? <div data-reader-official-tail style={{ display: 'contents' }}>
|
|
@@ -8,8 +8,20 @@
|
|
|
8
8
|
flex: 0 0 auto;
|
|
9
9
|
overflow-anchor: auto;
|
|
10
10
|
container-type: inline-size;
|
|
11
|
+
/* 40px at the host's default 14px content size. A smaller content size
|
|
12
|
+
must not shrink the lane under that floor. Positive font delta still
|
|
13
|
+
grows it, or the next sticky band lands on the previous one. */
|
|
14
|
+
--reader-lane-block: max(40px, calc(40px + var(--dsh-content-font-delta, 0px)));
|
|
15
|
+
/* Wallpaper themes clear --dsw-alias-bg-base, or leave it only partly
|
|
16
|
+
opaque. The token and the inner surface paint above Canvas, the page
|
|
17
|
+
surface colour (it follows color-scheme). An opaque stock token covers
|
|
18
|
+
that underlay, so the stock theme does not change. */
|
|
19
|
+
--reader-lane-plate: linear-gradient(var(--dsw-alias-bg-base), var(--dsw-alias-bg-base)), linear-gradient(var(--dsw-alias-bg-layer-1, transparent), var(--dsw-alias-bg-layer-1, transparent));
|
|
11
20
|
}
|
|
12
21
|
.column { max-width: var(--dsh-chat-content-width, 748px); width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: 22px; min-width: 0; }
|
|
22
|
+
/* The column is only a programmatic focus-restore target (tabIndex -1), never a
|
|
23
|
+
Tab stop, so it must not draw a ring around the whole transcript. */
|
|
24
|
+
.column:focus, .column:focus-visible { outline: none; }
|
|
13
25
|
.toolbar { position: sticky; top: 0; display: flex; justify-content: flex-end; align-items: center; gap: 6px; min-height: 32px; padding: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px); }
|
|
14
26
|
/* 吸顶带第二车道:左「正在处理」+ 右「自动折叠」共享同一条 40px 水平基线。
|
|
15
27
|
top / z-index 由下方 measured-flow 段统一定义:这一道永远钉在工具栏车道
|
|
@@ -24,12 +36,24 @@
|
|
|
24
36
|
.turn { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
25
37
|
/* Keep pinned process lanes inside the process/answer block, before turn-tail UI. */
|
|
26
38
|
.turnStickyRegion { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
27
|
-
.turnStickyLanes { position: sticky; top:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
39
|
+
.turnStickyLanes { position: sticky; top: var(--reader-toolbar-height, var(--reader-lane-block, 40px)); z-index: 6; display: flex; flex-direction: column; min-width: 0; }
|
|
40
|
+
/* Every turn renders through its own retiring lane, so the sibling rule has to
|
|
41
|
+
reach one level in: the margin still lands between two consecutive turns. */
|
|
42
|
+
.turn + .turn, .retiringContent + .retiringContent > .turn { margin-top: 12px; }
|
|
43
|
+
/* Host .userStack caps itself with min(axis * 0.702, 82%). In the native column
|
|
44
|
+
that percentage resolves against a .userRow stretched across the column. The
|
|
45
|
+
slot outlet is display:contents, so the host row is this cluster's flex item.
|
|
46
|
+
Naming the official slot here misses the mirrored seat
|
|
47
|
+
(dsh-better-display.official.nodes/…), the row shrink-wraps to the text, and
|
|
48
|
+
the same 82% is applied again. The cluster fills the column. The direct child
|
|
49
|
+
of its own slot — the host row, not a nested image slot — stretches with it.
|
|
50
|
+
The host .userStack is left alone. Pending bubbles are not that row, so they
|
|
51
|
+
carry the same cap themselves. */
|
|
52
|
+
.userCluster { display: flex; flex-direction: column; align-self: stretch; align-items: flex-end; width: 100%; max-width: 100%; gap: 8px; min-width: 0; margin-bottom: 4px; }
|
|
53
|
+
.userCluster > [data-slot] > * { align-self: stretch; width: 100%; max-width: 100%; min-width: 0; box-sizing: border-box; }
|
|
54
|
+
.userImages { display: flex; flex-direction: column; align-items: flex-end; align-self: flex-end; width: fit-content; max-width: 100%; min-width: 0; }
|
|
31
55
|
.userImages .imageFigure { align-self: flex-end; }
|
|
32
|
-
.user { align-self: flex-end; max-width:
|
|
56
|
+
.user { align-self: flex-end; width: fit-content; max-width: min(calc(var(--dsh-chat-content-width, 748px) * 0.702), 82%); margin-left: auto; padding: 10px 16px; border-radius: var(--dsw-radius-xl, 22px); background: var(--dsw-specific-bubble); color: var(--dsw-alias-label-primary); font-size: var(--dsh-content-font-size, 14px); line-height: calc(22px + var(--dsh-content-font-delta, 0px)); font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; box-sizing: border-box; }
|
|
33
57
|
.user .blocks { gap: 10px; }
|
|
34
58
|
.answer { display: flex; flex-direction: column; min-width: 0; overflow-wrap: anywhere; }
|
|
35
59
|
.readingText { min-width: 0; }
|
|
@@ -48,9 +72,9 @@
|
|
|
48
72
|
.blocks > :last-child { margin-bottom: 0; }
|
|
49
73
|
.blocks :global(.md-table-wide) { max-width: 100%; }
|
|
50
74
|
.disclosure { min-width: 0; }
|
|
51
|
-
.disclosureButton { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; min-height: 32px; border: 0; background: transparent; border-radius: 7px; color: var(--dsw-alias-label-tertiary); font-size: 14px; line-height: 24px; font-weight: 400; padding: 4px 6px; margin-left: -6px; text-align: left; }
|
|
75
|
+
.disclosureButton { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; min-height: 32px; border: 0; background: transparent; border-radius: 7px; color: var(--dsw-alias-label-tertiary); font-size: calc(14px + var(--dsh-content-font-delta, 0px)); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); font-weight: 400; padding: 4px 6px; margin-left: -6px; text-align: left; }
|
|
52
76
|
.disclosure[data-reader-status-only] { display: flex; align-items: center; min-height: 32px; }
|
|
53
|
-
.statusText { display: inline-flex; min-width: 0; max-width: 100%; font-size: 14px; line-height: 24px; font-weight: 400; color: var(--dsw-alias-label-secondary); }
|
|
77
|
+
.statusText { display: inline-flex; min-width: 0; max-width: 100%; font-size: calc(14px + var(--dsh-content-font-delta, 0px)); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); font-weight: 400; color: var(--dsw-alias-label-secondary); }
|
|
54
78
|
.srOnly { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
55
79
|
/* Transitions.dev — Thinking states, supplied by the user. Native DSH colors;
|
|
56
80
|
the hidden sizer shares this font and the short phases share a left edge. */
|
|
@@ -109,7 +133,20 @@
|
|
|
109
133
|
rail width, same gutter, so a column of nested rows reads as one line. */
|
|
110
134
|
.processContents { display: flow-root; border-left: 1px solid var(--dsw-alias-border-l2); padding: 3px 0 2px var(--reader-gutter, 16px); margin: 0 0 0 var(--reader-rail, 6px); color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 24px; }
|
|
111
135
|
.processContents > :last-child { margin-bottom: 0; }
|
|
112
|
-
|
|
136
|
+
/* Narration stays ordinary body text (the `.answer` typography) and carries no
|
|
137
|
+
left rule of its own. When collapsed reasoning/tool rows sat between two
|
|
138
|
+
narration blocks, the gap carries a compact folded-process summary in the
|
|
139
|
+
process-chain language: a 1px left rail plus the familiar `思考×N · 工具×K`
|
|
140
|
+
label with its chevron. The preceding narration cell drops its outer bottom
|
|
141
|
+
gap so the seam row owns exactly that space. */
|
|
142
|
+
.processSeamRow {
|
|
143
|
+
display: flex; align-items: center; min-height: 40px;
|
|
144
|
+
margin-left: var(--reader-rail, 6px); padding-left: var(--reader-gutter, 16px);
|
|
145
|
+
border-left: 1px solid var(--dsw-alias-border-l2); box-sizing: border-box;
|
|
146
|
+
}
|
|
147
|
+
.processSeamRow > .disclosure { display: flex; align-items: center; min-height: 24px; }
|
|
148
|
+
.processSeamRow .disclosureButton { min-height: 24px; padding-block: 2px; font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
|
|
149
|
+
.flowCell:has(> .flowCellInner > [data-reader-seam-after]) > .flowCellInner { padding-bottom: 0; }
|
|
113
150
|
.processFrame { min-width: 0; }
|
|
114
151
|
.toolActivity { min-width: 0; color: var(--dsw-alias-label-secondary); }
|
|
115
152
|
.nativeToolRow { min-width: 0; }
|
|
@@ -476,7 +513,7 @@
|
|
|
476
513
|
.morphDrawer { overflow: hidden; height: 0; transform-origin: top center; will-change: height, opacity, transform; }
|
|
477
514
|
.liveFoldImpact { animation: liveFoldElastic 180ms cubic-bezier(0.2, 0, 0, 1) both; }
|
|
478
515
|
@keyframes liveFoldElastic { 0% { transform: scale(1); } 50% { transform: scale(1.015); } 100% { transform: scale(1); } }
|
|
479
|
-
.liveFoldContainer { position: sticky; top: calc(
|
|
516
|
+
.liveFoldContainer { position: sticky; top: calc(var(--reader-toolbar-height, var(--reader-lane-block, 40px)) + var(--reader-status-height, var(--reader-lane-block, 40px)) + var(--reader-summary-height, var(--reader-lane-block, 40px))); z-index: 4; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px); padding: 4px 0; margin-bottom: 6px; animation: liveFoldPulse 320ms cubic-bezier(0.16, 1, 0.3, 1) both; transform-origin: left center; will-change: transform, opacity; }
|
|
480
517
|
@keyframes liveFoldPulse { 0% { transform: translateY(-4px) scale(0.98); opacity: 0.8; } 50% { transform: translateY(0) scale(1.02); opacity: 1; } 100% { transform: translateY(0) scale(1); opacity: 1; } }
|
|
481
518
|
[data-motion=off] .liveFoldContainer { animation: none !important; }
|
|
482
519
|
@media (prefers-reduced-motion: reduce) { .liveFoldContainer { animation: none !important; } }
|
|
@@ -514,7 +551,7 @@
|
|
|
514
551
|
[data-motion=off] .turnStatusLabel { animation: none !important; color: var(--dsw-static-deepseek-500, #4d6bfe) !important; -webkit-text-fill-color: var(--dsw-static-deepseek-500, #4d6bfe) !important; }
|
|
515
552
|
@media (prefers-reduced-motion: reduce) { .turnStatusLabel { animation: none !important; color: var(--dsw-static-deepseek-500, #4d6bfe) !important; -webkit-text-fill-color: var(--dsw-static-deepseek-500, #4d6bfe) !important; } }
|
|
516
553
|
@media (pointer: coarse) { .disclosureButton { min-height: 44px; } }
|
|
517
|
-
@media (max-width: 600px) { .root { padding-inline: 20px; } .column { gap: 18px; } .toolbar { font-size: 11px; }
|
|
554
|
+
@media (max-width: 600px) { .root { padding-inline: 20px; } .column { gap: 18px; } .toolbar { font-size: 11px; } }
|
|
518
555
|
|
|
519
556
|
/* Measured flow owns geometry; steps never change React parents when folded. */
|
|
520
557
|
.choreographedFlow { display: flex; flex-direction: column; min-width: 0; overflow-anchor: none; }
|
|
@@ -535,7 +572,7 @@
|
|
|
535
572
|
input card. Only the top toolbar lane may use 7: it pins to the scrollport's
|
|
536
573
|
first lane and its containing block always ends above the seat, so it can
|
|
537
574
|
never reach the band, and 7 keeps it above the host's code-block banners. */
|
|
538
|
-
.toolbar { width: 100%; max-width: 100%; align-self: stretch; box-sizing: border-box; min-height: 40px; z-index: 7; border-bottom: 0; backdrop-filter: none; }
|
|
575
|
+
.toolbar { width: 100%; max-width: 100%; align-self: stretch; box-sizing: border-box; min-height: var(--reader-lane-block, 40px); z-index: 7; border-bottom: 0; backdrop-filter: none; }
|
|
539
576
|
/* Every sticky lane used to pin to the SAME top: the status row, the folded
|
|
540
577
|
summary row and the live-fold container all sat at
|
|
541
578
|
max(toolbar-height, status-height) with z-index 6. Three siblings competing
|
|
@@ -543,7 +580,7 @@
|
|
|
543
580
|
内容被盖住、看不见" was: the disclosure opened, and the row that followed
|
|
544
581
|
pinned straight on top of it. Give each lane its own band instead, stacked
|
|
545
582
|
from the measured top: status first, then the summary, then the fold body. */
|
|
546
|
-
.turnProcessSticky { box-sizing: border-box; width: calc(100% - var(--reader-control-width, 148px)); top:
|
|
583
|
+
.turnProcessSticky { box-sizing: border-box; width: calc(100% - var(--reader-control-width, 148px)); top: var(--reader-toolbar-height, var(--reader-lane-block, 40px)); min-height: var(--reader-lane-block, 40px); padding-right: 0; z-index: 6; border-bottom: 1px solid transparent; background-clip: padding-box; backdrop-filter: none; }
|
|
547
584
|
/* A pinned lane's in-flow gap stays visible as an uncovered scrollport strip
|
|
548
585
|
while the lane itself paints opaque. Cast each plate upward over its strip
|
|
549
586
|
(the .turn 14px gap / the prior cell's 16px flowCellInner padding) so the
|
|
@@ -552,17 +589,17 @@
|
|
|
552
589
|
toolbar's control group, which leaves the same kind of strip there; paint
|
|
553
590
|
it too. The toolbar is z-index 7, above the lane's 6, so its buttons still
|
|
554
591
|
render on top of the extension. */
|
|
555
|
-
.turnProcessSticky { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base), var(--reader-control-width, 148px) 0 0 var(--dsw-alias-bg-base); }
|
|
592
|
+
.turnProcessSticky { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base), var(--reader-control-width, 148px) 0 0 var(--dsw-alias-bg-base), 0 -14px 0 var(--dsw-alias-bg-layer-1, transparent), var(--reader-control-width, 148px) 0 0 var(--dsw-alias-bg-layer-1, transparent), 0 -14px 0 Canvas, var(--reader-control-width, 148px) 0 0 Canvas; }
|
|
556
593
|
.turnStickyLanes .turnProcessSticky, .turnStickyLanes .closedProcessSummary { position: static; top: auto; z-index: auto; }
|
|
557
594
|
/* The same summary row stays in flow until it reaches the measured top lane. */
|
|
558
|
-
.flowCell[data-flow-summary], .closedProcessSummary { position: sticky; top: calc(
|
|
595
|
+
.flowCell[data-flow-summary], .closedProcessSummary { position: sticky; top: calc(var(--reader-toolbar-height, var(--reader-lane-block, 40px)) + var(--reader-status-height, var(--reader-lane-block, 40px))); z-index: 5; overflow: visible; box-sizing: border-box; min-height: var(--reader-lane-block, 40px); padding-block: 4px; background: var(--dsw-alias-bg-base); border-bottom: 0; }
|
|
559
596
|
/* Keep the official CodeBlock banner's internal z-index inside its wrapper so Reader lanes stay above it. */
|
|
560
597
|
.root :global(.md-code-block) { z-index: 0; }
|
|
561
598
|
/* Keep native code actions below the pinned Reader lanes. */
|
|
562
|
-
.turnStickyRegion:has(> .turnStickyLanes > .closedProcessSummary), .turnStickyRegion:has(.flowCell[data-flow-summary]) { --reader-code-summary-height: var(--reader-
|
|
599
|
+
.turnStickyRegion:has(> .turnStickyLanes > .closedProcessSummary), .turnStickyRegion:has(.flowCell[data-flow-summary]) { --reader-code-summary-height: var(--reader-summary-height, var(--reader-lane-block, 40px)); }
|
|
563
600
|
.root :global(.md-code-block > :has(+ [data-code-block-content])) { top: calc(max(var(--reader-toolbar-height, 40px), var(--reader-status-height, 0px)) + var(--reader-status-height, 0px) + var(--reader-code-summary-height, 0px)); }
|
|
564
|
-
.flowCell[data-flow-summary] { box-shadow: 0 -16px 0 var(--dsw-alias-bg-base); }
|
|
565
|
-
.closedProcessSummary { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base); }
|
|
601
|
+
.flowCell[data-flow-summary] { box-shadow: 0 -16px 0 var(--dsw-alias-bg-base), 0 -16px 0 var(--dsw-alias-bg-layer-1, transparent), 0 -16px 0 Canvas; }
|
|
602
|
+
.closedProcessSummary { box-shadow: 0 -14px 0 var(--dsw-alias-bg-base), 0 -14px 0 var(--dsw-alias-bg-layer-1, transparent), 0 -14px 0 Canvas; }
|
|
566
603
|
.foldControl { display: inline-flex; align-items: center; gap: 0; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-module-platform); }
|
|
567
604
|
.foldStop { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); padding: 2px 8px; border-radius: 6px; white-space: nowrap; font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; }
|
|
568
605
|
.foldStop[data-on] { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); }
|
|
@@ -576,6 +613,19 @@
|
|
|
576
613
|
.thinkSizer { white-space: normal; overflow-wrap: anywhere; }
|
|
577
614
|
.thinkText { white-space: normal; overflow-wrap: anywhere; overflow: visible; text-overflow: clip; }
|
|
578
615
|
|
|
616
|
+
/* Theme layers sit on an opaque page surface. A cleared token, or one at
|
|
617
|
+
10% opacity, composites over Canvas instead of the text colour, and the
|
|
618
|
+
heading underneath cannot show through. */
|
|
619
|
+
.toolbar,
|
|
620
|
+
.turnStickyLanes,
|
|
621
|
+
.turnProcessSticky,
|
|
622
|
+
.liveFoldContainer,
|
|
623
|
+
.flowCell[data-flow-summary],
|
|
624
|
+
.closedProcessSummary {
|
|
625
|
+
background-color: Canvas;
|
|
626
|
+
background-image: var(--reader-lane-plate);
|
|
627
|
+
background-repeat: no-repeat;
|
|
628
|
+
}
|
|
579
629
|
/* Skin mode is opt-in, and it has one job: some readers run a skinned host
|
|
580
630
|
(wallpaper / theme), and the reader's own opaque rows hide that skin. On =
|
|
581
631
|
every surface the reader paints becomes a thin token-derived wash and the
|
|
@@ -631,7 +681,7 @@
|
|
|
631
681
|
}
|
|
632
682
|
.root[data-reader-glass] .turnStickyLanes { display: contents; position: static; top: auto; z-index: auto; }
|
|
633
683
|
.root[data-reader-glass] .turnStickyRegion { --reader-code-summary-height: 0px; }
|
|
634
|
-
.root[data-reader-glass] .turnStickyLanes .turnProcessSticky { position: sticky; top:
|
|
684
|
+
.root[data-reader-glass] .turnStickyLanes .turnProcessSticky { position: sticky; top: var(--reader-toolbar-height, var(--reader-lane-block, 40px)); z-index: 6; }
|
|
635
685
|
/* User bubble becomes a sleek translucent frosted pill in skin mode */
|
|
636
686
|
.root[data-reader-glass] .user {
|
|
637
687
|
background: color-mix(in srgb, var(--dsw-specific-bubble, rgba(0,0,0,.08)) 25%, transparent);
|