dsh-better-display 0.3.8 → 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 +11 -0
- 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 +48 -19
- package/docs/openapi.yaml +147 -0
- package/docs/runbooks/blank-reader.md +39 -0
- package/lib/client.js +1417 -490
- package/lib/client.js.map +1 -1
- package/lib/dsh-better-display.js +378 -144
- 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 +43 -71
- 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.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 +6 -0
- 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 +136 -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 -7
- package/lib/types/client/official-slots.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/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 +9 -2
- package/lib/types/client/tool-activity.d.ts.map +1 -1
- package/lib/types/client/tool-activity.js +42 -2
- 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 +56 -7
- 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/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 +68 -18
- package/src/client/Reader.tsx +36 -76
- package/src/client/SettingsSection.tsx +1 -7
- package/src/client/StickyLane.tsx +81 -2
- package/src/client/ToolActivity.tsx +1 -1
- package/src/client/index.tsx +8 -1
- package/src/client/live-turn.ts +100 -69
- package/src/client/motion.tsx +135 -7
- package/src/client/official-slots.tsx +23 -7
- package/src/client/projection.ts +16 -4
- package/src/client/rewind-cut.ts +340 -0
- package/src/client/text-transform.ts +265 -0
- package/src/client/timeline.ts +0 -2
- package/src/client/tool-activity.ts +49 -2
- package/src/client/turn-slots.ts +80 -0
- package/src/client/types.ts +19 -0
- package/src/dsh-better-display.ts +58 -7
- package/src/flags.ts +40 -0
- package/src/log.ts +137 -0
- package/src/metrics.ts +50 -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 +64 -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 +1 -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 +1 -1
- package/tests/reader-official-users.test.ts +11 -1
- 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 +98 -6
- package/tests/reveal.test.ts +3 -3
- 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 +1 -1
- package/tests/stream-buffer.test.ts +1 -1
- package/tests/styles.test.ts +30 -3
- package/tests/terminal-input-preview.test.ts +1 -1
- 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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { flushSync } from 'react-dom';
|
|
4
|
+
import { SlotRegistry } from '@fixture/registry';
|
|
5
|
+
import { createSlotRenderer } from '@fixture/renderer';
|
|
6
|
+
import { UserMessageNodeView } from '@fixture/user-message';
|
|
7
|
+
import { Reader } from '../../src/client/Reader.js';
|
|
8
|
+
import { createReaderStore } from '../../src/client/store.js';
|
|
9
|
+
import { OFFICIAL_SEATS, OFFICIAL_SLOTS, installOfficialSlots, officialChildren } from '../../src/client/official-slots.js';
|
|
10
|
+
import type { ReaderProps } from '../../src/client/types.js';
|
|
11
|
+
|
|
12
|
+
// Real Reader, real slot seats, and the host's user/steering renderer.
|
|
13
|
+
// Session text is the only synthetic part. Width is measured after layout.
|
|
14
|
+
|
|
15
|
+
const CASES = [
|
|
16
|
+
{ name: 'short', width: 748, axis: '', text: '升级插件' },
|
|
17
|
+
{ name: 'thirty', width: 748, axis: '', text: '测'.repeat(30) },
|
|
18
|
+
{ name: 'long', width: 748, axis: '', text: '测'.repeat(80) },
|
|
19
|
+
{ name: 'narrow', width: 480, axis: '', text: '测'.repeat(30) },
|
|
20
|
+
{ name: 'wide-axis', width: 1000, axis: '1000px', text: '测'.repeat(80) },
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
function turnOf() {
|
|
24
|
+
return {
|
|
25
|
+
turn: 1, status: 'closed' as const, start: { seq: 1, time: 1 },
|
|
26
|
+
end: { time: 2, data: { reason: { kind: 'completed' } } }, data: new Map(), steps: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function messageNode(kind: 'user' | 'steering', text: string) {
|
|
31
|
+
const turn = turnOf();
|
|
32
|
+
return {
|
|
33
|
+
key: `${kind}-key`, kind, anchorSeq: kind === 'user' ? 1 : 2, visibility: 'visible' as const,
|
|
34
|
+
location: { kind: 'turn' as const, turn },
|
|
35
|
+
data: { seq: kind === 'user' ? 1 : 2, time: 1, content: [{ type: 'text', text }] },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readerProps(text: string, renderSlot: ReaderProps['renderSlot'], renderSlotChain: ReaderProps['renderSlotChain']): ReaderProps {
|
|
40
|
+
const turn = turnOf();
|
|
41
|
+
const user = messageNode('user', text);
|
|
42
|
+
const steering = messageNode('steering', text);
|
|
43
|
+
user.location = { kind: 'turn', turn };
|
|
44
|
+
steering.location = { kind: 'turn', turn };
|
|
45
|
+
const nodes = new Map([[user.key, user], [steering.key, steering]]);
|
|
46
|
+
const chat = { order: [user.key, steering.key], nodes, timeline: { turns: new Map([[1, turn]]) } };
|
|
47
|
+
const session = {
|
|
48
|
+
running: false, openState: 'ready', hasMore: false, loadingOlder: false,
|
|
49
|
+
pendingSubmissions: [{ requestId: `pending-${text.length}`, text, time: 1, placement: 'transcript' }],
|
|
50
|
+
};
|
|
51
|
+
const store = createReaderStore().create(`session-${text.length}`);
|
|
52
|
+
store.actions.setMotion(false);
|
|
53
|
+
const source = <T,>(value: T) => ({ getSnapshot: () => value, subscribe: () => () => {} });
|
|
54
|
+
return {
|
|
55
|
+
sessionId: `session-${text.length}`,
|
|
56
|
+
useChat: (select: (snapshot: typeof chat) => unknown) => select(chat),
|
|
57
|
+
useSession: (select: (snapshot: typeof session) => unknown) => select(session),
|
|
58
|
+
useSessions: (select: (snapshot: { byId: Record<string, { cwd: string }> }) => unknown) => select({ byId: { [`session-${text.length}`]: { cwd: '/fixture' } } }),
|
|
59
|
+
useSessionStatus: (select: (snapshot: Map<string, unknown>) => unknown) => select(new Map()),
|
|
60
|
+
useStore: (select: (snapshot: ReturnType<typeof store.getSnapshot>) => unknown) => select(store.getSnapshot()),
|
|
61
|
+
actions: store.actions,
|
|
62
|
+
t: (key: string) => key,
|
|
63
|
+
loadImage: async () => ({ data: new Uint8Array(), mediaType: 'image/png' }),
|
|
64
|
+
officialImageLoader: Object.assign(async () => null, { peek: () => null }),
|
|
65
|
+
officialFileMentions: () => undefined,
|
|
66
|
+
officialPreviewFile: () => {},
|
|
67
|
+
officialHost: source({ home: '/fixture' }),
|
|
68
|
+
fillComposer: () => true,
|
|
69
|
+
openFile: () => {},
|
|
70
|
+
loadOlder: async () => {},
|
|
71
|
+
openView: () => {},
|
|
72
|
+
renderSlot,
|
|
73
|
+
renderSlotChain,
|
|
74
|
+
} as ReaderProps;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function NativeColumn({ renderSlot, kind, text }: {
|
|
78
|
+
renderSlot: ReaderProps['renderSlot'];
|
|
79
|
+
kind: 'user' | 'steering';
|
|
80
|
+
text: string;
|
|
81
|
+
}) {
|
|
82
|
+
const node = messageNode(kind, text);
|
|
83
|
+
const owner = {
|
|
84
|
+
cwd: '/fixture', openFile: () => {}, openSkill: () => {}, forkAt: () => {},
|
|
85
|
+
inspectCall: () => {}, loadImage: async () => null,
|
|
86
|
+
renderMessageImages: () => null, fileMentions: () => undefined, node,
|
|
87
|
+
};
|
|
88
|
+
return (
|
|
89
|
+
<div data-native={kind} style={{ width: '100%', maxWidth: 'var(--dsh-chat-content-width, 748px)', display: 'flex', flexDirection: 'column' }}>
|
|
90
|
+
<div style={{ minWidth: 0 }}>
|
|
91
|
+
{renderSlot(OFFICIAL_SLOTS.nodes, owner, {
|
|
92
|
+
entryKey: kind,
|
|
93
|
+
hookContext: { turnData: node.location.turn.data, disclosureReset: { getSnapshot: () => 0, subscribe: () => () => {}, set: () => {} } },
|
|
94
|
+
fallback: null,
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function boxOf(root: ParentNode | null, kind: 'user' | 'steering' | 'pending') {
|
|
102
|
+
const scope = kind === 'pending'
|
|
103
|
+
? root?.querySelector('[data-reader-pending-submission]')
|
|
104
|
+
: root?.querySelector(`[data-reader-key="${kind}-key"]`);
|
|
105
|
+
if (!scope) return null;
|
|
106
|
+
if (kind === 'pending') {
|
|
107
|
+
return [...scope.children].find(element => element instanceof HTMLElement && element.querySelector('button') === null) ?? null;
|
|
108
|
+
}
|
|
109
|
+
const slot = scope.querySelector('[data-slot]');
|
|
110
|
+
return slot?.firstElementChild?.firstElementChild?.firstElementChild ?? null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function nativeBox(frame: ParentNode, kind: 'user' | 'steering') {
|
|
114
|
+
const column = frame.querySelector(`[data-native="${kind}"]`);
|
|
115
|
+
const slot = column?.querySelector('[data-slot]');
|
|
116
|
+
return slot?.firstElementChild?.firstElementChild?.firstElementChild ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function measure(element: Element | null) {
|
|
120
|
+
if (!element) return { w: 0, h: 0, font: '', line: '' };
|
|
121
|
+
const style = getComputedStyle(element);
|
|
122
|
+
const rect = element.getBoundingClientRect();
|
|
123
|
+
return {
|
|
124
|
+
w: Math.round(rect.width * 10) / 10,
|
|
125
|
+
h: Math.round(rect.height * 10) / 10,
|
|
126
|
+
font: style.fontSize,
|
|
127
|
+
line: style.lineHeight,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function verify() {
|
|
132
|
+
const root = new Context();
|
|
133
|
+
await root.plugin(SlotRegistry);
|
|
134
|
+
let run = async () => {};
|
|
135
|
+
await root.plugin({ name: 'reader-bubble-width', inject: ['slots'], async apply(ctx: Context) {
|
|
136
|
+
const slots = ctx.slots as any;
|
|
137
|
+
slots.install(createSlotRenderer());
|
|
138
|
+
const source = <T,>(value: T) => ({ getSnapshot: () => value, subscribe: () => () => {} });
|
|
139
|
+
const binding = { key: 'session-width', ctx: new Context(), hooks: {}, keyedHooks: {}, props: { sessionId: 'session-width' } };
|
|
140
|
+
slots.installScope('session', {
|
|
141
|
+
current: source(binding), bindingSource: () => source(binding), renderArea: (_: unknown, props: { children?: unknown }) => props.children,
|
|
142
|
+
});
|
|
143
|
+
slots.installLocale({ ...source({}), bind: () => (key: string) => key });
|
|
144
|
+
const HostUser = (props: Record<string, unknown>) => (
|
|
145
|
+
<UserMessageNodeView
|
|
146
|
+
{...props as never}
|
|
147
|
+
t={(props.t as ((key: string) => string) | undefined) ?? ((key: string) => key)}
|
|
148
|
+
openFile={(props.openFile as (() => void) | undefined) ?? (() => {})}
|
|
149
|
+
openSkill={(props.openSkill as (() => void) | undefined) ?? (() => {})}
|
|
150
|
+
renderMessageImages={(props.renderMessageImages as (() => null) | undefined) ?? (() => null)}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
slots.register({ name: 'root', children: {
|
|
154
|
+
'conversation.chat.node': { kind: 'keyed', scope: 'session', inject: { hooks: {
|
|
155
|
+
turnData: (_: unknown, context: { turnData?: unknown }) => () => context.turnData,
|
|
156
|
+
disclosure: (_: unknown, context: { disclosureReset?: { getSnapshot(): number } }) => () => context.disclosureReset,
|
|
157
|
+
} } },
|
|
158
|
+
'conversation.message.images': { kind: 'single', scope: 'session' },
|
|
159
|
+
'fixture.reader': { kind: 'single', scope: 'session' },
|
|
160
|
+
} }, ({ SessionProvider, renderSlot }: any) => (
|
|
161
|
+
<SessionProvider>
|
|
162
|
+
{renderSlot('fixture.reader', {})}
|
|
163
|
+
{CASES.map(item => (
|
|
164
|
+
<section key={`native-${item.name}`} data-native-case={item.name} style={{ width: item.width, ['--dsh-chat-content-width' as string]: item.axis || undefined }}>
|
|
165
|
+
<NativeColumn renderSlot={renderSlot} kind="user" text={item.text} />
|
|
166
|
+
<NativeColumn renderSlot={renderSlot} kind="steering" text={item.text} />
|
|
167
|
+
</section>
|
|
168
|
+
))}
|
|
169
|
+
</SessionProvider>
|
|
170
|
+
));
|
|
171
|
+
slots.register({ name: 'fixture.reader', children: {
|
|
172
|
+
...officialChildren(slots),
|
|
173
|
+
'dsh-better-display.block': { kind: 'chain', scope: 'session' },
|
|
174
|
+
} }, ({ renderSlot, renderSlotChain }: any) => (
|
|
175
|
+
<>
|
|
176
|
+
{CASES.map(item => (
|
|
177
|
+
<section key={item.name} data-case={item.name} style={{ width: item.width, ['--dsh-chat-content-width' as string]: item.axis || undefined }}>
|
|
178
|
+
<div data-reader-mount>
|
|
179
|
+
<Reader {...readerProps(item.text, renderSlot, renderSlotChain)} />
|
|
180
|
+
</div>
|
|
181
|
+
</section>
|
|
182
|
+
))}
|
|
183
|
+
</>
|
|
184
|
+
));
|
|
185
|
+
slots.register({ name: 'conversation.chat.node', key: 'user', locale: 'chat' }, HostUser);
|
|
186
|
+
slots.register({ name: 'conversation.chat.node', key: 'steering', locale: 'chat' }, HostUser);
|
|
187
|
+
const stopMirror = installOfficialSlots(ctx);
|
|
188
|
+
run = async () => {
|
|
189
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
190
|
+
const container = document.getElementById('app')!;
|
|
191
|
+
const reactRoot = createRoot(container);
|
|
192
|
+
flushSync(() => reactRoot.render(slots.renderSlot('root', {})));
|
|
193
|
+
for (const reader of container.querySelectorAll<HTMLElement>('[data-dsh-better-display]')) reader.style.padding = '0';
|
|
194
|
+
await new Promise(resolve => requestAnimationFrame(() => resolve(undefined)));
|
|
195
|
+
const readerSlot = container.querySelector('[data-reader-key="user-key"] [data-slot]')?.getAttribute('data-slot') ?? '';
|
|
196
|
+
const nativeSlot = container.querySelector('[data-native="user"] [data-slot]')?.getAttribute('data-slot') ?? '';
|
|
197
|
+
const rows = CASES.map(item => {
|
|
198
|
+
const frame = container.querySelector(`[data-case="${item.name}"]`);
|
|
199
|
+
const nativeFrame = container.querySelector(`[data-native-case="${item.name}"]`);
|
|
200
|
+
const mount = frame?.querySelector('[data-reader-mount]') ?? null;
|
|
201
|
+
const user = measure(boxOf(mount, 'user'));
|
|
202
|
+
const steering = measure(boxOf(mount, 'steering'));
|
|
203
|
+
const pending = measure(boxOf(mount, 'pending'));
|
|
204
|
+
const nativeUser = measure(nativeFrame ? nativeBox(nativeFrame, 'user') : null);
|
|
205
|
+
const nativeSteering = measure(nativeFrame ? nativeBox(nativeFrame, 'steering') : null);
|
|
206
|
+
return {
|
|
207
|
+
name: item.name,
|
|
208
|
+
user, steering, pending, nativeUser, nativeSteering,
|
|
209
|
+
readerHtml: mount?.querySelector('[data-reader-key="user-key"]')?.innerHTML.slice(0, 400) ?? '',
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
stopMirror();
|
|
213
|
+
Object.assign(window, { readerBubbleResult: { readerSlot, nativeSlot, expectedReaderSlot: OFFICIAL_SEATS.nodes, expectedNativeSlot: OFFICIAL_SLOTS.nodes, rows } });
|
|
214
|
+
};
|
|
215
|
+
} });
|
|
216
|
+
await run();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
verify().catch(error => {
|
|
220
|
+
console.error(error);
|
|
221
|
+
Object.assign(window, { readerBubbleResult: { error: `${error instanceof Error ? error.stack : String(error)}\n${document.getElementById('app')?.innerHTML.slice(0, 2000)}` } });
|
|
222
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { test } from 'vitest';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { apply } from '../src/dsh-better-display.js';
|
|
9
|
+
import { defaultFlags, loadFlags } from '../src/flags.js';
|
|
10
|
+
|
|
11
|
+
test('readerEnabled defaults to true', () => {
|
|
12
|
+
assert.equal(defaultFlags.readerEnabled, true);
|
|
13
|
+
const file = JSON.parse(readFileSync(fileURLToPath(new URL('../flags.json', import.meta.url)), 'utf8')) as { readerEnabled: boolean };
|
|
14
|
+
assert.equal(file.readerEnabled, true);
|
|
15
|
+
assert.equal(loadFlags().readerEnabled, true);
|
|
16
|
+
assert.equal(loadFlags({ readerEnabled: false }).readerEnabled, false);
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), 'bd-flags-'));
|
|
18
|
+
try {
|
|
19
|
+
const broken = join(dir, 'flags.json');
|
|
20
|
+
writeFileSync(broken, '{ "readerEnabled": "yes" }');
|
|
21
|
+
assert.equal(loadFlags(broken).readerEnabled, true);
|
|
22
|
+
assert.equal(loadFlags(join(dir, 'missing.json')).readerEnabled, true);
|
|
23
|
+
} finally {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('apply skips route registration when readerEnabled is false', () => {
|
|
29
|
+
let registered = 0;
|
|
30
|
+
let effects = 0;
|
|
31
|
+
apply({
|
|
32
|
+
webServer: {
|
|
33
|
+
register() {
|
|
34
|
+
registered += 1;
|
|
35
|
+
return () => undefined;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
effect(run: () => void) {
|
|
39
|
+
effects += 1;
|
|
40
|
+
run();
|
|
41
|
+
},
|
|
42
|
+
} as never, { flags: { readerEnabled: false } });
|
|
43
|
+
assert.equal(registered, 0);
|
|
44
|
+
assert.equal(effects, 0);
|
|
45
|
+
assert.doesNotThrow(() => apply({} as never, { flags: { readerEnabled: true } }));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('check-dead-flags fails when a flags.json key is unused under src', () => {
|
|
49
|
+
const script = fileURLToPath(new URL('../scripts/check-dead-flags.mjs', import.meta.url));
|
|
50
|
+
const ok = spawnSync(process.execPath, [script], { encoding: 'utf8' });
|
|
51
|
+
assert.equal(ok.status, 0, ok.stderr || ok.stdout);
|
|
52
|
+
const dir = mkdtempSync(join(tmpdir(), 'bd-dead-flags-'));
|
|
53
|
+
try {
|
|
54
|
+
const flags = join(dir, 'flags.json');
|
|
55
|
+
writeFileSync(flags, JSON.stringify({ readerEnabled: true, unusedFlag: false }));
|
|
56
|
+
const bad = spawnSync(process.execPath, [script], {
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
env: { ...process.env, FLAGS_JSON: flags },
|
|
59
|
+
});
|
|
60
|
+
assert.notEqual(bad.status, 0);
|
|
61
|
+
assert.match(bad.stderr, /unusedFlag/);
|
|
62
|
+
assert.doesNotMatch(bad.stderr, /readerEnabled/);
|
|
63
|
+
} finally {
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -1,28 +1,52 @@
|
|
|
1
|
-
import test from '
|
|
1
|
+
import { test } from 'vitest';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { collapseRows, containsNewUser, flowRows, FOLD_TIMING, retiringKeys } from '../src/client/fold-choreography.js';
|
|
4
4
|
import { presentLiveTurn } from '../src/client/live-turn.js';
|
|
5
5
|
import type { LiveStep } from '../src/client/live-turn.js';
|
|
6
6
|
const reasoning = (n: number): LiveStep => ({ kind: 'reasoning', key: `r${n}`, nodeKey: `n${n}`, start: 0, blocks: [], step: n });
|
|
7
7
|
const body: LiveStep = { kind: 'body', key: 'b1', nodeKey: 'n1', start: 1, blocks: [], step: 1 };
|
|
8
|
+
const tool: LiveStep = {
|
|
9
|
+
kind: 'tool',
|
|
10
|
+
key: 't1',
|
|
11
|
+
entry: {
|
|
12
|
+
kind: 'tool',
|
|
13
|
+
key: 't1',
|
|
14
|
+
callId: 'c1',
|
|
15
|
+
step: 0,
|
|
16
|
+
order: 0,
|
|
17
|
+
draft: { kind: 'tool-call', callId: 'c1', name: 'read', argsRaw: '{"path":"Reader.tsx"}' },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
8
20
|
const open = { status: 'open' } as const;
|
|
9
21
|
test('first fold retains every visible step key and admits no incoming reasoning', () => {
|
|
10
|
-
const before = presentLiveTurn([reasoning(1),
|
|
11
|
-
const after = presentLiveTurn([reasoning(1),
|
|
12
|
-
assert.deepEqual(retiringKeys(before, after, {}), ['r1', '
|
|
22
|
+
const before = presentLiveTurn([reasoning(1), tool], open);
|
|
23
|
+
const after = presentLiveTurn([reasoning(1), tool, reasoning(2)], open);
|
|
24
|
+
assert.deepEqual(retiringKeys(before, after, {}), ['r1', 't1']);
|
|
13
25
|
const rows = collapseRows(before, after);
|
|
14
|
-
assert.deepEqual(rows.filter(x => x.kind === 'step').map(x => x.key), ['r1', '
|
|
26
|
+
assert.deepEqual(rows.filter(x => x.kind === 'step').map(x => x.key), ['r1', 't1']);
|
|
15
27
|
assert.equal(rows[0].kind, 'summary');
|
|
16
|
-
assert.equal(rows[0].kind === 'summary' && rows[0].item.summary, '思考×0 ·
|
|
28
|
+
assert.equal(rows[0].kind === 'summary' && rows[0].item.summary, '思考×0 · 工具×0');
|
|
17
29
|
assert.equal(new Set(flowRows(after).map(row => row.key)).size, flowRows(after).length);
|
|
18
30
|
});
|
|
19
31
|
test('repeat fold retires only displayed open rows, retaining the same summary identity', () => {
|
|
20
|
-
const before = presentLiveTurn([reasoning(1),
|
|
21
|
-
const after = presentLiveTurn([reasoning(1),
|
|
32
|
+
const before = presentLiveTurn([reasoning(1), tool, reasoning(2)], open);
|
|
33
|
+
const after = presentLiveTurn([reasoning(1), tool, reasoning(2), reasoning(3)], open);
|
|
22
34
|
assert.deepEqual(retiringKeys(before, after, {}), ['r2']);
|
|
23
35
|
assert.equal(collapseRows(before, after)[0].key, flowRows(before)[0].key);
|
|
24
36
|
assert.deepEqual(retiringKeys(before, after, { 'live-fold:r1': true }), []);
|
|
25
37
|
});
|
|
38
|
+
test('narration never enters a fold cell, so choreography never retires it', () => {
|
|
39
|
+
const before = presentLiveTurn([reasoning(1)], open);
|
|
40
|
+
const withNarration = presentLiveTurn([reasoning(1), body], open);
|
|
41
|
+
assert.deepEqual(retiringKeys(before, withNarration, {}), []);
|
|
42
|
+
assert.deepEqual(flowRows(withNarration).map(row => row.key), ['r1', 'b1']);
|
|
43
|
+
|
|
44
|
+
const after = presentLiveTurn([reasoning(1), body, tool, reasoning(2)], open);
|
|
45
|
+
assert.equal(after.some(item => item.kind === 'fold' && item.steps.some(step => step.kind === 'body')), false);
|
|
46
|
+
// The prior reasoning run folds (r1 retires), but narration itself never does.
|
|
47
|
+
assert.deepEqual(retiringKeys(withNarration, after, {}), ['r1']);
|
|
48
|
+
assert.ok(!retiringKeys(withNarration, after, {}).includes('b1'));
|
|
49
|
+
});
|
|
26
50
|
test('user admission bypasses buffered choreography and preserves source ordering', () => {
|
|
27
51
|
const before = presentLiveTurn([reasoning(1)], open);
|
|
28
52
|
const user: LiveStep = { kind: 'user', key: 'u2', nodeKey: 'u2' };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test } from '
|
|
1
|
+
import { test } from 'vitest';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, resolve } from 'node:path';
|
|
@@ -57,17 +57,22 @@ test('fold slider maps 0/1/2 and migrates older persist flags', () => {
|
|
|
57
57
|
assert.equal(processOnlyFromIntensity(2), true);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test('autoFold false keeps every step open; autoFold true
|
|
60
|
+
test('autoFold false keeps every step open; autoFold true folds only thinking/tool runs', () => {
|
|
61
61
|
const steps = [reasoning(1), body, tool, reasoning(2)];
|
|
62
62
|
const none = presentLiveTurn(steps, open, false);
|
|
63
63
|
assert.equal(none.some(item => item.kind === 'fold'), false);
|
|
64
64
|
assert.deepEqual(none.map(item => item.key), ['r1', 'b1', 't1', 'r2']);
|
|
65
65
|
|
|
66
|
+
// Narration stays open; each prior reasoning/tool run folds in its own place.
|
|
66
67
|
const standard = presentLiveTurn(steps, open, true);
|
|
67
|
-
assert.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
assert.
|
|
68
|
+
assert.deepEqual(standard.map(item => item.kind), ['fold', 'open', 'fold', 'open']);
|
|
69
|
+
assert.deepEqual(standard.filter(item => item.kind === 'open').map(item => item.key), ['b1', 'r2']);
|
|
70
|
+
const folds = standard.filter(item => item.kind === 'fold');
|
|
71
|
+
assert.equal(folds.length, 2);
|
|
72
|
+
if (folds[0]?.kind !== 'fold' || folds[1]?.kind !== 'fold') throw new Error('expected standard folds');
|
|
73
|
+
assert.deepEqual(folds[0].steps.map(step => step.key), ['r1']);
|
|
74
|
+
assert.deepEqual(folds[1].steps.map(step => step.key), ['t1']);
|
|
75
|
+
assert.equal(standard.some(item => item.kind === 'fold' && item.steps.some(step => step.kind === 'body')), false);
|
|
71
76
|
});
|
|
72
77
|
|
|
73
78
|
test('root reader store persists glass off and standard fold on dsh.reader.v1', () => {
|
|
@@ -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)), '..');
|