dsh-better-display 0.3.2 → 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 (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.en.md +13 -15
  3. package/README.md +13 -15
  4. package/compat/harness-rc2.json +11 -9
  5. package/docs/official-rendering-bridge.md +1 -1
  6. package/lib/client.js +710 -2639
  7. package/lib/client.js.map +1 -1
  8. package/lib/types/client/Reader.js +1 -1
  9. package/lib/types/client/index.d.ts.map +1 -1
  10. package/lib/types/client/index.js.map +1 -1
  11. package/lib/types/client/markdown/render.d.ts.map +1 -1
  12. package/lib/types/client/markdown/render.js +2 -1
  13. package/lib/types/client/markdown/render.js.map +1 -1
  14. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -1
  15. package/lib/types/client/native/ContextInjectionRow.js +15 -3
  16. package/lib/types/client/native/ContextInjectionRow.js.map +1 -1
  17. package/lib/types/client/native/tool-update.d.ts +23 -0
  18. package/lib/types/client/native/tool-update.d.ts.map +1 -0
  19. package/lib/types/client/native/tool-update.js +22 -0
  20. package/lib/types/client/native/tool-update.js.map +1 -0
  21. package/lib/types/client/platform-media.d.ts +1 -1
  22. package/lib/types/client/platform-media.d.ts.map +1 -1
  23. package/lib/types/client/platform-media.js +9 -4
  24. package/lib/types/client/platform-media.js.map +1 -1
  25. package/package.json +21 -17
  26. package/scripts/link-harness-dependencies.mjs +2 -0
  27. package/scripts/verify-tool-shimmer.mjs +62 -0
  28. package/src/client/Reader.module.css +5 -2
  29. package/src/client/Reader.tsx +1 -1
  30. package/src/client/index.tsx +2 -0
  31. package/src/client/markdown/render.tsx +2 -1
  32. package/src/client/native/ContextInjectionRow.module.css +4 -0
  33. package/src/client/native/ContextInjectionRow.tsx +28 -6
  34. package/src/client/native/tool-update.ts +32 -0
  35. package/src/client/platform-media.ts +9 -3
  36. package/tests/context-tool-update.test.ts +33 -0
  37. package/tests/platform-media.test.ts +6 -0
  38. package/tests/stock-install.test.ts +17 -0
  39. package/tsdown.config.ts +7 -5
@@ -0,0 +1,32 @@
1
+ /** One developer tool-change block the 0.1.7-rc.2 context row names in place. */
2
+ export interface ToolChangeBlock {
3
+ type: 'tool-addition' | 'tool-removal'
4
+ toolName: string
5
+ }
6
+
7
+ /** Names collected from a context whose every block is a tool change. */
8
+ export interface ToolUpdateRows {
9
+ added: readonly string[]
10
+ removed: readonly string[]
11
+ /** The only block, when the row is a single add or removal. */
12
+ single: ToolChangeBlock | null
13
+ }
14
+
15
+ /**
16
+ * Classify a context as the rc.2 tool-update row.
17
+ * Mixed or empty content stays on the generic injection presentation.
18
+ * @param content - durable context blocks.
19
+ * @returns collected names, or null when any block is not a tool change.
20
+ */
21
+ export function toolUpdateRows(content: readonly { readonly type: string; readonly toolName?: string }[]): ToolUpdateRows | null {
22
+ if (content.length === 0) return null
23
+ const rows: ToolChangeBlock[] = []
24
+ for (const block of content) {
25
+ if (block.type !== 'tool-addition' && block.type !== 'tool-removal') return null
26
+ if (typeof block.toolName !== 'string') return null
27
+ rows.push({ type: block.type, toolName: block.toolName })
28
+ }
29
+ const added = rows.flatMap(row => row.type === 'tool-addition' ? [row.toolName] : [])
30
+ const removed = rows.flatMap(row => row.type === 'tool-removal' ? [row.toolName] : [])
31
+ return { added, removed, single: rows.length === 1 ? rows[0]! : null }
32
+ }
@@ -1,8 +1,14 @@
1
- /** RC2 Chat's same-origin local media contract. Authorization stays Host-side. */
1
+ /** Web or Desktop file route. Authorization stays Host-side. */
2
2
  export function localPathMediaUrl(protocol: string, origin: string, value: string): string | undefined {
3
- if (protocol !== 'http:' && protocol !== 'https:') return undefined;
4
3
  if (!value.startsWith('/') || value.startsWith('//')) return undefined;
5
- return `${origin}/api/file?path=${encodeURIComponent(value)}`;
4
+ if (protocol === 'http:' || protocol === 'https:') {
5
+ return `${origin}/api/file?path=${encodeURIComponent(value)}`;
6
+ }
7
+ // Official fileMediaUrl accepts the Desktop application base dsh-app://app/.
8
+ if (protocol === 'dsh-app:' && origin === 'dsh-app://app') {
9
+ return `dsh-app://app/api/file?path=${encodeURIComponent(value)}`;
10
+ }
11
+ return undefined;
6
12
  }
7
13
 
8
14
  export const readerPathImages = {
@@ -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