dsh-better-display 0.3.9 → 0.3.10

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 +1 @@
1
- {"version":3,"file":"primitive-labels.js","sourceRoot":"","sources":["../../../src/client/primitive-labels.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"primitive-labels.js","sourceRoot":"","sources":["../../../src/client/primitive-labels.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-display",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "A calmer reading view for DeepSeek Harness: native process details, live reasoning, and clean final answers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,30 +56,30 @@
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@deepseek-ai/cordis": "~4.0.4",
59
- "@deepseek-ai/dsh-api-session-controller": ">=0.2.0-rc.1 <0.2.1",
60
- "@deepseek-ai/dsh-attachment": ">=0.2.0-rc.1 <0.2.1",
61
- "@deepseek-ai/dsh-client-store": ">=0.2.0-rc.1 <0.2.1",
62
- "@deepseek-ai/dsh-client-ui-chat": ">=0.2.0-rc.1 <0.2.1",
63
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.2.0-rc.1 <0.2.1",
64
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.0-rc.1 <0.2.1",
65
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.0-rc.1 <0.2.1",
66
- "@deepseek-ai/dsh-client-ui-session": ">=0.2.0-rc.1 <0.2.1",
67
- "@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-rc.1 <0.2.1",
68
- "@deepseek-ai/dsh-client-ui-tool": ">=0.2.0-rc.1 <0.2.1",
69
- "@deepseek-ai/dsh-session": ">=0.2.0-rc.1 <0.2.1",
70
- "@deepseek-ai/dsh-util-workspace-path": ">=0.2.0-rc.1 <0.2.1",
59
+ "@deepseek-ai/dsh-api-session-controller": ">=0.2.1-rc.1 <0.2.2",
60
+ "@deepseek-ai/dsh-attachment": ">=0.2.1-rc.1 <0.2.2",
61
+ "@deepseek-ai/dsh-client-store": ">=0.2.1-rc.1 <0.2.2",
62
+ "@deepseek-ai/dsh-client-ui-chat": ">=0.2.1-rc.1 <0.2.2",
63
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.2.1-rc.1 <0.2.2",
64
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.1-rc.1 <0.2.2",
65
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.1-rc.1 <0.2.2",
66
+ "@deepseek-ai/dsh-client-ui-session": ">=0.2.1-rc.1 <0.2.2",
67
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.1 <0.2.2",
68
+ "@deepseek-ai/dsh-client-ui-tool": ">=0.2.1-rc.1 <0.2.2",
69
+ "@deepseek-ai/dsh-session": ">=0.2.1-rc.1 <0.2.2",
70
+ "@deepseek-ai/dsh-util-workspace-path": ">=0.2.1-rc.1 <0.2.2",
71
71
  "react": "^18.2.0"
72
72
  },
73
73
  "devDependencies": {
74
- "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
75
- "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
76
- "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
77
- "@deepseek-ai/dsh-session-turn-outline": "0.2.0-rc.2",
74
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.5",
75
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.5",
76
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
77
+ "@deepseek-ai/dsh-session-turn-outline": "0.2.1-rc.5",
78
78
  "@types/mdast": "^4.0.4",
79
79
  "@types/node": "^22.20.0",
80
80
  "@types/react": "~18.3.1",
81
81
  "lightningcss": "^1.33.0",
82
- "tsdown": "^0.22.2",
82
+ "tsdown": "^0.23.2",
83
83
  "tsx": "^4.22.4",
84
84
  "typescript": "^6.0.3",
85
85
  "semver": "^7.7.2",
@@ -131,7 +131,8 @@
131
131
  "docs"
132
132
  ],
133
133
  "engines": {
134
- "node": "^22.19.0 || >=24.0.0"
134
+ "node": "^22.19.0 || >=24.0.0",
135
+ "dsh": ">=0.2.1-rc.1 <0.2.2"
135
136
  },
136
137
  "repository": {
137
138
  "type": "git",
@@ -51,8 +51,10 @@ for (const path of execFileSync('rg', ['--files', 'packages/client', '-g', '*.ts
51
51
  visit(file);
52
52
  }
53
53
  registrations.sort();
54
+ const harnessCommit = execFileSync('git', ['-C', harness, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
54
55
  const current = {
55
56
  harnessVersion: JSON.parse(readFileSync(join(harness, 'packages/client/ui-chat/package.json'), 'utf8')).version,
57
+ harnessCommit,
56
58
  hashes, registrations,
57
59
  };
58
60
  if (process.argv.includes('--record')) {
@@ -63,8 +65,8 @@ if (process.argv.includes('--record')) {
63
65
  const changed = files.filter(file => current.hashes[file] !== baseline.hashes[file]);
64
66
  const added = registrations.filter(row => !baseline.registrations.includes(row));
65
67
  const removed = baseline.registrations.filter(row => !registrations.includes(row));
66
- if (current.harnessVersion !== baseline.harnessVersion || changed.length || added.length || removed.length) {
67
- console.error(JSON.stringify({ compatible: 'review-required', target: current.harnessVersion, changedContractsOrForkSources: changed, addedRegistrations: added, removedRegistrations: removed }, null, 2));
68
+ if (current.harnessVersion !== baseline.harnessVersion || current.harnessCommit !== baseline.harnessCommit || changed.length || added.length || removed.length) {
69
+ console.error(JSON.stringify({ compatible: 'review-required', target: current.harnessVersion, commit: current.harnessCommit, baselineCommit: baseline.harnessCommit, changedContractsOrForkSources: changed, addedRegistrations: added, removedRegistrations: removed }, null, 2));
68
70
  console.error('Review the differences and run the official bridge/visual tests. --record alone is not compatibility proof.');
69
71
  process.exitCode = 1;
70
72
  } else console.log(`Harness compatibility baseline passed (${current.harnessVersion}; ${registrations.length} official registrations).`);
@@ -1,9 +1,9 @@
1
- import { existsSync, mkdirSync, symlinkSync, realpathSync, readFileSync } from 'node:fs';
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
 
5
- const root = resolve(process.argv[2] || '');
6
- if (!process.argv[2] || !existsSync(join(root, 'tools/dshx/src/client-build.js'))) throw new Error('Pass a prepared Harness checkout.');
5
+ const root = resolve(process.argv[2] || process.env.DSH_REPO || '');
6
+ if (!root || !existsSync(join(root, 'tools/dshx/src/client-build.js'))) throw new Error('Pass a prepared Harness checkout, or set DSH_REPO.');
7
7
  const local = resolve('node_modules');
8
8
  mkdirSync(local, { recursive: true });
9
9
  const shared = {
@@ -24,23 +24,60 @@ const shared = {
24
24
  '@deepseek-ai/dsh-session': 'packages/core/session',
25
25
  '@deepseek-ai/dsh-util-workspace-path': 'packages/util/workspace-path',
26
26
  };
27
- function link(name, target) {
27
+ function link(name, target, replace = false) {
28
28
  if (!existsSync(target)) throw new Error(`Missing Harness dependency target: ${name} (${target})`);
29
29
  const destination = join(local, name);
30
30
  mkdirSync(dirname(destination), { recursive: true });
31
- if (existsSync(destination)) {
32
- if (realpathSync(destination) !== realpathSync(target)) throw new Error(`Dependency mismatch: ${name}`);
33
- return;
31
+ const stat = lstatSync(destination, { throwIfNoEntry: false });
32
+ if (stat) {
33
+ if (!replace && !stat.isSymbolicLink() && existsSync(destination)) return;
34
+ if (existsSync(destination) && realpathSync(destination) === realpathSync(target)) return;
35
+ rmSync(destination, { recursive: true, force: true });
34
36
  }
35
37
  symlinkSync(target, destination);
36
38
  }
37
- for (const [name, path] of Object.entries(shared)) link(name, join(root, path));
39
+ function packageDir(resolver, name) {
40
+ try { return dirname(resolver.resolve(`${name}/package.json`)); }
41
+ catch { /* pnpm may leave the package only in the store. */ }
42
+ const store = join(root, 'node_modules/.pnpm');
43
+ const prefix = name.startsWith('@') ? `${name.slice(1).replace('/', '+')}@` : `${name}@`;
44
+ const scoped = name.startsWith('@');
45
+ const match = existsSync(store) ? readdirSync(store).find(entry => entry.startsWith(prefix) || (scoped && entry.startsWith(name.replace('/', '+') + '@'))) : undefined;
46
+ if (!match) throw new Error(`Missing Harness dependency: ${name}`);
47
+ const nested = join(store, match, 'node_modules', name);
48
+ if (!existsSync(join(nested, 'package.json'))) throw new Error(`Missing Harness dependency: ${name} (${nested})`);
49
+ return nested;
50
+ }
51
+ function declaredTypesExist(pkgDir) {
52
+ const manifest = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
53
+ const types = typeof manifest.types === 'string'
54
+ ? manifest.types
55
+ : manifest.exports?.['.']?.types;
56
+ return typeof types !== 'string' || existsSync(join(pkgDir, types));
57
+ }
58
+ function builtPackage(name) {
59
+ const runtime = process.env.DSH_RUNTIME || join(dirname(root), 'runtime-rc5');
60
+ const store = join(runtime, 'node_modules/.pnpm');
61
+ if (!existsSync(store)) return undefined;
62
+ const slug = `${name.replace('/', '+')}@`;
63
+ const match = readdirSync(store).find(entry => entry.startsWith(slug));
64
+ if (!match) return undefined;
65
+ const pkgDir = join(store, match, 'node_modules', name);
66
+ return existsSync(join(pkgDir, 'package.json')) && declaredTypesExist(pkgDir) ? pkgDir : undefined;
67
+ }
68
+ for (const [name, path] of Object.entries(shared)) {
69
+ const source = join(root, path);
70
+ const target = (name === '@deepseek-ai/cordis' || !declaredTypesExist(source)) ? (builtPackage(name) ?? source) : source;
71
+ link(name, target, true);
72
+ }
38
73
  const require = createRequire(join(root, 'packages/client/ui-conversation/package.json'));
39
74
  const rootRequire = createRequire(join(root, 'package.json'));
40
- for (const name of ['react', '@types/react']) link(name, dirname(require.resolve(`${name}/package.json`)));
41
- for (const name of ['@types/node', 'typescript', 'tsdown', 'tsx']) link(name, dirname(rootRequire.resolve(`${name}/package.json`)));
75
+ for (const name of ['react', '@types/react']) link(name, packageDir(require, name));
76
+ for (const name of ['@types/node', 'typescript', 'tsdown', 'tsx']) link(name, packageDir(rootRequire, name));
42
77
  for (const name of ['@types/mdast', 'clsx', 'katex', 'mdast-util-from-markdown', 'mdast-util-gfm', 'mdast-util-math', 'micromark-core-commonmark', 'micromark-extension-gfm', 'micromark-extension-math', 'micromark-factory-space', 'micromark-util-character', 'micromark-util-classify-character', 'micromark-util-sanitize-uri', 'micromark-util-symbol', 'micromark-util-types']) {
43
- link(name, join(root, 'packages/client/ui-primitives/node_modules', name));
78
+ const target = join(root, 'packages/client/ui-primitives/node_modules', name);
79
+ if (!existsSync(target)) continue;
80
+ link(name, target);
44
81
  }
45
82
  mkdirSync(join(local, '.bin'), { recursive: true });
46
83
  for (const name of ['typescript', 'tsdown']) {
@@ -3,7 +3,7 @@ import type { AssistantActionOwnerProps, ChatConversationViewNode, ChatNode, Cha
3
3
  import { createSnapshotStore } from '@deepseek-ai/dsh-client-store';
4
4
  import type { AssistantBlock, ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
5
5
  type MessageId = AssistantActionOwnerProps['messageId'];
6
- import { OFFICIAL_SEATS } from './official-slots.js';
6
+ import { OFFICIAL_SEATS, readerGroupAction } from './official-slots.js';
7
7
  import type { BlockRenderProps } from './types.js';
8
8
  import { chatSeatProps } from './chat-seat.js';
9
9
  import { Blocks } from './Blocks.js';
@@ -42,17 +42,17 @@ function chatNodeOwner(render: BlockRenderProps, official: NonNullable<BlockRend
42
42
  return {
43
43
  cwd: render.cwd,
44
44
  openFile: render.openFile ?? (() => {}),
45
- openSkill: () => {},
46
45
  forkAt: render.forkAt ?? (() => {}),
47
46
  inspectCall: callId => official.openView('trajectory', callId),
48
47
  loadImage: official.officialImageLoader,
49
- renderMessageImages: images => official.renderSlot(OFFICIAL_SEATS.images, {
50
- ...images, loadImage: official.officialImageLoader,
51
- }),
52
48
  fileMentions: official.officialFileMentions,
53
49
  };
54
50
  }
55
51
 
52
+ function nodeHookContext(turnData: unknown, disclosureReset: ReturnType<typeof createSnapshotStore<number>>) {
53
+ return { turnData, disclosureReset, useGroupAction: readerGroupAction };
54
+ }
55
+
56
56
  export function OfficialNode({ node, fallback, ...render }: BlockRenderProps & { node: ChatConversationViewNode; fallback: ReactNode }) {
57
57
  const official = render.official;
58
58
  const [disclosureReset] = useState(() => createSnapshotStore(0));
@@ -61,7 +61,7 @@ export function OfficialNode({ node, fallback, ...render }: BlockRenderProps & {
61
61
  // The runtime node domain is open; the public SlotMap enumerates the known kinds.
62
62
  const renderNode = official.renderSlot as unknown as (key: string, owner: object, options: object) => ReactNode;
63
63
  return <div data-reader-official-node={node.kind} {...(node.kind === 'user' || node.kind === 'steering' ? chatSeatProps(node.kind, node.key) : {})}>
64
- {renderNode(OFFICIAL_SEATS.nodes, { ...chatNodeOwner(render, official), node }, { entryKey: node.kind, hookContext: { turnData: turn?.data, disclosureReset }, fallback })}
64
+ {renderNode(OFFICIAL_SEATS.nodes, { ...chatNodeOwner(render, official), node }, { entryKey: node.kind, hookContext: nodeHookContext(turn?.data, disclosureReset), fallback })}
65
65
  </div>;
66
66
  }
67
67
 
@@ -95,7 +95,7 @@ export function OfficialAssistantBody({
95
95
  // slice is what a replacement that ignores groupPart actually receives.
96
96
  return renderNode(OFFICIAL_SEATS.nodes, { ...chatNodeOwner(render, official), node: view, groupPart: 'response' }, {
97
97
  entryKey: 'assistant-step',
98
- hookContext: { turnData: turn?.data, disclosureReset },
98
+ hookContext: nodeHookContext(turn?.data, disclosureReset),
99
99
  fallback,
100
100
  });
101
101
  }
@@ -9,7 +9,7 @@ import { BlockBoundary, Blocks, contentBlocks, UserMessageActions } from './Bloc
9
9
  import { AssistantNode } from './AssistantNode.js';
10
10
  import { ToolActivity, ToolMedia } from './ToolActivity.js';
11
11
  import { OfficialNode, OfficialTail } from './OfficialContent.js';
12
- import { OFFICIAL_SEATS } from './official-slots.js';
12
+ import { OFFICIAL_SEATS, readerGroupAction } from './official-slots.js';
13
13
  import { preparingLabel, readerFlow } from './tool-activity.js';
14
14
  import { Disclosure, ProcessFragment, RetiringTurns, StatusText, useMotionAllowed, usePinnedSelection, useReadingScroll } from './motion.js';
15
15
  import { StreamMotionContext } from './streaming.js';
@@ -198,17 +198,13 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
198
198
  const owner: ChatNodeOwnerProps & Pick<ReaderProps, 'sessionId'> = {
199
199
  cwd: render.cwd,
200
200
  openFile: render.openFile ?? (() => {}),
201
- openSkill: () => {},
202
201
  forkAt: render.forkAt ?? (() => {}),
203
202
  inspectCall: callId => official.openView('trajectory', callId),
204
203
  loadImage: official.officialImageLoader,
205
- renderMessageImages: images => official.renderSlot(OFFICIAL_SEATS.images, {
206
- ...images, loadImage: official.officialImageLoader,
207
- }),
208
204
  fileMentions: official.officialFileMentions,
209
205
  sessionId: render.sessionId,
210
206
  };
211
- const options = { hookContext: { turnData: turn?.data, disclosureReset }, fallback };
207
+ const options = { hookContext: { turnData: turn?.data, disclosureReset, useGroupAction: readerGroupAction }, fallback };
212
208
  rendered = node.kind === 'user'
213
209
  ? official.renderSlot(OFFICIAL_SEATS.nodes, { ...owner, node }, { ...options, entryKey: 'user' })
214
210
  : official.renderSlot(OFFICIAL_SEATS.nodes, { ...owner, node }, { ...options, entryKey: 'steering' });
@@ -1,5 +1,6 @@
1
- import { createElement, memo, useMemo, type ComponentType, type ReactNode } from 'react';
1
+ import { createElement, memo, useEffect, useLayoutEffect, useMemo, useRef, type ComponentType, type ReactNode, type RefObject } from 'react';
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
+ import type { UseGroupAction } from '@deepseek-ai/dsh-client-ui-chat/client';
3
4
  import type { SlotEntryDef, SlotMap, SlotSpec, StoredEntry } from '@deepseek-ai/dsh-client-ui-slots';
4
5
  import type {} from '@deepseek-ai/dsh-client-ui-renderer/client';
5
6
 
@@ -18,7 +19,8 @@ export const OFFICIAL_SLOTS = {
18
19
  tools: 'tool.call.toolview',
19
20
  tail: 'conversation.chat.turnTail',
20
21
  nodes: 'conversation.chat.node',
21
- images: 'conversation.message.images',
22
+ messageImages: 'conversation.chat.message.images',
23
+ userImages: 'conversation.chat.user.images',
22
24
  } as const;
23
25
  export type OfficialFamily = keyof typeof OFFICIAL_SLOTS;
24
26
  export const OFFICIAL_SEATS = {
@@ -26,7 +28,8 @@ export const OFFICIAL_SEATS = {
26
28
  tools: 'dsh-better-display.official.tools/tool.call.toolview',
27
29
  tail: 'dsh-better-display.official.tail/conversation.chat.turnTail',
28
30
  nodes: 'dsh-better-display.official.nodes/conversation.chat.node',
29
- images: 'dsh-better-display.official.images/conversation.message.images',
31
+ messageImages: 'dsh-better-display.official.messageImages/conversation.chat.message.images',
32
+ userImages: 'dsh-better-display.official.userImages/conversation.chat.user.images',
30
33
  } as const;
31
34
  export type OfficialSeat = typeof OFFICIAL_SEATS[OfficialFamily];
32
35
  export const officialSeat = (family: OfficialFamily, source: string = OFFICIAL_SLOTS[family]) =>
@@ -52,10 +55,39 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
52
55
  'dsh-better-display.official.tools/tool.call.toolview': SlotMap['tool.call.toolview'];
53
56
  'dsh-better-display.official.tail/conversation.chat.turnTail': SlotMap['conversation.chat.turnTail'];
54
57
  'dsh-better-display.official.nodes/conversation.chat.node': SlotMap['conversation.chat.node'];
55
- 'dsh-better-display.official.images/conversation.message.images': SlotMap['conversation.message.images'];
58
+ 'dsh-better-display.official.messageImages/conversation.chat.message.images': SlotMap['conversation.chat.message.images'];
59
+ 'dsh-better-display.official.userImages/conversation.chat.user.images': SlotMap['conversation.chat.user.images'];
56
60
  }
57
61
  }
58
62
 
63
+ /**
64
+ * Searchable hide for official nodes drawn outside the Chat flow viewport.
65
+ * The flow's own `useGroupAction` is not in scope here; this keeps the same
66
+ * `hidden="until-found"` contract so browser find can still reveal the row.
67
+ */
68
+ export function useReaderGroupAction(hidden: boolean, reveal: () => void): RefObject<HTMLDivElement> {
69
+ const ref = useRef<HTMLDivElement>(null);
70
+ useLayoutEffect(() => {
71
+ const element = ref.current;
72
+ if (element === null) return;
73
+ if (hidden && element.contains(element.ownerDocument.activeElement)) {
74
+ reveal();
75
+ return;
76
+ }
77
+ if (hidden) element.setAttribute('hidden', 'until-found');
78
+ else element.removeAttribute('hidden');
79
+ }, [hidden, reveal]);
80
+ useEffect(() => {
81
+ const element = ref.current;
82
+ if (element === null) return;
83
+ element.addEventListener('beforematch', reveal);
84
+ return () => { element.removeEventListener('beforematch', reveal); };
85
+ }, [reveal]);
86
+ return ref;
87
+ }
88
+
89
+ export const readerGroupAction: UseGroupAction = useReaderGroupAction;
90
+
59
91
  // Reader draws these rows itself. Everything else, including future kinds, is
60
92
  // mirrored so the official renderer still runs inside the reading view.
61
93
  // 'user' and 'steering' stay mirrored (message-recall buttons and the like).
@@ -83,7 +115,8 @@ const EXPECTED: Record<OfficialFamily, Spec> = {
83
115
  tools: { kind: 'keyed', scope: 'session' },
84
116
  tail: { kind: 'list', scope: 'session' },
85
117
  nodes: { kind: 'keyed', scope: 'session' },
86
- images: { kind: 'single', scope: 'session' },
118
+ messageImages: { kind: 'single', scope: 'session' },
119
+ userImages: { kind: 'single', scope: 'session' },
87
120
  };
88
121
 
89
122
  /**
@@ -21,6 +21,7 @@ export const readBlockLabels: ReadBlockLabels = {
21
21
  };
22
22
 
23
23
  export const terminalBlockLabels: TerminalBlockLabels = {
24
+ commandLine: line => `命令第 ${line} 行`,
24
25
  signal: signal => `信号 ${signal}`, exitCode: code => `退出码 ${code}`, noExitCode: '无退出码',
25
26
  running: '执行中', failed: '失败', done: '已完成', copy: '复制', copied: '已复制',
26
27
  noOutput: '没有输出', collapseAria: '收起命令输出', collapse: '收起',
@@ -78,7 +78,8 @@ slots.register({ name: 'root', children: {
78
78
  'tool.call.toolview': { kind: 'keyed', scope: 'session' },
79
79
  'conversation.chat.turnTail': { kind: 'list', scope: 'session' },
80
80
  'conversation.chat.node': { kind: 'keyed', scope: 'session', inject: { hooks: { turnData: (_s: any, value: any) => () => value } } },
81
- 'conversation.message.images': { kind: 'single', scope: 'session' },
81
+ 'conversation.chat.message.images': { kind: 'single', scope: 'session' },
82
+ 'conversation.chat.user.images': { kind: 'single', scope: 'session' },
82
83
  'conversation.input.overlay': { kind: 'list', scope: 'session' },
83
84
  'fixture.reader': { kind: 'single', scope: 'session' },
84
85
  } }, ({ SessionProvider, renderSlot }: any) => <SessionProvider>
@@ -4,6 +4,7 @@ import { flushSync } from 'react-dom';
4
4
  import { SlotRegistry } from '@fixture/registry';
5
5
  import { createSlotRenderer } from '@fixture/renderer';
6
6
  import { UserMessageNodeView } from '@fixture/user-message';
7
+ import { UserContent } from '@fixture/user-content';
7
8
  import { Reader } from '../../src/client/Reader.js';
8
9
  import { createReaderStore } from '../../src/client/store.js';
9
10
  import { OFFICIAL_SEATS, OFFICIAL_SLOTS, installOfficialSlots, officialChildren } from '../../src/client/official-slots.js';
@@ -81,16 +82,20 @@ function NativeColumn({ renderSlot, kind, text }: {
81
82
  }) {
82
83
  const node = messageNode(kind, text);
83
84
  const owner = {
84
- cwd: '/fixture', openFile: () => {}, openSkill: () => {}, forkAt: () => {},
85
+ cwd: '/fixture', openFile: () => {}, forkAt: () => {},
85
86
  inspectCall: () => {}, loadImage: async () => null,
86
- renderMessageImages: () => null, fileMentions: () => undefined, node,
87
+ fileMentions: () => undefined, node,
87
88
  };
88
89
  return (
89
90
  <div data-native={kind} style={{ width: '100%', maxWidth: 'var(--dsh-chat-content-width, 748px)', display: 'flex', flexDirection: 'column' }}>
90
91
  <div style={{ minWidth: 0 }}>
91
92
  {renderSlot(OFFICIAL_SLOTS.nodes, owner, {
92
93
  entryKey: kind,
93
- hookContext: { turnData: node.location.turn.data, disclosureReset: { getSnapshot: () => 0, subscribe: () => () => {}, set: () => {} } },
94
+ hookContext: {
95
+ turnData: node.location.turn.data,
96
+ disclosureReset: { getSnapshot: () => 0, subscribe: () => () => {}, set: () => {} },
97
+ useGroupAction: () => ({ current: null }),
98
+ },
94
99
  fallback: null,
95
100
  })}
96
101
  </div>
@@ -98,22 +103,33 @@ function NativeColumn({ renderSlot, kind, text }: {
98
103
  );
99
104
  }
100
105
 
106
+ function paddedTextBox(scope: ParentNode | null): Element | null {
107
+ if (!scope) return null;
108
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT);
109
+ let node: Node | null;
110
+ while ((node = walker.nextNode())) {
111
+ if (!node.textContent?.trim()) continue;
112
+ const parent = node.parentElement;
113
+ if (!parent || parent.closest('button')) continue;
114
+ let element: HTMLElement | null = parent;
115
+ while (element && element !== scope) {
116
+ if (parseFloat(getComputedStyle(element).paddingTop) > 0) return element;
117
+ element = element.parentElement;
118
+ }
119
+ return parent;
120
+ }
121
+ return null;
122
+ }
123
+
101
124
  function boxOf(root: ParentNode | null, kind: 'user' | 'steering' | 'pending') {
102
125
  const scope = kind === 'pending'
103
126
  ? root?.querySelector('[data-reader-pending-submission]')
104
127
  : root?.querySelector(`[data-reader-key="${kind}-key"]`);
105
- if (!scope) return null;
106
- if (kind === 'pending') {
107
- return [...scope.children].find(element => element instanceof HTMLElement && element.querySelector('button') === null) ?? null;
108
- }
109
- const slot = scope.querySelector('[data-slot]');
110
- return slot?.firstElementChild?.firstElementChild?.firstElementChild ?? null;
128
+ return paddedTextBox(scope ?? null);
111
129
  }
112
130
 
113
131
  function nativeBox(frame: ParentNode, kind: 'user' | 'steering') {
114
- const column = frame.querySelector(`[data-native="${kind}"]`);
115
- const slot = column?.querySelector('[data-slot]');
116
- return slot?.firstElementChild?.firstElementChild?.firstElementChild ?? null;
132
+ return paddedTextBox(frame.querySelector(`[data-native="${kind}"]`));
117
133
  }
118
134
 
119
135
  function measure(element: Element | null) {
@@ -146,16 +162,15 @@ async function verify() {
146
162
  {...props as never}
147
163
  t={(props.t as ((key: string) => string) | undefined) ?? ((key: string) => key)}
148
164
  openFile={(props.openFile as (() => void) | undefined) ?? (() => {})}
149
- openSkill={(props.openSkill as (() => void) | undefined) ?? (() => {})}
150
- renderMessageImages={(props.renderMessageImages as (() => null) | undefined) ?? (() => null)}
151
165
  />
152
166
  );
153
167
  slots.register({ name: 'root', children: {
154
168
  'conversation.chat.node': { kind: 'keyed', scope: 'session', inject: { hooks: {
155
169
  turnData: (_: unknown, context: { turnData?: unknown }) => () => context.turnData,
156
170
  disclosure: (_: unknown, context: { disclosureReset?: { getSnapshot(): number } }) => () => context.disclosureReset,
171
+ groupAction: (_: unknown, context: { useGroupAction?: unknown }) => context.useGroupAction,
157
172
  } } },
158
- 'conversation.message.images': { kind: 'single', scope: 'session' },
173
+ 'conversation.chat.message.images': { kind: 'single', scope: 'session' },
159
174
  'fixture.reader': { kind: 'single', scope: 'session' },
160
175
  } }, ({ SessionProvider, renderSlot }: any) => (
161
176
  <SessionProvider>
@@ -182,6 +197,13 @@ async function verify() {
182
197
  ))}
183
198
  </>
184
199
  ));
200
+ slots.registerFactory({
201
+ name: 'conversation.chat.user.content', scope: 'session', locale: 'chat',
202
+ children: { 'conversation.chat.user.images': { kind: 'single', scope: 'session' } },
203
+ }, UserContent);
204
+ slots.registerFactory({
205
+ name: 'conversation.user.text', scope: 'session', locale: 'chat',
206
+ }, ({ text }: { text: string }) => <>{text}</>);
185
207
  slots.register({ name: 'conversation.chat.node', key: 'user', locale: 'chat' }, HostUser);
186
208
  slots.register({ name: 'conversation.chat.node', key: 'steering', locale: 'chat' }, HostUser);
187
209
  const stopMirror = installOfficialSlots(ctx);
@@ -48,6 +48,7 @@ test('reader user and steering bubbles match the host column cap', { skip, timeo
48
48
  '@fixture/registry': join(harness, 'packages/client/ui-renderer/src/client/registry.ts'),
49
49
  '@fixture/renderer': join(harness, 'packages/client/ui-renderer/src/client/scoped-slots.tsx'),
50
50
  '@fixture/user-message': join(harness, 'packages/client/ui-chat/src/client/chat/MessageItem.tsx'),
51
+ '@fixture/user-content': join(harness, 'packages/client/ui-chat/src/client/chat/UserContent.tsx'),
51
52
  '@deepseek-ai/dsh-client-ui-primitives': join(harness, 'packages/client/ui-primitives/src/index.ts'),
52
53
  '@deepseek-ai/dsh-client-ui-slots': join(harness, 'packages/client/ui-slots/src/index.ts'),
53
54
  '@deepseek-ai/dsh-client-store': join(harness, 'packages/client/store/src/index.ts'),
@@ -16,19 +16,23 @@ const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
16
16
  peerDependencies: Record<string, string>;
17
17
  };
18
18
 
19
- const HARNESS_PEER = '>=0.2.0-rc.1 <0.2.1';
19
+ const HARNESS_PEER = '>=0.2.1-rc.1 <0.2.2';
20
20
 
21
- test('Harness 0.2.0 peer range accepts the rc.1 and stable releases', () => {
21
+ test('Harness 0.2.1 peer range accepts rc.5 and rejects the 0.2.0 series', () => {
22
22
  const peers = Object.entries(pkg.peerDependencies).filter(([name]) => name.startsWith('@deepseek-ai/dsh-'));
23
23
  assert.ok(peers.length >= 12);
24
24
  for (const [name, range] of peers) {
25
25
  assert.equal(range, HARNESS_PEER, name);
26
- assert.equal(semver.satisfies('0.2.0-rc.2', range), true, name);
27
- assert.equal(semver.satisfies('0.2.0', range), true, name);
28
- assert.equal(semver.satisfies('0.2.0-alpha.1', range), false, name);
29
- assert.equal(semver.satisfies('0.2.0-alpha', range), false, name);
26
+ assert.equal(semver.satisfies('0.2.1-rc.5', range), true, name);
27
+ assert.equal(semver.satisfies('0.2.1-rc.1', range), true, name);
28
+ assert.equal(semver.satisfies('0.2.1', range), true, name);
29
+ assert.equal(semver.satisfies('0.2.1-alpha.1', range), false, name);
30
+ assert.equal(semver.satisfies('0.2.0-rc.2', range), false, name);
31
+ assert.equal(semver.satisfies('0.2.0', range), false, name);
30
32
  assert.equal(semver.satisfies('0.1.7-rc.2', range), false, name);
31
33
  }
34
+ const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { engines?: { dsh?: string } };
35
+ assert.equal(manifest.engines?.dsh, HARNESS_PEER);
32
36
  });
33
37
 
34
38
  test('declares dsh.bundle.patch so official add joins the profile layer stack', () => {