dsh-better-display 0.3.0 → 0.3.4

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.en.md +13 -15
  3. package/README.md +13 -15
  4. package/compat/harness-rc2.json +58 -18
  5. package/docs/official-rendering-bridge.md +1 -1
  6. package/lib/client.js +2227 -4059
  7. package/lib/client.js.map +1 -1
  8. package/lib/dsh-better-display.js +4 -2
  9. package/lib/types/client/OfficialContent.d.ts.map +1 -1
  10. package/lib/types/client/OfficialContent.js +16 -5
  11. package/lib/types/client/OfficialContent.js.map +1 -1
  12. package/lib/types/client/Reader.d.ts.map +1 -1
  13. package/lib/types/client/Reader.js +6 -7
  14. package/lib/types/client/Reader.js.map +1 -1
  15. package/lib/types/client/ToolActivity.js +3 -3
  16. package/lib/types/client/ToolActivity.js.map +1 -1
  17. package/lib/types/client/index.d.ts.map +1 -1
  18. package/lib/types/client/index.js.map +1 -1
  19. package/lib/types/client/markdown/render.d.ts.map +1 -1
  20. package/lib/types/client/markdown/render.js +2 -1
  21. package/lib/types/client/markdown/render.js.map +1 -1
  22. package/lib/types/client/native/ContextInjectionRow.d.ts +2 -2
  23. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -1
  24. package/lib/types/client/native/ContextInjectionRow.js +16 -4
  25. package/lib/types/client/native/ContextInjectionRow.js.map +1 -1
  26. package/lib/types/client/native/ReferenceIcon.js +3 -3
  27. package/lib/types/client/native/ReferenceIcon.js.map +1 -1
  28. package/lib/types/client/native/tool-call-model.js +1 -1
  29. package/lib/types/client/native/tool-call-model.js.map +1 -1
  30. package/lib/types/client/native/tool-update.d.ts +23 -0
  31. package/lib/types/client/native/tool-update.d.ts.map +1 -0
  32. package/lib/types/client/native/tool-update.js +22 -0
  33. package/lib/types/client/native/tool-update.js.map +1 -0
  34. package/lib/types/client/official-slots.js +1 -1
  35. package/lib/types/client/official-slots.js.map +1 -1
  36. package/lib/types/client/platform-media.d.ts +1 -1
  37. package/lib/types/client/platform-media.d.ts.map +1 -1
  38. package/lib/types/client/platform-media.js +9 -4
  39. package/lib/types/client/platform-media.js.map +1 -1
  40. package/lib/types/client/primitive-labels.d.ts.map +1 -1
  41. package/lib/types/client/primitive-labels.js +3 -2
  42. package/lib/types/client/primitive-labels.js.map +1 -1
  43. package/lib/types/client/tool-activity.js +2 -2
  44. package/lib/types/client/tool-activity.js.map +1 -1
  45. package/package.json +22 -18
  46. package/scripts/link-harness-dependencies.mjs +2 -0
  47. package/scripts/verify-official-bridge.mjs +6 -4
  48. package/scripts/verify-tool-shimmer.mjs +62 -0
  49. package/src/client/OfficialContent.tsx +19 -7
  50. package/src/client/Reader.module.css +17 -2
  51. package/src/client/Reader.tsx +8 -9
  52. package/src/client/ToolActivity.tsx +3 -3
  53. package/src/client/index.tsx +2 -0
  54. package/src/client/markdown/render.tsx +2 -1
  55. package/src/client/native/ContextInjectionRow.module.css +4 -0
  56. package/src/client/native/ContextInjectionRow.tsx +32 -10
  57. package/src/client/native/ReferenceIcon.tsx +3 -3
  58. package/src/client/native/tool-call-model.ts +1 -1
  59. package/src/client/native/tool-update.ts +32 -0
  60. package/src/client/official-slots.tsx +1 -1
  61. package/src/client/platform-media.ts +9 -3
  62. package/src/client/primitive-labels.ts +3 -2
  63. package/src/client/tool-activity.ts +2 -2
  64. package/tests/browser-auto-fold.tsx +1 -1
  65. package/tests/browser-official-bridge.tsx +17 -9
  66. package/tests/context-tool-update.test.ts +33 -0
  67. package/tests/platform-media.test.ts +6 -0
  68. package/tests/stock-install.test.ts +17 -0
  69. package/tsdown.config.ts +7 -5
