Skip to content

Commit 7ff5944

Browse files
committed
Implement support for the #Skill keyword to reference and embed installed agent skills within prompts
1 parent 212939f commit 7ff5944

20 files changed

Lines changed: 1395 additions & 727 deletions

‎packages/ui/src/components/editor/panel/PromptField/PromptField.module.scss‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@
216216
}
217217
}
218218

219+
&--skill &__icon::before {
220+
content: '\ec59';
221+
}
222+
219223
&--changes &__icon::before {
220224
content: '\ebdb';
221225
}

‎packages/ui/src/components/editor/panel/PromptField/hooks/use-handlers.ts‎

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const getKeywordRanges = (
2121
const ranges: { start: number; end: number }[] = []
2222
// This regex is a combination of all keyword types
2323
const regex =
24-
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"[^"]+")|(#(?:Commit|ContextAtCommit):[^:]+:[^\s"]+\s+"(?:\\.|[^"\\])*")|(<fragment path="[^"]+"(?: [^>]+)?>\n[\s\S]*?\n<\/fragment>)/g
24+
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"[^"]+")|(#(?:Commit|ContextAtCommit):[^:]+:[^\s"]+\s+"(?:\\.|[^"\\])*")|(<fragment path="[^"]+"(?: [^>]+)?>\n[\s\S]*?\n<\/fragment>)|(#Skill:[^:]+:[^:]+:[^\s]+)/g
2525

2626
let match
2727
while ((match = regex.exec(text)) !== null) {
@@ -149,6 +149,18 @@ const reconstruct_raw_value_from_node = (node: Node): string => {
149149
}
150150

151151
return `<fragment ${attributes}>\n${content}\n</fragment>`
152+
} else if (el.dataset.type == 'skill-keyword') {
153+
const agent = el.dataset.agent
154+
const repo = el.dataset.repo
155+
const skillName = el.dataset.skillName
156+
if (!agent || !repo || !skillName) return ''
157+
158+
const index = inner_content.indexOf(skillName)
159+
if (index != -1) {
160+
const prefix = inner_content.substring(0, index)
161+
const suffix = inner_content.substring(index + skillName.length)
162+
return `${prefix}#Skill:${agent}:${repo}:${skillName}${suffix}`
163+
}
152164
}
153165

154166
return inner_content
@@ -310,6 +322,18 @@ export const use_handlers = (
310322
const search_pattern = `#ContextAtCommit:${repo_name}:${commit_hash} "${commit_message.replace(/"/g, '\\"')}"`
311323
const start_index = props.value.indexOf(search_pattern)
312324

325+
if (start_index !== -1) {
326+
apply_keyword_deletion(start_index, start_index + search_pattern.length)
327+
}
328+
} else if (keyword_type == 'skill-keyword') {
329+
const agent = keyword_element.dataset.agent
330+
const repo = keyword_element.dataset.repo
331+
const skillName = keyword_element.dataset.skillName
332+
if (!agent || !repo || !skillName) return
333+
334+
const search_pattern = `#Skill:${agent}:${repo}:${skillName}`
335+
const start_index = props.value.indexOf(search_pattern)
336+
313337
if (start_index !== -1) {
314338
apply_keyword_deletion(start_index, start_index + search_pattern.length)
315339
}
@@ -658,6 +682,22 @@ export const use_handlers = (
658682
return false
659683
}
660684

685+
const handle_skill_keyword_deletion = (raw_pos: number): boolean => {
686+
const text_before_cursor = props.value.substring(0, raw_pos)
687+
const match = text_before_cursor.match(/#Skill:[^:]+:[^:]+:[^\s]+$/)
688+
689+
if (match) {
690+
const start_of_match = raw_pos - match[0].length
691+
const new_value =
692+
props.value.substring(0, start_of_match) +
693+
props.value.substring(raw_pos)
694+
const new_raw_cursor_pos = start_of_match
695+
update_value(new_value, new_raw_cursor_pos)
696+
return true
697+
}
698+
return false
699+
}
700+
661701
const handle_pasted_lines_keyword_deletion = (raw_pos: number): boolean => {
662702
const text_before_cursor = props.value.substring(0, raw_pos)
663703

@@ -718,6 +758,10 @@ export const use_handlers = (
718758
return handle_commit_keyword_deletion(raw_pos, context_file_paths)
719759
}
720760

761+
if (el.dataset.type == 'skill-keyword') {
762+
return handle_skill_keyword_deletion(raw_pos)
763+
}
764+
721765
if (el.dataset.type == 'pasted-lines-keyword') {
722766
return handle_pasted_lines_keyword_deletion(raw_pos)
723767
}
@@ -760,7 +804,8 @@ export const use_handlers = (
760804
parent.dataset.type == 'saved-context-keyword' ||
761805
parent.dataset.type == 'commit-keyword' ||
762806
parent.dataset.type == 'contextatcommit-keyword' ||
763-
parent.dataset.type == 'pasted-lines-keyword'
807+
parent.dataset.type == 'pasted-lines-keyword' ||
808+
parent.dataset.type == 'skill-keyword'
764809
) {
765810
const rangeAfter = document.createRange()
766811
rangeAfter.selectNodeContents(parent)
@@ -786,7 +831,8 @@ export const use_handlers = (
786831
el.dataset.type == 'saved-context-keyword' ||
787832
el.dataset.type == 'commit-keyword' ||
788833
el.dataset.type == 'contextatcommit-keyword' ||
789-
el.dataset.type == 'pasted-lines-keyword'
834+
el.dataset.type == 'pasted-lines-keyword' ||
835+
el.dataset.type == 'skill-keyword'
790836
) {
791837
const display_pos = get_caret_position_from_div(input_ref.current)
792838
if (handle_keyword_deletion_by_backspace(el, display_pos)) {

‎packages/ui/src/components/editor/panel/PromptField/utils/get-highlighted-text.tsx‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ export const get_highlighted_text = (params: {
5959
const fragment_regex_part =
6060
'<fragment path="[^"]+"(?: [^>]+)?>\\n[\\s\\S]*?\\n<\\/fragment>'
6161

62+
const skill_regex_part = '#Skill:[^:]+:[^:]+:[^\\s]+'
63+
6264
const regex = new RegExp(
63-
`(${fragment_regex_part}|#Selection|#Changes:[^\\s,;:!?]+|${saved_context_regex_part}|${commit_regex_part})`,
65+
`(${fragment_regex_part}|#Selection|#Changes:[^\\s,;:!?]+|${saved_context_regex_part}|${commit_regex_part}|${skill_regex_part})`,
6466
'g'
6567
)
6668
const parts = params.text.split(regex)
@@ -168,6 +170,27 @@ export const get_highlighted_text = (params: {
168170
}" data-role="keyword-text">${escape_html(short_hash)}</span></span>`
169171
}
170172

173+
const skill_match = part.match(/^#Skill:([^:]+):(.+):([^\s:]+)$/)
174+
if (part && skill_match) {
175+
const agent = skill_match[1]
176+
const repo = skill_match[2]
177+
const skill_name = skill_match[3]
178+
return `<span class="${cn(
179+
styles['keyword'],
180+
styles['keyword--skill']
181+
)}" data-type="skill-keyword" data-agent="${escape_html(
182+
agent
183+
)}" data-repo="${escape_html(repo)}" data-skill-name="${escape_html(
184+
skill_name
185+
)}" title="Skill: ${escape_html(skill_name)} (${escape_html(
186+
agent
187+
)})"><span class="${
188+
styles['keyword__icon']
189+
}" data-role="keyword-icon"></span><span class="${
190+
styles['keyword__text']
191+
}" data-role="keyword-text">${escape_html(skill_name)}</span></span>`
192+
}
193+
171194
return process_text_part_for_files(part, params.context_file_paths)
172195
})
173196
.join('')

‎packages/ui/src/components/editor/panel/PromptField/utils/position-mapping.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const map_display_pos_to_raw_pos = (
88
let last_raw_index = 0
99

1010
const regex =
11-
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"(?:\\.|[^"\\])*")|(<fragment path="[^"]+"(?: [^>]+)?>\n([\s\S]*?)\n<\/fragment>)/g
11+
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"(?:\\.|[^"\\])*")|(<fragment path="[^"]+"(?: [^>]+)?>\n([\s\S]*?)\n<\/fragment>)|(#Skill:[^:]+:[^:]+:[^\s]+)/g
1212
let match
1313

1414
while ((match = regex.exec(raw_text)) !== null) {
@@ -21,6 +21,7 @@ export const map_display_pos_to_raw_pos = (
2121
const commit_hash = match[7]
2222
const fragment_keyword = match[8]
2323
const fragment_content = match[9]
24+
const skill_keyword = match[10]
2425

2526
let is_replacement_match = false
2627
let display_match_length = 0
@@ -48,6 +49,11 @@ export const map_display_pos_to_raw_pos = (
4849
const lines_text = line_count === 1 ? 'line' : 'lines'
4950
display_match_length = `Pasted ${line_count} ${lines_text}`.length
5051
is_replacement_match = true
52+
} else if (skill_keyword) {
53+
const parts = skill_keyword.split(':')
54+
const skill_name = parts[parts.length - 1]
55+
display_match_length = skill_name.length
56+
is_replacement_match = true
5157
}
5258

5359
if (!is_replacement_match) {
@@ -78,7 +84,6 @@ export const map_display_pos_to_raw_pos = (
7884
last_raw_index = regex.lastIndex
7985
}
8086

81-
// Cursor is in the text after all matches
8287
return raw_pos + (display_pos - current_display_pos)
8388
}
8489

@@ -92,7 +97,7 @@ export const map_raw_pos_to_display_pos = (
9297
let last_raw_index = 0
9398

9499
const regex =
95-
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"(?:\\.|[^"\\])*")|(<fragment path="[^"]+"(?: [^>]+)?>\n([\s\S]*?)\n<\/fragment>)/g
100+
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"(?:\\.|[^"\\])*")|(<fragment path="[^"]+"(?: [^>]+)?>\n([\s\S]*?)\n<\/fragment>)|(#Skill:[^:]+:[^:]+:[^\s]+)/g
96101
let match
97102

98103
while ((match = regex.exec(raw_text)) !== null) {
@@ -105,6 +110,7 @@ export const map_raw_pos_to_display_pos = (
105110
const commit_hash = match[7]
106111
const fragment_keyword = match[8]
107112
const fragment_content = match[9]
113+
const skill_keyword = match[10]
108114

109115
let is_replacement_match = false
110116
let display_match_length = 0
@@ -132,6 +138,11 @@ export const map_raw_pos_to_display_pos = (
132138
const lines_text = line_count === 1 ? 'line' : 'lines'
133139
display_match_length = `Pasted ${line_count} ${lines_text}`.length
134140
is_replacement_match = true
141+
} else if (skill_keyword) {
142+
const parts = skill_keyword.split(':')
143+
const skill_name = parts[parts.length - 1]
144+
display_match_length = skill_name.length
145+
is_replacement_match = true
135146
}
136147

137148
if (!is_replacement_match) {

‎packages/vscode/src/views/panel/backend/message-handlers/handle-code-completion.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
replace_context_at_commit_symbol
2626
} from '../utils/replace-git-symbols'
2727
import { replace_saved_context_placeholder } from '@/utils/replace-saved-context-placeholder'
28+
import { replace_skill_symbol } from '../utils/replace-skill-symbol'
2829

2930
const get_code_completion_config = async (
3031
api_providers_manager: ModelProvidersManager,
@@ -388,6 +389,12 @@ export const handle_code_completion = async (
388389
})
389390
}
390391

392+
if (processed_completion_instructions.includes('#Skill:')) {
393+
processed_completion_instructions = await replace_skill_symbol({
394+
instruction: processed_completion_instructions
395+
})
396+
}
397+
391398
const files_collector = new FilesCollector(
392399
panel_provider.workspace_provider,
393400
panel_provider.open_editors_provider

‎packages/vscode/src/views/panel/backend/message-handlers/handle-copy-prompt.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
replace_context_at_commit_symbol
99
} from '@/views/panel/backend/utils/replace-git-symbols'
1010
import { replace_saved_context_placeholder } from '@/utils/replace-saved-context-placeholder'
11+
import { replace_skill_symbol } from '@/views/panel/backend/utils/replace-skill-symbol'
1112
import {
1213
code_completion_instructions_for_panel,
1314
prune_context_instructions_prefix,
@@ -125,6 +126,12 @@ export const handle_copy_prompt = async (params: {
125126
})
126127
}
127128

129+
if (processed_completion_instructions.includes('#Skill:')) {
130+
processed_completion_instructions = await replace_skill_symbol({
131+
instruction: processed_completion_instructions
132+
})
133+
}
134+
128135
const missing_text_tag = processed_completion_instructions
129136
? `<missing_text>${processed_completion_instructions}</missing_text>`
130137
: '<missing_text>'
@@ -175,6 +182,12 @@ export const handle_copy_prompt = async (params: {
175182
})
176183
}
177184

185+
if (processed_instructions.includes('#Skill:')) {
186+
processed_instructions = await replace_skill_symbol({
187+
instruction: processed_instructions
188+
})
189+
}
190+
178191
let system_instructions_xml = ''
179192

180193
if (params.panel_provider.web_prompt_type == 'edit-context') {

‎packages/vscode/src/views/panel/backend/message-handlers/handle-edit-context.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { replace_saved_context_placeholder } from '@/utils/replace-saved-context-placeholder'
2424
import { replace_selection_placeholder } from '@/views/panel/backend/utils/replace-selection-symbol'
2525
import { PanelProvider } from '@/views/panel/backend/panel-provider'
26+
import { replace_skill_symbol } from '@/views/panel/backend/utils/replace-skill-symbol'
2627
import { apply_reasoning_effort } from '@/utils/apply-reasoning-effort'
2728
import { EditContextMessage } from '@/views/panel/types/messages'
2829
import { dictionary } from '@shared/constants/dictionary'
@@ -337,6 +338,12 @@ export const handle_edit_context = async (
337338
})
338339
}
339340

341+
if (processed_instructions.includes('#Skill:')) {
342+
processed_instructions = await replace_skill_symbol({
343+
instruction: processed_instructions
344+
})
345+
}
346+
340347
const is_prune_context = panel_provider.api_prompt_type == 'prune-context'
341348
const collected_files = await files_collector.collect_files({
342349
compact: is_prune_context

0 commit comments

Comments
 (0)