dsh-better-display 0.3.6 → 0.3.7

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.
@@ -1,4 +1,5 @@
1
1
  import type {} from '@deepseek-ai/dsh-session-turn-outline/types';
2
+ import type {} from '@deepseek-ai/dsh-agent/types';
2
3
  import { Fragment, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
4
  import type { ReactNode, RefObject } from 'react';
4
5
  import type { ChatConversationViewNode, ChatNode, ChatNodeKind } from '@deepseek-ai/dsh-client-ui-chat/client';
@@ -30,11 +31,33 @@ import type { ReaderGroup, TurnBoundary } from './projection.js';
30
31
  import type { BlockRenderProps, ReaderProps } from './types.js';
31
32
  import css from './Reader.module.css';
32
33
  import { markdownLabels, truncatedJsonLabel } from './primitive-labels.js';
34
+ import { chatSeatProps } from './chat-seat.js';
33
35
 
34
36
  function isNode<K extends ChatNodeKind>(node: ChatConversationViewNode, kind: K): node is ChatNode<K> {
35
37
  return node.kind === kind;
36
38
  }
37
39
 
40
+ function observedInputIds(order: readonly string[], getNode: (key: string) => ChatConversationViewNode | undefined): Set<string> {
41
+ const ids = new Set<string>();
42
+ for (const key of order) {
43
+ const node = getNode(key);
44
+ if (!node || (node.kind !== 'user' && node.kind !== 'steering' && node.kind !== 'turn-trigger')) continue;
45
+ const source = (node.data as { source?: { kind?: string; rpcId?: unknown } }).source;
46
+ if (source?.kind === 'user' && typeof source.rpcId === 'string') ids.add(source.rpcId);
47
+ }
48
+ return ids;
49
+ }
50
+
51
+ function inboxText(item: { content?: readonly unknown[] }): string {
52
+ return (item.content ?? []).filter((block): block is { type: 'text'; text: string } => {
53
+ return typeof block === 'object' && block !== null && (block as { type?: unknown }).type === 'text' && typeof (block as { text?: unknown }).text === 'string';
54
+ }).map(block => block.text).join('');
55
+ }
56
+
57
+ function inboxAttachmentCount(item: { content?: readonly unknown[] }): number {
58
+ return (item.content ?? []).filter(block => typeof block === 'object' && block !== null && (block as { type?: unknown }).type !== 'text').length;
59
+ }
60
+
38
61
  function cleanErrorMessage(raw: string | undefined): string {
39
62
  if (!raw) return '模型服务暂时无响应或连接中断,请稍后重试。';
40
63
  let str = raw.trim();
@@ -198,7 +221,7 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
198
221
  const otherBlocks = blocks.filter(b => b.kind !== 'image');
199
222
  const text = otherBlocks.filter((block): block is Extract<typeof block, { kind: 'text' }> => block.kind === 'text').map(block => block.text).join('\n\n');
200
223
  const time = node.data.time;
201
- return <div className={css.userCluster} data-reader-anchor data-reader-key={nodeKey}>
224
+ return <div className={css.userCluster} data-reader-anchor data-reader-key={nodeKey} {...chatSeatProps(node.kind, nodeKey)}>
202
225
  {node.kind === 'steering' && <p className={css.meta}>补充消息</p>}
203
226
  {imageBlocks.length > 0 && <div className={css.userImages}>
204
227
  <Blocks {...render} blocks={imageBlocks} source="user" />
@@ -631,6 +654,25 @@ export function Reader(props: ReaderProps) {
631
654
  const loadingOlder = props.useSession(snapshot => snapshot.loadingOlder);
632
655
  const pendingSubmissions = props.useSession(snapshot => snapshot.pendingSubmissions);
633
656
  const pendingList = asReadonlyArray<PendingSubmissionEcho>(pendingSubmissions);
657
+ const inbox = props.useProjection?.('inbox');
658
+ type PendingInboxItem = { source?: { kind?: string; rpcId?: unknown }; content?: readonly unknown[] };
659
+ const inboxSteering = asReadonlyArray<PendingInboxItem>(inbox?.['next-step']).filter(item => item.source?.kind === 'user');
660
+ const observed = useMemo(() => observedInputIds(order, key => nodes.get(key)), [order, nodes]);
661
+ const visibleSubmissions = useMemo(() => pendingList.filter(sub => sub.placement !== 'queued' && !observed.has(sub.requestId)), [pendingList, observed]);
662
+ const pendingInputs = useMemo(() => {
663
+ const local = new Map<string, PendingSubmissionEcho>(visibleSubmissions.map(sub => [sub.requestId, sub]));
664
+ const localIds = new Set(pendingList.filter(sub => sub.placement !== 'queued').map(sub => sub.requestId));
665
+ const rows: Array<PendingSubmissionEcho | (typeof inboxSteering)[number]> = [];
666
+ for (const item of inboxSteering) {
667
+ const rpcId = typeof item.source?.rpcId === 'string' ? item.source.rpcId : undefined;
668
+ if (!rpcId) { rows.push(item); continue; }
669
+ const submission = local.get(rpcId);
670
+ if (submission) { local.delete(rpcId); rows.push(submission); }
671
+ else if (!localIds.has(rpcId)) rows.push(item);
672
+ }
673
+ rows.push(...local.values());
674
+ return rows;
675
+ }, [inboxSteering, pendingList, visibleSubmissions]);
634
676
  const waitAnchor = waitingAnchor(order, key => nodes.get(key), pendingList);
635
677
  const motionPreference = props.useStore(state => state.motion);
636
678
  const motion = useMotionAllowed(motionPreference);
@@ -808,11 +850,6 @@ export function Reader(props: ReaderProps) {
808
850
  }
809
851
  }, [lastKey, lastNode?.kind, lastSubmissionId, scroll]);
810
852
 
811
- const visibleSubmissions = useMemo(() => {
812
- if (pendingList.length === 0) return [];
813
- return pendingList.filter(sub => sub.placement !== 'queued');
814
- }, [pendingList]);
815
-
816
853
  // ChatView publishes data-chat-flow="" on its column. Skins treat a
817
854
  // scrollport without that hook as inspect-only and hide [data-composer-seat].
818
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'}>
@@ -840,10 +877,14 @@ export function Reader(props: ReaderProps) {
840
877
  {openError && <div className={css.error} role="alert">会话暂时无法读取:{openError.message}</div>}
841
878
  {loading && groups.length === 0 && <p className={css.empty} role="status">正在读取会话…</p>}
842
879
  {groups.map(group => <TurnGroup key={group.key} {...props} group={group} motion={motion} autoFold={autoFold} pinnedKeys={pinnedKeys} selectedProcessKeys={selectedProcessKeys} isAwaitingModel={isAwaitingModel && group.key === groups.at(-1)?.key} />)}
843
- {visibleSubmissions.map(submission => {
844
- const images = pendingSubmissionImages(submission);
880
+ {pendingInputs.map((item, index) => {
881
+ const submission = 'requestId' in item ? item : undefined;
882
+ const text = submission?.text ?? inboxText(item as PendingInboxItem);
883
+ const images = submission ? pendingSubmissionImages(submission) : [];
884
+ const attachmentCount = images.length;
885
+ const requestId = submission?.requestId ?? `inbox-${index}`;
845
886
  return (
846
- <div key={submission.requestId} className={css.userCluster} data-reader-pending-submission>
887
+ <div key={requestId} className={css.userCluster} data-reader-pending-submission data-pending-steering={submission ? undefined : ''} data-pending-attachment-count={attachmentCount}>
847
888
  {images.length > 0 && (
848
889
  <div className={css.userImages}>
849
890
  {images.map((item, idx) => (
@@ -855,12 +896,12 @@ export function Reader(props: ReaderProps) {
855
896
  ))}
856
897
  </div>
857
898
  )}
858
- {submission.text ? (
899
+ {text ? (
859
900
  <div className={css.user}>
860
- <div className={css.blocks}>{submission.text}</div>
901
+ <div className={css.blocks}>{text}</div>
861
902
  </div>
862
903
  ) : null}
863
- <UserMessageActions text={submission.text ?? ''} time={submission.time} />
904
+ <UserMessageActions text={text} time={submission?.time} />
864
905
  </div>
865
906
  );
866
907
  })}
@@ -0,0 +1,10 @@
1
+ export type ChatSeatKind = 'user' | 'steering';
2
+
3
+ export function chatSeatProps(kind: ChatSeatKind, key: string): Record<string, string> {
4
+ return {
5
+ 'data-chat-anchor-key': key,
6
+ 'data-chat-flow-key': key,
7
+ 'data-chat-node-key': key,
8
+ 'data-chat-flow-kind': kind,
9
+ };
10
+ }
@@ -45,6 +45,19 @@ test('committed client bundle publishes data-chat-flow for git installs', () =>
45
45
  assert.match(compiled, /"data-chat-flow":\s*""/);
46
46
  });
47
47
 
48
+ test('Reader exposes rewind chat seats and pending steering markers', () => {
49
+ const source = readFileSync(resolve(root, 'src/client/Reader.tsx'), 'utf8');
50
+ const seat = readFileSync(resolve(root, 'src/client/chat-seat.ts'), 'utf8');
51
+ assert.match(seat, /data-chat-anchor-key/);
52
+ assert.match(seat, /data-chat-flow-key/);
53
+ assert.match(seat, /data-chat-node-key/);
54
+ assert.match(source, /data-pending-steering/);
55
+ assert.match(source, /data-pending-attachment-count/);
56
+ const client = readFileSync(resolve(root, 'lib/client.js'), 'utf8');
57
+ assert.match(client, /data-chat-anchor-key/);
58
+ assert.match(client, /data-pending-steering/);
59
+ });
60
+
48
61
  test('skin empty-state hide stays off when Reader mounts inside the scrollport', () => {
49
62
  const inspectOnly: AttrNode = {
50
63
  attrs: { 'data-conversation-scroll': '' },
package/tsdown.config.ts CHANGED
@@ -41,6 +41,13 @@ const portableOutput: TsdownPlugin = {
41
41
  };
42
42
 
43
43
  export default bundle.map((config) => {
44
+ if (config.name === 'dsh-better-display') {
45
+ const { external: _external, ...hostConfig } = config;
46
+ return {
47
+ ...hostConfig,
48
+ deps: { ...hostConfig.deps, neverBundle: (specifier: string) => !specifier.startsWith('.') },
49
+ };
50
+ }
44
51
  if (config.name !== 'dsh-better-display/client') return config;
45
52
  const plugins = Array.isArray(config.plugins)
46
53
  ? config.plugins