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
package/src/reveal.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { spawn, type SpawnOptions } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
+
import { posix, win32 } from 'node:path';
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
import { isAbsoluteWorkspacePath } from './workspace-path.js';
|
|
7
|
+
|
|
8
|
+
export async function revealRoots(ctx: Context, sessionId: string | undefined): Promise<readonly string[]> {
|
|
9
|
+
if (sessionId !== undefined) {
|
|
10
|
+
const sessions = ctx.get?.('sessions') as { get: (id: string) => { header: { cwd?: string } } | undefined } | undefined;
|
|
11
|
+
const cwd = sessions?.get(sessionId)?.header.cwd;
|
|
12
|
+
if (cwd) return [cwd];
|
|
13
|
+
const query = ctx.get?.('sessionQuery') as {
|
|
14
|
+
observeSession: (id: string, options: { projectionMode: 'none' }) => Promise<{ header: { cwd?: string } } & Disposable>;
|
|
15
|
+
} | undefined;
|
|
16
|
+
if (query) {
|
|
17
|
+
using observation = await query.observeSession(sessionId, { projectionMode: 'none' });
|
|
18
|
+
if (observation.header.cwd) return [observation.header.cwd];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const registry = ctx.get?.('workspaceRegistry') as {
|
|
22
|
+
list: () => readonly { path: string; sessionIds: readonly string[] }[];
|
|
23
|
+
} | undefined;
|
|
24
|
+
return (registry?.list() ?? [])
|
|
25
|
+
.filter(workspace => sessionId === undefined || workspace.sessionIds.includes(sessionId))
|
|
26
|
+
.map(workspace => workspace.path);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveRevealPath(path: string, roots: readonly string[], exists = existsSync): string {
|
|
30
|
+
if (isAbsoluteWorkspacePath(path)) return path;
|
|
31
|
+
// Drive-relative and root-relative Windows paths do not identify a workspace.
|
|
32
|
+
if (/^[A-Za-z]:|^\\/.test(path)) throw new Error('Path is not workspace-relative');
|
|
33
|
+
const matches = new Set<string>();
|
|
34
|
+
for (const root of roots) {
|
|
35
|
+
if (!isAbsoluteWorkspacePath(root)) continue;
|
|
36
|
+
const paths = /^[A-Za-z]:[/\\]|^\\\\/.test(root) ? win32 : posix;
|
|
37
|
+
const candidate = paths.resolve(root, path);
|
|
38
|
+
if (exists(candidate)) matches.add(candidate);
|
|
39
|
+
}
|
|
40
|
+
if (matches.size !== 1) throw new Error(matches.size === 0 ? 'Workspace path not found' : 'Workspace path is ambiguous');
|
|
41
|
+
return [...matches][0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function revealPath(
|
|
45
|
+
path: string,
|
|
46
|
+
platform = process.platform,
|
|
47
|
+
spawnProcess: (command: string, args: string[], options: SpawnOptions) => unknown = spawn,
|
|
48
|
+
): void {
|
|
49
|
+
const options = { detached: true, stdio: 'ignore' } satisfies SpawnOptions;
|
|
50
|
+
if (platform === 'darwin') {
|
|
51
|
+
spawnProcess('open', ['-R', path], options);
|
|
52
|
+
} else if (platform === 'win32') {
|
|
53
|
+
spawnProcess('explorer.exe', [`/select,"${path}"`], { ...options, windowsVerbatimArguments: true });
|
|
54
|
+
} else {
|
|
55
|
+
spawnProcess('xdg-open', [path], options);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createRevealHandler({ roots, reveal = revealPath }: {
|
|
60
|
+
roots: (sessionId: string | undefined) => Promise<readonly string[]>;
|
|
61
|
+
reveal?: (path: string) => void;
|
|
62
|
+
}): (req: IncomingMessage, res: ServerResponse) => void {
|
|
63
|
+
return (req, res) => {
|
|
64
|
+
if (req.method !== 'POST') {
|
|
65
|
+
res.statusCode = 405;
|
|
66
|
+
res.end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let body = '';
|
|
70
|
+
req.on('data', chunk => { body += chunk; });
|
|
71
|
+
req.on('end', async () => {
|
|
72
|
+
res.setHeader('Content-Type', 'application/json');
|
|
73
|
+
try {
|
|
74
|
+
const data: unknown = JSON.parse(body);
|
|
75
|
+
const path = typeof data === 'object' && data !== null && 'path' in data && typeof data.path === 'string'
|
|
76
|
+
? data.path.trim() : '';
|
|
77
|
+
if (!path) throw new Error('Empty path');
|
|
78
|
+
const sessionId = typeof data === 'object' && data !== null && 'sessionId' in data && typeof data.sessionId === 'string'
|
|
79
|
+
? data.sessionId : undefined;
|
|
80
|
+
const targetPath = resolveRevealPath(path, isAbsoluteWorkspacePath(path) ? [] : await roots(sessionId));
|
|
81
|
+
reveal(targetPath);
|
|
82
|
+
res.statusCode = 200;
|
|
83
|
+
res.end(JSON.stringify({ ok: true }));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
res.statusCode = 400;
|
|
86
|
+
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useSyncExternalStore } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import { Reader } from '../src/client/Reader.js';
|
|
5
|
+
import { createReaderStore } from '../src/client/store.js';
|
|
6
|
+
import type { ReaderProps } from '../src/client/types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Real Reader + real root/session stores; only conversation data is synthetic.
|
|
10
|
+
*
|
|
11
|
+
* This fixture isolates the "process report narration" bug: an assistant step
|
|
12
|
+
* that carries a long non-reasoning text block (正文) between thinking steps used
|
|
13
|
+
* to be classified as process content and folded away. Contract under test:
|
|
14
|
+
* body text is always visible; folds may only hold reasoning / tool / other
|
|
15
|
+
* process records.
|
|
16
|
+
*
|
|
17
|
+
* Turn layout:
|
|
18
|
+
* 1 closed/completed : step0 = reasoning + 正文A + tool-call ; step1 = reasoning + 最终回答B
|
|
19
|
+
* 2 open : step0 = reasoning + 进行中汇报正文C ; step1 = reasoning (latest)
|
|
20
|
+
* 3 closed/error : step0 = reasoning + 失败轮正文D
|
|
21
|
+
* 4 closed/interrupted: step0 = reasoning + 中断轮正文E
|
|
22
|
+
*/
|
|
23
|
+
const BODY_A = '过程汇报正文A';
|
|
24
|
+
const REASON_A = '历史思考:先查简报素材';
|
|
25
|
+
const FINAL_B = '最终回答B';
|
|
26
|
+
const REASON_B = '第二次思考:整理结论';
|
|
27
|
+
const TOOL_SUMMARY = '读取 简报素材.txt';
|
|
28
|
+
const BODY_C = '进行中汇报正文C';
|
|
29
|
+
const REASON_C0 = '第一轮思考';
|
|
30
|
+
const REASON_C1 = '第二轮思考';
|
|
31
|
+
const BODY_D = '失败轮正文D';
|
|
32
|
+
const REASON_D = '报错前的思考';
|
|
33
|
+
const BODY_E = '中断轮正文E';
|
|
34
|
+
const REASON_E = '中断前的思考';
|
|
35
|
+
// Turn 5 exercises the segmented-report case: two adjacent narration paragraphs
|
|
36
|
+
// (with folded reasoning between them) plus a final answer, all with long text.
|
|
37
|
+
const BODY_F = '第一段汇报:已完成需求梳理,确认改造点集中在投影层与阅读渲染层。这一步整理的是已经核实的事实,不包括猜测。\n\n第二段汇报:接下来核对折叠链边界,并验证正文在收起状态下仍然可见,同时保持思维链与工具行可以展开。';
|
|
38
|
+
const REASON_F0 = '汇报前的思考';
|
|
39
|
+
const BODY_G = '补充汇报:折叠只保留思维链与工具调用,正文块保持原位。';
|
|
40
|
+
const REASON_G = '补充说明前的思考';
|
|
41
|
+
const REASON_H = '收尾思考';
|
|
42
|
+
const FINAL_H = '最终回答H';
|
|
43
|
+
const TOOL_CALL_ID = 'call-read-1';
|
|
44
|
+
const TOOL_ARGS = '{"path":"/fixture/工作区/简报素材.txt"}';
|
|
45
|
+
|
|
46
|
+
const store = createReaderStore();
|
|
47
|
+
const prefs = store.create();
|
|
48
|
+
const session = store.create('commentary-fixture');
|
|
49
|
+
|
|
50
|
+
type Turn = {
|
|
51
|
+
turn: number; status: 'open' | 'closed';
|
|
52
|
+
start: { seq: number; time: number };
|
|
53
|
+
end?: { time: number; data: { reason: { kind: string } } };
|
|
54
|
+
data: Map<string, any>;
|
|
55
|
+
steps: any[];
|
|
56
|
+
};
|
|
57
|
+
const nodes = new Map<string, any>();
|
|
58
|
+
const turns = new Map<number, Turn>();
|
|
59
|
+
|
|
60
|
+
function newTurn(turn: number, status: 'open' | 'closed', seq: number, reason?: string): Turn {
|
|
61
|
+
const value: Turn = {
|
|
62
|
+
turn, status, start: { seq, time: 1 },
|
|
63
|
+
end: status === 'closed' ? { time: 1001, data: { reason: { kind: reason ?? 'completed' } } } : undefined,
|
|
64
|
+
data: new Map<string, any>(),
|
|
65
|
+
steps: [],
|
|
66
|
+
};
|
|
67
|
+
turns.set(turn, value);
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function addStep(turnNumber: number, step: number, seq: number, status: string, blocks: any[]): any {
|
|
72
|
+
const turn = turns.get(turnNumber)!;
|
|
73
|
+
const data = { turn: turnNumber, step, status, time: 1, blocks };
|
|
74
|
+
const location = { step, start: { seq }, data: new Map([['assistant-step', data]]) };
|
|
75
|
+
turn.steps.push(location);
|
|
76
|
+
nodes.set(`turn-${turnNumber}-step-${step}`, {
|
|
77
|
+
key: `turn-${turnNumber}-step-${step}`, kind: 'assistant-step', visibility: 'visible',
|
|
78
|
+
anchorSeq: seq, location: { kind: 'step', turn, step: location }, data,
|
|
79
|
+
});
|
|
80
|
+
return location;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addToolCallNode(turnNumber: number, key: string, seq: number, location: any, callId: string, argsRaw: string): void {
|
|
84
|
+
const turn = turns.get(turnNumber)!;
|
|
85
|
+
const root = {
|
|
86
|
+
kind: 'tool-result', seq, time: 1, callId,
|
|
87
|
+
call: { name: 'read', argsRaw }, callTime: 1,
|
|
88
|
+
content: [{ kind: 'text', text: '简报素材的摘录内容' }], isError: false, subCalls: [],
|
|
89
|
+
};
|
|
90
|
+
nodes.set(key, {
|
|
91
|
+
key, kind: 'tool-call', visibility: 'visible', anchorSeq: seq,
|
|
92
|
+
location: { kind: 'step', turn, step: location }, data: { root },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- turn 1: completed; narration sits between reasoning and the final answer.
|
|
97
|
+
const turn1 = newTurn(1, 'closed', 100);
|
|
98
|
+
turn1.data.set('turn-tail', { closing: { step: 1 } });
|
|
99
|
+
const t1s0 = addStep(1, 0, 101, 'settled', [
|
|
100
|
+
{ kind: 'reasoning', text: REASON_A },
|
|
101
|
+
{ kind: 'text', text: BODY_A },
|
|
102
|
+
{ kind: 'tool-call', callId: TOOL_CALL_ID, name: 'read', argsRaw: TOOL_ARGS },
|
|
103
|
+
]);
|
|
104
|
+
addToolCallNode(1, 'turn-1-tool-call-1', 102, t1s0, TOOL_CALL_ID, TOOL_ARGS);
|
|
105
|
+
addStep(1, 1, 103, 'settled', [
|
|
106
|
+
{ kind: 'reasoning', text: REASON_B },
|
|
107
|
+
{ kind: 'text', text: FINAL_B },
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// --- turn 2: open; second reasoning arrives after a process report.
|
|
111
|
+
const turn2 = newTurn(2, 'open', 200);
|
|
112
|
+
addStep(2, 0, 201, 'settled', [
|
|
113
|
+
{ kind: 'reasoning', text: REASON_C0 },
|
|
114
|
+
{ kind: 'text', text: BODY_C },
|
|
115
|
+
]);
|
|
116
|
+
addStep(2, 1, 202, 'running', [
|
|
117
|
+
{ kind: 'reasoning', text: REASON_C1 },
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// --- turn 3: failed turn; nothing may fold.
|
|
121
|
+
newTurn(3, 'closed', 300, 'error');
|
|
122
|
+
addStep(3, 0, 301, 'settled', [
|
|
123
|
+
{ kind: 'reasoning', text: REASON_D },
|
|
124
|
+
{ kind: 'text', text: BODY_D },
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
// --- turn 4: interrupted turn; nothing may fold.
|
|
128
|
+
newTurn(4, 'closed', 400, 'interrupted');
|
|
129
|
+
addStep(4, 0, 401, 'settled', [
|
|
130
|
+
{ kind: 'reasoning', text: REASON_E },
|
|
131
|
+
{ kind: 'text', text: BODY_E },
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// --- turn 5: completed turn whose report is split into two narration paragraphs
|
|
135
|
+
// with folded reasoning between them; the rail must bridge them after collapsing.
|
|
136
|
+
const turn5 = newTurn(5, 'closed', 500);
|
|
137
|
+
turn5.data.set('turn-tail', { closing: { step: 2 } });
|
|
138
|
+
addStep(5, 0, 501, 'settled', [
|
|
139
|
+
{ kind: 'reasoning', text: REASON_F0 },
|
|
140
|
+
{ kind: 'text', text: BODY_F },
|
|
141
|
+
]);
|
|
142
|
+
addStep(5, 1, 502, 'settled', [
|
|
143
|
+
{ kind: 'reasoning', text: REASON_G },
|
|
144
|
+
{ kind: 'text', text: BODY_G },
|
|
145
|
+
]);
|
|
146
|
+
addStep(5, 2, 503, 'settled', [
|
|
147
|
+
{ kind: 'reasoning', text: REASON_H },
|
|
148
|
+
{ kind: 'text', text: FINAL_H },
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const chat = {
|
|
152
|
+
order: [
|
|
153
|
+
'turn-1-step-0', 'turn-1-tool-call-1', 'turn-1-step-1',
|
|
154
|
+
'turn-2-step-0', 'turn-2-step-1',
|
|
155
|
+
'turn-3-step-0', 'turn-4-step-0',
|
|
156
|
+
'turn-5-step-0', 'turn-5-step-1', 'turn-5-step-2',
|
|
157
|
+
],
|
|
158
|
+
nodes,
|
|
159
|
+
timeline: { turns },
|
|
160
|
+
} as unknown as ChatSnapshot;
|
|
161
|
+
const sessionSnapshot = { running: true, openState: 'ready', pendingSubmissions: [], hasMore: false, loadingOlder: false };
|
|
162
|
+
const sessionsSnapshot = { byId: { 'commentary-fixture': { cwd: '/fixture' } } };
|
|
163
|
+
const pending = new Map();
|
|
164
|
+
const host = { home: '/fixture' };
|
|
165
|
+
const useReaderStore: ReaderProps['useStore'] = selector => selector(useSyncExternalStore(session.subscribe, session.getSnapshot));
|
|
166
|
+
const props = {
|
|
167
|
+
sessionId: 'commentary-fixture', useChat: selector => selector(chat),
|
|
168
|
+
useSession: selector => selector(sessionSnapshot), useSessions: selector => selector(sessionsSnapshot),
|
|
169
|
+
useSessionStatus: selector => selector(new Map([['commentary-fixture', { running: true, pendingInteraction: pending.get('commentary-fixture'), completionUnread: false }]])),
|
|
170
|
+
useStore: useReaderStore, actions: session.actions, openPrefs: prefs,
|
|
171
|
+
t: key => key, renderSlot: (_name: string, _owner: any, options: any) => options?.fallback ?? null,
|
|
172
|
+
renderSlotChain: (_name: string, _owner: any, options: any) => options?.fallback ?? null,
|
|
173
|
+
loadImage: async () => ({ data: new Uint8Array(), mediaType: 'image/png' }),
|
|
174
|
+
officialImageLoader: Object.assign(async () => null, { peek: () => null }),
|
|
175
|
+
officialFileMentions: () => undefined, officialPreviewFile: () => {},
|
|
176
|
+
officialHost: { getSnapshot: () => host, subscribe: () => () => {} },
|
|
177
|
+
fillComposer: () => true, openFile: () => {}, loadOlder: async () => {}, openView: () => {},
|
|
178
|
+
} as unknown as ReaderProps;
|
|
179
|
+
|
|
180
|
+
Object.assign(window, {
|
|
181
|
+
commentaryFixture: {
|
|
182
|
+
// Exercise the same root preference the Settings panel uses.
|
|
183
|
+
preference: (value: boolean) => prefs.actions.setAutoFold(value),
|
|
184
|
+
state: () => session.getSnapshot(),
|
|
185
|
+
text: { BODY_A, REASON_A, FINAL_B, REASON_B, TOOL_SUMMARY, BODY_C, REASON_C0, REASON_C1, BODY_D, REASON_D, BODY_E, REASON_E, BODY_F, REASON_F0, BODY_G, REASON_G, REASON_H, FINAL_H, TOOL_CALL_ID },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
createRoot(document.getElementById('app')!).render(
|
|
190
|
+
<div data-conversation-scroll style={{ height: '100vh', overflow: 'auto' }}><Reader {...props} /></div>,
|
|
191
|
+
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
|
-
import { test } from '
|
|
4
|
+
import { test } from 'vitest';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
7
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
package/tests/cloud-gate.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 type { TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
4
4
|
import { basename, createProducedFileMentions, dirname, getTurnDeliverables, showDeliverablesRow } from '../src/client/deliverables.ts';
|
package/tests/entry.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from '
|
|
2
|
+
import { test } from 'vitest';
|
|
3
3
|
import { ReaderEntryPolicy, readerEntryRequested } from '../src/client/entry-policy.js';
|
|
4
4
|
|
|
5
5
|
test('fresh sessions default to reading, without requiring a special URL', () => {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { StickyLane, usePublishedHeight } from '../../src/client/StickyLane.js';
|
|
4
|
+
|
|
5
|
+
function Measured({ id, property, height }: { id: string; property: string; height: number }) {
|
|
6
|
+
const ref = usePublishedHeight(property);
|
|
7
|
+
return <div ref={ref} id={id} style={{ height }}>{id}</div>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function SummaryReader({ id, height, show }: { id: string; height: number; show: boolean }) {
|
|
11
|
+
return <div data-dsh-better-display={id} id={`root-${id}`}>
|
|
12
|
+
<section data-reader-turn="1" id={`turn-${id}`}>
|
|
13
|
+
{show && <Measured id={`row-${id}`} property="--reader-summary-height" height={height} />}
|
|
14
|
+
</section>
|
|
15
|
+
</div>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
const [height, setHeight] = useState(52);
|
|
20
|
+
const [show, setShow] = useState(true);
|
|
21
|
+
return <>
|
|
22
|
+
<button id="grow" type="button" onClick={() => setHeight(86)}>grow</button>
|
|
23
|
+
<button id="drop" type="button" onClick={() => setShow(false)}>drop</button>
|
|
24
|
+
<SummaryReader id="a" height={height} show={show} />
|
|
25
|
+
<SummaryReader id="b" height={52} show />
|
|
26
|
+
<div data-dsh-better-display="short" id="root-short">
|
|
27
|
+
<section data-reader-turn="1" id="turn-short">
|
|
28
|
+
<Measured id="row-short" property="--reader-status-height" height={38} />
|
|
29
|
+
</section>
|
|
30
|
+
</div>
|
|
31
|
+
<div data-dsh-better-display="toolbar" id="root-toolbar">
|
|
32
|
+
<StickyLane kind="toolbar" className="toolbarProbe">
|
|
33
|
+
<button id="toolbar-button" type="button" style={{ height: 12, padding: 0, border: 0 }}>开</button>
|
|
34
|
+
</StickyLane>
|
|
35
|
+
</div>
|
|
36
|
+
<div data-dsh-better-display="status" id="root-status">
|
|
37
|
+
<section data-reader-turn="1" id="turn-status">
|
|
38
|
+
<StickyLane kind="status" className="statusProbe">
|
|
39
|
+
<span style={{ display: 'block', height: 12 }}>用时</span>
|
|
40
|
+
</StickyLane>
|
|
41
|
+
</section>
|
|
42
|
+
</div>
|
|
43
|
+
</>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
createRoot(document.getElementById('app')!).render(<App />);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import * as Primitives from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { createRoot } from 'react-dom/client';
|
|
5
|
+
import { flushSync } from 'react-dom';
|
|
6
|
+
import { SlotRegistry } from '@fixture/registry';
|
|
7
|
+
import { createSlotRenderer } from '@fixture/renderer';
|
|
8
|
+
import { Reader } from '../../src/client/Reader.js';
|
|
9
|
+
import { createReaderStore } from '../../src/client/store.js';
|
|
10
|
+
import { installOfficialSlots, officialChildren, OFFICIAL_SEATS } from '../../src/client/official-slots.js';
|
|
11
|
+
import type { ReaderProps } from '../../src/client/types.js';
|
|
12
|
+
|
|
13
|
+
// Real Reader, registry, scope binding and slot renderer. Only session data is synthetic.
|
|
14
|
+
function equal(actual: unknown, expected: unknown) {
|
|
15
|
+
if (!Object.is(actual, expected)) throw new Error(`Expected ${String(expected)}, got ${String(actual)}`);
|
|
16
|
+
}
|
|
17
|
+
const assert = {
|
|
18
|
+
equal,
|
|
19
|
+
ok(value: unknown) { equal(Boolean(value), true); },
|
|
20
|
+
deepEqual(actual: unknown, expected: unknown) { equal(JSON.stringify(actual), JSON.stringify(expected)); },
|
|
21
|
+
};
|
|
22
|
+
async function verify() {
|
|
23
|
+
const root = new Context();
|
|
24
|
+
await root.plugin(SlotRegistry);
|
|
25
|
+
let run = async () => {};
|
|
26
|
+
await root.plugin({ name: 'reader-user-test', inject: ['slots'], async apply(ctx: Context) {
|
|
27
|
+
const slots = ctx.slots as any;
|
|
28
|
+
slots.install(createSlotRenderer());
|
|
29
|
+
const source = <T,>(value: T) => ({ getSnapshot: () => value, subscribe: () => () => {} });
|
|
30
|
+
const binding = { key: 'session-users', ctx: new Context(), hooks: {}, keyedHooks: {}, props: { sessionId: 'session-users' } };
|
|
31
|
+
slots.installScope('session', {
|
|
32
|
+
current: source(binding), bindingSource: () => source(binding), renderArea: (_: unknown, props: any) => props.children,
|
|
33
|
+
});
|
|
34
|
+
slots.installLocale({ ...source({}), bind: () => (key: string) => key });
|
|
35
|
+
const turn = { turn: 1, status: 'closed', start: { seq: 1, time: 1 },
|
|
36
|
+
end: { time: 2, data: { reason: { kind: 'completed' } } }, data: new Map(), steps: [] };
|
|
37
|
+
const nodes = new Map(['user', 'steering', 'hidden'].map((kind, index) => {
|
|
38
|
+
const key = `${kind}-key`;
|
|
39
|
+
return [key, { key, kind: kind === 'hidden' ? 'user' : kind, anchorSeq: index + 1,
|
|
40
|
+
visibility: kind === 'hidden' ? 'hidden' : 'visible', location: { kind: 'turn', turn },
|
|
41
|
+
data: { seq: index + 1, time: 1, content: [{ type: 'text', text: `${kind} content` }] } }];
|
|
42
|
+
}));
|
|
43
|
+
const chat = { order: [...nodes.keys()], nodes, timeline: { turns: new Map([[1, turn]]) } };
|
|
44
|
+
const session = { running: false, openState: 'ready', pendingSubmissions: [], hasMore: false, loadingOlder: false };
|
|
45
|
+
const store = createReaderStore().create('session-users');
|
|
46
|
+
const props = {
|
|
47
|
+
sessionId: 'session-users', useChat: (select: any) => select(chat),
|
|
48
|
+
useSession: (select: any) => select(session), useSessions: (select: any) => select({ byId: { 'session-users': { cwd: '/fixture' } } }),
|
|
49
|
+
useSessionStatus: (select: any) => select(new Map()), useStore: (select: any) => select(store.getSnapshot()), actions: store.actions,
|
|
50
|
+
t: (key: string) => key, loadImage: async () => ({ data: new Uint8Array(), mediaType: 'image/png' }),
|
|
51
|
+
officialImageLoader: Object.assign(async () => null, { peek: () => null }),
|
|
52
|
+
officialFileMentions: () => undefined, officialPreviewFile: () => {},
|
|
53
|
+
officialHost: source({ home: '/fixture' }), fillComposer: () => true,
|
|
54
|
+
openFile: () => {}, loadOlder: async () => {}, openView: () => {},
|
|
55
|
+
};
|
|
56
|
+
slots.register({ name: 'root', children: {
|
|
57
|
+
'conversation.chat.node': { kind: 'keyed', scope: 'session', inject: { hooks: {
|
|
58
|
+
turnData: (_: unknown, context: any) => () => context.turnData,
|
|
59
|
+
disclosure: (_: unknown, context: any) => () => context.disclosureReset,
|
|
60
|
+
} } },
|
|
61
|
+
'fixture.reader': { kind: 'single', scope: 'session' },
|
|
62
|
+
} }, ({ SessionProvider, renderSlot }: any) => <SessionProvider>{renderSlot('fixture.reader', {})}</SessionProvider>);
|
|
63
|
+
slots.register({ name: 'fixture.reader', children: { ...officialChildren(slots), 'dsh-better-display.block': { kind: 'chain', scope: 'session' } } }, ({ renderSlot, renderSlotChain }: any) =>
|
|
64
|
+
<Reader {...{ ...props, renderSlot, renderSlotChain } as ReaderProps} />);
|
|
65
|
+
const stopMirror = installOfficialSlots(ctx);
|
|
66
|
+
run = async () => {
|
|
67
|
+
const container = document.getElementById('app')!;
|
|
68
|
+
const reactRoot = createRoot(container);
|
|
69
|
+
const render = () => {
|
|
70
|
+
flushSync(() => reactRoot.render(slots.renderSlot('root', {})));
|
|
71
|
+
return container.innerHTML;
|
|
72
|
+
};
|
|
73
|
+
const count = (html: string, text: string) => html.split(text).length - 1;
|
|
74
|
+
const assertSeats = (html: string) => {
|
|
75
|
+
for (const kind of ['user', 'steering']) {
|
|
76
|
+
assert.equal(count(html, `data-reader-key="${kind}-key"`), 1);
|
|
77
|
+
assert.equal(count(html, `data-chat-anchor-key="${kind}-key"`), 1);
|
|
78
|
+
assert.equal(count(html, `data-chat-flow-key="${kind}-key"`), 1);
|
|
79
|
+
assert.equal(count(html, `data-chat-node-key="${kind}-key"`), 1);
|
|
80
|
+
assert.equal(count(html, `data-chat-flow-kind="${kind}"`), 1);
|
|
81
|
+
}
|
|
82
|
+
assert.equal(count(html, 'data-reader-anchor="true"'), 2);
|
|
83
|
+
assert.ok(!html.includes('hidden content'));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// The real renderSlot returns an outlet element for an unoccupied key.
|
|
87
|
+
let html = render();
|
|
88
|
+
assertSeats(html);
|
|
89
|
+
assert.equal(count(html, 'user content'), 1);
|
|
90
|
+
assert.equal(count(html, 'steering content'), 1);
|
|
91
|
+
assert.ok(html.includes('补充消息'));
|
|
92
|
+
const seen: any[] = [];
|
|
93
|
+
const official = ({ node, sessionId, injectedSession, useTurnData, useDisclosure }: any) => {
|
|
94
|
+
assert.equal(sessionId, 'session-users');
|
|
95
|
+
assert.equal(injectedSession, 'session-users');
|
|
96
|
+
assert.equal(useTurnData(), turn.data);
|
|
97
|
+
assert.equal(useDisclosure().getSnapshot(), 0);
|
|
98
|
+
assert.equal(node, nodes.get(node.key));
|
|
99
|
+
seen.push(node.kind);
|
|
100
|
+
return <div data-official-user={node.kind}>{node.data.content[0].text}<button>Official recall</button></div>;
|
|
101
|
+
};
|
|
102
|
+
const removeUser = slots.register({ name: 'conversation.chat.node', key: 'user',
|
|
103
|
+
inject: (sessionId: string) => ({ injectedSession: sessionId }), locale: 'chat' }, official);
|
|
104
|
+
const removeSteering = slots.register({ name: 'conversation.chat.node', key: 'steering',
|
|
105
|
+
inject: (sessionId: string) => ({ injectedSession: sessionId }), locale: 'chat' }, official);
|
|
106
|
+
const removeAssistant = slots.register({ name: 'conversation.chat.node', key: 'assistant-step' }, () => <span>Duplicate assistant</span>);
|
|
107
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
108
|
+
assert.deepEqual(slots.entriesOfSlot(OFFICIAL_SEATS.nodes).map((entry: any) => entry.options.key).sort(), ['steering', 'user']);
|
|
109
|
+
html = render();
|
|
110
|
+
assertSeats(html);
|
|
111
|
+
assert.equal(count(html, 'Official recall'), 2);
|
|
112
|
+
assert.equal(count(html, 'user content'), 1);
|
|
113
|
+
assert.equal(count(html, 'steering content'), 1);
|
|
114
|
+
assert.deepEqual(seen, ['user', 'steering']);
|
|
115
|
+
|
|
116
|
+
// A winner intentionally returning null must keep its row hidden.
|
|
117
|
+
const removeWinner = slots.register({ name: 'conversation.chat.node', key: 'user', priority: -1 }, () => null);
|
|
118
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
119
|
+
html = render();
|
|
120
|
+
assertSeats(html);
|
|
121
|
+
assert.equal(count(html, 'user content'), 0);
|
|
122
|
+
assert.equal(count(html, 'Official recall'), 1);
|
|
123
|
+
removeWinner();
|
|
124
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
125
|
+
assert.equal(count(render(), 'Official recall'), 2);
|
|
126
|
+
removeUser(); removeSteering(); removeAssistant();
|
|
127
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
128
|
+
html = render();
|
|
129
|
+
assertSeats(html);
|
|
130
|
+
assert.equal(count(html, 'user content'), 1);
|
|
131
|
+
assert.equal(count(html, 'steering content'), 1);
|
|
132
|
+
assert.equal(count(html, 'Official recall'), 0);
|
|
133
|
+
|
|
134
|
+
// A priority below the official assistant renderer is a replacement.
|
|
135
|
+
// Reader keeps the reasoning card and hands only the body blocks over.
|
|
136
|
+
const assistant = {
|
|
137
|
+
key: 'assistant-key', kind: 'assistant-step', anchorSeq: 4, visibility: 'visible',
|
|
138
|
+
location: { kind: 'turn', turn },
|
|
139
|
+
data: {
|
|
140
|
+
status: 'settled', turn: 1, step: 0, time: 3,
|
|
141
|
+
blocks: [
|
|
142
|
+
{ kind: 'reasoning', text: 'think privately' },
|
|
143
|
+
{ kind: 'text', text: '<div id="vcp-root">card face</div>' },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
// Reader memoizes on the order array and the nodes map. A real host
|
|
148
|
+
// publishes new references; this fixture has to do the same.
|
|
149
|
+
chat.nodes = new Map(chat.nodes);
|
|
150
|
+
chat.nodes.set('assistant-key', assistant);
|
|
151
|
+
chat.order = [...chat.order, 'assistant-key'];
|
|
152
|
+
store.actions.setExpanded('turn:1:closed:completed', true);
|
|
153
|
+
html = render();
|
|
154
|
+
assert.equal(count(html, 'data-foreign-assistant'), 0);
|
|
155
|
+
assert.ok(html.includes('vcp-root'));
|
|
156
|
+
assert.ok(html.includes('think privately'));
|
|
157
|
+
const calls: Array<{ groupPart: unknown; kinds: string[]; text: string }> = [];
|
|
158
|
+
const Foreign = (props: { groupPart?: unknown; node?: { data?: { blocks?: Array<{ kind: string; text?: string }> } } }) => {
|
|
159
|
+
const blocks = props.node?.data?.blocks ?? [];
|
|
160
|
+
calls.push({
|
|
161
|
+
groupPart: props.groupPart,
|
|
162
|
+
kinds: blocks.map(block => block.kind),
|
|
163
|
+
text: blocks.map(block => block.text ?? '').join('\n'),
|
|
164
|
+
});
|
|
165
|
+
const body = blocks.filter(block => block.kind === 'text').map(block => block.text ?? '').join('\n');
|
|
166
|
+
return <div data-foreign-assistant>{body}</div>;
|
|
167
|
+
};
|
|
168
|
+
const removeOfficialAssistant = slots.register({ name: 'conversation.chat.node', key: 'assistant-step' }, () => <span>Official assistant</span>);
|
|
169
|
+
const removeForeign = slots.register({ name: 'conversation.chat.node', key: 'assistant-step', priority: -10 }, Foreign);
|
|
170
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
171
|
+
assert.ok(slots.entriesOfSlot(OFFICIAL_SEATS.nodes).some((entry: { options: { key?: string; priority?: number } }) => entry.options.key === 'assistant-step' && entry.options.priority === -10));
|
|
172
|
+
html = render();
|
|
173
|
+
const answer = container.querySelector('[data-reader-body]');
|
|
174
|
+
assert.ok(answer);
|
|
175
|
+
assert.equal(answer!.querySelectorAll('[data-foreign-assistant]').length, 1);
|
|
176
|
+
assert.ok(answer!.textContent?.includes('<div id="vcp-root">card face</div>'));
|
|
177
|
+
assert.equal(answer!.querySelectorAll('pre').length, 0);
|
|
178
|
+
assert.equal(count(html, 'Official assistant'), 0);
|
|
179
|
+
assert.ok(calls.some(call => call.groupPart === 'response' && call.kinds.join(',') === 'text' && call.text.includes('vcp-root')));
|
|
180
|
+
assert.ok(calls.every(call => !call.kinds.includes('reasoning')));
|
|
181
|
+
const reasoning = container.querySelector('[data-reader-text-kind="reasoning"]');
|
|
182
|
+
assert.ok(reasoning);
|
|
183
|
+
assert.ok(reasoning!.textContent?.includes('think privately'));
|
|
184
|
+
assert.equal(reasoning!.querySelectorAll('[data-foreign-assistant]').length, 0);
|
|
185
|
+
removeForeign();
|
|
186
|
+
removeOfficialAssistant();
|
|
187
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
188
|
+
html = render();
|
|
189
|
+
assert.equal(count(html, 'data-foreign-assistant'), 0);
|
|
190
|
+
assert.ok(html.includes('vcp-root'));
|
|
191
|
+
assert.ok(html.includes('think privately'));
|
|
192
|
+
chat.nodes = new Map(chat.nodes);
|
|
193
|
+
chat.nodes.delete('assistant-key');
|
|
194
|
+
chat.order = chat.order.filter((key: string) => key !== 'assistant-key');
|
|
195
|
+
store.actions.setExpanded('turn:1:closed:completed', false);
|
|
196
|
+
|
|
197
|
+
const module = Reflect.get(window, 'easyrewriteModule');
|
|
198
|
+
if (module) {
|
|
199
|
+
const plugin = module.factory((name: string) => {
|
|
200
|
+
if (name === 'react') return React;
|
|
201
|
+
if (name === '@deepseek-ai/dsh-client-ui-primitives') return Primitives;
|
|
202
|
+
throw new Error(`Unexpected easyrewrite dependency: ${name}`);
|
|
203
|
+
});
|
|
204
|
+
const disposers: Array<() => void> = [];
|
|
205
|
+
plugin.apply({
|
|
206
|
+
effect: (effect: () => () => void) => disposers.push(effect()),
|
|
207
|
+
slots: {
|
|
208
|
+
inject: (key: string, effect: () => () => void) => key === 'conversation.chat.node' ? effect() : () => {},
|
|
209
|
+
register: (options: object, component: unknown) => slots.register(options, component),
|
|
210
|
+
},
|
|
211
|
+
sessions: { list: source({ current: 'session-users' }), scope: () => ({ get: () => undefined }) },
|
|
212
|
+
workspaces: {},
|
|
213
|
+
});
|
|
214
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
215
|
+
html = render();
|
|
216
|
+
assertSeats(html);
|
|
217
|
+
assert.equal(count(html, 'data-dsh-easyrewrite="user"'), 1);
|
|
218
|
+
assert.equal(count(html, 'data-dsh-easyrewrite="recall-key"'), 1);
|
|
219
|
+
assert.equal(count(html, 'user content'), 1);
|
|
220
|
+
assert.equal(count(html, 'steering content'), 1);
|
|
221
|
+
const recall = container.querySelector<HTMLButtonElement>('[data-dsh-easyrewrite="recall-key"]');
|
|
222
|
+
assert.ok(recall);
|
|
223
|
+
flushSync(() => recall!.click());
|
|
224
|
+
assert.equal(container.querySelectorAll('[data-dsh-easyrewrite="confirm-capsule"]').length, 1);
|
|
225
|
+
for (const dispose of disposers) dispose();
|
|
226
|
+
}
|
|
227
|
+
stopMirror();
|
|
228
|
+
reactRoot.unmount();
|
|
229
|
+
Object.assign(window, { readerUserResult: 'passed' });
|
|
230
|
+
};
|
|
231
|
+
} });
|
|
232
|
+
await run();
|
|
233
|
+
}
|
|
234
|
+
verify().catch(error => { console.error(error); Object.assign(window, { readerUserResult: `${error.stack}\n${document.getElementById('app')?.innerHTML}` }); });
|