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
package/src/client/Reader.tsx
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type {} from '@deepseek-ai/dsh-session-turn-outline/types';
|
|
2
2
|
import type {} from '@deepseek-ai/dsh-agent/types';
|
|
3
|
-
import { Fragment, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { Fragment, memo, useCallback, useEffect, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
import type { ReactNode, RefObject } from 'react';
|
|
5
|
-
import type { ChatConversationViewNode, ChatNode, ChatNodeKind } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
5
|
+
import type { ChatConversationViewNode, ChatNode, ChatNodeKind, ChatNodeOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
6
|
+
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
6
7
|
import { JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
7
|
-
import { BlockBoundary, Blocks, contentBlocks,
|
|
8
|
-
import {
|
|
8
|
+
import { BlockBoundary, Blocks, contentBlocks, UserMessageActions } from './Blocks.js';
|
|
9
|
+
import { AssistantNode } from './AssistantNode.js';
|
|
9
10
|
import { ToolActivity, ToolMedia } from './ToolActivity.js';
|
|
10
|
-
import {
|
|
11
|
+
import { OfficialNode, OfficialTail } from './OfficialContent.js';
|
|
12
|
+
import { OFFICIAL_SEATS } from './official-slots.js';
|
|
11
13
|
import { preparingLabel, readerFlow } from './tool-activity.js';
|
|
12
|
-
import { Disclosure, ProcessFragment,
|
|
14
|
+
import { Disclosure, ProcessFragment, RetiringTurns, StatusText, useMotionAllowed, usePinnedSelection, useReadingScroll } from './motion.js';
|
|
13
15
|
import { StreamMotionContext } from './streaming.js';
|
|
14
|
-
import {
|
|
16
|
+
import { boundaryOf, forkAnchorSeq, groupNodes, hasProcessContent, processChoiceKey, processExpanded, terminalLabel } from './projection.js';
|
|
15
17
|
import { basename, createProducedFileMentions, dirname, getTurnDeliverables, showDeliverablesRow } from './deliverables.js';
|
|
16
18
|
import { deliverableOpenModeOf, type DeliverableOpenMode } from './open-file.js';
|
|
17
19
|
import { asReadonlyArray, pendingSubmissionImages, type PendingSubmissionEcho } from './pending-submission.js';
|
|
@@ -21,17 +23,19 @@ import { ContextInjectionRow } from './native/ContextInjectionRow.js';
|
|
|
21
23
|
import { TimelineRail } from './TimelineRail.js';
|
|
22
24
|
import { landTurn, scrollerOf } from './conversation-scroll.js';
|
|
23
25
|
import { mergeTimelineItems, type TimelineItem } from './timeline.js';
|
|
24
|
-
import { presentLiveTurn, segmentLiveTurn } from './live-turn.js';
|
|
26
|
+
import { planProcessSeams, presentLiveTurn, segmentLiveTurn } from './live-turn.js';
|
|
25
27
|
import type { LiveStep } from './live-turn.js';
|
|
26
28
|
import { frostedGlassOf } from './fold-intensity.js';
|
|
27
29
|
import { ChoreographedFlow, useFlowChat } from './ChoreographedFlow.js';
|
|
28
30
|
import { ClosedProcessSummary } from './ClosedProcessSummary.js';
|
|
29
|
-
import { StickyLane } from './StickyLane.js';
|
|
31
|
+
import { ReaderLaneScope, StickyLane } from './StickyLane.js';
|
|
30
32
|
import type { ReaderGroup, TurnBoundary } from './projection.js';
|
|
31
33
|
import type { BlockRenderProps, ReaderProps } from './types.js';
|
|
32
34
|
import css from './Reader.module.css';
|
|
33
35
|
import { markdownLabels, truncatedJsonLabel } from './primitive-labels.js';
|
|
34
36
|
import { chatSeatProps } from './chat-seat.js';
|
|
37
|
+
import { visibleChatKeys, visibleChatSignature } from './rewind-cut.js';
|
|
38
|
+
import { ensureStyles } from './styles.js';
|
|
35
39
|
|
|
36
40
|
function isNode<K extends ChatNodeKind>(node: ChatConversationViewNode, kind: K): node is ChatNode<K> {
|
|
37
41
|
return node.kind === kind;
|
|
@@ -74,7 +78,7 @@ function cleanErrorMessage(raw: string | undefined): string {
|
|
|
74
78
|
return str;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat'> & {
|
|
81
|
+
type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat' | 'sessionId'> & {
|
|
78
82
|
nodeKey: string; boundary: TurnBoundary; pinned?: boolean; processOpen?: boolean;
|
|
79
83
|
};
|
|
80
84
|
|
|
@@ -167,53 +171,9 @@ const ProcessNode = memo(function ProcessNode({ useChat, t, nodeKey, open, motio
|
|
|
167
171
|
return content && <ProcessFragment open={open} motion={motion} onRead={onRead} returnFocusTo={returnFocusTo} nodeKey={nodeKey} framed>{content}</ProcessFragment>;
|
|
168
172
|
});
|
|
169
173
|
|
|
170
|
-
const
|
|
171
|
-
motion: boolean; onRead: () => void; returnFocusTo: RefObject<HTMLButtonElement>; partStart?: number; folded?: boolean;
|
|
172
|
-
}) {
|
|
173
|
-
const node = useChat(snapshot => snapshot.nodes.get(nodeKey));
|
|
174
|
-
if (!node || node.visibility === 'hidden' || !isNode(node, 'assistant-step')) return null;
|
|
175
|
-
const data = node.data;
|
|
176
|
-
const parts = assistantSegments(data.blocks);
|
|
177
|
-
const earlier = isEarlierNarration(data, boundary);
|
|
178
|
-
const hasToolCalls = data.blocks.some(block => block.kind === 'tool-call');
|
|
179
|
-
const isProcessStep = earlier || folded || hasToolCalls || (boundary.latestStep > 0 && data.step < boundary.latestStep);
|
|
180
|
-
const body = data.blocks.filter(block => block.kind !== 'reasoning' && block.kind !== 'tool-call');
|
|
181
|
-
const visible = partStart === undefined ? parts : parts.filter(part => part.start === partStart);
|
|
182
|
-
return <>{visible.map(part => {
|
|
183
|
-
const index = parts.findIndex(item => item.start === part.start);
|
|
184
|
-
const last = index === parts.length - 1;
|
|
185
|
-
return part.kind === 'reasoning'
|
|
186
|
-
? <ProcessFragment key={part.start} open={processOpen} motion={motion} onRead={onRead} returnFocusTo={returnFocusTo} nodeKey={nodeKey} framed>
|
|
187
|
-
<ReasoningCard step={data.step} active={processOpen && boundary.status === 'open' && data.step === boundary.latestStep} motion={motion} selected={pinned} onRead={onRead}>
|
|
188
|
-
<Blocks {...render} blocks={part.blocks} streaming={data.status === 'running' && last && data.blocks.at(-1)?.kind === 'reasoning'}
|
|
189
|
-
holdFormatting={pinned} startedAt={data.time} interrupted={data.status === 'interrupted'} liveText />
|
|
190
|
-
</ReasoningCard>
|
|
191
|
-
</ProcessFragment>
|
|
192
|
-
: isProcessStep ? <ProcessFragment key={part.start} open={processOpen} motion={motion} onRead={onRead} returnFocusTo={returnFocusTo} nodeKey={nodeKey}>
|
|
193
|
-
<article className={css.processCommentary}>
|
|
194
|
-
<Blocks {...render} blocks={part.blocks} streaming={data.status === 'running'} holdFormatting={pinned} startedAt={data.time} interrupted={data.status === 'interrupted'} liveText />
|
|
195
|
-
</article>
|
|
196
|
-
</ProcessFragment>
|
|
197
|
-
: hasVisibleBody(part.blocks) && <RetiringContent key={part.start} visible={pinned || processOpen || (!earlier && !folded)}>
|
|
198
|
-
<article className={css.answer} data-reader-answer data-reader-anchor data-reader-key={nodeKey} data-reader-source-start={part.start} data-answer-status={data.status} data-answer-phase={earlier || folded ? 'process' : 'body'}>
|
|
199
|
-
<Blocks {...render} blocks={part.blocks} streaming={data.status === 'running'} holdFormatting={pinned} startedAt={data.time} interrupted={data.status === 'interrupted'} liveText />
|
|
200
|
-
{last && data.status === 'interrupted' && <span className={css.stopped}>已停止</span>}
|
|
201
|
-
{last && !earlier && !folded && data.status !== 'running' && boundary.status === 'closed' && (
|
|
202
|
-
<CopyAnswer blocks={body} onFork={(() => {
|
|
203
|
-
// The fork anchor must be the durable closing message seq (same as
|
|
204
|
-
// the official turn-tail branch). AssistantChatData carries no seq
|
|
205
|
-
// of its own; passing it would fork the whole session instead.
|
|
206
|
-
const anchor = forkAnchorSeq([data.finalNode, { seq: render.forkSeq }]);
|
|
207
|
-
return render.forkAt && anchor !== undefined ? () => render.forkAt!(anchor) : undefined;
|
|
208
|
-
})()} metrics={render.metrics} extraActions={<OfficialActions official={render.official} messageId={data.finalNode?.messageId} />} />
|
|
209
|
-
)}
|
|
210
|
-
</article>
|
|
211
|
-
</RetiringContent>;
|
|
212
|
-
})}</>;
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, processOpen = false, ...render }: SeatProps) {
|
|
174
|
+
const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, processOpen: _processOpen = false, ...render }: SeatProps) {
|
|
216
175
|
const node = useChat(snapshot => snapshot.nodes.get(nodeKey));
|
|
176
|
+
const [disclosureReset] = useState(() => createSnapshotStore(0));
|
|
217
177
|
if (!node || node.visibility === 'hidden') return null;
|
|
218
178
|
if (isNode(node, 'user') || isNode(node, 'steering')) {
|
|
219
179
|
const blocks = contentBlocks(node.data.content);
|
|
@@ -221,7 +181,7 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
|
|
|
221
181
|
const otherBlocks = blocks.filter(b => b.kind !== 'image');
|
|
222
182
|
const text = otherBlocks.filter((block): block is Extract<typeof block, { kind: 'text' }> => block.kind === 'text').map(block => block.text).join('\n\n');
|
|
223
183
|
const time = node.data.time;
|
|
224
|
-
|
|
184
|
+
const fallback = <>
|
|
225
185
|
{node.kind === 'steering' && <p className={css.meta}>补充消息</p>}
|
|
226
186
|
{imageBlocks.length > 0 && <div className={css.userImages}>
|
|
227
187
|
<Blocks {...render} blocks={imageBlocks} source="user" />
|
|
@@ -230,7 +190,30 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
|
|
|
230
190
|
<Blocks {...render} blocks={otherBlocks} source="user" />
|
|
231
191
|
</div>}
|
|
232
192
|
<UserMessageActions text={text} time={time} />
|
|
233
|
-
|
|
193
|
+
</>;
|
|
194
|
+
const official = render.official;
|
|
195
|
+
let rendered: ReactNode = fallback;
|
|
196
|
+
if (official) {
|
|
197
|
+
const turn = node.location.kind === 'turn' || node.location.kind === 'step' ? node.location.turn : undefined;
|
|
198
|
+
const owner: ChatNodeOwnerProps & Pick<ReaderProps, 'sessionId'> = {
|
|
199
|
+
cwd: render.cwd,
|
|
200
|
+
openFile: render.openFile ?? (() => {}),
|
|
201
|
+
openSkill: () => {},
|
|
202
|
+
forkAt: render.forkAt ?? (() => {}),
|
|
203
|
+
inspectCall: callId => official.openView('trajectory', callId),
|
|
204
|
+
loadImage: official.officialImageLoader,
|
|
205
|
+
renderMessageImages: images => official.renderSlot(OFFICIAL_SEATS.images, {
|
|
206
|
+
...images, loadImage: official.officialImageLoader,
|
|
207
|
+
}),
|
|
208
|
+
fileMentions: official.officialFileMentions,
|
|
209
|
+
sessionId: render.sessionId,
|
|
210
|
+
};
|
|
211
|
+
const options = { hookContext: { turnData: turn?.data, disclosureReset }, fallback };
|
|
212
|
+
rendered = node.kind === 'user'
|
|
213
|
+
? official.renderSlot(OFFICIAL_SEATS.nodes, { ...owner, node }, { ...options, entryKey: 'user' })
|
|
214
|
+
: official.renderSlot(OFFICIAL_SEATS.nodes, { ...owner, node }, { ...options, entryKey: 'steering' });
|
|
215
|
+
}
|
|
216
|
+
return <div className={css.userCluster} data-reader-anchor data-reader-key={nodeKey} {...chatSeatProps(node.kind, nodeKey)}>{rendered}</div>;
|
|
234
217
|
}
|
|
235
218
|
if (isNode(node, 'assistant-step')) return null;
|
|
236
219
|
if (isNode(node, 'tool-call')) return <ToolMedia {...render} block={node.data.root} />;
|
|
@@ -295,7 +278,7 @@ function subagentCount(snapshot: { nodes: { get(key: string): ChatConversationVi
|
|
|
295
278
|
return 0;
|
|
296
279
|
}
|
|
297
280
|
|
|
298
|
-
function GroupStatus({ group,
|
|
281
|
+
function GroupStatus({ group, useChat, pending, motion }: Pick<ReaderProps, 'useChat'> & { group: ReaderGroup; motion: boolean; pending: { kind?: string } | undefined }) {
|
|
299
282
|
const text = useChat(snapshot => {
|
|
300
283
|
const turn = group.turn === null ? undefined : snapshot.timeline.turns.get(group.turn);
|
|
301
284
|
if (turn?.status === 'closed') {
|
|
@@ -511,7 +494,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
511
494
|
const firstKind = props.useChat(snapshot => snapshot.nodes.get(group.keys[0])?.kind);
|
|
512
495
|
const startsWithUser = firstKind === 'user';
|
|
513
496
|
const mainKeys = startsWithUser ? group.keys.slice(1) : group.keys;
|
|
514
|
-
const flow = useMemo(() => readerFlow({ ...group, keys: mainKeys }, turn, key => nodes.get(key)), [nodes, group, mainKeys, turn]);
|
|
497
|
+
const flow = useMemo(() => readerFlow({ ...group, keys: mainKeys }, turn, key => nodes.get(key), snapshot.order), [nodes, group, mainKeys, turn, snapshot.order]);
|
|
515
498
|
const steps = useMemo(() => segmentLiveTurn(flow, key => nodes.get(key)), [flow, nodes]);
|
|
516
499
|
const liveItems = useMemo(() => presentLiveTurn(steps, boundary, autoFold), [steps, boundary, autoFold]);
|
|
517
500
|
const openMode = deliverableOpenModeOf(useSyncExternalStore(
|
|
@@ -520,6 +503,8 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
520
503
|
() => 'external',
|
|
521
504
|
));
|
|
522
505
|
const hasProcess = flow.some(item => item.kind === 'tool' || hasProcessContent(nodes.get(item.nodeKey), boundary));
|
|
506
|
+
const showStatusLane = hasProcess && !isAwaitingModel;
|
|
507
|
+
const showClosedSummary = boundary.status === 'closed' && hasProcess && autoFold;
|
|
523
508
|
// Only a real, still-active text selection delays folding. Merely clicking,
|
|
524
509
|
// focusing or scrolling the live card does not create a permanent override.
|
|
525
510
|
const holdingSelection = selectedProcessKeys.some(key =>
|
|
@@ -528,6 +513,12 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
528
513
|
? item.key === key || item.steps.some(step => step.key === key || ('nodeKey' in step && step.nodeKey === key))
|
|
529
514
|
: item.key === key || ('nodeKey' in item.step && item.step.nodeKey === key)));
|
|
530
515
|
const expanded = !autoFold || holdingSelection || processExpanded(expansionChoice, boundary);
|
|
516
|
+
// Seam between narration blocks. Skip a run a live-fold cell already
|
|
517
|
+
// covers, and do not drop the previous body's bottom spacing.
|
|
518
|
+
const seamPlan = useMemo(
|
|
519
|
+
() => planProcessSeams(steps, liveItems, expanded),
|
|
520
|
+
[steps, liveItems, expanded],
|
|
521
|
+
);
|
|
531
522
|
const [foldOpenByKey, setFoldOpenByKey] = useState<Record<string, boolean>>({});
|
|
532
523
|
useLayoutEffect(() => {
|
|
533
524
|
// Restoring automatic folding also releases live "prior steps" disclosures.
|
|
@@ -586,6 +577,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
586
577
|
fileMentions,
|
|
587
578
|
metrics,
|
|
588
579
|
official,
|
|
580
|
+
sessionId: props.sessionId,
|
|
589
581
|
cwd,
|
|
590
582
|
};
|
|
591
583
|
const terminal = terminalLabel(boundary.reason);
|
|
@@ -600,7 +592,9 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
600
592
|
const renderStep = (step: LiveStep, folded: boolean) => {
|
|
601
593
|
const processOpen = folded || expanded;
|
|
602
594
|
if (step.kind === 'reasoning' || step.kind === 'body') return <BlockBoundary>
|
|
603
|
-
<AssistantNode {...shared} boundary={boundary} nodeKey={step.nodeKey} partStart={step.start} pinned={pinnedKeys.includes(step.nodeKey)} processOpen={processOpen} folded={folded} motion={motion} onRead={pinProcess} returnFocusTo={processButton}
|
|
595
|
+
<AssistantNode {...shared} boundary={boundary} nodeKey={step.nodeKey} partStart={step.start} pinned={pinnedKeys.includes(step.nodeKey)} processOpen={processOpen} folded={folded} motion={motion} onRead={pinProcess} returnFocusTo={processButton}
|
|
596
|
+
seamSummary={step.kind === 'body' ? seamPlan.before.get(step.key) : undefined}
|
|
597
|
+
seamAfter={step.kind === 'body' && seamPlan.after.has(step.key)} />
|
|
604
598
|
</BlockBoundary>;
|
|
605
599
|
if (step.kind === 'tool') return <BlockBoundary><ProcessFragment open={processOpen} motion={motion} onRead={pinProcess} returnFocusTo={processButton} nodeKey={step.key} framed>
|
|
606
600
|
<ToolActivity {...shared} entry={step.entry} motion={motion} turnClosed={boundary.status === 'closed'} onRead={pinProcess} />
|
|
@@ -614,25 +608,22 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
614
608
|
};
|
|
615
609
|
return <section className={css.turn} data-reader-turn={group.turn ?? 'unresolved'} data-reader-turn-state={boundary.status} data-reader-turn-result={boundary.reason ?? undefined}>
|
|
616
610
|
{startsWithUser && <BlockBoundary><MainNode {...shared} useChat={props.useChat} boundary={boundary} nodeKey={group.keys[0]} /></BlockBoundary>}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
<ChoreographedFlow id={flowId} frame={presentation} motion={motion} enabled={autoFold && boundary.status === 'open' && !holdingSelection}
|
|
631
|
-
urgent={hasTurnError || interaction !== undefined || !sessionRunning} open={foldOpenByKey} processOpen={expanded}
|
|
632
|
-
onOpenChange={(key, value) => { pinProcess(); setFoldOpenByKey(current => ({ ...current, [key]: value })); }} renderStep={renderStep} />
|
|
611
|
+
<div className={css.turnStickyRegion} data-reader-sticky-region>
|
|
612
|
+
{(showStatusLane || showClosedSummary) && <div className={css.turnStickyLanes}>
|
|
613
|
+
{showStatusLane && <StickyLane kind="status" className={css.turnProcessSticky}>
|
|
614
|
+
<Disclosure open={expanded} onChange={setExpanded} controls={flowId} buttonRef={processButton}
|
|
615
|
+
label={<GroupStatus group={group} useChat={props.useChat} pending={interaction} motion={motion} />} />
|
|
616
|
+
</StickyLane>}
|
|
617
|
+
{showClosedSummary && <ClosedProcessSummary open={expanded} onChange={setExpanded} controls={flowId}
|
|
618
|
+
steps={steps.filter(step => step.kind !== 'user' && step.kind !== 'body')} />}
|
|
619
|
+
</div>}
|
|
620
|
+
<ChoreographedFlow id={flowId} frame={presentation} motion={motion} enabled={autoFold && boundary.status === 'open' && !holdingSelection}
|
|
621
|
+
urgent={hasTurnError || interaction !== undefined || !sessionRunning} open={foldOpenByKey} processOpen={expanded}
|
|
622
|
+
onOpenChange={(key, value) => { pinProcess(); setFoldOpenByKey(current => ({ ...current, [key]: value })); }} renderStep={renderStep} />
|
|
623
|
+
</div>
|
|
633
624
|
{/* 状态指示永远排在流程之后:AI 的响应永远出现在最新消息(含补充消息)的下方 */}
|
|
634
625
|
{!hasProcess && boundary.status === 'open' && !isAwaitingModel && <div className={css.disclosure} data-reader-status-only>
|
|
635
|
-
<GroupStatus group={group}
|
|
626
|
+
<GroupStatus group={group} useChat={props.useChat} pending={interaction} motion={motion} />
|
|
636
627
|
</div>}
|
|
637
628
|
{showDeliverablesRow(boundary.status, deliverables) && <DeliverablesRow deliverables={deliverables} openFile={props.openFile} revealFile={props.revealFile} openMode={openMode} />}
|
|
638
629
|
{boundary.status === 'closed' && <BlockBoundary><OfficialTail official={official} owner={tailOwner} produced={deliverables} /></BlockBoundary>}
|
|
@@ -641,10 +632,14 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
|
|
|
641
632
|
});
|
|
642
633
|
|
|
643
634
|
export function Reader(props: ReaderProps) {
|
|
635
|
+
useInsertionEffect(() => { ensureStyles(); }, []);
|
|
644
636
|
const root = useRef<HTMLDivElement>(null);
|
|
645
637
|
const activatedAt = useRef(Date.now());
|
|
646
638
|
const order = props.useChat(snapshot => snapshot.order);
|
|
647
639
|
const nodes = props.useChat(snapshot => snapshot.nodes);
|
|
640
|
+
// A rewind upserts the command row in place, so order and nodes keep their identity.
|
|
641
|
+
const rewindSignature = props.useChat(snapshot => visibleChatSignature(snapshot));
|
|
642
|
+
const renderKeys = useMemo(() => visibleChatKeys({ order, nodes }), [order, nodes, rewindSignature]);
|
|
648
643
|
const timeline = props.useChat(snapshot => snapshot.timeline);
|
|
649
644
|
const running = props.useSession(snapshot => snapshot.running);
|
|
650
645
|
const pending = props.useSessionStatus(snapshot => snapshot.get(props.sessionId)?.pendingInteraction);
|
|
@@ -673,7 +668,7 @@ export function Reader(props: ReaderProps) {
|
|
|
673
668
|
rows.push(...local.values());
|
|
674
669
|
return rows;
|
|
675
670
|
}, [inboxSteering, pendingList, visibleSubmissions]);
|
|
676
|
-
const waitAnchor = waitingAnchor(
|
|
671
|
+
const waitAnchor = waitingAnchor(renderKeys, key => nodes.get(key), pendingList);
|
|
677
672
|
const motionPreference = props.useStore(state => state.motion);
|
|
678
673
|
const motion = useMotionAllowed(motionPreference);
|
|
679
674
|
const prefsSnap = useSyncExternalStore(
|
|
@@ -693,14 +688,14 @@ export function Reader(props: ReaderProps) {
|
|
|
693
688
|
}, [autoFold, props.actions]);
|
|
694
689
|
const frostedGlass = frostedGlassOf(prefsSnap);
|
|
695
690
|
const streamMotion = useMemo(() => ({ enabled: motion, activatedAt: activatedAt.current }), [motion]);
|
|
696
|
-
const groups = useMemo(() => groupNodes(
|
|
691
|
+
const groups = useMemo(() => groupNodes(renderKeys, key => nodes.get(key)), [renderKeys, nodes, timeline]);
|
|
697
692
|
const isAwaitingModel = useMemo(() => {
|
|
698
693
|
// 状态机铁律:「深度求索中」与「正在处理/思考」永远互斥。
|
|
699
694
|
// 1. 仅空闲发送(!running)的本地回显窗口:用户按下回车第 0 毫秒立即反馈。
|
|
700
695
|
// 忙碌时的 queued/steering 回显不算等待——模型本来就在工作。
|
|
701
696
|
if (!running && pendingList.length > 0) return true;
|
|
702
697
|
|
|
703
|
-
const lastKey =
|
|
698
|
+
const lastKey = renderKeys.at(-1);
|
|
704
699
|
const lastNode = lastKey ? nodes.get(lastKey) : undefined;
|
|
705
700
|
if (!lastKey || !lastNode) return false;
|
|
706
701
|
|
|
@@ -725,7 +720,7 @@ export function Reader(props: ReaderProps) {
|
|
|
725
720
|
const lastGroup = groups.at(-1);
|
|
726
721
|
const turn = lastGroup?.turn === null || lastGroup?.turn === undefined ? undefined : timeline.turns.get(lastGroup.turn);
|
|
727
722
|
return turn?.status !== 'closed';
|
|
728
|
-
}, [running, pendingList,
|
|
723
|
+
}, [running, pendingList, renderKeys, nodes, groups, timeline]);
|
|
729
724
|
const scroll = useReadingScroll(root, motion);
|
|
730
725
|
const pinnedKeys = usePinnedSelection(root);
|
|
731
726
|
const selectedProcessKeys = usePinnedSelection(root, '[data-reader-process]');
|
|
@@ -833,7 +828,7 @@ export function Reader(props: ReaderProps) {
|
|
|
833
828
|
}
|
|
834
829
|
}, [props.loadThrough, props.loadOlder, scroll.jump, scroll.release, timelineItems]);
|
|
835
830
|
|
|
836
|
-
const lastKey =
|
|
831
|
+
const lastKey = renderKeys.at(-1);
|
|
837
832
|
const lastNode = lastKey ? nodes.get(lastKey) : undefined;
|
|
838
833
|
const lastSubmissionId = pendingList.length ? pendingList[pendingList.length - 1].requestId : null;
|
|
839
834
|
const lastOrderKeyRef = useRef<string | undefined>(lastKey);
|
|
@@ -852,9 +847,9 @@ export function Reader(props: ReaderProps) {
|
|
|
852
847
|
|
|
853
848
|
// ChatView publishes data-chat-flow="" on its column. Skins treat a
|
|
854
849
|
// scrollport without that hook as inspect-only and hide [data-composer-seat].
|
|
855
|
-
return <StreamMotionContext.Provider value={streamMotion}><div ref={root} className={css.root} data-reader-build="0.3.4" data-dsh-better-display="0.3.4" data-reader-wait-clock-version="input-v1" data-reader-wait-start={waitAnchor.time ?? undefined} data-motion={motion ? 'on' : 'off'} data-reader-glass={frostedGlass || undefined} data-reader-auto-fold={autoFold ? 'on' : 'off'}>
|
|
850
|
+
return <StreamMotionContext.Provider value={streamMotion}><ReaderLaneScope><div ref={root} className={css.root} data-reader-build="0.3.4" data-dsh-better-display="0.3.4" data-reader-wait-clock-version="input-v1" data-reader-wait-start={waitAnchor.time ?? undefined} data-motion={motion ? 'on' : 'off'} data-reader-glass={frostedGlass || undefined} data-reader-auto-fold={autoFold ? 'on' : 'off'}>
|
|
856
851
|
<TimelineRail items={timelineItems} activeTurn={activeTurn} busyTurn={busyTurn} onNavigate={onNavigateTurn} />
|
|
857
|
-
<div className={css.column} data-chat-flow="">
|
|
852
|
+
<div className={css.column} data-chat-flow="" tabIndex={-1}>
|
|
858
853
|
<StickyLane kind="toolbar" className={css.toolbar}>
|
|
859
854
|
<button
|
|
860
855
|
type="button"
|
|
@@ -876,12 +871,12 @@ export function Reader(props: ReaderProps) {
|
|
|
876
871
|
{historyError && <div className={css.notice}>历史记录加载失败,可再次尝试;现有内容未改变。</div>}
|
|
877
872
|
{openError && <div className={css.error} role="alert">会话暂时无法读取:{openError.message}</div>}
|
|
878
873
|
{loading && groups.length === 0 && <p className={css.empty} role="status">正在读取会话…</p>}
|
|
879
|
-
{groups
|
|
874
|
+
<RetiringTurns groups={groups} render={group => <TurnGroup {...props} group={group} motion={motion} autoFold={autoFold} pinnedKeys={pinnedKeys} selectedProcessKeys={selectedProcessKeys} isAwaitingModel={isAwaitingModel && group.key === groups.at(-1)?.key} />} />
|
|
880
875
|
{pendingInputs.map((item, index) => {
|
|
881
876
|
const submission = 'requestId' in item ? item : undefined;
|
|
882
877
|
const text = submission?.text ?? inboxText(item as PendingInboxItem);
|
|
883
878
|
const images = submission ? pendingSubmissionImages(submission) : [];
|
|
884
|
-
const attachmentCount = images.length;
|
|
879
|
+
const attachmentCount = submission ? images.length : inboxAttachmentCount(item as PendingInboxItem);
|
|
885
880
|
const requestId = submission?.requestId ?? `inbox-${index}`;
|
|
886
881
|
return (
|
|
887
882
|
<div key={requestId} className={css.userCluster} data-reader-pending-submission data-pending-steering={submission ? undefined : ''} data-pending-attachment-count={attachmentCount}>
|
|
@@ -914,5 +909,5 @@ export function Reader(props: ReaderProps) {
|
|
|
914
909
|
<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true"><path d="M3 5.5 7 9.5 11 5.5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
|
915
910
|
</button></div>}
|
|
916
911
|
</div>
|
|
917
|
-
</div></StreamMotionContext.Provider>;
|
|
912
|
+
</div></ReaderLaneScope></StreamMotionContext.Provider>;
|
|
918
913
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { frostedGlassOf, type FoldIntensity } from './fold-intensity.js';
|
|
3
3
|
import { deliverableOpenModeOf, type DeliverableOpenMode } from './open-file.js';
|
|
4
4
|
import { settingsCopyFor, type SettingsCopy, type SettingsCopyKey } from './settings-copy.js';
|
|
5
5
|
import { CONVENTIONAL_SKILL_ROOTS, detectGenerativeMcpappsSkill, shortestInstallCommand, type SkillStatusProbe, type SkillStatusSnapshot } from './skill-status.js';
|
|
@@ -49,12 +49,6 @@ function text(props: SettingsProps, copy: SettingsCopy, key: keyof SettingsCopy)
|
|
|
49
49
|
return copy[key];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const FOLD_STOPS: { value: FoldIntensity; key: 'foldNone' | 'foldStandard' | 'foldSummary' }[] = [
|
|
53
|
-
{ value: 0, key: 'foldNone' },
|
|
54
|
-
{ value: 1, key: 'foldStandard' },
|
|
55
|
-
{ value: 2, key: 'foldSummary' },
|
|
56
|
-
];
|
|
57
|
-
|
|
58
52
|
export function SettingsSection(props: SettingsProps) {
|
|
59
53
|
const copy = props.copy ?? settingsCopyFor(props.languageTag);
|
|
60
54
|
const snap = useSyncExternalStore(
|
|
@@ -1,6 +1,85 @@
|
|
|
1
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
1
|
+
import { createContext, useContext, useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
+
const MIN_LANE_PX = 40;
|
|
5
|
+
type LaneOwners = Map<string, HTMLElement>;
|
|
6
|
+
|
|
7
|
+
const LaneOwnerContext = createContext<LaneOwners | null>(null);
|
|
8
|
+
const ownersByRoot = new WeakMap<HTMLElement, LaneOwners>();
|
|
9
|
+
|
|
10
|
+
/** One map per Reader, so two roots that both contain turn 1 do not share a writer. */
|
|
11
|
+
export function ReaderLaneScope({ children }: { children: ReactNode }) {
|
|
12
|
+
const owners = useRef<LaneOwners>(new Map());
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
const map = owners.current;
|
|
15
|
+
return () => { map.clear(); };
|
|
16
|
+
}, []);
|
|
17
|
+
return <LaneOwnerContext.Provider value={owners.current}>{children}</LaneOwnerContext.Provider>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function laneScope(el: HTMLElement) {
|
|
21
|
+
return el.closest<HTMLElement>('[data-reader-turn]') ?? el.closest<HTMLElement>('[data-dsh-better-display]');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ownersFor(el: HTMLElement, scoped: LaneOwners | null) {
|
|
25
|
+
if (scoped) return scoped;
|
|
26
|
+
const root = el.closest<HTMLElement>('[data-dsh-better-display]') ?? el;
|
|
27
|
+
let owners = ownersByRoot.get(root);
|
|
28
|
+
if (!owners) {
|
|
29
|
+
owners = new Map();
|
|
30
|
+
ownersByRoot.set(root, owners);
|
|
31
|
+
}
|
|
32
|
+
return owners;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function publishedLanePx(el: HTMLElement) {
|
|
36
|
+
return Math.max(MIN_LANE_PX, Math.ceil(el.getBoundingClientRect().height));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One writer per turn inside a single Reader. A retiring summary cannot clear the lane that replaced it. */
|
|
40
|
+
export function publishLaneHeight(el: HTMLElement, property: string, scoped: LaneOwners | null = null) {
|
|
41
|
+
const scope = laneScope(el);
|
|
42
|
+
if (!scope) return () => {};
|
|
43
|
+
const owners = ownersFor(el, scoped);
|
|
44
|
+
const root = el.closest<HTMLElement>('[data-dsh-better-display]');
|
|
45
|
+
const key = `${property}\0${scope.getAttribute('data-reader-turn') ?? 'root'}`;
|
|
46
|
+
const update = () => {
|
|
47
|
+
if (owners.get(key) !== el) return;
|
|
48
|
+
scope.style.setProperty(property, `${publishedLanePx(el)}px`);
|
|
49
|
+
};
|
|
50
|
+
owners.set(key, el);
|
|
51
|
+
update();
|
|
52
|
+
const observer = new ResizeObserver(update);
|
|
53
|
+
observer.observe(el);
|
|
54
|
+
return () => {
|
|
55
|
+
observer.disconnect();
|
|
56
|
+
if (owners.get(key) !== el) return;
|
|
57
|
+
owners.delete(key);
|
|
58
|
+
scope.style.removeProperty(property);
|
|
59
|
+
if (!scoped && root && owners.size === 0) ownersByRoot.delete(root);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function usePublishedHeight(property: string) {
|
|
64
|
+
const owners = useContext(LaneOwnerContext);
|
|
65
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
66
|
+
useLayoutEffect(() => {
|
|
67
|
+
const el = ref.current;
|
|
68
|
+
if (!el) return;
|
|
69
|
+
return publishLaneHeight(el, property, owners);
|
|
70
|
+
}, [owners, property]);
|
|
71
|
+
return ref;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useSummaryLane(ref: { readonly current: HTMLElement | null }, active: boolean) {
|
|
75
|
+
const owners = useContext(LaneOwnerContext);
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
const el = ref.current;
|
|
78
|
+
if (!active || !el) return;
|
|
79
|
+
return publishLaneHeight(el, '--reader-summary-height', owners);
|
|
80
|
+
}, [active, owners, ref]);
|
|
81
|
+
}
|
|
82
|
+
|
|
4
83
|
/** Publish real lane geometry; wrapped labels must not collide with statistics. */
|
|
5
84
|
export function StickyLane({ kind, className, children }: { kind: 'toolbar' | 'status'; className: string; children: ReactNode }) {
|
|
6
85
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -10,7 +89,7 @@ export function StickyLane({ kind, className, children }: { kind: 'toolbar' | 's
|
|
|
10
89
|
if (!el || !scope) return;
|
|
11
90
|
const property = kind === 'toolbar' ? '--reader-toolbar-height' : '--reader-status-height';
|
|
12
91
|
const update = () => {
|
|
13
|
-
scope.style.setProperty(property, `${el.getBoundingClientRect().height}px`);
|
|
92
|
+
scope.style.setProperty(property, `${Math.max(MIN_LANE_PX, el.getBoundingClientRect().height)}px`);
|
|
14
93
|
if (kind !== 'toolbar') return;
|
|
15
94
|
// The status lane is sized as calc(100% - --reader-control-width), so this
|
|
16
95
|
// must reserve the WHOLE control group. Measuring only the first button
|
|
@@ -6,7 +6,7 @@ import { DiffBlock, DisclosureRow, JsonTree, ReadBlock, SearchBlock, TerminalBlo
|
|
|
6
6
|
import { Blocks, contentBlocks } from './Blocks.js';
|
|
7
7
|
import { OfficialTool } from './OfficialContent.js';
|
|
8
8
|
import { ProcessFragment } from './motion.js';
|
|
9
|
-
import { activityPhase, activitySummary, callDiffHunks, diffTotals, executionFacts, forgetCallClock, objectValue, runningClockBase
|
|
9
|
+
import { activityPhase, activitySummary, callDiffHunks, diffTotals, executionFacts, forgetCallClock, objectValue, runningClockBase } from './tool-activity.js';
|
|
10
10
|
import type { ToolActivityEntry, ToolCategory, ToolPhase } from './tool-activity.js';
|
|
11
11
|
import type { BlockRenderProps } from './types.js';
|
|
12
12
|
import { classifyTool, toolRowModel, VARIANT_TITLES } from './native/tool-call-model.js';
|
|
@@ -17,6 +17,7 @@ import css from './Reader.module.css';
|
|
|
17
17
|
const LABEL: Record<ToolPhase, string> = { preparing: '输入生成中', running: '执行中', returned: '已返回', succeeded: '已完成', failed: '失败', interrupted: '已中断' };
|
|
18
18
|
const ICONS = { write: IconEditOutlineRegular, read: IconBrowseOutlineRegular, terminal: IconApiOutlineRegular, search: IconSearchOutlineRegular, web: IconSearchOutlineRegular, code: IconApiOutlineRegular, other: IconSparkleRegular } satisfies Record<ToolCategory, unknown>;
|
|
19
19
|
const number = new Intl.NumberFormat('zh-CN');
|
|
20
|
+
const preparingTerminalLabels = { ...terminalBlockLabels, running: '正在生成命令' };
|
|
20
21
|
const language = (path: string | undefined) => path?.split('.').at(-1);
|
|
21
22
|
const duration = (ms: number) => ms < 1000 ? `${Math.round(ms)} 毫秒` : `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} 秒`;
|
|
22
23
|
|
|
@@ -28,14 +29,17 @@ function generatedInput(content: string, target: string | undefined, preparing:
|
|
|
28
29
|
</div>;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function InputView({ model,
|
|
32
|
+
function InputView({ model, phase, fillComposer }: { model: ReturnType<typeof activitySummary>; phase: ToolPhase; fillComposer: BlockRenderProps['fillComposer'] }) {
|
|
33
|
+
const preparing = phase === 'preparing';
|
|
34
|
+
const inProgress = preparing || phase === 'running';
|
|
32
35
|
if ((model.name === 'render_ui' || model.name === 'show_widget') && typeof model.args?.html === 'string') {
|
|
33
36
|
return preparing
|
|
34
37
|
? <StreamingMcpAppPlaceholder title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} />
|
|
35
38
|
: <McpAppFrame html={model.args.html as string} title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} fillComposer={fillComposer} />;
|
|
36
39
|
}
|
|
37
40
|
if (model.content) return generatedInput(model.content, model.target, preparing);
|
|
38
|
-
if (model.command) return <div data-reader-tool-terminal><p className={css.toolDetailNote}>{preparing ? '正在生成命令 · 尚未执行' : '提交的命令'}</p><TerminalBlock command={model.command} cwd={model.cwd} labels={terminalBlockLabels} /></div>;
|
|
41
|
+
if (model.command) return <div data-reader-tool-terminal><p className={css.toolDetailNote}>{preparing ? '正在生成命令 · 尚未执行' : '提交的命令'}</p><TerminalBlock command={model.command} cwd={model.cwd} running={inProgress} labels={preparing ? preparingTerminalLabels : terminalBlockLabels} /></div>;
|
|
42
|
+
if (model.category === 'terminal' && preparing && model.argsState === 'partial') return <p className={css.toolDetailNote} data-reader-tool-command-pending>命令仍在生成,收到可识别的命令文本后会显示预览。</p>;
|
|
39
43
|
return <JsonTree data={model.args} label={preparing ? '已收到的输入字段' : '工具输入'} labels={jsonTreeLabels} />;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -95,11 +99,11 @@ function ResultFallback({ entry, model, phase, ...render }: BlockRenderProps & {
|
|
|
95
99
|
const block = entry.block;
|
|
96
100
|
if (!block || !('kind' in block)) return <>
|
|
97
101
|
<p className={css.toolDetailNote}>{phase === 'interrupted' ? '已中断,没有工具结果。已生成的输入仍可查看。' : phase === 'preparing' ? '模型正在生成工具输入,工具还未开始执行。' : '工具已开始执行,正在等待结果。'}</p>
|
|
98
|
-
<InputView model={model}
|
|
102
|
+
<InputView model={model} phase={phase} fillComposer={render.fillComposer} />
|
|
99
103
|
</>;
|
|
100
104
|
const meta = objectValue(block.meta);
|
|
101
105
|
const text = block.content.filter(item => item.type === 'text').map(item => item.text).join('\n');
|
|
102
|
-
if (phase === 'interrupted') return <><p className={css.toolDetailNote}>工具已取消,未正常完成。输入和原始返回记录仍可查看。</p><InputView model={model}
|
|
106
|
+
if (phase === 'interrupted') return <><p className={css.toolDetailNote}>工具已取消,未正常完成。输入和原始返回记录仍可查看。</p><InputView model={model} phase={phase} fillComposer={render.fillComposer} /><pre className={css.toolRaw}>{text}</pre></>;
|
|
103
107
|
|
|
104
108
|
if (model.category === 'terminal') {
|
|
105
109
|
const facts = executionFacts(block);
|
|
@@ -247,7 +251,7 @@ export const ToolActivity = memo(function ToolActivityView({ entry, motion, turn
|
|
|
247
251
|
<div ref={panel} id={`${detailId}-panel`} className={css.toolPanel} role="tabpanel" aria-labelledby={`${detailId}-${tab}`} tabIndex={0}>
|
|
248
252
|
{selected && <p className={css.toolDetailNote}>为保留选区,预览暂停更新;当前状态见卡片标题。</p>}
|
|
249
253
|
{tab === 'result' && <ResultView {...render} {...preview} />}
|
|
250
|
-
{tab === 'input' && <><InputView model={preview.model}
|
|
254
|
+
{tab === 'input' && <><InputView model={preview.model} phase={preview.phase} fillComposer={render.fillComposer} /><details className={css.detail}><summary>全部输入字段</summary><JsonTree data={preview.model.args} label="输入字段" labels={jsonTreeLabels} /></details></>}
|
|
251
255
|
{tab === 'raw' && <><p className={css.toolDetailNote}>完整记录 · 只读 · 不执行其中的代码</p><h4 className={css.toolRawLabel}>工具输入</h4><pre className={css.toolRaw}>{preview.model.raw || '输入尚未到达'}</pre>{rawResult && <><h4 className={css.toolRawLabel}>工具结果</h4><pre className={css.toolRaw}>{rawResult}</pre></>}</>}
|
|
252
256
|
</div>
|
|
253
257
|
</div>
|
|
@@ -23,6 +23,7 @@ export function basename(path: string): string {
|
|
|
23
23
|
export function dirname(path: string): string {
|
|
24
24
|
const normalized = path.replace(/[/\\]+$/, '');
|
|
25
25
|
const at = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
|
|
26
|
+
if (at === 0 || (at === 2 && /^[A-Za-z]:/.test(normalized))) return normalized.slice(0, at + 1);
|
|
26
27
|
return at === -1 ? '.' : normalized.slice(0, at) || '.';
|
|
27
28
|
}
|
|
28
29
|
|
package/src/client/index.tsx
CHANGED
|
@@ -6,6 +6,8 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
|
6
6
|
import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path';
|
|
7
7
|
import * as workspacePathPkg from '@deepseek-ai/dsh-util-workspace-path';
|
|
8
8
|
import { dirname } from './deliverables.js';
|
|
9
|
+
import { isAbsoluteWorkspacePath } from '../workspace-path.js';
|
|
10
|
+
import { ensureStyles } from './styles.js';
|
|
9
11
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
10
12
|
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client';
|
|
11
13
|
import { Reader } from './Reader.js';
|
|
@@ -17,6 +19,7 @@ import { fileAddressFor, modeFromSnapshot, openDeliverableFile } from './open-fi
|
|
|
17
19
|
import type { ReaderInjected } from './types.js';
|
|
18
20
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
19
21
|
import { installOfficialSlots, officialChildren, type CompositionRegistry } from './official-slots.js';
|
|
22
|
+
import { TEXT_TRANSFORM_SERVICE, createTextTransformRegistry, installTextTransformRegistry } from './text-transform.js';
|
|
20
23
|
|
|
21
24
|
/** Structural face of the sanctioned per-session composer writer. */
|
|
22
25
|
interface ComposerShell {
|
|
@@ -52,12 +55,21 @@ async function openWorkspacePath(
|
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
export type { ReaderBlockOwner } from './types.js';
|
|
58
|
+
export type { ReaderBlockOwner, ReaderBlockPresentation } from './types.js';
|
|
59
|
+
export type { DisplayCursor, DisplayText, DisplayTextKind, DisplayTextRequest, DisplayTextTransformer, TextTransformRegistry } from './text-transform.js';
|
|
56
60
|
export { McpAppFrame } from './McpAppFrame.js';
|
|
61
|
+
export { TEXT_TRANSFORM_SERVICE, createTextTransformRegistry, originalAnswerText } from './text-transform.js';
|
|
57
62
|
export const name = 'dsh-better-display-client';
|
|
58
63
|
export const inject = ['slots', 'sessions', 'conversation', 'uiConversation', 'remote', 'remote.session'];
|
|
59
64
|
|
|
65
|
+
// All CSS imports have injected their nodes before this module's body runs.
|
|
66
|
+
ensureStyles();
|
|
67
|
+
|
|
60
68
|
export function apply(ctx: Context): void {
|
|
69
|
+
const textTransform = createTextTransformRegistry();
|
|
70
|
+
const restoreTextTransform = installTextTransformRegistry(textTransform);
|
|
71
|
+
ctx.provide(TEXT_TRANSFORM_SERVICE, textTransform);
|
|
72
|
+
ctx.effect(() => restoreTextTransform);
|
|
61
73
|
const store = createReaderStore();
|
|
62
74
|
// conversation.view is session-scoped, so session persist keys are
|
|
63
75
|
// `dsh.reader.v1.<sessionId>`. One root instance keeps glass, fold
|
|
@@ -138,7 +150,7 @@ export function apply(ctx: Context): void {
|
|
|
138
150
|
const res = await fetch('/better-display/reveal', {
|
|
139
151
|
method: 'POST',
|
|
140
152
|
headers: { 'Content-Type': 'application/json' },
|
|
141
|
-
body: JSON.stringify({ path: targetPath }),
|
|
153
|
+
body: JSON.stringify({ path: targetPath, sessionId }),
|
|
142
154
|
});
|
|
143
155
|
if (res.ok) {
|
|
144
156
|
const data = await res.json();
|
|
@@ -150,6 +162,10 @@ export function apply(ctx: Context): void {
|
|
|
150
162
|
|
|
151
163
|
// 2. Fallback to official opener with parent directory
|
|
152
164
|
const parentDir = dirname(targetPath);
|
|
165
|
+
if (!isAbsoluteWorkspacePath(parentDir)) {
|
|
166
|
+
console.warn('[dsh-better-display] reveal failed; refusing to open a relative parent:', targetPath);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
153
169
|
const remote = ctx.remote as unknown as { session?: { openWorkspacePath: (arg: { path: string }) => Promise<{ ok: boolean; error?: { message: string } }> } } | undefined;
|
|
154
170
|
const remoteSession = remote?.session
|
|
155
171
|
?? (ctx.get?.('remote.session') as unknown as { openWorkspacePath: (arg: { path: string }) => Promise<{ ok: boolean; error?: { message: string } }> } | undefined);
|