返回 slidev
lmTools.ts
根目录 / packages / vscode / src / lmTools.ts
1 import { slash } from '@antfu/utils'
2 import { stringifySlide } from '@slidev/parser/core'
3 import { defineService, useDisposable } from 'reactive-vscode'
4 import { LanguageModelTextPart, LanguageModelToolResult, lm } from 'vscode'
5 import { useFocusedSlide } from './composables/useFocusedSlide'
6 import { activeEntry, activeProject, projects } from './projects'
7
8 export const useLmTools = defineService(() => {
9 const { focusedMarkdown, focusedSourceSlide, focusedSlideNo } = useFocusedSlide()
10
11 registerSimpleTool('slidev_getActiveSlide', () => {
12 const project = activeProject.value
13
14 if (project == null) {
15 throw new Error(`No active slide project found.`)
16 }
17
18 return formatObject({
19 'Entry file': project.entry,
20 'Root directory': project.userRoot,
21 'Preview server port': project.port.value || 'Not running',
22 'Number of slides': project.data.slides.length,
23 'Focused slide no. in presentation (from 1)': focusedSlideNo.value || 'None',
24 'Editing file': focusedMarkdown.value?.filepath || 'Not editing',
25 'Editing slide index in file (from 0)': focusedSourceSlide.value ? focusedSourceSlide.value.index : 'N/A',
26 })
27 })
28
29 registerSimpleTool('slidev_getSlideContent', (input: {
30 entrySlidePath: string
31 slideNo: number
32 }) => {
33 const project = resolveProjectFromEntry(input.entrySlidePath)
34 const slide = project.data.slides[input.slideNo - 1]
35
36 if (slide == null) {
37 throw new Error(`No content found for slide number ${input.slideNo} in entry: ${project.entry}. Available slides numbers: 1-${project.data.slides.length}`)
38 }
39
40 return `Content of slide number ${input.slideNo} in entry "${project.entry}" in file "${slide.source.filepath}":\n\n${stringifySlide(slide.source, 1)}`
41 })
42
43 // Get all slide titles
44 registerSimpleTool('slidev_getAllSlideTitles', (input: { entrySlidePath: string }) => {
45 const project = resolveProjectFromEntry(input.entrySlidePath)
46 const titles = project.data.slides.map((slide, idx) => `#${idx + 1}: ${slide.title || '(Untitled)'}`)
47 return formatList(titles)
48 })
49
50 // Find slide number by title
51 registerSimpleTool('slidev_findSlideNoByTitle', (input: { entrySlidePath: string, title: string }) => {
52 const project = resolveProjectFromEntry(input.entrySlidePath)
53 const idx = project.data.slides.findIndex(slide => slide.title === input.title)
54 if (idx === -1) {
55 throw new Error(`No slide found with title: "${input.title}".`)
56 }
57 return formatObject({
58 'Title': input.title,
59 'Slide number': idx + 1,
60 })
61 })
62
63 // List all loaded Slidev entries
64 registerSimpleTool('slidev_listEntries', () => {
65 const entries = [...projects.keys()]
66 if (entries.length === 0) {
67 return 'No loaded Slidev project entries.'
68 }
69 return formatList(entries)
70 })
71
72 // Get project preview port
73 registerSimpleTool('slidev_getPreviewPort', (input: { entrySlidePath: string }) => {
74 const project = resolveProjectFromEntry(input.entrySlidePath)
75 return formatObject({
76 'Project entry': project.entry,
77 'Preview port': project.port.value || 'Not running',
78 })
79 })
80
81 // Choose active Slidev entry
82 registerSimpleTool('slidev_chooseEntry', (input: { entrySlidePath: string }) => {
83 if (!input.entrySlidePath) {
84 throw new Error('entrySlidePath is required.')
85 }
86 const project = resolveProjectFromEntry(input.entrySlidePath)
87 activeEntry.value = project.entry
88 return formatObject({
89 'Active entry switched to': project.entry,
90 })
91 })
92 })
93
94 function registerSimpleTool<T>(name: string, invoke: (input: T) => string) {
95 useDisposable(lm.registerTool<T>(name, {
96 invoke({ input }) {
97 try {
98 const result = invoke(input)
99 return new LanguageModelToolResult([
100 new LanguageModelTextPart(result),
101 ])
102 }
103 catch (error: any) {
104 return new LanguageModelToolResult([
105 new LanguageModelTextPart(`Error: ${error.message || error.toString()}`),
106 ])
107 }
108 },
109 }))
110 }
111
112 function resolveProjectFromEntry(entry: string) {
113 if (entry === '' || entry === '$ACTIVE_SLIDE_ENTRY') {
114 if (!activeEntry.value) {
115 throw new Error('No active slide entry found. Please set an active slide entry before using this tool.')
116 }
117 entry = activeEntry.value
118 }
119
120 let project = projects.get(entry)
121 if (!project) {
122 entry = slash(entry)
123 const possibleProjects = [...projects.values()].filter(p => p.entry.includes(entry))
124 if (possibleProjects.length === 0) {
125 throw new Error(`No project found for entry: ${entry}. All entries: ${formatList(projects.keys())}`)
126 }
127 else if (possibleProjects.length > 1) {
128 throw new Error(`Multiple projects found for entry: ${entry}. Please specify the full path. All entries: ${formatList(projects.keys())}`)
129 }
130 else {
131 project = possibleProjects[0]
132 }
133 }
134
135 return project
136 }
137
138 function formatList(items: Iterable<string>): string {
139 const itemsArray = [...items]
140 if (itemsArray.length === 0) {
141 return 'No items found.'
142 }
143 return itemsArray.map(item => `- ${item}\n`).join('')
144 }
145
146 function formatObject(obj: Record<string, string | number>): string {
147 return Object.entries(obj)
148 .map(([key, value]) => `- ${key}: ${value}\n`)
149 .join('')
150 }
151
151 lines TYPESCRIPT