dsh-better-display 0.3.7 → 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 +22 -1
- 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 +59 -18
- package/docs/openapi.yaml +147 -0
- package/docs/runbooks/blank-reader.md +39 -0
- package/lib/client.js +1629 -586
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +535 -57
- 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 +76 -73
- 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.d.ts.map +1 -1
- package/lib/types/client/ToolActivity.js +10 -5
- package/lib/types/client/ToolActivity.js.map +1 -1
- package/lib/types/client/deliverables.d.ts.map +1 -1
- package/lib/types/client/deliverables.js +2 -0
- package/lib/types/client/deliverables.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 +15 -1
- 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 +144 -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 -4
- package/lib/types/client/official-slots.js.map +1 -1
- package/lib/types/client/open-file.d.ts.map +1 -1
- package/lib/types/client/open-file.js +1 -6
- package/lib/types/client/open-file.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/styles.d.ts +3 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/client/styles.js +25 -0
- package/lib/types/client/styles.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 +11 -4
- package/lib/types/client/tool-activity.d.ts.map +1 -1
- package/lib/types/client/tool-activity.js +54 -9
- 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 +55 -43
- 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/lib/types/reveal.d.ts +12 -0
- package/lib/types/reveal.d.ts.map +1 -0
- package/lib/types/reveal.js +147 -0
- package/lib/types/reveal.js.map +1 -0
- package/lib/types/workspace-path.d.ts +3 -0
- package/lib/types/workspace-path.d.ts.map +1 -0
- package/lib/types/workspace-path.js +5 -0
- package/lib/types/workspace-path.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 +103 -21
- package/src/client/Reader.tsx +83 -88
- package/src/client/SettingsSection.tsx +1 -7
- package/src/client/StickyLane.tsx +81 -2
- package/src/client/ToolActivity.tsx +10 -6
- package/src/client/deliverables.ts +1 -0
- package/src/client/index.tsx +18 -2
- package/src/client/live-turn.ts +100 -69
- package/src/client/motion.tsx +142 -7
- package/src/client/official-slots.tsx +23 -4
- package/src/client/open-file.ts +2 -8
- package/src/client/projection.ts +16 -4
- package/src/client/rewind-cut.ts +340 -0
- package/src/client/styles.ts +22 -0
- package/src/client/text-transform.ts +265 -0
- package/src/client/timeline.ts +0 -2
- package/src/client/tool-activity.ts +67 -9
- package/src/client/turn-slots.ts +80 -0
- package/src/client/types.ts +19 -0
- package/src/dsh-better-display.ts +57 -42
- package/src/flags.ts +40 -0
- package/src/log.ts +137 -0
- package/src/metrics.ts +50 -0
- package/src/reveal.ts +90 -0
- package/src/workspace-path.ts +4 -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 +234 -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 +9 -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 +28 -0
- package/tests/reader-official-users.test.ts +91 -0
- 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 +229 -0
- package/tests/reveal.test.ts +128 -0
- 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 +9 -1
- package/tests/stream-buffer.test.ts +1 -1
- package/tests/styles.test.ts +75 -0
- package/tests/terminal-input-preview.test.ts +113 -0
- 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
- package/tsdown.config.ts +49 -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,13 +8,25 @@
|
|
|
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; }
|
|
13
|
-
|
|
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; }
|
|
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 段统一定义:这一道永远钉在工具栏车道
|
|
16
28
|
之下、且永远低于宿主输入卡(composerSeat z-index 7)。 */
|
|
17
|
-
.turnProcessSticky { position: sticky; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px);
|
|
29
|
+
.turnProcessSticky { position: sticky; background: var(--dsw-alias-bg-base); backdrop-filter: blur(12px); min-height: 32px; display: flex; align-items: center; }
|
|
18
30
|
.turnProcessSticky .disclosureButton { min-height: 32px; }
|
|
19
31
|
/* 第二车道:折叠胶囊吸附在第一车道正下方,互不重叠 */
|
|
20
32
|
.textButton, .historyButton, .iconButton, .jump, .disclosureButton, .imageOpen, .dialogClose { font: inherit; cursor: pointer; }
|
|
@@ -22,11 +34,26 @@
|
|
|
22
34
|
.textButton:hover, .iconButton:hover, .disclosureButton:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
23
35
|
.root button:focus-visible, .root summary:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 3px; }
|
|
24
36
|
.turn { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
.
|
|
37
|
+
/* Keep pinned process lanes inside the process/answer block, before turn-tail UI. */
|
|
38
|
+
.turnStickyRegion { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
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; }
|
|
28
55
|
.userImages .imageFigure { align-self: flex-end; }
|
|
29
|
-
.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; }
|
|
30
57
|
.user .blocks { gap: 10px; }
|
|
31
58
|
.answer { display: flex; flex-direction: column; min-width: 0; overflow-wrap: anywhere; }
|
|
32
59
|
.readingText { min-width: 0; }
|
|
@@ -45,9 +72,9 @@
|
|
|
45
72
|
.blocks > :last-child { margin-bottom: 0; }
|
|
46
73
|
.blocks :global(.md-table-wide) { max-width: 100%; }
|
|
47
74
|
.disclosure { min-width: 0; }
|
|
48
|
-
.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; }
|
|
49
76
|
.disclosure[data-reader-status-only] { display: flex; align-items: center; min-height: 32px; }
|
|
50
|
-
.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); }
|
|
51
78
|
.srOnly { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
52
79
|
/* Transitions.dev — Thinking states, supplied by the user. Native DSH colors;
|
|
53
80
|
the hidden sizer shares this font and the short phases share a left edge. */
|
|
@@ -106,7 +133,20 @@
|
|
|
106
133
|
rail width, same gutter, so a column of nested rows reads as one line. */
|
|
107
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; }
|
|
108
135
|
.processContents > :last-child { margin-bottom: 0; }
|
|
109
|
-
|
|
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; }
|
|
110
150
|
.processFrame { min-width: 0; }
|
|
111
151
|
.toolActivity { min-width: 0; color: var(--dsw-alias-label-secondary); }
|
|
112
152
|
.nativeToolRow { min-width: 0; }
|
|
@@ -473,7 +513,7 @@
|
|
|
473
513
|
.morphDrawer { overflow: hidden; height: 0; transform-origin: top center; will-change: height, opacity, transform; }
|
|
474
514
|
.liveFoldImpact { animation: liveFoldElastic 180ms cubic-bezier(0.2, 0, 0, 1) both; }
|
|
475
515
|
@keyframes liveFoldElastic { 0% { transform: scale(1); } 50% { transform: scale(1.015); } 100% { transform: scale(1); } }
|
|
476
|
-
.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; }
|
|
477
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; } }
|
|
478
518
|
[data-motion=off] .liveFoldContainer { animation: none !important; }
|
|
479
519
|
@media (prefers-reduced-motion: reduce) { .liveFoldContainer { animation: none !important; } }
|
|
@@ -511,7 +551,7 @@
|
|
|
511
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; }
|
|
512
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; } }
|
|
513
553
|
@media (pointer: coarse) { .disclosureButton { min-height: 44px; } }
|
|
514
|
-
@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; } }
|
|
515
555
|
|
|
516
556
|
/* Measured flow owns geometry; steps never change React parents when folded. */
|
|
517
557
|
.choreographedFlow { display: flex; flex-direction: column; min-width: 0; overflow-anchor: none; }
|
|
@@ -532,7 +572,7 @@
|
|
|
532
572
|
input card. Only the top toolbar lane may use 7: it pins to the scrollport's
|
|
533
573
|
first lane and its containing block always ends above the seat, so it can
|
|
534
574
|
never reach the band, and 7 keeps it above the host's code-block banners. */
|
|
535
|
-
.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; }
|
|
536
576
|
/* Every sticky lane used to pin to the SAME top: the status row, the folded
|
|
537
577
|
summary row and the live-fold container all sat at
|
|
538
578
|
max(toolbar-height, status-height) with z-index 6. Three siblings competing
|
|
@@ -540,7 +580,7 @@
|
|
|
540
580
|
内容被盖住、看不见" was: the disclosure opened, and the row that followed
|
|
541
581
|
pinned straight on top of it. Give each lane its own band instead, stacked
|
|
542
582
|
from the measured top: status first, then the summary, then the fold body. */
|
|
543
|
-
.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; }
|
|
544
584
|
/* A pinned lane's in-flow gap stays visible as an uncovered scrollport strip
|
|
545
585
|
while the lane itself paints opaque. Cast each plate upward over its strip
|
|
546
586
|
(the .turn 14px gap / the prior cell's 16px flowCellInner padding) so the
|
|
@@ -549,11 +589,17 @@
|
|
|
549
589
|
toolbar's control group, which leaves the same kind of strip there; paint
|
|
550
590
|
it too. The toolbar is z-index 7, above the lane's 6, so its buttons still
|
|
551
591
|
render on top of the extension. */
|
|
552
|
-
.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; }
|
|
593
|
+
.turnStickyLanes .turnProcessSticky, .turnStickyLanes .closedProcessSummary { position: static; top: auto; z-index: auto; }
|
|
553
594
|
/* The same summary row stays in flow until it reaches the measured top lane. */
|
|
554
|
-
.flowCell[data-flow-summary], .closedProcessSummary { position: sticky; top: calc(
|
|
555
|
-
|
|
556
|
-
.
|
|
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; }
|
|
596
|
+
/* Keep the official CodeBlock banner's internal z-index inside its wrapper so Reader lanes stay above it. */
|
|
597
|
+
.root :global(.md-code-block) { z-index: 0; }
|
|
598
|
+
/* Keep native code actions below the pinned Reader lanes. */
|
|
599
|
+
.turnStickyRegion:has(> .turnStickyLanes > .closedProcessSummary), .turnStickyRegion:has(.flowCell[data-flow-summary]) { --reader-code-summary-height: var(--reader-summary-height, var(--reader-lane-block, 40px)); }
|
|
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)); }
|
|
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; }
|
|
557
603
|
.foldControl { display: inline-flex; align-items: center; gap: 0; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-module-platform); }
|
|
558
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; }
|
|
559
605
|
.foldStop[data-on] { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); }
|
|
@@ -567,6 +613,19 @@
|
|
|
567
613
|
.thinkSizer { white-space: normal; overflow-wrap: anywhere; }
|
|
568
614
|
.thinkText { white-space: normal; overflow-wrap: anywhere; overflow: visible; text-overflow: clip; }
|
|
569
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
|
+
}
|
|
570
629
|
/* Skin mode is opt-in, and it has one job: some readers run a skinned host
|
|
571
630
|
(wallpaper / theme), and the reader's own opaque rows hide that skin. On =
|
|
572
631
|
every surface the reader paints becomes a thin token-derived wash and the
|
|
@@ -578,11 +637,34 @@
|
|
|
578
637
|
.root[data-reader-glass] .liveFoldContainer {
|
|
579
638
|
background: transparent;
|
|
580
639
|
backdrop-filter: blur(16px);
|
|
581
|
-
-webkit-backdrop-filter: blur(16px);
|
|
582
640
|
border: 0;
|
|
583
641
|
border-radius: 0;
|
|
584
642
|
box-shadow: none;
|
|
585
643
|
}
|
|
644
|
+
/* Keep the skin visible between controls without changing the sticky lanes. */
|
|
645
|
+
.root[data-reader-glass] .toolbar,
|
|
646
|
+
.root[data-reader-glass] .turnProcessSticky {
|
|
647
|
+
backdrop-filter: none;
|
|
648
|
+
}
|
|
649
|
+
.root[data-reader-glass] .toolbar {
|
|
650
|
+
pointer-events: none;
|
|
651
|
+
}
|
|
652
|
+
.root[data-reader-glass] .toolbar > .textButton,
|
|
653
|
+
.root[data-reader-glass] .turnProcessSticky .disclosureButton {
|
|
654
|
+
pointer-events: auto;
|
|
655
|
+
padding: 2px 10px;
|
|
656
|
+
border-radius: 999px;
|
|
657
|
+
background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, .7)) 80%, transparent);
|
|
658
|
+
backdrop-filter: blur(12px);
|
|
659
|
+
}
|
|
660
|
+
.root[data-reader-glass] .turnProcessSticky .disclosureButton {
|
|
661
|
+
margin-left: 0;
|
|
662
|
+
max-width: 100%;
|
|
663
|
+
}
|
|
664
|
+
.root[data-reader-glass] .toolbar > .textButton:hover,
|
|
665
|
+
.root[data-reader-glass] .turnProcessSticky .disclosureButton:hover {
|
|
666
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
667
|
+
}
|
|
586
668
|
/* The z-index ladder is mode-independent: skin mode only drops the plate, so
|
|
587
669
|
a glass reader keeps exactly the same footer precedence as an opaque one. */
|
|
588
670
|
.root[data-reader-glass] .flowCell[data-flow-summary],
|
|
@@ -597,11 +679,13 @@
|
|
|
597
679
|
padding-left: 10px;
|
|
598
680
|
border-radius: 0;
|
|
599
681
|
}
|
|
682
|
+
.root[data-reader-glass] .turnStickyLanes { display: contents; position: static; top: auto; z-index: auto; }
|
|
683
|
+
.root[data-reader-glass] .turnStickyRegion { --reader-code-summary-height: 0px; }
|
|
684
|
+
.root[data-reader-glass] .turnStickyLanes .turnProcessSticky { position: sticky; top: var(--reader-toolbar-height, var(--reader-lane-block, 40px)); z-index: 6; }
|
|
600
685
|
/* User bubble becomes a sleek translucent frosted pill in skin mode */
|
|
601
686
|
.root[data-reader-glass] .user {
|
|
602
687
|
background: color-mix(in srgb, var(--dsw-specific-bubble, rgba(0,0,0,.08)) 25%, transparent);
|
|
603
688
|
backdrop-filter: blur(12px);
|
|
604
|
-
-webkit-backdrop-filter: blur(12px);
|
|
605
689
|
border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 30%, transparent);
|
|
606
690
|
}
|
|
607
691
|
/* Skin mode paints no plates of the reader's own: cards, panels and frames go
|
|
@@ -625,7 +709,6 @@
|
|
|
625
709
|
.root[data-reader-glass] .diffOverlay {
|
|
626
710
|
background: color-mix(in srgb, var(--dsw-alias-bg-module-platform, rgba(127,127,127,.1)) 20%, transparent);
|
|
627
711
|
backdrop-filter: blur(16px);
|
|
628
|
-
-webkit-backdrop-filter: blur(16px);
|
|
629
712
|
}
|
|
630
713
|
.root[data-reader-glass] .diffScrollArea {
|
|
631
714
|
background: transparent;
|
|
@@ -642,7 +725,6 @@
|
|
|
642
725
|
background: color-mix(in srgb, var(--dsw-alias-markdown-code-block, rgba(0,0,0,.06)) 25%, transparent) !important;
|
|
643
726
|
border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 30%, transparent);
|
|
644
727
|
backdrop-filter: blur(12px);
|
|
645
|
-
-webkit-backdrop-filter: blur(12px);
|
|
646
728
|
}
|
|
647
729
|
/* The fold control rides a sticky row: it must not paint a plate of its own. */
|
|
648
730
|
.root[data-reader-glass] .foldControl {
|