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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
4
|
-
import { test } from '
|
|
5
|
+
import { test } from 'vitest';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import semver from 'semver';
|
|
7
8
|
|
|
@@ -63,6 +64,13 @@ test('commits compiled lib entries and does not require a prepare script', () =>
|
|
|
63
64
|
assert.doesNotMatch(clientJs, /submission\.images\.length/);
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
test('compiled host entry loads in Node without a TypeScript loader', () => {
|
|
68
|
+
const result = spawnSync(process.execPath, ['--input-type=module', '--eval',
|
|
69
|
+
"const plugin = await import('./lib/dsh-better-display.js'); if (plugin.name !== 'dsh-better-display' || typeof plugin.apply !== 'function') process.exit(1);",
|
|
70
|
+
], { cwd: root, encoding: 'utf8' });
|
|
71
|
+
assert.equal(result.status, 0, result.stderr || String(result.error ?? 'host entry did not load'));
|
|
72
|
+
});
|
|
73
|
+
|
|
66
74
|
test('README leads with the official stock one-liner and names pnpm', () => {
|
|
67
75
|
for (const name of ['README.md', 'README.en.md']) {
|
|
68
76
|
const text = readFileSync(resolve(root, name), 'utf8');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from '
|
|
2
|
+
import { test } from 'vitest';
|
|
3
3
|
import { StreamBuffer, STREAM_TIMING } from '../src/client/stream-buffer.js';
|
|
4
4
|
|
|
5
5
|
test('a transport burst becomes multiple bounded, monotonically growing frames', () => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'vitest';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { ensureStyles } from '../src/client/styles.js';
|
|
5
|
+
|
|
6
|
+
test('ensureStyles tolerates module evaluation without a document', () => {
|
|
7
|
+
ensureStyles();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('ensureStyles restores removed plugin CSS and remains idempotent', t => {
|
|
11
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
|
12
|
+
t.onTestFinished(() => {
|
|
13
|
+
if (descriptor) Object.defineProperty(globalThis, 'document', descriptor);
|
|
14
|
+
else Reflect.deleteProperty(globalThis, 'document');
|
|
15
|
+
});
|
|
16
|
+
const reader = { dataset: { plugin: 'dsh-better-display', pluginCss: 'dsh-better-display/Reader.module.css' }, textContent: '.reader{height:auto}' };
|
|
17
|
+
const fold = { dataset: { plugin: 'dsh-better-display', pluginCss: 'dsh-better-display/Fold.module.css' }, textContent: '.fold{overflow:hidden}' };
|
|
18
|
+
const foreign = { dataset: { plugin: 'other', pluginCss: 'other/Fold.module.css' }, textContent: '.other{}' };
|
|
19
|
+
const tags = [reader, fold, foreign];
|
|
20
|
+
const document = {
|
|
21
|
+
querySelectorAll: (selector: string) => {
|
|
22
|
+
assert.equal(selector, 'style[data-plugin="dsh-better-display"][data-plugin-css]');
|
|
23
|
+
return tags.filter(tag => tag.dataset.plugin === 'dsh-better-display');
|
|
24
|
+
},
|
|
25
|
+
createElement: (name: string) => { assert.equal(name, 'style'); return { dataset: {}, textContent: '' }; },
|
|
26
|
+
head: { appendChild: (tag: typeof reader) => { tags.push(tag); } },
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(globalThis, 'document', { configurable: true, value: document });
|
|
29
|
+
ensureStyles();
|
|
30
|
+
assert.equal(tags.length, 3);
|
|
31
|
+
tags.splice(0, 2);
|
|
32
|
+
ensureStyles();
|
|
33
|
+
assert.deepEqual(tags, [foreign, reader, fold]);
|
|
34
|
+
ensureStyles();
|
|
35
|
+
assert.equal(tags.length, 3);
|
|
36
|
+
tags[1].textContent = '.reader{height:100px}';
|
|
37
|
+
ensureStyles();
|
|
38
|
+
tags.splice(1, 1);
|
|
39
|
+
ensureStyles();
|
|
40
|
+
assert.equal(tags.find(tag => tag.dataset.pluginCss === reader.dataset.pluginCss)?.textContent, '.reader{height:100px}');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('CSS is cached after module injection and restored before mount layout effects', () => {
|
|
44
|
+
const index = readFileSync(new URL('../src/client/index.tsx', import.meta.url), 'utf8');
|
|
45
|
+
const reader = readFileSync(new URL('../src/client/Reader.tsx', import.meta.url), 'utf8');
|
|
46
|
+
assert.match(index, /\nensureStyles\(\);/);
|
|
47
|
+
// Whitespace-tolerant: the source is formatted by the project's own toolchain,
|
|
48
|
+
// which may break the effect body onto its own line.
|
|
49
|
+
assert.match(reader, /useInsertionEffect\(\(\) => \{[\s\S]{0,40}?ensureStyles\(\);[\s\S]{0,20}?\}, \[\]\)/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('user cluster fills the column so the host cap resolves once', () => {
|
|
53
|
+
const css = readFileSync(new URL('../src/client/Reader.module.css', import.meta.url), 'utf8');
|
|
54
|
+
const cluster = css.match(/\.userCluster\s*\{[^}]*\}/)?.[0] ?? '';
|
|
55
|
+
assert.match(cluster, /width:\s*100%/);
|
|
56
|
+
assert.match(cluster, /max-width:\s*100%/);
|
|
57
|
+
assert.doesNotMatch(cluster, /0\.702/);
|
|
58
|
+
assert.doesNotMatch(cluster, /525px/);
|
|
59
|
+
assert.doesNotMatch(css, /525px/);
|
|
60
|
+
// A direct-child cap misses the display:contents slot. The host row is the
|
|
61
|
+
// slot's child; capping that grandchild would replace .userStack's own 82%.
|
|
62
|
+
assert.doesNotMatch(css, /\.userCluster\s*>\s*\*/);
|
|
63
|
+
assert.doesNotMatch(css, /\[data-slot="conversation\.chat\.node"\]/);
|
|
64
|
+
assert.match(css, /\.userCluster > \[data-slot\] > \* \{[^}]*align-self:\s*stretch/);
|
|
65
|
+
assert.match(css, /\.userCluster > \[data-slot\] > \* \{[^}]*width:\s*100%/);
|
|
66
|
+
assert.doesNotMatch(css, /\.userCluster > \[data-slot\] > \* > \*/);
|
|
67
|
+
const user = css.match(/\.user\s*\{[^}]*\}/)?.[0] ?? '';
|
|
68
|
+
assert.match(user, /max-width:\s*min\(calc\(var\(--dsh-chat-content-width,\s*748px\)\s*\*\s*0\.702\),\s*82%\)/);
|
|
69
|
+
assert.match(user, /font-size:\s*var\(--dsh-content-font-size,\s*14px\)/);
|
|
70
|
+
assert.match(user, /line-height:\s*calc\(22px \+ var\(--dsh-content-font-delta,\s*0px\)\)/);
|
|
71
|
+
assert.match(user, /font-weight:\s*400/);
|
|
72
|
+
assert.match(user, /white-space:\s*pre-wrap/);
|
|
73
|
+
assert.match(user, /border-radius:\s*var\(--dsw-radius-xl,\s*22px\)/);
|
|
74
|
+
assert.doesNotMatch(css, /\.user\s*\{\s*max-width:\s*90%/);
|
|
75
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { test } from 'vitest';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import type { PreparingToolCall, RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
4
|
+
import { activityPhase, activitySummary } from '../src/client/tool-activity.ts';
|
|
5
|
+
import type { ToolDraft } from '../src/client/tool-activity.ts';
|
|
6
|
+
|
|
7
|
+
function draft(name: string, argsRaw: string): ToolDraft {
|
|
8
|
+
return { kind: 'tool-call', callId: 'terminal-preview', name, argsRaw };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test('terminal drafts expose a pending placeholder state until a command field appears', () => {
|
|
12
|
+
for (const name of ['bash', 'pwsh']) {
|
|
13
|
+
const early = activitySummary({ draft: draft(name, '{"comman') });
|
|
14
|
+
assert.equal(early.category, 'terminal');
|
|
15
|
+
assert.equal(early.argsState, 'partial');
|
|
16
|
+
assert.equal(early.command, undefined);
|
|
17
|
+
|
|
18
|
+
const partial = activitySummary({ draft: draft(name, '{"command":"Get-C') });
|
|
19
|
+
assert.equal(partial.argsState, 'partial');
|
|
20
|
+
assert.equal(partial.command, 'Get-C');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('complete Bash and PowerShell arguments remain available in full', () => {
|
|
25
|
+
for (const name of ['bash', 'pwsh']) {
|
|
26
|
+
const values = {
|
|
27
|
+
command: name === 'pwsh' ? 'Get-ChildItem -Force' : 'printf "ready\\n"',
|
|
28
|
+
workdir: '/work/project',
|
|
29
|
+
description: 'List project files',
|
|
30
|
+
timeout_ms: 4000,
|
|
31
|
+
};
|
|
32
|
+
const model = activitySummary({ draft: draft(name, JSON.stringify(values)) });
|
|
33
|
+
assert.equal(model.argsState, 'complete');
|
|
34
|
+
assert.equal(model.command, values.command);
|
|
35
|
+
assert.deepEqual(model.args, values);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('partial escaped and multiline commands keep their authored text', () => {
|
|
40
|
+
const argsRaw = '{"command":"printf \\"first\\nsecond';
|
|
41
|
+
const model = activitySummary({ draft: draft('bash', argsRaw) });
|
|
42
|
+
assert.equal(model.argsState, 'partial');
|
|
43
|
+
assert.equal(model.command, 'printf "first\nsecond');
|
|
44
|
+
|
|
45
|
+
const multiline = 'printf "first line\\nsecond line"\necho 你好';
|
|
46
|
+
const complete = activitySummary({ draft: draft('bash', JSON.stringify({ command: multiline, workdir: '/tmp' })) });
|
|
47
|
+
assert.equal(complete.command, multiline);
|
|
48
|
+
assert.equal(complete.args.workdir, '/tmp');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('complete empty and non-command arguments are not mistaken for partial commands', () => {
|
|
52
|
+
const empty = activitySummary({ draft: draft('bash', '{}') });
|
|
53
|
+
assert.equal(empty.argsState, 'complete');
|
|
54
|
+
assert.deepEqual(empty.args, {});
|
|
55
|
+
assert.equal(empty.command, undefined);
|
|
56
|
+
|
|
57
|
+
const read = activitySummary({ draft: draft('read', '{"path":"/tmp/readme.md"}') });
|
|
58
|
+
assert.equal(read.argsState, 'complete');
|
|
59
|
+
assert.equal(read.category, 'read');
|
|
60
|
+
assert.equal(read.command, undefined);
|
|
61
|
+
assert.equal(read.args.path, '/tmp/readme.md');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('the activity lifecycle distinguishes draft, dispatched call, and result', () => {
|
|
65
|
+
const argsRaw = '{"command":"echo ready"}';
|
|
66
|
+
const pending = draft('bash', argsRaw);
|
|
67
|
+
const running: RunningToolCall = {
|
|
68
|
+
phase: 'start', callId: pending.callId, name: pending.name, turn: 1, step: 0, time: 1000, argsRaw, subCalls: [],
|
|
69
|
+
};
|
|
70
|
+
const result: ToolResultNode = {
|
|
71
|
+
kind: 'tool-result', seq: 2, time: 2000, callId: pending.callId,
|
|
72
|
+
call: { name: pending.name, argsRaw }, callTime: 1000,
|
|
73
|
+
content: [{ type: 'text', text: 'ready\n[exit code: 0]' }], isError: false, meta: { exitCode: 0 }, subCalls: [],
|
|
74
|
+
};
|
|
75
|
+
assert.equal(activityPhase({ draft: pending }), 'preparing');
|
|
76
|
+
assert.equal(activityPhase({ block: running }), 'running');
|
|
77
|
+
assert.equal(activityPhase({ block: result }), 'succeeded');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a canonical preparing call remains preparing until dispatch', () => {
|
|
81
|
+
for (const name of ['bash', 'pwsh']) {
|
|
82
|
+
const block: PreparingToolCall = {
|
|
83
|
+
phase: 'preparing', callId: 'preparing-head', name, turn: 1, step: 0, time: 1000, subCalls: [],
|
|
84
|
+
};
|
|
85
|
+
assert.equal(activityPhase({ block }), 'preparing');
|
|
86
|
+
assert.equal(activityPhase({ block }, true), 'interrupted');
|
|
87
|
+
const model = activitySummary({ block });
|
|
88
|
+
assert.equal(model.name, name);
|
|
89
|
+
assert.equal(model.argsState, 'partial');
|
|
90
|
+
assert.equal(model.raw, '');
|
|
91
|
+
assert.equal(model.command, undefined);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a canonical preparing head retains progressively generated draft arguments', () => {
|
|
96
|
+
for (const name of ['bash', 'pwsh']) {
|
|
97
|
+
const block: PreparingToolCall = {
|
|
98
|
+
phase: 'preparing', callId: 'preparing-head', name, turn: 1, step: 0, time: 1000, subCalls: [],
|
|
99
|
+
};
|
|
100
|
+
for (const raw of ['{"comman', '{"command":"echo ready']) {
|
|
101
|
+
const pending = { ...draft(name, raw), callId: block.callId };
|
|
102
|
+
const model = activitySummary({ block, draft: pending });
|
|
103
|
+
assert.equal(model.raw, raw);
|
|
104
|
+
assert.equal(model.argsState, 'partial');
|
|
105
|
+
assert.equal(model.command, raw === '{"comman' ? undefined : 'echo ready');
|
|
106
|
+
}
|
|
107
|
+
const dispatched: RunningToolCall = { ...block, phase: 'start', argsRaw: '{"command":"echo sent"}' };
|
|
108
|
+
const model = activitySummary({ block: dispatched, draft: draft(name, '{"command":"stale') });
|
|
109
|
+
assert.equal(activityPhase({ block: dispatched }), 'running');
|
|
110
|
+
assert.equal(model.command, 'echo sent');
|
|
111
|
+
assert.equal(model.argsState, 'complete');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'vitest';
|
|
3
|
+
import { StreamBuffer } from '../src/client/stream-buffer.js';
|
|
4
|
+
import {
|
|
5
|
+
activeTextTransform,
|
|
6
|
+
createTextTransformRegistry,
|
|
7
|
+
displayStreaming,
|
|
8
|
+
installTextTransformRegistry,
|
|
9
|
+
nextDisplayCursor,
|
|
10
|
+
originalAnswerText,
|
|
11
|
+
readDisplayText,
|
|
12
|
+
readerBlockOwner,
|
|
13
|
+
type DisplayTextRequest,
|
|
14
|
+
} from '../src/client/text-transform.js';
|
|
15
|
+
import type { ReaderBlockOwner } from '../src/client/types.js';
|
|
16
|
+
import { WordTimeline } from '../src/client/word-timeline.js';
|
|
17
|
+
|
|
18
|
+
const reasoning = (text: string, streaming = true, blockKey = 'block-1'): DisplayTextRequest => ({
|
|
19
|
+
text, streaming, blockKey, kind: 'reasoning', source: 'assistant',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('no transformer leaves the source text and the streaming flag untouched', () => {
|
|
23
|
+
const registry = createTextTransformRegistry();
|
|
24
|
+
const request = reasoning('Hello world', true);
|
|
25
|
+
const shown = readDisplayText(registry, request);
|
|
26
|
+
assert.equal(shown.text, request.text);
|
|
27
|
+
assert.equal(shown.final, false);
|
|
28
|
+
assert.equal(shown.motionRevision, 0);
|
|
29
|
+
assert.equal(shown.transformRevision, 0);
|
|
30
|
+
assert.equal(readDisplayText(undefined, request).text, 'Hello world');
|
|
31
|
+
assert.equal(displayStreaming(true, shown, false), true);
|
|
32
|
+
assert.equal(displayStreaming(false, { final: true }, false), false);
|
|
33
|
+
assert.equal(registry.has('reasoning'), false);
|
|
34
|
+
assert.equal(registry.has('body'), false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('a sync transformer rewrites the display string and leaves the block alone', () => {
|
|
38
|
+
const registry = createTextTransformRegistry();
|
|
39
|
+
const source = 'Hello';
|
|
40
|
+
const block = { kind: 'text' as const, text: source };
|
|
41
|
+
registry.register('body', {
|
|
42
|
+
read: request => ({ text: request.text.toUpperCase(), final: !request.streaming }),
|
|
43
|
+
});
|
|
44
|
+
const streaming = readDisplayText(registry, { text: source, streaming: true, blockKey: 'a', kind: 'body', source: 'assistant' });
|
|
45
|
+
const settled = readDisplayText(registry, { text: source, streaming: false, blockKey: 'a', kind: 'body', source: 'assistant' });
|
|
46
|
+
assert.equal(streaming.text, 'HELLO');
|
|
47
|
+
assert.equal(streaming.final, false);
|
|
48
|
+
assert.equal(displayStreaming(true, streaming, true), true);
|
|
49
|
+
assert.equal(settled.text, 'HELLO');
|
|
50
|
+
assert.equal(settled.final, true);
|
|
51
|
+
assert.equal(displayStreaming(false, settled, true), false);
|
|
52
|
+
assert.equal(source, 'Hello');
|
|
53
|
+
assert.equal(block.text, 'Hello');
|
|
54
|
+
assert.equal(originalAnswerText([block, { kind: 'reasoning', text: 'secret thought' }]), 'Hello');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('an async append bumps the transform revision without restarting the reveal', () => {
|
|
58
|
+
const registry = createTextTransformRegistry();
|
|
59
|
+
let shown = 'Hello';
|
|
60
|
+
const listeners = new Set<() => void>();
|
|
61
|
+
registry.register('reasoning', {
|
|
62
|
+
read: () => ({ text: shown, final: false }),
|
|
63
|
+
subscribe(listener) {
|
|
64
|
+
listeners.add(listener);
|
|
65
|
+
return () => { listeners.delete(listener); };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const request = reasoning('thinking starts', true);
|
|
69
|
+
const first = registry.read(request);
|
|
70
|
+
assert.equal(first.text, 'Hello');
|
|
71
|
+
assert.equal(first.transformRevision, 0);
|
|
72
|
+
assert.equal(first.motionRevision, 0);
|
|
73
|
+
|
|
74
|
+
const buffer = new StreamBuffer('');
|
|
75
|
+
const timeline = new WordTimeline();
|
|
76
|
+
timeline.begin('', true, 0, 0);
|
|
77
|
+
const paint = (text: string, now: number) => {
|
|
78
|
+
buffer.update(text, now, { finished: false });
|
|
79
|
+
buffer.advance(now + 120);
|
|
80
|
+
timeline.begin(buffer.visible, true, buffer.revision, now + 120);
|
|
81
|
+
};
|
|
82
|
+
paint(first.text, 100);
|
|
83
|
+
assert.equal(buffer.revision, 0);
|
|
84
|
+
assert.ok(buffer.visible.length > 0);
|
|
85
|
+
const generation = timeline.generation;
|
|
86
|
+
const prefixBorn = timeline.bornAt(0);
|
|
87
|
+
const visible = buffer.visible;
|
|
88
|
+
assert.notEqual(prefixBorn, null);
|
|
89
|
+
|
|
90
|
+
shown = 'Hello there';
|
|
91
|
+
for (const listener of listeners) listener();
|
|
92
|
+
const second = registry.read(request);
|
|
93
|
+
assert.equal(second.text, 'Hello there');
|
|
94
|
+
assert.equal(second.motionRevision, first.motionRevision);
|
|
95
|
+
assert.equal(second.transformRevision, first.transformRevision + 1);
|
|
96
|
+
paint(second.text, 400);
|
|
97
|
+
assert.equal(buffer.revision, 0);
|
|
98
|
+
assert.ok(buffer.visible.startsWith(visible));
|
|
99
|
+
assert.ok(buffer.visible.length > visible.length);
|
|
100
|
+
assert.equal(timeline.generation, generation);
|
|
101
|
+
assert.equal(timeline.bornAt(0), prefixBorn);
|
|
102
|
+
|
|
103
|
+
// The transform counter is a different clock. Feeding it to the timeline
|
|
104
|
+
// settles the paragraph; the reader does not do that.
|
|
105
|
+
const trap = new WordTimeline();
|
|
106
|
+
trap.begin('', true, 0, 0);
|
|
107
|
+
trap.begin(visible, true, first.motionRevision, 220);
|
|
108
|
+
const trapGeneration = trap.generation;
|
|
109
|
+
trap.begin(second.text, true, second.transformRevision, 520);
|
|
110
|
+
assert.notEqual(trap.generation, trapGeneration);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('a replaced translation settles on the new string instead of replaying from empty', () => {
|
|
114
|
+
const registry = createTextTransformRegistry();
|
|
115
|
+
let shown = 'Hello there';
|
|
116
|
+
registry.register('reasoning', {
|
|
117
|
+
read: () => ({ text: shown, final: false }),
|
|
118
|
+
});
|
|
119
|
+
const request = reasoning('source', true);
|
|
120
|
+
const first = registry.read(request);
|
|
121
|
+
const buffer = new StreamBuffer('');
|
|
122
|
+
buffer.update(first.text, 100);
|
|
123
|
+
buffer.advance(220);
|
|
124
|
+
assert.equal(buffer.visible, 'Hello there');
|
|
125
|
+
const timeline = new WordTimeline();
|
|
126
|
+
timeline.begin('', true, buffer.revision, 0);
|
|
127
|
+
timeline.begin(buffer.visible, true, buffer.revision, 220);
|
|
128
|
+
const generation = timeline.generation;
|
|
129
|
+
|
|
130
|
+
shown = '你好世界';
|
|
131
|
+
const second = registry.read(request);
|
|
132
|
+
assert.equal(second.motionRevision, first.motionRevision + 1);
|
|
133
|
+
assert.ok(second.transformRevision > first.transformRevision);
|
|
134
|
+
buffer.update(second.text, 400);
|
|
135
|
+
assert.equal(buffer.visible, '你好世界');
|
|
136
|
+
assert.notEqual(buffer.visible, '');
|
|
137
|
+
assert.equal(buffer.revision, 1);
|
|
138
|
+
timeline.begin(buffer.visible, true, buffer.revision, 400);
|
|
139
|
+
assert.notEqual(timeline.generation, generation);
|
|
140
|
+
assert.equal(timeline.bornAt(0), null);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('the block owner carries presentation, holdFormatting, and liveText', () => {
|
|
144
|
+
const block = { kind: 'reasoning' as const, text: 'original' };
|
|
145
|
+
const owner = readerBlockOwner(block as ReaderBlockOwner['block'], true, 'assistant', {
|
|
146
|
+
startedAt: 42, interrupted: false, liveText: true,
|
|
147
|
+
}, false);
|
|
148
|
+
assert.equal(owner.block, block);
|
|
149
|
+
assert.equal(owner.streaming, true);
|
|
150
|
+
assert.equal(owner.source, 'assistant');
|
|
151
|
+
assert.deepEqual(owner.presentation, { startedAt: 42, interrupted: false, liveText: true });
|
|
152
|
+
assert.equal(owner.holdFormatting, false);
|
|
153
|
+
assert.equal(owner.liveText, true);
|
|
154
|
+
assert.equal(owner.block.text, 'original');
|
|
155
|
+
const bare: ReaderBlockOwner = { block: owner.block, streaming: false, source: 'user' };
|
|
156
|
+
assert.equal(bare.presentation, undefined);
|
|
157
|
+
assert.equal(bare.holdFormatting, undefined);
|
|
158
|
+
assert.equal(bare.liveText, undefined);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('unregistering restores the original text and the install pointer nests', () => {
|
|
162
|
+
const registry = createTextTransformRegistry();
|
|
163
|
+
const dispose = registry.register(['reasoning', 'body'], {
|
|
164
|
+
read: request => request.text.toUpperCase(),
|
|
165
|
+
});
|
|
166
|
+
assert.equal(registry.has('reasoning'), true);
|
|
167
|
+
assert.equal(registry.has('body'), true);
|
|
168
|
+
assert.equal(readDisplayText(registry, reasoning('Ab')).text, 'AB');
|
|
169
|
+
dispose();
|
|
170
|
+
assert.equal(registry.has('reasoning'), false);
|
|
171
|
+
assert.equal(readDisplayText(registry, reasoning('Ab')).text, 'Ab');
|
|
172
|
+
|
|
173
|
+
const outer = createTextTransformRegistry();
|
|
174
|
+
const restoreOuter = installTextTransformRegistry(outer);
|
|
175
|
+
try {
|
|
176
|
+
assert.equal(activeTextTransform(), outer);
|
|
177
|
+
const inner = createTextTransformRegistry();
|
|
178
|
+
const restoreInner = installTextTransformRegistry(inner);
|
|
179
|
+
try {
|
|
180
|
+
assert.equal(activeTextTransform(), inner);
|
|
181
|
+
} finally {
|
|
182
|
+
restoreInner();
|
|
183
|
+
}
|
|
184
|
+
assert.equal(activeTextTransform(), outer);
|
|
185
|
+
} finally {
|
|
186
|
+
restoreOuter();
|
|
187
|
+
}
|
|
188
|
+
assert.equal(activeTextTransform(), undefined);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('a transformer notify reaches reader subscribers, and release drops the cursor', () => {
|
|
192
|
+
const registry = createTextTransformRegistry();
|
|
193
|
+
let shown = 'Hello';
|
|
194
|
+
let notify = () => {};
|
|
195
|
+
registry.register('reasoning', {
|
|
196
|
+
read: () => ({ text: shown, final: false }),
|
|
197
|
+
subscribe(listener) {
|
|
198
|
+
notify = listener;
|
|
199
|
+
return () => {};
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
let seen = 0;
|
|
203
|
+
const stop = registry.subscribe('reasoning', () => { seen += 1; });
|
|
204
|
+
const first = registry.read(reasoning('source'));
|
|
205
|
+
assert.equal(first.text, 'Hello');
|
|
206
|
+
assert.equal(first.transformRevision, 0);
|
|
207
|
+
shown = 'Hello there';
|
|
208
|
+
notify();
|
|
209
|
+
assert.equal(seen, 1);
|
|
210
|
+
const next = registry.read(reasoning('source'));
|
|
211
|
+
assert.equal(next.text, 'Hello there');
|
|
212
|
+
assert.equal(next.motionRevision, 0);
|
|
213
|
+
assert.equal(next.transformRevision, 1);
|
|
214
|
+
stop();
|
|
215
|
+
registry.release('reasoning', 'block-1');
|
|
216
|
+
shown = 'Hello there friend';
|
|
217
|
+
const restarted = registry.read(reasoning('source'));
|
|
218
|
+
assert.equal(restarted.transformRevision, 0);
|
|
219
|
+
assert.equal(restarted.motionRevision, 0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('an extending display string keeps the motion revision', () => {
|
|
223
|
+
const first = nextDisplayCursor(undefined, { text: 'Hello', final: false });
|
|
224
|
+
const second = nextDisplayCursor(first, { text: 'Hello world', final: false });
|
|
225
|
+
const same = nextDisplayCursor(second, { text: 'Hello world', final: false });
|
|
226
|
+
assert.equal(second.motionRevision, first.motionRevision);
|
|
227
|
+
assert.equal(second.transformRevision, 1);
|
|
228
|
+
assert.equal(same, second);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('a throwing transformer falls back to the original text and recovers without a remount', () => {
|
|
232
|
+
const registry = createTextTransformRegistry();
|
|
233
|
+
let broken = true;
|
|
234
|
+
let notify = () => {};
|
|
235
|
+
const warn = console.warn;
|
|
236
|
+
console.warn = () => {};
|
|
237
|
+
try {
|
|
238
|
+
registry.register('reasoning', {
|
|
239
|
+
subscribe(listener) { notify = listener; return () => {}; },
|
|
240
|
+
read(request) {
|
|
241
|
+
if (broken) throw new Error('engine down');
|
|
242
|
+
return { text: request.text.toUpperCase(), final: !request.streaming };
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const first = registry.read(reasoning('hello', true));
|
|
246
|
+
assert.equal(first.text, 'hello');
|
|
247
|
+
assert.equal(first.final, false);
|
|
248
|
+
broken = false;
|
|
249
|
+
notify();
|
|
250
|
+
const recovered = registry.read(reasoning('hello', true));
|
|
251
|
+
assert.equal(recovered.text, 'HELLO');
|
|
252
|
+
} finally {
|
|
253
|
+
console.warn = warn;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('a transformer whose subscribe throws still registers and reads', () => {
|
|
258
|
+
const registry = createTextTransformRegistry();
|
|
259
|
+
const warn = console.warn;
|
|
260
|
+
console.warn = () => {};
|
|
261
|
+
try {
|
|
262
|
+
const off = registry.register('body', {
|
|
263
|
+
subscribe() { throw new Error('no events'); },
|
|
264
|
+
read(request) { return request.text.toUpperCase(); },
|
|
265
|
+
});
|
|
266
|
+
assert.equal(registry.read({ text: 'ok', streaming: false, blockKey: 'b', kind: 'body', source: 'assistant' }).text, 'OK');
|
|
267
|
+
off();
|
|
268
|
+
assert.equal(registry.has('body'), false);
|
|
269
|
+
} finally {
|
|
270
|
+
console.warn = warn;
|
|
271
|
+
}
|
|
272
|
+
});
|
package/tests/timeline.test.ts
CHANGED
package/tests/tool-clock.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test } from '
|
|
1
|
+
import { test } from 'vitest';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { callStartTime, forgetCallClock, runningClockBase } from '../src/client/tool-activity.ts';
|
|
4
4
|
import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
package/tests/tool-diff.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test } from '
|
|
1
|
+
import { test } from 'vitest';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { callDiffHunks, DIFF_FALLBACK_TOOLS, diffTotals } from '../src/client/tool-activity.ts';
|
|
4
4
|
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from '
|
|
2
|
+
import { test } from 'vitest';
|
|
3
3
|
import type { Nodes } from 'mdast';
|
|
4
4
|
import { WORD_MOTION, WordTimeline } from '../src/client/word-timeline.js';
|
|
5
5
|
import { IncrementalMarkdownParser } from '../src/client/markdown/incremental.js';
|
package/tsdown.config.ts
CHANGED
|
@@ -40,12 +40,60 @@ const portableOutput: TsdownPlugin = {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Guard against the 0.3.7 regression class: the host entry must not ship a
|
|
45
|
+
* relative specifier that cannot resolve inside the published tarball.
|
|
46
|
+
*
|
|
47
|
+
* The specific failure was `import ... from "./skill-roots.ts"` — a path to the
|
|
48
|
+
* TypeScript source that is never emitted next to the entry, so the host died
|
|
49
|
+
* with ERR_MODULE_NOT_FOUND on every start while the market still reported the
|
|
50
|
+
* plugin as installed. A build-time assertion turns that silent publish-time
|
|
51
|
+
* mistake into a red build.
|
|
52
|
+
*/
|
|
53
|
+
const hostPortableOutput: TsdownPlugin = {
|
|
54
|
+
name: 'dsh-better-display-host-portable-output',
|
|
55
|
+
generateBundle(_options, output) {
|
|
56
|
+
for (const [fileName, chunk] of Object.entries(output)) {
|
|
57
|
+
if (chunk.type !== 'chunk') continue;
|
|
58
|
+
const offenders = [
|
|
59
|
+
// Relative specifiers that still point at TS sources.
|
|
60
|
+
...chunk.code.matchAll(/from\s*["'](\.[^"']*\.ts)["']/g),
|
|
61
|
+
// Relative specifiers reaching into node_modules-written shapes.
|
|
62
|
+
...chunk.code.matchAll(/from\s*["'](\.\.?\/[^"']*node_modules\/[^"']*)["']/g),
|
|
63
|
+
].map((match) => match[1]);
|
|
64
|
+
if (offenders.length > 0) {
|
|
65
|
+
this.error(
|
|
66
|
+
`${fileName} contains relative specifiers that will not resolve in the published package: ` +
|
|
67
|
+
`${[...new Set(offenders)].join(', ')}. Relative imports must be bundled, not externalised ` +
|
|
68
|
+
`(see the neverBundle note above).`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
43
75
|
export default bundle.map((config) => {
|
|
44
76
|
if (config.name === 'dsh-better-display') {
|
|
45
77
|
const { external: _external, ...hostConfig } = config;
|
|
46
78
|
return {
|
|
47
79
|
...hostConfig,
|
|
48
|
-
|
|
80
|
+
plugins: [
|
|
81
|
+
...(Array.isArray(hostConfig.plugins)
|
|
82
|
+
? hostConfig.plugins
|
|
83
|
+
: hostConfig.plugins === undefined
|
|
84
|
+
? []
|
|
85
|
+
: [hostConfig.plugins]),
|
|
86
|
+
hostPortableOutput,
|
|
87
|
+
],
|
|
88
|
+
// NEVER re-introduce a resolved-id predicate here. tsdown may hand this
|
|
89
|
+
// callback an already-resolved absolute path, in which case a
|
|
90
|
+
// `!specifier.startsWith('.')` test is true for EVERY module and the
|
|
91
|
+
// relative source imports get externalised instead of bundled — the
|
|
92
|
+
// host entry then ships as `import ... from "./skill-roots.ts"`, which
|
|
93
|
+
// no longer exists in the package (0.3.7 regression, ERR_MODULE_NOT_FOUND).
|
|
94
|
+
// `true` keeps the documented semantics: bare npm specifiers stay
|
|
95
|
+
// external "as written", relative imports keep being bundled.
|
|
96
|
+
deps: { ...hostConfig.deps, neverBundle: true },
|
|
49
97
|
};
|
|
50
98
|
}
|
|
51
99
|
if (config.name !== 'dsh-better-display/client') return config;
|