dsh-better-display 0.1.1
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 +51 -0
- package/DESIGN.md +72 -0
- package/LICENSE +24 -0
- package/README.en.md +41 -0
- package/README.md +41 -0
- package/THIRD_PARTY_NOTICES.md +65 -0
- package/cordis.patch.yml +5 -0
- package/cordis.yml +4 -0
- package/dshx.yml +5 -0
- package/lib/client.js +49923 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-better-display.js +256 -0
- package/lib/types/client/Blocks.d.ts +45 -0
- package/lib/types/client/Blocks.d.ts.map +1 -0
- package/lib/types/client/Blocks.js +136 -0
- package/lib/types/client/Blocks.js.map +1 -0
- package/lib/types/client/ChoreographedFlow.d.ts +22 -0
- package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
- package/lib/types/client/ChoreographedFlow.js +170 -0
- package/lib/types/client/ChoreographedFlow.js.map +1 -0
- package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
- package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
- package/lib/types/client/ClosedProcessSummary.js +15 -0
- package/lib/types/client/ClosedProcessSummary.js.map +1 -0
- package/lib/types/client/LiveFold.d.ts +10 -0
- package/lib/types/client/LiveFold.d.ts.map +1 -0
- package/lib/types/client/LiveFold.js +39 -0
- package/lib/types/client/LiveFold.js.map +1 -0
- package/lib/types/client/McpAppFrame.d.ts +32 -0
- package/lib/types/client/McpAppFrame.d.ts.map +1 -0
- package/lib/types/client/McpAppFrame.js +193 -0
- package/lib/types/client/McpAppFrame.js.map +1 -0
- package/lib/types/client/Reader.d.ts +3 -0
- package/lib/types/client/Reader.d.ts.map +1 -0
- package/lib/types/client/Reader.js +537 -0
- package/lib/types/client/Reader.js.map +1 -0
- package/lib/types/client/ReasoningCard.d.ts +11 -0
- package/lib/types/client/ReasoningCard.d.ts.map +1 -0
- package/lib/types/client/ReasoningCard.js +301 -0
- package/lib/types/client/ReasoningCard.js.map +1 -0
- package/lib/types/client/StickyLane.d.ts +8 -0
- package/lib/types/client/StickyLane.d.ts.map +1 -0
- package/lib/types/client/StickyLane.js +28 -0
- package/lib/types/client/StickyLane.js.map +1 -0
- package/lib/types/client/TimelineRail.d.ts +10 -0
- package/lib/types/client/TimelineRail.d.ts.map +1 -0
- package/lib/types/client/TimelineRail.js +168 -0
- package/lib/types/client/TimelineRail.js.map +1 -0
- package/lib/types/client/ToolActivity.d.ts +17 -0
- package/lib/types/client/ToolActivity.d.ts.map +1 -0
- package/lib/types/client/ToolActivity.js +192 -0
- package/lib/types/client/ToolActivity.js.map +1 -0
- package/lib/types/client/TurnMetrics.d.ts +11 -0
- package/lib/types/client/TurnMetrics.d.ts.map +1 -0
- package/lib/types/client/TurnMetrics.js +44 -0
- package/lib/types/client/TurnMetrics.js.map +1 -0
- package/lib/types/client/WaitingStatus.d.ts +6 -0
- package/lib/types/client/WaitingStatus.d.ts.map +1 -0
- package/lib/types/client/WaitingStatus.js +19 -0
- package/lib/types/client/WaitingStatus.js.map +1 -0
- package/lib/types/client/conversation-scroll.d.ts +19 -0
- package/lib/types/client/conversation-scroll.d.ts.map +1 -0
- package/lib/types/client/conversation-scroll.js +25 -0
- package/lib/types/client/conversation-scroll.js.map +1 -0
- package/lib/types/client/deliverables.d.ts +24 -0
- package/lib/types/client/deliverables.d.ts.map +1 -0
- package/lib/types/client/deliverables.js +104 -0
- package/lib/types/client/deliverables.js.map +1 -0
- package/lib/types/client/entry-policy.d.ts +10 -0
- package/lib/types/client/entry-policy.d.ts.map +1 -0
- package/lib/types/client/entry-policy.js +24 -0
- package/lib/types/client/entry-policy.js.map +1 -0
- package/lib/types/client/entry.d.ts +4 -0
- package/lib/types/client/entry.d.ts.map +1 -0
- package/lib/types/client/entry.js +34 -0
- package/lib/types/client/entry.js.map +1 -0
- package/lib/types/client/fold-choreography.d.ts +28 -0
- package/lib/types/client/fold-choreography.d.ts.map +1 -0
- package/lib/types/client/fold-choreography.js +31 -0
- package/lib/types/client/fold-choreography.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +153 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/live-turn.d.ts +58 -0
- package/lib/types/client/live-turn.d.ts.map +1 -0
- package/lib/types/client/live-turn.js +144 -0
- package/lib/types/client/live-turn.js.map +1 -0
- package/lib/types/client/markdown/MarkdownText.d.ts +39 -0
- package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
- package/lib/types/client/markdown/MarkdownText.js +171 -0
- package/lib/types/client/markdown/MarkdownText.js.map +1 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
- package/lib/types/client/markdown/incremental.d.ts +69 -0
- package/lib/types/client/markdown/incremental.d.ts.map +1 -0
- package/lib/types/client/markdown/incremental.js +108 -0
- package/lib/types/client/markdown/incremental.js.map +1 -0
- package/lib/types/client/markdown/katex.d.ts +27 -0
- package/lib/types/client/markdown/katex.d.ts.map +1 -0
- package/lib/types/client/markdown/katex.js +88 -0
- package/lib/types/client/markdown/katex.js.map +1 -0
- package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
- package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
- package/lib/types/client/markdown/mathCompatibility.js +293 -0
- package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
- package/lib/types/client/markdown/parse.d.ts +25 -0
- package/lib/types/client/markdown/parse.d.ts.map +1 -0
- package/lib/types/client/markdown/parse.js +42 -0
- package/lib/types/client/markdown/parse.js.map +1 -0
- package/lib/types/client/markdown/render.d.ts +119 -0
- package/lib/types/client/markdown/render.d.ts.map +1 -0
- package/lib/types/client/markdown/render.js +435 -0
- package/lib/types/client/markdown/render.js.map +1 -0
- package/lib/types/client/mcp-app.d.ts +50 -0
- package/lib/types/client/mcp-app.d.ts.map +1 -0
- package/lib/types/client/mcp-app.js +328 -0
- package/lib/types/client/mcp-app.js.map +1 -0
- package/lib/types/client/message-chrome.d.ts +19 -0
- package/lib/types/client/message-chrome.d.ts.map +1 -0
- package/lib/types/client/message-chrome.js +42 -0
- package/lib/types/client/message-chrome.js.map +1 -0
- package/lib/types/client/motion.d.ts +38 -0
- package/lib/types/client/motion.d.ts.map +1 -0
- package/lib/types/client/motion.js +367 -0
- package/lib/types/client/motion.js.map +1 -0
- package/lib/types/client/native/ContextBody.d.ts +130 -0
- package/lib/types/client/native/ContextBody.d.ts.map +1 -0
- package/lib/types/client/native/ContextBody.js +398 -0
- package/lib/types/client/native/ContextBody.js.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts +26 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.js +27 -0
- package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
- package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
- package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
- package/lib/types/client/native/ReferenceIcon.js +16 -0
- package/lib/types/client/native/ReferenceIcon.js.map +1 -0
- package/lib/types/client/native/tool-call-model.d.ts +58 -0
- package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
- package/lib/types/client/native/tool-call-model.js +219 -0
- package/lib/types/client/native/tool-call-model.js.map +1 -0
- package/lib/types/client/open-file.d.ts +24 -0
- package/lib/types/client/primitive-labels.d.ts +10 -0
- package/lib/types/client/primitive-labels.d.ts.map +1 -0
- package/lib/types/client/primitive-labels.js +37 -0
- package/lib/types/client/primitive-labels.js.map +1 -0
- package/lib/types/client/projection.d.ts +41 -0
- package/lib/types/client/projection.d.ts.map +1 -0
- package/lib/types/client/projection.js +112 -0
- package/lib/types/client/projection.js.map +1 -0
- package/lib/types/client/reasoning-follow.d.ts +6 -0
- package/lib/types/client/reasoning-follow.d.ts.map +1 -0
- package/lib/types/client/reasoning-follow.js +12 -0
- package/lib/types/client/reasoning-follow.js.map +1 -0
- package/lib/types/client/settings-copy.d.ts +6 -0
- package/lib/types/client/settings-slots.d.ts +16 -0
- package/lib/types/client/store.d.ts +17 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/client/store.js +13 -0
- package/lib/types/client/store.js.map +1 -0
- package/lib/types/client/stream-buffer.d.ts +29 -0
- package/lib/types/client/stream-buffer.d.ts.map +1 -0
- package/lib/types/client/stream-buffer.js +113 -0
- package/lib/types/client/stream-buffer.js.map +1 -0
- package/lib/types/client/streaming.d.ts +18 -0
- package/lib/types/client/streaming.d.ts.map +1 -0
- package/lib/types/client/streaming.js +77 -0
- package/lib/types/client/streaming.js.map +1 -0
- package/lib/types/client/timeline.d.ts +26 -0
- package/lib/types/client/timeline.d.ts.map +1 -0
- package/lib/types/client/timeline.js +48 -0
- package/lib/types/client/timeline.js.map +1 -0
- package/lib/types/client/tool-activity.d.ts +52 -0
- package/lib/types/client/tool-activity.d.ts.map +1 -0
- package/lib/types/client/tool-activity.js +178 -0
- package/lib/types/client/tool-activity.js.map +1 -0
- package/lib/types/client/types.d.ts +66 -0
- package/lib/types/client/types.d.ts.map +1 -0
- package/lib/types/client/types.js +2 -0
- package/lib/types/client/types.js.map +1 -0
- package/lib/types/client/waiting-clock.d.ts +17 -0
- package/lib/types/client/waiting-clock.d.ts.map +1 -0
- package/lib/types/client/waiting-clock.js +24 -0
- package/lib/types/client/waiting-clock.js.map +1 -0
- package/lib/types/client/word-motion.d.ts +16 -0
- package/lib/types/client/word-motion.d.ts.map +1 -0
- package/lib/types/client/word-motion.js +89 -0
- package/lib/types/client/word-motion.js.map +1 -0
- package/lib/types/client/word-timeline.d.ts +28 -0
- package/lib/types/client/word-timeline.d.ts.map +1 -0
- package/lib/types/client/word-timeline.js +100 -0
- package/lib/types/client/word-timeline.js.map +1 -0
- package/lib/types/dsh-better-display.d.ts +17 -0
- package/lib/types/dsh-better-display.d.ts.map +1 -0
- package/lib/types/dsh-better-display.js +53 -0
- package/lib/types/dsh-better-display.js.map +1 -0
- package/package.json +121 -0
- package/scripts/build-motion-fixture.mjs +26 -0
- package/scripts/link-harness-dependencies.mjs +51 -0
- package/skills/generative-mcpapps/SKILL.md +47 -0
- package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
- package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
- package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
- package/src/client/Blocks.tsx +205 -0
- package/src/client/ChoreographedFlow.tsx +182 -0
- package/src/client/ClosedProcessSummary.tsx +19 -0
- package/src/client/LiveFold.tsx +43 -0
- package/src/client/McpAppFrame.module.css +210 -0
- package/src/client/McpAppFrame.tsx +317 -0
- package/src/client/Reader.module.css +329 -0
- package/src/client/Reader.tsx +814 -0
- package/src/client/ReasoningCard.tsx +285 -0
- package/src/client/SettingsSection.module.css +147 -0
- package/src/client/SettingsSection.tsx +117 -0
- package/src/client/StickyLane.tsx +24 -0
- package/src/client/TimelineRail.module.css +170 -0
- package/src/client/TimelineRail.tsx +262 -0
- package/src/client/ToolActivity.tsx +214 -0
- package/src/client/TurnMetrics.module.css +139 -0
- package/src/client/TurnMetrics.tsx +160 -0
- package/src/client/WaitingStatus.tsx +22 -0
- package/src/client/conversation-scroll.ts +28 -0
- package/src/client/deliverables.ts +122 -0
- package/src/client/entry-policy.ts +20 -0
- package/src/client/entry.tsx +40 -0
- package/src/client/fold-choreography.ts +38 -0
- package/src/client/index.tsx +198 -0
- package/src/client/live-turn.ts +155 -0
- package/src/client/markdown/MarkdownText.module.css +304 -0
- package/src/client/markdown/MarkdownText.tsx +187 -0
- package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
- package/src/client/markdown/incremental.ts +131 -0
- package/src/client/markdown/katex.tsx +91 -0
- package/src/client/markdown/mathCompatibility.ts +350 -0
- package/src/client/markdown/parse.ts +45 -0
- package/src/client/markdown/render.tsx +630 -0
- package/src/client/markdown/upstream.json +14 -0
- package/src/client/mcp-app.ts +323 -0
- package/src/client/message-chrome.ts +47 -0
- package/src/client/motion.tsx +338 -0
- package/src/client/native/ContextBody.module.css +161 -0
- package/src/client/native/ContextBody.tsx +591 -0
- package/src/client/native/ContextInjectionRow.module.css +62 -0
- package/src/client/native/ContextInjectionRow.tsx +72 -0
- package/src/client/native/ReferenceIcon.tsx +35 -0
- package/src/client/native/tool-call-model.ts +248 -0
- package/src/client/native/upstream.json +41 -0
- package/src/client/open-file.ts +94 -0
- package/src/client/pending-submission.ts +49 -0
- package/src/client/primitive-labels.ts +53 -0
- package/src/client/projection.ts +118 -0
- package/src/client/reasoning-follow.ts +12 -0
- package/src/client/settings-copy.ts +53 -0
- package/src/client/settings-slots.ts +20 -0
- package/src/client/settings.tsx +73 -0
- package/src/client/skill-status.ts +78 -0
- package/src/client/store.ts +30 -0
- package/src/client/stream-buffer.ts +100 -0
- package/src/client/streaming.tsx +62 -0
- package/src/client/style.d.ts +4 -0
- package/src/client/timeline.ts +77 -0
- package/src/client/tool-activity.ts +152 -0
- package/src/client/types.ts +66 -0
- package/src/client/waiting-clock.ts +25 -0
- package/src/client/word-motion.tsx +91 -0
- package/src/client/word-timeline.ts +96 -0
- package/src/dsh-better-display.ts +108 -0
- package/src/skill-roots.ts +138 -0
- package/src/skill-status.ts +81 -0
- package/tests/browser-closed-gap.tsx +20 -0
- package/tests/browser-motion.tsx +65 -0
- package/tests/browser-sticky-lanes.tsx +22 -0
- package/tests/browser-waiting-clock.tsx +14 -0
- package/tests/chat-flow-hook.test.ts +66 -0
- package/tests/cloud-gate.test.ts +34 -0
- package/tests/conversation-scroll.test.ts +47 -0
- package/tests/deliverables.test.ts +128 -0
- package/tests/entry.test.ts +42 -0
- package/tests/fixtures/maid-composer-hide.html +31 -0
- package/tests/fold-choreography.test.ts +38 -0
- package/tests/history-word-regression.test.ts +16 -0
- package/tests/live-turn.test.ts +218 -0
- package/tests/mcp-apps.test.ts +157 -0
- package/tests/message-chrome.test.ts +29 -0
- package/tests/open-file.test.ts +103 -0
- package/tests/pending-submission.test.ts +45 -0
- package/tests/projection.test.ts +184 -0
- package/tests/reasoning-follow.test.ts +34 -0
- package/tests/settings-copy.test.ts +63 -0
- package/tests/skill-status.test.ts +137 -0
- package/tests/stock-install.test.ts +50 -0
- package/tests/stream-buffer.test.ts +97 -0
- package/tests/timeline.test.ts +59 -0
- package/tests/waiting-clock.test.ts +24 -0
- package/tests/word-timeline.test.ts +136 -0
- package/tsconfig.json +11 -0
- package/tsdown.config.ts +49 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
GENERATIVE_MCPAPPS_SKILL,
|
|
8
|
+
skillListIncludes,
|
|
9
|
+
skillRootEntryMatches,
|
|
10
|
+
type HostSkillStatus,
|
|
11
|
+
type SkillRootInfo,
|
|
12
|
+
} from './skill-status.js';
|
|
13
|
+
|
|
14
|
+
export function expandHomePath(path: string, home = homedir()): string {
|
|
15
|
+
if (path === '~') return home;
|
|
16
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return join(home, path.slice(2));
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `$DSH_HOME` or `~/.dsh`, matching official `resolveDshHome`. */
|
|
21
|
+
export function resolveDshHome(env: Record<string, string | undefined> = process.env, home = homedir()): string {
|
|
22
|
+
const fromEnv = env.DSH_HOME;
|
|
23
|
+
const selected = fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : join(home, '.dsh');
|
|
24
|
+
return resolve(expandHomePath(selected, home));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `$DSH_AGENTS_HOME` or `~/.agents`. */
|
|
28
|
+
export function resolveAgentsHome(env: Record<string, string | undefined> = process.env, home = homedir()): string {
|
|
29
|
+
const fromEnv = env.DSH_AGENTS_HOME;
|
|
30
|
+
const selected = fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : join(home, '.agents');
|
|
31
|
+
return resolve(expandHomePath(selected, home));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function userSkillRoots(
|
|
35
|
+
env: Record<string, string | undefined> = process.env,
|
|
36
|
+
home = homedir(),
|
|
37
|
+
): readonly SkillRootInfo[] {
|
|
38
|
+
return [
|
|
39
|
+
{ source: 'user-dsh', path: join(resolveDshHome(env, home), 'skills') },
|
|
40
|
+
{ source: 'user-agents', path: join(resolveAgentsHome(env, home), 'skills') },
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function projectSkillRoots(projectRoot: string): readonly SkillRootInfo[] {
|
|
45
|
+
return [
|
|
46
|
+
{ source: 'project-dsh', path: join(projectRoot, '.dsh', 'skills') },
|
|
47
|
+
{ source: 'project-agents', path: join(projectRoot, '.agents', 'skills') },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function findProjectRoot(cwd: string): Promise<string> {
|
|
52
|
+
let current = resolve(cwd);
|
|
53
|
+
while (true) {
|
|
54
|
+
if (existsSync(join(current, '.git'))) return current;
|
|
55
|
+
const parent = dirname(current);
|
|
56
|
+
if (parent === current) return resolve(cwd);
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function rootHasGenerativeMcpapps(rootPath: string): Promise<boolean> {
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = await readdir(rootPath, { withFileTypes: true });
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (entry.name === '.system') continue;
|
|
70
|
+
if (!skillRootEntryMatches(entry.name)) continue;
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
try {
|
|
73
|
+
await access(join(rootPath, entry.name, 'SKILL.md'));
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (entry.isFile()) return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolve the plugin-shipped pack. This path is never treated as a skill root. */
|
|
85
|
+
export function pluginSkillPackPath(moduleUrl = import.meta.url): string | undefined {
|
|
86
|
+
const here = dirname(fileURLToPath(moduleUrl));
|
|
87
|
+
const candidates = [
|
|
88
|
+
join(here, '../skills', GENERATIVE_MCPAPPS_SKILL),
|
|
89
|
+
join(here, '../../skills', GENERATIVE_MCPAPPS_SKILL),
|
|
90
|
+
join(here, 'skills', GENERATIVE_MCPAPPS_SKILL),
|
|
91
|
+
];
|
|
92
|
+
return candidates.find(path => existsSync(join(path, 'SKILL.md')));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function scanGenerativeMcpappsStatus(options: {
|
|
96
|
+
cwd?: string;
|
|
97
|
+
env?: Record<string, string | undefined>;
|
|
98
|
+
home?: string;
|
|
99
|
+
packPath?: string;
|
|
100
|
+
listSkills?: () => Promise<readonly { readonly name?: string }[] | undefined>;
|
|
101
|
+
} = {}): Promise<HostSkillStatus> {
|
|
102
|
+
const env = options.env ?? process.env;
|
|
103
|
+
const home = options.home ?? homedir();
|
|
104
|
+
const roots: SkillRootInfo[] = [...userSkillRoots(env, home)];
|
|
105
|
+
const cwd = typeof options.cwd === 'string' && options.cwd.trim().length > 0
|
|
106
|
+
? (isAbsolute(options.cwd) ? options.cwd : resolve(options.cwd))
|
|
107
|
+
: undefined;
|
|
108
|
+
if (cwd !== undefined) roots.unshift(...projectSkillRoots(await findProjectRoot(cwd)));
|
|
109
|
+
|
|
110
|
+
const inspected: SkillRootInfo[] = [];
|
|
111
|
+
let rootHit = false;
|
|
112
|
+
for (const root of roots) {
|
|
113
|
+
const present = await rootHasGenerativeMcpapps(root.path);
|
|
114
|
+
inspected.push({ ...root, present });
|
|
115
|
+
if (present) rootHit = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let via: HostSkillStatus['via'] = null;
|
|
119
|
+
let listHit = false;
|
|
120
|
+
if (options.listSkills) {
|
|
121
|
+
try {
|
|
122
|
+
listHit = skillListIncludes(await options.listSkills());
|
|
123
|
+
if (listHit) via = 'skills.list';
|
|
124
|
+
} catch {
|
|
125
|
+
// Registry/RPC absence falls back to the scanned roots.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!listHit && rootHit) via = 'skill-root';
|
|
129
|
+
|
|
130
|
+
const packPath = options.packPath ?? pluginSkillPackPath();
|
|
131
|
+
return {
|
|
132
|
+
name: GENERATIVE_MCPAPPS_SKILL,
|
|
133
|
+
installed: listHit || rootHit,
|
|
134
|
+
via,
|
|
135
|
+
roots: inspected,
|
|
136
|
+
...packPath !== undefined ? { packPath } : {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Shared generative-mcpapps identity. Plugin-tree presence is not installation. */
|
|
2
|
+
|
|
3
|
+
export const GENERATIVE_MCPAPPS_SKILL = 'generative-mcpapps';
|
|
4
|
+
|
|
5
|
+
/** Conventional relative roots only. Never expand $HOME or dump host `root.path`. */
|
|
6
|
+
export const CONVENTIONAL_SKILL_ROOTS = ['.dsh/skills', '.agents/skills'] as const;
|
|
7
|
+
|
|
8
|
+
/** Copy source relative to the plugin / checkout, not an absolute pack path. */
|
|
9
|
+
export const SKILL_PACK_RELATIVE = `skills/${GENERATIVE_MCPAPPS_SKILL}`;
|
|
10
|
+
|
|
11
|
+
export type SkillRootSource =
|
|
12
|
+
| 'project-dsh'
|
|
13
|
+
| 'project-agents'
|
|
14
|
+
| 'user-dsh'
|
|
15
|
+
| 'user-agents'
|
|
16
|
+
| 'custom'
|
|
17
|
+
| 'bundled'
|
|
18
|
+
| (string & {});
|
|
19
|
+
|
|
20
|
+
export interface SkillRootInfo {
|
|
21
|
+
readonly source: SkillRootSource;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly present?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface HostSkillStatus {
|
|
27
|
+
readonly name: typeof GENERATIVE_MCPAPPS_SKILL;
|
|
28
|
+
readonly installed: boolean;
|
|
29
|
+
readonly via: 'skills.list' | 'skill-root' | null;
|
|
30
|
+
readonly roots: readonly SkillRootInfo[];
|
|
31
|
+
/** Plugin-shipped pack path for copy guidance. Not a harness skill root. */
|
|
32
|
+
readonly packPath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function skillNameMatches(name: unknown): boolean {
|
|
36
|
+
return typeof name === 'string' && name === GENERATIVE_MCPAPPS_SKILL;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function skillListIncludes(entries: readonly { readonly name?: string }[] | undefined): boolean {
|
|
40
|
+
return Array.isArray(entries) && entries.some(entry => skillNameMatches(entry?.name));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Directory bundle `name/SKILL.md` or flat `name.md` at a scanned root. */
|
|
44
|
+
export function skillRootEntryMatches(entryName: string): boolean {
|
|
45
|
+
return entryName === GENERATIVE_MCPAPPS_SKILL || entryName === `${GENERATIVE_MCPAPPS_SKILL}.md`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function skillsFromListResult(result: unknown): { name?: string }[] {
|
|
49
|
+
if (Array.isArray(result)) return result;
|
|
50
|
+
if (result === null || typeof result !== 'object') return [];
|
|
51
|
+
const record = result as {
|
|
52
|
+
ok?: boolean;
|
|
53
|
+
value?: { skills?: unknown } | unknown;
|
|
54
|
+
skills?: unknown;
|
|
55
|
+
};
|
|
56
|
+
if (record.ok === false) return [];
|
|
57
|
+
if (Array.isArray(record.skills)) return record.skills as { name?: string }[];
|
|
58
|
+
if (record.value && typeof record.value === 'object') {
|
|
59
|
+
const value = record.value as { skills?: unknown };
|
|
60
|
+
if (Array.isArray(value.skills)) return value.skills as { name?: string }[];
|
|
61
|
+
if (Array.isArray(record.value)) return record.value as { name?: string }[];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function publicSkillRoots(): readonly SkillRootInfo[] {
|
|
67
|
+
return CONVENTIONAL_SKILL_ROOTS.map(path => ({ source: 'conventional', path }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Strip host home / pack absolutes before anything user-facing (HTTP or Settings).
|
|
72
|
+
* Detection still uses the raw scan; this is the public face.
|
|
73
|
+
*/
|
|
74
|
+
export function toPublicSkillStatus(status: HostSkillStatus): HostSkillStatus {
|
|
75
|
+
return {
|
|
76
|
+
name: status.name,
|
|
77
|
+
installed: status.installed,
|
|
78
|
+
via: status.via,
|
|
79
|
+
roots: publicSkillRoots(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import { ChoreographedFlow } from '../src/client/ChoreographedFlow.js';
|
|
5
|
+
import { ClosedProcessSummary } from '../src/client/ClosedProcessSummary.js';
|
|
6
|
+
import { ProcessFragment } from '../src/client/motion.js';
|
|
7
|
+
import { presentLiveTurn } from '../src/client/live-turn.js';
|
|
8
|
+
import type { LiveStep } from '../src/client/live-turn.js';
|
|
9
|
+
function Fixture() {
|
|
10
|
+
const [open,setOpen]=useState(false);const button=useRef<HTMLButtonElement>(null);
|
|
11
|
+
const steps=useMemo(()=>Array.from({length:500},(_,i):LiveStep=>({kind:'reasoning',key:`r${i}`,nodeKey:`r${i}`,step:i,start:0,blocks:[]})),[]);
|
|
12
|
+
const frame=useMemo(()=>({items:presentLiveTurn(steps,{status:'closed',reason:'completed',closingStep:501,latestStep:501}),snapshot:{} as ChatSnapshot}),[steps]);
|
|
13
|
+
return <main style={{padding:30}}><h1>完成后 500 条过程记录:实际组件回归</h1>
|
|
14
|
+
<button ref={button} onClick={()=>setOpen(v=>!v)}>切换过程展开</button>
|
|
15
|
+
<ClosedProcessSummary steps={steps} open={open} onChange={setOpen} controls="closed-flow"/>
|
|
16
|
+
<ChoreographedFlow id="closed-flow" frame={frame} motion enabled={false} urgent={false} processOpen={open} open={{}} onOpenChange={()=>{}}
|
|
17
|
+
renderStep={step=><ProcessFragment open={open} motion onRead={()=>{}} returnFocusTo={button} nodeKey={step.key}><p>过程内容 {step.key}</p></ProcessFragment>}/>
|
|
18
|
+
<article data-final-answer>最终回答必须紧随统计,不能隔着 8000px 空白;展开、收起以后仍应如此。</article></main>;
|
|
19
|
+
}
|
|
20
|
+
createRoot(document.getElementById('app')!).render(<Fixture/>);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import { ChoreographedFlow, useFlowChat } from '../src/client/ChoreographedFlow.js';
|
|
5
|
+
import { presentLiveTurn } from '../src/client/live-turn.js';
|
|
6
|
+
import type { LiveStep } from '../src/client/live-turn.js';
|
|
7
|
+
import { Disclosure, StatusText, useMotionAllowed, useReadingScroll } from '../src/client/motion.js';
|
|
8
|
+
import { ReasoningCard } from '../src/client/ReasoningCard.js';
|
|
9
|
+
import { StreamMotionContext, useStreamingText } from '../src/client/streaming.js';
|
|
10
|
+
import css from '../src/client/Reader.module.css';
|
|
11
|
+
|
|
12
|
+
const sample = '旧内容保持原位,先完成连续收拢,再确认数字,最后继续输出。This is a real component regression fixture, not a replacement animation. ';
|
|
13
|
+
function Text({ step }: { step: LiveStep }) {
|
|
14
|
+
const node = useFlowChat(s => 'nodeKey' in step ? s.nodes.get(step.nodeKey) : undefined);
|
|
15
|
+
const source = (node?.data as { text?: string })?.text ?? '';
|
|
16
|
+
const text = useStreamingText(source, true, { startedAt: 0, interrupted: false, selected: false });
|
|
17
|
+
if (step.kind === 'reasoning') return <ReasoningCard step={step.step} active motion selected={false} onRead={() => {}}>
|
|
18
|
+
<div data-test-text={step.key} style={{ whiteSpace: 'pre-wrap' }}>{text.text}</div>
|
|
19
|
+
</ReasoningCard>;
|
|
20
|
+
return <div data-reader-anchor style={{ padding: '10px 0' }} data-test-text={step.key}>{step.kind === 'body' ? sample : '工具 · 读取源文件,等待下一个步骤'}</div>;
|
|
21
|
+
}
|
|
22
|
+
function Demo() {
|
|
23
|
+
const [n, setN] = useState(1);
|
|
24
|
+
const [tail, setTail] = useState(0);
|
|
25
|
+
const [auto, setAuto] = useState(true);
|
|
26
|
+
const [motionSetting, setMotion] = useState(true);
|
|
27
|
+
const motion = useMotionAllowed(motionSetting);
|
|
28
|
+
const [urgent, setUrgent] = useState(false);
|
|
29
|
+
const [expanded, setExpanded] = useState(true);
|
|
30
|
+
const [open, setOpen] = useState<Record<string, boolean>>({});
|
|
31
|
+
const root = useRef<HTMLDivElement>(null); const button=useRef<HTMLButtonElement>(null);
|
|
32
|
+
useReadingScroll(root, motion);
|
|
33
|
+
const frame = useMemo(() => {
|
|
34
|
+
const steps: LiveStep[] = [];
|
|
35
|
+
const nodes = new Map();
|
|
36
|
+
for (let i=1; i<=n; i++) {
|
|
37
|
+
steps.push({ kind: 'reasoning', key: `r${i}`, nodeKey:`r${i}`, start:0, blocks:[], step:i });
|
|
38
|
+
nodes.set(`r${i}`, { data: { text: sample.repeat(i===n && n>1 ? 3 + tail : 5) } });
|
|
39
|
+
if (i<n || n===1) {
|
|
40
|
+
steps.push({kind:'body',key:`b${i}`,nodeKey:`b${i}`,start:1,blocks:[],step:i});
|
|
41
|
+
steps.push({kind:'other',key:`t${i}`,nodeKey:`t${i}`});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {items:presentLiveTurn(steps,{status:'open'},auto),snapshot:{nodes:{get:(key:string)=>nodes.get(key)}} as unknown as ChatSnapshot};
|
|
45
|
+
}, [n,tail,auto]);
|
|
46
|
+
(window as any).fixture = { next:()=>setN(v=>v+1), burst:()=>{setN(v=>v+1); setTimeout(()=>setN(v=>v+2),100); setTimeout(()=>setTail(v=>v+2),180);}, stop:()=>setUrgent(true), reduce:()=>setMotion(false), reset:()=>{setN(1);setTail(0);setUrgent(false);setOpen({});setMotion(true);setAuto(true);} };
|
|
47
|
+
return <><nav style={{padding:12,display:'flex',gap:12,flexWrap:'wrap'}}>
|
|
48
|
+
<b>转场验收 · 真实组件 / 模拟数据</b>
|
|
49
|
+
<button onClick={()=>(window as any).fixture.next()}>下一步</button>
|
|
50
|
+
<button onClick={()=>(window as any).fixture.burst()}>突发三步</button>
|
|
51
|
+
<button onClick={()=>(window as any).fixture.stop()}>停止</button>
|
|
52
|
+
<button onClick={()=>(window as any).fixture.reset()}>重置</button>
|
|
53
|
+
<button onClick={()=>setMotion(v=>!v)}>动效{motion?'开':'关'}</button>
|
|
54
|
+
</nav><div data-conversation-scroll style={{height:'calc(100vh - 70px)',overflow:'auto'}}>
|
|
55
|
+
<StreamMotionContext.Provider value={{enabled:motion,activatedAt:Date.now()}}>
|
|
56
|
+
<div ref={root} className={css.root} data-motion={motion?'on':'off'}><div className={css.column} data-chat-flow="">
|
|
57
|
+
<div className={css.toolbar}><button className={css.textButton} onClick={()=>setAuto(v=>!v)}>自动折叠{auto?'开':'关'}</button></div>
|
|
58
|
+
<div className={css.turnProcessSticky}><Disclosure open={expanded} onChange={setExpanded} buttonRef={button} label={<StatusText text="正在处理" motion={motion}/>} /></div>
|
|
59
|
+
<ChoreographedFlow id="fixture-flow" frame={frame} motion={motion} enabled={auto} urgent={urgent} processOpen={expanded}
|
|
60
|
+
open={open} onOpenChange={(key,value)=>setOpen(s=>({...s,[key]:value}))} renderStep={step=><Text step={step}/>} />
|
|
61
|
+
<div data-test-footer style={{height:72}}>输入区位置参照 · 后台数据不会被修改</div>
|
|
62
|
+
</div></div></StreamMotionContext.Provider>
|
|
63
|
+
</div></>;
|
|
64
|
+
}
|
|
65
|
+
createRoot(document.getElementById('app')!).render(<Demo />);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import { StickyLane } from '../src/client/StickyLane.js';
|
|
5
|
+
import { ChoreographedFlow } from '../src/client/ChoreographedFlow.js';
|
|
6
|
+
import { ClosedProcessSummary } from '../src/client/ClosedProcessSummary.js';
|
|
7
|
+
import { Disclosure, StatusText } from '../src/client/motion.js';
|
|
8
|
+
import { presentLiveTurn } from '../src/client/live-turn.js';
|
|
9
|
+
import type { LiveStep } from '../src/client/live-turn.js';
|
|
10
|
+
import css from '../src/client/Reader.module.css';
|
|
11
|
+
const steps:LiveStep[]=[...Array.from({length:6},(_,i):LiveStep=>({kind:'reasoning',key:`r${i}`,nodeKey:`r${i}`,step:i,start:0,blocks:[]})),{kind:'body',key:'body',nodeKey:'body',step:6,start:0,blocks:[]},{kind:'other',key:'extra',nodeKey:'extra'},{kind:'reasoning',key:'latest',nodeKey:'latest',step:7,start:0,blocks:[]},{kind:'body',key:'answer',nodeKey:'answer',step:7,start:0,blocks:[]}];
|
|
12
|
+
function Fixture(){
|
|
13
|
+
const [expanded,setExpanded]=useState(true),[open,setOpen]=useState<Record<string,boolean>>({}),[closed,setClosed]=useState(false);const button=useRef<HTMLButtonElement>(null);
|
|
14
|
+
const frame=useMemo(()=>({items:presentLiveTurn(steps,{status:'open',reason:null,latestStep:7,closingStep:null}),snapshot:{} as ChatSnapshot}),[]);
|
|
15
|
+
return <><button onClick={()=>setClosed(v=>!v)}>切换完成状态</button><div data-conversation-scroll style={{height:'calc(100vh - 32px)',overflow:'auto'}}><div className={css.root} data-dsh-better-display="fixture"><div className={css.column} data-chat-flow="">
|
|
16
|
+
<StickyLane kind="toolbar" className={css.toolbar}><button className={css.textButton}>自动折叠开</button></StickyLane>
|
|
17
|
+
<section className={css.turn} data-reader-turn="1"><div style={{height:240,background:'#eef2ff',alignSelf:'flex-end',width:'65%'}}>用户消息:滚动抵达顶部才吸顶。</div>
|
|
18
|
+
<StickyLane kind="status" className={css.turnProcessSticky}><Disclosure open={expanded} onChange={setExpanded} buttonRef={button} label={<StatusText text="正在使用工具" motion={false}/>} /></StickyLane>
|
|
19
|
+
{closed?<><ClosedProcessSummary steps={steps.slice(0,-1)} open={expanded} onChange={setExpanded} controls="body"/><div id="body" style={{height:1600}}>最终回答占位,验证完成后统计仍然吸顶。</div></>:<ChoreographedFlow id="flow" frame={frame} motion={false} enabled={false} urgent={false} processOpen={expanded} open={open} onOpenChange={(k,v)=>{setExpanded(true);setOpen(x=>({...x,[k]:v}));}} renderStep={s=>s.key==='answer'?<div style={{height:1600}}>当前输出保持完整,统计不应被推出视野。</div>:expanded?<div style={{height:s.key==='latest'?300:100}}>过程 {s.key}</div>:null}/>}
|
|
20
|
+
</section><div style={{height:180}}>下一轮之前</div></div></div></div></>;
|
|
21
|
+
}
|
|
22
|
+
createRoot(document.getElementById('app')!).render(<Fixture/>);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { WaitingStatus } from '../src/client/WaitingStatus.js';
|
|
4
|
+
import { waitingAnchor } from '../src/client/waiting-clock.js';
|
|
5
|
+
function Fixture() {
|
|
6
|
+
const [inputs,setInputs]=useState([{key:'u1',kind:'user',data:{time:Date.now()-300000}}]);
|
|
7
|
+
const [renders,setRenders]=useState(0);
|
|
8
|
+
const anchor=waitingAnchor(inputs.map(x=>x.key),key=>inputs.find(x=>x.key===key));
|
|
9
|
+
return <main style={{padding:30}}><h1>等待计时回归:真实组件、模拟输入</h1>
|
|
10
|
+
<button onClick={()=>setInputs(x=>[...x,{key:`s${x.length}`,kind:'steering',data:{time:Date.now()}}])}>追加 steering</button>
|
|
11
|
+
<button onClick={()=>setRenders(x=>x+1)}>普通输出更新</button><span>更新次数 {renders}</span>
|
|
12
|
+
<WaitingStatus anchor={anchor} label="深度求索中..."/></main>;
|
|
13
|
+
}
|
|
14
|
+
createRoot(document.getElementById('app')!).render(<Fixture/>);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
|
|
9
|
+
/** Maid-atelier / phoebe-atelier hide rule, reduced to the :has() predicate. */
|
|
10
|
+
const SKIN_HIDE = '[data-conversation-scroll]:not(:has([data-chat-flow])) > [data-composer-seat]';
|
|
11
|
+
|
|
12
|
+
interface AttrNode {
|
|
13
|
+
attrs: Record<string, string>;
|
|
14
|
+
children: AttrNode[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasAttr(node: AttrNode, name: string): boolean {
|
|
18
|
+
if (Object.hasOwn(node.attrs, name)) return true;
|
|
19
|
+
return node.children.some(child => hasAttr(child, name));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True when the skin rule would set the seat to display:none. */
|
|
23
|
+
function skinHidesComposerSeat(scroll: AttrNode): boolean {
|
|
24
|
+
return !hasAttr(scroll, 'data-chat-flow');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readerColumn(hook: boolean): AttrNode {
|
|
28
|
+
return {
|
|
29
|
+
attrs: hook ? { 'data-chat-flow': '' } : {},
|
|
30
|
+
children: [{ attrs: { 'data-reader-turn': '1' }, children: [] }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('Reader source keeps the native ChatView chat-flow hook on its column', () => {
|
|
35
|
+
const source = readFileSync(resolve(root, 'src/client/Reader.tsx'), 'utf8');
|
|
36
|
+
const column = source.match(/<div className=\{css\.column\}[^>]*>/);
|
|
37
|
+
assert.ok(column, 'Reader still renders css.column');
|
|
38
|
+
assert.match(column[0], /data-chat-flow=""/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('committed client bundle publishes data-chat-flow for git installs', () => {
|
|
42
|
+
const client = readFileSync(resolve(root, 'lib/client.js'), 'utf8');
|
|
43
|
+
assert.match(client, /"data-chat-flow":\s*""/);
|
|
44
|
+
const compiled = readFileSync(resolve(root, 'lib/types/client/Reader.js'), 'utf8');
|
|
45
|
+
assert.match(compiled, /"data-chat-flow":\s*""/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('skin empty-state hide stays off when Reader mounts inside the scrollport', () => {
|
|
49
|
+
const inspectOnly: AttrNode = {
|
|
50
|
+
attrs: { 'data-conversation-scroll': '' },
|
|
51
|
+
children: [
|
|
52
|
+
{ attrs: { 'data-reader-turn': '1' }, children: [] },
|
|
53
|
+
{ attrs: { 'data-composer-seat': '' }, children: [] },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
const withReader: AttrNode = {
|
|
57
|
+
attrs: { 'data-conversation-scroll': '' },
|
|
58
|
+
children: [readerColumn(true), { attrs: { 'data-composer-seat': '' }, children: [] }],
|
|
59
|
+
};
|
|
60
|
+
assert.equal(skinHidesComposerSeat(inspectOnly), true, SKIN_HIDE);
|
|
61
|
+
assert.equal(skinHidesComposerSeat(withReader), false, SKIN_HIDE);
|
|
62
|
+
assert.equal(skinHidesComposerSeat({
|
|
63
|
+
attrs: { 'data-conversation-scroll': '' },
|
|
64
|
+
children: [readerColumn(false), { attrs: { 'data-composer-seat': '' }, children: [] }],
|
|
65
|
+
}), true);
|
|
66
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
|
|
9
|
+
test('cloud hard-gate files are committed and ban other models / computer use', () => {
|
|
10
|
+
const hooks = JSON.parse(readFileSync(resolve(root, '.cursor/hooks.json'), 'utf8')) as {
|
|
11
|
+
hooks: { subagentStart: { command: string }[] };
|
|
12
|
+
};
|
|
13
|
+
assert.equal(hooks.hooks.subagentStart[0]?.command, '.cursor/hooks/block-other-models.sh');
|
|
14
|
+
|
|
15
|
+
const script = resolve(root, '.cursor/hooks/block-other-models.sh');
|
|
16
|
+
accessSync(script, constants.X_OK);
|
|
17
|
+
const hook = readFileSync(script, 'utf8');
|
|
18
|
+
assert.match(hook, /computeruse|computerUse/);
|
|
19
|
+
assert.match(hook, /browser/);
|
|
20
|
+
assert.match(hook, /claude|sonnet|opus/);
|
|
21
|
+
|
|
22
|
+
const worker = readFileSync(resolve(root, '.cursor/agents/worker.md'), 'utf8');
|
|
23
|
+
assert.match(worker, /model: inherit/);
|
|
24
|
+
assert.match(worker, /force-default-model: true/);
|
|
25
|
+
|
|
26
|
+
const cloud = readFileSync(resolve(root, '.cursor/CLOUD.md'), 'utf8');
|
|
27
|
+
assert.match(cloud, /Only use Grok 4\.6/);
|
|
28
|
+
assert.match(cloud, /Never Claude, Sonnet, Opus, GPT, or Gemini/);
|
|
29
|
+
assert.match(cloud, /model: inherit/);
|
|
30
|
+
assert.match(cloud, /Do not spawn the Task tool, computerUse, or browser/);
|
|
31
|
+
assert.match(cloud, /Composer is the only fallback/);
|
|
32
|
+
assert.match(cloud, /Do not pass a `model` argument/);
|
|
33
|
+
assert.match(cloud, /Prefer Grok or Composer only/);
|
|
34
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { flowTop, landTurn, READING_LINE_OFFSET_PX, scrollerOf } from '../src/client/conversation-scroll.ts';
|
|
4
|
+
|
|
5
|
+
interface FakeBox {
|
|
6
|
+
top: number;
|
|
7
|
+
scrollTop: number;
|
|
8
|
+
closest: (selector: string) => FakeBox | null;
|
|
9
|
+
getBoundingClientRect: () => { top: number };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function box(top: number, extra: Partial<FakeBox> = {}): FakeBox {
|
|
13
|
+
const node: FakeBox = {
|
|
14
|
+
top,
|
|
15
|
+
scrollTop: 0,
|
|
16
|
+
closest: () => null,
|
|
17
|
+
getBoundingClientRect: () => ({ top: node.top }),
|
|
18
|
+
...extra,
|
|
19
|
+
};
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('scrollerOf prefers the conversation scroll host over the reader root', () => {
|
|
24
|
+
const host = box(40);
|
|
25
|
+
const root = box(80, { closest: selector => selector === '[data-conversation-scroll]' ? host : null });
|
|
26
|
+
assert.equal(scrollerOf(root as unknown as HTMLElement), host as unknown as HTMLElement);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('scrollerOf falls back to the reader root when no host exists', () => {
|
|
30
|
+
const root = box(0);
|
|
31
|
+
assert.equal(scrollerOf(root as unknown as HTMLElement), root as unknown as HTMLElement);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('landTurn writes only scrollport.scrollTop using the official reading-line offset', () => {
|
|
35
|
+
const scrollport = box(100, { scrollTop: 50 });
|
|
36
|
+
const row = box(300);
|
|
37
|
+
landTurn(row as unknown as HTMLElement, scrollport as unknown as HTMLElement);
|
|
38
|
+
assert.equal(flowTop(row as unknown as HTMLElement, scrollport as unknown as HTMLElement), 200);
|
|
39
|
+
assert.equal(scrollport.scrollTop, 50 + 200 - READING_LINE_OFFSET_PX);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('landTurn to the last row clamps naturally via the caller scrollport assignment', () => {
|
|
43
|
+
const scrollport = box(0, { scrollTop: 900 });
|
|
44
|
+
const row = box(40);
|
|
45
|
+
landTurn(row as unknown as HTMLElement, scrollport as unknown as HTMLElement);
|
|
46
|
+
assert.equal(scrollport.scrollTop, 900 + 40 - READING_LINE_OFFSET_PX);
|
|
47
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import type { TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
4
|
+
import { basename, createProducedFileMentions, dirname, getTurnDeliverables, showDeliverablesRow } from '../src/client/deliverables.ts';
|
|
5
|
+
import type { ReaderFlowEntry } from '../src/client/tool-activity.ts';
|
|
6
|
+
|
|
7
|
+
test('basename and dirname handle POSIX and Windows style paths', () => {
|
|
8
|
+
assert.equal(basename('src/client/Reader.tsx'), 'Reader.tsx');
|
|
9
|
+
assert.equal(dirname('src/client/Reader.tsx'), 'src/client');
|
|
10
|
+
assert.equal(basename('C:\\project\\src\\index.ts'), 'index.ts');
|
|
11
|
+
assert.equal(dirname('C:\\project\\src\\index.ts'), 'C:\\project\\src');
|
|
12
|
+
assert.equal(basename('simple.txt'), 'simple.txt');
|
|
13
|
+
assert.equal(dirname('simple.txt'), '.');
|
|
14
|
+
assert.equal(basename('/root/file.md/'), 'file.md');
|
|
15
|
+
assert.equal(dirname('/root/file.md/'), '/root');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('getTurnDeliverables extracts produced paths from turn deliverables data', () => {
|
|
19
|
+
const map = new Map<string, unknown>();
|
|
20
|
+
map.set('deliverables', {
|
|
21
|
+
produced: [
|
|
22
|
+
{ seq: 1, path: 'src/client/Reader.tsx' },
|
|
23
|
+
{ seq: 2, path: 'src/client/Reader.module.css' },
|
|
24
|
+
{ seq: 3, path: 'src/client/Reader.tsx' }, // duplicate
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const turn = { data: map } as unknown as TurnLocation;
|
|
28
|
+
const paths = getTurnDeliverables(turn);
|
|
29
|
+
assert.deepEqual(paths, ['src/client/Reader.tsx', 'src/client/Reader.module.css']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('getTurnDeliverables falls back to tool flow when turn data is absent', () => {
|
|
33
|
+
const flow: ReaderFlowEntry[] = [
|
|
34
|
+
{
|
|
35
|
+
kind: 'tool',
|
|
36
|
+
key: 'tool:1',
|
|
37
|
+
callId: 'call:1',
|
|
38
|
+
step: 1,
|
|
39
|
+
order: 0,
|
|
40
|
+
block: {
|
|
41
|
+
kind: 'tool-call',
|
|
42
|
+
name: 'write',
|
|
43
|
+
argsRaw: JSON.stringify({ file_path: 'src/client/new-feature.ts', content: 'hello' }),
|
|
44
|
+
} as any,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
kind: 'tool',
|
|
48
|
+
key: 'tool:2',
|
|
49
|
+
callId: 'call:2',
|
|
50
|
+
step: 2,
|
|
51
|
+
order: 1,
|
|
52
|
+
block: {
|
|
53
|
+
kind: 'tool-call',
|
|
54
|
+
name: 'edit',
|
|
55
|
+
argsRaw: JSON.stringify({ file_path: 'src/client/Reader.tsx', old_string: 'a', new_string: 'b' }),
|
|
56
|
+
} as any,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
kind: 'tool',
|
|
60
|
+
key: 'tool:3',
|
|
61
|
+
callId: 'call:3',
|
|
62
|
+
step: 3,
|
|
63
|
+
order: 2,
|
|
64
|
+
block: {
|
|
65
|
+
kind: 'tool-result',
|
|
66
|
+
isError: true, // error result should be skipped
|
|
67
|
+
name: 'write',
|
|
68
|
+
argsRaw: JSON.stringify({ file_path: 'bad.txt', content: 'err' }),
|
|
69
|
+
} as any,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
kind: 'tool',
|
|
73
|
+
key: 'tool:4',
|
|
74
|
+
callId: 'call:4',
|
|
75
|
+
step: 4,
|
|
76
|
+
order: 3,
|
|
77
|
+
block: {
|
|
78
|
+
kind: 'tool-call',
|
|
79
|
+
name: 'read', // read should not be considered a deliverable
|
|
80
|
+
argsRaw: JSON.stringify({ file_path: 'package.json' }),
|
|
81
|
+
} as any,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const paths = getTurnDeliverables(undefined, flow);
|
|
86
|
+
assert.deepEqual(paths, ['src/client/new-feature.ts', 'src/client/Reader.tsx']);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('createProducedFileMentions resolves exact paths and unique basenames, leaving ambiguous basenames inert', () => {
|
|
90
|
+
const opened: string[] = [];
|
|
91
|
+
const openFile = (p: string) => { opened.push(p); };
|
|
92
|
+
const paths = [
|
|
93
|
+
'src/client/Reader.tsx',
|
|
94
|
+
'src/server/Reader.tsx', // duplicate basename
|
|
95
|
+
'src/client/Reader.module.css', // unique basename
|
|
96
|
+
];
|
|
97
|
+
const mentions = createProducedFileMentions(paths, openFile);
|
|
98
|
+
|
|
99
|
+
// Exact path resolves
|
|
100
|
+
const exact = mentions.resolve('src/client/Reader.tsx');
|
|
101
|
+
assert.ok(exact);
|
|
102
|
+
assert.equal(exact.title, 'src/client/Reader.tsx');
|
|
103
|
+
assert.equal(exact.label, '打开 src/client/Reader.tsx');
|
|
104
|
+
exact.open();
|
|
105
|
+
assert.deepEqual(opened, ['src/client/Reader.tsx']);
|
|
106
|
+
|
|
107
|
+
// Unique basename resolves
|
|
108
|
+
const unique = mentions.resolve('Reader.module.css');
|
|
109
|
+
assert.ok(unique);
|
|
110
|
+
assert.equal(unique.title, 'src/client/Reader.module.css');
|
|
111
|
+
unique.open();
|
|
112
|
+
assert.deepEqual(opened, ['src/client/Reader.tsx', 'src/client/Reader.module.css']);
|
|
113
|
+
|
|
114
|
+
// Ambiguous basename leaves undefined (never guess or open the wrong file)
|
|
115
|
+
const ambiguous = mentions.resolve('Reader.tsx');
|
|
116
|
+
assert.equal(ambiguous, undefined);
|
|
117
|
+
|
|
118
|
+
// Unrelated file leaves undefined
|
|
119
|
+
const unrelated = mentions.resolve('unknown.js');
|
|
120
|
+
assert.equal(unrelated, undefined);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('produced-files row waits for turn close even when paths already exist', () => {
|
|
124
|
+
const paths = ['src/client/Watcher.tsx'];
|
|
125
|
+
assert.equal(showDeliverablesRow('open', paths), false);
|
|
126
|
+
assert.equal(showDeliverablesRow('closed', paths), true);
|
|
127
|
+
assert.equal(showDeliverablesRow('closed', []), false);
|
|
128
|
+
});
|