@@ -10,8 +10,10 @@ import { readImageToolview } from '@fixture/read-image-row';
10
10
  import { en as conversationEn } from '@fixture/tool-locale';
11
11
  import { officialChildren, installOfficialSlots, OFFICIAL_SEATS } from '../src/client/official-slots.js';
12
12
  import { OfficialActions, OfficialTool } from '../src/client/OfficialContent.js';
13
- import { Deliverables } from '@fixture/deliverables';
13
+ import { DeliverablesTail } from '@fixture/deliverables';
14
14
  import { zh as deliverablesZh, en as deliverablesEn } from '@fixture/deliverables-locale';
15
+ import { FileRouteAction } from '@fixture/open-in-app-action';
16
+ import { zh as openInAppZh, en as openInAppEn } from '@fixture/open-in-app-locale';
15
17
  import { CopyAnswer } from '../src/client/Blocks.js';
16
18
  import { MotionMarkdown } from '../src/client/word-motion.js';
17
19
  import css from '../src/client/Reader.module.css';
@@ -32,7 +34,7 @@ const source = <T,>(initial: T) => {
32
34
  };
33
35
  const localeSource = source({ revision: 0 });
34
36
  let language = 'zh';
35
- const dictionaries: Record<string, any> = { deliverables: { zh: deliverablesZh, en: deliverablesEn }, conversation: { zh: conversationEn, en: conversationEn } };
37
+ const dictionaries: Record<string, any> = { deliverables: { zh: deliverablesZh, en: deliverablesEn }, 'open-in-app': { zh: openInAppZh, en: openInAppEn }, conversation: { zh: conversationEn, en: conversationEn } };
36
38
  const locale = {
37
39
  ...localeSource,
38
40
  register(ns: string, dictionary: any) { dictionaries[ns] = dictionary; return () => { delete dictionaries[ns]; }; },
@@ -60,7 +62,7 @@ ctx.provide('remote', {
60
62
  });
61
63
  const binding = { key: 'session-a', ctx: new Context(), hooks: {}, keyedHooks: {}, props: { sessionId: 'session-a' } };
62
64
  const current = source(binding);
63
- slots.installScope('session', { current, resolve: (id: string) => id === binding.key ? binding : undefined, renderArea: (_b: any, props: any) => props.children });
65
+ slots.installScope('session', { current, bindingSource: (target: any) => source(target === binding.key ? binding : undefined), renderArea: (_b: any, props: any) => props.children });
64
66
  const toolBlock = {
65
67
  kind: 'tool-result', callId: 'read-1', call: { name: 'read', args: { file_path: '/fixture/sample.ts', offset: 12 }, argsRaw: '{"file_path":"/fixture/sample.ts","offset":12}' },
66
68
  content: [{ type: 'text', text: '<path>/fixture/sample.ts</path>\n<type>file</type>\n<content>\n12: export const value = 1;\n</content>' }], isError: false, subCalls: [], time: 100, callTime: 10,
@@ -74,7 +76,7 @@ let stopMirror = () => {};
74
76
  slots.register({ name: 'root', children: {
75
77
  'conversation.chat.assistant-actions': { kind: 'list', scope: 'session' },
76
78
  'tool.call.toolview': { kind: 'keyed', scope: 'session' },
77
- 'conversation.chat.turnTail': { kind: 'chain', scope: 'session' },
79
+ 'conversation.chat.turnTail': { kind: 'list', scope: 'session' },
78
80
  'conversation.chat.node': { kind: 'keyed', scope: 'session', inject: { hooks: { turnData: (_s: any, value: any) => () => value } } },
79
81
  'conversation.message.images': { kind: 'single', scope: 'session' },
80
82
  'conversation.input.overlay': { kind: 'list', scope: 'session' },
@@ -90,12 +92,18 @@ slots.provideRoot({ hooks: { sessions: source({ byId: { 'session-a': { cwd: '/fi
90
92
  const artifact = { path: '/fixture/report.pdf', description: '演示报告', seq: 42, index: 0 };
91
93
  const presentedOpen = source({});
92
94
  const presentedHost = source({ available: true, fileManager: 'finder' });
93
- slots.register({ name: 'conversation.chat.turnTail', locale: 'deliverables',
94
- select: () => ({ produced: ['/fixture/sample.ts'], presented: [artifact] }),
95
- inject: () => ({ hooks: { presentedOpen, presentedHost }, reloadPresentedHost: async () => {},
95
+ const changesDiff = source({});
96
+ const changesSummary = source({});
97
+ const showCodeDiff = source(false);
98
+ slots.register({ name: 'conversation.chat.turnTail', id: '@deepseek-ai/dsh-client-ui-deliverables', locale: 'deliverables',
99
+ children: { 'deliverables.file.actions': { kind: 'list', scope: 'session' } },
100
+ inject: () => ({ hooks: { changesDiff, showCodeDiff, presentedOpen, presentedHost, changesSummary },
101
+ loadChangesDiff: async () => {}, reloadPresentedHost: async () => {}, loadChangesSummary: async () => {},
96
102
  openPresented: async (...args: any[]) => { observations.opens.push(['presented', ...args]); },
103
+ openChanged: async () => {}, openChangesReview: () => {},
97
104
  }),
98
- }, Deliverables);
105
+ }, DeliverablesTail);
106
+ slots.register({ name: 'deliverables.file.actions', id: 'open-in-app', locale: 'open-in-app' }, FileRouteAction);
99
107
  let liveDetail = source('initial');
100
108
  const storeHandle = { create: () => {
101
109
  observations.mounts++;
@@ -122,7 +130,7 @@ function ReaderFixture({ renderSlot, renderSlotChain }: any) {
122
130
  <CopyAnswer blocks={[{ kind: 'text', text }]} extraActions={<OfficialActions official={official} messageId={'message-1' as any} />} />
123
131
  </article>
124
132
  <div data-reader-tool><OfficialTool {...blockProps} official={official} renderSlotChain={renderSlotChain} block={toolBlock as any} toolName="read" fallback={<span>fallback</span>} /></div>
125
- {renderSlotChain(OFFICIAL_SEATS.tail, { openFile: blockProps.openFile, readerProducedPaths: ['/fixture/sample.ts'] })}
133
+ {renderSlot(OFFICIAL_SEATS.tail, { turn: { data: new Map([['deliverables', { changes: null, presented: [artifact] }]]) }, seq: 43, openFile: blockProps.openFile, readerProducedPaths: ['/fixture/sample.ts'] })}
126
134
  {renderSlot(OFFICIAL_SEATS.nodes, {}, { entryKey: 'future-widget', hookContext: 'turn-context' })}
127
135
  <button onClick={() => setText(text + '\n\n继续输出。')}>Append text</button>
128
136
  </div></main>;
@@ -0,0 +1,33 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { toolUpdateRows } from '../src/client/native/tool-update.js';
4
+
5
+ test('a single tool addition is its own closed row', () => {
6
+ assert.deepEqual(toolUpdateRows([{ type: 'tool-addition', toolName: 'search' }]), {
7
+ added: ['search'],
8
+ removed: [],
9
+ single: { type: 'tool-addition', toolName: 'search' },
10
+ });
11
+ });
12
+
13
+ test('mixed tool changes stay one expandable update', () => {
14
+ assert.deepEqual(toolUpdateRows([
15
+ { type: 'tool-addition', toolName: 'search' },
16
+ { type: 'tool-addition', toolName: 'read_file' },
17
+ { type: 'tool-removal', toolName: 'old_search' },
18
+ ]), {
19
+ added: ['search', 'read_file'],
20
+ removed: ['old_search'],
21
+ single: null,
22
+ });
23
+ });
24
+
25
+ test('text or a missing tool name stays a generic context row', () => {
26
+ assert.equal(toolUpdateRows([{ type: 'text', toolName: 'search' }]), null);
27
+ assert.equal(toolUpdateRows([{ type: 'tool-addition' }]), null);
28
+ assert.equal(toolUpdateRows([]), null);
29
+ assert.equal(toolUpdateRows([
30
+ { type: 'tool-addition', toolName: 'search' },
31
+ { type: 'text' },
32
+ ]), null);
33
+ });
@@ -14,4 +14,10 @@ test('relative, protocol-relative and non-HTTP page paths are not sent to the Ho
14
14
  assert.equal(localPathMediaUrl('http:', 'http://local', path), undefined);
15
15
  }
16
16
  assert.equal(localPathMediaUrl('file:', 'null', '/tmp/image.png'), undefined);
17
+ assert.equal(localPathMediaUrl('dsh-app:', 'dsh-app://other', '/tmp/image.png'), undefined);
18
+ });
19
+ test('Desktop file routes use the application file API', () => {
20
+ const url = localPathMediaUrl('dsh-app:', 'dsh-app://app', '/tmp/a.png')!;
21
+ assert.equal(url.startsWith('dsh-app://app/api/file?'), true);
22
+ assert.equal(new URL(url).searchParams.get('path'), '/tmp/a.png');
17
23
  });
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import { test } from 'node:test';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import semver from 'semver';
6
7
 
7
8
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
9
  const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
@@ -11,8 +12,24 @@ const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
11
12
  exports: Record<string, { default?: string } | string>;
12
13
  files: string[];
13
14
  dsh: { bundle?: { patch?: string } };
15
+ peerDependencies: Record<string, string>;
14
16
  };
15
17
 
18
+ const HARNESS_PEER = '>=0.2.0-rc.1 <0.2.1';
19
+
20
+ test('Harness 0.2.0 peer range accepts the rc.1 and stable releases', () => {
21
+ const peers = Object.entries(pkg.peerDependencies).filter(([name]) => name.startsWith('@deepseek-ai/dsh-'));
22
+ assert.ok(peers.length >= 12);
23
+ for (const [name, range] of peers) {
24
+ assert.equal(range, HARNESS_PEER, name);
25
+ assert.equal(semver.satisfies('0.2.0-rc.2', range), true, name);
26
+ assert.equal(semver.satisfies('0.2.0', range), true, name);
27
+ assert.equal(semver.satisfies('0.2.0-alpha.1', range), false, name);
28
+ assert.equal(semver.satisfies('0.2.0-alpha', range), false, name);
29
+ assert.equal(semver.satisfies('0.1.7-rc.2', range), false, name);
30
+ }
31
+ });
32
+
16
33
  test('declares dsh.bundle.patch so official add joins the profile layer stack', () => {
17
34
  assert.equal(pkg.dsh.bundle?.patch, './cordis.patch.yml');
18
35
  assert.equal(existsSync(resolve(root, 'cordis.patch.yml')), true);
package/tsdown.config.ts CHANGED
@@ -1,21 +1,23 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
4
+ import { pathToFileURL, fileURLToPath } from 'node:url';
5
5
  import type { TsdownPlugin, UserConfig } from 'tsdown';
6
6
 
7
- function resolveHarness(): string {
7
+ const vendored = fileURLToPath(new URL('./tools/client-build.js', import.meta.url));
8
+
9
+ function resolveHarnessAdapter(): string {
8
10
  const configured = process.env.DSHX_HARNESS?.trim();
9
- if (configured) return resolve(configured);
11
+ if (configured) return join(resolve(configured), 'tools/dshx/src/client-build.js');
10
12
  const configPath = join(homedir(), '.config/dshx/harness');
11
13
  const recorded = existsSync(configPath) ? readFileSync(configPath, 'utf8').trim() : undefined;
12
14
  if (!recorded) {
13
15
  throw new Error('client rebuild requires a Harness root from DSHX_HARNESS or ~/.config/dshx/harness');
14
16
  }
15
- return resolve(recorded);
17
+ return join(resolve(recorded), 'tools/dshx/src/client-build.js');
16
18
  }
17
19
 
18
- const adapter = join(resolveHarness(), 'tools/dshx/src/client-build.js');
20
+ const adapter = existsSync(vendored) ? vendored : resolveHarnessAdapter();
19
21
  if (!existsSync(adapter)) throw new Error('externalClientBundle adapter is missing.');
20
22
  const { externalClientBundle } = await import(pathToFileURL(adapter).href);
21
23